diff --git a/example/HackBook/index.html b/example/HackBook/index.html index 767ada7c9..81a07f46c 100644 --- a/example/HackBook/index.html +++ b/example/HackBook/index.html @@ -303,7 +303,7 @@

Like button

- + diff --git a/install b/install deleted file mode 100755 index 38f672d7f..000000000 --- a/install +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env ruby - -def replace_in_file(filepath, regexp, *args, &block) - content = File.read(filepath).gsub(regexp, *args, &block) - File.open(filepath, 'wb') { |file| file.write(content) } -end - -file = File.expand_path(ARGV[0]) -platform = ( File.extension(file) == ".plist" ? "ios" : "android" ) - -if (platform == "ios") - replace_in_file(file, /\s*com.phonegap.facebook.Connect<\/key>\n/mi) do |match| - "" - end - replace_in_file(file, /\s*FacebookConnectPlugin<\/string>\n/mi) do |match| - "" - end - replace_in_file(file, /Plugins<\/key>\n\s*/mi) do |match| - "Plugins\n\t\n\t\tcom.phonegap.facebook.Connect\n\t\tFacebookConnectPlugin" - end -elsif (platform == "android") - replace_in_file(file, /\n/mi) do |match| - "" - end - replace_in_file(file, /\s*<\/plugins>/mi) do |match| - "\t\n" - end -end \ No newline at end of file diff --git a/install.bat b/install.bat deleted file mode 100644 index ad3992dce..000000000 --- a/install.bat +++ /dev/null @@ -1 +0,0 @@ -cscript install.cscript diff --git a/install.cscript b/install.cscript deleted file mode 100644 index 8bff1142b..000000000 --- a/install.cscript +++ /dev/null @@ -1,13 +0,0 @@ -function replaceInFile(filename, regexp, replacement) { - var fso = WScript.CreateObject("Scripting.FileSystemObject"); - var s = fso.OpenTextFile(filename, 1, true).ReadAll(); - s = s.replace(regexp, replacement); - var f = fso.OpenTextFile(filename, 2, true); - f.Write(s); - f.Close(); -} - -file = File.expand_path(ARGV[0]) - -replaceInFile(file, /\n/gm, ""); -replaceInFile(file, /\s*<\/plugins>/gm, "\t\n"); \ No newline at end of file diff --git a/install.js b/install.js deleted file mode 100644 index c6d468297..000000000 --- a/install.js +++ /dev/null @@ -1,66 +0,0 @@ -var fs = require('fs'), - util = require('util'), - exec = require('child_process').exec, - shell = function(command, cb) { exec(command, function(error, stdout, stderr) { - if (error !== null) { - console.log('ERROR!' + error); - util.puts(stderr); - } else { - util.puts(stdout); - } - if (cb) cb(); - }); }; - -var appDir = process.argv[2], - platform = process.argv[3].toLowerCase(); - -// Normalize slash -if (appDir[appDir.length-1] != '/') { - appDir = appDir + '/'; -} - -if (platform == 'android') { - // Figure out the package for the generated app. - exec('find ' + appDir + 'src -name "*.java"', function(e, o, err) { - var javaFile = o.replace(/\n/g, ''); - var contents = fs.readFileSync(javaFile).toString(); - var pkg = contents.match(/package\s(.*);/)[1]; - // Copy facebook-android-sdk res into app dir - // TODO: compile/jar this up instead of doing this hacky BS - shell("cp -rf lib/facebook-android-sdk/facebook/src " + appDir, function() { - var dialogFile = appDir + 'src/com/facebook/android/FbDialog.java'; - var dialogContents = fs.readFileSync(dialogFile).toString(); - dialogContents = dialogContents.replace(/public class/gi, 'import ' + pkg + '.*;\npublic class'); // HACK: to get around android.R package resolution issues - fs.writeFileSync(dialogFile, dialogContents); - }); - }); - - // Add connect plugin to plugins.xml - var pluginsFile = appDir + 'res/xml/plugins.xml'; - var pluginsXml = fs.readFileSync(pluginsFile).toString(); - pluginsXml = pluginsXml.replace(/<\/plugins>/gi,''); - fs.writeFileSync(pluginsFile, pluginsXml); - - // Generate and patch the facebook-js, then copy it into the - // application dir. - //shell("rm lib/facebook_js_sdk.js*", function() { - // shell("cd lib/facebook-js-sdk && php all.js.php >> ../facebook_js_sdk.js && cd .. && patch < facebook-js-patch && cp facebook_js_sdk.js " + appDir + "assets/www"); - //}); - - // Copy facebook-android-sdk res into app dir - shell("cp -rf lib/facebook-android-sdk/facebook/res " + appDir); - - // Copy native ConnectPlugin source into app dir - shell("cp -r native/android/ " + appDir); - - // Copy ConnectPlugin JS into app dir - shell("cp www/pg-plugin-fb-connect.js " + appDir + "assets/www"); - - // Copy example index in. - shell("cp example/www/index.html " + appDir + "assets/www"); - - // Remind user to edit AndroidManifest.xml with their App Secret. - console.log('In your app please make sure to properly include your Facebook application ID when you call "FB.init"!'); -} else if (platform == 'ios') { - console.log('Sorry dawg, not yet yo! Follow the manual iOS installation instructions in the README.'); -} diff --git a/lib/facebook-android-sdk b/lib/facebook-android-sdk deleted file mode 160000 index 3fe851097..000000000 --- a/lib/facebook-android-sdk +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3fe851097878e4800824792fc1acada300fe0802 diff --git a/lib/facebook-js-patch b/lib/facebook-js-patch deleted file mode 100644 index 4de3d2818..000000000 --- a/lib/facebook-js-patch +++ /dev/null @@ -1,591 +0,0 @@ -diff --git facebook_js_sdk.js facebook_js_sdk.js -index a4d3007..a62fb82 100644 ---- facebook_js_sdk.js -+++ facebook_js_sdk.js -@@ -1,542 +1,3 @@ --/** -- * This is the stock JSON2 implementation from www.json.org. -- * -- * Modifications include: -- * 1/ Removal of jslint settings -- * -- * @provides fb.thirdparty.json2 -- */ -- --/* -- http://www.JSON.org/json2.js -- 2009-09-29 -- -- Public Domain. -- -- NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. -- -- See http://www.JSON.org/js.html -- -- This file creates a global JSON object containing two methods: stringify -- and parse. -- -- JSON.stringify(value, replacer, space) -- value any JavaScript value, usually an object or array. -- -- replacer an optional parameter that determines how object -- values are stringified for objects. It can be a -- function or an array of strings. -- -- space an optional parameter that specifies the indentation -- of nested structures. If it is omitted, the text will -- be packed without extra whitespace. If it is a number, -- it will specify the number of spaces to indent at each -- level. If it is a string (such as '\t' or ' '), -- it contains the characters used to indent at each level. -- -- This method produces a JSON text from a JavaScript value. -- -- When an object value is found, if the object contains a toJSON -- method, its toJSON method will be called and the result will be -- stringified. A toJSON method does not serialize: it returns the -- value represented by the name/value pair that should be serialized, -- or undefined if nothing should be serialized. The toJSON method -- will be passed the key associated with the value, and this will be -- bound to the value -- -- For example, this would serialize Dates as ISO strings. -- -- Date.prototype.toJSON = function (key) { -- function f(n) { -- // Format integers to have at least two digits. -- return n < 10 ? '0' + n : n; -- } -- -- return this.getUTCFullYear() + '-' + -- f(this.getUTCMonth() + 1) + '-' + -- f(this.getUTCDate()) + 'T' + -- f(this.getUTCHours()) + ':' + -- f(this.getUTCMinutes()) + ':' + -- f(this.getUTCSeconds()) + 'Z'; -- }; -- -- You can provide an optional replacer method. It will be passed the -- key and value of each member, with this bound to the containing -- object. The value that is returned from your method will be -- serialized. If your method returns undefined, then the member will -- be excluded from the serialization. -- -- If the replacer parameter is an array of strings, then it will be -- used to select the members to be serialized. It filters the results -- such that only members with keys listed in the replacer array are -- stringified. -- -- Values that do not have JSON representations, such as undefined or -- functions, will not be serialized. Such values in objects will be -- dropped; in arrays they will be replaced with null. You can use -- a replacer function to replace those with JSON values. -- JSON.stringify(undefined) returns undefined. -- -- The optional space parameter produces a stringification of the -- value that is filled with line breaks and indentation to make it -- easier to read. -- -- If the space parameter is a non-empty string, then that string will -- be used for indentation. If the space parameter is a number, then -- the indentation will be that many spaces. -- -- Example: -- -- text = JSON.stringify(['e', {pluribus: 'unum'}]); -- // text is '["e",{"pluribus":"unum"}]' -- -- -- text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); -- // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' -- -- text = JSON.stringify([new Date()], function (key, value) { -- return this[key] instanceof Date ? -- 'Date(' + this[key] + ')' : value; -- }); -- // text is '["Date(---current time---)"]' -- -- -- JSON.parse(text, reviver) -- This method parses a JSON text to produce an object or array. -- It can throw a SyntaxError exception. -- -- The optional reviver parameter is a function that can filter and -- transform the results. It receives each of the keys and values, -- and its return value is used instead of the original value. -- If it returns what it received, then the structure is not modified. -- If it returns undefined then the member is deleted. -- -- Example: -- -- // Parse the text. Values that look like ISO date strings will -- // be converted to Date objects. -- -- myData = JSON.parse(text, function (key, value) { -- var a; -- if (typeof value === 'string') { -- a = --/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); -- if (a) { -- return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], -- +a[5], +a[6])); -- } -- } -- return value; -- }); -- -- myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { -- var d; -- if (typeof value === 'string' && -- value.slice(0, 5) === 'Date(' && -- value.slice(-1) === ')') { -- d = new Date(value.slice(5, -1)); -- if (d) { -- return d; -- } -- } -- return value; -- }); -- -- -- This is a reference implementation. You are free to copy, modify, or -- redistribute. -- -- This code should be minified before deployment. -- See http://javascript.crockford.com/jsmin.html -- -- USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO -- NOT CONTROL. --*/ -- -- --// Create a JSON object only if one does not already exist. We create the --// methods in a closure to avoid creating global variables. -- --if (!this.JSON) { -- this.JSON = {}; --} -- --(function () { -- -- function f(n) { -- // Format integers to have at least two digits. -- return n < 10 ? '0' + n : n; -- } -- -- if (typeof Date.prototype.toJSON !== 'function') { -- -- Date.prototype.toJSON = function (key) { -- -- return isFinite(this.valueOf()) ? -- this.getUTCFullYear() + '-' + -- f(this.getUTCMonth() + 1) + '-' + -- f(this.getUTCDate()) + 'T' + -- f(this.getUTCHours()) + ':' + -- f(this.getUTCMinutes()) + ':' + -- f(this.getUTCSeconds()) + 'Z' : null; -- }; -- -- String.prototype.toJSON = -- Number.prototype.toJSON = -- Boolean.prototype.toJSON = function (key) { -- return this.valueOf(); -- }; -- } -- -- var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, -- escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, -- gap, -- indent, -- meta = { // table of character substitutions -- '\b': '\\b', -- '\t': '\\t', -- '\n': '\\n', -- '\f': '\\f', -- '\r': '\\r', -- '"' : '\\"', -- '\\': '\\\\' -- }, -- rep; -- -- -- function quote(string) { -- --// If the string contains no control characters, no quote characters, and no --// backslash characters, then we can safely slap some quotes around it. --// Otherwise we must also replace the offending characters with safe escape --// sequences. -- -- escapable.lastIndex = 0; -- return escapable.test(string) ? -- '"' + string.replace(escapable, function (a) { -- var c = meta[a]; -- return typeof c === 'string' ? c : -- '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); -- }) + '"' : -- '"' + string + '"'; -- } -- -- -- function str(key, holder) { -- --// Produce a string from holder[key]. -- -- var i, // The loop counter. -- k, // The member key. -- v, // The member value. -- length, -- mind = gap, -- partial, -- value = holder[key]; -- --// If the value has a toJSON method, call it to obtain a replacement value. -- -- if (value && typeof value === 'object' && -- typeof value.toJSON === 'function') { -- value = value.toJSON(key); -- } -- --// If we were called with a replacer function, then call the replacer to --// obtain a replacement value. -- -- if (typeof rep === 'function') { -- value = rep.call(holder, key, value); -- } -- --// What happens next depends on the value's type. -- -- switch (typeof value) { -- case 'string': -- return quote(value); -- -- case 'number': -- --// JSON numbers must be finite. Encode non-finite numbers as null. -- -- return isFinite(value) ? String(value) : 'null'; -- -- case 'boolean': -- case 'null': -- --// If the value is a boolean or null, convert it to a string. Note: --// typeof null does not produce 'null'. The case is included here in --// the remote chance that this gets fixed someday. -- -- return String(value); -- --// If the type is 'object', we might be dealing with an object or an array or --// null. -- -- case 'object': -- --// Due to a specification blunder in ECMAScript, typeof null is 'object', --// so watch out for that case. -- -- if (!value) { -- return 'null'; -- } -- --// Make an array to hold the partial results of stringifying this object value. -- -- gap += indent; -- partial = []; -- --// Is the value an array? -- -- if (Object.prototype.toString.apply(value) === '[object Array]') { -- --// The value is an array. Stringify every element. Use null as a placeholder --// for non-JSON values. -- -- length = value.length; -- for (i = 0; i < length; i += 1) { -- partial[i] = str(i, value) || 'null'; -- } -- --// Join all of the elements together, separated with commas, and wrap them in --// brackets. -- -- v = partial.length === 0 ? '[]' : -- gap ? '[\n' + gap + -- partial.join(',\n' + gap) + '\n' + -- mind + ']' : -- '[' + partial.join(',') + ']'; -- gap = mind; -- return v; -- } -- --// If the replacer is an array, use it to select the members to be stringified. -- -- if (rep && typeof rep === 'object') { -- length = rep.length; -- for (i = 0; i < length; i += 1) { -- k = rep[i]; -- if (typeof k === 'string') { -- v = str(k, value); -- if (v) { -- partial.push(quote(k) + (gap ? ': ' : ':') + v); -- } -- } -- } -- } else { -- --// Otherwise, iterate through all of the keys in the object. -- -- for (k in value) { -- if (Object.hasOwnProperty.call(value, k)) { -- v = str(k, value); -- if (v) { -- partial.push(quote(k) + (gap ? ': ' : ':') + v); -- } -- } -- } -- } -- --// Join all of the member texts together, separated with commas, --// and wrap them in braces. -- -- v = partial.length === 0 ? '{}' : -- gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + -- mind + '}' : '{' + partial.join(',') + '}'; -- gap = mind; -- return v; -- } -- } -- --// If the JSON object does not yet have a stringify method, give it one. -- -- if (typeof JSON.stringify !== 'function') { -- JSON.stringify = function (value, replacer, space) { -- --// The stringify method takes a value and an optional replacer, and an optional --// space parameter, and returns a JSON text. The replacer can be a function --// that can replace values, or an array of strings that will select the keys. --// A default replacer method can be provided. Use of the space parameter can --// produce text that is more easily readable. -- -- var i; -- gap = ''; -- indent = ''; -- --// If the space parameter is a number, make an indent string containing that --// many spaces. -- -- if (typeof space === 'number') { -- for (i = 0; i < space; i += 1) { -- indent += ' '; -- } -- --// If the space parameter is a string, it will be used as the indent string. -- -- } else if (typeof space === 'string') { -- indent = space; -- } -- --// If there is a replacer, it must be a function or an array. --// Otherwise, throw an error. -- -- rep = replacer; -- if (replacer && typeof replacer !== 'function' && -- (typeof replacer !== 'object' || -- typeof replacer.length !== 'number')) { -- throw new Error('JSON.stringify'); -- } -- --// Make a fake root object containing our value under the key of ''. --// Return the result of stringifying the value. -- -- return str('', {'': value}); -- }; -- } -- -- --// If the JSON object does not yet have a parse method, give it one. -- -- if (typeof JSON.parse !== 'function') { -- JSON.parse = function (text, reviver) { -- --// The parse method takes a text and an optional reviver function, and returns --// a JavaScript value if the text is a valid JSON text. -- -- var j; -- -- function walk(holder, key) { -- --// The walk method is used to recursively walk the resulting structure so --// that modifications can be made. -- -- var k, v, value = holder[key]; -- if (value && typeof value === 'object') { -- for (k in value) { -- if (Object.hasOwnProperty.call(value, k)) { -- v = walk(value, k); -- if (v !== undefined) { -- value[k] = v; -- } else { -- delete value[k]; -- } -- } -- } -- } -- return reviver.call(holder, key, value); -- } -- -- --// Parsing happens in four stages. In the first stage, we replace certain --// Unicode characters with escape sequences. JavaScript handles many characters --// incorrectly, either silently deleting them, or treating them as line endings. -- -- cx.lastIndex = 0; -- if (cx.test(text)) { -- text = text.replace(cx, function (a) { -- return '\\u' + -- ('0000' + a.charCodeAt(0).toString(16)).slice(-4); -- }); -- } -- --// In the second stage, we run the text against regular expressions that look --// for non-JSON patterns. We are especially concerned with '()' and 'new' --// because they can cause invocation, and '=' because it can cause mutation. --// But just to be safe, we want to reject all unexpected forms. -- --// We split the second stage into 4 regexp operations in order to work around --// crippling inefficiencies in IE's and Safari's regexp engines. First we --// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we --// replace all simple value tokens with ']' characters. Third, we delete all --// open brackets that follow a colon or comma or that begin the text. Finally, --// we look to see that the remaining characters are only whitespace or ']' or --// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. -- -- if (/^[\],:{}\s]*$/. --test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). --replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). --replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { -- --// In the third stage we use the eval function to compile the text into a --// JavaScript structure. The '{' operator is subject to a syntactic ambiguity --// in JavaScript: it can begin a block or an object literal. We wrap the text --// in parens to eliminate the ambiguity. -- -- j = eval('(' + text + ')'); -- --// In the optional fourth stage, we recursively walk the new structure, passing --// each name/value pair to a reviver function for possible transformation. -- -- return typeof reviver === 'function' ? -- walk({'': j}, '') : j; -- } -- --// If the text is not JSON parseable, then a SyntaxError is thrown. -- -- throw new SyntaxError('JSON.parse'); -- }; -- } --}()); --/** -- * Copyright Facebook Inc. -- * -- * Licensed under the Apache License, Version 2.0 (the "License"); -- * you may not use this file except in compliance with the License. -- * You may obtain a copy of the License at -- * -- * http://www.apache.org/licenses/LICENSE-2.0 -- * -- * Unless required by applicable law or agreed to in writing, software -- * distributed under the License is distributed on an "AS IS" BASIS, -- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- * See the License for the specific language governing permissions and -- * limitations under the License. -- * -- * -- * -- * @provides fb.prelude -- */ -- --/** -- * Prelude. -- * -- * Namespaces are one honking great idea -- let's do more of those! -- * -- Tim Peters -- * -- * The Prelude is what keeps us from being messy. In order to co-exist with -- * arbitary environments, we need to control our footprint. The one and only -- * rule to follow here is that we need to limit the globals we introduce. The -- * only global we should every have is ``FB``. This is exactly what the prelude -- * enables us to do. -- * -- * The main method to take away from this file is `FB.copy()`_. As the name -- * suggests it copies things. Its powerful -- but to get started you only need -- * to know that this is what you use when you are augmenting the FB object. For -- * example, this is skeleton for how ``FB.Event`` is defined:: -- * -- * FB.provide('Event', { -- * subscribe: function() { ... }, -- * unsubscribe: function() { ... }, -- * fire: function() { ... } -- * }); -- * -- * This is similar to saying:: -- * -- * FB.Event = { -- * subscribe: function() { ... }, -- * unsubscribe: function() { ... }, -- * fire: function() { ... } -- * }; -- * -- * Except it does some housekeeping, prevents redefinition by default and other -- * goodness. -- * -- * .. _FB.copy(): #method_FB.copy -- * -- * @class FB -- * @static -- * @access private -- */ - if (!window.FB) { - FB = { - // use the init method to set these values correctly -@@ -3111,6 +2572,12 @@ FB.provide('', { - - FB._apiKey = options.appId || options.apiKey; - -+ // if nativeInterface is specified then fire off the native initialization as well. -+ FB._nativeInterface = options.nativeInterface; -+ if (FB._nativeInterface) { -+ FB._nativeInterface.init(FB._apiKey, function(e) {alert('PhoneGap Facebook Connect plugin fail on init!');}); -+ } -+ - // disable logging if told to do so, but only if the url doesnt have the - // token to turn it on. this allows for easier debugging of third party - // sites even if logging has been turned off. -@@ -3637,6 +3104,23 @@ FB.provide('', { - return; - } - -+ // If the nativeInterface arg is specified then call out to the nativeInterface -+ // which uses the native app rather than using the iframe / popup web -+ if (FB._nativeInterface) { -+ switch (params.method) { -+ case 'auth.login': -+ FB._nativeInterface.login(params, cb, function(e) {alert('PhoneGap Facebook Connect plugin fail on login!' + e);}); -+ break; -+ case 'auth.logout': -+ FB._nativeInterface.logout(cb, function(e) {alert('PhoneGap Facebook Connect plugin fail on logout!');}); -+ break; -+ case 'auth.status': -+ FB._nativeInterface.getLoginStatus(cb, function(e) {alert('PhoneGap Facebook Connect plugin fail on auth.status!');}); -+ break; -+ } -+ return; -+ } -+ - var call = FB.UIServer.prepareCall(params, cb); - if (!call) { // aborted - return; -@@ -8434,4 +7918,4 @@ FB.provide('FB.Intl', { - 'cs:add-profile-tab-on-facebook': 'Add Profile Tab on Facebook' - } - }); --FB.Dom.addCssRules("\/**\n * Copyright Facebook Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n *\n * Styles for the client side Dialogs.\n *\n * @author naitik\n * @provides fb.css.dialog\n * @requires fb.css.base fb.dom\n *\/\n\n.fb_dialog {\n position: absolute;\n top: -10000px;\n z-index: 10001;\n}\n.fb_dialog_advanced {\n background: rgba(82, 82, 82, 0.7);\n padding: 10px;\n -moz-border-radius: 8px;\n -webkit-border-radius: 8px;\n}\n.fb_dialog_content {\n background: #ffffff;\n color: #333333;\n}\n.fb_dialog_close_icon {\n background: url(http:\/\/static.ak.fbcdn.net\/images\/fbconnect\/connect_icon_remove.gif) no-repeat scroll 3px 0 transparent;\n cursor: pointer;\n display: block;\n height: 16px;\n position: absolute;\n right: 19px;\n top: 18px;\n width: 14px;\n \/* this rule applies to all IE browsers only because using the \\9 hack *\/\n top: 10px\\9;\n right: 7px\\9;\n}\n.fb_dialog_close_icon:hover {\n background: url(http:\/\/static.ak.fbcdn.net\/images\/fbconnect\/connect_icon_remove.gif) no-repeat scroll -10px 0 transparent;\n}\n.fb_dialog_loader {\n background-color: #f2f2f2;\n border: 1px solid #606060;\n font-size: 24px;\n padding: 20px;\n}\n#fb_dialog_loader_close {\n background: url(http:\/\/static.ak.fbcdn.net\/images\/sidebar\/close-off.gif) no-repeat scroll left top transparent;\n cursor: pointer;\n display: -moz-inline-block;\n display: inline-block;\n height: 9px;\n margin-left: 20px;\n position: relative;\n vertical-align: middle;\n width: 9px;\n}\n#fb_dialog_loader_close:hover {\n background-image: url(http:\/\/static.ak.fbcdn.net\/images\/gigaboxx\/clear_search.png);\n}\n\n\n\/**\n * Rounded corners and borders with alpha transparency for older browsers.\n *\/\n.fb_dialog_top_left,\n.fb_dialog_top_right,\n.fb_dialog_bottom_left,\n.fb_dialog_bottom_right {\n height: 10px;\n width: 10px;\n overflow: hidden;\n position: absolute;\n}\n\/* @noflip *\/\n.fb_dialog_top_left {\n background: url(http:\/\/static.ak.fbcdn.net\/imgs\/pop-dialog-sprite.png) no-repeat 0 0;\n left: -10px;\n top: -10px;\n}\n\/* @noflip *\/\n.fb_dialog_top_right {\n background: url(http:\/\/static.ak.fbcdn.net\/imgs\/pop-dialog-sprite.png) no-repeat 0 -10px;\n right: -10px;\n top: -10px;\n}\n\/* @noflip *\/\n.fb_dialog_bottom_left {\n background: url(http:\/\/static.ak.fbcdn.net\/imgs\/pop-dialog-sprite.png) no-repeat 0 -20px;\n bottom: -10px;\n left: -10px;\n}\n\/* @noflip *\/\n.fb_dialog_bottom_right {\n background: url(http:\/\/static.ak.fbcdn.net\/imgs\/pop-dialog-sprite.png) no-repeat 0 -30px;\n right: -10px;\n bottom: -10px;\n}\n.fb_dialog_vert_left,\n.fb_dialog_vert_right,\n.fb_dialog_horiz_top,\n.fb_dialog_horiz_bottom {\n position: absolute;\n background: #525252;\n filter: alpha(opacity=70);\n opacity: .7;\n}\n.fb_dialog_vert_left,\n.fb_dialog_vert_right {\n width: 10px;\n height: 100%;\n}\n.fb_dialog_vert_left {\n margin-left: -10px;\n}\n.fb_dialog_vert_right {\n right: 0;\n margin-right: -10px;\n}\n.fb_dialog_horiz_top,\n.fb_dialog_horiz_bottom {\n width: 100%;\n height: 10px;\n}\n.fb_dialog_horiz_top {\n margin-top: -10px;\n}\n.fb_dialog_horiz_bottom {\n bottom: 0;\n margin-bottom: -10px;\n}\n\n\/* dialogs used for iframe need this to prevent potential whitespace from\n * showing because iframes are inline elements and not block level elements. *\/\n.fb_dialog_iframe {\n line-height: 0;\n}\n\/**\n * Copyright Facebook Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @author blaise\n * @provides fb.css.button\n * @layer xfbml\n *\/\n\n\/**\n * simple buttons are very completely separate from the pretty buttons below.\n *\/\n.fb_button_simple,\n.fb_button_simple_rtl {\n background-image: url(http:\/\/static.ak.fbcdn.net\/images\/connect_favicon.png);\n background-repeat: no-repeat;\n cursor: pointer;\n outline: none;\n text-decoration: none;\n}\n.fb_button_simple_rtl {\n background-position: right 0px;\n}\n\n.fb_button_simple .fb_button_text {\n margin: 0 0 0px 20px;\n padding-bottom: 1px;\n}\n\n.fb_button_simple_rtl .fb_button_text {\n margin: 0px 10px 0px 0px;\n}\n\na.fb_button_simple:hover .fb_button_text,\na.fb_button_simple_rtl:hover .fb_button_text,\n.fb_button_simple:hover .fb_button_text,\n.fb_button_simple_rtl:hover .fb_button_text {\n text-decoration: underline;\n}\n\n\n\/**\n * these are the new style pretty buttons with various size options\n *\/\n.fb_button,\n.fb_button_rtl {\n background: #29447e url(http:\/\/static.ak.fbcdn.net\/images\/connect_sprite.png);\n background-repeat: no-repeat;\n cursor: pointer;\n display: inline-block;\n padding: 0px 0px 0px 1px;\n text-decoration: none;\n outline: none;\n}\n\n.fb_button .fb_button_text,\n.fb_button_rtl .fb_button_text {\n background: #5f78ab url(http:\/\/static.ak.fbcdn.net\/images\/connect_sprite.png);\n border-top: solid 1px #879ac0;\n border-bottom: solid 1px #1a356e;\n color: white;\n display: block;\n font-family: \"lucida grande\",tahoma,verdana,arial,sans-serif;\n font-weight: bold;\n padding: 2px 6px 3px 6px;\n margin: 1px 1px 0px 21px;\n text-shadow: none;\n}\n\n\na.fb_button,\na.fb_button_rtl,\n.fb_button,\n.fb_button_rtl {\n text-decoration: none;\n}\n\na.fb_button:active .fb_button_text,\na.fb_button_rtl:active .fb_button_text,\n.fb_button:active .fb_button_text,\n.fb_button_rtl:active .fb_button_text {\n border-bottom: solid 1px #29447e;\n border-top: solid 1px #45619d;\n background: #4f6aa3;\n text-shadow: none;\n}\n\n\n.fb_button_xlarge,\n.fb_button_xlarge_rtl {\n background-position: left -60px;\n font-size: 24px;\n line-height: 30px;\n}\n.fb_button_xlarge .fb_button_text {\n padding: 3px 8px 3px 12px;\n margin-left: 38px;\n}\na.fb_button_xlarge:active {\n background-position: left -99px;\n}\n.fb_button_xlarge_rtl {\n background-position: right -268px;\n}\n.fb_button_xlarge_rtl .fb_button_text {\n padding: 3px 8px 3px 12px;\n margin-right: 39px;\n}\na.fb_button_xlarge_rtl:active {\n background-position: right -307px;\n}\n\n.fb_button_large,\n.fb_button_large_rtl {\n background-position: left -138px;\n font-size: 13px;\n line-height: 16px;\n}\n.fb_button_large .fb_button_text {\n margin-left: 24px;\n padding: 2px 6px 4px 6px;\n}\na.fb_button_large:active {\n background-position: left -163px;\n}\n.fb_button_large_rtl {\n background-position: right -346px;\n}\n.fb_button_large_rtl .fb_button_text {\n margin-right: 25px;\n}\na.fb_button_large_rtl:active {\n background-position: right -371px;\n}\n\n.fb_button_medium,\n.fb_button_medium_rtl {\n background-position: left -188px;\n font-size: 11px;\n line-height: 14px;\n}\na.fb_button_medium:active {\n background-position: left -210px;\n}\n\n.fb_button_medium_rtl {\n background-position: right -396px;\n}\n.fb_button_text_rtl,\n.fb_button_medium_rtl .fb_button_text {\n padding: 2px 6px 3px 6px;\n margin-right: 22px;\n}\na.fb_button_medium_rtl:active {\n background-position: right -418px;\n}\n\n.fb_button_small,\n.fb_button_small_rtl {\n background-position: left -232px;\n font-size: 10px;\n line-height: 10px;\n}\n.fb_button_small .fb_button_text {\n padding: 2px 6px 3px;\n margin-left: 17px;\n}\na.fb_button_small:active,\n.fb_button_small:active {\n background-position: left -250px;\n}\n\n.fb_button_small_rtl {\n background-position: right -440px;\n}\n.fb_button_small_rtl .fb_button_text {\n padding: 2px 6px;\n margin-right: 18px;\n}\na.fb_button_small_rtl:active {\n background-position: right -458px;\n}\n\/**\n * Copyright Facebook Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @author arunv\n * @provides fb.css.sharebutton\n * @layer xfbml\n * @requires fb.css.button\n *\/\n.fb_share_count_wrapper {\n position: relative;\n float: left;\n}\n\n.fb_share_count {\n background: #b0b9ec none repeat scroll 0 0;\n color: #333333;\n font-family: \"lucida grande\", tahoma, verdana, arial, sans-serif;\n text-align: center;\n}\n\n.fb_share_count_inner {\n background: #e8ebf2;\n display: block;\n}\n\n.fb_share_count_right {\n margin-left: -1px;\n display: inline-block;\n}\n\n.fb_share_count_right .fb_share_count_inner {\n border-top: solid 1px #e8ebf2;\n border-bottom: solid 1px #b0b9ec;\n margin: 1px 1px 0px 1px;\n font-size: 10px;\n line-height: 10px;\n padding: 2px 6px 3px;\n font-weight: bold;\n}\n\n.fb_share_count_top {\n display: block;\n letter-spacing: -1px;\n line-height: 34px;\n margin-bottom: 7px;\n font-size: 22px;\n border: solid 1px #b0b9ec;\n}\n\n.fb_share_count_nub_top {\n border: none;\n display: block;\n position: absolute;\n left: 7px;\n top: 35px;\n margin: 0;\n padding: 0;\n width: 6px;\n height: 7px;\n background-repeat: no-repeat;\n background-image: url(http:\/\/static.ak.fbcdn.net\/images\/sharepro\/sp_h_nub.png);\n}\n\n.fb_share_count_nub_right {\n border: none;\n display: inline-block;\n padding: 0;\n width: 5px;\n height: 10px;\n background-repeat: no-repeat;\n background-image: url(http:\/\/static.ak.fbcdn.net\/images\/sharepro\/sp_v_nub.png);\n vertical-align: top;\n background-position:right 5px;\n z-index: 10;\n left: 2px;\n margin: 0px 2px 0px 0px;\n position: relative;\n}\n\n.fb_share_no_count {\n display: none;\n}\n\n.fb_share_size_Small .fb_share_count_right .fb_share_count_inner {\n font-size: 10px;\n}\n\n.fb_share_size_Medium .fb_share_count_right .fb_share_count_inner {\n font-size: 11px;\n padding: 2px 6px 3px;\n letter-spacing: -1px;\n line-height: 14px;\n}\n\n.fb_share_size_Large .fb_share_count_right .fb_share_count_inner {\n font-size: 13px;\n line-height: 16px;\n padding: 2px 6px 4px;\n font-weight: normal;\n letter-spacing: -1px;\n}\n\/**\n * Copyright Facebook Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @author naitik\n * @provides fb.css.base\n *\/\n\n.fb_hidden {\n position: absolute;\n top: -10000px;\n z-index: 10001;\n}\n\n.fb_reset {\n background: none;\n border-spacing: 0;\n border: 0px;\n color: #000;\n cursor: auto;\n direction: ltr;\n font-family: \"lucida grande\", tahoma, verdana, arial, sans-serif;\n font-size: 11px;\n font-style: normal;\n font-variant: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n margin: 0;\n overflow: visible;\n padding: 0;\n text-align: left;\n text-decoration: none;\n text-indent: 0;\n text-shadow: none;\n text-transform: none;\n visibility: visible;\n white-space: normal;\n word-spacing: normal;\n}\n\n.fb_link img {\n border: none;\n}\n\/**\n * Copyright Facebook Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @author naitik\n * @provides fb.css.iframewidget\n * @layer xfbml\n *\/\n.fb_iframe_widget {\n position: relative;\n display: -moz-inline-block; \/* ff2 *\/\n display: inline-block;\n}\n.fb_iframe_widget iframe {\n \/* this is necessary for IE. without it, once hidden, it wont become visible\n * again *\/\n position: relative;\n \/* this is to remove the bottom margin appearing on the iframe widgets *\/\n vertical-align: text-bottom;\n}\n\n.fb_iframe_widget span {\n \/* this is necessary for IE as well. without it, the content of the iframe would be\n * totally off when resizing the parent window.\n * probably related to this bug http:\/\/friendlybit.com\/css\/ie6-resize-bug\/\n *\/\n position: relative;\n}\n\n.fb_hide_iframes iframe {\n position: relative;\n left: -10000px;\n}\n.fb_iframe_widget_loader {\n position: relative;\n display: inline-block;\n}\n.fb_iframe_widget_loader iframe {\n min-height: 32px;\n z-index: 2;\n zoom: 1;\n}\n.fb_iframe_widget_loader .FB_Loader {\n background: url(http:\/\/static.ak.fbcdn.net\/images\/loaders\/indicator_blue_large.gif) no-repeat;\n height: 32px;\n width: 32px;\n margin-left: -16px;\n position: absolute;\n left: 50%;\n z-index: 4;\n}\n", ["pkg"]) -\ No newline at end of file -+FB.Dom.addCssRules("\/**\n * Copyright Facebook Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n *\n * Styles for the client side Dialogs.\n *\n * @author naitik\n * @provides fb.css.dialog\n * @requires fb.css.base fb.dom\n *\/\n\n.fb_dialog {\n position: absolute;\n top: -10000px;\n z-index: 10001;\n}\n.fb_dialog_advanced {\n background: rgba(82, 82, 82, 0.7);\n padding: 10px;\n -moz-border-radius: 8px;\n -webkit-border-radius: 8px;\n}\n.fb_dialog_content {\n background: #ffffff;\n color: #333333;\n}\n.fb_dialog_close_icon {\n background: url(http:\/\/static.ak.fbcdn.net\/images\/fbconnect\/connect_icon_remove.gif) no-repeat scroll 3px 0 transparent;\n cursor: pointer;\n display: block;\n height: 16px;\n position: absolute;\n right: 19px;\n top: 18px;\n width: 14px;\n \/* this rule applies to all IE browsers only because using the \\9 hack *\/\n top: 10px\\9;\n right: 7px\\9;\n}\n.fb_dialog_close_icon:hover {\n background: url(http:\/\/static.ak.fbcdn.net\/images\/fbconnect\/connect_icon_remove.gif) no-repeat scroll -10px 0 transparent;\n}\n.fb_dialog_loader {\n background-color: #f2f2f2;\n border: 1px solid #606060;\n font-size: 24px;\n padding: 20px;\n}\n#fb_dialog_loader_close {\n background: url(http:\/\/static.ak.fbcdn.net\/images\/sidebar\/close-off.gif) no-repeat scroll left top transparent;\n cursor: pointer;\n display: -moz-inline-block;\n display: inline-block;\n height: 9px;\n margin-left: 20px;\n position: relative;\n vertical-align: middle;\n width: 9px;\n}\n#fb_dialog_loader_close:hover {\n background-image: url(http:\/\/static.ak.fbcdn.net\/images\/gigaboxx\/clear_search.png);\n}\n\n\n\/**\n * Rounded corners and borders with alpha transparency for older browsers.\n *\/\n.fb_dialog_top_left,\n.fb_dialog_top_right,\n.fb_dialog_bottom_left,\n.fb_dialog_bottom_right {\n height: 10px;\n width: 10px;\n overflow: hidden;\n position: absolute;\n}\n\/* @noflip *\/\n.fb_dialog_top_left {\n background: url(http:\/\/static.ak.fbcdn.net\/imgs\/pop-dialog-sprite.png) no-repeat 0 0;\n left: -10px;\n top: -10px;\n}\n\/* @noflip *\/\n.fb_dialog_top_right {\n background: url(http:\/\/static.ak.fbcdn.net\/imgs\/pop-dialog-sprite.png) no-repeat 0 -10px;\n right: -10px;\n top: -10px;\n}\n\/* @noflip *\/\n.fb_dialog_bottom_left {\n background: url(http:\/\/static.ak.fbcdn.net\/imgs\/pop-dialog-sprite.png) no-repeat 0 -20px;\n bottom: -10px;\n left: -10px;\n}\n\/* @noflip *\/\n.fb_dialog_bottom_right {\n background: url(http:\/\/static.ak.fbcdn.net\/imgs\/pop-dialog-sprite.png) no-repeat 0 -30px;\n right: -10px;\n bottom: -10px;\n}\n.fb_dialog_vert_left,\n.fb_dialog_vert_right,\n.fb_dialog_horiz_top,\n.fb_dialog_horiz_bottom {\n position: absolute;\n background: #525252;\n filter: alpha(opacity=70);\n opacity: .7;\n}\n.fb_dialog_vert_left,\n.fb_dialog_vert_right {\n width: 10px;\n height: 100%;\n}\n.fb_dialog_vert_left {\n margin-left: -10px;\n}\n.fb_dialog_vert_right {\n right: 0;\n margin-right: -10px;\n}\n.fb_dialog_horiz_top,\n.fb_dialog_horiz_bottom {\n width: 100%;\n height: 10px;\n}\n.fb_dialog_horiz_top {\n margin-top: -10px;\n}\n.fb_dialog_horiz_bottom {\n bottom: 0;\n margin-bottom: -10px;\n}\n\n\/* dialogs used for iframe need this to prevent potential whitespace from\n * showing because iframes are inline elements and not block level elements. *\/\n.fb_dialog_iframe {\n line-height: 0;\n}\n\/**\n * Copyright Facebook Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @author blaise\n * @provides fb.css.button\n * @layer xfbml\n *\/\n\n\/**\n * simple buttons are very completely separate from the pretty buttons below.\n *\/\n.fb_button_simple,\n.fb_button_simple_rtl {\n background-image: url(http:\/\/static.ak.fbcdn.net\/images\/connect_favicon.png);\n background-repeat: no-repeat;\n cursor: pointer;\n outline: none;\n text-decoration: none;\n}\n.fb_button_simple_rtl {\n background-position: right 0px;\n}\n\n.fb_button_simple .fb_button_text {\n margin: 0 0 0px 20px;\n padding-bottom: 1px;\n}\n\n.fb_button_simple_rtl .fb_button_text {\n margin: 0px 10px 0px 0px;\n}\n\na.fb_button_simple:hover .fb_button_text,\na.fb_button_simple_rtl:hover .fb_button_text,\n.fb_button_simple:hover .fb_button_text,\n.fb_button_simple_rtl:hover .fb_button_text {\n text-decoration: underline;\n}\n\n\n\/**\n * these are the new style pretty buttons with various size options\n *\/\n.fb_button,\n.fb_button_rtl {\n background: #29447e url(http:\/\/static.ak.fbcdn.net\/images\/connect_sprite.png);\n background-repeat: no-repeat;\n cursor: pointer;\n display: inline-block;\n padding: 0px 0px 0px 1px;\n text-decoration: none;\n outline: none;\n}\n\n.fb_button .fb_button_text,\n.fb_button_rtl .fb_button_text {\n background: #5f78ab url(http:\/\/static.ak.fbcdn.net\/images\/connect_sprite.png);\n border-top: solid 1px #879ac0;\n border-bottom: solid 1px #1a356e;\n color: white;\n display: block;\n font-family: \"lucida grande\",tahoma,verdana,arial,sans-serif;\n font-weight: bold;\n padding: 2px 6px 3px 6px;\n margin: 1px 1px 0px 21px;\n text-shadow: none;\n}\n\n\na.fb_button,\na.fb_button_rtl,\n.fb_button,\n.fb_button_rtl {\n text-decoration: none;\n}\n\na.fb_button:active .fb_button_text,\na.fb_button_rtl:active .fb_button_text,\n.fb_button:active .fb_button_text,\n.fb_button_rtl:active .fb_button_text {\n border-bottom: solid 1px #29447e;\n border-top: solid 1px #45619d;\n background: #4f6aa3;\n text-shadow: none;\n}\n\n\n.fb_button_xlarge,\n.fb_button_xlarge_rtl {\n background-position: left -60px;\n font-size: 24px;\n line-height: 30px;\n}\n.fb_button_xlarge .fb_button_text {\n padding: 3px 8px 3px 12px;\n margin-left: 38px;\n}\na.fb_button_xlarge:active {\n background-position: left -99px;\n}\n.fb_button_xlarge_rtl {\n background-position: right -268px;\n}\n.fb_button_xlarge_rtl .fb_button_text {\n padding: 3px 8px 3px 12px;\n margin-right: 39px;\n}\na.fb_button_xlarge_rtl:active {\n background-position: right -307px;\n}\n\n.fb_button_large,\n.fb_button_large_rtl {\n background-position: left -138px;\n font-size: 13px;\n line-height: 16px;\n}\n.fb_button_large .fb_button_text {\n margin-left: 24px;\n padding: 2px 6px 4px 6px;\n}\na.fb_button_large:active {\n background-position: left -163px;\n}\n.fb_button_large_rtl {\n background-position: right -346px;\n}\n.fb_button_large_rtl .fb_button_text {\n margin-right: 25px;\n}\na.fb_button_large_rtl:active {\n background-position: right -371px;\n}\n\n.fb_button_medium,\n.fb_button_medium_rtl {\n background-position: left -188px;\n font-size: 11px;\n line-height: 14px;\n}\na.fb_button_medium:active {\n background-position: left -210px;\n}\n\n.fb_button_medium_rtl {\n background-position: right -396px;\n}\n.fb_button_text_rtl,\n.fb_button_medium_rtl .fb_button_text {\n padding: 2px 6px 3px 6px;\n margin-right: 22px;\n}\na.fb_button_medium_rtl:active {\n background-position: right -418px;\n}\n\n.fb_button_small,\n.fb_button_small_rtl {\n background-position: left -232px;\n font-size: 10px;\n line-height: 10px;\n}\n.fb_button_small .fb_button_text {\n padding: 2px 6px 3px;\n margin-left: 17px;\n}\na.fb_button_small:active,\n.fb_button_small:active {\n background-position: left -250px;\n}\n\n.fb_button_small_rtl {\n background-position: right -440px;\n}\n.fb_button_small_rtl .fb_button_text {\n padding: 2px 6px;\n margin-right: 18px;\n}\na.fb_button_small_rtl:active {\n background-position: right -458px;\n}\n\/**\n * Copyright Facebook Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @author arunv\n * @provides fb.css.sharebutton\n * @layer xfbml\n * @requires fb.css.button\n *\/\n.fb_share_count_wrapper {\n position: relative;\n float: left;\n}\n\n.fb_share_count {\n background: #b0b9ec none repeat scroll 0 0;\n color: #333333;\n font-family: \"lucida grande\", tahoma, verdana, arial, sans-serif;\n text-align: center;\n}\n\n.fb_share_count_inner {\n background: #e8ebf2;\n display: block;\n}\n\n.fb_share_count_right {\n margin-left: -1px;\n display: inline-block;\n}\n\n.fb_share_count_right .fb_share_count_inner {\n border-top: solid 1px #e8ebf2;\n border-bottom: solid 1px #b0b9ec;\n margin: 1px 1px 0px 1px;\n font-size: 10px;\n line-height: 10px;\n padding: 2px 6px 3px;\n font-weight: bold;\n}\n\n.fb_share_count_top {\n display: block;\n letter-spacing: -1px;\n line-height: 34px;\n margin-bottom: 7px;\n font-size: 22px;\n border: solid 1px #b0b9ec;\n}\n\n.fb_share_count_nub_top {\n border: none;\n display: block;\n position: absolute;\n left: 7px;\n top: 35px;\n margin: 0;\n padding: 0;\n width: 6px;\n height: 7px;\n background-repeat: no-repeat;\n background-image: url(http:\/\/static.ak.fbcdn.net\/images\/sharepro\/sp_h_nub.png);\n}\n\n.fb_share_count_nub_right {\n border: none;\n display: inline-block;\n padding: 0;\n width: 5px;\n height: 10px;\n background-repeat: no-repeat;\n background-image: url(http:\/\/static.ak.fbcdn.net\/images\/sharepro\/sp_v_nub.png);\n vertical-align: top;\n background-position:right 5px;\n z-index: 10;\n left: 2px;\n margin: 0px 2px 0px 0px;\n position: relative;\n}\n\n.fb_share_no_count {\n display: none;\n}\n\n.fb_share_size_Small .fb_share_count_right .fb_share_count_inner {\n font-size: 10px;\n}\n\n.fb_share_size_Medium .fb_share_count_right .fb_share_count_inner {\n font-size: 11px;\n padding: 2px 6px 3px;\n letter-spacing: -1px;\n line-height: 14px;\n}\n\n.fb_share_size_Large .fb_share_count_right .fb_share_count_inner {\n font-size: 13px;\n line-height: 16px;\n padding: 2px 6px 4px;\n font-weight: normal;\n letter-spacing: -1px;\n}\n\/**\n * Copyright Facebook Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @author naitik\n * @provides fb.css.base\n *\/\n\n.fb_hidden {\n position: absolute;\n top: -10000px;\n z-index: 10001;\n}\n\n.fb_reset {\n background: none;\n border-spacing: 0;\n border: 0px;\n color: #000;\n cursor: auto;\n direction: ltr;\n font-family: \"lucida grande\", tahoma, verdana, arial, sans-serif;\n font-size: 11px;\n font-style: normal;\n font-variant: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-height: 1;\n margin: 0;\n overflow: visible;\n padding: 0;\n text-align: left;\n text-decoration: none;\n text-indent: 0;\n text-shadow: none;\n text-transform: none;\n visibility: visible;\n white-space: normal;\n word-spacing: normal;\n}\n\n.fb_link img {\n border: none;\n}\n\/**\n * Copyright Facebook Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * @author naitik\n * @provides fb.css.iframewidget\n * @layer xfbml\n *\/\n.fb_iframe_widget {\n position: relative;\n display: -moz-inline-block; \/* ff2 *\/\n display: inline-block;\n}\n.fb_iframe_widget iframe {\n \/* this is necessary for IE. without it, once hidden, it wont become visible\n * again *\/\n position: relative;\n \/* this is to remove the bottom margin appearing on the iframe widgets *\/\n vertical-align: text-bottom;\n}\n\n.fb_iframe_widget span {\n \/* this is necessary for IE as well. without it, the content of the iframe would be\n * totally off when resizing the parent window.\n * probably related to this bug http:\/\/friendlybit.com\/css\/ie6-resize-bug\/\n *\/\n position: relative;\n}\n\n.fb_hide_iframes iframe {\n position: relative;\n left: -10000px;\n}\n.fb_iframe_widget_loader {\n position: relative;\n display: inline-block;\n}\n.fb_iframe_widget_loader iframe {\n min-height: 32px;\n z-index: 2;\n zoom: 1;\n}\n.fb_iframe_widget_loader .FB_Loader {\n background: url(http:\/\/static.ak.fbcdn.net\/images\/loaders\/indicator_blue_large.gif) no-repeat;\n height: 32px;\n width: 32px;\n margin-left: -16px;\n position: absolute;\n left: 50%;\n z-index: 4;\n}\n", ["pkg"]) diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 000000000..2227f472a --- /dev/null +++ b/plugin.xml @@ -0,0 +1,161 @@ + + + + Facebook Connect + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native/android/src/org/apache/cordova/facebook/ConnectPlugin.java b/src/android/ConnectPlugin.java similarity index 100% rename from native/android/src/org/apache/cordova/facebook/ConnectPlugin.java rename to src/android/ConnectPlugin.java diff --git a/src/android/facebook/AsyncFacebookRunner.java b/src/android/facebook/AsyncFacebookRunner.java new file mode 100644 index 000000000..be3870a1a --- /dev/null +++ b/src/android/facebook/AsyncFacebookRunner.java @@ -0,0 +1,316 @@ +/* + * Copyright 2010 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; + +import android.content.Context; +import android.os.Bundle; + +/** + * A sample implementation of asynchronous API requests. This class provides + * the ability to execute API methods and have the call return immediately, + * without blocking the calling thread. This is necessary when accessing the + * API in the UI thread, for instance. The request response is returned to + * the caller via a callback interface, which the developer must implement. + * + * This sample implementation simply spawns a new thread for each request, + * and makes the API call immediately. This may work in many applications, + * but more sophisticated users may re-implement this behavior using a thread + * pool, a network thread, a request queue, or other mechanism. Advanced + * functionality could be built, such as rate-limiting of requests, as per + * a specific application's needs. + * + * @see RequestListener + * The callback interface. + * + * @author Jim Brusstar (jimbru@fb.com), + * Yariv Sadan (yariv@fb.com), + * Luke Shepard (lshepard@fb.com) + */ +public class AsyncFacebookRunner { + + Facebook fb; + + public AsyncFacebookRunner(Facebook fb) { + this.fb = fb; + } + + /** + * Invalidate the current user session by removing the access token in + * memory, clearing the browser cookies, and calling auth.expireSession + * through the API. The application will be notified when logout is + * complete via the callback interface. + * + * Note that this method is asynchronous and the callback will be invoked + * in a background thread; operations that affect the UI will need to be + * posted to the UI thread or an appropriate handler. + * + * @param context + * The Android context in which the logout should be called: it + * should be the same context in which the login occurred in + * order to clear any stored cookies + * @param listener + * Callback interface to notify the application when the request + * has completed. + * @param state + * An arbitrary object used to identify the request when it + * returns to the callback. This has no effect on the request + * itself. + */ + public void logout(final Context context, + final RequestListener listener, + final Object state) { + new Thread() { + @Override public void run() { + try { + String response = fb.logout(context); + if (response.length() == 0 || response.equals("false")){ + listener.onFacebookError(new FacebookError( + "auth.expireSession failed"), state); + return; + } + listener.onComplete(response, state); + } catch (FileNotFoundException e) { + listener.onFileNotFoundException(e, state); + } catch (MalformedURLException e) { + listener.onMalformedURLException(e, state); + } catch (IOException e) { + listener.onIOException(e, state); + } + } + }.start(); + } + + public void logout(final Context context, final RequestListener listener) { + logout(context, listener, /* state */ null); + } + + /** + * Make a request to Facebook's old (pre-graph) API with the given + * parameters. One of the parameter keys must be "method" and its value + * should be a valid REST server API method. + * + * See http://developers.facebook.com/docs/reference/rest/ + * + * Note that this method is asynchronous and the callback will be invoked + * in a background thread; operations that affect the UI will need to be + * posted to the UI thread or an appropriate handler. + * + * Example: + * + * Bundle parameters = new Bundle(); + * parameters.putString("method", "auth.expireSession", new Listener()); + * String response = request(parameters); + * + * + * @param parameters + * Key-value pairs of parameters to the request. Refer to the + * documentation: one of the parameters must be "method". + * @param listener + * Callback interface to notify the application when the request + * has completed. + * @param state + * An arbitrary object used to identify the request when it + * returns to the callback. This has no effect on the request + * itself. + */ + public void request(Bundle parameters, + RequestListener listener, + final Object state) { + request(null, parameters, "GET", listener, state); + } + + public void request(Bundle parameters, RequestListener listener) { + request(null, parameters, "GET", listener, /* state */ null); + } + + /** + * Make a request to the Facebook Graph API without any parameters. + * + * See http://developers.facebook.com/docs/api + * + * Note that this method is asynchronous and the callback will be invoked + * in a background thread; operations that affect the UI will need to be + * posted to the UI thread or an appropriate handler. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param listener + * Callback interface to notify the application when the request + * has completed. + * @param state + * An arbitrary object used to identify the request when it + * returns to the callback. This has no effect on the request + * itself. + */ + public void request(String graphPath, + RequestListener listener, + final Object state) { + request(graphPath, new Bundle(), "GET", listener, state); + } + + public void request(String graphPath, RequestListener listener) { + request(graphPath, new Bundle(), "GET", listener, /* state */ null); + } + + /** + * Make a request to the Facebook Graph API with the given string parameters + * using an HTTP GET (default method). + * + * See http://developers.facebook.com/docs/api + * + * Note that this method is asynchronous and the callback will be invoked + * in a background thread; operations that affect the UI will need to be + * posted to the UI thread or an appropriate handler. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param parameters + * key-value string parameters, e.g. the path "search" with + * parameters "q" : "facebook" would produce a query for the + * following graph resource: + * https://graph.facebook.com/search?q=facebook + * @param listener + * Callback interface to notify the application when the request + * has completed. + * @param state + * An arbitrary object used to identify the request when it + * returns to the callback. This has no effect on the request + * itself. + */ + public void request(String graphPath, + Bundle parameters, + RequestListener listener, + final Object state) { + request(graphPath, parameters, "GET", listener, state); + } + + public void request(String graphPath, + Bundle parameters, + RequestListener listener) { + request(graphPath, parameters, "GET", listener, /* state */ null); + } + + /** + * Make a request to the Facebook Graph API with the given HTTP method and + * string parameters. Note that binary data parameters (e.g. pictures) are + * not yet supported by this helper function. + * + * See http://developers.facebook.com/docs/api + * + * Note that this method is asynchronous and the callback will be invoked + * in a background thread; operations that affect the UI will need to be + * posted to the UI thread or an appropriate handler. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param parameters + * key-value string parameters, e.g. the path "search" with + * parameters {"q" : "facebook"} would produce a query for the + * following graph resource: + * https://graph.facebook.com/search?q=facebook + * @param httpMethod + * http verb, e.g. "POST", "DELETE" + * @param listener + * Callback interface to notify the application when the request + * has completed. + * @param state + * An arbitrary object used to identify the request when it + * returns to the callback. This has no effect on the request + * itself. + */ + public void request(final String graphPath, + final Bundle parameters, + final String httpMethod, + final RequestListener listener, + final Object state) { + new Thread() { + @Override public void run() { + try { + String resp = fb.request(graphPath, parameters, httpMethod); + listener.onComplete(resp, state); + } catch (FileNotFoundException e) { + listener.onFileNotFoundException(e, state); + } catch (MalformedURLException e) { + listener.onMalformedURLException(e, state); + } catch (IOException e) { + listener.onIOException(e, state); + } + } + }.start(); + } + + /** + * Callback interface for API requests. + * + * Each method includes a 'state' parameter that identifies the calling + * request. It will be set to the value passed when originally calling the + * request method, or null if none was passed. + */ + public static interface RequestListener { + + /** + * Called when a request completes with the given response. + * + * Executed by a background thread: do not update the UI in this method. + */ + public void onComplete(String response, Object state); + + /** + * Called when a request has a network or request error. + * + * Executed by a background thread: do not update the UI in this method. + */ + public void onIOException(IOException e, Object state); + + /** + * Called when a request fails because the requested resource is + * invalid or does not exist. + * + * Executed by a background thread: do not update the UI in this method. + */ + public void onFileNotFoundException(FileNotFoundException e, + Object state); + + /** + * Called if an invalid graph path is provided (which may result in a + * malformed URL). + * + * Executed by a background thread: do not update the UI in this method. + */ + public void onMalformedURLException(MalformedURLException e, + Object state); + + /** + * Called when the server-side Facebook method fails. + * + * Executed by a background thread: do not update the UI in this method. + */ + public void onFacebookError(FacebookError e, Object state); + + } + +} diff --git a/src/android/facebook/DialogError.java b/src/android/facebook/DialogError.java new file mode 100644 index 000000000..51d06c9a1 --- /dev/null +++ b/src/android/facebook/DialogError.java @@ -0,0 +1,51 @@ +/* + * Copyright 2010 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +/** + * Encapsulation of Dialog Error. + * + * @author ssoneff@facebook.com + */ +public class DialogError extends Throwable { + + private static final long serialVersionUID = 1L; + + /** + * The ErrorCode received by the WebView: see + * http://developer.android.com/reference/android/webkit/WebViewClient.html + */ + private int mErrorCode; + + /** The URL that the dialog was trying to load */ + private String mFailingUrl; + + public DialogError(String message, int errorCode, String failingUrl) { + super(message); + mErrorCode = errorCode; + mFailingUrl = failingUrl; + } + + int getErrorCode() { + return mErrorCode; + } + + String getFailingUrl() { + return mFailingUrl; + } + +} diff --git a/src/android/facebook/Facebook.java b/src/android/facebook/Facebook.java new file mode 100644 index 000000000..173f34205 --- /dev/null +++ b/src/android/facebook/Facebook.java @@ -0,0 +1,1179 @@ +/* + * Copyright 2010 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; + +import android.Manifest; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.Signature; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.text.TextUtils; +import android.webkit.CookieSyncManager; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Main Facebook object for interacting with the Facebook developer API. + * Provides methods to log in and log out a user, make requests using the REST + * and Graph APIs, and start user interface interactions with the API (such as + * pop-ups promoting for credentials, permissions, stream posts, etc.) + * + * @author Jim Brusstar (jimbru@facebook.com), + * Yariv Sadan (yariv@facebook.com), + * Luke Shepard (lshepard@facebook.com) + */ +public class Facebook { + + // Strings used in the authorization flow + public static final String REDIRECT_URI = "fbconnect://success"; + public static final String CANCEL_URI = "fbconnect://cancel"; + public static final String TOKEN = "access_token"; + public static final String EXPIRES = "expires_in"; + public static final String SINGLE_SIGN_ON_DISABLED = "service_disabled"; + + public static final Uri ATTRIBUTION_ID_CONTENT_URI = + Uri.parse("content://com.facebook.katana.provider.AttributionIdProvider"); + public static final String ATTRIBUTION_ID_COLUMN_NAME = "aid"; + + private static final String ATTRIBUTION_PREFERENCES = "com.facebook.sdk.attributionTracking"; + private static final String PUBLISH_ACTIVITY_PATH = "%s/activities"; + private static final String MOBILE_INSTALL_EVENT = "MOBILE_APP_INSTALL"; + private static final String SUPPORTS_ATTRIBUTION = "supports_attribution"; + private static final String APPLICATION_FIELDS = "fields"; + private static final String ANALYTICS_EVENT = "event"; + private static final String ATTRIBUTION_KEY = "attribution"; + + public static final int FORCE_DIALOG_AUTH = -1; + + private static final String LOGIN = "oauth"; + + // Used as default activityCode by authorize(). See authorize() below. + private static final int DEFAULT_AUTH_ACTIVITY_CODE = 32665; + + // Facebook server endpoints: may be modified in a subclass for testing + protected static String DIALOG_BASE_URL = + "https://m.facebook.com/dialog/"; + protected static String GRAPH_BASE_URL = + "https://graph.facebook.com/"; + protected static String RESTSERVER_URL = + "https://api.facebook.com/restserver.php"; + + private String mAccessToken = null; + private long mLastAccessUpdate = 0; + private long mAccessExpires = 0; + private String mAppId; + + private Activity mAuthActivity; + private String[] mAuthPermissions; + private int mAuthActivityCode; + private DialogListener mAuthDialogListener; + + // If the last time we extended the access token was more than 24 hours ago + // we try to refresh the access token again. + final private long REFRESH_TOKEN_BARRIER = 24L * 60L * 60L * 1000L; + + private boolean shouldAutoPublishInstall = true; + private AutoPublishAsyncTask mAutoPublishAsyncTask = null; + + /** + * Constructor for Facebook object. + * + * @param appId + * Your Facebook application ID. Found at + * www.facebook.com/developers/apps.php. + */ + public Facebook(String appId) { + if (appId == null) { + throw new IllegalArgumentException( + "You must specify your application ID when instantiating " + + "a Facebook object. See README for details."); + } + mAppId = appId; + } + + /** + * Default authorize method. Grants only basic permissions. + * + * See authorize() below for @params. + */ + public void authorize(Activity activity, final DialogListener listener) { + authorize(activity, new String[] {}, DEFAULT_AUTH_ACTIVITY_CODE, + listener); + } + + /** + * Authorize method that grants custom permissions. + * + * See authorize() below for @params. + */ + public void authorize(Activity activity, String[] permissions, + final DialogListener listener) { + authorize(activity, permissions, DEFAULT_AUTH_ACTIVITY_CODE, listener); + } + + /** + * Full authorize method. + * + * Starts either an Activity or a dialog which prompts the user to log in to + * Facebook and grant the requested permissions to the given application. + * + * This method will, when possible, use Facebook's single sign-on for + * Android to obtain an access token. This involves proxying a call through + * the Facebook for Android stand-alone application, which will handle the + * authentication flow, and return an OAuth access token for making API + * calls. + * + * Because this process will not be available for all users, if single + * sign-on is not possible, this method will automatically fall back to the + * OAuth 2.0 User-Agent flow. In this flow, the user credentials are handled + * by Facebook in an embedded WebView, not by the client application. As + * such, the dialog makes a network request and renders HTML content rather + * than a native UI. The access token is retrieved from a redirect to a + * special URL that the WebView handles. + * + * Note that User credentials could be handled natively using the OAuth 2.0 + * Username and Password Flow, but this is not supported by this SDK. + * + * See http://developers.facebook.com/docs/authentication/ and + * http://wiki.oauth.net/OAuth-2 for more details. + * + * Note that this method is asynchronous and the callback will be invoked in + * the original calling thread (not in a background thread). + * + * Also note that requests may be made to the API without calling authorize + * first, in which case only public information is returned. + * + * IMPORTANT: Note that single sign-on authentication will not function + * correctly if you do not include a call to the authorizeCallback() method + * in your onActivityResult() function! Please see below for more + * information. single sign-on may be disabled by passing FORCE_DIALOG_AUTH + * as the activityCode parameter in your call to authorize(). + * + * @param activity + * The Android activity in which we want to display the + * authorization dialog. + * @param applicationId + * The Facebook application identifier e.g. "350685531728" + * @param permissions + * A list of permissions required for this application: e.g. + * "read_stream", "publish_stream", "offline_access", etc. see + * http://developers.facebook.com/docs/authentication/permissions + * This parameter should not be null -- if you do not require any + * permissions, then pass in an empty String array. + * @param activityCode + * Single sign-on requires an activity result to be called back + * to the client application -- if you are waiting on other + * activities to return data, pass a custom activity code here to + * avoid collisions. If you would like to force the use of legacy + * dialog-based authorization, pass FORCE_DIALOG_AUTH for this + * parameter. Otherwise just omit this parameter and Facebook + * will use a suitable default. See + * http://developer.android.com/reference/android/ + * app/Activity.html for more information. + * @param listener + * Callback interface for notifying the calling application when + * the authentication dialog has completed, failed, or been + * canceled. + */ + public void authorize(Activity activity, String[] permissions, + int activityCode, final DialogListener listener) { + + boolean singleSignOnStarted = false; + + mAuthDialogListener = listener; + + // fire off an auto-attribution publish if appropriate. + autoPublishAsync(activity.getApplicationContext()); + + // Prefer single sign-on, where available. + if (activityCode >= 0) { + singleSignOnStarted = startSingleSignOn(activity, mAppId, + permissions, activityCode); + } + // Otherwise fall back to traditional dialog. + if (!singleSignOnStarted) { + startDialogAuth(activity, permissions); + } + } + + /** + * Internal method to handle single sign-on backend for authorize(). + * + * @param activity + * The Android Activity that will parent the ProxyAuth Activity. + * @param applicationId + * The Facebook application identifier. + * @param permissions + * A list of permissions required for this application. If you do + * not require any permissions, pass an empty String array. + * @param activityCode + * Activity code to uniquely identify the result Intent in the + * callback. + */ + private boolean startSingleSignOn(Activity activity, String applicationId, + String[] permissions, int activityCode) { + boolean didSucceed = true; + Intent intent = new Intent(); + + intent.setClassName("com.facebook.katana", + "com.facebook.katana.ProxyAuth"); + intent.putExtra("client_id", applicationId); + if (permissions.length > 0) { + intent.putExtra("scope", TextUtils.join(",", permissions)); + } + + // Verify that the application whose package name is + // com.facebook.katana.ProxyAuth + // has the expected FB app signature. + if (!validateActivityIntent(activity, intent)) { + return false; + } + + mAuthActivity = activity; + mAuthPermissions = permissions; + mAuthActivityCode = activityCode; + try { + activity.startActivityForResult(intent, activityCode); + } catch (ActivityNotFoundException e) { + didSucceed = false; + } + + return didSucceed; + } + + /** + * Helper to validate an activity intent by resolving and checking the + * provider's package signature. + * + * @param context + * @param intent + * @return true if the service intent resolution happens successfully and the + * signatures match. + */ + private boolean validateActivityIntent(Context context, Intent intent) { + ResolveInfo resolveInfo = + context.getPackageManager().resolveActivity(intent, 0); + if (resolveInfo == null) { + return false; + } + + return validateAppSignatureForPackage( + context, + resolveInfo.activityInfo.packageName); + } + + + /** + * Helper to validate a service intent by resolving and checking the + * provider's package signature. + * + * @param context + * @param intent + * @return true if the service intent resolution happens successfully and the + * signatures match. + */ + private boolean validateServiceIntent(Context context, Intent intent) { + ResolveInfo resolveInfo = + context.getPackageManager().resolveService(intent, 0); + if (resolveInfo == null) { + return false; + } + + return validateAppSignatureForPackage( + context, + resolveInfo.serviceInfo.packageName); + } + + /** + * Query the signature for the application that would be invoked by the + * given intent and verify that it matches the FB application's signature. + * + * @param context + * @param packageName + * @return true if the app's signature matches the expected signature. + */ + private boolean validateAppSignatureForPackage(Context context, + String packageName) { + + PackageInfo packageInfo; + try { + packageInfo = context.getPackageManager().getPackageInfo( + packageName, PackageManager.GET_SIGNATURES); + } catch (NameNotFoundException e) { + return false; + } + + for (Signature signature : packageInfo.signatures) { + if (signature.toCharsString().equals(FB_APP_SIGNATURE)) { + return true; + } + } + return false; + } + + /** + * Internal method to handle dialog-based authentication backend for + * authorize(). + * + * @param activity + * The Android Activity that will parent the auth dialog. + * @param applicationId + * The Facebook application identifier. + * @param permissions + * A list of permissions required for this application. If you do + * not require any permissions, pass an empty String array. + */ + private void startDialogAuth(Activity activity, String[] permissions) { + Bundle params = new Bundle(); + if (permissions.length > 0) { + params.putString("scope", TextUtils.join(",", permissions)); + } + CookieSyncManager.createInstance(activity); + dialog(activity, LOGIN, params, new DialogListener() { + + public void onComplete(Bundle values) { + // ensure any cookies set by the dialog are saved + CookieSyncManager.getInstance().sync(); + setAccessToken(values.getString(TOKEN)); + setAccessExpiresIn(values.getString(EXPIRES)); + if (isSessionValid()) { + Util.logd("Facebook-authorize", "Login Success! access_token=" + + getAccessToken() + " expires=" + + getAccessExpires()); + mAuthDialogListener.onComplete(values); + } else { + mAuthDialogListener.onFacebookError(new FacebookError( + "Failed to receive access token.")); + } + } + + public void onError(DialogError error) { + Util.logd("Facebook-authorize", "Login failed: " + error); + mAuthDialogListener.onError(error); + } + + public void onFacebookError(FacebookError error) { + Util.logd("Facebook-authorize", "Login failed: " + error); + mAuthDialogListener.onFacebookError(error); + } + + public void onCancel() { + Util.logd("Facebook-authorize", "Login canceled"); + mAuthDialogListener.onCancel(); + } + }); + } + + /** + * IMPORTANT: This method must be invoked at the top of the calling + * activity's onActivityResult() function or Facebook authentication will + * not function properly! + * + * If your calling activity does not currently implement onActivityResult(), + * you must implement it and include a call to this method if you intend to + * use the authorize() method in this SDK. + * + * For more information, see + * http://developer.android.com/reference/android/app/ + * Activity.html#onActivityResult(int, int, android.content.Intent) + */ + public void authorizeCallback(int requestCode, int resultCode, Intent data) { + if (requestCode == mAuthActivityCode) { + + // Successfully redirected. + if (resultCode == Activity.RESULT_OK) { + + // Check OAuth 2.0/2.10 error code. + String error = data.getStringExtra("error"); + if (error == null) { + error = data.getStringExtra("error_type"); + } + + // A Facebook error occurred. + if (error != null) { + if (error.equals(SINGLE_SIGN_ON_DISABLED) + || error.equals("AndroidAuthKillSwitchException")) { + Util.logd("Facebook-authorize", "Hosted auth currently " + + "disabled. Retrying dialog auth..."); + startDialogAuth(mAuthActivity, mAuthPermissions); + } else if (error.equals("access_denied") + || error.equals("OAuthAccessDeniedException")) { + Util.logd("Facebook-authorize", "Login canceled by user."); + mAuthDialogListener.onCancel(); + } else { + String description = data.getStringExtra("error_description"); + if (description != null) { + error = error + ":" + description; + } + Util.logd("Facebook-authorize", "Login failed: " + error); + mAuthDialogListener.onFacebookError( + new FacebookError(error)); + } + + // No errors. + } else { + setAccessToken(data.getStringExtra(TOKEN)); + setAccessExpiresIn(data.getStringExtra(EXPIRES)); + if (isSessionValid()) { + Util.logd("Facebook-authorize", + "Login Success! access_token=" + + getAccessToken() + " expires=" + + getAccessExpires()); + mAuthDialogListener.onComplete(data.getExtras()); + } else { + mAuthDialogListener.onFacebookError(new FacebookError( + "Failed to receive access token.")); + } + } + + // An error occurred before we could be redirected. + } else if (resultCode == Activity.RESULT_CANCELED) { + + // An Android error occured. + if (data != null) { + Util.logd("Facebook-authorize", + "Login failed: " + data.getStringExtra("error")); + mAuthDialogListener.onError( + new DialogError( + data.getStringExtra("error"), + data.getIntExtra("error_code", -1), + data.getStringExtra("failing_url"))); + + // User pressed the 'back' button. + } else { + Util.logd("Facebook-authorize", "Login canceled by user."); + mAuthDialogListener.onCancel(); + } + } + } + } + + /** + * Refresh OAuth access token method. Binds to Facebook for Android + * stand-alone application application to refresh the access token. This + * method tries to connect to the Facebook App which will handle the + * authentication flow, and return a new OAuth access token. This method + * will automatically replace the old token with a new one. Note that this + * method is asynchronous and the callback will be invoked in the original + * calling thread (not in a background thread). + * + * @param context + * The Android Context that will be used to bind to the Facebook + * RefreshToken Service + * @param serviceListener + * Callback interface for notifying the calling application when + * the refresh request has completed or failed (can be null). In + * case of a success a new token can be found inside the result + * Bundle under Facebook.ACCESS_TOKEN key. + * @return true if the binding to the RefreshToken Service was created + */ + public boolean extendAccessToken(Context context, ServiceListener serviceListener) { + Intent intent = new Intent(); + + intent.setClassName("com.facebook.katana", + "com.facebook.katana.platform.TokenRefreshService"); + + // Verify that the application whose package name is + // com.facebook.katana + // has the expected FB app signature. + if (!validateServiceIntent(context, intent)) { + return false; + } + + return context.bindService(intent, + new TokenRefreshServiceConnection(context, serviceListener), + Context.BIND_AUTO_CREATE); + } + + /** + * Calls extendAccessToken if shouldExtendAccessToken returns true. + * + * @return the same value as extendAccessToken if the the token requires + * refreshing, true otherwise + */ + public boolean extendAccessTokenIfNeeded(Context context, ServiceListener serviceListener) { + if (shouldExtendAccessToken()) { + return extendAccessToken(context, serviceListener); + } + return true; + } + + /** + * Check if the access token requires refreshing. + * + * @return true if the last time a new token was obtained was over 24 hours ago. + */ + public boolean shouldExtendAccessToken() { + return isSessionValid() && + (System.currentTimeMillis() - mLastAccessUpdate >= REFRESH_TOKEN_BARRIER); + } + + /** + * Handles connection to the token refresh service (this service is a part + * of Facebook App). + */ + private class TokenRefreshServiceConnection implements ServiceConnection { + + final Messenger messageReceiver = new Messenger(new Handler() { + @Override + public void handleMessage(Message msg) { + String token = msg.getData().getString(TOKEN); + long expiresAt = msg.getData().getLong(EXPIRES) * 1000L; + + // To avoid confusion we should return the expiration time in + // the same format as the getAccessExpires() function - that + // is in milliseconds. + Bundle resultBundle = (Bundle) msg.getData().clone(); + resultBundle.putLong(EXPIRES, expiresAt); + + if (token != null) { + setAccessToken(token); + setAccessExpires(expiresAt); + if (serviceListener != null) { + serviceListener.onComplete(resultBundle); + } + } else if (serviceListener != null) { // extract errors only if client wants them + String error = msg.getData().getString("error"); + if (msg.getData().containsKey("error_code")) { + int errorCode = msg.getData().getInt("error_code"); + serviceListener.onFacebookError(new FacebookError(error, null, errorCode)); + } else { + serviceListener.onError(new Error(error != null ? error + : "Unknown service error")); + } + } + + // The refreshToken function should be called rarely, + // so there is no point in keeping the binding open. + applicationsContext.unbindService(TokenRefreshServiceConnection.this); + } + }); + + final ServiceListener serviceListener; + final Context applicationsContext; + + Messenger messageSender = null; + + public TokenRefreshServiceConnection(Context applicationsContext, + ServiceListener serviceListener) { + this.applicationsContext = applicationsContext; + this.serviceListener = serviceListener; + } + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + messageSender = new Messenger(service); + refreshToken(); + } + + @Override + public void onServiceDisconnected(ComponentName arg) { + serviceListener.onError(new Error("Service disconnected")); + // We returned an error so there's no point in + // keeping the binding open. + applicationsContext.unbindService(TokenRefreshServiceConnection.this); + } + + private void refreshToken() { + Bundle requestData = new Bundle(); + requestData.putString(TOKEN, mAccessToken); + + Message request = Message.obtain(); + request.setData(requestData); + request.replyTo = messageReceiver; + + try { + messageSender.send(request); + } catch (RemoteException e) { + serviceListener.onError(new Error("Service connection error")); + } + } + }; + + /** + * Invalidate the current user session by removing the access token in + * memory, clearing the browser cookie, and calling auth.expireSession + * through the API. + * + * Note that this method blocks waiting for a network response, so do not + * call it in a UI thread. + * + * @param context + * The Android context in which the logout should be called: it + * should be the same context in which the login occurred in + * order to clear any stored cookies + * @throws IOException + * @throws MalformedURLException + * @return JSON string representation of the auth.expireSession response + * ("true" if successful) + */ + public String logout(Context context) + throws MalformedURLException, IOException { + Util.clearCookies(context); + Bundle b = new Bundle(); + b.putString("method", "auth.expireSession"); + String response = request(b); + setAccessToken(null); + setAccessExpires(0); + return response; + } + + /** + * Make a request to Facebook's old (pre-graph) API with the given + * parameters. One of the parameter keys must be "method" and its value + * should be a valid REST server API method. + * + * See http://developers.facebook.com/docs/reference/rest/ + * + * Note that this method blocks waiting for a network response, so do not + * call it in a UI thread. + * + * Example: + * + * Bundle parameters = new Bundle(); + * parameters.putString("method", "auth.expireSession"); + * String response = request(parameters); + * + * + * @param parameters + * Key-value pairs of parameters to the request. Refer to the + * documentation: one of the parameters must be "method". + * @throws IOException + * if a network error occurs + * @throws MalformedURLException + * if accessing an invalid endpoint + * @throws IllegalArgumentException + * if one of the parameters is not "method" + * @return JSON string representation of the response + */ + public String request(Bundle parameters) + throws MalformedURLException, IOException { + if (!parameters.containsKey("method")) { + throw new IllegalArgumentException("API method must be specified. " + + "(parameters must contain key \"method\" and value). See" + + " http://developers.facebook.com/docs/reference/rest/"); + } + return request(null, parameters, "GET"); + } + + /** + * Make a request to the Facebook Graph API without any parameters. + * + * See http://developers.facebook.com/docs/api + * + * Note that this method blocks waiting for a network response, so do not + * call it in a UI thread. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @throws IOException + * @throws MalformedURLException + * @return JSON string representation of the response + */ + public String request(String graphPath) + throws MalformedURLException, IOException { + return request(graphPath, new Bundle(), "GET"); + } + + /** + * Make a request to the Facebook Graph API with the given string parameters + * using an HTTP GET (default method). + * + * See http://developers.facebook.com/docs/api + * + * Note that this method blocks waiting for a network response, so do not + * call it in a UI thread. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param parameters + * key-value string parameters, e.g. the path "search" with + * parameters "q" : "facebook" would produce a query for the + * following graph resource: + * https://graph.facebook.com/search?q=facebook + * @throws IOException + * @throws MalformedURLException + * @return JSON string representation of the response + */ + public String request(String graphPath, Bundle parameters) + throws MalformedURLException, IOException { + return request(graphPath, parameters, "GET"); + } + + /** + * Synchronously make a request to the Facebook Graph API with the given + * HTTP method and string parameters. Note that binary data parameters + * (e.g. pictures) are not yet supported by this helper function. + * + * See http://developers.facebook.com/docs/api + * + * Note that this method blocks waiting for a network response, so do not + * call it in a UI thread. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param params + * Key-value string parameters, e.g. the path "search" with + * parameters {"q" : "facebook"} would produce a query for the + * following graph resource: + * https://graph.facebook.com/search?q=facebook + * @param httpMethod + * http verb, e.g. "GET", "POST", "DELETE" + * @throws IOException + * @throws MalformedURLException + * @return JSON string representation of the response + */ + public String request(String graphPath, Bundle params, String httpMethod) + throws FileNotFoundException, MalformedURLException, IOException { + params.putString("format", "json"); + if (isSessionValid()) { + params.putString(TOKEN, getAccessToken()); + } + String url = (graphPath != null) ? GRAPH_BASE_URL + graphPath + : RESTSERVER_URL; + return Util.openUrl(url, httpMethod, params); + } + + /** + * Generate a UI dialog for the request action in the given Android context. + * + * Note that this method is asynchronous and the callback will be invoked in + * the original calling thread (not in a background thread). + * + * @param context + * The Android context in which we will generate this dialog. + * @param action + * String representation of the desired method: e.g. "login", + * "stream.publish", ... + * @param listener + * Callback interface to notify the application when the dialog + * has completed. + */ + public void dialog(Context context, String action, + DialogListener listener) { + dialog(context, action, new Bundle(), listener); + } + + /** + * Generate a UI dialog for the request action in the given Android context + * with the provided parameters. + * + * Note that this method is asynchronous and the callback will be invoked in + * the original calling thread (not in a background thread). + * + * @param context + * The Android context in which we will generate this dialog. + * @param action + * String representation of the desired method: e.g. "feed" ... + * @param parameters + * String key-value pairs to be passed as URL parameters. + * @param listener + * Callback interface to notify the application when the dialog + * has completed. + */ + public void dialog(Context context, String action, Bundle parameters, + final DialogListener listener) { + + String endpoint = DIALOG_BASE_URL + action; + parameters.putString("display", "touch"); + parameters.putString("redirect_uri", REDIRECT_URI); + + if (action.equals(LOGIN)) { + parameters.putString("type", "user_agent"); + parameters.putString("client_id", mAppId); + } else { + parameters.putString("app_id", mAppId); + } + + if (isSessionValid()) { + parameters.putString(TOKEN, getAccessToken()); + } + String url = endpoint + "?" + Util.encodeUrl(parameters); + if (context.checkCallingOrSelfPermission(Manifest.permission.INTERNET) + != PackageManager.PERMISSION_GRANTED) { + Util.showAlert(context, "Error", + "Application requires permission to access the Internet"); + } else { + new FbDialog(context, url, listener).show(); + } + } + + /** + * @return boolean - whether this object has an non-expired session token + */ + public boolean isSessionValid() { + return (getAccessToken() != null) && + ((getAccessExpires() == 0) || + (System.currentTimeMillis() < getAccessExpires())); + } + + /** + * Retrieve the OAuth 2.0 access token for API access: treat with care. + * Returns null if no session exists. + * + * @return String - access token + */ + public String getAccessToken() { + return mAccessToken; + } + + /** + * Retrieve the current session's expiration time (in milliseconds since + * Unix epoch), or 0 if the session doesn't expire or doesn't exist. + * + * @return long - session expiration time + */ + public long getAccessExpires() { + return mAccessExpires; + } + + /** + * Retrieve the last time the token was updated (in milliseconds since + * the Unix epoch), or 0 if the token has not been set. + * + * @return long - timestamp of the last token update. + */ + public long getLastAccessUpdate() { + return mLastAccessUpdate; + } + + /** + * Restore the token, expiration time, and last update time from cached values. + * These should be values obtained from getAccessToken(), getAccessExpires, and + * getLastAccessUpdate() respectively. + * + * @param accessToken - access token + * @param accessExpires - access token expiration time + * @param lastAccessUpdate - timestamp of the last token update + */ + public void setTokenFromCache(String accessToken, long accessExpires, long lastAccessUpdate) { + mAccessToken = accessToken; + mAccessExpires = accessExpires; + mLastAccessUpdate = lastAccessUpdate; + } + + /** + * Set the OAuth 2.0 access token for API access. + * + * @param token - access token + */ + public void setAccessToken(String token) { + mAccessToken = token; + mLastAccessUpdate = System.currentTimeMillis(); + } + + /** + * Set the current session's expiration time (in milliseconds since Unix + * epoch), or 0 if the session doesn't expire. + * + * @param time - timestamp in milliseconds + */ + public void setAccessExpires(long time) { + mAccessExpires = time; + } + + /** + * Set the current session's duration (in seconds since Unix epoch), or "0" + * if session doesn't expire. + * + * @param expiresIn + * - duration in seconds (or 0 if the session doesn't expire) + */ + public void setAccessExpiresIn(String expiresIn) { + if (expiresIn != null) { + long expires = expiresIn.equals("0") + ? 0 + : System.currentTimeMillis() + Long.parseLong(expiresIn) * 1000L; + setAccessExpires(expires); + } + } + + public String getAppId() { + return mAppId; + } + + public void setAppId(String appId) { + mAppId = appId; + } + + /** + * Get Attribution ID for app install conversion tracking. + * @param contentResolver + * @return Attribution ID that will be used for conversion tracking. It will be null only if + * the user has not installed or logged in to the Facebook app. + */ + public static String getAttributionId(ContentResolver contentResolver) { + String [] projection = {ATTRIBUTION_ID_COLUMN_NAME}; + Cursor c = contentResolver.query(ATTRIBUTION_ID_CONTENT_URI, projection, null, null, null); + if (c == null || !c.moveToFirst()) { + return null; + } + String attributionId = c.getString(c.getColumnIndex(ATTRIBUTION_ID_COLUMN_NAME)); + + return attributionId; + } + + /** + * Get the auto install publish setting. If true, an install event will be published during authorize(), unless + * it has occurred previously or the app does not have install attribution enabled on the application's developer + * config page. + * @return + */ + public boolean getShouldAutoPublishInstall() { + return shouldAutoPublishInstall; + } + + /** + * Sets whether auto publishing of installs will occur. + * @param value + */ + public void setShouldAutoPublishInstall(boolean value) { + shouldAutoPublishInstall = value; + } + + /** + * Manually publish install attribution to the facebook graph. Internally handles tracking repeat calls to prevent + * multiple installs being published to the graph. + * @param context + * @return returns false on error. Applications should retry until true is returned. Safe to call again after + * true is returned. + */ + public boolean publishInstall(final Context context) { + try { + // copy the application id to guarantee thread safety.. + String applicationId = mAppId; + if (applicationId != null) { + publishInstall(this, applicationId, context); + return true; + } + } catch (Exception e) { + // if there was an error, fall through to the failure case. + Util.logd("Facebook-publish", e.getMessage()); + } + return false; + } + + /** + * This function does the heavy lifting of publishing an install. + * @param fb + * @param applicationId + * @param context + * @throws Exception + */ + private static void publishInstall(final Facebook fb, final String applicationId, final Context context) + throws JSONException, FacebookError, MalformedURLException, IOException { + + String attributionId = Facebook.getAttributionId(context.getContentResolver()); + SharedPreferences preferences = context.getSharedPreferences(ATTRIBUTION_PREFERENCES, Context.MODE_PRIVATE); + String pingKey = applicationId+"ping"; + long lastPing = preferences.getLong(pingKey, 0); + if (lastPing == 0 && attributionId != null) { + Bundle supportsAttributionParams = new Bundle(); + supportsAttributionParams.putString(APPLICATION_FIELDS, SUPPORTS_ATTRIBUTION); + JSONObject supportResponse = Util.parseJson(fb.request(applicationId, supportsAttributionParams)); + Object doesSupportAttribution = (Boolean)supportResponse.get(SUPPORTS_ATTRIBUTION); + + if (!(doesSupportAttribution instanceof Boolean)) { + throw new JSONException(String.format( + "%s contains %s instead of a Boolean", SUPPORTS_ATTRIBUTION, doesSupportAttribution)); + } + + if ((Boolean)doesSupportAttribution) { + Bundle publishParams = new Bundle(); + publishParams.putString(ANALYTICS_EVENT, MOBILE_INSTALL_EVENT); + publishParams.putString(ATTRIBUTION_KEY, attributionId); + + String publishUrl = String.format(PUBLISH_ACTIVITY_PATH, applicationId); + + fb.request(publishUrl, publishParams, "POST"); + + // denote success since no error threw from the post. + SharedPreferences.Editor editor = preferences.edit(); + editor.putLong(pingKey, System.currentTimeMillis()); + editor.commit(); + } + } + } + + void autoPublishAsync(final Context context) { + AutoPublishAsyncTask asyncTask = null; + synchronized (this) { + if (mAutoPublishAsyncTask == null && getShouldAutoPublishInstall()) { + // copy the application id to guarantee thread safety against our container. + String applicationId = Facebook.this.mAppId; + + // skip publish if we don't have an application id. + if (applicationId != null) { + asyncTask = mAutoPublishAsyncTask = new AutoPublishAsyncTask(applicationId, context); + } + } + } + + if (asyncTask != null) { + asyncTask.execute(); + } + } + + /** + * Async implementation to allow auto publishing to not block the ui thread. + */ + private class AutoPublishAsyncTask extends AsyncTask { + private final String mApplicationId; + private final Context mApplicationContext; + + public AutoPublishAsyncTask(String applicationId, Context context) { + mApplicationId = applicationId; + mApplicationContext = context.getApplicationContext(); + } + + @Override + protected Void doInBackground(Void... voids) { + try { + Facebook.publishInstall(Facebook.this, mApplicationId, mApplicationContext); + } catch (Exception e) { + Util.logd("Facebook-publish", e.getMessage()); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + // always clear out the publisher to allow other invocations. + synchronized (Facebook.this) { + mAutoPublishAsyncTask = null; + } + } + } + + /** + * Callback interface for dialog requests. + * + */ + public static interface DialogListener { + + /** + * Called when a dialog completes. + * + * Executed by the thread that initiated the dialog. + * + * @param values + * Key-value string pairs extracted from the response. + */ + public void onComplete(Bundle values); + + /** + * Called when a Facebook responds to a dialog with an error. + * + * Executed by the thread that initiated the dialog. + * + */ + public void onFacebookError(FacebookError e); + + /** + * Called when a dialog has an error. + * + * Executed by the thread that initiated the dialog. + * + */ + public void onError(DialogError e); + + /** + * Called when a dialog is canceled by the user. + * + * Executed by the thread that initiated the dialog. + * + */ + public void onCancel(); + + } + + /** + * Callback interface for service requests. + */ + public static interface ServiceListener { + + /** + * Called when a service request completes. + * + * @param values + * Key-value string pairs extracted from the response. + */ + public void onComplete(Bundle values); + + /** + * Called when a Facebook server responds to the request with an error. + */ + public void onFacebookError(FacebookError e); + + /** + * Called when a Facebook Service responds to the request with an error. + */ + public void onError(Error e); + + } + + public static final String FB_APP_SIGNATURE = + "30820268308201d102044a9c4610300d06092a864886f70d0101040500307a310" + + "b3009060355040613025553310b30090603550408130243413112301006035504" + + "07130950616c6f20416c746f31183016060355040a130f46616365626f6f6b204" + + "d6f62696c653111300f060355040b130846616365626f6f6b311d301b06035504" + + "03131446616365626f6f6b20436f72706f726174696f6e3020170d30393038333" + + "13231353231365a180f32303530303932353231353231365a307a310b30090603" + + "55040613025553310b30090603550408130243413112301006035504071309506" + + "16c6f20416c746f31183016060355040a130f46616365626f6f6b204d6f62696c" + + "653111300f060355040b130846616365626f6f6b311d301b06035504031314466" + + "16365626f6f6b20436f72706f726174696f6e30819f300d06092a864886f70d01" + + "0101050003818d0030818902818100c207d51df8eb8c97d93ba0c8c1002c928fa" + + "b00dc1b42fca5e66e99cc3023ed2d214d822bc59e8e35ddcf5f44c7ae8ade50d7" + + "e0c434f500e6c131f4a2834f987fc46406115de2018ebbb0d5a3c261bd97581cc" + + "fef76afc7135a6d59e8855ecd7eacc8f8737e794c60a761c536b72b11fac8e603" + + "f5da1a2d54aa103b8a13c0dbc10203010001300d06092a864886f70d010104050" + + "0038181005ee9be8bcbb250648d3b741290a82a1c9dc2e76a0af2f2228f1d9f9c" + + "4007529c446a70175c5a900d5141812866db46be6559e2141616483998211f4a6" + + "73149fb2232a10d247663b26a9031e15f84bc1c74d141ff98a02d76f85b2c8ab2" + + "571b6469b232d8e768a7f7ca04f7abe4a775615916c07940656b58717457b42bd" + + "928a2"; + +} diff --git a/src/android/facebook/FacebookError.java b/src/android/facebook/FacebookError.java new file mode 100644 index 000000000..3a2c6cd43 --- /dev/null +++ b/src/android/facebook/FacebookError.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +/** + * Encapsulation of a Facebook Error: a Facebook request that could not be + * fulfilled. + * + * @author ssoneff@facebook.com + */ +public class FacebookError extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private int mErrorCode = 0; + private String mErrorType; + + public FacebookError(String message) { + super(message); + } + + public FacebookError(String message, String type, int code) { + super(message); + mErrorType = type; + mErrorCode = code; + } + + public int getErrorCode() { + return mErrorCode; + } + + public String getErrorType() { + return mErrorType; + } + +} diff --git a/src/android/facebook/FbDialog.java b/src/android/facebook/FbDialog.java new file mode 100644 index 000000000..1f0b31406 --- /dev/null +++ b/src/android/facebook/FbDialog.java @@ -0,0 +1,198 @@ +/* + * Copyright 2010 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.Window; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.facebook.android.Facebook.DialogListener; + +public class FbDialog extends Dialog { + + static final int FB_BLUE = 0xFF6D84B4; + static final float[] DIMENSIONS_DIFF_LANDSCAPE = {20, 60}; + static final float[] DIMENSIONS_DIFF_PORTRAIT = {40, 60}; + static final FrameLayout.LayoutParams FILL = + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT); + static final int MARGIN = 4; + static final int PADDING = 2; + static final String DISPLAY_STRING = "touch"; + static final String FB_ICON = "icon.png"; + + private String mUrl; + private DialogListener mListener; + private ProgressDialog mSpinner; + private ImageView mCrossImage; + private WebView mWebView; + private FrameLayout mContent; + + public FbDialog(Context context, String url, DialogListener listener) { + super(context, android.R.style.Theme_Translucent_NoTitleBar); + mUrl = url; + mListener = listener; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSpinner = new ProgressDialog(getContext()); + mSpinner.requestWindowFeature(Window.FEATURE_NO_TITLE); + mSpinner.setMessage("Loading..."); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + mContent = new FrameLayout(getContext()); + + /* Create the 'x' image, but don't add to the mContent layout yet + * at this point, we only need to know its drawable width and height + * to place the webview + */ + createCrossImage(); + + /* Now we know 'x' drawable width and height, + * layout the webivew and add it the mContent layout + */ + int crossWidth = mCrossImage.getDrawable().getIntrinsicWidth(); + setUpWebView(crossWidth / 2); + + /* Finally add the 'x' image to the mContent layout and + * add mContent to the Dialog view + */ + mContent.addView(mCrossImage, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + addContentView(mContent, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); + } + + private void createCrossImage() { + mCrossImage = new ImageView(getContext()); + // Dismiss the dialog when user click on the 'x' + mCrossImage.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mListener.onCancel(); + FbDialog.this.dismiss(); + } + }); + int id = getContext().getResources().getIdentifier("close", "drawable", getContext().getPackageName()); + Drawable crossDrawable = getContext().getResources().getDrawable(id); + mCrossImage.setImageDrawable(crossDrawable); + /* 'x' should not be visible while webview is loading + * make it visible only after webview has fully loaded + */ + mCrossImage.setVisibility(View.INVISIBLE); + } + + private void setUpWebView(int margin) { + LinearLayout webViewContainer = new LinearLayout(getContext()); + mWebView = new WebView(getContext()); + mWebView.setVerticalScrollBarEnabled(false); + mWebView.setHorizontalScrollBarEnabled(false); + mWebView.setWebViewClient(new FbDialog.FbWebViewClient()); + mWebView.getSettings().setJavaScriptEnabled(true); + mWebView.loadUrl(mUrl); + mWebView.setLayoutParams(FILL); + mWebView.setVisibility(View.INVISIBLE); + mWebView.getSettings().setSavePassword(false); + + webViewContainer.setPadding(margin, margin, margin, margin); + webViewContainer.addView(mWebView); + mContent.addView(webViewContainer); + } + + private class FbWebViewClient extends WebViewClient { + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + Util.logd("Facebook-WebView", "Redirect URL: " + url); + if (url.startsWith(Facebook.REDIRECT_URI)) { + Bundle values = Util.parseUrl(url); + + String error = values.getString("error"); + if (error == null) { + error = values.getString("error_type"); + } + + if (error == null) { + mListener.onComplete(values); + } else if (error.equals("access_denied") || + error.equals("OAuthAccessDeniedException")) { + mListener.onCancel(); + } else { + mListener.onFacebookError(new FacebookError(error)); + } + + FbDialog.this.dismiss(); + return true; + } else if (url.startsWith(Facebook.CANCEL_URI)) { + mListener.onCancel(); + FbDialog.this.dismiss(); + return true; + } else if (url.contains(DISPLAY_STRING)) { + return false; + } + // launch non-dialog URLs in a full browser + getContext().startActivity( + new Intent(Intent.ACTION_VIEW, Uri.parse(url))); + return true; + } + + @Override + public void onReceivedError(WebView view, int errorCode, + String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + mListener.onError( + new DialogError(description, errorCode, failingUrl)); + FbDialog.this.dismiss(); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + Util.logd("Facebook-WebView", "Webview loading URL: " + url); + super.onPageStarted(view, url, favicon); + mSpinner.show(); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + mSpinner.dismiss(); + /* + * Once webview is fully loaded, set the mContent background to be transparent + * and make visible the 'x' image. + */ + mContent.setBackgroundColor(Color.TRANSPARENT); + mWebView.setVisibility(View.VISIBLE); + mCrossImage.setVisibility(View.VISIBLE); + } + } +} diff --git a/src/android/facebook/Util.java b/src/android/facebook/Util.java new file mode 100644 index 000000000..52cf4d87b --- /dev/null +++ b/src/android/facebook/Util.java @@ -0,0 +1,329 @@ +/* + * Copyright 2010 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.net.URLEncoder; + +import org.json.JSONException; +import org.json.JSONObject; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; + +/** + * Utility class supporting the Facebook Object. + * + * @author ssoneff@facebook.com + * + */ +public final class Util { + + /** + * Set this to true to enable log output. Remember to turn this back off + * before releasing. Sending sensitive data to log is a security risk. + */ + private static boolean ENABLE_LOG = false; + + /** + * Generate the multi-part post body providing the parameters and boundary + * string + * + * @param parameters the parameters need to be posted + * @param boundary the random string as boundary + * @return a string of the post body + */ + public static String encodePostBody(Bundle parameters, String boundary) { + if (parameters == null) return ""; + StringBuilder sb = new StringBuilder(); + + for (String key : parameters.keySet()) { + Object parameter = parameters.get(key); + if (!(parameter instanceof String)) { + continue; + } + + sb.append("Content-Disposition: form-data; name=\"" + key + + "\"\r\n\r\n" + (String)parameter); + sb.append("\r\n" + "--" + boundary + "\r\n"); + } + + return sb.toString(); + } + + public static String encodeUrl(Bundle parameters) { + if (parameters == null) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (String key : parameters.keySet()) { + Object parameter = parameters.get(key); + if (!(parameter instanceof String)) { + continue; + } + + if (first) first = false; else sb.append("&"); + sb.append(URLEncoder.encode(key) + "=" + + URLEncoder.encode(parameters.getString(key))); + } + return sb.toString(); + } + + public static Bundle decodeUrl(String s) { + Bundle params = new Bundle(); + if (s != null) { + String array[] = s.split("&"); + for (String parameter : array) { + String v[] = parameter.split("="); + if (v.length == 2) { + params.putString(URLDecoder.decode(v[0]), + URLDecoder.decode(v[1])); + } + } + } + return params; + } + + /** + * Parse a URL query and fragment parameters into a key-value bundle. + * + * @param url the URL to parse + * @return a dictionary bundle of keys and values + */ + public static Bundle parseUrl(String url) { + // hack to prevent MalformedURLException + url = url.replace("fbconnect", "http"); + try { + URL u = new URL(url); + Bundle b = decodeUrl(u.getQuery()); + b.putAll(decodeUrl(u.getRef())); + return b; + } catch (MalformedURLException e) { + return new Bundle(); + } + } + + + /** + * Connect to an HTTP URL and return the response as a string. + * + * Note that the HTTP method override is used on non-GET requests. (i.e. + * requests are made as "POST" with method specified in the body). + * + * @param url - the resource to open: must be a welformed URL + * @param method - the HTTP method to use ("GET", "POST", etc.) + * @param params - the query parameter for the URL (e.g. access_token=foo) + * @return the URL contents as a String + * @throws MalformedURLException - if the URL format is invalid + * @throws IOException - if a network problem occurs + */ + public static String openUrl(String url, String method, Bundle params) + throws MalformedURLException, IOException { + // random string as boundary for multi-part http post + String strBoundary = "3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f"; + String endLine = "\r\n"; + + OutputStream os; + + if (method.equals("GET")) { + url = url + "?" + encodeUrl(params); + } + Util.logd("Facebook-Util", method + " URL: " + url); + HttpURLConnection conn = + (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestProperty("User-Agent", System.getProperties(). + getProperty("http.agent") + " FacebookAndroidSDK"); + if (!method.equals("GET")) { + Bundle dataparams = new Bundle(); + for (String key : params.keySet()) { + Object parameter = params.get(key); + if (parameter instanceof byte[]) { + dataparams.putByteArray(key, (byte[])parameter); + } + } + + // use method override + if (!params.containsKey("method")) { + params.putString("method", method); + } + + if (params.containsKey("access_token")) { + String decoded_token = + URLDecoder.decode(params.getString("access_token")); + params.putString("access_token", decoded_token); + } + + conn.setRequestMethod("POST"); + conn.setRequestProperty( + "Content-Type", + "multipart/form-data;boundary="+strBoundary); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setRequestProperty("Connection", "Keep-Alive"); + conn.connect(); + os = new BufferedOutputStream(conn.getOutputStream()); + + os.write(("--" + strBoundary +endLine).getBytes()); + os.write((encodePostBody(params, strBoundary)).getBytes()); + os.write((endLine + "--" + strBoundary + endLine).getBytes()); + + if (!dataparams.isEmpty()) { + + for (String key: dataparams.keySet()){ + os.write(("Content-Disposition: form-data; filename=\"" + key + "\"" + endLine).getBytes()); + os.write(("Content-Type: content/unknown" + endLine + endLine).getBytes()); + os.write(dataparams.getByteArray(key)); + os.write((endLine + "--" + strBoundary + endLine).getBytes()); + + } + } + os.flush(); + } + + String response = ""; + try { + response = read(conn.getInputStream()); + } catch (FileNotFoundException e) { + // Error Stream contains JSON that we can parse to a FB error + response = read(conn.getErrorStream()); + } + return response; + } + + private static String read(InputStream in) throws IOException { + StringBuilder sb = new StringBuilder(); + BufferedReader r = new BufferedReader(new InputStreamReader(in), 1000); + for (String line = r.readLine(); line != null; line = r.readLine()) { + sb.append(line); + } + in.close(); + return sb.toString(); + } + + public static void clearCookies(Context context) { + // Edge case: an illegal state exception is thrown if an instance of + // CookieSyncManager has not be created. CookieSyncManager is normally + // created by a WebKit view, but this might happen if you start the + // app, restore saved state, and click logout before running a UI + // dialog in a WebView -- in which case the app crashes + @SuppressWarnings("unused") + CookieSyncManager cookieSyncMngr = + CookieSyncManager.createInstance(context); + CookieManager cookieManager = CookieManager.getInstance(); + cookieManager.removeAllCookie(); + } + + /** + * Parse a server response into a JSON Object. This is a basic + * implementation using org.json.JSONObject representation. More + * sophisticated applications may wish to do their own parsing. + * + * The parsed JSON is checked for a variety of error fields and + * a FacebookException is thrown if an error condition is set, + * populated with the error message and error type or code if + * available. + * + * @param response - string representation of the response + * @return the response as a JSON Object + * @throws JSONException - if the response is not valid JSON + * @throws FacebookError - if an error condition is set + */ + public static JSONObject parseJson(String response) + throws JSONException, FacebookError { + // Edge case: when sending a POST request to /[post_id]/likes + // the return value is 'true' or 'false'. Unfortunately + // these values cause the JSONObject constructor to throw + // an exception. + if (response.equals("false")) { + throw new FacebookError("request failed"); + } + if (response.equals("true")) { + response = "{value : true}"; + } + JSONObject json = new JSONObject(response); + + // errors set by the server are not consistent + // they depend on the method and endpoint + if (json.has("error")) { + JSONObject error = json.getJSONObject("error"); + throw new FacebookError( + error.getString("message"), error.getString("type"), 0); + } + if (json.has("error_code") && json.has("error_msg")) { + throw new FacebookError(json.getString("error_msg"), "", + Integer.parseInt(json.getString("error_code"))); + } + if (json.has("error_code")) { + throw new FacebookError("request failed", "", + Integer.parseInt(json.getString("error_code"))); + } + if (json.has("error_msg")) { + throw new FacebookError(json.getString("error_msg")); + } + if (json.has("error_reason")) { + throw new FacebookError(json.getString("error_reason")); + } + return json; + } + + /** + * Display a simple alert dialog with the given text and title. + * + * @param context + * Android context in which the dialog should be displayed + * @param title + * Alert dialog title + * @param text + * Alert dialog message + */ + public static void showAlert(Context context, String title, String text) { + Builder alertBuilder = new Builder(context); + alertBuilder.setTitle(title); + alertBuilder.setMessage(text); + alertBuilder.create().show(); + } + + /** + * A proxy for Log.d api that kills log messages in release build. It + * not recommended to send sensitive information to log output in + * shipping apps. + * + * @param tag + * @param msg + */ + public static void logd(String tag, String msg) { + if (ENABLE_LOG) { + Log.d(tag, msg); + } + } +} diff --git a/src/android/facebook/res/drawable-hdpi/close.png b/src/android/facebook/res/drawable-hdpi/close.png new file mode 100644 index 000000000..d44c33575 Binary files /dev/null and b/src/android/facebook/res/drawable-hdpi/close.png differ diff --git a/src/android/facebook/res/drawable-hdpi/facebook_icon.png b/src/android/facebook/res/drawable-hdpi/facebook_icon.png new file mode 100644 index 000000000..af8e077ac Binary files /dev/null and b/src/android/facebook/res/drawable-hdpi/facebook_icon.png differ diff --git a/src/android/facebook/res/drawable-ldpi/close.png b/src/android/facebook/res/drawable-ldpi/close.png new file mode 100644 index 000000000..fe4be250e Binary files /dev/null and b/src/android/facebook/res/drawable-ldpi/close.png differ diff --git a/src/android/facebook/res/drawable-ldpi/facebook_icon.png b/src/android/facebook/res/drawable-ldpi/facebook_icon.png new file mode 100644 index 000000000..5bbc2cc91 Binary files /dev/null and b/src/android/facebook/res/drawable-ldpi/facebook_icon.png differ diff --git a/src/android/facebook/res/drawable-xhdpi/close.png b/src/android/facebook/res/drawable-xhdpi/close.png new file mode 100644 index 000000000..e3aff5ae5 Binary files /dev/null and b/src/android/facebook/res/drawable-xhdpi/close.png differ diff --git a/src/android/facebook/res/drawable/close.png b/src/android/facebook/res/drawable/close.png new file mode 100644 index 000000000..ed8b374b1 Binary files /dev/null and b/src/android/facebook/res/drawable/close.png differ diff --git a/src/android/facebook/res/drawable/facebook_icon.png b/src/android/facebook/res/drawable/facebook_icon.png new file mode 100644 index 000000000..413396be6 Binary files /dev/null and b/src/android/facebook/res/drawable/facebook_icon.png differ diff --git a/native/ios/FacebookConnectPlugin.h b/src/ios/FacebookConnectPlugin.h similarity index 100% rename from native/ios/FacebookConnectPlugin.h rename to src/ios/FacebookConnectPlugin.h diff --git a/native/ios/FacebookConnectPlugin.m b/src/ios/FacebookConnectPlugin.m similarity index 100% rename from native/ios/FacebookConnectPlugin.m rename to src/ios/FacebookConnectPlugin.m diff --git a/src/ios/facebook/FBCacheDescriptor.h b/src/ios/facebook/FBCacheDescriptor.h new file mode 100644 index 000000000..ee3333c2e --- /dev/null +++ b/src/ios/facebook/FBCacheDescriptor.h @@ -0,0 +1,42 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBSession.h" + +/*! + @class + + @abstract + Base class from which CacheDescriptors derive, provides a method to fetch data for later use + + @discussion + Cache descriptors allow your application to specify the arguments that will be + later used with another object, such as the FBFriendPickerViewController. By using a cache descriptor + instance, an application can choose to fetch data ahead of the point in time where the data is needed. + */ +@interface FBCacheDescriptor : NSObject + +/*! + @method + @abstract + Fetches and caches the data described by the cache descriptor instance, for the given session. + + @param session the to use for fetching data + */ +- (void)prefetchAndCacheForSession:(FBSession*)session; + +@end diff --git a/src/ios/facebook/FBCacheDescriptor.m b/src/ios/facebook/FBCacheDescriptor.m new file mode 100644 index 000000000..7d9596d3c --- /dev/null +++ b/src/ios/facebook/FBCacheDescriptor.m @@ -0,0 +1,26 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBCacheDescriptor.h" + +@implementation FBCacheDescriptor + +- (void)prefetchAndCacheForSession:(FBSession *)session { + // we are treating this method as abstract virtual here + [self doesNotRecognizeSelector:_cmd]; +} + +@end \ No newline at end of file diff --git a/src/ios/facebook/FBCacheIndex.h b/src/ios/facebook/FBCacheIndex.h new file mode 100644 index 000000000..2d48d2968 --- /dev/null +++ b/src/ios/facebook/FBCacheIndex.h @@ -0,0 +1,73 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import + +@class FBCacheIndex; + +@protocol FBCacheIndexFileDelegate + +@required +// Informs the disk cache to write contents to the specified file. The callback +// should not block and should be executed in order. +- (void) cacheIndex:(FBCacheIndex*)cacheIndex + writeFileWithName:(NSString*)name + data:(NSData*)data; +// Informs the disk cache to delete the specified file. +- (void) cacheIndex:(FBCacheIndex*)cacheIndex + deleteFileWithName:(NSString*)name; + +@end + +@interface FBCacheIndex : NSObject +{ +@private + id _delegate; + + NSCache* _cachedEntries; + + NSUInteger _currentDiskUsage; + NSUInteger _diskCapacity; + + sqlite3* _database; + sqlite3_stmt* _insertStatement; + sqlite3_stmt* _removeByKeyStatement; + sqlite3_stmt* _selectByKeyStatement; + sqlite3_stmt* _selectByKeyFragmentStatement; + sqlite3_stmt* _selectExcludingKeyFragmentStatement; + sqlite3_stmt* _trimStatement; + sqlite3_stmt* _updateStatement; + + dispatch_queue_t _databaseQueue; +} + +- (id)initWithCacheFolder:(NSString*)folderPath; + +@property (assign) id delegate; +@property (nonatomic, readonly) NSUInteger currentDiskUsage; +@property (nonatomic, assign) NSUInteger diskCapacity; +@property (nonatomic, assign) NSUInteger entryCacheCountLimit; +@property (nonatomic, readonly) dispatch_queue_t databaseQueue; + +- (NSString*)fileNameForKey:(NSString*)key; +- (NSString*)storeFileForKey:(NSString*)key withData:(NSData*)data; +- (void)removeEntryForKey:(NSString*)key; +- (void)removeEntries:(NSString*)keyFragment excludingFragment:(BOOL)exclude; + +@end + + diff --git a/src/ios/facebook/FBCacheIndex.m b/src/ios/facebook/FBCacheIndex.m new file mode 100644 index 000000000..dff38b0ec --- /dev/null +++ b/src/ios/facebook/FBCacheIndex.m @@ -0,0 +1,704 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBCacheIndex.h" + +#define CHECK_SQLITE(res, expectedResult, db) { \ + int result = (res); \ + if (result != expectedResult) { \ + NSLog(@"FBCacheIndex: Expecting result %d, actual %d", \ + expectedResult, \ + result); \ + if (db) { \ + NSLog(@"FBCacheIndex: SQLite error: %s", sqlite3_errmsg(db)); \ + } \ + NSCAssert(NO, @""); \ + } \ +} + +#define CHECK_SQLITE_SUCCESS(res, db) CHECK_SQLITE(res, SQLITE_OK, db) +#define CHECK_SQLITE_DONE(res, db) CHECK_SQLITE(res, SQLITE_DONE, db) + +// Number of entries cached to memory +static const NSInteger kDefaultCacheCountLimit = 500; + +static NSString* const cacheFilename = @"cache.db"; +static const char* schema = + "CREATE TABLE IF NOT EXISTS cache_index " + "(uuid TEXT, key TEXT PRIMARY KEY, access_time REAL, file_size INTEGER)"; + +static const char* insertQuery = + "INSERT INTO cache_index VALUES (?, ?, ?, ?)"; + +static const char* updateQuery = + "UPDATE cache_index " + "SET uuid=?, access_time=?, file_size=? " + "WHERE key=?"; + +static const char* selectByKeyQuery = + "SELECT uuid, key, access_time, file_size FROM cache_index WHERE key = ?"; + +static const char* selectByKeyFragmentQuery = + "SELECT uuid, key, access_time, file_size FROM cache_index WHERE key LIKE ?"; + +static const char* selectExcludingKeyFragmentQuery = + "SELECT uuid, key, access_time, file_size FROM cache_index WHERE key NOT LIKE ?"; + +static const char* selectStorageSizeQuery = + "SELECT SUM(file_size) FROM cache_index"; + +static const char* deleteEntryQuery = + "DELETE FROM cache_index WHERE key=?"; + +static const char* trimQuery = + "CREATE TABLE trimmed AS " + "SELECT uuid, key, access_time, file_size, running_total " + "FROM ( " + "SELECT a1.uuid, a1.key, a1.access_time, " + "a1.file_size, SUM(a2.file_size) running_total " + "FROM cache_index a1, cache_index a2 " + "WHERE a1.access_time > a2.access_time OR " + "(a1.access_time = a2.access_time AND a1.uuid = a2.uuid) " + "GROUP BY a1.uuid ORDER BY a1.access_time) rt " + "WHERE rt.running_total <= ?"; + +#pragma mark - C Helpers + +static void initializeStatement( + sqlite3* database, + sqlite3_stmt** statement, + const char* statementText) +{ + if (*statement == nil) { + CHECK_SQLITE_SUCCESS( + sqlite3_prepare_v2(database, statementText, -1, statement, nil), + database + ); + } else { + CHECK_SQLITE_SUCCESS(sqlite3_reset(*statement), database); + } +} + +static void releaseStatement(sqlite3_stmt* statement, sqlite3* database) +{ + if (statement != nil) { + CHECK_SQLITE_SUCCESS(sqlite3_finalize(statement), database); + } +} + +@interface FBCacheEntityInfo : NSObject +{ +@private + NSString* _uuid; + NSString* _key; + CFTimeInterval _accessTime; + NSUInteger _fileSize; + BOOL _dirty; +} + +- (id)initWithKey:(NSString*)key + uuid:(NSString*)uuid + accessTime:(CFTimeInterval)accessTime + fileSize:(NSUInteger)fileSize; + +@property (copy, readonly) NSString* key; +@property (copy, readonly) NSString* uuid; +@property (assign, readonly) CFTimeInterval accessTime; +@property (assign, readonly) NSUInteger fileSize; +@property (assign, getter = isDirty) BOOL dirty; + +- (void)registerAccess; + +@end + +@interface FBCacheIndex() + +- (FBCacheEntityInfo*)_entryForKey:(NSString*)key; +- (void)_fetchCurrentDiskUsage; +- (FBCacheEntityInfo*)_readEntryFromDatabase:(NSString*)key; +- (NSMutableArray*) _readEntriesFromDatabase: (NSString*)keyFragment excludingFragment:(BOOL)exclude; +- (FBCacheEntityInfo*)_createCacheEntityInfo:(sqlite3_stmt*)selectStatement; +- (void)_removeEntryFromDatabaseForKey:(NSString*)key; +- (void)_trimDatabase; +- (void)_updateEntryInDatabaseForKey:(NSString*)key + entry:(FBCacheEntityInfo*)entry; +- (void)_writeEntryInDatabase:(FBCacheEntityInfo*)entry; + +@end + +@implementation FBCacheIndex + +@synthesize delegate = _delegate; +@synthesize currentDiskUsage = _currentDiskUsage; +@synthesize diskCapacity = _diskCapacity; +@synthesize databaseQueue = _databaseQueue; + +#pragma mark - Lifecycle + +- (id)initWithCacheFolder:(NSString*)folderPath +{ + self = [super init]; + if (self) { + NSString* cacheDBFullPath = + [folderPath stringByAppendingPathComponent:cacheFilename]; + + dispatch_queue_t lowPriQueue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); + _databaseQueue = dispatch_queue_create( + "Data Cache queue", + DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(_databaseQueue, lowPriQueue); + + __block BOOL success = YES; + + // TODO: This is really bad if higher layers are going to be + // multi-threaded. And this has to be unblocked. + dispatch_sync( + _databaseQueue, + ^{ + success = (sqlite3_open_v2( + cacheDBFullPath.UTF8String, + &_database, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, + nil) == SQLITE_OK); + + if (success) { + success = (sqlite3_exec( + _database, + schema, + nil, + nil, + nil) == SQLITE_OK); + } + } + ); + + if (!success) { + NSAssert(NO, @"SQL Lite open/exec error."); + [self release]; + return nil; + } + + // Get disk usage asynchronously + dispatch_async(_databaseQueue, ^{ + [self _fetchCurrentDiskUsage]; + }); + + _cachedEntries = [[NSCache alloc] init]; + _cachedEntries.delegate = self; + _cachedEntries.countLimit = kDefaultCacheCountLimit; + } + + return self; +} + +- (void)dealloc { + if (_databaseQueue) { + // Copy these locally so we don't capture self in the block + sqlite3* const db = _database; + sqlite3_stmt* const is = _insertStatement; + sqlite3_stmt* const sbks = _selectByKeyStatement; + sqlite3_stmt* const sbkfs = _selectByKeyFragmentStatement; + sqlite3_stmt* const sekfs = _selectExcludingKeyFragmentStatement; + sqlite3_stmt* const rbks = _removeByKeyStatement; + sqlite3_stmt* const ts = _trimStatement; + sqlite3_stmt* const us = _updateStatement; + dispatch_async(_databaseQueue, ^{ + releaseStatement(is, nil); + releaseStatement(sbks, nil); + releaseStatement(sbkfs, nil); + releaseStatement(sekfs, nil); + releaseStatement(rbks, nil); + releaseStatement(ts, nil); + releaseStatement(us, nil); + + CHECK_SQLITE_SUCCESS(sqlite3_close(db), nil); + }); + + dispatch_release(_databaseQueue); + } + + _cachedEntries.delegate = nil; + [_cachedEntries release]; + [super dealloc]; +} + +#pragma mark - Properties + +- (NSUInteger)entryCacheCountLimit +{ + return _cachedEntries.countLimit; +} + +- (void)setEntryCacheCountLimit:(NSUInteger)entryCacheCountLimit +{ + _cachedEntries.countLimit = entryCacheCountLimit; +} + +#pragma mark - Public + +- (NSString*)fileNameForKey:(NSString*)key +{ + FBCacheEntityInfo* entryInfo = [self _entryForKey:key]; + [entryInfo registerAccess]; + if (entryInfo) { + return [[entryInfo.uuid retain] autorelease]; + } else { + return nil; + } +} + +- (NSString*)storeFileForKey:(NSString*)key withData:(NSData*)data +{ + CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); + NSString* uuidString = + (NSString*)CFUUIDCreateString(kCFAllocatorDefault, uuid); + + CFRelease(uuid); + FBCacheEntityInfo* entry = [[FBCacheEntityInfo alloc] + initWithKey:key + uuid:uuidString + accessTime:0 + fileSize:data.length]; + + [entry registerAccess]; + dispatch_async(_databaseQueue, ^{ + [self _writeEntryInDatabase:entry]; + + _currentDiskUsage += data.length; + if (_currentDiskUsage > _diskCapacity) { + [self _trimDatabase]; + } + }); + + [self.delegate cacheIndex:self writeFileWithName:uuidString data:data]; + + [_cachedEntries setObject:entry forKey:key]; + [entry release]; + + return [uuidString autorelease]; +} + +- (void)removeEntryForKey:(NSString*)key +{ + FBCacheEntityInfo* entry = [self _entryForKey:key]; + entry.dirty = NO; // Removing, so no need to flush to disk + + NSInteger spaceSaved = entry.fileSize; + [_cachedEntries removeObjectForKey:key]; + + dispatch_async(_databaseQueue, ^{ + [self _removeEntryFromDatabaseForKey:key]; + if (_currentDiskUsage >= spaceSaved) { + _currentDiskUsage -= spaceSaved; + } else { + NSAssert(NO, @"Our disk usage is out of whack"); + // This means current disk usage is out of whack - let's re-read + [self _fetchCurrentDiskUsage]; + }; + + [self.delegate cacheIndex:self deleteFileWithName:entry.uuid]; + }); +} + +- (void)removeEntries:(NSString*)keyFragment excludingFragment:(BOOL)exclude +{ + if (keyFragment == nil) { + return; + } + + __block NSMutableArray* entries; + + dispatch_sync(_databaseQueue, ^{ + entries = [self _readEntriesFromDatabase:keyFragment excludingFragment:exclude]; + }); + + for (FBCacheEntityInfo* entry in entries) { + if ([_cachedEntries objectForKey:entry.key] == nil) { + // Adding to the cache since the call to removeEntryForKey will look for the entry and + // try to retrieve it from the DB which will in turn add it to the cache anyways. So + // pre-emptively adding it to the in memory cache saves some DB roundtrips. + // + // This is only done for NSCache entries that don't already exist since replacing the + // old one with the new one will trigger willEvictObject which will try and perform + // a DB write. Since the write is async, we might end up in a weird state. + [_cachedEntries setObject:entry forKey:entry.key]; + } + + [self removeEntryForKey:entry.key]; + } +} + +#pragma mark - NSCache delegate + +- (void)cache:(NSCache*)cache willEvictObject:(id)obj +{ + FBCacheEntityInfo* entryInfo = (FBCacheEntityInfo*)obj; + if (entryInfo.dirty) { + dispatch_async(_databaseQueue, ^{ + [self _writeEntryInDatabase:entryInfo]; + }); + } +} + +#pragma mark - Private + +- (void)_updateEntryInDatabaseForKey:(NSString*)key + entry:(FBCacheEntityInfo*)entry +{ + NSAssert(dispatch_get_current_queue() == _databaseQueue, @""); + initializeStatement(_database, &_updateStatement, updateQuery); + + CHECK_SQLITE_SUCCESS(sqlite3_bind_text( + _updateStatement, + 1, + entry.uuid.UTF8String, + entry.uuid.length, + nil), _database); + + CHECK_SQLITE_SUCCESS(sqlite3_bind_double( + _updateStatement, + 2, + entry.accessTime), _database); + + CHECK_SQLITE_SUCCESS(sqlite3_bind_int( + _updateStatement, + 3, + entry.fileSize), _database); + + CHECK_SQLITE_SUCCESS(sqlite3_bind_text( + _updateStatement, + 4, + entry.key.UTF8String, + entry.key.length, + nil), _database); + + CHECK_SQLITE_DONE(sqlite3_step(_updateStatement), _database); + + entry.dirty = NO; +} + +- (void)_writeEntryInDatabase:(FBCacheEntityInfo*)entry +{ + NSAssert(dispatch_get_current_queue() == _databaseQueue, @""); + + FBCacheEntityInfo* existing = [self _readEntryFromDatabase:entry.key]; + if (existing) { + + // Entry already exists - update the entry + [self _updateEntryInDatabaseForKey:existing.key + entry:entry]; + + if (![existing.uuid isEqualToString:entry.uuid]) { + // The files have changed. Schedule a delete for existing file + [self.delegate cacheIndex:self deleteFileWithName:existing.uuid]; + } + return; + } + + initializeStatement(_database, &_insertStatement, insertQuery); + CHECK_SQLITE_SUCCESS(sqlite3_bind_text( + _insertStatement, + 1, + entry.uuid.UTF8String, + entry.uuid.length, + nil), _database); + + CHECK_SQLITE_SUCCESS(sqlite3_bind_text( + _insertStatement, + 2, + entry.key.UTF8String, + entry.key.length, + nil), _database); + + CHECK_SQLITE_SUCCESS(sqlite3_bind_double( + _insertStatement, + 3, + entry.accessTime), _database); + + CHECK_SQLITE_SUCCESS(sqlite3_bind_int( + _insertStatement, + 4, + entry.fileSize), _database); + + CHECK_SQLITE_DONE(sqlite3_step(_insertStatement), _database); + + entry.dirty = NO; +} + +- (FBCacheEntityInfo*)_readEntryFromDatabase:(NSString*)key +{ + NSAssert(dispatch_get_current_queue() == _databaseQueue, @""); + initializeStatement(_database, &_selectByKeyStatement, selectByKeyQuery); + + CHECK_SQLITE_SUCCESS(sqlite3_bind_text( + _selectByKeyStatement, + 1, + key.UTF8String, + key.length, + nil), _database); + + return [self _createCacheEntityInfo:_selectByKeyStatement]; +} + +- (NSMutableArray*) _readEntriesFromDatabase:(NSString*)keyFragment + excludingFragment:(BOOL)exclude +{ + NSAssert(dispatch_get_current_queue() == _databaseQueue, @""); + + sqlite3_stmt* selectStatement; + const char* query; + if (exclude) { + selectStatement = _selectExcludingKeyFragmentStatement; + query = selectExcludingKeyFragmentQuery; + } else { + selectStatement = _selectByKeyFragmentStatement; + query = selectByKeyFragmentQuery; + } + + initializeStatement(_database, &selectStatement, query); + NSString* wildcardKeyFragment = [NSString stringWithFormat:@"%%%@%%", keyFragment]; + + CHECK_SQLITE_SUCCESS(sqlite3_bind_text( + selectStatement, + 1, + wildcardKeyFragment.UTF8String, + wildcardKeyFragment.length, + nil), _database); + + NSMutableArray *entries = [[[NSMutableArray alloc] init] autorelease]; + FBCacheEntityInfo* entry; + + while ((entry = [self _createCacheEntityInfo:selectStatement]) != nil) { + [entries addObject:entry]; + } + + return entries; +} + +-(FBCacheEntityInfo*)_createCacheEntityInfo:(sqlite3_stmt*)selectStatement +{ + int result = sqlite3_step(selectStatement); + if (result != SQLITE_ROW) { + return nil; + } + + const unsigned char* uuidStr = + sqlite3_column_text(selectStatement, 0); + const unsigned char* key = + sqlite3_column_text(selectStatement, 1); + CFTimeInterval accessTime = + sqlite3_column_double(selectStatement, 2); + NSUInteger fileSize = sqlite3_column_int(selectStatement, 3); + + FBCacheEntityInfo* entry = [[FBCacheEntityInfo alloc] + initWithKey:[NSString + stringWithCString:(const char*)key + encoding:NSUTF8StringEncoding] + uuid:[NSString + stringWithCString:(const char*)uuidStr + encoding:NSUTF8StringEncoding] + accessTime:accessTime + fileSize:fileSize]; + return [entry autorelease]; +} + +- (void)_fetchCurrentDiskUsage +{ + NSAssert(dispatch_get_current_queue() == _databaseQueue, @""); + + sqlite3_stmt* sizeStatement = nil; + initializeStatement(_database, &sizeStatement, selectStorageSizeQuery); + + CHECK_SQLITE(sqlite3_step(sizeStatement), SQLITE_ROW, _database); + _currentDiskUsage = sqlite3_column_int(sizeStatement, 0); + releaseStatement(sizeStatement, _database); +} + +- (FBCacheEntityInfo*)_entryForKey:(NSString*)key +{ + NSAssert(dispatch_get_current_queue() != _databaseQueue, @""); + + __block FBCacheEntityInfo *entryInfo = [_cachedEntries objectForKey:key]; + if (entryInfo == nil) { + // TODO: This is really bad if higher layers are going to be + // multi-threaded. And this has to be unblocked. + dispatch_sync(_databaseQueue, ^{ + entryInfo = [self _readEntryFromDatabase:key]; + }); + + if (entryInfo) { + [_cachedEntries setObject:entryInfo forKey:key]; + } + } + + return entryInfo; +} + +- (void)_removeEntryFromDatabaseForKey:(NSString*)key +{ + NSAssert(dispatch_get_current_queue() == _databaseQueue, @""); + + initializeStatement(_database, &_removeByKeyStatement, deleteEntryQuery); + CHECK_SQLITE_SUCCESS(sqlite3_bind_text( + _removeByKeyStatement, + 1, + key.UTF8String, + key.length, + nil), _database); + + CHECK_SQLITE_DONE(sqlite3_step(_removeByKeyStatement), _database); +} + +- (void)_dropTrimmingTable +{ + sqlite3_stmt* trimCleanStatement = nil; + + static const char* trimDropQuery = "DROP TABLE IF EXISTS trimmed"; + initializeStatement(_database, &trimCleanStatement, trimDropQuery); + + CHECK_SQLITE_DONE(sqlite3_step(trimCleanStatement), _database); + releaseStatement(trimCleanStatement, _database); +} + +- (void)_flushOrphanedFiles +{ + // TODO: #1001434 +} + +// Trimming of cache entries based on LRU eviction policy. +// All the computations are done at the DB level, as follows: +// - create a temporary table 'trimmed', which computes which records need +// purging, based on access time and running total of file size +// - iterate over 'trimmed', clear in-memory cache, queue data files for +// deletion on a background queue +// - batch-remove these entries from the index +// - drop the temporary 'trimmed' table. +- (void)_trimDatabase +{ + NSAssert(dispatch_get_current_queue() == _databaseQueue, @""); + NSAssert(_currentDiskUsage > _diskCapacity, @""); + if (_currentDiskUsage <= _diskCapacity) { + return; + } + + [self _dropTrimmingTable]; + initializeStatement(_database, &_trimStatement, trimQuery); + CHECK_SQLITE_SUCCESS(sqlite3_bind_int( + _trimStatement, + 1, + _currentDiskUsage - _diskCapacity * 0.8), _database); + + CHECK_SQLITE_DONE(sqlite3_step(_trimStatement), _database); + + // Need to re-prep this statement as it's bound to the temporary table + // and can be stored between trims + static const char* trimSelectQuery = + "SELECT uuid, key, file_size FROM trimmed"; + + sqlite3_stmt* trimSelectStatement = nil; + initializeStatement( + _database, + &trimSelectStatement, + trimSelectQuery); + + NSUInteger spaceCleaned = 0; + while (sqlite3_step(trimSelectStatement) == SQLITE_ROW) { + const unsigned char* uuidStr = + sqlite3_column_text(trimSelectStatement, 0); + const unsigned char* keyStr = + sqlite3_column_text(trimSelectStatement, 1); + spaceCleaned += sqlite3_column_int(trimSelectStatement, 2); + + // Remove in-memory cache entry if present + NSString* key = [NSString + stringWithCString:(const char*)keyStr + encoding:NSUTF8StringEncoding]; + + NSString* uuid = [NSString + stringWithCString:(const char*)uuidStr + encoding:NSUTF8StringEncoding]; + + FBCacheEntityInfo* entry = [_cachedEntries objectForKey:key]; + entry.dirty = NO; + [_cachedEntries removeObjectForKey:key]; + + // Delete the file + [self.delegate cacheIndex:self deleteFileWithName:uuid]; + } + + releaseStatement(trimSelectStatement, _database); + + // Batch remove statement + sqlite3_stmt* trimCleanStatement = nil; + static const char* trimCleanQuery = + "DELETE FROM cache_index WHERE key IN (SELECT key from trimmed)"; + + initializeStatement(_database, &trimCleanStatement, trimCleanQuery); + CHECK_SQLITE_DONE(sqlite3_step(trimCleanStatement), _database); + + releaseStatement(trimCleanStatement, _database); + trimCleanStatement = nil; + + _currentDiskUsage -= spaceCleaned; + NSAssert(_currentDiskUsage <= _diskCapacity, @""); + + // Okay to drop the trimming table + [self _dropTrimmingTable]; + [self _flushOrphanedFiles]; +} + +@end + +@implementation FBCacheEntityInfo + +@synthesize accessTime = _accessTime; +@synthesize uuid = _uuid; +@synthesize fileSize = _fileSize; +@synthesize key = _key; +@synthesize dirty = _dirty; + +#pragma mark - Lifecycle + +- (id)initWithKey:(NSString*)key + uuid:(NSString*)uuid + accessTime:(CFTimeInterval)accessTime + fileSize:(NSUInteger)fileSize +{ + self = [super init]; + if (self != nil) { + _key = [key copy]; + _uuid = [uuid copy]; + _accessTime = accessTime; + _fileSize = fileSize; + } + + return self; +} + +- (void)dealloc { + [_uuid release]; + [_key release]; + [super dealloc]; +} + +- (void)registerAccess +{ + _accessTime = CFAbsoluteTimeGetCurrent(); + _dirty = YES; +} + +@end diff --git a/src/ios/facebook/FBConnect.h b/src/ios/facebook/FBConnect.h new file mode 100644 index 000000000..e9e67ddc4 --- /dev/null +++ b/src/ios/facebook/FBConnect.h @@ -0,0 +1,22 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include "Facebook.h" +#include "FBDialog.h" +#include "FBLoginDialog.h" +#include "FBRequest.h" +#include "FBSBJSON.h" \ No newline at end of file diff --git a/src/ios/facebook/FBContentLink.h b/src/ios/facebook/FBContentLink.h new file mode 100644 index 000000000..59f1699b6 --- /dev/null +++ b/src/ios/facebook/FBContentLink.h @@ -0,0 +1,29 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +@interface FBContentLink : NSObject + +@property (readonly, copy) NSURL *targetURL; +@property (readonly, copy) NSArray *actionTypes; +@property (readonly, copy) NSString *source; +@property (readonly, copy) NSArray *ref; +@property (readonly, copy) NSDictionary *originalQueryParameters; + +- (id)initWithURL:(NSURL*)url; + +@end diff --git a/src/ios/facebook/FBContentLink.m b/src/ios/facebook/FBContentLink.m new file mode 100644 index 000000000..05d35861f --- /dev/null +++ b/src/ios/facebook/FBContentLink.m @@ -0,0 +1,62 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBContentLink.h" +#import "FBUtility.h" + +@interface FBContentLink () + +@property (readwrite, copy) NSURL *targetURL; +@property (readwrite, copy) NSArray *actionTypes; +@property (readwrite, copy) NSString *source; +@property (readwrite, copy) NSArray *ref; +@property (readwrite, copy) NSDictionary *originalQueryParameters; + +@end + +@implementation FBContentLink + +@synthesize targetURL; +@synthesize actionTypes; +@synthesize source; +@synthesize ref; +@synthesize originalQueryParameters; + +- (id)initWithURL:(NSURL*)url { + if (self = [super init]) { + NSString *query = [url fragment]; + NSDictionary *params = [FBUtility dictionaryByParsingURLQueryPart:query]; + + self.targetURL = [[[NSURL alloc] initWithString:[params valueForKey:@"target_url"]] autorelease]; + self.actionTypes = [[params valueForKey:@"fb_action_types"] componentsSeparatedByString:@","]; + self.source = [params valueForKey:@"fb_source"]; + self.ref = [[params valueForKey:@"fb_ref"] componentsSeparatedByString:@","]; + self.originalQueryParameters = params; + } + return self; +} + +- (void)dealloc +{ + self.targetURL = nil; + self.actionTypes =nil; + self.source = nil; + self.ref = nil; + self.originalQueryParameters = nil; + [super dealloc]; +} + +@end diff --git a/src/ios/facebook/FBDataDiskCache.h b/src/ios/facebook/FBDataDiskCache.h new file mode 100644 index 000000000..aded6ebb8 --- /dev/null +++ b/src/ios/facebook/FBDataDiskCache.h @@ -0,0 +1,44 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import "FBSession.h" + +@class FBCacheIndex; + +// This is a Disk based cache used internally by Facebook SDK +@interface FBDataDiskCache : NSObject +{ +@private + NSCache* _inMemoryCache; + FBCacheIndex* _cacheIndex; + NSString* _dataCachePath; + + dispatch_queue_t _fileQueue; +} + ++ (FBDataDiskCache*)sharedCache; + +@property (nonatomic, assign) NSUInteger cacheSizeMemory; +@property (nonatomic, readonly) dispatch_queue_t fileQueue; + +- (NSData*)dataForURL:(NSURL*)dataURL; +- (void)setData:(NSData*)data forURL:(NSURL*)url; +- (void)removeDataForUrl:(NSURL*)url; +- (void)removeDataForSession:(FBSession*)session; + +@end diff --git a/src/ios/facebook/FBDataDiskCache.m b/src/ios/facebook/FBDataDiskCache.m new file mode 100644 index 000000000..e7136dc58 --- /dev/null +++ b/src/ios/facebook/FBDataDiskCache.m @@ -0,0 +1,227 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBDataDiskCache.h" +#import "FBCacheIndex.h" + +static const NSUInteger kMaxDataInMemorySize = 1 * 1024 * 1024; // 1MB +static const NSUInteger kMaxDiskCacheSize = 10 * 1024 * 1024; // 10MB + +static NSString* const kDataDiskCachePath = @"DataDiskCache"; +static NSString* const kCacheInfoFile = @"CacheInfo"; +static NSString *const kAccessTokenKey = @"access_token"; + +@interface FBDataDiskCache() +@property (nonatomic, copy) NSString* dataCachePath; +@end + +@implementation FBDataDiskCache + +@synthesize dataCachePath = _dataCachePath; +@synthesize fileQueue = _fileQueue; + +#pragma mark - Lifecycle + +- (id)init +{ + self = [super init]; + if (self) { + NSArray* cacheList = NSSearchPathForDirectoriesInDomains( + NSCachesDirectory, + NSUserDomainMask, + YES); + + NSString* cachePath = [cacheList objectAtIndex:0]; + _dataCachePath = + [[cachePath stringByAppendingPathComponent:kDataDiskCachePath] + copy]; + [[NSFileManager defaultManager] + createDirectoryAtPath:_dataCachePath + withIntermediateDirectories:YES + attributes:nil + error:nil]; + + dispatch_queue_t bgPriQueue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); + _fileQueue = dispatch_queue_create( + "File Cache Queue", + DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(_fileQueue, bgPriQueue); + + _cacheIndex = [[FBCacheIndex alloc] initWithCacheFolder:_dataCachePath]; + _cacheIndex.diskCapacity = kMaxDiskCacheSize; + _cacheIndex.delegate = self; + + _inMemoryCache = [[NSCache alloc] init]; + _inMemoryCache.totalCostLimit = kMaxDataInMemorySize; + } + + return self; +} + +- (void)dealloc +{ + if (_fileQueue) { + dispatch_release(_fileQueue); + } + + [_cacheIndex release]; + [_dataCachePath release]; + [_inMemoryCache release]; + [super dealloc]; +} + ++ (FBDataDiskCache*)sharedCache +{ + static FBDataDiskCache* _instance; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + _instance = [[FBDataDiskCache alloc] init]; + }); + + return _instance; +} + +#pragma mark - Properties + +- (NSUInteger)cacheSizeMemory +{ + return _inMemoryCache.totalCostLimit; +} + +- (void)setCacheSizeMemory:(NSUInteger)cacheSizeMemory +{ + _inMemoryCache.totalCostLimit = cacheSizeMemory; +} + +#pragma mark - FBCacheIndexFileDelegate + +- (void) cacheIndex:(FBCacheIndex*)cacheIndex + writeFileWithName:(NSString*)name + data:(NSData*)data +{ + NSString* filePath = [_dataCachePath stringByAppendingPathComponent:name]; + dispatch_async(_fileQueue, ^{ + [data writeToFile:filePath atomically:YES]; + }); +} + +- (void) cacheIndex:(FBCacheIndex*)cacheIndex + deleteFileWithName:(NSString*)name +{ + NSString* filePath = [_dataCachePath stringByAppendingPathComponent:name]; + dispatch_async(_fileQueue, ^{ + [[NSFileManager defaultManager] + removeItemAtPath:filePath + error:nil]; + }); +} + +#pragma mark - Other Methods + +- (BOOL)_doesFileExist:(NSString*)name +{ + NSString* filePath = [_dataCachePath stringByAppendingPathComponent:name]; + return [[NSFileManager defaultManager] fileExistsAtPath:filePath]; +} + +- (NSData*)dataForURL:(NSURL*)dataURL +{ + // TODO: Synchronize this across threads + NSData* data = nil; + @try { + data = (NSData*)[_inMemoryCache objectForKey:dataURL]; + NSString* fileName = + [_cacheIndex fileNameForKey:dataURL.absoluteString]; + + if (data == nil && fileName != nil) { + // Not in-memory, on-disk only, read in + if ([self _doesFileExist:fileName]) { + NSString* cachePath = + [_dataCachePath stringByAppendingPathComponent:fileName]; + + data = [NSData + dataWithContentsOfFile:cachePath + options:NSDataReadingMappedAlways | NSDataReadingUncached + error:nil]; + + if (data) { + // It is possible that the file doesn't exist + [_inMemoryCache + setObject:data + forKey:dataURL + cost:data.length]; + } + } + } + } @catch (NSException* exception) { + NSLog(@"FBDiskCache error: %@", exception.reason); + } @finally { + return data; + } +} + +- (void)removeDataForUrl:(NSURL*)url +{ + // TODO: Synchronize this across threads + @try { + [_inMemoryCache removeObjectForKey:url]; + [_cacheIndex removeEntryForKey:url.absoluteString]; + } @catch (NSException* exception) { + NSLog(@"FBDiskCache error: %@", exception.reason); + } +} + +- (void)removeDataForSession:(FBSession*)session +{ + if (session == nil) { + return; + } + + // Here we are removing all cache entries that don't have session context + // These are things like images and the like. The thorough way would + // be to maintain refCounts of these entries associated with accessTokens + // and use that to decide which images to delete. However, this might be + // overkill for a cache. Maybe revisit later? + [_cacheIndex removeEntries:kAccessTokenKey excludingFragment:YES]; + + NSString* accessToken = [session accessToken]; + if (accessToken != nil) { + // Here we are removing all cache entries that have this session's access + // token in the url. + [_cacheIndex removeEntries:accessToken excludingFragment:NO]; + } +} + +- (void)setData:(NSData*)data forURL:(NSURL*)url +{ + // TODO: Synchronize this across threads + @try { + [_cacheIndex + storeFileForKey:url.absoluteString + withData:data]; + + [_inMemoryCache + setObject:data + forKey:url + cost:data.length]; + } @catch (NSException* exception) { + NSLog(@"FBDiskCache error: %@", exception.reason); + } +} + +@end diff --git a/src/ios/facebook/FBDialog.h b/src/ios/facebook/FBDialog.h new file mode 100644 index 000000000..1c0218aa6 --- /dev/null +++ b/src/ios/facebook/FBDialog.h @@ -0,0 +1,165 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import + +@protocol FBDialogDelegate; +@class FBFrictionlessRequestSettings; + +/** + * Do not use this interface directly, instead, use dialog in Facebook.h + * + * Facebook dialog interface for start the facebook webView UIServer Dialog. + */ + +@interface FBDialog : UIView { + id _delegate; + NSMutableDictionary *_params; + NSString * _serverURL; + NSURL* _loadingURL; + UIWebView* _webView; + UIActivityIndicatorView* _spinner; + UIButton* _closeButton; + UIInterfaceOrientation _orientation; + BOOL _showingKeyboard; + BOOL _isViewInvisible; + FBFrictionlessRequestSettings* _frictionlessSettings; + + // Ensures that UI elements behind the dialog are disabled. + UIView* _modalBackgroundView; +} + +/** + * The delegate. + */ +@property(nonatomic,assign) id delegate; + +/** + * The parameters. + */ +@property(nonatomic, retain) NSMutableDictionary* params; + +- (NSString *) getStringFromUrl: (NSString*) url needle:(NSString *) needle; + +- (id)initWithURL: (NSString *) loadingURL + params: (NSMutableDictionary *) params + isViewInvisible: (BOOL) isViewInvisible + frictionlessSettings: (FBFrictionlessRequestSettings *) frictionlessSettings + delegate: (id ) delegate; + +/** + * Displays the view with an animation. + * + * The view will be added to the top of the current key window. + */ +- (void)show; + +/** + * Displays the first page of the dialog. + * + * Do not ever call this directly. It is intended to be overriden by subclasses. + */ +- (void)load; + +/** + * Displays a URL in the dialog. + */ +- (void)loadURL:(NSString*)url + get:(NSDictionary*)getParams; + +/** + * Hides the view and notifies delegates of success or cancellation. + */ +- (void)dismissWithSuccess:(BOOL)success animated:(BOOL)animated; + +/** + * Hides the view and notifies delegates of an error. + */ +- (void)dismissWithError:(NSError*)error animated:(BOOL)animated; + +/** + * Subclasses may override to perform actions just prior to showing the dialog. + */ +- (void)dialogWillAppear; + +/** + * Subclasses may override to perform actions just after the dialog is hidden. + */ +- (void)dialogWillDisappear; + +/** + * Subclasses should override to process data returned from the server in a 'fbconnect' url. + * + * Implementations must call dismissWithSuccess:YES at some point to hide the dialog. + */ +- (void)dialogDidSucceed:(NSURL *)url; + +/** + * Subclasses should override to process data returned from the server in a 'fbconnect' url. + * + * Implementations must call dismissWithSuccess:YES at some point to hide the dialog. + */ +- (void)dialogDidCancel:(NSURL *)url; +@end + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/* + *Your application should implement this delegate + */ +@protocol FBDialogDelegate + +@optional + +/** + * Called when the dialog succeeds and is about to be dismissed. + */ +- (void)dialogDidComplete:(FBDialog *)dialog; + +/** + * Called when the dialog succeeds with a returning url. + */ +- (void)dialogCompleteWithUrl:(NSURL *)url; + +/** + * Called when the dialog get canceled by the user. + */ +- (void)dialogDidNotCompleteWithUrl:(NSURL *)url; + +/** + * Called when the dialog is cancelled and is about to be dismissed. + */ +- (void)dialogDidNotComplete:(FBDialog *)dialog; + +/** + * Called when dialog failed to load due to an error. + */ +- (void)dialog:(FBDialog*)dialog didFailWithError:(NSError *)error; + +/** + * Asks if a link touched by a user should be opened in an external browser. + * + * If a user touches a link, the default behavior is to open the link in the Safari browser, + * which will cause your app to quit. You may want to prevent this from happening, open the link + * in your own internal browser, or perhaps warn the user that they are about to leave your app. + * If so, implement this method on your delegate and return NO. If you warn the user, you + * should hold onto the URL and once you have received their acknowledgement open the URL yourself + * using [[UIApplication sharedApplication] openURL:]. + */ +- (BOOL)dialog:(FBDialog*)dialog shouldOpenURLInExternalBrowser:(NSURL *)url; + +@end diff --git a/src/ios/facebook/FBDialog.m b/src/ios/facebook/FBDialog.m new file mode 100644 index 000000000..011645549 --- /dev/null +++ b/src/ios/facebook/FBDialog.m @@ -0,0 +1,682 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#import "FBDialog.h" +#import "FBSBJSON.h" +#import "Facebook.h" +#import "FBFrictionlessRequestSettings.h" +#import "FBUtility.h" + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// global + +static CGFloat kBorderGray[4] = {0.3, 0.3, 0.3, 0.8}; +static CGFloat kBorderBlack[4] = {0.3, 0.3, 0.3, 1}; + +static CGFloat kTransitionDuration = 0.3; + +static CGFloat kPadding = 0; +static CGFloat kBorderWidth = 10; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +static BOOL FBIsDeviceIPad() { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 30200 + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + return YES; + } +#endif + return NO; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation FBDialog + +@synthesize delegate = _delegate, +params = _params; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// private + +- (void)addRoundedRectToPath:(CGContextRef)context rect:(CGRect)rect radius:(float)radius { + CGContextBeginPath(context); + CGContextSaveGState(context); + + if (radius == 0) { + CGContextTranslateCTM(context, CGRectGetMinX(rect), CGRectGetMinY(rect)); + CGContextAddRect(context, rect); + } else { + rect = CGRectOffset(CGRectInset(rect, 0.5, 0.5), 0.5, 0.5); + CGContextTranslateCTM(context, CGRectGetMinX(rect)-0.5, CGRectGetMinY(rect)-0.5); + CGContextScaleCTM(context, radius, radius); + float fw = CGRectGetWidth(rect) / radius; + float fh = CGRectGetHeight(rect) / radius; + + CGContextMoveToPoint(context, fw, fh/2); + CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1); + CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1); + CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1); + CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1); + } + + CGContextClosePath(context); + CGContextRestoreGState(context); +} + +- (void)drawRect:(CGRect)rect fill:(const CGFloat*)fillColors radius:(CGFloat)radius { + CGContextRef context = UIGraphicsGetCurrentContext(); + CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB(); + + if (fillColors) { + CGContextSaveGState(context); + CGContextSetFillColor(context, fillColors); + if (radius) { + [self addRoundedRectToPath:context rect:rect radius:radius]; + CGContextFillPath(context); + } else { + CGContextFillRect(context, rect); + } + CGContextRestoreGState(context); + } + + CGColorSpaceRelease(space); +} + +- (void)strokeLines:(CGRect)rect stroke:(const CGFloat*)strokeColor { + CGContextRef context = UIGraphicsGetCurrentContext(); + CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB(); + + CGContextSaveGState(context); + CGContextSetStrokeColorSpace(context, space); + CGContextSetStrokeColor(context, strokeColor); + CGContextSetLineWidth(context, 1.0); + + { + CGPoint points[] = {{rect.origin.x+0.5, rect.origin.y-0.5}, + {rect.origin.x+rect.size.width, rect.origin.y-0.5}}; + CGContextStrokeLineSegments(context, points, 2); + } + { + CGPoint points[] = {{rect.origin.x+0.5, rect.origin.y+rect.size.height-0.5}, + {rect.origin.x+rect.size.width-0.5, rect.origin.y+rect.size.height-0.5}}; + CGContextStrokeLineSegments(context, points, 2); + } + { + CGPoint points[] = {{rect.origin.x+rect.size.width-0.5, rect.origin.y}, + {rect.origin.x+rect.size.width-0.5, rect.origin.y+rect.size.height}}; + CGContextStrokeLineSegments(context, points, 2); + } + { + CGPoint points[] = {{rect.origin.x+0.5, rect.origin.y}, + {rect.origin.x+0.5, rect.origin.y+rect.size.height}}; + CGContextStrokeLineSegments(context, points, 2); + } + + CGContextRestoreGState(context); + + CGColorSpaceRelease(space); +} + +- (BOOL)shouldRotateToOrientation:(UIInterfaceOrientation)orientation { + if (orientation == _orientation) { + return NO; + } else { + return orientation == UIInterfaceOrientationPortrait + || orientation == UIInterfaceOrientationPortraitUpsideDown + || orientation == UIInterfaceOrientationLandscapeLeft + || orientation == UIInterfaceOrientationLandscapeRight; + } +} + +- (CGAffineTransform)transformForOrientation { + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + if (orientation == UIInterfaceOrientationLandscapeLeft) { + return CGAffineTransformMakeRotation(M_PI*1.5); + } else if (orientation == UIInterfaceOrientationLandscapeRight) { + return CGAffineTransformMakeRotation(M_PI/2); + } else if (orientation == UIInterfaceOrientationPortraitUpsideDown) { + return CGAffineTransformMakeRotation(-M_PI); + } else { + return CGAffineTransformIdentity; + } +} + +- (void)sizeToFitOrientation:(BOOL)transform { + if (transform) { + self.transform = CGAffineTransformIdentity; + } + + CGRect frame = [UIScreen mainScreen].applicationFrame; + CGPoint center = CGPointMake( + frame.origin.x + ceil(frame.size.width/2), + frame.origin.y + ceil(frame.size.height/2)); + + CGFloat scale_factor = 1.0f; + if (FBIsDeviceIPad()) { + // On the iPad the dialog's dimensions should only be 60% of the screen's + scale_factor = 0.6f; + } + + CGFloat width = floor(scale_factor * frame.size.width) - kPadding * 2; + CGFloat height = floor(scale_factor * frame.size.height) - kPadding * 2; + + _orientation = [UIApplication sharedApplication].statusBarOrientation; + if (UIInterfaceOrientationIsLandscape(_orientation)) { + self.frame = CGRectMake(kPadding, kPadding, height, width); + } else { + self.frame = CGRectMake(kPadding, kPadding, width, height); + } + self.center = center; + + if (transform) { + self.transform = [self transformForOrientation]; + } +} + +- (void)updateWebOrientation { + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + if (UIInterfaceOrientationIsLandscape(orientation)) { + [_webView stringByEvaluatingJavaScriptFromString: + @"document.body.setAttribute('orientation', 90);"]; + } else { + [_webView stringByEvaluatingJavaScriptFromString: + @"document.body.removeAttribute('orientation');"]; + } +} + +- (void)bounce1AnimationStopped { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:kTransitionDuration/2]; + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(bounce2AnimationStopped)]; + self.transform = CGAffineTransformScale([self transformForOrientation], 0.9, 0.9); + [UIView commitAnimations]; +} + +- (void)bounce2AnimationStopped { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:kTransitionDuration/2]; + self.transform = [self transformForOrientation]; + [UIView commitAnimations]; +} + +- (NSURL*)generateURL:(NSString*)baseURL params:(NSDictionary*)params { + if (params) { + NSMutableArray* pairs = [NSMutableArray array]; + for (NSString* key in params.keyEnumerator) { + NSString* value = [params objectForKey:key]; + NSString* escaped_value = [FBUtility stringByURLEncodingString:value]; + [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, escaped_value]]; + } + + NSString* query = [pairs componentsJoinedByString:@"&"]; + NSString* url = [NSString stringWithFormat:@"%@?%@", baseURL, query]; + return [NSURL URLWithString:url]; + } else { + return [NSURL URLWithString:baseURL]; + } +} + +- (void)addObservers { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(deviceOrientationDidChange:) + name:@"UIDeviceOrientationDidChangeNotification" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillShow:) name:@"UIKeyboardWillShowNotification" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillHide:) name:@"UIKeyboardWillHideNotification" object:nil]; +} + +- (void)removeObservers { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:@"UIDeviceOrientationDidChangeNotification" object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:@"UIKeyboardWillShowNotification" object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:@"UIKeyboardWillHideNotification" object:nil]; +} + +- (void)postDismissCleanup { + [self removeObservers]; + [self removeFromSuperview]; + [_modalBackgroundView removeFromSuperview]; +} + +- (void)dismiss:(BOOL)animated { + [self dialogWillDisappear]; + + // If the dialog has been closed, then we need to cancel the order to open it. + // This happens in the case of a frictionless request, see webViewDidFinishLoad for details + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(showWebView) + object:nil]; + + [_loadingURL release]; + _loadingURL = nil; + + if (animated) { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:kTransitionDuration]; + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(postDismissCleanup)]; + self.alpha = 0; + [UIView commitAnimations]; + } else { + [self postDismissCleanup]; + } +} + +- (void)cancel { + [self dialogDidCancel:nil]; +} + +- (BOOL)testBoolUrlParam:(NSURL *)url param:(NSString *)param { + NSString *paramVal = [self getStringFromUrl: [url absoluteString] + needle: param]; + return [paramVal boolValue]; +} + +- (void)dialogSuccessHandleFrictionlessResponses:(NSURL *)url { + // did we receive a recipient list? + NSString *recipientJson = [self getStringFromUrl:[url absoluteString] + needle:@"frictionless_recipients="]; + if (recipientJson) { + // if value parses as an array, treat as set of fbids + FBSBJsonParser *parser = [[[FBSBJsonParser alloc] + init] + autorelease]; + id recipients = [parser objectWithString:recipientJson]; + + // if we got something usable, copy the ids out and update the cache + if ([recipients isKindOfClass:[NSArray class]]) { + NSMutableArray *ids = [[[NSMutableArray alloc] + initWithCapacity:[recipients count]] + autorelease]; + for (id recipient in recipients) { + NSString *fbid = [NSString stringWithFormat:@"%@", recipient]; + [ids addObject:fbid]; + } + // we may be tempted to terminate outstanding requests before this + // point, but that would cause problems if the user cancelled a dialog + [_frictionlessSettings updateRecipientCacheWithRecipients:ids]; + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// NSObject + +- (id)init { + if ((self = [super initWithFrame:CGRectZero])) { + _delegate = nil; + _loadingURL = nil; + _showingKeyboard = NO; + + self.backgroundColor = [UIColor clearColor]; + self.autoresizesSubviews = YES; + self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.contentMode = UIViewContentModeRedraw; + + _webView = [[UIWebView alloc] initWithFrame:CGRectMake(kPadding, kPadding, 480, 480)]; + _webView.delegate = self; + _webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:_webView]; + + UIImage* closeImage = [UIImage imageNamed:@"FacebookSDKResources.bundle/FBDialog/images/close.png"]; + + UIColor* color = [UIColor colorWithRed:167.0/255 green:184.0/255 blue:216.0/255 alpha:1]; + _closeButton = [[UIButton buttonWithType:UIButtonTypeCustom] retain]; + [_closeButton setImage:closeImage forState:UIControlStateNormal]; + [_closeButton setTitleColor:color forState:UIControlStateNormal]; + [_closeButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; + [_closeButton addTarget:self action:@selector(cancel) + forControlEvents:UIControlEventTouchUpInside]; + + // To be compatible with OS 2.x +#if __IPHONE_OS_VERSION_MAX_ALLOWED <= __IPHONE_2_2 + _closeButton.font = [UIFont boldSystemFontOfSize:12]; +#else + _closeButton.titleLabel.font = [UIFont boldSystemFontOfSize:12]; +#endif + + _closeButton.showsTouchWhenHighlighted = YES; + _closeButton.autoresizingMask = UIViewAutoresizingFlexibleRightMargin + | UIViewAutoresizingFlexibleBottomMargin; + [self addSubview:_closeButton]; + + _spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle: + UIActivityIndicatorViewStyleWhiteLarge]; + if ([_spinner respondsToSelector:@selector(setColor:)]) { + [_spinner setColor:[UIColor grayColor]]; + } else { + [_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleGray]; + } + _spinner.autoresizingMask = + UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin + | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [self addSubview:_spinner]; + _modalBackgroundView = [[UIView alloc] init]; + } + return self; +} + +- (void)dealloc { + _webView.delegate = nil; + [_webView release]; + [_params release]; + [_serverURL release]; + [_spinner release]; + [_closeButton release]; + [_loadingURL release]; + [_modalBackgroundView release]; + [_frictionlessSettings release]; + [super dealloc]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// UIView + +- (void)drawRect:(CGRect)rect { + [self drawRect:rect fill:kBorderGray radius:0]; + + CGRect webRect = CGRectMake( + ceil(rect.origin.x + kBorderWidth), ceil(rect.origin.y + kBorderWidth)+1, + rect.size.width - kBorderWidth*2, _webView.frame.size.height+1); + + [self strokeLines:webRect stroke:kBorderBlack]; +} + +// Display the dialog's WebView with a slick pop-up animation +- (void)showWebView { + UIWindow* window = [UIApplication sharedApplication].keyWindow; + if (!window) { + window = [[UIApplication sharedApplication].windows objectAtIndex:0]; + } + _modalBackgroundView.frame = window.frame; + [_modalBackgroundView addSubview:self]; + [window addSubview:_modalBackgroundView]; + + self.transform = CGAffineTransformScale([self transformForOrientation], 0.001, 0.001); + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:kTransitionDuration/1.5]; + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:@selector(bounce1AnimationStopped)]; + self.transform = CGAffineTransformScale([self transformForOrientation], 1.1, 1.1); + [UIView commitAnimations]; + + [self dialogWillAppear]; + [self addObservers]; +} + +// Show a spinner during the loading time for the dialog. This is designed to show +// on top of the webview but before the contents have loaded. +- (void)showSpinner { + [_spinner sizeToFit]; + [_spinner startAnimating]; + _spinner.center = _webView.center; +} + +- (void)hideSpinner { + [_spinner stopAnimating]; + _spinner.hidden = YES; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// UIWebViewDelegate + +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request + navigationType:(UIWebViewNavigationType)navigationType { + NSURL* url = request.URL; + + if ([url.scheme isEqualToString:@"fbconnect"]) { + if ([[url.resourceSpecifier substringToIndex:8] isEqualToString:@"//cancel"]) { + NSString * errorCode = [self getStringFromUrl:[url absoluteString] needle:@"error_code="]; + NSString * errorStr = [self getStringFromUrl:[url absoluteString] needle:@"error_msg="]; + if (errorCode) { + NSDictionary * errorData = [NSDictionary dictionaryWithObject:errorStr forKey:@"error_msg"]; + NSError * error = [NSError errorWithDomain:@"facebookErrDomain" + code:[errorCode intValue] + userInfo:errorData]; + [self dismissWithError:error animated:YES]; + } else { + [self dialogDidCancel:url]; + } + } else { + if (_frictionlessSettings.enabled) { + [self dialogSuccessHandleFrictionlessResponses:url]; + } + [self dialogDidSucceed:url]; + } + return NO; + } else if ([_loadingURL isEqual:url]) { + return YES; + } else if (navigationType == UIWebViewNavigationTypeLinkClicked) { + if ([_delegate respondsToSelector:@selector(dialog:shouldOpenURLInExternalBrowser:)]) { + if (![_delegate dialog:self shouldOpenURLInExternalBrowser:url]) { + return NO; + } + } + + [[UIApplication sharedApplication] openURL:request.URL]; + return NO; + } else { + return YES; + } +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView { + if (_isViewInvisible) { + // if our cache asks us to hide the view, then we do, but + // in case of a stale cache, we will display the view in a moment + // note that showing the view now would cause a visible white + // flash in the common case where the cache is up to date + [self performSelector:@selector(showWebView) withObject:nil afterDelay:.05]; + } else { + [self hideSpinner]; + } + [self updateWebOrientation]; +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { + // 102 == WebKitErrorFrameLoadInterruptedByPolicyChange + // -999 == "Operation could not be completed", note -999 occurs when the user clicks away before + // the page has completely loaded, if we find cases where we want this to result in dialog failure + // (usually this just means quick-user), then we should add something more robust here to account + // for differences in application needs + if (!(([error.domain isEqualToString:@"NSURLErrorDomain"] && error.code == -999) || + ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102))) { + [self dismissWithError:error animated:YES]; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// UIDeviceOrientationDidChangeNotification + +- (void)deviceOrientationDidChange:(void*)object { + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + if (!_showingKeyboard && [self shouldRotateToOrientation:orientation]) { + [self updateWebOrientation]; + + CGFloat duration = [UIApplication sharedApplication].statusBarOrientationAnimationDuration; + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:duration]; + [self sizeToFitOrientation:YES]; + [UIView commitAnimations]; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// UIKeyboardNotifications + +- (void)keyboardWillShow:(NSNotification*)notification { + + _showingKeyboard = YES; + + if (FBIsDeviceIPad()) { + // On the iPad the screen is large enough that we don't need to + // resize the dialog to accomodate the keyboard popping up + return; + } + + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + if (UIInterfaceOrientationIsLandscape(orientation)) { + _webView.frame = CGRectInset(_webView.frame, + -(kPadding + kBorderWidth), + -(kPadding + kBorderWidth)); + } +} + +- (void)keyboardWillHide:(NSNotification*)notification { + _showingKeyboard = NO; + + if (FBIsDeviceIPad()) { + return; + } + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + if (UIInterfaceOrientationIsLandscape(orientation)) { + _webView.frame = CGRectInset(_webView.frame, + kPadding + kBorderWidth, + kPadding + kBorderWidth); + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// public + +/** + * Find a specific parameter from the url + */ +- (NSString *) getStringFromUrl: (NSString*) url needle:(NSString *) needle { + NSString * str = nil; + NSRange start = [url rangeOfString:needle]; + if (start.location != NSNotFound) { + // confirm that the parameter is not a partial name match + unichar c = '?'; + if (start.location != 0) { + c = [url characterAtIndex:start.location - 1]; + } + if (c == '?' || c == '&' || c == '#') { + NSRange end = [[url substringFromIndex:start.location+start.length] rangeOfString:@"&"]; + NSUInteger offset = start.location+start.length; + str = end.location == NSNotFound ? + [url substringFromIndex:offset] : + [url substringWithRange:NSMakeRange(offset, end.location)]; + str = [str stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + } + } + return str; +} + +- (id)initWithURL: (NSString *) serverURL + params: (NSMutableDictionary *) params + isViewInvisible: (BOOL)isViewInvisible + frictionlessSettings: (FBFrictionlessRequestSettings*) frictionlessSettings + delegate: (id ) delegate { + + self = [self init]; + _serverURL = [serverURL retain]; + _params = [params retain]; + _delegate = delegate; + _isViewInvisible = isViewInvisible; + _frictionlessSettings = [frictionlessSettings retain]; + + return self; +} + +- (void)load { + [self loadURL:_serverURL get:_params]; +} + +- (void)loadURL:(NSString*)url get:(NSDictionary*)getParams { + + [_loadingURL release]; + _loadingURL = [[self generateURL:url params:getParams] retain]; + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:_loadingURL]; + + [_webView loadRequest:request]; +} + +- (void)show { + [self load]; + [self sizeToFitOrientation:NO]; + + CGFloat innerWidth = self.frame.size.width - (kBorderWidth+1)*2; + [_closeButton sizeToFit]; + + _closeButton.frame = CGRectMake( + 2, + 2, + 29, + 29); + + _webView.frame = CGRectMake( + kBorderWidth+1, + kBorderWidth+1, + innerWidth, + self.frame.size.height - (1 + kBorderWidth*2)); + + if (!_isViewInvisible) { + [self showSpinner]; + [self showWebView]; + } +} + +- (void)dismissWithSuccess:(BOOL)success animated:(BOOL)animated { + if (success) { + if ([_delegate respondsToSelector:@selector(dialogDidComplete:)]) { + [_delegate dialogDidComplete:self]; + } + } else { + if ([_delegate respondsToSelector:@selector(dialogDidNotComplete:)]) { + [_delegate dialogDidNotComplete:self]; + } + } + + [self dismiss:animated]; +} + +- (void)dismissWithError:(NSError*)error animated:(BOOL)animated { + if ([_delegate respondsToSelector:@selector(dialog:didFailWithError:)]) { + [_delegate dialog:self didFailWithError:error]; + } + + [self dismiss:animated]; +} + +- (void)dialogWillAppear { +} + +- (void)dialogWillDisappear { +} + +- (void)dialogDidSucceed:(NSURL *)url { + + if ([_delegate respondsToSelector:@selector(dialogCompleteWithUrl:)]) { + [_delegate dialogCompleteWithUrl:url]; + } + [self dismissWithSuccess:YES animated:YES]; +} + +- (void)dialogDidCancel:(NSURL *)url { + if ([_delegate respondsToSelector:@selector(dialogDidNotCompleteWithUrl:)]) { + [_delegate dialogDidNotCompleteWithUrl:url]; + } + [self dismissWithSuccess:NO animated:YES]; +} + +@end diff --git a/src/ios/facebook/FBError.h b/src/ios/facebook/FBError.h new file mode 100644 index 000000000..5053bdea2 --- /dev/null +++ b/src/ios/facebook/FBError.h @@ -0,0 +1,123 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +/// The error domain of all error codes returned by the Facebook SDK +extern NSString *const FacebookSDKDomain; + +// ---------------------------------------------------------------------------- +// Keys in the userInfo NSDictionary of NSError where you can find additional +// information about the error. All are optional. + +/// The key for an inner NSError. +extern NSString *const FBErrorInnerErrorKey; + +/// The key for parsed JSON response from the server. In case of a batch, +/// includes the JSON for a single FBRequest. +extern NSString *const FBErrorParsedJSONResponseKey; + +/// The key for HTTP status code. +extern NSString *const FBErrorHTTPStatusCodeKey; + +// ---------------------------------------------------------------------------- +/*! + @abstract Error codes returned by the Facebook SDK in NSError. + + @discussion + These are valid only in the scope of FacebookSDKDomain. + */ +typedef enum FBErrorCode { + /*! + Like nil for FBErrorCode values, represents an error code that + has not been initialized yet. + */ + FBErrorInvalid = 0, + + /// The operation failed because it was cancelled. + FBErrorOperationCancelled, + + /// A login attempt failed + FBErrorLoginFailedOrCancelled, + + /// The graph API returned an error for this operation. + FBErrorRequestConnectionApi, + + /*! + The operation failed because the server returned an unexpected + response. You can get this error if you are not using the most + recent SDK, or if you set your application's migration settings + incorrectly for the version of the SDK you are using. + + If this occurs on the current SDK with proper app migration + settings, you may need to try changing to one request per batch. + */ + FBErrorProtocolMismatch, + + /// Non-success HTTP status code was returned from the operation. + FBErrorHTTPError, + + /// An endpoint that returns a binary response was used with FBRequestConnection; + /// endpoints that return image/jpg, etc. should be accessed using NSURLRequest + FBErrorNonTextMimeTypeReturned, + + /// An error occurred while trying to display a native dialog + FBErrorNativeDialog, +} FBErrorCode; + +/*! + The key in the userInfo NSDictionary of NSError where you can find + the inner NSError (if any). + */ +extern NSString *const FBErrorInnerErrorKey; + +/// The NSError key used by session to capture login failure reason +extern NSString *const FBErrorLoginFailedReason; + +/// the NSError key used by session to capture login failure error code +extern NSString *const FBErrorLoginFailedOriginalErrorCode; + +/// used by session when an inline dialog fails +extern NSString *const FBErrorLoginFailedReasonInlineCancelledValue; +extern NSString *const FBErrorLoginFailedReasonInlineNotCancelledValue; +extern NSString *const FBErrorLoginFailedReasonUnitTestResponseUnrecognized; + +/// used by session when a reauthorize fails +extern NSString *const FBErrorReauthorizeFailedReasonSessionClosed; +extern NSString *const FBErrorReauthorizeFailedReasonUserCancelled; +extern NSString *const FBErrorReauthorizeFailedReasonWrongUser; + +/// The key to retrieve the reason for a native dialog error +extern NSString *const FBErrorNativeDialogReasonKey; + +/// indicates that a native dialog is not supported in the current OS +extern NSString *const FBErrorNativeDialogNotSupported; +/// indicates that a native dialog can't be displayed because it is not appropriate for the current session +extern NSString *const FBErrorNativeDialogInvalidForSession; +/// indicates that a native dialog can't be displayed for some other reason +extern NSString *const FBErrorNativeDialogCantBeDisplayed; + +// Exception strings raised by the Facebook SDK + +/*! + This exception is raised by methods in the Facebook SDK to indicate + that an attempted operation is invalid + */ +extern NSString *const FBInvalidOperationException; + +// Facebook SDK also raises exceptions the following common exceptions: +// NSInvalidArgumentException + diff --git a/src/ios/facebook/FBError.m b/src/ios/facebook/FBError.m new file mode 100644 index 000000000..52532f277 --- /dev/null +++ b/src/ios/facebook/FBError.m @@ -0,0 +1,36 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBError.h" + +NSString *const FacebookSDKDomain = @"com.facebook.sdk"; +NSString *const FBErrorInnerErrorKey = @"com.facebook.sdk:ErrorInnerErrorKey"; +NSString *const FBErrorParsedJSONResponseKey = @"com.facebook.sdk:ParsedJSONResponseKey"; +NSString *const FBErrorHTTPStatusCodeKey = @"com.facebook.sdk:HTTPStatusCode"; + +NSString *const FBErrorLoginFailedReason = @"com.facebook.sdk:ErrorLoginFailedReason"; +NSString *const FBErrorLoginFailedOriginalErrorCode = @"com.facebook.sdk:ErrorLoginFailedOriginalErrorCode"; + +NSString *const FBErrorReauthorizeFailedReasonSessionClosed = @"com.facebook.sdk:ErrorReauthorizeFailedReasonSessionClosed"; +NSString *const FBErrorReauthorizeFailedReasonUserCancelled = @"com.facebook.sdk:ErrorReauthorizeFailedReasonUserCancelled"; +NSString *const FBErrorReauthorizeFailedReasonWrongUser = @"com.facebook.sdk:ErrorReauthorizeFailedReasonWrongUser"; + +NSString *const FBInvalidOperationException = @"com.facebook.sdk:InvalidOperationException"; + +NSString *const FBErrorNativeDialogReasonKey = @"com.facebook.sdk:NativeDialogReasonKey"; +NSString *const FBErrorNativeDialogNotSupported = @"com.facebook.sdk:NativeDialogNotSupported"; +NSString *const FBErrorNativeDialogInvalidForSession = @"NativeDialogInvalidForSession"; +NSString *const FBErrorNativeDialogCantBeDisplayed = @"NativeDialogCantBeDisplayed"; diff --git a/src/ios/facebook/FBFrictionlessRequestSettings.h b/src/ios/facebook/FBFrictionlessRequestSettings.h new file mode 100644 index 000000000..b905ea519 --- /dev/null +++ b/src/ios/facebook/FBFrictionlessRequestSettings.h @@ -0,0 +1,74 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +@class Facebook; + +/** + * Do not use this interface directly, instead, use methods in Facebook.h + * + * Handles frictionless interaction and recipient-caching by the SDK, + * see https://developers.facebook.com/docs/reference/dialogs/requests/ + */ +@interface FBFrictionlessRequestSettings : NSObject { +@private + NSArray* _allowedRecipients; + FBRequest* _activeRequest; + BOOL _enabled; +} + +/** + * BOOL indicating whether frictionless request sending has been enabled + */ +@property(nonatomic, readonly) BOOL enabled; + +/** + * Enable frictionless request sending by the sdk; this means: + * 1. query and cache the current set of frictionless recipients + * 2. flag other facets of the sdk to behave in a frictionless way + */ +- (void)enableWithFacebook:(Facebook*)facebook; + +/** + * Reload recipient cache; called by the sdk to keep the cache fresh; + * method makes graph request: me/apprequestformerrecipients + */ +- (void)reloadRecipientCacheWithFacebook:(Facebook*)facebook; + +/** + * Update the recipient cache; called by the sdk to keep the cache fresh; + */ +- (void)updateRecipientCacheWithRecipients:(NSArray*)ids; + +/** + * Given an fbID for a user, indicates whether user is enabled for + * frictionless calls + */ +- (BOOL)isFrictionlessEnabledForRecipient:(id)fbid; + +/** + * Given an array of user fbIDs, indicates whether they are enabled for + * frictionless calls + */ +- (BOOL)isFrictionlessEnabledForRecipients:(NSArray*)fbids; + +/** + * init the frictionless cache object + */ +- (id)init; + +@end diff --git a/src/ios/facebook/FBFrictionlessRequestSettings.m b/src/ios/facebook/FBFrictionlessRequestSettings.m new file mode 100644 index 000000000..c388feb98 --- /dev/null +++ b/src/ios/facebook/FBFrictionlessRequestSettings.m @@ -0,0 +1,162 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Facebook.h" +#import "FBFrictionlessRequestSettings.h" + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// private interface +// +@interface FBFrictionlessRequestSettings () + +@property (readwrite, retain) NSArray * allowedRecipients; +@property (readwrite, retain) FBRequest* activeRequest; + +@end + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation FBFrictionlessRequestSettings + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// public + +@synthesize enabled = _enabled; + +- (id)init { + if (self = [super init]) { + // start life with an empty frictionless cache + self.allowedRecipients = [[[NSArray alloc] init] autorelease]; + } + return self; +} + +- (void)enableWithFacebook:(Facebook*)facebook { + if (!_enabled) { + _enabled = YES; + [self reloadRecipientCacheWithFacebook:facebook]; + } +} + +- (void)reloadRecipientCacheWithFacebook:(Facebook *)facebook { + // request the list of frictionless recipients from the server + id request = [facebook requestWithGraphPath:@"me/apprequestformerrecipients" + andDelegate:self]; + if (request) { + self.activeRequest = request; + } +} + +- (void)updateRecipientCacheWithRecipients:(NSArray*)ids { + // if setting recipients directly, no need to complete pending request + self.activeRequest = nil; + + if (ids == nil) { + self.allowedRecipients = [[[NSArray alloc] init] autorelease]; + } else { + self.allowedRecipients = [[[NSArray alloc] initWithArray:ids] autorelease]; + } +} + +- (BOOL)isFrictionlessEnabledForRecipient:(NSString *)fbid { + // trim whitespace from edges + fbid = [fbid stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceCharacterSet]]; + + // linear search through cache for a match + for (NSString *entry in self.allowedRecipients) { + if ([entry isEqualToString:fbid]) { + return YES; + } + } + return NO; +} + +- (BOOL)isFrictionlessEnabledForRecipients:(NSArray*)fbids { + // we handle arrays of NSString and NSNumber, and throw on anything else + for (id fbid in fbids) { + NSString* fbidstr; + // give us a number, and we convert it to a string + if ([fbid isKindOfClass:[NSNumber class]]) { + fbidstr = [(NSNumber*)fbid stringValue]; + } else if ([fbid isKindOfClass:[NSString class]]) { + // or give us a string, and we just use it as is + fbidstr = (NSString*)fbid; + } else { + // unexpected type found in the array of fbids + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:@"items in fbids must be NSString or NSNumber" + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + [fbid class], @"invalid class", + nil]]; + } + + // if we miss our cache once, we fail the set + if (![self isFrictionlessEnabledForRecipient:fbidstr]) { + return NO; + } + } + return YES; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// FBRequestDelegate + +- (void)request:(FBRequest *)request + didLoad:(id)result { + + // a little request bookkeeping + self.activeRequest = nil; + + int items = [[result objectForKey: @"data"] count]; + NSMutableArray* recipients = [[[NSMutableArray alloc] initWithCapacity: items] autorelease]; + + for (int i = 0; i < items; i++) { + [recipients addObject: [[[result objectForKey: @"data"] + objectAtIndex: i] + objectForKey: @"recipient_id"]] ; + } + + self.allowedRecipients = recipients; +} + +- (void)request:(FBRequest *)request didFailWithError:(NSError *)error { + // if the request to load the frictionless recipients fails, proceed without updating + // the recipients cache; the cache may become invalid due to a failed update or other reasons + // (e.g. simultaneous use of the same app from multiple devices), in the case of an invalid + // cache, a request dialog may either appear a moment later than it usually would, or appear + // briefly when it should not appear at all; in either case the correct request behavior + // occurs, and the cache is updated + self.activeRequest = nil; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// NSObject + +- (void)dealloc { + self.activeRequest = nil; + self.allowedRecipients = nil; + [super dealloc]; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// private helpers +// + +@synthesize allowedRecipients = _allowedRecipients; +@synthesize activeRequest = _activeRequest; + +@end diff --git a/src/ios/facebook/FBFriendPickerCacheDescriptor.h b/src/ios/facebook/FBFriendPickerCacheDescriptor.h new file mode 100644 index 000000000..35d011745 --- /dev/null +++ b/src/ios/facebook/FBFriendPickerCacheDescriptor.h @@ -0,0 +1,91 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBCacheDescriptor.h" + +/* + @class + + @abstract + Represents the data needed by an FBFriendPickerViewController, in order to construct + the necessary request to populate the view; instances of FBFriendPickerCacheDescriptor + are used to fetch data ahead of the point when the data is used to populate a display. + + @discussion + A common use of an FBFriendPickerCacheDescriptor instance, is to allocate an instance + at the point when a session is opened, and then call prefetchAndCacheForSession. This + causes the API to fetch and cache the data needed by the FBFriendPickerViewController. + If at some point the user goes to select friends, the FBFriendPickerViewController + will first check the cache for a copy of the friends list, and then after displaying + whatever cached data is available, then it will fetch a fresh copy of the friends list. + + @unsorted + */ +@interface FBFriendPickerCacheDescriptor : FBCacheDescriptor + +/* + @method + + @abstract + Initializes an instance with default values for populating + a FBFriendPickerViewController, at some later point. +*/ +- (id)init; + +/* + @method + + @abstract + Initializes an instance specifying the userID to use for populating + a FBFriendPickerViewController, at some later point. +*/ +- (id)initWithUserID:(NSString*)userID; + +/* + @method + + @abstract + Initializes an instance specifying the fields to use for populating + a FBFriendPickerViewController, at some later point. +*/ +- (id)initWithFieldsForRequest:(NSSet*)fieldsForRequest; + +/* + @method + + @abstract + Initializes an instance specifying the userID and fields to use for populating + a FBFriendPickerViewController, at some later point. + + @param userID fbid of the user whose friends we wish to display; nil='me' + @param fieldsForRequest set of additional fields to include in request for friends + */ +- (id)initWithUserID:(NSString*)userID fieldsForRequest:(NSSet*)fieldsForRequest; + +/* + @abstract + Fields to use when fetching data for the view + */ +@property (nonatomic, readonly, copy) NSSet *fieldsForRequest; + +/* + @abstract + Indicates the fbid of the user whose friends are being viewed + */ +@property (nonatomic, readonly, copy) NSString *userID; + +@end diff --git a/src/ios/facebook/FBFriendPickerCacheDescriptor.m b/src/ios/facebook/FBFriendPickerCacheDescriptor.m new file mode 100644 index 000000000..1039fffce --- /dev/null +++ b/src/ios/facebook/FBFriendPickerCacheDescriptor.m @@ -0,0 +1,137 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBFriendPickerCacheDescriptor.h" +#import "FBFriendPickerViewController+Internal.h" +#import "FBSession.h" +#import "FBRequest.h" +#import "FBRequestConnection.h" +#import "FBGraphObjectTableDataSource.h" +#import "FBGraphObjectPagingLoader.h" + +@interface FBFriendPickerCacheDescriptor () + +@property (nonatomic, readwrite, copy) NSSet *fieldsForRequest; +@property (nonatomic, readwrite, copy) NSString *userID; +@property (nonatomic, readwrite, retain) FBGraphObjectPagingLoader *loader; + +// these properties are only used by unit tests, and should not be removed or made public +@property (nonatomic, readwrite, assign) BOOL hasCompletedFetch; +@property (nonatomic, readwrite, assign) BOOL usePageLimitOfOne; +- (void)setUsePageLimitOfOne; + +@end + +@implementation FBFriendPickerCacheDescriptor + +@synthesize fieldsForRequest = _fieldsForRequest, + userID = _userID, + loader = _loader, + hasCompletedFetch = _hasCompletedFetch, + usePageLimitOfOne = _usePageLimitOfOne; + +- (id)init { + return [self initWithUserID:nil + fieldsForRequest:nil]; +} + +- (id)initWithUserID:(NSString*)userID { + return [self initWithUserID:userID + fieldsForRequest:nil]; +} + +- (id)initWithFieldsForRequest:(NSSet*)fieldsForRequest { + return [self initWithUserID:nil + fieldsForRequest:fieldsForRequest]; +} + +- (id)initWithUserID:(NSString*)userID fieldsForRequest:(NSSet*)fieldsForRequest { + self = [super init]; + if (self) { + self.fieldsForRequest = fieldsForRequest ? fieldsForRequest : [NSSet set]; + self.userID = userID; + self.hasCompletedFetch = NO; + self.usePageLimitOfOne = NO; + } + return self; +} + +- (void)dealloc { + self.fieldsForRequest = nil; + self.userID = nil; + self.loader = nil; + [super dealloc]; +} + +- (void)prefetchAndCacheForSession:(FBSession*)session { + // Friend queries require a session, so do nothing if we don't have one. + if (session == nil) { + return; + } + + // datasource has some field ownership, so we need one here + FBGraphObjectTableDataSource *datasource = [[[FBGraphObjectTableDataSource alloc] init] autorelease]; + datasource.groupByField = @"name"; + + // me or one of my friends that also uses the app + NSString *user = self.userID; + if (!user) { + user = @"me"; + } + + // create the request object that we will start with + FBRequest *request = [FBFriendPickerViewController requestWithUserID:user + fields:self.fieldsForRequest + dataSource:datasource + session:session]; + + // this property supports unit testing + if(self.usePageLimitOfOne) { + [request.parameters setObject:@"1" + forKey:@"limit"]; + } + + self.loader.delegate = nil; + self.loader = [[[FBGraphObjectPagingLoader alloc] initWithDataSource:datasource + pagingMode:FBGraphObjectPagingModeImmediateViewless] + autorelease]; + self.loader.session = session; + + self.loader.delegate = self; + + // make sure we are around to handle the delegate call + [self retain]; + + // seed the cache + [self.loader startLoadingWithRequest:request + cacheIdentity:FBFriendPickerCacheIdentity + skipRoundtripIfCached:NO]; +} + +- (void)setUsePageLimitOfOne { + self.usePageLimitOfOne = YES; +} + +- (void)pagingLoaderDidFinishLoading:(FBGraphObjectPagingLoader *)pagingLoader { + self.loader.delegate = nil; + self.loader = nil; + self.hasCompletedFetch = YES; + + // this feels like suicide! + [self release]; +} + +@end diff --git a/src/ios/facebook/FBFriendPickerViewController+Internal.h b/src/ios/facebook/FBFriendPickerViewController+Internal.h new file mode 100644 index 000000000..2bedf6efe --- /dev/null +++ b/src/ios/facebook/FBFriendPickerViewController+Internal.h @@ -0,0 +1,33 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBFriendPickerViewController.h" +#import "FBRequest.h" +#import "FBGraphObjectTableDataSource.h" +#import "FBSession.h" + +// This is the cache identity used by both the view controller and cache descriptor objects +extern NSString *const FBFriendPickerCacheIdentity; + +@interface FBFriendPickerViewController (Internal) + ++ (FBRequest*)requestWithUserID:(NSString*)userID + fields:(NSSet*)fields + dataSource:(FBGraphObjectTableDataSource*)datasource + session:(FBSession*)session; + +@end \ No newline at end of file diff --git a/src/ios/facebook/FBFriendPickerViewController.h b/src/ios/facebook/FBFriendPickerViewController.h new file mode 100644 index 000000000..b9e433244 --- /dev/null +++ b/src/ios/facebook/FBFriendPickerViewController.h @@ -0,0 +1,290 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBGraphUser.h" +#import "FBSession.h" +#import "FBCacheDescriptor.h" +#import "FBViewController.h" + +@protocol FBFriendPickerDelegate; +@class FBFriendPickerCacheDescriptor; + +/*! + @typedef FBFriendSortOrdering enum + + @abstract Indicates the order in which friends should be listed in the friend picker. + + @discussion + */ +typedef enum { + /*! Sort friends by first, middle, last names. */ + FBFriendSortByFirstName, + /*! Sort friends by last, first, middle names. */ + FBFriendSortByLastName +} FBFriendSortOrdering; + +/*! + @typedef FBFriendDisplayOrdering enum + + @abstract Indicates whether friends should be displayed first-name-first or last-name-first. + + @discussion + */ +typedef enum { + /*! Display friends as First Middle Last. */ + FBFriendDisplayByFirstName, + /*! Display friends as Last First Middle. */ + FBFriendDisplayByLastName, +} FBFriendDisplayOrdering; + + +/*! + @class + + @abstract + The `FBFriendPickerViewController` class creates a controller object that manages + the user interface for displaying and selecting Facebook friends. + + @discussion + When the `FBFriendPickerViewController` view loads it creates a `UITableView` object + where the friends will be displayed. You can access this view through the `tableView` + property. The friend display can be sorted by first name or last name. Friends' + names can be displayed with the first name first or the last name first. + + The friend data can be pre-fetched and cached prior to using the view controller. The + cache is setup using an object that can trigger the + data fetch. Any friend data requests will first check the cache and use that data. + If the friend picker is being displayed cached data will initially be shown before + a fresh copy is retrieved. + + The `delegate` property may be set to an object that conforms to the + protocol. The `delegate` object will receive updates related to friend selection and + data changes. The delegate can also be used to filter the friends to display in the + picker. + */ +@interface FBFriendPickerViewController : FBViewController + +/*! + @abstract + Returns an outlet for the spinner used in the view controller. + */ +@property (nonatomic, retain) IBOutlet UIActivityIndicatorView *spinner; + +/*! + @abstract + Returns an outlet for the table view managed by the view controller. + */ +@property (nonatomic, retain) IBOutlet UITableView *tableView; + +/*! + @abstract + A Boolean value that specifies whether multi-select is enabled. + */ +@property (nonatomic) BOOL allowsMultipleSelection; + +/*! + @abstract + A Boolean value that indicates whether friend profile pictures are displayed. + */ +@property (nonatomic) BOOL itemPicturesEnabled; + +/*! + @abstract + Addtional fields to fetch when making the Graph API call to get friend data. + */ +@property (nonatomic, copy) NSSet *fieldsForRequest; + +/*! + @abstract + The session that is used in the request for friend data. + */ +@property (nonatomic, retain) FBSession *session; + +/*! + @abstract + The profile ID of the user whose friends are being viewed. + */ +@property (nonatomic, copy) NSString *userID; + +/*! + @abstract + The list of friends that are currently selected in the veiw. + The items in the array are objects. + */ +@property (nonatomic, retain, readonly) NSArray *selection; + +/*! + @abstract + The order in which friends are sorted in the display. + */ +@property (nonatomic) FBFriendSortOrdering sortOrdering; + +/*! + @abstract + The order in which friends' names are displayed. + */ +@property (nonatomic) FBFriendDisplayOrdering displayOrdering; + +/*! + @abstract + Initializes a friend picker view controller. + */ +- (id)init; + +/*! + @abstract + Initializes a friend picker view controller. + + @param aDecoder An unarchiver object. + */ +- (id)initWithCoder:(NSCoder *)aDecoder; + +/*! + @abstract + Used to initialize the object + + @param nibNameOrNil The name of the nib file to associate with the view controller. The nib file name should not contain any leading path information. If you specify nil, the nibName property is set to nil. + @param nibBundleOrNil The bundle in which to search for the nib file. This method looks for the nib file in the bundle's language-specific project directories first, followed by the Resources directory. If nil, this method looks for the nib file in the main bundle. + */ +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil; + +/*! + @abstract + Configures the properties used in the caching data queries. + + @discussion + Cache descriptors are used to fetch and cache the data used by the view controller. + If the view controller finds a cached copy of the data, it will + first display the cached content then fetch a fresh copy from the server. + + @param cacheDescriptor The containing the cache query properties. + */ +- (void)configureUsingCachedDescriptor:(FBCacheDescriptor*)cacheDescriptor; + +/*! + @abstract + Initiates a query to get friend data. + + @discussion + A cached copy will be returned if available. The cached view is temporary until a fresh copy is + retrieved from the server. It is legal to call this more than once. + */ +- (void)loadData; + +/*! + @abstract + Updates the view locally without fetching data from the server or from cache. + + @discussion + Use this if the filter or sort properties change. This may affect the order or + display of friend information but should not need require new data. + */ +- (void)updateView; + +/*! + @abstract + Clears the current selection, so the picker is ready for a fresh use. + */ +- (void)clearSelection; + +/*! + @method + + @abstract + Creates a cache descriptor based on default settings of the `FBFriendPickerViewController` object. + + @discussion + An `FBCacheDescriptor` object may be used to pre-fetch data before it is used by + the view controller. It may also be used to configure the `FBFriendPickerViewController` + object. + */ ++ (FBCacheDescriptor*)cacheDescriptor; + +/*! + @method + + @abstract + Creates a cache descriptor with additional fields and a profile ID for use with the `FBFriendPickerViewController` object. + + @discussion + An `FBCacheDescriptor` object may be used to pre-fetch data before it is used by + the view controller. It may also be used to configure the `FBFriendPickerViewController` + object. + + @param userID The profile ID of the user whose friends will be displayed. A nil value implies a "me" alias. + @param fieldsForRequest The set of additional fields to include in the request for friend data. + */ ++ (FBCacheDescriptor*)cacheDescriptorWithUserID:(NSString*)userID fieldsForRequest:(NSSet*)fieldsForRequest; + +@end + +/*! + @protocol + + @abstract + The `FBFriendPickerDelegate` protocol defines the methods used to receive event + notifications and allow for deeper control of the + view. + */ +@protocol FBFriendPickerDelegate +@optional + +/*! + @abstract + Tells the delegate that data has been loaded. + + @discussion + The object's `tableView` property is automatically + reloaded when this happens. However, if another table view, for example the + `UISearchBar` is showing data, then it may also need to be reloaded. + + @param friendPicker The friend picker view controller whose data changed. + */ +- (void)friendPickerViewControllerDataDidChange:(FBFriendPickerViewController *)friendPicker; + +/*! + @abstract + Tells the delegate that the selection has changed. + + @param friendPicker The friend picker view controller whose selection changed. + */ +- (void)friendPickerViewControllerSelectionDidChange:(FBFriendPickerViewController *)friendPicker; + +/*! + @abstract + Asks the delegate whether to include a friend in the list. + + @discussion + This can be used to implement a search bar that filters the friend list. + + @param friendPicker The friend picker view controller that is requesting this information. + @param user An object representing the friend. + */ +- (BOOL)friendPickerViewController:(FBFriendPickerViewController *)friendPicker + shouldIncludeUser:(id )user; + +/*! + @abstract + Tells the delegate that there is a communication error. + + @param friendPicker The friend picker view controller that encountered the error. + @param error An error object containing details of the error. + */ +- (void)friendPickerViewController:(FBFriendPickerViewController *)friendPicker + handleError:(NSError *)error; + +@end diff --git a/src/ios/facebook/FBFriendPickerViewController.m b/src/ios/facebook/FBFriendPickerViewController.m new file mode 100644 index 000000000..274044abd --- /dev/null +++ b/src/ios/facebook/FBFriendPickerViewController.m @@ -0,0 +1,514 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBError.h" +#import "FBFriendPickerViewController.h" +#import "FBFriendPickerViewController+Internal.h" +#import "FBFriendPickerCacheDescriptor.h" +#import "FBGraphObjectPagingLoader.h" +#import "FBGraphObjectTableDataSource.h" +#import "FBGraphObjectTableSelection.h" +#import "FBGraphObjectTableCell.h" +#import "FBLogger.h" +#import "FBRequest.h" +#import "FBRequestConnection.h" +#import "FBUtility.h" +#import "FBSession+Internal.h" +#import "FBSettings.h" + +NSString *const FBFriendPickerCacheIdentity = @"FBFriendPicker"; +static NSString *defaultImageName = @"FacebookSDKResources.bundle/FBFriendPickerView/images/default.png"; + +int const FBRefreshCacheDelaySeconds = 2; + +@interface FBFriendPickerViewController () + +@property (nonatomic, retain) FBGraphObjectTableDataSource *dataSource; +@property (nonatomic, retain) FBGraphObjectTableSelection *selectionManager; +@property (nonatomic, retain) FBGraphObjectPagingLoader *loader; +@property (nonatomic) BOOL trackActiveSession; + +- (void)initialize; +- (void)centerAndStartSpinner; +- (void)loadDataSkippingRoundTripIfCached:(NSNumber*)skipRoundTripIfCached; +- (FBRequest*)requestForLoadData; +- (void)addSessionObserver:(FBSession*)session; +- (void)removeSessionObserver:(FBSession*)session; +- (void)clearData; + +@end + +@implementation FBFriendPickerViewController { + BOOL _allowsMultipleSelection; +} + +@synthesize dataSource = _dataSource; +@synthesize delegate = _delegate; +@synthesize fieldsForRequest = _fieldsForRequest; +@synthesize selectionManager = _selectionManager; +@synthesize spinner = _spinner; +@synthesize tableView = _tableView; +@synthesize userID = _userID; +@synthesize loader = _loader; +@synthesize sortOrdering = _sortOrdering; +@synthesize displayOrdering = _displayOrdering; +@synthesize trackActiveSession = _trackActiveSession; +@synthesize session = _session; + +- (id)init { + self = [super init]; + + if (self) { + [self initialize]; + } + + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + + if (self) { + [self initialize]; + } + + return self; +} + +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + + if (self) { + [self initialize]; + } + + return self; +} + +- (void)initialize { + // Data Source + FBGraphObjectTableDataSource *dataSource = [[[FBGraphObjectTableDataSource alloc] + init] + autorelease]; + dataSource.defaultPicture = [UIImage imageNamed:defaultImageName]; + dataSource.controllerDelegate = self; + dataSource.itemTitleSuffixEnabled = YES; + + // Selection Manager + FBGraphObjectTableSelection *selectionManager = [[[FBGraphObjectTableSelection alloc] + initWithDataSource:dataSource] + autorelease]; + selectionManager.delegate = self; + + // Paging loader + id loader = [[[FBGraphObjectPagingLoader alloc] initWithDataSource:dataSource + pagingMode:FBGraphObjectPagingModeImmediate] + autorelease]; + self.loader = loader; + self.loader.delegate = self; + + // Self + self.allowsMultipleSelection = YES; + self.dataSource = dataSource; + self.delegate = nil; + self.itemPicturesEnabled = YES; + self.selectionManager = selectionManager; + self.userID = @"me"; + self.sortOrdering = FBFriendSortByFirstName; + self.displayOrdering = FBFriendDisplayByFirstName; + self.trackActiveSession = YES; +} + +- (void)dealloc { + [_loader cancel]; + _loader.delegate = nil; + [_loader release]; + + _dataSource.controllerDelegate = nil; + + [_dataSource release]; + [_fieldsForRequest release]; + [_selectionManager release]; + [_spinner release]; + [_tableView release]; + [_userID release]; + + [self removeSessionObserver:_session]; + [_session release]; + + [super dealloc]; +} + +#pragma mark - Custom Properties + +- (BOOL)allowsMultipleSelection { + return _allowsMultipleSelection; +} + +- (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection { + _allowsMultipleSelection = allowsMultipleSelection; + if (self.selectionManager) { + self.selectionManager.allowsMultipleSelection = allowsMultipleSelection; + } +} + +- (BOOL)itemPicturesEnabled { + return self.dataSource.itemPicturesEnabled; +} + +- (void)setItemPicturesEnabled:(BOOL)itemPicturesEnabled { + self.dataSource.itemPicturesEnabled = itemPicturesEnabled; +} + +- (NSArray *)selection { + return self.selectionManager.selection; +} + +// We don't really need to store session, let the loader hold it. +- (void)setSession:(FBSession *)session { + if (session != _session) { + [self removeSessionObserver:_session]; + + [_session release]; + _session = [session retain]; + + [self addSessionObserver:session]; + + self.loader.session = session; + + self.trackActiveSession = (session == nil); + } +} + + +#pragma mark - Public Methods + +- (void)viewDidLoad +{ + [super viewDidLoad]; + [FBLogger registerCurrentTime:FBLoggingBehaviorPerformanceCharacteristics + withTag:self]; + CGRect bounds = self.canvasView.bounds; + + if (!self.tableView) { + UITableView *tableView = [[[UITableView alloc] initWithFrame:bounds] autorelease]; + tableView.autoresizingMask = + UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + self.tableView = tableView; + [self.canvasView addSubview:tableView]; + } + + if (!self.spinner) { + UIActivityIndicatorView *spinner = [[[UIActivityIndicatorView alloc] + initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray] + autorelease]; + spinner.hidesWhenStopped = YES; + // We want user to be able to scroll while we load. + spinner.userInteractionEnabled = NO; + + self.spinner = spinner; + [self.canvasView addSubview:spinner]; + } + + self.selectionManager.allowsMultipleSelection = self.allowsMultipleSelection; + self.tableView.delegate = self.selectionManager; + [self.dataSource bindTableView:self.tableView]; + self.loader.tableView = self.tableView; +} + +- (void)viewDidUnload { + [super viewDidUnload]; + + self.loader.tableView = nil; + self.spinner = nil; + self.tableView = nil; +} + +- (void)configureUsingCachedDescriptor:(FBCacheDescriptor*)cacheDescriptor { + if (![cacheDescriptor isKindOfClass:[FBFriendPickerCacheDescriptor class]]) { + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBFriendPickerViewController: An attempt was made to configure " + @"an instance with a cache descriptor object that was not created " + @"by the FBFriendPickerViewController class" + userInfo:nil] + raise]; + } + FBFriendPickerCacheDescriptor *cd = (FBFriendPickerCacheDescriptor*)cacheDescriptor; + self.userID = cd.userID; + self.fieldsForRequest = cd.fieldsForRequest; +} + +- (void)loadData { + // when the app calls loadData, + // if we don't have a session and there is + // an open active session, use that + if (!self.session || + (self.trackActiveSession && ![self.session isEqual:[FBSession activeSessionIfOpen]])) { + self.session = [FBSession activeSessionIfOpen]; + self.trackActiveSession = YES; + } + [self loadDataSkippingRoundTripIfCached:[NSNumber numberWithBool:YES]]; +} + +- (void)updateView { + [self.dataSource update]; + [self.tableView reloadData]; +} + +- (void)clearSelection { + [self.selectionManager clearSelectionInTableView:self.tableView]; +} + +- (void)addSessionObserver:(FBSession *)session { + [session addObserver:self + forKeyPath:@"state" + options:NSKeyValueObservingOptionNew + context:nil]; +} + +- (void)removeSessionObserver:(FBSession *)session { + [session removeObserver:self + forKeyPath:@"state"]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([object isEqual:self.session] && + self.session.isOpen == NO) { + [self clearData]; + } +} + +- (void)clearData { + [self.dataSource clearGraphObjects]; + [self.selectionManager clearSelectionInTableView:self.tableView]; + [self.tableView reloadData]; + [self.loader reset]; +} + +#pragma mark - public class members + ++ (FBCacheDescriptor*)cacheDescriptor { + return [[[FBFriendPickerCacheDescriptor alloc] init] autorelease]; +} + ++ (FBCacheDescriptor*)cacheDescriptorWithUserID:(NSString*)userID + fieldsForRequest:(NSSet*)fieldsForRequest { + return [[[FBFriendPickerCacheDescriptor alloc] initWithUserID:userID + fieldsForRequest:fieldsForRequest] + autorelease]; +} + + +#pragma mark - private members + +- (FBRequest*)requestForLoadData { + + // Respect user settings in case they have changed. + NSMutableArray *sortFields = [NSMutableArray array]; + NSString *groupByField = nil; + if (self.sortOrdering == FBFriendSortByFirstName) { + [sortFields addObject:@"first_name"]; + [sortFields addObject:@"middle_name"]; + [sortFields addObject:@"last_name"]; + groupByField = @"first_name"; + } else { + [sortFields addObject:@"last_name"]; + [sortFields addObject:@"first_name"]; + [sortFields addObject:@"middle_name"]; + groupByField = @"last_name"; + } + [self.dataSource setSortingByFields:sortFields ascending:YES]; + self.dataSource.groupByField = groupByField; + self.dataSource.useCollation = YES; + + // me or one of my friends that also uses the app + NSString *user = self.userID; + if (!user) { + user = @"me"; + } + + // create the request and start the loader + FBRequest *request = [FBFriendPickerViewController requestWithUserID:user + fields:self.fieldsForRequest + dataSource:self.dataSource + session:self.session]; + return request; +} + +- (void)loadDataSkippingRoundTripIfCached:(NSNumber*)skipRoundTripIfCached { + if (self.session) { + [self.loader startLoadingWithRequest:[self requestForLoadData] + cacheIdentity:FBFriendPickerCacheIdentity + skipRoundtripIfCached:skipRoundTripIfCached.boolValue]; + } +} + ++ (FBRequest*)requestWithUserID:(NSString*)userID + fields:(NSSet*)fields + dataSource:(FBGraphObjectTableDataSource*)datasource + session:(FBSession*)session { + + FBRequest *request = [FBRequest requestForGraphPath:[NSString stringWithFormat:@"%@/friends", userID]]; + [request setSession:session]; + + NSString *allFields = [datasource fieldsForRequestIncluding:fields, + @"id", + @"name", + @"first_name", + @"middle_name", + @"last_name", + @"picture", + nil]; + [request.parameters setObject:allFields forKey:@"fields"]; + + return request; +} + +- (void)centerAndStartSpinner { + [FBUtility centerView:self.spinner tableView:self.tableView]; + [self.spinner startAnimating]; +} + +#pragma mark - FBGraphObjectSelectionChangedDelegate + +- (void)graphObjectTableSelectionDidChange:(FBGraphObjectTableSelection *)selection { + if ([self.delegate respondsToSelector: + @selector(friendPickerViewControllerSelectionDidChange:)]) { + [(id)self.delegate friendPickerViewControllerSelectionDidChange:self]; + } +} + +#pragma mark - FBGraphObjectViewControllerDelegate + +- (BOOL)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + filterIncludesItem:(id)item { + id user = (id)item; + + if ([self.delegate + respondsToSelector:@selector(friendPickerViewController:shouldIncludeUser:)]) { + return [(id)self.delegate friendPickerViewController:self + shouldIncludeUser:user]; + } else { + return YES; + } +} + +- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + titleOfItem:(id)graphUser { + // Title is either "First Middle" or "Last" depending on display order. + if (self.displayOrdering == FBFriendDisplayByFirstName) { + if (graphUser.middle_name) { + return [NSString stringWithFormat:@"%@ %@", graphUser.first_name, graphUser.middle_name]; + } else { + return graphUser.first_name; + } + } else { + return graphUser.last_name; + } +} + +- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + titleSuffixOfItem:(id)graphUser { + // Title suffix is either "Last" or "First Middle" depending on display order. + if (self.displayOrdering == FBFriendDisplayByLastName) { + if (graphUser.middle_name) { + return [NSString stringWithFormat:@"%@ %@", graphUser.first_name, graphUser.middle_name]; + } else { + return graphUser.first_name; + } + } else { + return graphUser.last_name; + } + +} + +- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + pictureUrlOfItem:(id)graphObject { + id picture = [graphObject objectForKey:@"picture"]; + // Depending on what migration the app is in, we may get back either a string, or a + // dictionary with a "data" property that is a dictionary containing a "url" property. + if ([picture isKindOfClass:[NSString class]]) { + return picture; + } + id data = [picture objectForKey:@"data"]; + return [data objectForKey:@"url"]; +} + +- (void)graphObjectTableDataSource:(FBGraphObjectTableDataSource*)dataSource + customizeTableCell:(FBGraphObjectTableCell*)cell { + // We want to bold whichever part of the name we are sorting on. + cell.boldTitle = (self.sortOrdering == FBFriendSortByFirstName && self.displayOrdering == FBFriendDisplayByFirstName) || + (self.sortOrdering == FBFriendSortByLastName && self.displayOrdering == FBFriendDisplayByLastName); + cell.boldTitleSuffix = (self.sortOrdering == FBFriendSortByFirstName && self.displayOrdering == FBFriendDisplayByLastName) || + (self.sortOrdering == FBFriendSortByLastName && self.displayOrdering == FBFriendDisplayByFirstName); +} + +#pragma mark FBGraphObjectPagingLoaderDelegate members + +- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader willLoadURL:(NSString*)url { + [self centerAndStartSpinner]; +} + +- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader didLoadData:(NSDictionary*)results { + // This logging currently goes here because we're effectively complete with our initial view when + // the first page of results come back. In the future, when we do caching, we will need to move + // this to a more appropriate place (e.g., after the cache has been brought in). + [FBLogger singleShotLogEntry:FBLoggingBehaviorPerformanceCharacteristics + timestampTag:self + formatString:@"Friend Picker: first render "]; // logger will append "%d msec" + + + if ([self.delegate respondsToSelector:@selector(friendPickerViewControllerDataDidChange:)]) { + [(id)self.delegate friendPickerViewControllerDataDidChange:self]; + } +} + +- (void)pagingLoaderDidFinishLoading:(FBGraphObjectPagingLoader *)pagingLoader { + // finished loading, stop animating + [self.spinner stopAnimating]; + + // Call the delegate from here as well, since this might be the first response of a query + // that has no results. + if ([self.delegate respondsToSelector:@selector(friendPickerViewControllerDataDidChange:)]) { + [(id)self.delegate friendPickerViewControllerDataDidChange:self]; + } + + // if our current display is from cache, then kick-off a near-term refresh + if (pagingLoader.isResultFromCache) { + [self performSelector:@selector(loadDataSkippingRoundTripIfCached:) + withObject:[NSNumber numberWithBool:NO] + afterDelay:FBRefreshCacheDelaySeconds]; + } +} + +- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader handleError:(NSError*)error { + if ([self.delegate + respondsToSelector:@selector(friendPickerViewController:handleError:)]) { + [(id)self.delegate friendPickerViewController:self handleError:error]; + } +} + +- (void)pagingLoaderWasCancelled:(FBGraphObjectPagingLoader*)pagingLoader { + [self.spinner stopAnimating]; +} + +@end diff --git a/src/ios/facebook/FBGraphLocation.h b/src/ios/facebook/FBGraphLocation.h new file mode 100644 index 000000000..1b3937108 --- /dev/null +++ b/src/ios/facebook/FBGraphLocation.h @@ -0,0 +1,77 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBGraphObject.h" + +/*! + @protocol + + @abstract + The `FBGraphLocation` protocol enables typed access to the `location` property + of a Facebook place object. + + + @discussion + The `FBGraphLocation` protocol represents the most commonly used properties of a + location object. It may be used to access an `NSDictionary` object that has + been wrapped with an facade. + */ +@protocol FBGraphLocation + +/*! + @property + @abstract Typed access to a location's street. + */ +@property (retain, nonatomic) NSString *street; + +/*! + @property + @abstract Typed access to a location's city. + */ +@property (retain, nonatomic) NSString *city; + +/*! + @property + @abstract Typed access to a location's state. + */ +@property (retain, nonatomic) NSString *state; + +/*! + @property + @abstract Typed access to a location's country. + */ +@property (retain, nonatomic) NSString *country; + +/*! + @property + @abstract Typed access to a location's zip code. + */ +@property (retain, nonatomic) NSString *zip; + +/*! + @property + @abstract Typed access to a location's latitude. + */ +@property (retain, nonatomic) NSNumber *latitude; + +/*! + @property + @abstract Typed access to a location's longitude. + */ +@property (retain, nonatomic) NSNumber *longitude; + +@end \ No newline at end of file diff --git a/src/ios/facebook/FBGraphObject.h b/src/ios/facebook/FBGraphObject.h new file mode 100644 index 000000000..28a777bdf --- /dev/null +++ b/src/ios/facebook/FBGraphObject.h @@ -0,0 +1,224 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +/*! + @protocol + + @abstract + The `FBGraphObject` protocol is the base protocol which enables typed access to graph objects and + open graph objects. Inherit from this protocol or a sub-protocol in order to introduce custom types + for typed access to Facebook objects. + + @discussion + The `FBGraphObject` protocol is the core type used by the Facebook SDK for iOS to + represent objects in the Facebook Social Graph and the Facebook Open Graph (OG). + The `FBGraphObject` class implements useful default functionality, but is rarely + used directly by applications. The `FBGraphObject` protocol, in contrast is the + base protocol for all graph object access via the SDK. + + Goals of the FBGraphObject types: +
    +
  • Lightweight/maintainable/robust
  • +
  • Extensible and resilient to change, both by Facebook and third party (OG)
  • +
  • Simple and natural extension to Objective-C
  • +
+ + The FBGraphObject at its core is a duck typed (if it walks/swims/quacks... + its a duck) model which supports an optional static facade. Duck-typing achieves + the flexibility necessary for Social Graph and OG uses, and the static facade + increases discoverability, maintainability, robustness and simplicity. + The following excerpt from the PlacePickerSample shows a simple use of the + a facade protocol `FBGraphPlace` by an application: + +
+ ‐ (void)placePickerViewControllerSelectionDidChange:(FBPlacePickerViewController *)placePicker
+ {
+   id<FBGraphPlace> place = placePicker.selection;
+ 
+   // we'll use logging to show the simple typed property access to place and location info
+   NSLog(@"place=%@, city=%@, state=%@, lat long=%@ %@", 
+     place.name,
+     place.location.city,
+     place.location.state,
+     place.location.latitude,
+     place.location.longitude);
+ }
+ 
+ + Note that in this example, access to common place information is available through typed property + syntax. But if at some point places in the Social Graph supported additional fields "foo" and "bar", not + reflected in the `FBGraphPlace` protocol, the application could still access the values like so: + +
+ NSString *foo = [place objectForKey:@"foo"]; // perhaps located at the ... in the preceding example
+ NSNumber *bar = [place objectForKey:@"bar"]; // extensibility applies to Social and Open graph uses
+ 
+ + In addition to untyped access, applications and future revisions of the SDK may add facade protocols by + declaring a protocol inheriting the `FBGraphObject` protocol, like so: + +
+ @protocol MyGraphThing<FBGraphObject>
+ @property (copy, nonatomic) NSString *id;
+ @property (copy, nonatomic) NSString *name;
+ @end
+ 
+ + Important: facade implementations are inferred by graph objects returned by the methods of the SDK. This + means that no explicit implementation is required by application or SDK code. Any `FBGraphObject` instance + may be cast to any `FBGraphObject` facade protocol, and accessed via properties. If a field is not present + for a given facade property, the property will return nil. + + The following layer diagram depicts some of the concepts discussed thus far: + +
+                       *-------------* *------------* *-------------**--------------------------*
+            Facade --> | FBGraphUser | |FBGraphPlace| | MyGraphThing|| MyGraphPersonExtentension| ...
+                       *-------------* *------------* *-------------**--------------------------*
+                       *------------------------------------* *--------------------------------------*
+  Transparent impl --> |     FBGraphObject (instances)      | |      CustomClass<FBGraphObject>      |
+                       *------------------------------------* *--------------------------------------*
+                       *-------------------**------------------------* *-----------------------------*
+     Apparent impl --> |NSMutableDictionary||FBGraphObject (protocol)| |FBGraphObject (class methods)|
+                       *-------------------**------------------------* *-----------------------------*
+ 
+ + The *Facade* layer is meant for typed access to graph objects. The *Transparent impl* layer (more + specifically, the instance capabilities of `FBGraphObject`) are used by the SDK and app logic + internally, but are not part of the public interface between application and SDK. The *Apparent impl* + layer represents the lower-level "duck-typed" use of graph objects. + + Implementation note: the SDK returns `NSMutableDictionary` derived instances with types declared like + one of the following: + +
+ NSMutableDictionary<FBGraphObject> *obj;     // no facade specified (still castable by app)
+ NSMutableDictionary<FBGraphPlace> *person;   // facade specified when possible
+ 
+ + However, when passing a graph object to the SDK, `NSMutableDictionary` is not assumed; only the + FBGraphObject protocol is assumed, like so: + +
+ id<FBGraphObject> anyGraphObj;
+ 
+ + As such, the methods declared on the `FBGraphObject` protocol represent the methods used by the SDK to + consume graph objects. While the `FBGraphObject` class implements the full `NSMutableDictionary` and KVC + interfaces, these are not consumed directly by the SDK, and are optional for custom implementations. + */ +@protocol FBGraphObject + +/*! + @method + @abstract + Returns the number of properties on this `FBGraphObject`. + */ +- (NSUInteger)count; +/*! + @method + @abstract + Returns a property on this `FBGraphObject`. + + @param aKey name of the property to return + */ +- (id)objectForKey:(id)aKey; +/*! + @method + @abstract + Returns an enumerator of the property naems on this `FBGraphObject`. + */ +- (NSEnumerator *)keyEnumerator; +/*! + @method + @abstract + Removes a property on this `FBGraphObject`. + + @param aKey name of the property to remove + */ +- (void)removeObjectForKey:(id)aKey; +/*! + @method + @abstract + Sets the value of a property on this `FBGraphObject`. + + @param anObject the new value of the property + @param aKey name of the property to set + */ +- (void)setObject:(id)anObject forKey:(id)aKey; + +@end + +/*! + @class + + @abstract + Static class with helpers for use with graph objects + + @discussion + The public interface of this class is useful for creating objects that have the same graph characteristics + of those returned by methods of the SDK. This class also represents the internal implementation of the + `FBGraphObject` protocol, used by the Facebook SDK. Application code should not use the `FBGraphObject` class to + access instances and instance members, favoring the protocol. + */ +@interface FBGraphObject : NSMutableDictionary + +/*! + @method + @abstract + Used to create a graph object for, usually for use in posting a new graph object or action + */ ++ (NSMutableDictionary*)graphObject; + +/*! + @method + @abstract + Used to wrap an existing dictionary with a `FBGraphObject` facade + + @discussion + Normally you will not need to call this method, as the Facebook SDK already "FBGraphObject-ifys" json objects + fetch via `FBRequest` and `FBRequestConnection`. However, you may have other reasons to create json objects in your + application, which you would like to treat as a graph object. The pattern for doing this is that you pass the root + node of the json to this method, to retrieve a wrapper. From this point, if you traverse the graph, any other objects + deeper in the hierarchy will be wrapped as `FBGraphObject`'s in a lazy fashion. + + This method is designed to avoid unnecessary memory allocations, and object copying. Due to this, the method does + not copy the source object if it can be avoided, but rather wraps and uses it as is. The returned object derives + callers shoudl use the returned object after calls to this method, rather than continue to call methods on the original + object. + + @param jsonDictionary the dictionary representing the underlying object to wrap + */ ++ (NSMutableDictionary*)graphObjectWrappingDictionary:(NSDictionary*)jsonDictionary; + +/*! + @method + @abstract + Used to compare two `FBGraphObject`s to determine if represent the same object. We do not overload + the concept of equality as there are various types of equality that may be important for an `FBGraphObject` + (for instance, two different `FBGraphObject`s could represent the same object, but contain different + subsets of fields). + + @param anObject an `FBGraphObject` to test + + @param anotherObject the `FBGraphObject` to compare it against + */ ++ (BOOL)isGraphObjectID:(id)anObject sameAs:(id)anotherObject; + + +@end diff --git a/src/ios/facebook/FBGraphObject.m b/src/ios/facebook/FBGraphObject.m new file mode 100644 index 000000000..446642af7 --- /dev/null +++ b/src/ios/facebook/FBGraphObject.m @@ -0,0 +1,451 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBGraphObject.h" +#import + +// Module Summary: +// this is the module that does the heavy lifting to implement the public-facing +// developer-model described in FBGraphObject.h, namely typed protocol +// accessors over "duck-typed" dictionaries, for interating with Facebook Graph and +// Open Graph objects and actions +// +// Message forwarding is used as described here: +// https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html +// in order to infer implementations for property getter +// and setter selectors, backed by objectForKey: and setObject:forKey: respectively +// +// The basic flow is straightforward, though there are a number of details; +// The flow is: +// * System doesn't recognize a selector +// * It calls methodSignatureForSelector: +// * We return a new signature if we intend to handle the selector +// * The system populates the invoke object with the old selector and the new signature +// * The system passes the invocation to forwardInvocation: +// * We swap out selectors and invoke +// +// Additional details include, deferred wrapping of objects as they are fetched by callers, +// implementations for common methods such as respondsToSelector and conformsToProtocol, as +// suggested in the previously referenced documentation + +static NSString *const FBIsGraphObjectKey = @"com.facebook.FBIsGraphObjectKey"; + +// used internally by the category impl +typedef enum _SelectorInferredImplType { + SelectorInferredImplTypeNone = 0, + SelectorInferredImplTypeGet = 1, + SelectorInferredImplTypeSet = 2 +} SelectorInferredImplType; + + +// internal-only wrapper +@interface FBGraphObjectArray : NSMutableArray + +- (id)initWrappingArray:(NSArray *)otherArray; +- (id)graphObjectifyAtIndex:(NSUInteger)index; +- (void)graphObjectifyAll; + +@end + + +@interface FBGraphObject () + +- (id)initWrappingDictionary:(NSDictionary *)otherDictionary; +- (void)graphObjectifyAll; +- (id)graphObjectifyAtKey:(id)key; + ++ (id)graphObjectWrappingObject:(id)originalObject; ++ (SelectorInferredImplType)inferredImplTypeForSelector:(SEL)sel; ++ (BOOL)isProtocolImplementationInferable:(Protocol *)protocol checkFBGraphObjectAdoption:(BOOL)checkAdoption; + +@end + +@implementation FBGraphObject { + NSMutableDictionary *_jsonObject; +} + +#pragma mark Lifecycle + +- (id)initWrappingDictionary:(NSDictionary *)jsonObject { + self = [super init]; + if (self) { + if ([jsonObject isKindOfClass:[FBGraphObject class]]) { + // in this case, we prefer to return the original object, + // rather than allocate a wrapper + + // we are about to return this, better make it the caller's + [jsonObject retain]; + + // we don't need self after all + [self release]; + + // no wrapper needed, returning the object that was provided + return (FBGraphObject*)jsonObject; + } else { + _jsonObject = [[NSMutableDictionary dictionaryWithDictionary:jsonObject] retain]; + } + } + return self; +} + +- (void)dealloc { + [_jsonObject release]; + [super dealloc]; +} + +#pragma mark - +#pragma mark Public Members + ++ (NSMutableDictionary*)graphObject { + return [FBGraphObject graphObjectWrappingDictionary:[NSMutableDictionary dictionary]]; +} + ++ (NSMutableDictionary*)graphObjectWrappingDictionary:(NSDictionary*)jsonDictionary { + return [FBGraphObject graphObjectWrappingObject:jsonDictionary]; +} + ++ (BOOL)isGraphObjectID:(id)anObject sameAs:(id)anotherObject { + if (anObject != nil && + anObject == anotherObject) { + return YES; + } + id anID = [anObject objectForKey:@"id"]; + id anotherID = [anotherObject objectForKey:@"id"]; + if ([anID isKindOfClass:[NSString class]] && + [anotherID isKindOfClass:[NSString class]]) { + return [(NSString*)anID isEqualToString:anotherID]; + } + return NO; +} + +#pragma mark - +#pragma mark NSObject overrides + +// make the respondsToSelector method do the right thing for the selectors we handle +- (BOOL)respondsToSelector:(SEL)sel +{ + return [super respondsToSelector:sel] || + ([FBGraphObject inferredImplTypeForSelector:sel] != SelectorInferredImplTypeNone); +} + +- (BOOL)conformsToProtocol:(Protocol *)protocol { + return [super conformsToProtocol:protocol] || + ([FBGraphObject isProtocolImplementationInferable:protocol + checkFBGraphObjectAdoption:YES]); +} + +// returns the signature for the method that we will actually invoke +- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { + SEL alternateSelector = sel; + + // if we should forward, to where? + switch ([FBGraphObject inferredImplTypeForSelector:sel]) { + case SelectorInferredImplTypeGet: + alternateSelector = @selector(objectForKey:); + break; + case SelectorInferredImplTypeSet: + alternateSelector = @selector(setObject:forKey:); + break; + case SelectorInferredImplTypeNone: + default: + break; + } + + return [super methodSignatureForSelector:alternateSelector]; +} + +// forwards otherwise missing selectors that match the FBGraphObject convention +- (void)forwardInvocation:(NSInvocation *)invocation { + // if we should forward, to where? + switch ([FBGraphObject inferredImplTypeForSelector:[invocation selector]]) { + case SelectorInferredImplTypeGet: { + // property getter impl uses the selector name as an argument... + NSString *propertyName = NSStringFromSelector([invocation selector]); + [invocation setArgument:&propertyName atIndex:2]; + //... to the replacement method objectForKey: + invocation.selector = @selector(objectForKey:); + [invocation invokeWithTarget:self]; + break; + } + case SelectorInferredImplTypeSet: { + // property setter impl uses the selector name as an argument... + NSMutableString *propertyName = [NSMutableString stringWithString:NSStringFromSelector([invocation selector])]; + // remove 'set' and trailing ':', and lowercase the new first character + [propertyName deleteCharactersInRange:NSMakeRange(0, 3)]; // "set" + [propertyName deleteCharactersInRange:NSMakeRange(propertyName.length - 1, 1)]; // ":" + + NSString *firstChar = [[propertyName substringWithRange:NSMakeRange(0,1)] lowercaseString]; + [propertyName replaceCharactersInRange:NSMakeRange(0, 1) withString:firstChar]; + // the object argument is already in the right place (2), but we need to set the key argument + [invocation setArgument:&propertyName atIndex:3]; + // and replace the missing method with setObject:forKey: + invocation.selector = @selector(setObject:forKey:); + [invocation invokeWithTarget:self]; + break; + } + case SelectorInferredImplTypeNone: + default: + [super forwardInvocation:invocation]; + return; + } +} + +- (id)graphObjectifyAtKey:(id)key { + id object = [_jsonObject objectForKey:key]; + // make certain it is FBObjectGraph-ified + id possibleReplacement = [FBGraphObject graphObjectWrappingObject:object]; + if (object != possibleReplacement) { + // and if not-yet, replace the original with the wrapped object + [_jsonObject setObject:possibleReplacement forKey:key]; + object = possibleReplacement; + } + return object; +} + +- (void)graphObjectifyAll { + NSArray *keys = [_jsonObject allKeys]; + for (NSString *key in keys) { + [self graphObjectifyAtKey:key]; + } +} + + +#pragma mark - + +#pragma mark NSDictionary and NSMutableDictionary overrides + +- (NSUInteger)count { + return _jsonObject.count; +} + +- (id)objectForKey:(id)key { + return [self graphObjectifyAtKey:key]; +} + +- (NSEnumerator *)keyEnumerator { + [self graphObjectifyAll]; + return _jsonObject.keyEnumerator; +} + +- (void)setObject:(id)object forKey:(id)key { + return [_jsonObject setObject:object forKey:key]; +} + +- (void)removeObjectForKey:(id)key { + return [_jsonObject removeObjectForKey:key]; +} + +#pragma mark - +#pragma mark Public Members + +#pragma mark - +#pragma mark Private Class Members + ++ (id)graphObjectWrappingObject:(id)originalObject { + // non-array and non-dictionary case, returns original object + id result = originalObject; + + // array and dictionary wrap + if ([originalObject isKindOfClass:[NSDictionary class]]) { + result = [[[FBGraphObject alloc] initWrappingDictionary:originalObject] autorelease]; + } else if ([originalObject isKindOfClass:[NSArray class]]) { + result = [[[FBGraphObjectArray alloc] initWrappingArray:originalObject] autorelease]; + } + + // return our object + return result; +} + +// helper method used by the catgory implementation to determine whether a selector should be handled ++ (SelectorInferredImplType)inferredImplTypeForSelector:(SEL)sel { + // the overhead in this impl is high relative to the cost of a normal property + // accessor; if needed we will optimize by caching results of the following + // processing, indexed by selector + + NSString *selectorName = NSStringFromSelector(sel); + int parameterCount = [[selectorName componentsSeparatedByString:@":"] count]-1; + // we will process a selector as a getter if paramCount == 0 + if (parameterCount == 0) { + return SelectorInferredImplTypeGet; + // otherwise we consider a setter if... + } else if (parameterCount == 1 && // ... we have the correct arity + [selectorName hasPrefix:@"set"] && // ... we have the proper prefix + selectorName.length > 4) { // ... there are characters other than "set" & ":" + return SelectorInferredImplTypeSet; + } + + return SelectorInferredImplTypeNone; +} + ++ (BOOL)isProtocolImplementationInferable:(Protocol*)protocol checkFBGraphObjectAdoption:(BOOL)checkAdoption { + // first handle base protocol questions + if (checkAdoption && !protocol_conformsToProtocol(protocol, @protocol(FBGraphObject))) { + return NO; + } + + if ([protocol isEqual:@protocol(FBGraphObject)]) { + return YES; // by definition + } + + unsigned int count = 0; + struct objc_method_description *methods = nil; + + // then confirm that all methods are required + methods = protocol_copyMethodDescriptionList(protocol, + NO, // optional + YES, // instance + &count); + if (methods) { + free(methods); + return NO; + } + + @try { + // fetch methods of the protocol and confirm that each can be implemented automatically + methods = protocol_copyMethodDescriptionList(protocol, + YES, // required + YES, // instance + &count); + for (int index = 0; index < count; index++) { + if ([FBGraphObject inferredImplTypeForSelector:methods[index].name] == SelectorInferredImplTypeNone) { + // we have a bad actor, short circuit + return NO; + } + } + } @finally { + if (methods) { + free(methods); + } + } + + // fetch adopted protocols + Protocol **adopted = nil; + @try { + adopted = protocol_copyProtocolList(protocol, &count); + for (int index = 0; index < count; index++) { + // here we go again... + if (![FBGraphObject isProtocolImplementationInferable:adopted[index] + checkFBGraphObjectAdoption:NO]) { + return NO; + } + } + } @finally { + if (adopted) { + free(adopted); + } + } + + // protocol ran the gauntlet + return YES; +} + +#pragma mark - + +@end + +#pragma mark internal classes + +@implementation FBGraphObjectArray { + NSMutableArray *_jsonArray; +} + +- (id)initWrappingArray:(NSArray *)jsonArray { + self = [super init]; + if (self) { + if ([jsonArray isKindOfClass:[FBGraphObjectArray class]]) { + // in this case, we prefer to return the original object, + // rather than allocate a wrapper + + // we are about to return this, better make it the caller's + [jsonArray retain]; + + // we don't need self after all + [self release]; + + // no wrapper needed, returning the object that was provided + return (FBGraphObjectArray*)jsonArray; + } else { + _jsonArray = [[NSMutableArray arrayWithArray:jsonArray] retain]; + } + } + return self; +} + +- (void)dealloc { + [_jsonArray release]; + [super dealloc]; +} + +- (NSUInteger)count { + return _jsonArray.count; +} + +- (id)graphObjectifyAtIndex:(NSUInteger)index { + id object = [_jsonArray objectAtIndex:index]; + // make certain it is FBObjectGraph-ified + id possibleReplacement = [FBGraphObject graphObjectWrappingObject:object]; + if (object != possibleReplacement) { + // and if not-yet, replace the original with the wrapped object + [_jsonArray replaceObjectAtIndex:index withObject:possibleReplacement]; + object = possibleReplacement; + } + return object; +} + +- (void)graphObjectifyAll { + int count = [_jsonArray count]; + for (int i = 0; i < count; ++i) { + [self graphObjectifyAtIndex:i]; + } +} + +- (id)objectAtIndex:(NSUInteger)index { + return [self graphObjectifyAtIndex:index]; +} + +- (NSEnumerator *)objectEnumerator { + [self graphObjectifyAll]; + return _jsonArray.objectEnumerator; +} + +- (NSEnumerator *)reverseObjectEnumerator { + [self graphObjectifyAll]; + return _jsonArray.reverseObjectEnumerator; +} + +- (void)insertObject:(id)object atIndex:(NSUInteger)index { + [_jsonArray insertObject:object atIndex:index]; +} + +- (void)removeObjectAtIndex:(NSUInteger)index { + [_jsonArray removeObjectAtIndex:index]; +} + +- (void)addObject:(id)object { + [_jsonArray addObject:object]; +} + +- (void)removeLastObject { + [_jsonArray removeLastObject]; +} + +- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)object { + [_jsonArray replaceObjectAtIndex:index withObject:object]; +} + +@end + +#pragma mark - diff --git a/src/ios/facebook/FBGraphObjectPagingLoader.h b/src/ios/facebook/FBGraphObjectPagingLoader.h new file mode 100644 index 000000000..c79ded144 --- /dev/null +++ b/src/ios/facebook/FBGraphObjectPagingLoader.h @@ -0,0 +1,63 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBGraphObjectTableDataSource.h" + +@class FBSession; +@class FBRequest; +@protocol FBGraphObjectPagingLoaderDelegate; + +typedef enum { + // Paging links will be followed as soon as one set of results is loaded + FBGraphObjectPagingModeImmediate, + // Paging links will be followed as soon as one set of results is loaded, even without a view + FBGraphObjectPagingModeImmediateViewless, + // Paging links will be followed only when the user scrolls to the bottom of the table + FBGraphObjectPagingModeAsNeeded +} FBGraphObjectPagingMode; + +@interface FBGraphObjectPagingLoader : NSObject + +@property (nonatomic, retain) UITableView *tableView; +@property (nonatomic, retain) FBGraphObjectTableDataSource *dataSource; +@property (nonatomic, retain) FBSession *session; +@property (nonatomic, assign) id delegate; +@property (nonatomic, readonly) FBGraphObjectPagingMode pagingMode; +@property (nonatomic, readonly) BOOL isResultFromCache; + +- (id)initWithDataSource:(FBGraphObjectTableDataSource*)aDataSource + pagingMode:(FBGraphObjectPagingMode)pagingMode; +- (void)startLoadingWithRequest:(FBRequest*)request + cacheIdentity:(NSString*)cacheIdentity + skipRoundtripIfCached:(BOOL)skipRoundtripIfCached; +- (void)addResultsAndUpdateView:(NSDictionary*)results; +- (void)cancel; +- (void)reset; + +@end + +@protocol FBGraphObjectPagingLoaderDelegate + +@optional + +- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader willLoadURL:(NSString*)url; +- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader didLoadData:(NSDictionary*)results; +- (void)pagingLoaderDidFinishLoading:(FBGraphObjectPagingLoader*)pagingLoader; +- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader handleError:(NSError*)error; +- (void)pagingLoaderWasCancelled:(FBGraphObjectPagingLoader*)pagingLoader; + +@end diff --git a/src/ios/facebook/FBGraphObjectPagingLoader.m b/src/ios/facebook/FBGraphObjectPagingLoader.m new file mode 100644 index 000000000..d81901b3f --- /dev/null +++ b/src/ios/facebook/FBGraphObjectPagingLoader.m @@ -0,0 +1,314 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBGraphObjectPagingLoader.h" +#import "FBRequest.h" +#import "FBError.h" +#import "FBRequestConnection+Internal.h" + +@interface FBGraphObjectPagingLoader () + +@property (nonatomic, retain) NSString *nextLink; +@property (nonatomic, retain) FBRequestConnection *connection; +@property (nonatomic, copy) NSString *cacheIdentity; +@property (nonatomic, assign) BOOL skipRoundtripIfCached; +@property (nonatomic) FBGraphObjectPagingMode pagingMode; + +- (void)followNextLink; +- (void)requestCompleted:(FBRequestConnection *)connection + result:(id)result + error:(NSError *)error; + +@end + + +@implementation FBGraphObjectPagingLoader + +@synthesize tableView = _tableView; +@synthesize dataSource = _dataSource; +@synthesize pagingMode = _pagingMode; +@synthesize nextLink = _nextLink; +@synthesize session = _session; +@synthesize connection = _connection; +@synthesize delegate = _delegate; +@synthesize isResultFromCache = _isResultFromCache; +@synthesize cacheIdentity = _cacheIdentity; +@synthesize skipRoundtripIfCached = _skipRoundtripIfCached; + +#pragma mark Lifecycle methods + +- (id)initWithDataSource:(FBGraphObjectTableDataSource*)aDataSource + pagingMode:(FBGraphObjectPagingMode)pagingMode;{ + if (self = [super init]) { + // Note that pagingMode must be set before dataSource. + self.pagingMode = pagingMode; + self.dataSource = aDataSource; + _isResultFromCache = NO; + } + return self; +} + +- (void)dealloc { + [_tableView release]; + [_dataSource release]; + [_nextLink release]; + [_session release]; + [_connection release]; + [_cacheIdentity release]; + + [super dealloc]; +} + +#pragma mark - + +- (void)setDataSource:(FBGraphObjectTableDataSource *)dataSource { + [dataSource retain]; + [_dataSource release]; + _dataSource = dataSource; + if (self.pagingMode == FBGraphObjectPagingModeAsNeeded) { + _dataSource.dataNeededDelegate = self; + } else { + _dataSource.dataNeededDelegate = nil; + } +} + +- (void)setTableView:(UITableView*)tableView { + [tableView retain]; + [_tableView release]; + _tableView = tableView; + + // If we already have a nextLink and we are in immediate paging mode, re-start + // loading when we are reconnected to a table view. + if (self.pagingMode == FBGraphObjectPagingModeImmediate && + self.nextLink && + self.tableView) { + [self followNextLink]; + } +} + +- (void)updateView +{ + [self.dataSource update]; + [self.tableView reloadData]; +} + +// Adds new results to the table and attempts to preserve visual context in the table +- (void)addResultsAndUpdateView:(NSDictionary*)results { + NSArray *data = (NSArray *)[results objectForKey:@"data"]; + if (data.count == 0) { + // If we got no data, stop following paging links. + self.nextLink = nil; + // Tell the data source we're done. + [self.dataSource appendGraphObjects:nil]; + [self updateView]; + + // notify of completion + if ([self.delegate respondsToSelector:@selector(pagingLoaderDidFinishLoading:)]) { + [self.delegate pagingLoaderDidFinishLoading:self]; + } + return; + } else { + NSDictionary *paging = (NSDictionary *)[results objectForKey:@"paging"]; + NSString *next = (NSString *)[paging objectForKey:@"next"]; + self.nextLink = next; + } + + if (!self.dataSource.hasGraphObjects) { + // If we don't have any data already, this is easy. + [self.dataSource appendGraphObjects:data]; + [self updateView]; + } else { + // As we fetch additional results and add them to the table, we do not + // want the table jumping around seemingly at random, frustrating the user's + // attempts at scrolling, etc. Since results may be added anywhere in + // the table, we choose to try to keep the first visible row in a fixed + // position (from the user's perspective). We try to keep it positioned at + // the same offset from the top of the screen so adding new items seems + // smoother, as opposed to having it "snap" to a multiple of row height + // (as would happen by simply calling [UITableView + // scrollToRowAtIndexPath:atScrollPosition:animated:]. + + // Which object is currently at the top of the table (the "anchor" object)? + // (If possible, we choose the second row, to give context above and below and avoid + // cases where the first row is only barely visible, thus providing little context.) + NSArray *visibleRowIndexPaths = [self.tableView indexPathsForVisibleRows]; + if (visibleRowIndexPaths.count > 0) { + int anchorRowIndex = (visibleRowIndexPaths.count > 1) ? 1 : 0; + NSIndexPath *anchorIndexPath = [visibleRowIndexPaths objectAtIndex:anchorRowIndex]; + id anchorObject = [self.dataSource itemAtIndexPath:anchorIndexPath]; + + // What is its rect, and what is the overall contentOffset of the table? + CGRect anchorRowRectBefore = [self.tableView rectForRowAtIndexPath:anchorIndexPath]; + CGPoint contentOffset = self.tableView.contentOffset; + + // Update with new data and reload the table. + [self.dataSource appendGraphObjects:data]; + [self updateView]; + + // Where is the anchor object now? + anchorIndexPath = [self.dataSource indexPathForItem:anchorObject]; + CGRect anchorRowRectAfter = [self.tableView rectForRowAtIndexPath:anchorIndexPath]; + + // Keep the content offset the same relative to the rect of the row (so if it was + // 1/4 scrolled off the top before, it still will be, etc.) + contentOffset.y += anchorRowRectAfter.origin.y - anchorRowRectBefore.origin.y; + self.tableView.contentOffset = contentOffset; + } + } + + if ([self.delegate respondsToSelector:@selector(pagingLoader:didLoadData:)]) { + [self.delegate pagingLoader:self didLoadData:results]; + } + + // If we are supposed to keep paging, do so. But unless we are viewless, if we have lost + // our tableView, take that as a sign to stop (probably because the view was unloaded). + // If tableView is re-set, we will start again. + if ((self.pagingMode == FBGraphObjectPagingModeImmediate && + self.tableView) || + self.pagingMode == FBGraphObjectPagingModeImmediateViewless) { + [self followNextLink]; + } +} + +- (void)followNextLink { + if (self.nextLink && + self.session) { + [self.connection cancel]; + self.connection = nil; + + if ([self.delegate respondsToSelector:@selector(pagingLoader:willLoadURL:)]) { + [self.delegate pagingLoader:self willLoadURL:self.nextLink]; + } + + FBRequest *request = [[FBRequest alloc] initWithSession:self.session + graphPath:nil]; + + FBRequestConnection *connection = [[FBRequestConnection alloc] init]; + [connection addRequest:request completionHandler: + ^(FBRequestConnection *connection, id result, NSError *error) { + _isResultFromCache = _isResultFromCache || connection.isResultFromCache; + self.connection = nil; + [self requestCompleted:connection result:result error:error]; + }]; + + // Override the URL using the one passed back in 'next'. + NSURL *url = [NSURL URLWithString:self.nextLink]; + NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url]; + connection.urlRequest = urlRequest; + + self.nextLink = nil; + + self.connection = connection; + [self.connection startWithCacheIdentity:self.cacheIdentity + skipRoundtripIfCached:self.skipRoundtripIfCached]; + + [request release]; + [connection release]; + } +} + +- (void)startLoadingWithRequest:(FBRequest*)request + cacheIdentity:(NSString*)cacheIdentity + skipRoundtripIfCached:(BOOL)skipRoundtripIfCached { + [self.dataSource prepareForNewRequest]; + + [self.connection cancel]; + _isResultFromCache = NO; + + self.cacheIdentity = cacheIdentity; + self.skipRoundtripIfCached = skipRoundtripIfCached; + + FBRequestConnection *connection = [[FBRequestConnection alloc] init]; + [connection addRequest:request + completionHandler:^(FBRequestConnection *connection, id result, NSError *error) { + _isResultFromCache = _isResultFromCache || connection.isResultFromCache; + [self requestCompleted:connection result:result error:error]; + }]; + + self.connection = connection; + [self.connection startWithCacheIdentity:self.cacheIdentity + skipRoundtripIfCached:self.skipRoundtripIfCached]; + + [connection release]; + + NSString *urlString = [[[self.connection urlRequest] URL] absoluteString]; + if ([self.delegate respondsToSelector:@selector(pagingLoader:willLoadURL:)]) { + [self.delegate pagingLoader:self willLoadURL:urlString]; + } +} + +- (void)cancel { + [self.connection cancel]; +} + +- (void)reset { + [self cancel]; + self.connection = nil; + self.nextLink = nil; +} + +- (void)requestCompleted:(FBRequestConnection *)connection + result:(id)result + error:(NSError *)error { + self.connection = nil; + + NSDictionary *resultDictionary = (NSDictionary *)result; + + NSArray *data = nil; + if (!error && [result isKindOfClass:[NSDictionary class]]) { + id rawData = [resultDictionary objectForKey:@"data"]; + if ([rawData isKindOfClass:[NSArray class]]) { + data = (NSArray *)rawData; + } + } + + if (!error && !data) { + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:result + forKey:FBErrorParsedJSONResponseKey]; + error = [[[NSError alloc] initWithDomain:FacebookSDKDomain + code:FBErrorProtocolMismatch + userInfo:userInfo] + autorelease]; + } + + if (error) { + // Cancellation is not really an error we want to bother the delegate with. + BOOL cancelled = [error.domain isEqualToString:FacebookSDKDomain] && + error.code == FBErrorOperationCancelled; + + if (cancelled) { + if ([self.delegate respondsToSelector:@selector(pagingLoaderWasCancelled:)]) { + [self.delegate pagingLoaderWasCancelled:self]; + } + } else if ([self.delegate respondsToSelector:@selector(pagingLoader:handleError:)]) { + [self.delegate pagingLoader:self handleError:error]; + } + } else { + [self addResultsAndUpdateView:resultDictionary]; + } +} + +#pragma mark FBGraphObjectDataSourceDataNeededDelegate methods + +- (void)graphObjectTableDataSourceNeedsData:(FBGraphObjectTableDataSource *)dataSource triggeredByIndexPath:(NSIndexPath*)indexPath { + if (self.pagingMode == FBGraphObjectPagingModeAsNeeded) { + [self followNextLink]; + } +} + +#pragma mark - + +@end diff --git a/src/ios/facebook/FBGraphObjectTableCell.h b/src/ios/facebook/FBGraphObjectTableCell.h new file mode 100644 index 000000000..c2752d05a --- /dev/null +++ b/src/ios/facebook/FBGraphObjectTableCell.h @@ -0,0 +1,36 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +@interface FBGraphObjectTableCell : UITableViewCell + +// We allow the title to be split into two parts, with one (or both) optionally +// bolded. titleSuffix will be appended to the end of title with a space in between. +@property (copy, nonatomic) NSString *title; +@property (copy, nonatomic) NSString *titleSuffix; +@property (nonatomic) BOOL boldTitle; +@property (nonatomic) BOOL boldTitleSuffix; + +@property (copy, nonatomic) NSString *subtitle; +@property (retain, nonatomic) UIImage *picture; + ++ (CGFloat)rowHeight; + +- (void)startAnimatingActivityIndicator; +- (void)stopAnimatingActivityIndicator; + +@end diff --git a/src/ios/facebook/FBGraphObjectTableCell.m b/src/ios/facebook/FBGraphObjectTableCell.m new file mode 100644 index 000000000..742b6da11 --- /dev/null +++ b/src/ios/facebook/FBGraphObjectTableCell.m @@ -0,0 +1,228 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBGraphObjectTableCell.h" + +static const CGFloat titleFontHeight = 16; +static const CGFloat subtitleFontHeight = 12; +static const CGFloat pictureEdge = 40; +static const CGFloat pictureMargin = 1; +static const CGFloat horizontalMargin = 4; +static const CGFloat titleTopNoSubtitle = 11; +static const CGFloat titleTopWithSubtitle = 3; +static const CGFloat subtitleTop = 23; +static const CGFloat titleHeight = titleFontHeight * 1.25; +static const CGFloat subtitleHeight = subtitleFontHeight * 1.25; + +@interface FBGraphObjectTableCell() + +@property (nonatomic, retain) UIImageView *pictureView;\ +@property (nonatomic, retain) UILabel* titleSuffixLabel; +@property (nonatomic, retain) UIActivityIndicatorView *activityIndicator; + +- (void)updateFonts; + +@end + +@implementation FBGraphObjectTableCell + +@synthesize pictureView = _pictureView; +@synthesize titleSuffixLabel = _titleSuffixLabel; +@synthesize activityIndicator = _activityIndicator; +@synthesize boldTitle = _boldTitle; +@synthesize boldTitleSuffix = _boldTitleSuffix; + +#pragma mark - Lifecycle + +- (id)initWithStyle:(UITableViewCellStyle)style + reuseIdentifier:(NSString*)reuseIdentifier +{ + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + // Picture + UIImageView *pictureView = [[UIImageView alloc] init]; + pictureView.clipsToBounds = YES; + pictureView.contentMode = UIViewContentModeScaleAspectFill; + + self.pictureView = pictureView; + [self.contentView addSubview:pictureView]; + [pictureView release]; + + // Subtitle + self.detailTextLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; + self.detailTextLabel.textColor = [UIColor colorWithRed:0.4 green:0.6 blue:0.8 alpha:1.0]; + self.detailTextLabel.font = [UIFont systemFontOfSize:subtitleFontHeight]; + + // Title + self.textLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; + self.textLabel.font = [UIFont systemFontOfSize:titleFontHeight]; + + // Content View + self.contentView.clipsToBounds = YES; + } + + return self; +} + +- (void)dealloc +{ + [_titleSuffixLabel release]; + [_pictureView release]; + + [super dealloc]; +} + +#pragma mark - + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + [self updateFonts]; + + BOOL hasPicture = (self.picture != nil); + BOOL hasSubtitle = (self.subtitle != nil); + BOOL hasTitleSuffix = (self.titleSuffix != nil); + + CGFloat pictureWidth = hasPicture ? pictureEdge : 0; + CGSize cellSize = self.contentView.bounds.size; + CGFloat textLeft = (hasPicture ? ((2 * pictureMargin) + pictureWidth) : 0) + horizontalMargin; + CGFloat textWidth = cellSize.width - (textLeft + horizontalMargin); + CGFloat titleTop = hasSubtitle ? titleTopWithSubtitle : titleTopNoSubtitle; + + self.pictureView.frame = CGRectMake(pictureMargin, pictureMargin, pictureEdge, pictureWidth); + self.detailTextLabel.frame = CGRectMake(textLeft, subtitleTop, textWidth, subtitleHeight); + if (!hasTitleSuffix) { + self.textLabel.frame = CGRectMake(textLeft, titleTop, textWidth, titleHeight); + } else { + CGSize titleSize = [self.textLabel.text sizeWithFont:self.textLabel.font]; + CGSize spaceSize = [@" " sizeWithFont:self.textLabel.font]; + CGFloat titleWidth = titleSize.width + spaceSize.width; + self.textLabel.frame = CGRectMake(textLeft, titleTop, titleWidth, titleHeight); + + CGFloat titleSuffixLeft = textLeft + titleWidth; + CGFloat titleSuffixWidth = textWidth - titleWidth; + self.titleSuffixLabel.frame = CGRectMake(titleSuffixLeft, titleTop, titleSuffixWidth, titleHeight); + } + + [self.pictureView setHidden:!(hasPicture)]; + [self.detailTextLabel setHidden:!(hasSubtitle)]; + [self.titleSuffixLabel setHidden:!(hasTitleSuffix)]; +} + ++ (CGFloat)rowHeight +{ + return pictureEdge + (2 * pictureMargin) + 1; +} + +- (void)startAnimatingActivityIndicator { + CGRect cellBounds = self.bounds; + if (!self.activityIndicator) { + UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + activityIndicator.hidesWhenStopped = YES; + activityIndicator.autoresizingMask = + (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin); + + self.activityIndicator = activityIndicator; + [self addSubview:activityIndicator]; + [activityIndicator release]; + } + + self.activityIndicator.center = CGPointMake(CGRectGetMidX(cellBounds), CGRectGetMidY(cellBounds)); + + [self.activityIndicator startAnimating]; +} + +- (void)stopAnimatingActivityIndicator { + if (self.activityIndicator) { + [self.activityIndicator stopAnimating]; + } +} + +- (void)updateFonts { + if (self.boldTitle) { + self.textLabel.font = [UIFont boldSystemFontOfSize:titleFontHeight]; + } else { + self.textLabel.font = [UIFont systemFontOfSize:titleFontHeight]; + } + + if (self.boldTitleSuffix) { + self.titleSuffixLabel.font = [UIFont boldSystemFontOfSize:titleFontHeight]; + } else { + self.titleSuffixLabel.font = [UIFont systemFontOfSize:titleFontHeight]; + } +} + +- (void)createTitleSuffixLabel { + if (!self.titleSuffixLabel) { + UILabel *titleSuffixLabel = [[UILabel alloc] init]; + titleSuffixLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [self.contentView addSubview:titleSuffixLabel]; + + self.titleSuffixLabel = titleSuffixLabel; + [titleSuffixLabel release]; + } +} +#pragma mark - Properties + +- (UIImage *)picture +{ + return self.pictureView.image; +} + +- (void)setPicture:(UIImage *)picture +{ + self.pictureView.image = picture; + [self setNeedsLayout]; +} + +- (NSString*)subtitle +{ + return self.detailTextLabel.text; +} + +- (void)setSubtitle:(NSString *)subtitle +{ + self.detailTextLabel.text = subtitle; + [self setNeedsLayout]; +} + +- (NSString*)title +{ + return self.textLabel.text; +} + +- (void)setTitle:(NSString *)title +{ + self.textLabel.text = title; + [self setNeedsLayout]; +} + +- (NSString*)titleSuffix +{ + return self.titleSuffixLabel.text; +} + +- (void)setTitleSuffix:(NSString *)titleSuffix +{ + if (titleSuffix) { + [self createTitleSuffixLabel]; + self.titleSuffixLabel.text = titleSuffix; + } + [self setNeedsLayout]; +} + +@end diff --git a/src/ios/facebook/FBGraphObjectTableDataSource.h b/src/ios/facebook/FBGraphObjectTableDataSource.h new file mode 100644 index 000000000..2267ddc3d --- /dev/null +++ b/src/ios/facebook/FBGraphObjectTableDataSource.h @@ -0,0 +1,102 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBGraphObject.h" + +@protocol FBGraphObjectViewControllerDelegate; +@protocol FBGraphObjectSelectionQueryDelegate; +@protocol FBGraphObjectDataSourceDataNeededDelegate; +@class FBGraphObjectTableCell; + +@interface FBGraphObjectTableDataSource : NSObject + +@property (nonatomic, retain) UIImage *defaultPicture; +@property (nonatomic, assign) id controllerDelegate; +@property (nonatomic, copy) NSString *groupByField; +@property (nonatomic, assign) BOOL useCollation; +@property (nonatomic) BOOL itemTitleSuffixEnabled; +@property (nonatomic) BOOL itemPicturesEnabled; +@property (nonatomic) BOOL itemSubtitleEnabled; +@property (nonatomic, assign) id selectionDelegate; +@property (nonatomic, assign) id dataNeededDelegate; +@property (nonatomic, copy) NSArray *sortDescriptors; + +- (NSString *)fieldsForRequestIncluding:(NSSet *)customFields, ...; + +- (void)setSortingBySingleField:(NSString*)fieldName ascending:(BOOL)ascending; +- (void)setSortingByFields:(NSArray*)fieldNames ascending:(BOOL)ascending; + +- (void)prepareForNewRequest; +// Clears all graph objects from the data source. +- (void)clearGraphObjects; +// Adds additional graph objects (pass nil to indicate all objects have been added). +- (void)appendGraphObjects:(NSArray *)data; +- (BOOL)hasGraphObjects; + +- (void)bindTableView:(UITableView *)tableView; + +- (void)cancelPendingRequests; + +// Call this when updating any property or if +// delegate.filterIncludesItem would return a different answer now. +- (void)update; + +// Returns the graph object at a given indexPath. +- (FBGraphObject *)itemAtIndexPath:(NSIndexPath *)indexPath; + +// Returns the indexPath for a given graph object. +- (NSIndexPath *)indexPathForItem:(FBGraphObject *)item; + +@end + +@protocol FBGraphObjectViewControllerDelegate +@required + +- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + titleOfItem:(id)graphObject; + +@optional + +- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + titleSuffixOfItem:(id)graphObject; + +- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + subtitleOfItem:(id)graphObject; + +- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + pictureUrlOfItem:(id)graphObject; + +- (BOOL)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + filterIncludesItem:(id)item; + +- (void)graphObjectTableDataSource:(FBGraphObjectTableDataSource*)dataSource + customizeTableCell:(FBGraphObjectTableCell*)cell; + +@end + +@protocol FBGraphObjectSelectionQueryDelegate + +- (BOOL)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + selectionIncludesItem:(id)item; + +@end + +@protocol FBGraphObjectDataSourceDataNeededDelegate + +- (void)graphObjectTableDataSourceNeedsData:(FBGraphObjectTableDataSource *)dataSource triggeredByIndexPath:(NSIndexPath*)indexPath; + +@end diff --git a/src/ios/facebook/FBGraphObjectTableDataSource.m b/src/ios/facebook/FBGraphObjectTableDataSource.m new file mode 100644 index 000000000..322822377 --- /dev/null +++ b/src/ios/facebook/FBGraphObjectTableDataSource.m @@ -0,0 +1,585 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBGraphObjectTableDataSource.h" +#import "FBGraphObjectTableCell.h" +#import "FBGraphObject.h" +#import "FBURLConnection.h" +#import "FBUtility.h" + +// Magic number - iPhone address book doesn't show scrubber for less than 5 contacts +static const NSInteger kMinimumCountToCollate = 6; + +@interface FBGraphObjectTableDataSource () + +@property (nonatomic, retain) NSArray *data; +@property (nonatomic, retain) NSArray *indexKeys; +@property (nonatomic, retain) NSDictionary *indexMap; +@property (nonatomic, retain) NSMutableSet *pendingURLConnections; +@property (nonatomic, assign) BOOL expectingMoreGraphObjects; +@property (nonatomic, retain) UILocalizedIndexedCollation *collation; +@property (nonatomic, assign) BOOL showSections; + +- (BOOL)filterIncludesItem:(FBGraphObject *)item; +- (FBGraphObjectTableCell *)cellWithTableView:(UITableView *)tableView; +- (NSString *)indexKeyOfItem:(FBGraphObject *)item; +- (UIImage *)tableView:(UITableView *)tableView imageForItem:(FBGraphObject *)item; +- (void)addOrRemovePendingConnection:(FBURLConnection *)connection; +- (BOOL)isActivityIndicatorIndexPath:(NSIndexPath *)indexPath; +- (BOOL)isLastSection:(NSInteger)section; + +@end + +@implementation FBGraphObjectTableDataSource + +@synthesize data = _data; +@synthesize defaultPicture = _defaultPicture; +@synthesize controllerDelegate = _controllerDelegate; +@synthesize groupByField = _groupByField; +@synthesize useCollation = _useCollation; +@synthesize showSections = _showSections; +@synthesize indexKeys = _indexKeys; +@synthesize indexMap = _indexMap; +@synthesize itemTitleSuffixEnabled = _itemTitleSuffixEnabled; +@synthesize itemPicturesEnabled = _itemPicturesEnabled; +@synthesize itemSubtitleEnabled = _itemSubtitleEnabled; +@synthesize pendingURLConnections = _pendingURLConnections; +@synthesize selectionDelegate = _selectionDelegate; +@synthesize sortDescriptors = _sortDescriptors; +@synthesize dataNeededDelegate = _dataNeededDelegate; +@synthesize expectingMoreGraphObjects = _expectingMoreGraphObjects; +@synthesize collation = _collation; + +- (void)setUseCollation:(BOOL)useCollation +{ + if (_useCollation != useCollation) { + _useCollation = useCollation; + self.collation = _useCollation ? [UILocalizedIndexedCollation currentCollation] : nil; + } +} + +- (id)init +{ + self = [super init]; + + if (self) { + NSMutableSet *pendingURLConnections = [[NSMutableSet alloc] init]; + self.pendingURLConnections = pendingURLConnections; + [pendingURLConnections release]; + self.expectingMoreGraphObjects = YES; + } + + return self; +} + +- (void)dealloc +{ + FBConditionalLog(![_pendingURLConnections count], + @"FBGraphObjectTableDataSource pending connection did not retain self"); + + [_collation release]; + [_data release]; + [_defaultPicture release]; + [_groupByField release]; + [_indexKeys release]; + [_indexMap release]; + [_pendingURLConnections release]; + [_sortDescriptors release]; + + [super dealloc]; +} + +#pragma mark - Public Methods + +- (NSString *)fieldsForRequestIncluding:(NSSet *)customFields, ... +{ + // Start with custom fields. + NSMutableSet *nameSet = [[NSMutableSet alloc] initWithSet:customFields]; + + // Iterate through varargs after the initial set, and add them + id vaName; + va_list vaArguments; + va_start(vaArguments, customFields); + while ((vaName = va_arg(vaArguments, id))) { + [nameSet addObject:vaName]; + } + va_end(vaArguments); + + // Add fields needed for data source functionality. + if (self.groupByField) { + [nameSet addObject:self.groupByField]; + } + + // get a stable order for our fields, because we use the resulting URL as a cache ID + NSMutableArray *sortedFields = [[nameSet allObjects] mutableCopy]; + [sortedFields sortUsingSelector:@selector(caseInsensitiveCompare:)]; + + [nameSet release]; + + // Build the comma-separated string + NSMutableString *fields = [[[NSMutableString alloc] init] autorelease]; + + for (NSString *field in sortedFields) { + if ([fields length]) { + [fields appendString:@","]; + } + [fields appendString:field]; + } + + [sortedFields release]; + return fields; +} + +- (void)prepareForNewRequest { + self.data = nil; + self.expectingMoreGraphObjects = YES; +} + +- (void)clearGraphObjects { + self.indexKeys = nil; + self.indexMap = nil; + [self prepareForNewRequest]; +} + +- (void)appendGraphObjects:(NSArray *)data +{ + if (self.data) { + self.data = [self.data arrayByAddingObjectsFromArray:data]; + } else { + self.data = data; + } + if (data == nil) { + self.expectingMoreGraphObjects = NO; + } +} + +- (BOOL)hasGraphObjects { + return self.data && self.data.count > 0; +} + +- (void)bindTableView:(UITableView *)tableView +{ + tableView.dataSource = self; + tableView.rowHeight = [FBGraphObjectTableCell rowHeight]; +} + +- (void)cancelPendingRequests +{ + // Cancel all active connections. + for (FBURLConnection *connection in _pendingURLConnections) { + [connection cancel]; + } +} + +// Called after changing any properties. To simplify the code here, +// since this class is internal, we do not auto-update on property +// changes. +// +// This builds indexMap and indexKeys, the data structures used to +// respond to UITableDataSource protocol requests. UITable expects +// a list of section names, and then ask for items given a section +// index and item index within that section. In addition, we need +// to do reverse mapping from item to table location. +// +// To facilitate both of these, we build an array of section titles, +// and a dictionary mapping title -> item array. We could consider +// building a reverse-lookup map too, but this seems unnecessary. +- (void)update +{ + NSInteger objectsShown = 0; + NSMutableDictionary *indexMap = [[[NSMutableDictionary alloc] init] autorelease]; + NSMutableArray *indexKeys = [[[NSMutableArray alloc] init] autorelease]; + + for (FBGraphObject *item in self.data) { + if (![self filterIncludesItem:item]) { + continue; + } + + NSString *key = [self indexKeyOfItem:item]; + NSMutableArray *existingSection = [indexMap objectForKey:key]; + NSMutableArray *section = existingSection; + + if (!section) { + section = [[[NSMutableArray alloc] init] autorelease]; + } + [section addObject:item]; + + if (!existingSection) { + [indexMap setValue:section forKey:key]; + [indexKeys addObject:key]; + } + objectsShown++; + } + + if (self.sortDescriptors) { + for (NSString *key in indexKeys) { + [[indexMap objectForKey:key] sortUsingDescriptors:self.sortDescriptors]; + } + } + if (!self.useCollation) { + [indexKeys sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; + } + + self.showSections = objectsShown >= kMinimumCountToCollate; + self.indexKeys = indexKeys; + self.indexMap = indexMap; +} + +#pragma mark - Private Methods + +- (BOOL)filterIncludesItem:(FBGraphObject *)item +{ + if (![self.controllerDelegate respondsToSelector: + @selector(graphObjectTableDataSource:filterIncludesItem:)]) { + return YES; + } + + return [self.controllerDelegate graphObjectTableDataSource:self + filterIncludesItem:item]; +} + +- (void)setSortingByFields:(NSArray*)fieldNames ascending:(BOOL)ascending { + NSMutableArray *sortDescriptors = [NSMutableArray arrayWithCapacity:fieldNames.count]; + for (NSString *fieldName in fieldNames) { + NSSortDescriptor *sortBy = [NSSortDescriptor + sortDescriptorWithKey:fieldName + ascending:ascending + selector:@selector(localizedCaseInsensitiveCompare:)]; + [sortDescriptors addObject:sortBy]; + } + self.sortDescriptors = sortDescriptors; +} + +- (void)setSortingBySingleField:(NSString*)fieldName ascending:(BOOL)ascending { + [self setSortingByFields:[NSArray arrayWithObject:fieldName] ascending:ascending]; +} + +- (FBGraphObjectTableCell *)cellWithTableView:(UITableView *)tableView +{ + static NSString * const cellKey = @"fbTableCell"; + FBGraphObjectTableCell *cell = + (FBGraphObjectTableCell*)[tableView dequeueReusableCellWithIdentifier:cellKey]; + + if (!cell) { + cell = [[FBGraphObjectTableCell alloc] + initWithStyle:UITableViewCellStyleSubtitle + reuseIdentifier:cellKey]; + [cell autorelease]; + + cell.selectionStyle = UITableViewCellSelectionStyleNone; + } + + return cell; +} + +- (NSString *)indexKeyOfItem:(FBGraphObject *)item +{ + NSString *text = @""; + + if (self.groupByField) { + text = [item objectForKey:self.groupByField]; + } + + if (self.useCollation) { + NSInteger collationSection = [self.collation sectionForObject:item collationStringSelector:NSSelectorFromString(self.groupByField)]; + text = [[self.collation sectionTitles] objectAtIndex:collationSection]; + } else { + + if ([text length] > 1) { + text = [text substringToIndex:1]; + } + + text = [text uppercaseString]; + } + return text; +} + +- (FBGraphObject *)itemAtIndexPath:(NSIndexPath *)indexPath +{ + id key = nil; + if (self.useCollation) { + NSString *sectionTitle = [self.collation.sectionTitles objectAtIndex:indexPath.section]; + key = sectionTitle; + } else if (indexPath.section >= 0 && indexPath.section < self.indexKeys.count) { + key = [self.indexKeys objectAtIndex:indexPath.section]; + } + NSArray *sectionItems = [self.indexMap objectForKey:key]; + if (indexPath.row >= 0 && indexPath.row < sectionItems.count) { + return [sectionItems objectAtIndex:indexPath.row]; + } + return nil; +} + +- (NSIndexPath *)indexPathForItem:(FBGraphObject *)item +{ + NSString *key = [self indexKeyOfItem:item]; + NSMutableArray *sectionItems = [self.indexMap objectForKey:key]; + if (!sectionItems) { + return nil; + } + + NSInteger sectionIndex = 0; + if (self.useCollation) { + sectionIndex = [self.collation.sectionTitles indexOfObject:key]; + } else { + sectionIndex = [self.indexKeys indexOfObject:key]; + } + if (sectionIndex == NSNotFound) { + return nil; + } + + id matchingObject = [FBUtility graphObjectInArray:sectionItems withSameIDAs:item]; + if (matchingObject == nil) { + return nil; + } + + NSInteger itemIndex = [sectionItems indexOfObject:matchingObject]; + if (itemIndex == NSNotFound) { + return nil; + } + + return [NSIndexPath indexPathForRow:itemIndex inSection:sectionIndex]; +} + +- (BOOL)isLastSection:(NSInteger)section { + if (self.useCollation) { + return section == self.collation.sectionTitles.count - 1; + } else { + return section == self.indexKeys.count - 1; + } +} + +- (BOOL)isActivityIndicatorIndexPath:(NSIndexPath *)indexPath { + if ([self isLastSection:indexPath.section]) { + NSArray *sectionItems = [self sectionItemsForSection:indexPath.section]; + + if (indexPath.row == sectionItems.count) { + // Last section has one more row that items if we are expecting more objects. + return YES; + } + } + return NO; +} + + +- (NSString *)titleForSection:(NSInteger)sectionIndex +{ + id key; + if (self.useCollation) { + NSString *sectionTitle = [self.collation.sectionTitles objectAtIndex:sectionIndex]; + key = sectionTitle; + } else { + key = [self.indexKeys objectAtIndex:sectionIndex]; + } + return key; +} + +- (NSArray *)sectionItemsForSection:(NSInteger)sectionIndex +{ + id key = [self titleForSection:sectionIndex]; + NSArray *sectionItems = [self.indexMap objectForKey:key]; + return sectionItems; +} +- (UIImage *)tableView:(UITableView *)tableView imageForItem:(FBGraphObject *)item +{ + __block UIImage *image = nil; + NSString *urlString = [self.controllerDelegate graphObjectTableDataSource:self + pictureUrlOfItem:item]; + if (urlString) { + FBURLConnectionHandler handler = + ^(FBURLConnection *connection, NSError *error, NSURLResponse *response, NSData *data) { + [self addOrRemovePendingConnection:connection]; + if (!error) { + image = [UIImage imageWithData:data]; + + NSIndexPath *indexPath = [self indexPathForItem:item]; + if (indexPath) { + FBGraphObjectTableCell *cell = + (FBGraphObjectTableCell*)[tableView cellForRowAtIndexPath:indexPath]; + + if (cell) { + cell.picture = image; + } + } + } + }; + + FBURLConnection *connection = [[[FBURLConnection alloc] + initWithURL:[NSURL URLWithString:urlString] + completionHandler:handler] + autorelease]; + + [self addOrRemovePendingConnection:connection]; + } + + // If the picture had not been fetched yet by this object, but is cached in the + // URL cache, we can complete synchronously above. In this case, we will not + // find the cell in the table because we are in the process of creating it. We can + // just return the object here. + if (image) { + return image; + } + + return self.defaultPicture; +} + +// In tableView:imageForItem:, there are two code-paths, and both always run. +// Whichever runs first adds the connection to the collection of pending requests, +// and whichever runs second removes it. This allows us to track all requests +// for which one code-path has run and the other has not. +- (void)addOrRemovePendingConnection:(FBURLConnection *)connection +{ + if ([self.pendingURLConnections containsObject:connection]) { + [self.pendingURLConnections removeObject:connection]; + } else { + [self.pendingURLConnections addObject:connection]; + } +} + +#pragma mark - UITableViewDataSource + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + if (self.useCollation) { + return self.collation.sectionTitles.count; + } else { + return [self.indexKeys count]; + } +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + NSArray *sectionItems = [self sectionItemsForSection:section]; + + int count = [sectionItems count]; + // If we are expecting more objects to be loaded via paging, add 1 to the + // row count for the last section. + if (self.expectingMoreGraphObjects && + self.dataNeededDelegate && + [self isLastSection:section]) { + ++count; + } + return count; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section +{ + if (!self.showSections) { + return nil; + } + + NSArray *sectionItems = [self sectionItemsForSection:section]; + return sectionItems.count > 0 ? [self titleForSection:section] : nil; +} + +- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index +{ + if (self.useCollation) { + return [self.collation sectionForSectionIndexTitleAtIndex:index]; + } else { + return index; + } +} + +- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView +{ + if (!self.showSections) { + return nil; + } + + if (self.useCollation) { + return self.collation.sectionIndexTitles; + } else { + return [self.indexKeys count] > 1 ? self.indexKeys : nil; + } +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + return NO; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + FBGraphObjectTableCell *cell = [self cellWithTableView:tableView]; + + if ([self isActivityIndicatorIndexPath:indexPath]) { + cell.picture = nil; + cell.subtitle = nil; + cell.title = nil; + cell.accessoryType = UITableViewCellAccessoryNone; + cell.selected = NO; + + [cell startAnimatingActivityIndicator]; + + [self.dataNeededDelegate graphObjectTableDataSourceNeedsData:self + triggeredByIndexPath:indexPath]; + } else { + FBGraphObject *item = [self itemAtIndexPath:indexPath]; + + // This is a no-op if it doesn't have an activity indicator. + [cell stopAnimatingActivityIndicator]; + if (item) { + if (self.itemPicturesEnabled) { + cell.picture = [self tableView:tableView imageForItem:item]; + } else { + cell.picture = nil; + } + + if (self.itemTitleSuffixEnabled) { + cell.titleSuffix = [self.controllerDelegate graphObjectTableDataSource:self + titleSuffixOfItem:item]; + } else { + cell.titleSuffix = nil; + } + + if (self.itemSubtitleEnabled) { + cell.subtitle = [self.controllerDelegate graphObjectTableDataSource:self + subtitleOfItem:item]; + } else { + cell.subtitle = nil; + } + + cell.title = [self.controllerDelegate graphObjectTableDataSource:self + titleOfItem:item]; + + if ([self.selectionDelegate graphObjectTableDataSource:self + selectionIncludesItem:item]) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + cell.selected = YES; + } else { + cell.accessoryType = UITableViewCellAccessoryNone; + cell.selected = NO; + } + + if ([self.controllerDelegate respondsToSelector:@selector(graphObjectTableDataSource:customizeTableCell:)]) { + [self.controllerDelegate graphObjectTableDataSource:self + customizeTableCell:cell]; + } + } else { + cell.picture = nil; + cell.subtitle = nil; + cell.title = nil; + cell.accessoryType = UITableViewCellAccessoryNone; + cell.selected = NO; + } + } + + return cell; +} + +@end diff --git a/src/ios/facebook/FBGraphObjectTableSelection.h b/src/ios/facebook/FBGraphObjectTableSelection.h new file mode 100644 index 000000000..ac62edeeb --- /dev/null +++ b/src/ios/facebook/FBGraphObjectTableSelection.h @@ -0,0 +1,38 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBGraphObjectTableDataSource.h" + +@protocol FBGraphObjectSelectionChangedDelegate; + +@interface FBGraphObjectTableSelection : NSObject + +@property (nonatomic, assign) NSObject *delegate; +@property (nonatomic, retain, readonly) NSArray *selection; +@property (nonatomic) BOOL allowsMultipleSelection; + +- (id)initWithDataSource:(FBGraphObjectTableDataSource *)dataSource; +- (void)clearSelectionInTableView:(UITableView*)tableView; + + +@end + +@protocol FBGraphObjectSelectionChangedDelegate + +- (void)graphObjectTableSelectionDidChange:(FBGraphObjectTableSelection *)selection; + +@end \ No newline at end of file diff --git a/src/ios/facebook/FBGraphObjectTableSelection.m b/src/ios/facebook/FBGraphObjectTableSelection.m new file mode 100644 index 000000000..f788f719e --- /dev/null +++ b/src/ios/facebook/FBGraphObjectTableSelection.m @@ -0,0 +1,202 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBGraphObjectTableSelection.h" +#import "FBUtility.h" + +@interface FBGraphObjectTableSelection() + +@property (nonatomic, retain) FBGraphObjectTableDataSource *dataSource; +@property (nonatomic, retain) NSArray *selection; + +- (void)selectItem:(FBGraphObject *)item + cell:(UITableViewCell *)cell; +- (void)deselectItem:(FBGraphObject *)item + cell:(UITableViewCell *)cell; +- (void)selectionChanged; + +@end + +@implementation FBGraphObjectTableSelection + +@synthesize dataSource = _dataSource; +@synthesize delegate = _delegate; +@synthesize selection = _selection; +@synthesize allowsMultipleSelection = _allowMultipleSelection; + +- (id)initWithDataSource:(FBGraphObjectTableDataSource *)dataSource +{ + self = [super init]; + + if (self) { + dataSource.selectionDelegate = self; + + self.dataSource = dataSource; + self.allowsMultipleSelection = YES; + + NSArray *selection = [[NSArray alloc] init]; + self.selection = selection; + [selection release]; + } + + return self; +} + +- (void)dealloc +{ + _dataSource.selectionDelegate = nil; + + [_dataSource release]; + [_selection release]; + + [super dealloc]; +} + +- (void)clearSelectionInTableView:(UITableView*)tableView { + [self deselectItems:self.selection tableView:tableView]; +} + +- (void)selectItem:(FBGraphObject *)item + cell:(UITableViewCell *)cell +{ + if ([FBUtility graphObjectInArray:self.selection withSameIDAs:item] == nil) { + NSMutableArray *selection = [[NSMutableArray alloc] initWithArray:self.selection]; + [selection addObject:item]; + self.selection = selection; + [selection release]; + } + cell.accessoryType = UITableViewCellAccessoryCheckmark; + [self selectionChanged]; +} + +- (void)deselectItem:(FBGraphObject *)item + cell:(UITableViewCell *)cell +{ + id selectedItem = [FBUtility graphObjectInArray:self.selection withSameIDAs:item]; + if (selectedItem) { + NSMutableArray *selection = [[NSMutableArray alloc] initWithArray:self.selection]; + [selection removeObject:selectedItem]; + self.selection = selection; + [selection release]; + } + cell.accessoryType = UITableViewCellAccessoryNone; + [self selectionChanged]; +} + +- (void)deselectItems:(NSArray*)items tableView:(UITableView*)tableView +{ + // Copy this so it doesn't change from under us. + items = [NSArray arrayWithArray:items]; + + for (FBGraphObject *item in items) { + NSIndexPath *indexPath = [self.dataSource indexPathForItem:item]; + + UITableViewCell *cell = nil; + if (indexPath != nil) { + cell = [tableView cellForRowAtIndexPath:indexPath]; + } + + [self deselectItem:item cell:cell]; + } +} + +- (void)selectionChanged +{ + if ([self.delegate respondsToSelector: + @selector(graphObjectTableSelectionDidChange:)]) { + // Let the table view finish updating its UI before notifying the delegate. + [self.delegate performSelector:@selector(graphObjectTableSelectionDidChange:) withObject:self afterDelay:.1]; + } +} + +- (BOOL)selectionIncludesItem:(id)item +{ + return [FBUtility graphObjectInArray:self.selection withSameIDAs:item] != nil; +} + +#pragma mark - FBGraphObjectSelectionDelegate + +- (BOOL)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + selectionIncludesItem:(id)item +{ + return [self selectionIncludesItem:item]; +} + +#pragma mark - UITableViewDelegate + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + // cell may be nil, which is okay, it will pick up the right selected state when it is created. + + FBGraphObject *item = [self.dataSource itemAtIndexPath:indexPath]; + if (item != nil) { + // We want to support multi-select on iOS <5.0, so rather than rely on the table view's notion + // of selection, just treat this as a toggle. If it is already selected, deselect it, and vice versa. + if (![self selectionIncludesItem:item]) { + if (self.allowsMultipleSelection == NO) { + // No multi-select allowed, deselect what is already selected. + [self deselectItems:self.selection tableView:tableView]; + } + [self selectItem:item cell:cell]; + } else { + [self deselectItem:item cell:cell]; + } + } +} + +- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (self.allowsMultipleSelection == NO) { + // Only deselect if we are not allowing multi select. Otherwise, the user will manually + // deselect this item by clicking on it again. + + // cell may be nil, which is okay, it will pick up the right selected state when it is created. + UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + + FBGraphObject *item = [self.dataSource itemAtIndexPath:indexPath]; + [self deselectItem:item cell:cell]; + } +} + +#pragma mark Debugging helpers + +- (NSString*)description { + NSMutableString *result = [NSMutableString stringWithFormat:@"<%@: %p, allowsMultipleSelection: %@, delegate: %p, selection: [", + NSStringFromClass([self class]), + self, + self.allowsMultipleSelection ? @"YES" : @"NO", + self.delegate]; + + bool firstItem = YES; + for (FBGraphObject *item in self.selection) { + id objectId = [item objectForKey:@"id"]; + if (!firstItem) { + [result appendFormat:@", "]; + } + firstItem = NO; + [result appendFormat:@"%@", (objectId != nil) ? objectId : @""]; + } + [result appendFormat:@"]>"]; + + return result; + +} + + +#pragma mark - + +@end diff --git a/src/ios/facebook/FBGraphPlace.h b/src/ios/facebook/FBGraphPlace.h new file mode 100644 index 000000000..9e9bc3a4e --- /dev/null +++ b/src/ios/facebook/FBGraphPlace.h @@ -0,0 +1,60 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBGraphLocation.h" +#import "FBGraphObject.h" + +/*! + @protocol + + @abstract + The `FBGraphPlace` protocol enables typed access to a place object + as represented in the Graph API. + + + @discussion + The `FBGraphPlace` protocol represents the most commonly used properties of a + Facebook place object. It may be used to access an `NSDictionary` object that has + been wrapped with an facade. + */ +@protocol FBGraphPlace + +/*! + @property + @abstract Typed access to the place ID. + */ +@property (retain, nonatomic) NSString *id; + +/*! + @property + @abstract Typed access to the place name. + */ +@property (retain, nonatomic) NSString *name; + +/*! + @property + @abstract Typed access to the place category. + */ +@property (retain, nonatomic) NSString *category; + +/*! + @property + @abstract Typed access to the place location. + */ +@property (retain, nonatomic) id location; + +@end \ No newline at end of file diff --git a/src/ios/facebook/FBGraphUser.h b/src/ios/facebook/FBGraphUser.h new file mode 100644 index 000000000..5d7f6371b --- /dev/null +++ b/src/ios/facebook/FBGraphUser.h @@ -0,0 +1,90 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBGraphPlace.h" +#import "FBGraphObject.h" + +/*! + @protocol + + @abstract + The `FBGraphUser` protocol enables typed access to a user object + as represented in the Graph API. + + + @discussion + The `FBGraphUser` protocol represents the most commonly used properties of a + Facebook user object. It may be used to access an `NSDictionary` object that has + been wrapped with an facade. + */ +@protocol FBGraphUser + +/*! + @property + @abstract Typed access to the user's ID. + */ +@property (retain, nonatomic) NSString *id; + +/*! + @property + @abstract Typed access to the user's name. + */ +@property (retain, nonatomic) NSString *name; + +/*! + @property + @abstract Typed access to the user's first name. + */ +@property (retain, nonatomic) NSString *first_name; + +/*! + @property + @abstract Typed access to the user's middle name. + */ +@property (retain, nonatomic) NSString *middle_name; + +/*! + @property + @abstract Typed access to the user's last name. + */ +@property (retain, nonatomic) NSString *last_name; + +/*! + @property + @abstract Typed access to the user's profile URL. + */ +@property (retain, nonatomic) NSString *link; + +/*! + @property + @abstract Typed access to the user's username. + */ +@property (retain, nonatomic) NSString *username; + +/*! + @property + @abstract Typed access to the user's birthday. + */ +@property (retain, nonatomic) NSString *birthday; + +/*! + @property + @abstract Typed access to the user's current city. + */ +@property (retain, nonatomic) id location; + +@end \ No newline at end of file diff --git a/src/ios/facebook/FBLogger.h b/src/ios/facebook/FBLogger.h new file mode 100644 index 000000000..93a9c2e41 --- /dev/null +++ b/src/ios/facebook/FBLogger.h @@ -0,0 +1,86 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +/*! + @class FBLogger + + @abstract + Simple logging utility for conditionally logging strings and then emitting them + via NSLog(). + + @unsorted + */ +@interface FBLogger : NSObject + +// Access current accumulated contents of the logger. +@property (copy, nonatomic) NSString *contents; + +// Each FBLogger gets a unique serial number to allow the client to log these numbers and, for instance, correlation of Request/Response +@property (nonatomic, readonly) NSUInteger loggerSerialNumber; + +// The logging behavior of this logger. See the FB_LOG_BEHAVIOR* constants in FBSession.h +@property (copy, nonatomic, readonly) NSString *loggingBehavior; + +// Is the current logger instance active, based on its loggingBehavior? +@property (nonatomic, readonly) BOOL isActive; + +// +// Instance methods +// + +// Create with specified logging behavior +- (id)initWithLoggingBehavior:(NSString *)loggingBehavior; + +// Append string, or key/value pair +- (void)appendString:(NSString *)string; +- (void)appendFormat:(NSString *)formatString, ...; +- (void)appendKey:(NSString *)key value:(NSString *)value; + +// Emit log, clearing out the logger contents. +- (void)emitToNSLog; + +// +// Class methods +// + +// +// Return a globally unique serial number to be used for correlating multiple output from the same logger. +// ++ (NSUInteger)newSerialNumber; + +// Simple helper to write a single log entry, based upon whether the behavior matches a specified on. ++ (void)singleShotLogEntry:(NSString *)loggingBehavior + logEntry:(NSString *)logEntry; + ++ (void)singleShotLogEntry:(NSString *)loggingBehavior + formatString:(NSString *)formatString, ...; + ++ (void)singleShotLogEntry:(NSString *)loggingBehavior + timestampTag:(NSObject *)timestampTag + formatString:(NSString *)formatString, ...; + +// Register a timestamp label with the "current" time, to then be retrieved by singleShotLogEntry +// to include a duration. ++ (void)registerCurrentTime:(NSString *)loggingBehavior + withTag:(NSObject *)timestampTag; + +// When logging strings, replace all instances of 'replace' with instances of 'replaceWith'. ++ (void)registerStringToReplace:(NSString *)replace + replaceWith:(NSString *)replaceWith; + +@end diff --git a/src/ios/facebook/FBLogger.m b/src/ios/facebook/FBLogger.m new file mode 100644 index 000000000..9c52bc869 --- /dev/null +++ b/src/ios/facebook/FBLogger.m @@ -0,0 +1,219 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBLogger.h" +#import "FBSession.h" +#import "FBSettings.h" +#import "FBUtility.h" + +static NSUInteger g_serialNumberCounter = 1111; +static NSMutableDictionary *g_stringsToReplace = nil; +static NSMutableDictionary *g_startTimesWithTags = nil; + +@interface FBLogger () + +@property (nonatomic, retain, readonly) NSMutableString *internalContents; + +@end + +@implementation FBLogger + +@synthesize internalContents = _internalContents; +@synthesize isActive = _isActive; +@synthesize loggingBehavior = _loggingBehavior; +@synthesize loggerSerialNumber = _loggerSerialNumber; + +// Lifetime + +- (id)initWithLoggingBehavior:(NSString *)loggingBehavior { + if (self = [super init]) { + _isActive = [[FBSettings loggingBehavior] containsObject:loggingBehavior]; + _loggingBehavior = loggingBehavior; + if (_isActive) { + _internalContents = [[NSMutableString alloc] init]; + _loggerSerialNumber = [FBLogger newSerialNumber]; + } + } + + return self; +} + +- (void)dealloc { + [_internalContents release]; + [super dealloc]; +} + +// Public properties + +- (NSString *)contents { + return _internalContents; +} + +- (void)setContents:(NSString *)contents { + if (_isActive) { + [_internalContents release]; + _internalContents = [NSMutableString stringWithString:contents]; + } +} + +// Public instance methods + +- (void)appendString:(NSString *)string { + if (_isActive) { + [_internalContents appendString:string]; + } +} + +- (void)appendFormat:(NSString *)formatString, ... { + if (_isActive) { + va_list vaArguments; + va_start(vaArguments, formatString); + NSString *logString = [[[NSString alloc] initWithFormat:formatString arguments:vaArguments] autorelease]; + va_end(vaArguments); + + [self appendString:logString]; + } +} + + +- (void)appendKey:(NSString *)key value:(NSString *)value { + if (_isActive && [value length]) { + [_internalContents appendFormat:@" %@:\t%@\n", key, value]; + } +} + +- (void)emitToNSLog { + if (_isActive) { + + for (NSString *key in [g_stringsToReplace keyEnumerator]) { + [_internalContents replaceOccurrencesOfString:key + withString:[g_stringsToReplace objectForKey:key] + options:NSLiteralSearch + range:NSMakeRange(0, _internalContents.length)]; + } + + // Xcode 4.4 hangs on extremely long NSLog output (http://openradar.appspot.com/11972490). Truncate if needed. + const int MAX_LOG_STRING_LENGTH = 10000; + NSString *logString = _internalContents; + if (_internalContents.length > MAX_LOG_STRING_LENGTH) { + logString = [NSString stringWithFormat:@"TRUNCATED: %@", [_internalContents substringToIndex:MAX_LOG_STRING_LENGTH]]; + } + NSLog(@"FBSDKLog: %@", logString); + + [_internalContents setString:@""]; + } +} + +// Public static methods + ++ (NSUInteger)newSerialNumber { + return g_serialNumberCounter++; +} + ++ (void)singleShotLogEntry:(NSString *)loggingBehavior + logEntry:(NSString *)logEntry { + if ([[FBSettings loggingBehavior] containsObject:loggingBehavior]) { + FBLogger *logger = [[FBLogger alloc] initWithLoggingBehavior:loggingBehavior]; + [logger appendString:logEntry]; + [logger emitToNSLog]; + [logger release]; + } +} + ++ (void)singleShotLogEntry:(NSString *)loggingBehavior + formatString:(NSString *)formatString, ... { + + if ([[FBSettings loggingBehavior] containsObject:loggingBehavior]) { + va_list vaArguments; + va_start(vaArguments, formatString); + NSString *logString = [[[NSString alloc] initWithFormat:formatString arguments:vaArguments] autorelease]; + va_end(vaArguments); + + [self singleShotLogEntry:loggingBehavior logEntry:logString]; + } +} + + ++ (void)singleShotLogEntry:(NSString *)loggingBehavior + timestampTag:(NSObject *)timestampTag + formatString:(NSString *)formatString, ... { + + if ([[FBSettings loggingBehavior] containsObject:loggingBehavior]) { + va_list vaArguments; + va_start(vaArguments, formatString); + NSString *logString = [[[NSString alloc] initWithFormat:formatString arguments:vaArguments] autorelease]; + va_end(vaArguments); + + // Start time of this "timestampTag" is stashed in the dictionary. + // Treat the incoming object tag simply as an address, since it's only used to identify during lifetime. If + // we send in as an object, the dictionary will try to copy it. + NSNumber *tagAsNumber = [NSNumber numberWithUnsignedLong:(unsigned long)(void *)timestampTag]; + NSNumber *startTimeNumber = [g_startTimesWithTags objectForKey:tagAsNumber]; + + // Only log if there's been an associated start time. + if (startTimeNumber) { + unsigned long elapsed = [FBUtility currentTimeInMilliseconds] - startTimeNumber.unsignedLongValue; + [g_startTimesWithTags removeObjectForKey:tagAsNumber]; // served its purpose, remove + + // Log string is appended with "%d msec", with nothing intervening. This gives the most control to the caller. + logString = [NSString stringWithFormat:@"%@%lu msec", logString, elapsed]; + + [self singleShotLogEntry:loggingBehavior logEntry:logString]; + } + } +} + ++ (void)registerCurrentTime:(NSString *)loggingBehavior + withTag:(NSObject *)timestampTag { + + if ([[FBSettings loggingBehavior] containsObject:loggingBehavior]) { + + if (!g_startTimesWithTags) { + g_startTimesWithTags = [[NSMutableDictionary alloc] init]; + } + + FBConditionalLog(g_startTimesWithTags.count < 1000, + @"Unexpectedly large number of outstanding perf logging start times, something is likely wrong."); + + unsigned long currTime = [FBUtility currentTimeInMilliseconds]; + + // Treat the incoming object tag simply as an address, since it's only used to identify during lifetime. If + // we send in as an object, the dictionary will try to copy it. + unsigned long tagAsNumber = (unsigned long)(void *)timestampTag; + [g_startTimesWithTags setObject:[NSNumber numberWithUnsignedLong:currTime] + forKey:[NSNumber numberWithUnsignedLong:tagAsNumber]]; + } +} + + ++ (void)registerStringToReplace:(NSString *)replace + replaceWith:(NSString *)replaceWith { + + // Strings sent in here never get cleaned up, but that's OK, don't ever expect too many. + + if ([[FBSettings loggingBehavior] count] > 0) { // otherwise there's no logging. + + if (!g_stringsToReplace) { + g_stringsToReplace = [[NSMutableDictionary alloc] init]; + } + + [g_stringsToReplace setValue:replaceWith forKey:replace]; + } +} + + + +@end diff --git a/src/ios/facebook/FBLoginDialog.h b/src/ios/facebook/FBLoginDialog.h new file mode 100644 index 000000000..cd364a40e --- /dev/null +++ b/src/ios/facebook/FBLoginDialog.h @@ -0,0 +1,48 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#import "FBDialog.h" + +@protocol FBLoginDialogDelegate; + +/** + * Do not use this interface directly, instead, use authorize in Facebook.h + * + * Facebook Login Dialog interface for start the facebook webView login dialog. + * It start pop-ups prompting for credentials and permissions. + */ + +@interface FBLoginDialog : FBDialog { + id _loginDelegate; +} + +-(id) initWithURL:(NSString *) loginURL + loginParams:(NSMutableDictionary *) params + delegate:(id ) delegate; +@end + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol FBLoginDialogDelegate + +- (void)fbDialogLogin:(NSString*)token expirationDate:(NSDate*)expirationDate; + +- (void)fbDialogNotLogin:(BOOL)cancelled; + +@end + + diff --git a/src/ios/facebook/FBLoginDialog.m b/src/ios/facebook/FBLoginDialog.m new file mode 100644 index 000000000..5c70a3f40 --- /dev/null +++ b/src/ios/facebook/FBLoginDialog.m @@ -0,0 +1,94 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBDialog.h" +#import "FBLoginDialog.h" + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation FBLoginDialog + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// public + +/* + * initialize the FBLoginDialog with url and parameters + */ +- (id)initWithURL:(NSString*) loginURL + loginParams:(NSMutableDictionary*) params + delegate:(id ) delegate{ + + self = [super init]; + _serverURL = [loginURL retain]; + _params = [params retain]; + _loginDelegate = delegate; + return self; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// FBDialog + +/** + * Override FBDialog : to call when the webView Dialog did succeed + */ +- (void) dialogDidSucceed:(NSURL*)url { + NSString *q = [url absoluteString]; + NSString *token = [self getStringFromUrl:q needle:@"access_token="]; + NSString *expTime = [self getStringFromUrl:q needle:@"expires_in="]; + NSDate *expirationDate =nil; + + if (expTime != nil) { + int expVal = [expTime intValue]; + if (expVal == 0) { + expirationDate = [NSDate distantFuture]; + } else { + expirationDate = [NSDate dateWithTimeIntervalSinceNow:expVal]; + } + } + + if ((token == (NSString *) [NSNull null]) || (token.length == 0)) { + [self dialogDidCancel:url]; + [self dismissWithSuccess:NO animated:YES]; + } else { + if ([_loginDelegate respondsToSelector:@selector(fbDialogLogin:expirationDate:)]) { + [_loginDelegate fbDialogLogin:token expirationDate:expirationDate]; + } + [self dismissWithSuccess:YES animated:YES]; + } + +} + +/** + * Override FBDialog : to call with the login dialog get canceled + */ +- (void)dialogDidCancel:(NSURL *)url { + [self dismissWithSuccess:NO animated:YES]; + if ([_loginDelegate respondsToSelector:@selector(fbDialogNotLogin:)]) { + [_loginDelegate fbDialogNotLogin:YES]; + } +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { + if (!(([error.domain isEqualToString:@"NSURLErrorDomain"] && error.code == -999) || + ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102))) { + [super webView:webView didFailLoadWithError:error]; + if ([_loginDelegate respondsToSelector:@selector(fbDialogNotLogin:)]) { + [_loginDelegate fbDialogNotLogin:NO]; + } + } +} + +@end diff --git a/src/ios/facebook/FBLoginView.h b/src/ios/facebook/FBLoginView.h new file mode 100644 index 000000000..f3a1106ae --- /dev/null +++ b/src/ios/facebook/FBLoginView.h @@ -0,0 +1,160 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBSession.h" +#import "FBGraphUser.h" + +@protocol FBLoginViewDelegate; + +/*! + @class + @abstract + */ +@interface FBLoginView : UIView + +/*! + @abstract + The permissions to login with. Defaults to nil, meaning basic permissions. + + @discussion Methods and properties that specify permissions without a read or publish + qualification are deprecated; use of a read-qualified or publish-qualified alternative is preferred. + */ +@property (readwrite, copy) NSArray *permissions __attribute__((deprecated)); + +/*! + @abstract + The read permissions to request if the user logs in via this view. + + @discussion + Note, that if read permissions are specified, then publish permissions should not be specified. + */ +@property (nonatomic, copy) NSArray *readPermissions; + +/*! + @abstract + The publish permissions to request if the user logs in via this view. + + @discussion + Note, that a defaultAudience value of FBSessionDefaultAudienceOnlyMe, FBSessionDefaultAudienceEveryone, or + FBSessionDefaultAudienceFriends should be set if publish permissions are specified. Additionally, when publish + permissions are specified, then read should not be specified. + */ +@property (nonatomic, copy) NSArray *publishPermissions; + +/*! + @abstract + The default audience to use, if publish permissions are requested at login time. + */ +@property (nonatomic, assign) FBSessionDefaultAudience defaultAudience; + + +/*! + @abstract + Initializes and returns an `FBLoginView` object. The underlying session has basic permissions granted to it. + */ +- (id)init; + +/*! + @method + + @abstract + Initializes and returns an `FBLoginView` object constructed with the specified permissions. + + @param permissions An array of strings representing the permissions to request during the + authentication flow. A value of nil will indicates basic permissions. + + @discussion Methods and properties that specify permissions without a read or publish + qualification are deprecated; use of a read-qualified or publish-qualified alternative is preferred. + */ +- (id)initWithPermissions:(NSArray *)permissions __attribute__((deprecated)); + +/*! + @method + + @abstract + Initializes and returns an `FBLoginView` object constructed with the specified permissions. + + @param readPermissions An array of strings representing the read permissions to request during the + authentication flow. A value of nil will indicates basic permissions. + + */ +- (id)initWithReadPermissions:(NSArray *)readPermissions; + +/*! + @method + + @abstract + Initializes and returns an `FBLoginView` object constructed with the specified permissions. + + @param publishPermissions An array of strings representing the publish permissions to request during the + authentication flow. + + @param defaultAudience An audience for published posts; note that FBSessionDefaultAudienceNone is not valid + for permission requests that include publish or manage permissions. + + */ +- (id)initWithPublishPermissions:(NSArray *)publishPermissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience; + +/*! + @abstract + The delegate object that receives updates for selection and display control. + */ +@property (nonatomic, assign) IBOutlet id delegate; + +@end + +/*! + @protocol + + @abstract + The `FBLoginViewDelegate` protocol defines the methods used to receive event + notifications from `FBLoginView` objects. + */ +@protocol FBLoginViewDelegate + +@optional + +/*! + @abstract + Tells the delegate that the view is now in logged in mode + + @param loginView The login view that transitioned its view mode + */ +- (void)loginViewShowingLoggedInUser:(FBLoginView *)loginView; + +/*! + @abstract + Tells the delegate that the view is has now fetched user info + + @param loginView The login view that transitioned its view mode + + @param user The user info object describing the logged in user + */ +- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView + user:(id)user; + +/*! + @abstract + Tells the delegate that the view is now in logged out mode + + @param loginView The login view that transitioned its view mode + */ +- (void)loginViewShowingLoggedOutUser:(FBLoginView *)loginView; + +@end + diff --git a/src/ios/facebook/FBLoginView.m b/src/ios/facebook/FBLoginView.m new file mode 100644 index 000000000..d2f858098 --- /dev/null +++ b/src/ios/facebook/FBLoginView.m @@ -0,0 +1,429 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBLoginView.h" +#import "FBProfilePictureView.h" +#import "FBRequest.h" +#import "FBRequestConnection+Internal.h" +#import "FBSession.h" +#import "FBGraphUser.h" +#import "FBUtility.h" + +static NSString *const FBLoginViewCacheIdentity = @"FBLoginView"; +const int kButtonLabelX = 46; + +CGSize g_imageSize; + +@interface FBLoginView() + +- (void)initialize; +- (void)buttonPressed:(id)sender; +- (void)configureViewForStateLoggedIn:(BOOL)isLoggedIn; +- (void)wireViewForSession:(FBSession *)session; +- (void)wireViewForSessionWithoutOpening:(FBSession *)session; +- (void)unwireViewForSession ; +- (void)fetchMeInfo; +- (void)informDelegate:(BOOL)userOnly; +- (void)handleActiveSessionSetNotifications:(NSNotification *)notification; +- (void)handleActiveSessionUnsetNotifications:(NSNotification *)notification; + +@property (retain, nonatomic) UILabel *label; +@property (retain, nonatomic) UIButton *button; +@property (retain, nonatomic) FBSession *session; +@property (retain, nonatomic) FBRequestConnection *request; +@property (retain, nonatomic) id user; + +@end + +@implementation FBLoginView + +@synthesize delegate = _delegate, + label = _label, + button = _button, + session = _session, + request = _request, + user = _user, + permissions = _permissions, + readPermissions = _readPermissions, + publishPermissions = _publishPermissions, + defaultAudience = _defaultAudience; + + +- (id)init { + self = [super init]; + if (self) { + [self initialize]; + } + return self; +} + +- (id)initWithPermissions:(NSArray *)permissions { + self = [super init]; + if (self) { + self.permissions = permissions; + [self initialize]; + } + return self; +} + +- (id)initWithReadPermissions:(NSArray *)readPermissions { + self = [super init]; + if (self) { + self.readPermissions = readPermissions; + [self initialize]; + } + return self; +} + +- (id)initWithPublishPermissions:(NSArray *)publishPermissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience { + self = [super init]; + if (self) { + self.publishPermissions = publishPermissions; + self.defaultAudience = defaultAudience; + [self initialize]; + } + return self; +} + +- (id)initWithFrame:(CGRect)aRect { + self = [super initWithFrame:aRect]; + if (self) { + [self initialize]; + } + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (self) { + [self initialize]; + } + return self; +} + +- (void)dealloc { + + // removes all observers for self + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + // if we have an outstanding request, cancel + [self.request cancel]; + + [_request release]; + [_label release]; + [_button release]; + [_session release]; + [_user release]; + [_permissions release]; + + [super dealloc]; +} + +- (void)setDelegate:(id)newValue { + if (_delegate != newValue) { + _delegate = newValue; + + // whenever the delegate value changes, we schedule one initial call to inform the delegate + // of our current state; we use a delay in order to avoid a callback in a setup or init method + [self performSelector:@selector(informDelegate:) + withObject:nil + afterDelay:.01]; + } +} + +- (void)initialize { + // the base class can cause virtual recursion, so + // to handle this we make initialize idempotent + if (self.button) { + return; + } + + // setup view + self.autoresizesSubviews = YES; + self.clipsToBounds = YES; + + // if our session has a cached token ready, we open it; note that it is important + // that we open the session before notification wiring is in place + [FBSession openActiveSessionWithAllowLoginUI:NO]; + + // wire-up the current session to the login view, before adding global session-change handlers + [self wireViewForSession:FBSession.activeSession]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleActiveSessionSetNotifications:) + name:FBSessionDidSetActiveSessionNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleActiveSessionUnsetNotifications:) + name:FBSessionDidUnsetActiveSessionNotification + object:nil]; + + // setup button + self.button = [UIButton buttonWithType:UIButtonTypeCustom]; + [self.button addTarget:self + action:@selector(buttonPressed:) + forControlEvents:UIControlEventTouchUpInside]; + self.button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill; + self.button.autoresizingMask = UIViewAutoresizingFlexibleWidth; + + UIImage *image = [[UIImage imageNamed:@"FacebookSDKResources.bundle/FBLoginView/images/login-button-small.png"] + stretchableImageWithLeftCapWidth:kButtonLabelX topCapHeight:0]; + g_imageSize = image.size; + [self.button setBackgroundImage:image forState:UIControlStateNormal]; + + image = [[UIImage imageNamed:@"FacebookSDKResources.bundle/FBLoginView/images/login-button-small-pressed.png"] + stretchableImageWithLeftCapWidth:kButtonLabelX topCapHeight:0]; + [self.button setBackgroundImage:image forState:UIControlStateHighlighted]; + + [self addSubview:self.button]; + + // add a label that will appear over the button + self.label = [[[UILabel alloc] init] autorelease]; + self.label.autoresizingMask = UIViewAutoresizingFlexibleWidth; + self.label.textAlignment = UITextAlignmentCenter; + self.label.backgroundColor = [UIColor clearColor]; + self.label.font = [UIFont boldSystemFontOfSize:16.0]; + self.label.textColor = [UIColor whiteColor]; + self.label.shadowColor = [UIColor blackColor]; + self.label.shadowOffset = CGSizeMake(0.0, -1.0); + [self addSubview:self.label]; + + // We force our height to be the same as the image, but we will let someone make us wider + // than the default image. + CGFloat width = MAX(self.frame.size.width, g_imageSize.width); + CGRect frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, + width, image.size.height); + self.frame = frame; + + CGRect buttonFrame = CGRectMake(0, 0, width, image.size.height); + self.button.frame = buttonFrame; + + self.label.frame = CGRectMake(kButtonLabelX, 0, width - kButtonLabelX, image.size.height); + + self.backgroundColor = [UIColor clearColor]; + + if (self.session.isOpen) { + [self fetchMeInfo]; + [self configureViewForStateLoggedIn:YES]; + } else { + [self configureViewForStateLoggedIn:NO]; + } +} + +- (CGSize)sizeThatFits:(CGSize)size { + CGSize logInSize = [[self logInText] sizeWithFont:self.label.font]; + CGSize logOutSize = [[self logOutText] sizeWithFont:self.label.font]; + + // Leave at least a small margin around the label. + CGFloat desiredWidth = kButtonLabelX + 20 + MAX(logInSize.width, logOutSize.width); + // Never get smaller than the image + CGFloat width = MAX(desiredWidth, g_imageSize.width); + + return CGSizeMake(width, g_imageSize.height); +} + +- (NSString *)logInText { + return [FBUtility localizedStringForKey:@"FBLV:LogInButton" withDefault:@"Log In"]; +} + +- (NSString *)logOutText { + return [FBUtility localizedStringForKey:@"FBLV:LogOutButton" withDefault:@"Log Out"]; +} + +- (void)configureViewForStateLoggedIn:(BOOL)isLoggedIn { + if (isLoggedIn) { + self.label.text = [self logOutText]; + } else { + self.label.text = [self logInText]; + self.user = nil; + } +} + +- (void)fetchMeInfo { + FBRequest *request = [FBRequest requestForMe]; + [request setSession:self.session]; + self.request = [[[FBRequestConnection alloc] init] autorelease]; + [self.request addRequest:request + completionHandler:^(FBRequestConnection *connection, NSMutableDictionary *result, NSError *error) { + if (result) { + self.user = result; + [self informDelegate:YES]; + } else { + self.user = nil; + } + self.request = nil; + }]; + [self.request startWithCacheIdentity:FBLoginViewCacheIdentity + skipRoundtripIfCached:YES]; + +} + +- (void)informDelegate:(BOOL)userOnly { + if (userOnly) { + if ([self.delegate respondsToSelector:@selector(loginViewFetchedUserInfo:user:)]) { + [self.delegate loginViewFetchedUserInfo:self + user:self.user]; + } + } else if (FBSession.activeSession.isOpen) { + if ([self.delegate respondsToSelector:@selector(loginViewShowingLoggedInUser:)]) { + [self.delegate loginViewShowingLoggedInUser:self]; + } + // any time we inform/reinform of isOpen event, we want to be sure + // to repass the user if we have it + if (self.user) { + [self informDelegate:YES]; + } + } else { + if ([self.delegate respondsToSelector:@selector(loginViewShowingLoggedOutUser:)]) { + [self.delegate loginViewShowingLoggedOutUser:self]; + } + } +} + +- (void)wireViewForSessionWithoutOpening:(FBSession *)session { + // if there is an outstanding request for the previous session, cancel + [self.request cancel]; + self.request = nil; + + self.session = session; + + // register a KVO observer + [self.session addObserver:self + forKeyPath:@"state" + options:NSKeyValueObservingOptionNew + context:nil]; +} + +- (void)wireViewForSession:(FBSession *)session { + [self wireViewForSessionWithoutOpening:session]; + + // anytime we find that our session is created with an available token + // we open it on the spot + if (self.session.state == FBSessionStateCreatedTokenLoaded) { + [FBSession openActiveSessionWithAllowLoginUI:NO]; + } +} + +- (void)unwireViewForSession { + // this line of code is the main reason we need to hold on + // to the session object at all + [self.session removeObserver:self + forKeyPath:@"state"]; + self.session = nil; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if (self.session.isOpen) { + [self fetchMeInfo]; + [self configureViewForStateLoggedIn:YES]; + } else { + [self configureViewForStateLoggedIn:NO]; + } + [self informDelegate:NO]; +} + +- (void)handleActiveSessionSetNotifications:(NSNotification *)notification { + // NSNotificationCenter is a global channel, so we guard against + // unexpected uses of this notification the best we can + if ([notification.object isKindOfClass:[FBSession class]]) { + [self wireViewForSession:notification.object]; + } +} + +- (void)handleActiveSessionUnsetNotifications:(NSNotification *)notification { + [self unwireViewForSession]; +} + +- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex +{ + if (buttonIndex == 0) { // logout + [FBSession.activeSession closeAndClearTokenInformation]; + } +} + +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +- (void)buttonPressed:(id)sender { + if (self.session == FBSession.activeSession) { + if (!self.session.isOpen) { // login + + // the policy here is: + // 1) if you provide unspecified permissions, then we fall back on legacy fast-app-switch + // 2) if you provide only read permissions, then we call a read-based open method that will use integrated auth + // 3) if you provide any publish permissions, then we combine the read-set and publish-set and call the publish-based + // method that will use integrated auth when availab le + // 4) if you provide any publish permissions, and don't specify a valid audience, the control will throw an exception + // when the user presses login + if (self.permissions) { + [FBSession openActiveSessionWithPermissions:self.permissions + allowLoginUI:YES + completionHandler:nil]; + } else if (![self.publishPermissions count]) { + [FBSession openActiveSessionWithReadPermissions:self.publishPermissions + allowLoginUI:YES + completionHandler:nil]; + } else { + // combined read and publish permissions will usually fail, but if the app wants us to + // try it here, then we will pass the aggregate set to the server + NSArray *permissions = self.publishPermissions; + if ([self.readPermissions count]) { + NSMutableSet *set = [NSMutableSet setWithArray:self.publishPermissions]; + [set addObjectsFromArray:self.readPermissions]; + permissions = [set allObjects]; + } + [FBSession openActiveSessionWithPublishPermissions:permissions + defaultAudience:self.defaultAudience + allowLoginUI:YES + completionHandler:nil]; + } + } else { // logout action sheet + NSString *name = self.user.name; + NSString *title = nil; + if (name) { + title = [NSString stringWithFormat:[FBUtility localizedStringForKey:@"FBLV:LoggedInAs" + withDefault:@"Logged in as %@"], name]; + } else { + title = [FBUtility localizedStringForKey:@"FBLV:LoggedInUsingFacebook" + withDefault:@"Logged in using Facebook"]; + } + + NSString *cancelTitle = [FBUtility localizedStringForKey:@"FBLV:CancelAction" + withDefault:@"Cancel"]; + NSString *logOutTitle = [FBUtility localizedStringForKey:@"FBLV:LogOutAction" + withDefault:@"Log Out"]; + UIActionSheet *sheet = [[[UIActionSheet alloc] initWithTitle:title + delegate:self + cancelButtonTitle:cancelTitle + destructiveButtonTitle:logOutTitle + otherButtonTitles:nil] + autorelease]; + // Show the sheet + [sheet showInView:self]; + } + } else { // state of view out of sync with active session + // so resync + [self unwireViewForSession]; + [self wireViewForSession:FBSession.activeSession]; + [self configureViewForStateLoggedIn:self.session.isOpen]; + [self informDelegate:NO]; + } +} +#pragma GCC diagnostic warning "-Wdeprecated-declarations" +@end diff --git a/src/ios/facebook/FBNativeDialogs.h b/src/ios/facebook/FBNativeDialogs.h new file mode 100644 index 000000000..c866649d8 --- /dev/null +++ b/src/ios/facebook/FBNativeDialogs.h @@ -0,0 +1,170 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import + +@class FBSession; + +/*! + @typedef FBNativeDialogResult enum + + @abstract + Passed to a handler to indicate the result of a dialog being displayed to the user. +*/ +typedef enum { + /*! Indicates that the dialog action completed successfully. */ + FBNativeDialogResultSucceeded, + /*! Indicates that the dialog action was cancelled (either by the user or the system). */ + FBNativeDialogResultCancelled, + /*! Indicates that the dialog could not be shown (because not on ios6 or ios6 auth was not used). */ + FBNativeDialogResultError +} FBNativeDialogResult; + +/*! + @typedef + + @abstract Defines a handler that will be called in response to the native share dialog + being displayed. + */ +typedef void (^FBShareDialogHandler)(FBNativeDialogResult result, NSError *error); + +/*! + @class FBNativeDialogs + + @abstract + Provides methods to display native (i.e., non-Web-based) dialogs to the user. + Currently the iOS 6 sharing dialog is supported. +*/ +@interface FBNativeDialogs : NSObject + +/*! + @abstract + Presents a dialog that allows the user to share a status update that may include + text, images, or URLs. This dialog is only available on iOS 6.0 and above. The + current active session returned by [FBSession activeSession] will be used to determine + whether the dialog will be displayed. If a session is active, it must be open and the + login method used to authenticate the user must be native iOS 6.0 authentication. + If no session active, then whether the call succeeds or not will depend on + whether Facebook integration has been configured. + + @param viewController The view controller which will present the dialog. + + @param initialText The text which will initially be populated in the dialog. The user + will have the opportunity to edit this text before posting it. May be nil. + + @param image A UIImage that will be attached to the status update. May be nil. + + @param url An NSURL that will be attached to the status update. May be nil. + + @param handler A handler that will be called when the dialog is dismissed, or if an error + occurs. May be nil. + + @return YES if the dialog was presented, NO if not (in the case of a NO result, the handler + will still be called, with an error indicating the reason the dialog was not displayed) + */ ++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController + initialText:(NSString*)initialText + image:(UIImage*)image + url:(NSURL*)url + handler:(FBShareDialogHandler)handler; + +/*! + @abstract + Presents a dialog that allows the user to share a status update that may include + text, images, or URLs. This dialog is only available on iOS 6.0 and above. The + current active session returned by [FBSession activeSession] will be used to determine + whether the dialog will be displayed. If a session is active, it must be open and the + login method used to authenticate the user must be native iOS 6.0 authentication. + If no session active, then whether the call succeeds or not will depend on + whether Facebook integration has been configured. + + @param viewController The view controller which will present the dialog. + + @param initialText The text which will initially be populated in the dialog. The user + will have the opportunity to edit this text before posting it. May be nil. + + @param images An array of UIImages that will be attached to the status update. May + be nil. + + @param urls An array of NSURLs that will be attached to the status update. May be nil. + + @param handler A handler that will be called when the dialog is dismissed, or if an error + occurs. May be nil. + + @return YES if the dialog was presented, NO if not (in the case of a NO result, the handler + will still be called, with an error indicating the reason the dialog was not displayed) + */ ++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController + initialText:(NSString*)initialText + images:(NSArray*)images + urls:(NSArray*)urls + handler:(FBShareDialogHandler)handler; + +/*! + @abstract + Presents a dialog that allows the user to share a status update that may include + text, images, or URLs. This dialog is only available on iOS 6.0 and above. An + may be specified, or nil may be passed to indicate that the current + active session should be used. If a session is specified (whether explicitly or by + virtue of being the active session), it must be open and the login method used to + authenticate the user must be native iOS 6.0 authentication. If no session is specified + (and there is no active session), then whether the call succeeds or not will depend on + whether Facebook integration has been configured. + + @param viewController The view controller which will present the dialog. + + @param session The to use to determine whether or not the user has been + authenticated with iOS native authentication. If nil, then [FBSession activeSession] + will be checked. See discussion above for the implications of nil or non-nil session. + + @param initialText The text which will initially be populated in the dialog. The user + will have the opportunity to edit this text before posting it. May be nil. + + @param images An array of UIImages that will be attached to the status update. May + be nil. + + @param urls An array of NSURLs that will be attached to the status update. May be nil. + + @param handler A handler that will be called when the dialog is dismissed, or if an error + occurs. May be nil. + + @return YES if the dialog was presented, NO if not (in the case of a NO result, the handler + will still be called, with an error indicating the reason the dialog was not displayed) + */ ++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController + session:(FBSession*)session + initialText:(NSString*)initialText + images:(NSArray*)images + urls:(NSArray*)urls + handler:(FBShareDialogHandler)handler; + +/*! + @abstract + Determines whether a call to presentShareDialogModallyFrom: will successfully present + a dialog. This is useful for applications that need to modify the available UI controls + depending on whether the dialog is available on the current platform and for the current + user. + + @param session The to use to determine whether or not the user has been + authenticated with iOS native authentication. If nil, then [FBSession activeSession] + will be checked. See discussion above for the implications of nil or non-nil session. + + @return YES if the dialog would be presented for the session, and NO if not + */ ++ (BOOL)canPresentShareDialogWithSession:(FBSession*)session; + +@end diff --git a/src/ios/facebook/FBNativeDialogs.m b/src/ios/facebook/FBNativeDialogs.m new file mode 100644 index 000000000..509096cc8 --- /dev/null +++ b/src/ios/facebook/FBNativeDialogs.m @@ -0,0 +1,153 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBNativeDialogs.h" +#import "FBSession.h" +#import "FBError.h" +#import "FBUtility.h" +#import "Social/Social.h" + +@interface FBNativeDialogs () + ++ (NSError*)createError:(NSString*)reason; + +@end + +@implementation FBNativeDialogs + ++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController + initialText:(NSString*)initialText + image:(UIImage*)image + url:(NSURL*)url + handler:(FBShareDialogHandler)handler { + NSArray *images = image ? [NSArray arrayWithObject:image] : nil; + NSArray *urls = url ? [NSArray arrayWithObject:url] : nil; + + return [self presentShareDialogModallyFrom:viewController + session:nil + initialText:initialText + images:images + urls:urls + handler:handler]; +} + ++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController + initialText:(NSString*)initialText + images:(NSArray*)images + urls:(NSArray*)urls + handler:(FBShareDialogHandler)handler { + + return [self presentShareDialogModallyFrom:viewController + session:nil + initialText:initialText + images:images + urls:urls + handler:handler]; +} + ++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController + session:(FBSession*)session + initialText:(NSString*)initialText + images:(NSArray*)images + urls:(NSArray*)urls + handler:(FBShareDialogHandler)handler { + + SLComposeViewController *composeViewController = [FBNativeDialogs composeViewControllerWithSession:session + handler:handler]; + if (!composeViewController) { + return NO; + } + + if (initialText) { + [composeViewController setInitialText:initialText]; + } + if (images && images.count > 0) { + for (UIImage *image in images) { + [composeViewController addImage:image]; + } + } + if (urls && urls.count > 0) { + for (NSURL *url in urls) { + [composeViewController addURL:url]; + } + } + + [composeViewController setCompletionHandler:^(SLComposeViewControllerResult result) { + BOOL cancelled = (result == SLComposeViewControllerResultCancelled); + if (handler) { + handler(cancelled ? FBNativeDialogResultCancelled : FBNativeDialogResultSucceeded, nil); + } + }]; + + [viewController presentModalViewController:composeViewController animated:YES]; + + return YES; +} + ++ (BOOL)canPresentShareDialogWithSession:(FBSession*)session { + return [FBNativeDialogs composeViewControllerWithSession:session + handler:nil] != nil; +} + ++ (SLComposeViewController*)composeViewControllerWithSession:(FBSession*)session + handler:(FBShareDialogHandler)handler { + // Can we even call the iOS API? + Class composeViewControllerClass = [SLComposeViewController class]; + if (composeViewControllerClass == nil || + [composeViewControllerClass isAvailableForServiceType:SLServiceTypeFacebook] == NO) { + if (handler) { + handler(FBNativeDialogResultError, [self createError:FBErrorNativeDialogNotSupported]); + } + return nil; + } + + if (session == nil) { + // No session provided -- do we have an activeSession? We must either have a session that + // was authenticated with native auth, or no session at all (in which case the app is + // running unTOSed and we will rely on the OS to authenticate/TOS the user). + session = [FBSession activeSession]; + } + if (session != nil) { + // If we have an open session and it's not native auth, fail. If the session is + // not open, attempting to put up the dialog will prompt the user to configure + // their account. + if (session.isOpen && session.loginType != FBSessionLoginTypeSystemAccount) { + if (handler) { + handler(FBNativeDialogResultError, [self createError:FBErrorNativeDialogInvalidForSession]); + } + return nil; + } + } + + SLComposeViewController *composeViewController = [composeViewControllerClass composeViewControllerForServiceType:SLServiceTypeFacebook]; + if (composeViewController == nil) { + if (handler) { + handler(FBNativeDialogResultError, [self createError:FBErrorNativeDialogCantBeDisplayed]); + } + return nil; + } + return composeViewController; +} + ++ (NSError*)createError:(NSString*)reason { + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:reason, FBErrorNativeDialogReasonKey, nil]; + NSError *error = [NSError errorWithDomain:FacebookSDKDomain + code:FBErrorNativeDialog + userInfo:userInfo]; + return error; +} + +@end diff --git a/src/ios/facebook/FBOpenGraphAction.h b/src/ios/facebook/FBOpenGraphAction.h new file mode 100644 index 000000000..599e8eb4e --- /dev/null +++ b/src/ios/facebook/FBOpenGraphAction.h @@ -0,0 +1,127 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/xlicenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBGraphObject.h" + +@protocol FBGraphPlace; +@protocol FBGraphUser; + +/*! + @protocol + + @abstract + The `FBOpenGraphAction` protocol is the base protocol for use in posting and retrieving Open Graph actions. + It inherits from the `FBGraphObject` protocol; you may derive custome protocols from `FBOpenGraphAction` in order + implement typed access to your application's custom actions. + + @discussion + Represents an Open Graph custom action, to be used directly, or from which to + derive custom action protocols with custom properties. + */ +@protocol FBOpenGraphAction + +/*! + @property + @abstract Typed access to action's id + */ +@property (retain, nonatomic) NSString *id; + +/*! + @property + @abstract Typed access to action's start time + */ +@property (retain, nonatomic) NSString *start_time; + +/*! + @property + @abstract Typed access to action's end time + */ +@property (retain, nonatomic) NSString *end_time; + +/*! + @property + @abstract Typed access to action's publication time + */ +@property (retain, nonatomic) NSString *publish_time; + +/*! + @property + @abstract Typed access to action's creation time + */ +@property (retain, nonatomic) NSString *created_time; + +/*! + @property + @abstract Typed access to action's expiration time + */ +@property (retain, nonatomic) NSString *expires_time; + +/*! + @property + @abstract Typed access to action's ref + */ +@property (retain, nonatomic) NSString *ref; + +/*! + @property + @abstract Typed access to action's user message + */ +@property (retain, nonatomic) NSString *message; + +/*! + @property + @abstract Typed access to action's place + */ +@property (retain, nonatomic) id place; + +/*! + @property + @abstract Typed access to action's tags + */ +@property (retain, nonatomic) NSArray *tags; + +/*! + @property + @abstract Typed access to action's images + */ +@property (retain, nonatomic) NSArray *image; + +/*! + @property + @abstract Typed access to action's from-user + */ +@property (retain, nonatomic) id from; + +/*! + @property + @abstract Typed access to action's likes + */ +@property (retain, nonatomic) NSArray *likes; + +/*! + @property + @abstract Typed access to action's application + */ +@property (retain, nonatomic) id application; + +/*! + @property + @abstract Typed access to action's comments + */ +@property (retain, nonatomic) NSArray *comments; + +@end \ No newline at end of file diff --git a/src/ios/facebook/FBPlacePickerCacheDescriptor.h b/src/ios/facebook/FBPlacePickerCacheDescriptor.h new file mode 100644 index 000000000..6b9b2eda4 --- /dev/null +++ b/src/ios/facebook/FBPlacePickerCacheDescriptor.h @@ -0,0 +1,36 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import "FBCacheDescriptor.h" + +@interface FBPlacePickerCacheDescriptor : FBCacheDescriptor + +- (id)initWithLocationCoordinate:(CLLocationCoordinate2D)locationCoordinate + radiusInMeters:(NSInteger)radiusInMeters + searchText:(NSString*)searchText + resultsLimit:(NSInteger)resultsLimit + fieldsForRequest:(NSSet*)fieldsForRequest; + +@property (nonatomic, readonly) CLLocationCoordinate2D locationCoordinate; +@property (nonatomic, readonly) NSInteger radiusInMeters; +@property (nonatomic, readonly) NSInteger resultsLimit; +@property (nonatomic, readonly, copy) NSString *searchText; +@property (nonatomic, readonly, copy) NSSet *fieldsForRequest; + +@end + diff --git a/src/ios/facebook/FBPlacePickerCacheDescriptor.m b/src/ios/facebook/FBPlacePickerCacheDescriptor.m new file mode 100644 index 000000000..ecd512853 --- /dev/null +++ b/src/ios/facebook/FBPlacePickerCacheDescriptor.m @@ -0,0 +1,114 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBPlacePickerCacheDescriptor.h" +#import "FBGraphObjectTableDataSource.h" +#import "FBGraphObjectPagingLoader.h" +#import "FBPlacePickerViewController.h" +#import "FBPlacePickerViewController+Internal.h" + +@interface FBPlacePickerCacheDescriptor () + +@property (nonatomic, readwrite) CLLocationCoordinate2D locationCoordinate; +@property (nonatomic, readwrite) NSInteger radiusInMeters; +@property (nonatomic, readwrite) NSInteger resultsLimit; +@property (nonatomic, readwrite, copy) NSString *searchText; +@property (nonatomic, readwrite, copy) NSSet *fieldsForRequest; +@property (nonatomic, readwrite, retain) FBGraphObjectPagingLoader *loader; + +// this property is only used by unit tests, and should not be removed or made public +@property (nonatomic, readwrite, assign) BOOL hasCompletedFetch; + +@end + +@implementation FBPlacePickerCacheDescriptor + +@synthesize locationCoordinate = _locationCoordinate, + radiusInMeters = _radiusInMeters, + resultsLimit = _resultsLimit, + searchText = _searchText, + fieldsForRequest = _fieldsForRequest, + loader = _loader, + hasCompletedFetch = _hasCompletedFetch; + +- (id)initWithLocationCoordinate:(CLLocationCoordinate2D)locationCoordinate + radiusInMeters:(NSInteger)radiusInMeters + searchText:(NSString*)searchText + resultsLimit:(NSInteger)resultsLimit + fieldsForRequest:(NSSet*)fieldsForRequest { + self = [super init]; + if (self) { + self.locationCoordinate = locationCoordinate; + self.radiusInMeters = radiusInMeters <= 0 ? defaultRadius : radiusInMeters; + self.searchText = searchText; + self.resultsLimit = resultsLimit <= 0 ? defaultResultsLimit : resultsLimit; + self.fieldsForRequest = fieldsForRequest; + self.hasCompletedFetch = NO; + } + return self; +} + +- (void)dealloc { + self.fieldsForRequest = nil; + self.searchText = nil; + self.loader = nil; + [super dealloc]; +} + +- (void)prefetchAndCacheForSession:(FBSession*)session { + // Place queries require a session, so do nothing if we don't have one. + if (session == nil) { + return; + } + + // datasource has some field ownership, so we need one here + FBGraphObjectTableDataSource *datasource = [[[FBGraphObjectTableDataSource alloc] init] autorelease]; + + // create the request object that we will start with + FBRequest *request = [FBPlacePickerViewController requestForPlacesSearchAtCoordinate:self.locationCoordinate + radiusInMeters:self.radiusInMeters + resultsLimit:self.resultsLimit + searchText:self.searchText + fields:self.fieldsForRequest + datasource:datasource + session:session]; + + self.loader.delegate = nil; + self.loader = [[[FBGraphObjectPagingLoader alloc] initWithDataSource:datasource + pagingMode:FBGraphObjectPagingModeAsNeeded] + autorelease]; + self.loader.session = session; + self.loader.delegate = self; + + // make sure we are around to handle the delegate call + [self retain]; + + // seed the cache + [self.loader startLoadingWithRequest:request + cacheIdentity:FBPlacePickerCacheIdentity + skipRoundtripIfCached:NO]; +} + +- (void)pagingLoaderDidFinishLoading:(FBGraphObjectPagingLoader *)pagingLoader { + self.loader.delegate = nil; + self.loader = nil; + self.hasCompletedFetch = YES; + + // achieving detachment + [self release]; +} + +@end diff --git a/src/ios/facebook/FBPlacePickerViewController+Internal.h b/src/ios/facebook/FBPlacePickerViewController+Internal.h new file mode 100644 index 000000000..f4c4e707a --- /dev/null +++ b/src/ios/facebook/FBPlacePickerViewController+Internal.h @@ -0,0 +1,38 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBPlacePickerViewController.h" +#import "FBRequest.h" +#import "FBGraphObjectTableDataSource.h" +#import "FBSession.h" + +// This is the cache identity used by both the view controller and cache descriptor objects +extern NSString *const FBPlacePickerCacheIdentity; + +extern const NSInteger defaultResultsLimit; +extern const NSInteger defaultRadius; + +@interface FBPlacePickerViewController (Internal) + ++ (FBRequest*)requestForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate + radiusInMeters:(NSInteger)radius + resultsLimit:(NSInteger)resultsLimit + searchText:(NSString*)searchText + fields:(NSSet*)fieldsForRequest + datasource:(FBGraphObjectTableDataSource*)datasource + session:(FBSession*)session; +@end \ No newline at end of file diff --git a/src/ios/facebook/FBPlacePickerViewController.h b/src/ios/facebook/FBPlacePickerViewController.h new file mode 100644 index 000000000..6a27196e0 --- /dev/null +++ b/src/ios/facebook/FBPlacePickerViewController.h @@ -0,0 +1,247 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import "FBGraphPlace.h" +#import "FBSession.h" +#import "FBCacheDescriptor.h" +#import "FBViewController.h" + +@protocol FBPlacePickerDelegate; + +/*! + @class FBPlacePickerViewController + + @abstract + The `FBPlacePickerViewController` class creates a controller object that manages + the user interface for displaying and selecting nearby places. + + @discussion + When the `FBPlacePickerViewController` view loads it creates a `UITableView` object + where the places near a given location will be displayed. You can access this view + through the `tableView` property. + + The place data can be pre-fetched and cached prior to using the view controller. The + cache is setup using an object that can trigger the + data fetch. Any place data requests will first check the cache and use that data. + If the place picker is being displayed cached data will initially be shown before + a fresh copy is retrieved. + + The `delegate` property may be set to an object that conforms to the + protocol. The `delegate` object will receive updates related to place selection and + data changes. The delegate can also be used to filter the places to display in the + picker. + */ +@interface FBPlacePickerViewController : FBViewController + +/*! + @abstract + Returns an outlet for the spinner used in the view controller. + */ +@property (nonatomic, retain) IBOutlet UIActivityIndicatorView *spinner; + +/*! + @abstract + Returns an outlet for the table view managed by the view controller. + */ +@property (nonatomic, retain) IBOutlet UITableView *tableView; + +/*! + @abstract + Addtional fields to fetch when making the Graph API call to get place data. + */ +@property (nonatomic, copy) NSSet *fieldsForRequest; + +/*! + @abstract + A Boolean value that indicates whether place profile pictures are displayed. + */ +@property (nonatomic) BOOL itemPicturesEnabled; + +/*! + @abstract + The coordinates to use for place discovery. + */ +@property (nonatomic) CLLocationCoordinate2D locationCoordinate; + +/*! + @abstract + The radius to use for place discovery. + */ +@property (nonatomic) NSInteger radiusInMeters; + +/*! + @abstract + The maximum number of places to fetch. + */ +@property (nonatomic) NSInteger resultsLimit; + +/*! + @abstract + The search words used to narrow down the results returned. + */ +@property (nonatomic, copy) NSString *searchText; + +/*! + @abstract + The session that is used in the request for place data. + */ +@property (nonatomic, retain) FBSession *session; + +/*! + @abstract + The place that is currently selected in the view. This is nil + if nothing is selected. + */ +@property (nonatomic, retain, readonly) id selection; + +/*! + @abstract + Clears the current selection, so the picker is ready for a fresh use. + */ +- (void)clearSelection; + +/*! + @abstract + Initializes a place picker view controller. + */ +- (id)init; + +/*! + @abstract + Initializes a place picker view controller. + + @param aDecoder An unarchiver object. + */ +- (id)initWithCoder:(NSCoder *)aDecoder; + +/*! + @abstract + Initializes a place picker view controller. + + @param nibNameOrNil The name of the nib file to associate with the view controller. The nib file name should not contain any leading path information. If you specify nil, the nibName property is set to nil. + @param nibBundleOrNil The bundle in which to search for the nib file. This method looks for the nib file in the bundle's language-specific project directories first, followed by the Resources directory. If nil, this method looks for the nib file in the main bundle. + */ +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil; + +/*! + @abstract + Configures the properties used in the caching data queries. + + @discussion + Cache descriptors are used to fetch and cache the data used by the view controller. + If the view controller finds a cached copy of the data, it will + first display the cached content then fetch a fresh copy from the server. + + @param cacheDescriptor The containing the cache query properties. + */ +- (void)configureUsingCachedDescriptor:(FBCacheDescriptor*)cacheDescriptor; + +/*! + @abstract + Initiates a query to get place data the first time or in response to changes in + the search criteria, filter, or location information. + + + @discussion + A cached copy will be returned if available. The cached view is temporary until a fresh copy is + retrieved from the server. It is legal to call this more than once. + */ +- (void)loadData; + +/*! + @method + + @abstract + Creates a cache descriptor with additional fields and a profile ID for use with the + `FBPlacePickerViewController` object. + + @discussion + An `FBCacheDescriptor` object may be used to pre-fetch data before it is used by + the view controller. It may also be used to configure the `FBPlacePickerViewController` + object. + + @param locationCoordinate The coordinates to use for place discovery. + @param radiusInMeters The radius to use for place discovery. + @param searchText The search words used to narrow down the results returned. + @param resultsLimit The maximum number of places to fetch. + @param fieldsForRequest Addtional fields to fetch when making the Graph API call to get place data. + */ ++ (FBCacheDescriptor*)cacheDescriptorWithLocationCoordinate:(CLLocationCoordinate2D)locationCoordinate + radiusInMeters:(NSInteger)radiusInMeters + searchText:(NSString*)searchText + resultsLimit:(NSInteger)resultsLimit + fieldsForRequest:(NSSet*)fieldsForRequest; + +@end + +/*! + @protocol + + @abstract + The `FBPlacePickerDelegate` protocol defines the methods used to receive event + notifications and allow for deeper control of the + view. + */ +@protocol FBPlacePickerDelegate +@optional + +/*! + @abstract + Tells the delegate that data has been loaded. + + @discussion + The object's `tableView` property is automatically + reloaded when this happens. However, if another table view, for example the + `UISearchBar` is showing data, then it may also need to be reloaded. + + @param placePicker The place picker view controller whose data changed. + */ +- (void)placePickerViewControllerDataDidChange:(FBPlacePickerViewController *)placePicker; + +/*! + @abstract + Tells the delegate that the selection has changed. + + @param placePicker The place picker view controller whose selection changed. + */ +- (void)placePickerViewControllerSelectionDidChange:(FBPlacePickerViewController *)placePicker; + +/*! + @abstract + Asks the delegate whether to include a place in the list. + + @discussion + This can be used to implement a search bar that filters the places list. + + @param placePicker The place picker view controller that is requesting this information. + @param place An object representing the place. + */ +- (BOOL)placePickerViewController:(FBPlacePickerViewController *)placePicker + shouldIncludePlace:(id )place; + +/*! + @abstract + Called if there is a communication error. + + @param placePicker The place picker view controller that encountered the error. + @param error An error object containing details of the error. + */ +- (void)placePickerViewController:(FBPlacePickerViewController *)placePicker + handleError:(NSError *)error; + +@end diff --git a/src/ios/facebook/FBPlacePickerViewController.m b/src/ios/facebook/FBPlacePickerViewController.m new file mode 100644 index 000000000..9fc126ee9 --- /dev/null +++ b/src/ios/facebook/FBPlacePickerViewController.m @@ -0,0 +1,541 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#import "FBError.h" +#import "FBGraphObjectPagingLoader.h" +#import "FBGraphObjectTableDataSource.h" +#import "FBGraphObjectTableSelection.h" +#import "FBLogger.h" +#import "FBPlacePickerViewController.h" +#import "FBRequest.h" +#import "FBRequestConnection.h" +#import "FBUtility.h" +#import "FBPlacePickerCacheDescriptor.h" +#import "FBSession+Internal.h" +#import "FBSettings.h" + +NSString *const FBPlacePickerCacheIdentity = @"FBPlacePicker"; + +static const NSInteger searchTextChangedTimerInterval = 2; +const NSInteger defaultResultsLimit = 100; +const NSInteger defaultRadius = 1000; // 1km +static NSString *defaultImageName = @"FacebookSDKResources.bundle/FBPlacePickerView/images/fb_generic_place.png"; + +@interface FBPlacePickerViewController () + +@property (nonatomic, retain) FBGraphObjectTableDataSource *dataSource; +@property (nonatomic, retain) FBGraphObjectTableSelection *selectionManager; +@property (nonatomic, retain) FBGraphObjectPagingLoader *loader; +@property (nonatomic, retain) NSTimer *searchTextChangedTimer; +@property (nonatomic) BOOL trackActiveSession; + +- (void)initialize; +- (void)loadDataPostThrottleSkippingRoundTripIfCached:(NSNumber*)skipRoundTripIfCached; +- (NSTimer *)createSearchTextChangedTimer; +- (void)updateView; +- (void)centerAndStartSpinner; +- (void)addSessionObserver:(FBSession*)session; +- (void)removeSessionObserver:(FBSession*)session; +- (void)clearData; + +@end + +@implementation FBPlacePickerViewController { + BOOL _hasSearchTextChangedSinceLastQuery; + +} + +@synthesize dataSource = _dataSource; +@synthesize delegate = _delegate; +@synthesize fieldsForRequest = _fieldsForRequest; +@synthesize loader = _loader; +@synthesize locationCoordinate = _locationCoordinate; +@synthesize radiusInMeters = _radiusInMeters; +@synthesize resultsLimit = _resultsLimit; +@synthesize searchText = _searchText; +@synthesize searchTextChangedTimer = _searchTextChangedTimer; +@synthesize selectionManager = _selectionManager; +@synthesize spinner = _spinner; +@synthesize tableView = _tableView; +@synthesize session = _session; +@synthesize trackActiveSession = _trackActiveSession; + +- (id)init +{ + self = [super init]; + + if (self) { + [self initialize]; + } + + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + + if (self) { + [self initialize]; + } + + return self; +} + +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + + + if (self) { + [self initialize]; + } + + return self; +} + +- (void)initialize +{ + // Data Source + FBGraphObjectTableDataSource *dataSource = [[[FBGraphObjectTableDataSource alloc] + init] + autorelease]; + dataSource.defaultPicture = [UIImage imageNamed:defaultImageName]; + dataSource.controllerDelegate = self; + dataSource.itemSubtitleEnabled = YES; + + // Selection Manager + FBGraphObjectTableSelection *selectionManager = [[[FBGraphObjectTableSelection alloc] + initWithDataSource:dataSource] + autorelease]; + selectionManager.delegate = self; + + // Paging loader + id loader = [[[FBGraphObjectPagingLoader alloc] initWithDataSource:dataSource + pagingMode:FBGraphObjectPagingModeAsNeeded] + autorelease]; + self.loader = loader; + self.loader.delegate = self; + + // Self + self.dataSource = dataSource; + self.delegate = nil; + self.selectionManager = selectionManager; + self.selectionManager.allowsMultipleSelection = NO; + self.resultsLimit = defaultResultsLimit; + self.radiusInMeters = defaultRadius; + self.itemPicturesEnabled = YES; + self.trackActiveSession = YES; +} + +- (void)dealloc +{ + [_loader cancel]; + _loader.delegate = nil; + [_loader release]; + + _dataSource.controllerDelegate = nil; + + [_dataSource release]; + [_fieldsForRequest release]; + [_searchText release]; + [_searchTextChangedTimer release]; + [_selectionManager release]; + [_spinner release]; + [_tableView release]; + + [self removeSessionObserver:_session]; + [_session release]; + + [super dealloc]; +} + +#pragma mark - Custom Properties + +- (BOOL)itemPicturesEnabled +{ + return self.dataSource.itemPicturesEnabled; +} + +- (void)setItemPicturesEnabled:(BOOL)itemPicturesEnabled +{ + self.dataSource.itemPicturesEnabled = itemPicturesEnabled; +} + +- (id)selection +{ + NSArray *selection = self.selectionManager.selection; + if ([selection count]) { + return [selection objectAtIndex:0]; + } else { + return nil; + } +} + +- (void)setSession:(FBSession *)session { + if (session != _session) { + [self removeSessionObserver:_session]; + + [_session release]; + _session = [session retain]; + + [self addSessionObserver:session]; + + self.loader.session = session; + + self.trackActiveSession = (session == nil); + } +} + +#pragma mark - Public Methods + +- (void)loadData +{ + // when the app calls loadData, + // if we don't have a session and there is + // an open active session, use that + if (!self.session || + (self.trackActiveSession && ![self.session isEqual:[FBSession activeSessionIfOpen]])) { + self.session = [FBSession activeSessionIfOpen]; + self.trackActiveSession = YES; + } + + // Sending a request on every keystroke is wasteful of bandwidth. Send a + // request the first time the user types something, then set up a 2-second timer + // and send whatever changes the user has made since then. (If nothing has changed + // in 2 seconds, we reset so the next change will cause an immediate re-query.) + if (!self.searchTextChangedTimer) { + self.searchTextChangedTimer = [self createSearchTextChangedTimer]; + [self loadDataPostThrottleSkippingRoundTripIfCached:[NSNumber numberWithBool:YES]]; + } else { + _hasSearchTextChangedSinceLastQuery = YES; + } +} + +- (void)configureUsingCachedDescriptor:(FBCacheDescriptor*)cacheDescriptor { + if (![cacheDescriptor isKindOfClass:[FBPlacePickerCacheDescriptor class]]) { + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBPlacePickerViewController: An attempt was made to configure " + @"an instance with a cache descriptor object that was not created " + @"by the FBPlacePickerViewController class" + userInfo:nil] + raise]; + } + FBPlacePickerCacheDescriptor *cd = (FBPlacePickerCacheDescriptor*)cacheDescriptor; + self.locationCoordinate = cd.locationCoordinate; + self.radiusInMeters = cd.radiusInMeters; + self.resultsLimit = cd.resultsLimit; + self.searchText = cd.searchText; + self.fieldsForRequest = cd.fieldsForRequest; +} + +- (void)clearSelection { + [self.selectionManager clearSelectionInTableView:self.tableView]; +} + +#pragma mark - Public Class Methods + ++ (FBCacheDescriptor*)cacheDescriptorWithLocationCoordinate:(CLLocationCoordinate2D)locationCoordinate + radiusInMeters:(NSInteger)radiusInMeters + searchText:(NSString*)searchText + resultsLimit:(NSInteger)resultsLimit + fieldsForRequest:(NSSet*)fieldsForRequest { + + return [[[FBPlacePickerCacheDescriptor alloc] initWithLocationCoordinate:locationCoordinate + radiusInMeters:radiusInMeters + searchText:searchText + resultsLimit:resultsLimit + fieldsForRequest:fieldsForRequest] + autorelease]; +} + +#pragma mark - private methods + +- (void)viewDidLoad +{ + [super viewDidLoad]; + [FBLogger registerCurrentTime:FBLoggingBehaviorPerformanceCharacteristics + withTag:self]; + CGRect bounds = self.canvasView.bounds; + + if (!self.tableView) { + UITableView *tableView = [[[UITableView alloc] initWithFrame:bounds] autorelease]; + tableView.autoresizingMask = + UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + self.tableView = tableView; + [self.canvasView addSubview:tableView]; + } + + if (!self.spinner) { + UIActivityIndicatorView *spinner = [[[UIActivityIndicatorView alloc] + initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray] + autorelease]; + spinner.hidesWhenStopped = YES; + // We want user to be able to scroll while we load. + spinner.userInteractionEnabled = NO; + + self.spinner = spinner; + [self.canvasView addSubview:spinner]; + } + + self.tableView.delegate = self.selectionManager; + [self.dataSource bindTableView:self.tableView]; + self.loader.tableView = self.tableView; +} + +- (void)viewDidUnload +{ + [super viewDidUnload]; + + self.loader.tableView = nil; + self.spinner = nil; + self.tableView = nil; +} + ++ (FBRequest*)requestForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate + radiusInMeters:(NSInteger)radius + resultsLimit:(NSInteger)resultsLimit + searchText:(NSString*)searchText + fields:(NSSet*)fieldsForRequest + datasource:(FBGraphObjectTableDataSource*)datasource + session:(FBSession*)session { + + FBRequest *request = [FBRequest requestForPlacesSearchAtCoordinate:coordinate + radiusInMeters:radius + resultsLimit:resultsLimit + searchText:searchText]; + [request setSession:session]; + + NSString *fields = [datasource fieldsForRequestIncluding:fieldsForRequest, + @"id", + @"name", + @"location", + @"category", + @"picture", + @"were_here_count", + nil]; + + [request.parameters setObject:fields forKey:@"fields"]; + + return request; +} + +- (void)loadDataPostThrottleSkippingRoundTripIfCached:(NSNumber*)skipRoundTripIfCached { + // Place queries require a session, so do nothing if we don't have one. + if (self.session) { + FBRequest *request = [FBPlacePickerViewController requestForPlacesSearchAtCoordinate:self.locationCoordinate + radiusInMeters:self.radiusInMeters + resultsLimit:self.resultsLimit + searchText:self.searchText + fields:self.fieldsForRequest + datasource:self.dataSource + session:self.session]; + _hasSearchTextChangedSinceLastQuery = NO; + [self.loader startLoadingWithRequest:request + cacheIdentity:FBPlacePickerCacheIdentity + skipRoundtripIfCached:skipRoundTripIfCached.boolValue]; + } +} + +- (void)updateView +{ + [self.dataSource update]; + [self.tableView reloadData]; +} + +- (NSTimer *)createSearchTextChangedTimer { + NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:searchTextChangedTimerInterval + target:self + selector:@selector(searchTextChangedTimerFired:) + userInfo:nil + repeats:YES]; + return timer; +} + +- (void)searchTextChangedTimerFired:(NSTimer *)timer +{ + if (_hasSearchTextChangedSinceLastQuery) { + [self loadDataPostThrottleSkippingRoundTripIfCached:[NSNumber numberWithBool:YES]]; + } else { + // Nothing has changed in 2 seconds. Invalidate and forget about this timer. + // Next time the user types, we will fire a query immediately again. + [self.searchTextChangedTimer invalidate]; + self.searchTextChangedTimer = nil; + } +} + +- (void)centerAndStartSpinner +{ + [FBUtility centerView:self.spinner tableView:self.tableView]; + [self.spinner startAnimating]; +} + +- (void)addSessionObserver:(FBSession *)session { + [session addObserver:self + forKeyPath:@"state" + options:NSKeyValueObservingOptionNew + context:nil]; +} + +- (void)removeSessionObserver:(FBSession *)session { + [session removeObserver:self + forKeyPath:@"state"]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([object isEqual:self.session] && + self.session.isOpen == NO) { + [self clearData]; + } +} + +- (void)clearData { + [self.dataSource clearGraphObjects]; + [self.selectionManager clearSelectionInTableView:self.tableView]; + [self.tableView reloadData]; + [self.loader reset]; +} + +#pragma mark - FBGraphObjectSelectionChangedDelegate + +- (void)graphObjectTableSelectionDidChange: +(FBGraphObjectTableSelection *)selection +{ + if ([self.delegate respondsToSelector: + @selector(placePickerViewControllerSelectionDidChange:)]) { + [(id)self.delegate placePickerViewControllerSelectionDidChange:self]; + } +} + +#pragma mark - FBGraphObjectViewControllerDelegate + +- (BOOL)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + filterIncludesItem:(id)item +{ + id place = (id)item; + + if ([self.delegate + respondsToSelector:@selector(placePickerViewController:shouldIncludePlace:)]) { + return [(id)self.delegate placePickerViewController:self + shouldIncludePlace:place]; + } else { + return YES; + } +} + +- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + titleOfItem:(id)graphObject +{ + return [graphObject objectForKey:@"name"]; +} + +- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + subtitleOfItem:(id)graphObject +{ + NSString *category = [graphObject objectForKey:@"category"]; + NSNumber *wereHereCount = [graphObject objectForKey:@"were_here_count"]; + + if (wereHereCount) { + NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle]; + NSString *wereHere = [numberFormatter stringFromNumber:wereHereCount]; + [numberFormatter release]; + + if (category) { + return [NSString stringWithFormat:@"%@ • %@ were here", [category capitalizedString], wereHere]; + } + return [NSString stringWithFormat:@"%@ were here", wereHere]; + } + if (category) { + return [category capitalizedString]; + } + return nil; +} + +- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource + pictureUrlOfItem:(id)graphObject +{ + id picture = [graphObject objectForKey:@"picture"]; + // Depending on what migration the app is in, we may get back either a string, or a + // dictionary with a "data" property that is a dictionary containing a "url" property. + if ([picture isKindOfClass:[NSString class]]) { + return picture; + } + id data = [picture objectForKey:@"data"]; + return [data objectForKey:@"url"]; +} + +#pragma mark FBGraphObjectPagingLoaderDelegate members + +- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader willLoadURL:(NSString*)url { + // We only want to display our spinner on loading the first page. After that, + // a spinner will display in the last cell to indicate to the user that data is loading. + if ([self.dataSource numberOfSectionsInTableView:self.tableView] == 0) { + [self centerAndStartSpinner]; + } +} + +- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader didLoadData:(NSDictionary*)results { + [self.spinner stopAnimating]; + + // This logging currently goes here because we're effectively complete with our initial view when + // the first page of results come back. In the future, when we do caching, we will need to move + // this to a more appropriate place (e.g., after the cache has been brought in). + [FBLogger singleShotLogEntry:FBLoggingBehaviorPerformanceCharacteristics + timestampTag:self + formatString:@"Places Picker: first render "]; // logger will append "%d msec" + + if ([self.delegate respondsToSelector:@selector(placePickerViewControllerDataDidChange:)]) { + [(id)self.delegate placePickerViewControllerDataDidChange:self]; + } +} + +- (void)pagingLoaderDidFinishLoading:(FBGraphObjectPagingLoader *)pagingLoader { + // No more results, stop spinner + [self.spinner stopAnimating]; + + // Call the delegate from here as well, since this might be the first response of a query + // that has no results. + if ([self.delegate respondsToSelector:@selector(placePickerViewControllerDataDidChange:)]) { + [(id)self.delegate placePickerViewControllerDataDidChange:self]; + } + + // if our current display is from cache, then kick-off a near-term refresh + if (pagingLoader.isResultFromCache) { + [self loadDataPostThrottleSkippingRoundTripIfCached:[NSNumber numberWithBool:NO]]; + } +} + +- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader handleError:(NSError*)error { + if ([self.delegate respondsToSelector:@selector(placePickerViewController:handleError:)]) { + [(id)self.delegate placePickerViewController:self handleError:error]; + } + +} + +- (void)pagingLoaderWasCancelled:(FBGraphObjectPagingLoader*)pagingLoader { + [self.spinner stopAnimating]; +} + +@end + diff --git a/src/ios/facebook/FBProfilePictureView.h b/src/ios/facebook/FBProfilePictureView.h new file mode 100644 index 000000000..a0ec50a10 --- /dev/null +++ b/src/ios/facebook/FBProfilePictureView.h @@ -0,0 +1,80 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +/*! + @typedef FBProfilePictureCropping enum + + @abstract + Type used to specify the cropping treatment of the profile picture. + + @discussion + */ +typedef enum { + + /*! Square (default) - the square version that the Facebook user defined. */ + FBProfilePictureCroppingSquare = 0, + + /*! Original - the original profile picture, as uploaded. */ + FBProfilePictureCroppingOriginal = 1 + +} FBProfilePictureCropping; + +/*! + @class + @abstract + An instance of `FBProfilePictureView` is used to display a profile picture. + + The default behavior of this control is to center the profile picture + in the view and shrinks it, if necessary, to the view's bounds, preserving the aspect ratio. The smallest + possible image is downloaded to ensure that scaling up never happens. Resizing the view may result in + a different size of the image being loaded. Canonical image sizes are documented in the "Pictures" section + of https://developers.facebook.com/docs/reference/api. + */ +@interface FBProfilePictureView : UIView + +/*! + @abstract + The Facebook ID of the user, place or object for which a picture should be fetched and displayed. + */ +@property (copy, nonatomic) NSString* profileID; + +/*! + @abstract + The cropping to use for the profile picture. + */ +@property (nonatomic) FBProfilePictureCropping pictureCropping; + +/*! + @abstract + Initializes and returns a profile view object. + */ +- (id)init; + + +/*! + @abstract + Initializes and returns a profile view object for the given Facebook ID and cropping. + + @param profileID The Facebook ID of the user, place or object for which a picture should be fetched and displayed. + @param pictureCropping The cropping to use for the profile picture. + */ +- (id)initWithProfileID:(NSString*)profileID + pictureCropping:(FBProfilePictureCropping)pictureCropping; + + +@end diff --git a/src/ios/facebook/FBProfilePictureView.m b/src/ios/facebook/FBProfilePictureView.m new file mode 100644 index 000000000..d649b85f5 --- /dev/null +++ b/src/ios/facebook/FBProfilePictureView.m @@ -0,0 +1,237 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBProfilePictureView.h" +#import "FBURLConnection.h" +#import "FBRequest.h" +#import "FBUtility.h" +#import "FBSDKVersion.h" + +@interface FBProfilePictureView() + +@property (readonly, nonatomic) NSString *imageQueryParamString; +@property (retain, nonatomic) NSString *previousImageQueryParamString; + +@property (retain, nonatomic) FBURLConnection *connection; +@property (retain, nonatomic) UIImageView *imageView; + +- (void)initialize; +- (void)refreshImage:(BOOL)forceRefresh; +- (void)ensureImageViewContentMode; + +@end + +@implementation FBProfilePictureView + +@synthesize profileID = _profileID; +@synthesize pictureCropping = _pictureCropping; +@synthesize connection = _connection; +@synthesize imageView = _imageView; +@synthesize previousImageQueryParamString = _previousImageQueryParamString; + +#pragma mark - Lifecycle + +- (void)dealloc { + [_profileID release]; + [_imageView release]; + [_connection release]; + [_previousImageQueryParamString release]; + + [super dealloc]; +} + +- (id)init { + self = [super init]; + if (self) { + [self initialize]; + } + + return self; +} + +- (id)initWithProfileID:(NSString *)profileID + pictureCropping:(FBProfilePictureCropping)pictureCropping { + self = [self init]; + if (self) { + self.pictureCropping = pictureCropping; + self.profileID = profileID; + } + + return self; +} + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self initialize]; + } + + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (self) { + [self initialize]; + } + return self; +} + +#pragma mark - + +- (NSString *)imageQueryParamString { + + static CGFloat screenScaleFactor = 0.0; + if (screenScaleFactor == 0.0) { + screenScaleFactor = [[UIScreen mainScreen] scale]; + } + + // Retina display doesn't increase the bounds that iOS returns. The larger size to fetch needs + // to be calculated using the scale factor accessed above. + int width = (int)(self.bounds.size.width * screenScaleFactor); + + if (self.pictureCropping == FBProfilePictureCroppingSquare) { + return [NSString stringWithFormat:@"width=%d&height=%d&migration_bundle=%@", + width, + width, + FB_IOS_SDK_MIGRATION_BUNDLE]; + } + + // For non-square images, we choose between three variants knowing that the small profile picture is + // 50 pixels wide, normal is 100, and large is about 200. + if (width <= 50) { + return @"type=small"; + } else if (width <= 100) { + return @"type=normal"; + } else { + return @"type=large"; + } +} + +- (void)initialize { + // the base class can cause virtual recursion, so + // to handle this we make initialize idempotent + if (self.imageView) { + return; + } + + UIImageView* imageView = [[[UIImageView alloc] initWithFrame:self.bounds] autorelease]; + imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.imageView = imageView; + + self.autoresizesSubviews = YES; + self.clipsToBounds = YES; + + [self addSubview:self.imageView]; +} + +- (void)refreshImage:(BOOL)forceRefresh { + NSString *newImageQueryParamString = self.imageQueryParamString; + + // If not forcing refresh, check to see if the previous size we used would be the same + // as what we'd request now, as this method could be called often on control bounds animation, + // and we only want to fetch when needed. + if (!forceRefresh && [self.previousImageQueryParamString isEqualToString:newImageQueryParamString]) { + + // But we still may need to adjust the contentMode. + [self ensureImageViewContentMode]; + return; + } + + if (self.profileID) { + + [self.connection cancel]; + + FBURLConnectionHandler handler = + ^(FBURLConnection *connection, NSError *error, NSURLResponse *response, NSData *data) { + FBConditionalLog(self.connection == connection, @"Inconsistent connection state"); + + self.connection = nil; + if (!error) { + self.imageView.image = [UIImage imageWithData:data]; + [self ensureImageViewContentMode]; + } + }; + + NSString *template = @"%@/%@/picture?%@"; + NSString *urlString = [NSString stringWithFormat:template, + FBGraphBasePath, + self.profileID, + newImageQueryParamString]; + NSURL *url = [NSURL URLWithString:urlString]; + + self.connection = [[[FBURLConnection alloc] initWithURL:url + completionHandler:handler] + autorelease]; + } else { + BOOL isSquare = (self.pictureCropping == FBProfilePictureCroppingSquare); + + NSString *blankImageName = + [NSString + stringWithFormat:@"FacebookSDKResources.bundle/FBProfilePictureView/images/fb_blank_profile_%@.png", + isSquare ? @"square" : @"portrait"]; + + self.imageView.image = [UIImage imageNamed:blankImageName]; + [self ensureImageViewContentMode]; + } + + self.previousImageQueryParamString = newImageQueryParamString; +} + +- (void)ensureImageViewContentMode { + // Set the image's contentMode such that if the image is larger than the control, we scale it down, preserving aspect + // ratio. Otherwise, we center it. This ensures that we never scale up, and pixellate, the image. + CGSize viewSize = self.bounds.size; + CGSize imageSize = self.imageView.image.size; + UIViewContentMode contentMode; + + // If both of the view dimensions are larger than the image, we'll center the image to prevent scaling up. + // Note that unlike in choosing the image size, we *don't* use any Retina-display scaling factor to choose centering + // vs. filling. If we were to do so, we'd get profile pics shrinking to fill the the view on non-Retina, but getting + // centered and clipped on Retina. + if (viewSize.width > imageSize.width && viewSize.height > imageSize.height) { + contentMode = UIViewContentModeCenter; + } else { + contentMode = UIViewContentModeScaleAspectFit; + } + + self.imageView.contentMode = contentMode; +} + +- (void)setProfileID:(NSString*)profileID { + if (!_profileID || ![_profileID isEqualToString:profileID]) { + [_profileID release]; + _profileID = [profileID copy]; + [self refreshImage:YES]; + } +} + +- (void)setPictureCropping:(FBProfilePictureCropping)pictureCropping { + if (_pictureCropping != pictureCropping) { + _pictureCropping = pictureCropping; + [self refreshImage:YES]; + } +} + +// Lets us catch resizes of the control, or any outer layout, allowing us to potentially +// choose a different image. +- (void)layoutSubviews { + [self refreshImage:NO]; + [super layoutSubviews]; +} + + +@end diff --git a/src/ios/facebook/FBRequest.h b/src/ios/facebook/FBRequest.h new file mode 100644 index 000000000..6626c5720 --- /dev/null +++ b/src/ios/facebook/FBRequest.h @@ -0,0 +1,504 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import "FBRequestConnection.h" +#import "FBGraphObject.h" + +/*! The base URL used for graph requests */ +extern NSString* const FBGraphBasePath; + +// up-front decl's +@protocol FBRequestDelegate; +@class FBSession; +@class UIImage; + +/*! + @typedef FBRequestState + + @abstract + Deprecated - do not use in new code. + + @discussion + FBRequestState is retained from earlier versions of the SDK to give existing + apps time to remove dependency on this. + + @deprecated +*/ +typedef NSUInteger FBRequestState __attribute__((deprecated)); + +/*! + @class FBRequest + + @abstract + The `FBRequest` object is used to setup and manage requests to Facebook Graph + and REST APIs. This class provides helper methods that simplify the connection + and response handling. + + @discussion + An object is required for all authenticated uses of `FBRequest`. + Requests that do not require an unauthenticated user are also supported and + do not require an object to be passed in. + + An instance of `FBRequest` represents the arguments and setup for a connection + to Facebook. After creating an `FBRequest` object it can be used to setup a + connection to Facebook through the object. The + object is created to manage a single connection. To + cancel a connection use the instance method in the class. + + An `FBRequest` object may be reused to issue multiple connections to Facebook. + However each instance will manage one connection. + + Class and instance methods prefixed with **start* ** can be used to perform the + request setup and initiate the connection in a single call. + +*/ +@interface FBRequest : NSObject { +@private + id _delegate; + NSString* _url; + NSURLConnection* _connection; + NSMutableData* _responseText; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + FBRequestState _state; +#pragma GCC diagnostic pop + NSError* _error; + BOOL _sessionDidExpire; + id _graphObject; +} + +/*! + @methodgroup Creating a request + + @method + Calls with the default parameters. +*/ +- (id)init; + +/*! + @method + Calls with default parameters + except for the ones provided to this method. + + @param session The session object representing the identity of the Facebook user making + the request. A nil value indicates a request that requires no token; to + use the active session pass `[FBSession activeSession]`. + + @param graphPath The Graph API endpoint to use for the request, for example "me". +*/ +- (id)initWithSession:(FBSession*)session + graphPath:(NSString *)graphPath; + +/*! + @method + + @abstract + Initializes an `FBRequest` object for a Graph API request call. + + @discussion + Note that this only sets properties on the `FBRequest` object. + + To send the request, initialize an object, add this request, + and send <[FBRequestConnection start]>. See other methods on this + class for shortcuts to simplify this process. + + @param session The session object representing the identity of the Facebook user making + the request. A nil value indicates a request that requires no token; to + use the active session pass `[FBSession activeSession]`. + + @param graphPath The Graph API endpoint to use for the request, for example "me". + + @param parameters The parameters for the request. A value of nil sends only the automatically handled + parameters, for example, the access token. The default is nil. + + @param HTTPMethod The HTTP method to use for the request. The default is value of nil implies a GET. +*/ +- (id)initWithSession:(FBSession*)session + graphPath:(NSString *)graphPath + parameters:(NSDictionary *)parameters + HTTPMethod:(NSString *)HTTPMethod; + +/*! + @method + @abstract + Initialize a `FBRequest` object that will do a graph request. + + @discussion + Note that this only sets properties on the `FBRequest`. + + To send the request, initialize a , add this request, + and send <[FBRequestConnection start]>. See other methods on this + class for shortcuts to simplify this process. + + @param session The session object representing the identity of the Facebook user making + the request. A nil value indicates a request that requires no token; to + use the active session pass `[FBSession activeSession]`. + + @param graphPath The Graph API endpoint to use for the request, for example "me". + + @param graphObject An object or open graph action to post. +*/ +- (id)initForPostWithSession:(FBSession*)session + graphPath:(NSString *)graphPath + graphObject:(id)graphObject; + +/*! + @method + @abstract + Initialize a `FBRequest` object that will do a rest API request. + + @discussion + Prefer to use graph requests instead of this where possible. + + Note that this only sets properties on the `FBRequest`. + + To send the request, initialize a , add this request, + and send <[FBRequestConnection start]>. See other methods on this + class for shortcuts to simplify this process. + + @param session The session object representing the identity of the Facebook user making + the request. A nil value indicates a request that requires no token; to + use the active session pass `[FBSession activeSession]`. + + @param restMethod A valid REST API method. + + @param parameters The parameters for the request. A value of nil sends only the automatically handled + parameters, for example, the access token. The default is nil. + + @param HTTPMethod The HTTP method to use for the request. The default is value of nil implies a GET. + +*/ +- (id)initWithSession:(FBSession*)session + restMethod:(NSString *)restMethod + parameters:(NSDictionary *)parameters + HTTPMethod:(NSString *)HTTPMethod; + +/*! + @abstract + The parameters for the request. + + @discussion + May be used to read the parameters that were automatically set during + the object initiliazation. Make any required modifications prior to + sending the request. + + `NSString` parameters are used to generate URL parameter values or JSON + parameters. `NSData` and `UIImage` parameters are added as attachments + to the HTTP body and referenced by name in the URL and/or JSON. +*/ +@property(nonatomic, retain, readonly) NSMutableDictionary *parameters; + +/*! + @abstract + The session object to use for the request. + + @discussion + May be used to read the session that was automatically set during + the object initiliazation. Make any required modifications prior to + sending the request. +*/ +@property(nonatomic, retain) FBSession *session; + +/*! + @abstract + The Graph API endpoint to use for the request, for example "me". + + @discussion + May be used to read the Graph API endpoint that was automatically set during + the object initiliazation. Make any required modifications prior to + sending the request. +*/ +@property(nonatomic, copy) NSString *graphPath; + +/*! + @abstract + A valid REST API method. + + @discussion + May be used to read the REST method that was automatically set during + the object initiliazation. Make any required modifications prior to + sending the request. + + Use the Graph API equivalent of the API if it exists as the REST API + method is deprecated if there is a Graph API equivalent. +*/ +@property(nonatomic, copy) NSString *restMethod; + +/*! + @abstract + The HTTPMethod to use for the request, for example "GET" or "POST". + + @discussion + May be used to read the HTTP method that was automatically set during + the object initiliazation. Make any required modifications prior to + sending the request. +*/ +@property(nonatomic, copy) NSString *HTTPMethod; + +/*! + @abstract + The graph object to post with the request. + + @discussion + May be used to read the graph object that was automatically set during + the object initiliazation. Make any required modifications prior to + sending the request. +*/ +@property(nonatomic, retain) id graphObject; + +/*! + @methodgroup Instance methods +*/ + +/*! + @method + + @abstract + Starts a connection to the Facebook API. + + @discussion + This is used to start an API call to Facebook and call the block when the + request completes with a success, error, or cancel. + + @param handler The handler block to call when the request completes with a success, error, or cancel action. +*/ +- (FBRequestConnection*)startWithCompletionHandler:(FBRequestHandler)handler; + +/*! + @methodgroup FBRequestConnection start methods + + @abstract + These methods start an . + + @discussion + These methods simplify the process of preparing a request and starting + the connection. The methods handle initializing an `FBRequest` object, + initializing a object, adding the `FBRequest` + object to the to the , and finally starting the + connection. +*/ + +/*! + @methodgroup FBRequest factory methods + + @abstract + These methods initialize a `FBRequest` for common scenarios. + + @discussion + These simplify the process of preparing a request to send. These + initialize a `FBRequest` based on strongly typed parameters that are + specific to the scenario. + + These method do not initialize an object. To initiate the API + call first instantiate an object, add the request to this object, + then call the `start` method on the connection instance. +*/ + +// request* +// +// Summary: +// Helper methods used to create common request objects which can be used to create single or batch connections +// +// session: - the session object representing the identity of the +// Facebook user making the request; nil implies an +// unauthenticated request; default=nil + +/*! + @method + + @abstract + Creates a request representing a Graph API call to the "me" endpoint, using the active session. + + @discussion + Simplifies preparing a request to retrieve the user's identity. + + This method does not initialize an object. To initiate the API + call first instantiate an object, add the request to this object, + then call the `start` method on the connection instance. + + A successful Graph API call will return an object representing the + user's identity. + + Note you may change the session property after construction if a session other than + the active session is preferred. +*/ ++ (FBRequest*)requestForMe; + +/*! + @method + + @abstract + Creates a request representing a Graph API call to the "me/friends" endpoint using the active session. + + @discussion + Simplifies preparing a request to retrieve the user's friends. + + This method does not initialize an object. To initiate the API + call first instantiate an object, add the request to this object, + then call the `start` method on the connection instance. + + A successful Graph API call will return an array of objects representing the + user's friends. +*/ ++ (FBRequest*)requestForMyFriends; + +/*! + @method + + @abstract + Creates a request representing a Graph API call to upload a photo to the app's album using the active session. + + @discussion + Simplifies preparing a request to post a photo. + + To post a photo to a specific album, get the `FBRequest` returned from this method + call, then modify the request parameters by adding the album ID to an "album" key. + + This method does not initialize an object. To initiate the API + call first instantiate an object, add the request to this object, + then call the `start` method on the connection instance. + + @param photo A `UIImage` for the photo to upload. +*/ ++ (FBRequest*)requestForUploadPhoto:(UIImage *)photo; + +/*! + @method + + @abstract + Creates a request representing a status update. + + @discussion + Simplifies preparing a request to post a status update. + + This method does not initialize an object. To initiate the API + call first instantiate an object, add the request to this object, + then call the `start` method on the connection instance. + + @param message The message to post. + */ ++ (FBRequest *)requestForPostStatusUpdate:(NSString *)message; + +/*! + @method + + @abstract + Creates a request representing a status update. + + @discussion + Simplifies preparing a request to post a status update. + + This method does not initialize an object. To initiate the API + call first instantiate an object, add the request to this object, + then call the `start` method on the connection instance. + + @param message The message to post. + @param place The place to checkin with, or nil. Place may be an fbid or a + graph object representing a place. + @param tags Array of friends to tag in the status update, each element + may be an fbid or a graph object representing a user. + */ ++ (FBRequest *)requestForPostStatusUpdate:(NSString *)message + place:(id)place + tags:(id)tags; + +/*! + @method + + @abstract + Creates a request representing a Graph API call to the "search" endpoint + for a given location using the active session. + + @discussion + Simplifies preparing a request to search for places near a coordinate. + + This method does not initialize an object. To initiate the API + call first instantiate an object, add the request to this object, + then call the `start` method on the connection instance. + + A successful Graph API call will return an array of objects representing + the nearby locations. + + @param coordinate The search coordinates. + + @param radius The search radius in meters. + + @param limit The maxiumum number of results to return. It is + possible to receive fewer than this because of the radius and because of server limits. + + @param searchText The text to use in the query to narrow the set of places + returned. +*/ ++ (FBRequest*)requestForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate + radiusInMeters:(NSInteger)radius + resultsLimit:(NSInteger)limit + searchText:(NSString*)searchText; + +/*! + @method + + @abstract + Returns a newly initialized request object that can be used to make a Graph API call for the active session. + + @discussion + This method simplifies the preparation of a Graph API call. + + This method does not initialize an object. To initiate the API + call first instantiate an object, add the request to this object, + then call the `start` method on the connection instance. + + @param graphPath The Graph API endpoint to use for the request, for example "me". + */ ++ (FBRequest*)requestForGraphPath:(NSString*)graphPath; + +/*! + @method + + @abstract + Creates a request representing a POST for a graph object. + + @param graphPath The Graph API endpoint to use for the request, for example "me". + + @param graphObject An object or open graph action to post. + */ ++ (FBRequest*)requestForPostWithGraphPath:(NSString*)graphPath + graphObject:(id)graphObject; + +/*! + @method + + @abstract + Returns a newly initialized request object that can be used to make a Graph API call for the active session. + + @discussion + This method simplifies the preparation of a Graph API call. + + This method does not initialize an object. To initiate the API + call first instantiate an object, add the request to this object, + then call the `start` method on the connection instance. + + @param graphPath The Graph API endpoint to use for the request, for example "me". + + @param parameters The parameters for the request. A value of nil sends only the automatically handled parameters, for example, the access token. The default is nil. + + @param HTTPMethod The HTTP method to use for the request. A nil value implies a GET. + */ ++ (FBRequest*)requestWithGraphPath:(NSString*)graphPath + parameters:(NSDictionary*)parameters + HTTPMethod:(NSString*)HTTPMethod; +@end diff --git a/src/ios/facebook/FBRequest.m b/src/ios/facebook/FBRequest.m new file mode 100644 index 000000000..a7f2700e2 --- /dev/null +++ b/src/ios/facebook/FBRequest.m @@ -0,0 +1,445 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Facebook.h" +#import "FBUtility.h" +#import "FBSession+Internal.h" +#import "FBSDKVersion.h" + +// constants +NSString *const FBGraphBasePath = @"https://graph." FB_BASE_URL; + +static NSString *const kGetHTTPMethod = @"GET"; +static NSString *const kPostHTTPMethod = @"POST"; + +// ---------------------------------------------------------------------------- +// FBRequest + +@implementation FBRequest + +@synthesize parameters = _parameters; +@synthesize session = _session; +@synthesize graphPath = _graphPath; +@synthesize restMethod = _restMethod; +@synthesize HTTPMethod = _HTTPMethod; + +- (id)init +{ + return [self initWithSession:nil + graphPath:nil + parameters:nil + HTTPMethod:nil]; +} + +- (id)initWithSession:(FBSession*)session + graphPath:(NSString *)graphPath +{ + return [self initWithSession:session + graphPath:graphPath + parameters:nil + HTTPMethod:nil]; +} + +- (id)initForPostWithSession:(FBSession*)session + graphPath:(NSString *)graphPath + graphObject:(id)graphObject { + self = [self initWithSession:session + graphPath:graphPath + parameters:nil + HTTPMethod:kPostHTTPMethod]; + if (self) { + self.graphObject = graphObject; + } + return self; +} + +- (id)initWithSession:(FBSession*)session + restMethod:(NSString *)restMethod + parameters:(NSDictionary *)parameters + HTTPMethod:(NSString *)HTTPMethod +{ + // reusing the more common initializer... + self = [self initWithSession:session + graphPath:nil // but assuring a nil graphPath for the rest case + parameters:parameters + HTTPMethod:HTTPMethod]; + if (self) { + self.restMethod = restMethod; + } + return self; +} + +- (id)initWithSession:(FBSession*)session + graphPath:(NSString *)graphPath + parameters:(NSDictionary *)parameters + HTTPMethod:(NSString *)HTTPMethod +{ + if (self = [super init]) { + // set default for nil + if (!HTTPMethod) { + HTTPMethod = kGetHTTPMethod; + } + + self.session = session; + self.graphPath = graphPath; + self.HTTPMethod = HTTPMethod; + + // all request objects start life with a migration bundle set for the SDK + _parameters = [[NSMutableDictionary alloc] + initWithObjectsAndKeys:FB_IOS_SDK_MIGRATION_BUNDLE, @"migration_bundle", nil]; + if (parameters) { + // but the incoming dictionary's migration bundle trumps the default one, if present + [self.parameters addEntriesFromDictionary:parameters]; + } + } + return self; +} + +- (void)dealloc +{ + [_graphObject release]; + [_session release]; + [_graphPath release]; + [_restMethod release]; + [_HTTPMethod release]; + [_parameters release]; + [super dealloc]; +} + +//@property(nonatomic,retain) id graphObject; +- (id)graphObject { + return _graphObject; +} + +- (void)setGraphObject:(id)newValue { + if (_graphObject != newValue) { + [_graphObject release]; + _graphObject = [newValue retain]; + } + + // setting this property implies you want a post, if you really + // want a get, reset the method to get after setting this property + self.HTTPMethod = kPostHTTPMethod; +} + +- (FBRequestConnection*)startWithCompletionHandler:(FBRequestHandler)handler +{ + FBRequestConnection *connection = [[[FBRequestConnection alloc] init] autorelease]; + [connection addRequest:self completionHandler:handler]; + [connection start]; + return connection; +} + ++ (FBRequest*)requestForMe { + return [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen] + graphPath:@"me"] + autorelease]; +} + ++ (FBRequest*)requestForMyFriends { + return [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen] + graphPath:@"me/friends" + parameters:[NSDictionary dictionaryWithObjectsAndKeys: + @"id,name,username,first_name,last_name", @"fields", + nil] + HTTPMethod:nil] + autorelease]; +} + ++ (FBRequest *)requestForUploadPhoto:(UIImage *)photo +{ + NSString *graphPath = @"me/photos"; + NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init]; + [parameters setObject:photo forKey:@"picture"]; + + FBRequest *request = [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen] + graphPath:graphPath + parameters:parameters + HTTPMethod:@"POST"] + autorelease]; + + [parameters release]; + + return request; +} + ++ (FBRequest*)requestForGraphPath:(NSString*)graphPath +{ + FBRequest *request = [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen] + graphPath:graphPath + parameters:nil + HTTPMethod:nil] + autorelease]; + return request; +} + ++ (FBRequest*)requestForPostWithGraphPath:(NSString*)graphPath + graphObject:(id)graphObject { + return [[[FBRequest alloc] initForPostWithSession:[FBSession activeSessionIfOpen] + graphPath:graphPath + graphObject:graphObject] + autorelease]; +} + ++ (FBRequest *)requestForPostStatusUpdate:(NSString *)message { + return [FBRequest requestForPostStatusUpdate:message + place:nil + tags:nil]; +} + ++ (FBRequest *)requestForPostStatusUpdate:(NSString *)message + place:(id)place + tags:(id)tags { + + NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObject:message forKey:@"message"]; + // if we have a place object, use it + if (place) { + [params setObject:[FBUtility stringFBIDFromObject:place] + forKey:@"place"]; + } + // ditto tags + if (tags) { + NSMutableString *tagsValue = [NSMutableString string]; + NSString *format = @"%@"; + for (id tag in tags) { + [tagsValue appendFormat:format, [FBUtility stringFBIDFromObject:tag]]; + format = @",%@"; + } + if ([tagsValue length]) { + [params setObject:tagsValue + forKey:@"tags"]; + } + } + + return [FBRequest requestWithGraphPath:@"me/feed" + parameters:params + HTTPMethod:@"POST"]; +} + ++ (FBRequest*)requestWithGraphPath:(NSString*)graphPath + parameters:(NSDictionary*)parameters + HTTPMethod:(NSString*)HTTPMethod { + return [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen] + graphPath:graphPath + parameters:parameters + HTTPMethod:HTTPMethod] + autorelease]; +} + ++ (FBRequest*)requestForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate + radiusInMeters:(NSInteger)radius + resultsLimit:(NSInteger)limit + searchText:(NSString*)searchText +{ + NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init]; + [parameters setObject:@"place" forKey:@"type"]; + [parameters setObject:[NSString stringWithFormat:@"%d", limit] forKey:@"limit"]; + [parameters setObject:[NSString stringWithFormat:@"%lf,%lf", coordinate.latitude, coordinate.longitude] + forKey:@"center"]; + [parameters setObject:[NSString stringWithFormat:@"%d", radius] forKey:@"distance"]; + if ([searchText length]) { + [parameters setObject:searchText forKey:@"q"]; + } + + FBRequest *request = [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen] + graphPath:@"search" + parameters:parameters + HTTPMethod:nil] + autorelease]; + [parameters release]; + + return request; +} + +@end + +// ---------------------------------------------------------------------------- +// Deprecated FBRequest implementation + +@implementation FBRequest (Deprecated) + +// ---------------------------------------------------------------------------- +// deprecated public properties + +//@property(nonatomic,assign) id delegate; +- (id)delegate { + return _delegate; +} + +- (void)setDelegate:(id)newValue { + _delegate = newValue; +} + +//@property(nonatomic,copy) NSString* url; +- (NSString*)url { + return _url; +} + +- (void)setUrl:(NSString*)newValue { + if (_url != newValue) { + [_url release]; + _url = [newValue copy]; + } +} + +//@property(nonatomic,copy) NSString* httpMethod; +- (NSString*)httpMethod { + return self.HTTPMethod; +} + +- (void)setHttpMethod:(NSString*)newValue { + self.HTTPMethod = newValue; +} + +//@property(nonatomic,retain) NSMutableDictionary* params; +- (NSMutableDictionary*)params { + return _parameters; +} + +- (void)setParams:(NSMutableDictionary*)newValue { + if (_parameters != newValue) { + [_parameters release]; + _parameters = [newValue retain]; + } +} + +//@property(nonatomic,retain) NSURLConnection* connection; +- (NSURLConnection*)connection { + return _connection; +} + +- (void)setConnection:(NSURLConnection*)newValue { + if (_connection != newValue) { + [_connection release]; + _connection = [newValue retain]; + } +} + +//@property(nonatomic,retain) NSMutableData* responseText; +- (NSMutableData*)responseText { + return _responseText; +} + +- (void)setResponseText:(NSMutableData*)newValue { + if (_responseText != newValue) { + [_responseText release]; + _responseText = [newValue retain]; + } +} + +//@property(nonatomic,retain) NSError* error; +- (NSError*)error { + return _error; +} + +- (void)setError:(NSError*)newValue { + if (_error != newValue) { + [_error release]; + _error = [newValue retain]; + } +} + +//@property(nonatomic,readonly+readwrite) FBRequestState state; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +- (FBRequestState)state { + return _state; +} + +- (void)setState:(FBRequestState)newValue { + _state = newValue; +} +#pragma GCC diagnostic pop + +//@property(nonatomic,readonly+readwrite) BOOL sessionDidExpire; +- (BOOL)sessionDidExpire { + return _sessionDidExpire; +} + +- (void)setSessionDidExpire:(BOOL)newValue { + _sessionDidExpire = newValue; +} + +// ---------------------------------------------------------------------------- +// deprecated public methods + +- (BOOL)loading +{ + return (_state == kFBRequestStateLoading); +} + ++ (NSString *)serializeURL:(NSString *)baseUrl + params:(NSDictionary *)params { + return [self serializeURL:baseUrl params:params httpMethod:kGetHTTPMethod]; +} + ++ (NSString*)serializeURL:(NSString *)baseUrl + params:(NSDictionary *)params + httpMethod:(NSString *)httpMethod { + + NSURL* parsedURL = [NSURL URLWithString:baseUrl]; + NSString* queryPrefix = parsedURL.query ? @"&" : @"?"; + + NSMutableArray* pairs = [NSMutableArray array]; + for (NSString* key in [params keyEnumerator]) { + id value = [params objectForKey:key]; + if ([value isKindOfClass:[UIImage class]] + || [value isKindOfClass:[NSData class]]) { + if ([httpMethod isEqualToString:kGetHTTPMethod]) { + NSLog(@"can not use GET to upload a file"); + } + continue; + } + + NSString* escaped_value = [FBUtility stringByURLEncodingString:value]; + [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, escaped_value]]; + } + NSString* query = [pairs componentsJoinedByString:@"&"]; + + return [NSString stringWithFormat:@"%@%@%@", baseUrl, queryPrefix, query]; +} + +#pragma mark Debugging helpers + +- (NSString*)description { + NSMutableString *result = [NSMutableString stringWithFormat:@"<%@: %p, session: %p", + NSStringFromClass([self class]), + self, + self.session]; + if (self.graphPath) { + [result appendFormat:@", graphPath: %@", self.graphPath]; + } + if (self.graphObject) { + [result appendFormat:@", graphObject: %@", self.graphObject]; + NSString *graphObjectID = [self.graphObject objectForKey:@"id"]; + if (graphObjectID) { + [result appendFormat:@" (id=%@)", graphObjectID]; + } + } + if (self.restMethod) { + [result appendFormat:@", restMethod: %@", self.restMethod]; + } + if (self.HTTPMethod) { + [result appendFormat:@", HTTPMethod: %@", self.HTTPMethod]; + } + [result appendFormat:@", parameters: %@>", [self.parameters description]]; + return result; + +} + +#pragma mark - + +@end diff --git a/src/ios/facebook/FBRequestBody.h b/src/ios/facebook/FBRequestBody.h new file mode 100644 index 000000000..ea76e5082 --- /dev/null +++ b/src/ios/facebook/FBRequestBody.h @@ -0,0 +1,41 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import "FBLogger.h" + +@interface FBRequestBody : NSObject + +@property (nonatomic, retain, readonly) NSData *data; + +- (id)init; + +- (void)appendWithKey:(NSString *)key + formValue:(NSString *)value + logger:(FBLogger *)logger; + +- (void)appendWithKey:(NSString *)key + imageValue:(UIImage *)image + logger:(FBLogger *)logger; + +- (void)appendWithKey:(NSString *)key + dataValue:(NSData *)data + logger:(FBLogger *)logger; + ++ (NSString *)mimeContentType; + +@end diff --git a/src/ios/facebook/FBRequestBody.m b/src/ios/facebook/FBRequestBody.m new file mode 100644 index 000000000..44bc52e9d --- /dev/null +++ b/src/ios/facebook/FBRequestBody.m @@ -0,0 +1,113 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBRequestBody.h" + +static NSString *kStringBoundary = @"3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f"; + +@interface FBRequestBody () +@property (nonatomic, retain, readonly) NSMutableData *mutableData; +- (void)appendUTF8:(NSString *)utf8; +@end + +@implementation FBRequestBody + +@synthesize mutableData = _mutableData; + +- (id)init +{ + if (self = [super init]) { + _mutableData = [[NSMutableData alloc] init]; + } + + return self; +} + +- (void)dealloc +{ + [_mutableData release]; + [super dealloc]; +} + ++ (NSString *)mimeContentType +{ + return [NSString stringWithFormat:@"multipart/form-data; boundary=%@", kStringBoundary]; +} + +- (void)appendUTF8:(NSString *)utf8 +{ + if (![self.mutableData length]) { + NSString *headerUTF8 = [NSString stringWithFormat:@"--%@\r\n", kStringBoundary]; + NSData *headerData = [headerUTF8 dataUsingEncoding:NSUTF8StringEncoding]; + [self.mutableData appendData:headerData]; + } + NSData *data = [utf8 dataUsingEncoding:NSUTF8StringEncoding]; + [self.mutableData appendData:data]; +} + +- (void)appendRecordBoundary +{ + NSString *boundary = [NSString stringWithFormat:@"\r\n--%@\r\n", kStringBoundary]; + [self appendUTF8:boundary]; +} + +- (void)appendWithKey:(NSString *)key + formValue:(NSString *)value + logger:(FBLogger *)logger +{ + NSString *disposition = + [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", key]; + [self appendUTF8:disposition]; + [self appendUTF8:value]; + [self appendRecordBoundary]; + [logger appendFormat:@"\n %@:\t%@", key, (NSString *)value]; +} + +- (void)appendWithKey:(NSString *)key + imageValue:(UIImage *)image + logger:(FBLogger *)logger +{ + NSString *disposition = + [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, key]; + [self appendUTF8:disposition]; + [self appendUTF8:@"Content-Type: image/png\r\n\r\n"]; + NSData *data = UIImagePNGRepresentation(image); + [self.mutableData appendData:data]; + [self appendRecordBoundary]; + [logger appendFormat:@"\n %@:\t", key, [data length] / 1024]; +} + +- (void)appendWithKey:(NSString *)key + dataValue:(NSData *)data + logger:(FBLogger *)logger +{ + NSString *disposition = + [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, key]; + [self appendUTF8:disposition]; + [self appendUTF8:@"Content-Type: content/unknown\r\n\r\n"]; + [self.mutableData appendData:data]; + [self appendRecordBoundary]; + [logger appendFormat:@"\n %@:\t", key, [data length] / 1024]; +} + +- (NSData *)data +{ + // No need to enforce immutability since this is internal-only and sdk will + // never cast/modify. + return self.mutableData; +} + +@end diff --git a/src/ios/facebook/FBRequestConnection+Internal.h b/src/ios/facebook/FBRequestConnection+Internal.h new file mode 100644 index 000000000..c44442d41 --- /dev/null +++ b/src/ios/facebook/FBRequestConnection+Internal.h @@ -0,0 +1,26 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBRequestConnection.h" + +@interface FBRequestConnection (Internal) + +@property (nonatomic, readonly) BOOL isResultFromCache; + +- (void)startWithCacheIdentity:(NSString*)cacheIdentity + skipRoundtripIfCached:(BOOL)consultCache; + +@end diff --git a/src/ios/facebook/FBRequestConnection.h b/src/ios/facebook/FBRequestConnection.h new file mode 100644 index 000000000..21f7a2d4a --- /dev/null +++ b/src/ios/facebook/FBRequestConnection.h @@ -0,0 +1,389 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import "FBGraphObject.h" + +// up-front decl's +@class FBRequest; +@class FBRequestConnection; +@class UIImage; + +/*! + Normally requests return JSON data that is parsed into a set of `NSDictionary` + and `NSArray` objects. + + When a request returns a non-JSON response, that response is packaged in + a `NSDictionary` using FBNonJSONResponseProperty as the key and the literal + response as the value. +*/ +extern NSString *const FBNonJSONResponseProperty; + +/*! + @typedef FBRequestHandler + + @abstract + A block that is passed to addRequest to register for a callback with the results of that + request once the connection completes. + + @discussion + Pass a block of this type when calling addRequest. This will be called once + the request completes. The call occurs on the UI thread. + + @param connection The `FBRequestConnection` that sent the request. + + @param result The result of the request. This is a translation of + JSON data to `NSDictionary` and `NSArray` objects. This + is nil if there was an error. + + @param error The `NSError` representing any error that occurred. + +*/ +typedef void (^FBRequestHandler)(FBRequestConnection *connection, + id result, + NSError *error); + +/*! + @class FBRequestConnection + + @abstract + The `FBRequestConnection` represents a single connection to Facebook to service a request. + + @discussion + The request settings are encapsulated in a reusable object. The + `FBRequestConnection` object encapsulates the concerns of a single communication + e.g. starting a connection, canceling a connection, or batching requests. + +*/ +@interface FBRequestConnection : NSObject + +/*! + @methodgroup Creating a request +*/ + +/*! + @method + + Calls with a default timeout of 180 seconds. +*/ +- (id)init; + +/*! + @method + + @abstract + `FBRequestConnection` objects are used to issue one or more requests as a single + request/response connection with Facebook. + + @discussion + For a single request, the usual method for creating an `FBRequestConnection` + object is to call one of the **start* ** methods on . However, it is + allowable to init an `FBRequestConnection` object directly, and call + to add one or more request objects to the + connection, before calling start. + + Note that if requests are part of a batch, they must have an open + FBSession that has an access token associated with it. Alternatively a default App ID + must be set either in the plist or through an explicit call to <[FBSession defaultAppID]>. + + @param timeout The `NSTimeInterval` (seconds) to wait for a response before giving up. +*/ + +- (id)initWithTimeout:(NSTimeInterval)timeout; + +// properties + +/*! + @abstract + The request that will be sent to the server. + + @discussion + This property can be used to create a `NSURLRequest` without using + `FBRequestConnection` to send that request. It is legal to set this property + in which case the provided `NSMutableURLRequest` will be used instead. However, + the `NSMutableURLRequest` must result in an appropriate response. Furthermore, once + this property has been set, no more objects can be added to this + `FBRequestConnection`. +*/ +@property(nonatomic, retain, readwrite) NSMutableURLRequest *urlRequest; + +/*! + @abstract + The raw response that was returned from the server. (readonly) + + @discussion + This property can be used to inspect HTTP headers that were returned from + the server. + + The property is nil until the request completes. If there was a response + then this property will be non-nil during the FBRequestHandler callback. +*/ +@property(nonatomic, retain, readonly) NSHTTPURLResponse *urlResponse; + +/*! + @methodgroup Adding requests +*/ + +/*! + @method + + @abstract + This method adds an object to this connection and then calls + on the connection. + + @discussion + The completion handler is retained until the block is called upon the + completion or cancellation of the connection. + + @param request A request to be included in the round-trip when start is called. + @param handler A handler to call back when the round-trip completes or times out. +*/ +- (void)addRequest:(FBRequest*)request + completionHandler:(FBRequestHandler)handler; + +/*! + @method + + @abstract + This method adds an object to this connection and then calls + on the connection. + + @discussion + The completion handler is retained until the block is called upon the + completion or cancellation of the connection. This request can be named + to allow for using the request's response in a subsequent request. + + @param request A request to be included in the round-trip when start is called. + + @param handler A handler to call back when the round-trip completes or times out. + + @param name An optional name for this request. This can be used to feed + the results of one request to the input of another in the same + `FBRequestConnection` as described in + [Graph API Batch Requests]( https://developers.facebook.com/docs/reference/api/batch/ ). +*/ +- (void)addRequest:(FBRequest*)request + completionHandler:(FBRequestHandler)handler + batchEntryName:(NSString*)name; + +/*! + @methodgroup Instance methods +*/ + +/*! + @method + + @abstract + This method starts a connection with the server and is capable of handling all of the + requests that were added to the connection. + + @discussion + Errors are reported via the handler callback, even in cases where no + communication is attempted by the implementation of `FBRequestConnection`. In + such cases multiple error conditions may apply, and if so the following + priority (highest to lowest) is used: + + - `FBRequestConnectionInvalidRequestKey` -- this error is reported when an + cannot be encoded for transmission. + + - `FBRequestConnectionInvalidBatchKey` -- this error is reported when any + request in the connection cannot be encoded for transmission with the batch. + In this scenario all requests fail. + + This method cannot be called twice for an `FBRequestConnection` instance. +*/ +- (void)start; + +/*! + @method + + @abstract + Signals that a connection should be logically terminated as the + application is no longer interested in a response. + + @discussion + Synchronously calls any handlers indicating the request was cancelled. Cancel + does not guarantee that the request-related processing will cease. It + does promise that all handlers will complete before the cancel returns. A call to + cancel prior to a start implies a cancellation of all requests associated + with the connection. +*/ +- (void)cancel; + +/*! + @method + + @abstract + Simple method to make a graph API request for user info (/me), creates an + then uses an object to start the connection with Facebook. The + request uses the active session represented by `[FBSession activeSession]`. + + See + + @param handler The handler block to call when the request completes with a success, error, or cancel action. + */ ++ (FBRequestConnection*)startForMeWithCompletionHandler:(FBRequestHandler)handler; + +/*! + @method + + @abstract + Simple method to make a graph API request for user friends (/me/friends), creates an + then uses an object to start the connection with Facebook. The + request uses the active session represented by `[FBSession activeSession]`. + + See + + @param handler The handler block to call when the request completes with a success, error, or cancel action. + */ ++ (FBRequestConnection*)startForMyFriendsWithCompletionHandler:(FBRequestHandler)handler; + +/*! + @method + + @abstract + Simple method to make a graph API post of a photo. The request + uses the active session represented by `[FBSession activeSession]`. + + @param photo A `UIImage` for the photo to upload. + @param handler The handler block to call when the request completes with a success, error, or cancel action. + */ ++ (FBRequestConnection*)startForUploadPhoto:(UIImage *)photo + completionHandler:(FBRequestHandler)handler; + +/*! + @method + + @abstract + Simple method to make a graph API post of a status update. The request + uses the active session represented by `[FBSession activeSession]`. + + @param message The message to post. + @param handler The handler block to call when the request completes with a success, error, or cancel action. + */ ++ (FBRequestConnection *)startForPostStatusUpdate:(NSString *)message + completionHandler:(FBRequestHandler)handler; + +/*! + @method + + @abstract + Simple method to make a graph API post of a status update. The request + uses the active session represented by `[FBSession activeSession]`. + + @param message The message to post. + @param place The place to checkin with, or nil. Place may be an fbid or a + graph object representing a place. + @param tags Array of friends to tag in the status update, each element + may be an fbid or a graph object representing a user. + @param handler The handler block to call when the request completes with a success, error, or cancel action. + */ ++ (FBRequestConnection *)startForPostStatusUpdate:(NSString *)message + place:(id)place + tags:(id)tags + completionHandler:(FBRequestHandler)handler; + +/*! + @method + + @abstract + Starts a request representing a Graph API call to the "search" endpoint + for a given location using the active session. + + @discussion + Simplifies starting a request to search for places near a coordinate. + + This method creates the necessary object and initializes and + starts an object. A successful Graph API call will + return an array of objects representing the nearby locations. + + @param coordinate The search coordinates. + + @param radius The search radius in meters. + + @param limit The maxiumum number of results to return. It is + possible to receive fewer than this because of the + radius and because of server limits. + + @param searchText The text to use in the query to narrow the set of places + returned. + @param handler The handler block to call when the request completes with a success, error, or cancel action. + */ ++ (FBRequestConnection*)startForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate + radiusInMeters:(NSInteger)radius + resultsLimit:(NSInteger)limit + searchText:(NSString*)searchText + completionHandler:(FBRequestHandler)handler; + +/*! + @method + + @abstract + Simple method to make a graph API request, creates an object for then + uses an object to start the connection with Facebook. The + request uses the active session represented by `[FBSession activeSession]`. + + See + + @param graphPath The Graph API endpoint to use for the request, for example "me". + @param handler The handler block to call when the request completes with a success, error, or cancel action. + */ ++ (FBRequestConnection*)startWithGraphPath:(NSString*)graphPath + completionHandler:(FBRequestHandler)handler; + +/*! + @method + + @abstract + Simple method to make post an object using the graph API, creates an object for + HTTP POST, then uses to start a connection with Facebook. The request uses + the active session represented by `[FBSession activeSession]`. + + @param graphPath The Graph API endpoint to use for the request, for example "me". + + @param graphObject An object or open graph action to post. + + @param handler The handler block to call when the request completes with a success, error, or cancel action. + */ ++ (FBRequestConnection*)startForPostWithGraphPath:(NSString*)graphPath + graphObject:(id)graphObject + completionHandler:(FBRequestHandler)handler; + +/*! + @method + + @abstract + Creates an `FBRequest` object for a Graph API call, instantiate an + object, add the request to the newly created + connection and finally start the connection. Use this method for + specifying the request parameters and HTTP Method. The request uses + the active session represented by `[FBSession activeSession]`. + + @param graphPath The Graph API endpoint to use for the request, for example "me". + + @param parameters The parameters for the request. A value of nil sends only the automatically handled parameters, for example, the access token. The default is nil. + + @param HTTPMethod The HTTP method to use for the request. A nil value implies a GET. + + @param handler The handler block to call when the request completes with a success, error, or cancel action. + */ ++ (FBRequestConnection*)startWithGraphPath:(NSString*)graphPath + parameters:(NSDictionary*)parameters + HTTPMethod:(NSString*)HTTPMethod + completionHandler:(FBRequestHandler)handler; + +@end diff --git a/src/ios/facebook/FBRequestConnection.m b/src/ios/facebook/FBRequestConnection.m new file mode 100644 index 000000000..cdb563208 --- /dev/null +++ b/src/ios/facebook/FBRequestConnection.m @@ -0,0 +1,1438 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBSBJSON.h" +#import "FBError.h" +#import "FBURLConnection.h" +#import "FBRequestBody.h" +#import "FBSession.h" +#import "FBSession+Internal.h" +#import "FBSettings.h" +#import "FBRequestConnection.h" +#import "FBRequestConnection+Internal.h" +#import "FBRequest.h" +#import "Facebook.h" +#import "FBGraphObject.h" +#import "FBLogger.h" +#import "FBUtility.h" +#import "FBDataDiskCache.h" +#import "FBSDKVersion.h" + +// URL construction constants +NSString *const kGraphURL = @"https://graph." FB_BASE_URL; +NSString *const kGraphBaseURL = @"https://graph." FB_BASE_URL @"/"; +NSString *const kRestBaseURL = @"https://api." FB_BASE_URL @"/method/"; +NSString *const kBatchKey = @"batch"; +NSString *const kBatchMethodKey = @"method"; +NSString *const kBatchRelativeURLKey = @"relative_url"; +NSString *const kBatchAttachmentKey = @"attached_files"; +NSString *const kBatchFileNamePrefix = @"file"; + +NSString *const kAccessTokenKey = @"access_token"; +NSString *const kSDK = @"ios"; +NSString *const kUserAgentBase = @"FBiOSSDK"; + +NSString *const kExtendTokenRestMethod = @"auth.extendSSOAccessToken"; +NSString *const kBatchRestMethodBaseURL = @"method/"; + +// response object property/key +NSString *const FBNonJSONResponseProperty = @"FACEBOOK_NON_JSON_RESULT"; + +static const int kRESTAPIAccessTokenErrorCode = 190; +static const int kRESTAPIPermissionErrorCode = 200; +static const int kAPISessionNoLongerActiveErrorCode = 2500; +static const NSTimeInterval kDefaultTimeout = 180.0; +static const int kMaximumBatchSize = 50; + +typedef void (^KeyValueActionHandler)(NSString *key, id value); + +// ---------------------------------------------------------------------------- +// Private class to store requests and their metadata. +// +@interface FBRequestMetadata : NSObject + +@property (nonatomic, retain) FBRequest *request; +@property (nonatomic, copy) FBRequestHandler completionHandler; +@property (nonatomic, copy) NSString *batchEntryName; + +- (id) initWithRequest:(FBRequest *)request + completionHandler:(FBRequestHandler)handler + batchEntryName:(NSString *)name; + +@end + +@implementation FBRequestMetadata + +@synthesize batchEntryName = _batchEntryName; +@synthesize completionHandler = _completionHandler; +@synthesize request = _request; + +- (id) initWithRequest:(FBRequest *)request + completionHandler:(FBRequestHandler)handler + batchEntryName:(NSString *)name { + + if (self = [super init]) { + self.request = request; + self.completionHandler = handler; + self.batchEntryName = name; + } + return self; +} + +- (void) dealloc { + [_request release]; + [_completionHandler release]; + [_batchEntryName release]; + [super dealloc]; +} + +- (NSString*)description { + return [NSString stringWithFormat:@"<%@: %p, batchEntryName: %@, completionHandler: %p, request: %@>", + NSStringFromClass([self class]), + self, + self.batchEntryName, + self.completionHandler, + self.request.description]; +} + +@end + +// ---------------------------------------------------------------------------- +// FBRequestConnectionState + +typedef enum FBRequestConnectionState { + kStateCreated, + kStateSerialized, + kStateStarted, + kStateCompleted, + kStateCancelled, +} FBRequestConnectionState; + +// ---------------------------------------------------------------------------- +// Private properties and methods + +@interface FBRequestConnection () + +@property (nonatomic, retain) FBURLConnection *connection; +@property (nonatomic, retain) NSMutableArray *requests; +@property (nonatomic) FBRequestConnectionState state; +@property (nonatomic) NSTimeInterval timeout; +@property (nonatomic, retain) NSMutableURLRequest *internalUrlRequest; +@property (nonatomic, retain, readwrite) NSHTTPURLResponse *urlResponse; +@property (nonatomic, retain) FBRequest *deprecatedRequest; +@property (nonatomic, retain) FBLogger *logger; +@property (nonatomic) unsigned long requestStartTime; +@property (nonatomic, readonly) BOOL isResultFromCache; + +- (NSMutableURLRequest *)requestWithBatch:(NSArray *)requests + timeout:(NSTimeInterval)timeout; + +- (NSString *)urlStringForSingleRequest:(FBRequest *)request forBatch:(BOOL)forBatch; + +- (void)appendJSONRequests:(NSArray *)requests + toBody:(FBRequestBody *)body + andNameAttachments:(NSMutableDictionary *)attachments + logger:(FBLogger *)logger; + +- (void)addRequest:(FBRequestMetadata *)metadata + toBatch:(NSMutableArray *)batch + attachments:(NSDictionary *)attachments; + +- (BOOL)isAttachment:(id)item; + +- (void)appendAttachments:(NSDictionary *)attachments + toBody:(FBRequestBody *)body + addFormData:(BOOL)addFormData + logger:(FBLogger *)logger; + ++ (void)processGraphObject:(id)object + forPath:(NSString*)path + withAction:(KeyValueActionHandler)action; + +- (void)completeWithResponse:(NSURLResponse *)response + data:(NSData *)data + orError:(NSError *)error; + +- (NSArray *)parseJSONResponse:(NSData *)data + error:(NSError **)error + statusCode:(int)statusCode; + +- (id)parseJSONOrOtherwise:(NSString *)utf8 + error:(NSError **)error; + +- (void)completeDeprecatedWithData:(NSData *)data + results:(NSArray *)results + orError:(NSError *)error; + +- (void)completeWithResults:(NSArray *)results + orError:(NSError *)error; + +- (NSError *)errorFromResult:(id)idResult; + +- (NSError *)errorWithCode:(FBErrorCode)code + statusCode:(int)statusCode + parsedJSONResponse:(id)response + innerError:(NSError*)innerError + message:(NSString*)message; + +- (NSError *)checkConnectionError:(NSError *)innerError + statusCode:(int)statusCode + parsedJSONResponse:(id)response; + +- (BOOL)isInvalidSessionError:(NSError *)error + resultIndex:(int)index; + +- (void)registerTokenToOmitFromLog:(NSString *)token; + +- (void)addPiggybackRequests; + +- (void)logRequest:(NSMutableURLRequest *)request + bodyLength:(int)bodyLength + bodyLogger:(FBLogger *)bodyLogger + attachmentLogger:(FBLogger *)attachmentLogger; + +- (NSString *)getBatchAppID:(NSArray*)requests; + ++ (NSString *)userAgent; + ++ (void)addRequestToExtendTokenForSession:(FBSession*)session connection:(FBRequestConnection*)connection; + +@end + +// ---------------------------------------------------------------------------- +// FBRequestConnection + +@implementation FBRequestConnection + +// ---------------------------------------------------------------------------- +// Property implementations + +@synthesize connection = _connection; +@synthesize requests = _requests; +@synthesize state = _state; +@synthesize timeout = _timeout; +@synthesize internalUrlRequest = _internalUrlRequest; +@synthesize urlResponse = _urlResponse; +@synthesize deprecatedRequest = _deprecatedRequest; +@synthesize logger = _logger; +@synthesize requestStartTime = _requestStartTime; +@synthesize isResultFromCache = _isResultFromCache; + +- (NSMutableURLRequest *)urlRequest +{ + if (self.internalUrlRequest) { + NSMutableURLRequest *request = self.internalUrlRequest; + + [request setValue:[FBRequestConnection userAgent] forHTTPHeaderField:@"User-Agent"]; + [self logRequest:request bodyLength:0 bodyLogger:nil attachmentLogger:nil]; + + return request; + + } else { + // CONSIDER: Could move to kStateSerialized here by caching result, but + // it seems bad for a get accessor to modify state in observable manner. + return [self requestWithBatch:self.requests timeout:_timeout]; + } +} + +- (void)setUrlRequest:(NSMutableURLRequest *)request +{ + NSAssert((self.state == kStateCreated) || (self.state == kStateSerialized), + @"Cannot set urlRequest after starting or cancelling."); + self.state = kStateSerialized; + + self.internalUrlRequest = request; +} + +// ---------------------------------------------------------------------------- +// Lifetime + +- (id)init +{ + return [self initWithTimeout:kDefaultTimeout]; +} + +- (id)initWithTimeout:(NSTimeInterval)timeout +{ + if (self = [super init]) { + _requests = [[NSMutableArray alloc] init]; + _timeout = timeout; + _state = kStateCreated; + _logger = [[FBLogger alloc] initWithLoggingBehavior:FBLoggingBehaviorFBRequests]; + _isResultFromCache = NO; + } + return self; +} + +- (void)dealloc +{ + [_connection cancel]; + [_connection release]; + [_requests release]; + [_internalUrlRequest release]; + [_urlResponse release]; + [_deprecatedRequest release]; + [_logger release]; + [super dealloc]; +} + +// ---------------------------------------------------------------------------- +// Public methods + +- (void)addRequest:(FBRequest *)request + completionHandler:(FBRequestHandler)handler +{ + [self addRequest:request completionHandler:handler batchEntryName:nil]; +} + +- (void)addRequest:(FBRequest *)request + completionHandler:(FBRequestHandler)handler + batchEntryName:(NSString *)name +{ + NSAssert(self.state == kStateCreated, + @"Requests must be added before starting or cancelling."); + + FBRequestMetadata *metadata = [[FBRequestMetadata alloc] initWithRequest:request + completionHandler:handler + batchEntryName:name]; + [self.requests addObject:metadata]; + [metadata release]; +} + +- (void)start +{ + [self startWithCacheIdentity:nil + skipRoundtripIfCached:NO]; +} + +- (void)cancel { + // Cancelling self.connection might trigger error handlers that cause us to + // get freed. Make sure we stick around long enough to finish this method call. + [[self retain] autorelease]; + + [self.connection cancel]; + self.connection = nil; + self.state = kStateCancelled; +} + +// ---------------------------------------------------------------------------- +// Public class methods + ++ (FBRequestConnection*)startForMeWithCompletionHandler:(FBRequestHandler)handler { + FBRequest *request = [FBRequest requestForMe]; + return [request startWithCompletionHandler:handler]; +} + ++ (FBRequestConnection*)startForMyFriendsWithCompletionHandler:(FBRequestHandler)handler { + FBRequest *request = [FBRequest requestForMyFriends]; + return [request startWithCompletionHandler:handler]; +} + ++ (FBRequestConnection*)startForUploadPhoto:(UIImage *)photo + completionHandler:(FBRequestHandler)handler { + FBRequest *request = [FBRequest requestForUploadPhoto:photo]; + return [request startWithCompletionHandler:handler]; +} + ++ (FBRequestConnection *)startForPostStatusUpdate:(NSString *)message + completionHandler:(FBRequestHandler)handler { + FBRequest *request = [FBRequest requestForPostStatusUpdate:message]; + return [request startWithCompletionHandler:handler]; +} + ++ (FBRequestConnection *)startForPostStatusUpdate:(NSString *)message + place:(id)place + tags:(id)tags + completionHandler:(FBRequestHandler)handler { + FBRequest *request = [FBRequest requestForPostStatusUpdate:message + place:place + tags:tags]; + return [request startWithCompletionHandler:handler]; +} + ++ (FBRequestConnection*)startForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate + radiusInMeters:(NSInteger)radius + resultsLimit:(NSInteger)limit + searchText:(NSString*)searchText + completionHandler:(FBRequestHandler)handler { + FBRequest *request = [FBRequest requestForPlacesSearchAtCoordinate:coordinate + radiusInMeters:radius + resultsLimit:limit + searchText:searchText]; + + return [request startWithCompletionHandler:handler]; +} + ++ (FBRequestConnection*)startWithGraphPath:(NSString*)graphPath + completionHandler:(FBRequestHandler)handler +{ + return [FBRequestConnection startWithGraphPath:graphPath + parameters:nil + HTTPMethod:nil + completionHandler:handler]; +} + ++ (FBRequestConnection*)startForPostWithGraphPath:(NSString*)graphPath + graphObject:(id)graphObject + completionHandler:(FBRequestHandler)handler +{ + FBRequest *request = [FBRequest requestForPostWithGraphPath:graphPath + graphObject:graphObject]; + + return [request startWithCompletionHandler:handler]; +} + ++ (FBRequestConnection*)startWithGraphPath:(NSString*)graphPath + parameters:(NSDictionary*)parameters + HTTPMethod:(NSString*)HTTPMethod + completionHandler:(FBRequestHandler)handler +{ + FBRequest *request = [FBRequest requestWithGraphPath:graphPath + parameters:parameters + HTTPMethod:HTTPMethod]; + + return [request startWithCompletionHandler:handler]; +} + +// ---------------------------------------------------------------------------- +// Private methods + +- (void)startWithCacheIdentity:(NSString*)cacheIdentity + skipRoundtripIfCached:(BOOL)skipRoundtripIfCached +{ + if ([self.requests count] == 1) { + FBRequestMetadata *firstMetadata = [self.requests objectAtIndex:0]; + if ([firstMetadata.request delegate]) { + self.deprecatedRequest = firstMetadata.request; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + [self.deprecatedRequest setState:kFBRequestStateLoading]; +#pragma GCC diagnostic pop + } + } + + NSMutableURLRequest *request = nil; + NSData *cachedData = nil; + NSURL *cacheIdentityURL = nil; + if (cacheIdentity) { + // warning! this property has significant side-effects, and should be executed at the right moment + // depending on whether there may be batching or whether we are certain there is no batching + request = self.urlRequest; + + // when we generalize this for consumers of FBRequest, then we will use a more + // normalized form for our identification scheme than this URL construction; given the only + // clients are the two pickers -- this scheme achieves stability via being a closed system, + // and provides a simple first step to the more general solution + cacheIdentityURL = [[[NSURL alloc] initWithScheme:@"FBRequestCache" + host:cacheIdentity + path:[NSString stringWithFormat:@"/%@", request.URL]] + autorelease]; + + if (skipRoundtripIfCached) { + cachedData = [[FBDataDiskCache sharedCache] dataForURL:cacheIdentityURL]; + } + } + + if (self.internalUrlRequest == nil && !cacheIdentity) { + // If we have all Graph API calls, see if we want to piggyback any internal calls onto + // the request to reduce round-trips. (The piggybacked calls may themselves be non-Graph + // API calls, but must be limited to API calls which are batchable. Not all are, which is + // why we won't piggyback on top of a REST API call.) Don't try this if the caller gave us + // an already-formed request object, since we don't know its structure. + BOOL safeForPiggyback = YES; + for (FBRequestMetadata *requestMetadata in self.requests) { + if (requestMetadata.request.restMethod) { + safeForPiggyback = NO; + break; + } + } + // If we wouldn't be able to compute a batch_app_id, don't piggyback on this + // request. + NSString *batchAppID = [self getBatchAppID:self.requests]; + safeForPiggyback &= (batchAppID != nil) && (batchAppID.length > 0); + + if (safeForPiggyback) { + [self addPiggybackRequests]; + } + } + + // warning! this property is side-effecting (and should probably be refactored at some point...) + // still, if we have made it this far and still don't have a request object, we need one now + if (!request) { + request = self.urlRequest; + } + + NSAssert((self.state == kStateCreated) || (self.state == kStateSerialized), + @"Cannot call start again after calling start or cancel."); + self.state = kStateStarted; + + _requestStartTime = [FBUtility currentTimeInMilliseconds]; + + if (!cachedData) { + FBURLConnectionHandler handler = + ^(FBURLConnection *connection, + NSError *error, + NSURLResponse *response, + NSData *responseData) { + // cache this data if we have successful response and a cache identity to work with + if (cacheIdentityURL && + [response isKindOfClass:[NSHTTPURLResponse class]] && + ((NSHTTPURLResponse*)response).statusCode == 200) { + [[FBDataDiskCache sharedCache] setData:responseData + forURL:cacheIdentityURL]; + } + // complete on result from round-trip to server + [self completeWithResponse:response + data:responseData + orError:error]; + }; + + id deprecatedDelegate = [self.deprecatedRequest delegate]; + if ([deprecatedDelegate respondsToSelector:@selector(requestLoading:)]) { + [deprecatedDelegate requestLoading:self.deprecatedRequest]; + } + + FBURLConnection *connection = [[FBURLConnection alloc] initWithRequest:request + skipRoundTripIfCached:NO + completionHandler:handler]; + self.connection = connection; + [connection release]; + } else { + _isResultFromCache = YES; + + // complete on result from cache + [self completeWithResponse:nil + data:cachedData + orError:nil]; + + } +} + +// +// Generates a NSURLRequest based on the contents of self.requests, and sets +// options on the request. Chooses between URL-based request for a single +// request and JSON-based request for batches. +// +- (NSMutableURLRequest *)requestWithBatch:(NSArray *)requests + timeout:(NSTimeInterval)timeout +{ + FBRequestBody *body = [[FBRequestBody alloc] init]; + FBLogger *bodyLogger = [[FBLogger alloc] initWithLoggingBehavior:_logger.loggingBehavior]; + FBLogger *attachmentLogger = [[FBLogger alloc] initWithLoggingBehavior:_logger.loggingBehavior]; + + NSMutableURLRequest *request; + + if (requests.count == 0) { + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBRequestConnection: Must have at least one request or urlRequest not specified." + userInfo:nil] + raise]; + + } + + if ([requests count] == 1) { + FBRequestMetadata *metadata = [requests objectAtIndex:0]; + NSURL *url = [NSURL URLWithString:[self urlStringForSingleRequest:metadata.request forBatch:NO]]; + request = [NSMutableURLRequest requestWithURL:url + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData + timeoutInterval:timeout]; + + // HTTP methods are case-sensitive; be helpful in case someone provided a mixed case one. + NSString *httpMethod = [metadata.request.HTTPMethod uppercaseString]; + [request setHTTPMethod:httpMethod]; + [self appendAttachments:metadata.request.parameters + toBody:body + addFormData:[httpMethod isEqualToString:@"POST"] + logger:attachmentLogger]; + + // if we have a post object, also roll that into the body + if (metadata.request.graphObject) { + [FBRequestConnection processGraphObject:metadata.request.graphObject + forPath:[url path] + withAction:^(NSString *key, id value) { + [body appendWithKey:key formValue:value logger:bodyLogger]; + }]; + } + } else { + // Find the session with an app ID and use that as the batch_app_id. If we can't + // find one, try to load it from the plist. As a last resort, pass 0. + NSString *batchAppID = [self getBatchAppID:requests]; + if (!batchAppID || batchAppID.length == 0) { + // The Graph API batch method requires either an access token or batch_app_id. + // If we can't determine an App ID to use for the batch, we can't issue it. + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBRequestConnection: At least one request in a" + " batch must have an open FBSession, or a default" + " app ID must be specified." + userInfo:nil] + raise]; + } + + [body appendWithKey:@"batch_app_id" formValue:batchAppID logger:bodyLogger]; + + NSMutableDictionary *attachments = [[NSMutableDictionary alloc] init]; + + [self appendJSONRequests:requests + toBody:body + andNameAttachments:attachments + logger:bodyLogger]; + + [self appendAttachments:attachments + toBody:body + addFormData:NO + logger:attachmentLogger]; + + [attachments release]; + + request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kGraphURL] + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData + timeoutInterval:timeout]; + [request setHTTPMethod:@"POST"]; + } + + [request setHTTPBody:[body data]]; + NSUInteger bodyLength = [[body data] length] / 1024; + [body release]; + + [request setValue:[FBRequestConnection userAgent] forHTTPHeaderField:@"User-Agent"]; + [request setValue:[FBRequestBody mimeContentType] forHTTPHeaderField:@"Content-Type"]; + + [self logRequest:request bodyLength:bodyLength bodyLogger:bodyLogger attachmentLogger:attachmentLogger]; + + // Safely release now that everything's serialized into the logger. + [bodyLogger release]; + [attachmentLogger release]; + + return request; +} + +- (void)logRequest:(NSMutableURLRequest *)request + bodyLength:(int)bodyLength + bodyLogger:(FBLogger *)bodyLogger + attachmentLogger:(FBLogger *)attachmentLogger +{ + if (_logger.isActive) { + [_logger appendFormat:@"Request <#%d>:\n", _logger.loggerSerialNumber]; + [_logger appendKey:@"URL" value:[[request URL] absoluteString]]; + [_logger appendKey:@"Method" value:[request HTTPMethod]]; + [_logger appendKey:@"UserAgent" value:[request valueForHTTPHeaderField:@"User-Agent"]]; + [_logger appendKey:@"MIME" value:[request valueForHTTPHeaderField:@"Content-Type"]]; + + if (bodyLength != 0) { + [_logger appendKey:@"Body Size" value:[NSString stringWithFormat:@"%d kB", bodyLength / 1024]]; + } + + if (bodyLogger != nil) { + [_logger appendKey:@"Body (w/o attachments)" value:bodyLogger.contents]; + } + + if (attachmentLogger != nil) { + [_logger appendKey:@"Attachments" value:attachmentLogger.contents]; + } + + [_logger appendString:@"\n"]; + + [_logger emitToNSLog]; + } +} + +// +// Generates a URL for a batch containing only a single request, +// and names all attachments that need to go in the body of the +// request. +// +// The URL contains all parameters that are not body attachments, +// including the session key if present. +// +// Attachments are named and referenced by name in the URL. +// +- (NSString *)urlStringForSingleRequest:(FBRequest *)request forBatch:(BOOL)forBatch +{ + [request.parameters setValue:@"json" forKey:@"format"]; + [request.parameters setValue:kSDK forKey:@"sdk"]; + NSString *token = request.session.accessToken; + if (token) { + [request.parameters setValue:token forKey:kAccessTokenKey]; + [self registerTokenToOmitFromLog:token]; + } + + NSString *baseURL; + if (request.restMethod) { + if (forBatch) { + baseURL = [kBatchRestMethodBaseURL stringByAppendingString:request.restMethod]; + } else { + baseURL = [kRestBaseURL stringByAppendingString:request.restMethod]; + } + } else { + if (forBatch) { + baseURL = request.graphPath; + } else { + baseURL = [kGraphBaseURL stringByAppendingString:request.graphPath]; + } + } + + NSString *url = [FBRequest serializeURL:baseURL + params:request.parameters + httpMethod:request.HTTPMethod]; + return url; +} + +// Find the first session with an app ID and use that as the batch_app_id. If we can't +// find one, return the default app ID (which may still be nil if not specified +// programmatically or via the plist). +- (NSString *)getBatchAppID:(NSArray*)requests +{ + for (FBRequestMetadata *metadata in requests) { + if (metadata.request.session.appID.length > 0) { + return metadata.request.session.appID; + } + } + return [FBSession defaultAppID]; +} + +// +// Serializes all requests in the batch to JSON and appends the result to +// body. Also names all attachments that need to go as separate blocks in +// the body of the request. +// +// All the requests are serialized into JSON, with any binary attachments +// named and referenced by name in the JSON. +// +- (void)appendJSONRequests:(NSArray *)requests + toBody:(FBRequestBody *)body + andNameAttachments:(NSMutableDictionary *)attachments + logger:(FBLogger *)logger +{ + NSMutableArray *batch = [[NSMutableArray alloc] init]; + for (FBRequestMetadata *metadata in requests) { + [self addRequest:metadata + toBatch:batch + attachments:attachments]; + } + + FBSBJSON *writer = [[FBSBJSON alloc] init]; + NSString *jsonBatch = [writer stringWithObject:batch]; + [writer release]; + [batch release]; + + [body appendWithKey:kBatchKey formValue:jsonBatch logger:logger]; +} + +// +// Adds request data to a batch in a format expected by the JsonWriter. +// Binary attachments are referenced by name in JSON and added to the +// attachments dictionary. +// +- (void)addRequest:(FBRequestMetadata *)metadata + toBatch:(NSMutableArray *)batch + attachments:(NSDictionary *)attachments +{ + NSMutableDictionary *requestElement = [[[NSMutableDictionary alloc] init] autorelease]; + + if (metadata.batchEntryName) { + [requestElement setObject:metadata.batchEntryName forKey:@"name"]; + } + + NSString *token = metadata.request.session.accessToken; + if (token) { + [metadata.request.parameters setObject:token forKey:kAccessTokenKey]; + [self registerTokenToOmitFromLog:token]; + } + + NSString *urlString = [self urlStringForSingleRequest:metadata.request forBatch:YES]; + [requestElement setObject:urlString forKey:kBatchRelativeURLKey]; + [requestElement setObject:metadata.request.HTTPMethod forKey:kBatchMethodKey]; + + NSMutableString *attachmentNames = [NSMutableString string]; + + for (id key in [metadata.request.parameters keyEnumerator]) { + NSObject *value = [metadata.request.parameters objectForKey:key]; + if ([self isAttachment:value]) { + NSString *name = [NSString stringWithFormat:@"%@%d", + kBatchFileNamePrefix, + [attachments count]]; + if ([attachmentNames length]) { + [attachmentNames appendString:@","]; + } + [attachmentNames appendString:name]; + [attachments setValue:value forKey:name]; + } + } + + // if we have a post object, also roll that into the body + if (metadata.request.graphObject) { + NSMutableString *bodyValue = [[[NSMutableString alloc] init] autorelease]; + __block NSString *delimiter = @""; + [FBRequestConnection + processGraphObject:metadata.request.graphObject + forPath:urlString + withAction:^(NSString *key, id value) { + // escape the value + value = [FBUtility stringByURLEncodingString:[value description]]; + [bodyValue appendFormat:@"%@%@=%@", + delimiter, + key, + value]; + delimiter = @"&"; + }]; + [requestElement setObject:bodyValue forKey:@"body"]; + } + + if ([attachmentNames length]) { + [requestElement setObject:attachmentNames forKey:kBatchAttachmentKey]; + } + + [batch addObject:requestElement]; +} + +- (BOOL)isAttachment:(id)item +{ + return + [item isKindOfClass:[UIImage class]] || + [item isKindOfClass:[NSData class]]; +} + +- (void)appendAttachments:(NSDictionary *)attachments + toBody:(FBRequestBody *)body + addFormData:(BOOL)addFormData + logger:(FBLogger *)logger +{ + // key is name for both, first case is string which we can print, second pass grabs object + if (addFormData) { + for (NSString *key in [attachments keyEnumerator]) { + NSObject *value = [attachments objectForKey:key]; + if ([value isKindOfClass:[NSString class]]) { + [body appendWithKey:key formValue:(NSString *)value logger:logger]; + } + } + } + + for (NSString *key in [attachments keyEnumerator]) { + NSObject *value = [attachments objectForKey:key]; + if ([value isKindOfClass:[UIImage class]]) { + [body appendWithKey:key imageValue:(UIImage *)value logger:logger]; + } else if ([value isKindOfClass:[NSData class]]) { + [body appendWithKey:key dataValue:(NSData *)value logger:logger]; + } + } +} + +#pragma mark Graph Object serialization + ++ (void)processGraphObjectPropertyKey:(NSString*)key + value:(id)value + action:(KeyValueActionHandler)action + passByValue:(BOOL)passByValue { + if ([value conformsToProtocol:@protocol(FBGraphObject)]) { + NSDictionary *refObject = (NSDictionary*)value; + + if (passByValue) { + // We need to pass all properties of this object in key[propertyName] format. + for (NSString *propertyName in refObject) { + NSString *subKey = [NSString stringWithFormat:@"%@[%@]", key, propertyName]; + id subValue = [refObject objectForKey:propertyName]; + // Note that passByValue is not inherited by subkeys. + [self processGraphObjectPropertyKey:subKey value:subValue action:action passByValue:NO]; + } + } else { + // Normal case is passing objects by reference, so just pass the ID or URL, if any. + NSString *subValue; + if ((subValue = [refObject objectForKey:@"id"])) { // fbid + if ([subValue isKindOfClass:[NSDecimalNumber class]]) { + subValue = [(NSDecimalNumber*)subValue stringValue]; + } + action(key, subValue); + } else if ((subValue = [refObject objectForKey:@"url"])) { // canonical url (external) + action(key, subValue); + } + } + } else if ([value isKindOfClass:[NSString class]] || + [value isKindOfClass:[NSNumber class]]) { + // Just serialize these. + action(key, value); + } else if ([value isKindOfClass:[NSArray class]]) { + // Arrays are serialized as multiple elements with keys of the + // form key[0], key[1], etc. + NSArray *array = (NSArray*)value; + int count = array.count; + for (int i = 0; i < count; ++i) { + NSString *subKey = [NSString stringWithFormat:@"%@[%d]", key, i]; + id subValue = [array objectAtIndex:i]; + [self processGraphObjectPropertyKey:subKey value:subValue action:action passByValue:passByValue]; + } + } +} + ++ (void)processGraphObject:(id)object forPath:(NSString*)path withAction:(KeyValueActionHandler)action { + BOOL isOGAction = NO; + if ([path hasPrefix:@"me/"] || + [path hasPrefix:@"/me/"]) { + // In general, graph objects are passed by reference (ID/URL). But if this is an OG Action, + // we need to pass the entire values of the contents of the 'image' property, as they + // contain important metadata beyond just a URL. We don't have a 100% foolproof way of knowing + // if we are posting an OG Action, given that batched requests can have parameter substitution, + // but passing the OG Action type as a substituted parameter is unlikely. + // It looks like an OG Action if it's posted to me/namespace:action[?other=stuff]. + NSUInteger colonLocation = [path rangeOfString:@":"].location; + NSUInteger questionMarkLocation = [path rangeOfString:@"?"].location; + isOGAction = (colonLocation != NSNotFound && colonLocation > 3) && + (questionMarkLocation == NSNotFound || colonLocation < questionMarkLocation); + } + + for (NSString *key in [object keyEnumerator]) { + NSObject *value = [object objectForKey:key]; + BOOL passByValue = isOGAction && [key isEqualToString:@"image"]; + [self processGraphObjectPropertyKey:key value:value action:action passByValue:passByValue]; + } +} + +#pragma mark - + +- (void)completeWithResponse:(NSURLResponse *)response + data:(NSData *)data + orError:(NSError *)error +{ + NSAssert(self.state == kStateStarted, + @"Unexpected state %d in completeWithResponse", + self.state); + self.state = kStateCompleted; + + int statusCode; + if (response) { + NSAssert([response isKindOfClass:[NSHTTPURLResponse class]], + @"Expected NSHTTPURLResponse, got %@", + response); + self.urlResponse = (NSHTTPURLResponse *)response; + statusCode = self.urlResponse.statusCode; + + if (!error && [response.MIMEType hasPrefix:@"image"]) { + error = [self errorWithCode:FBErrorNonTextMimeTypeReturned + statusCode:0 + parsedJSONResponse:nil + innerError:nil + message:@"Response is a non-text MIME type; endpoints that return images and other " + @"binary data should be fetched using NSURLRequest and NSURLConnection"]; + } + } else { + // the cached case is always successful, from an http perspective + statusCode = 200; + } + + + + NSArray *results = nil; + if (!error) { + results = [self parseJSONResponse:data + error:&error + statusCode:statusCode]; + } + + // the cached case has data but no response, + // in which case we skip connection-related errors + if (response || !data) { + error = [self checkConnectionError:error + statusCode:statusCode + parsedJSONResponse:results]; + } + + if (!error) { + if ([self.requests count] != [results count]) { + NSLog(@"Expected %d results, got %d", [self.requests count], [results count]); + error = [self errorWithCode:FBErrorProtocolMismatch + statusCode:statusCode + parsedJSONResponse:results + innerError:nil + message:nil]; + } + } + + if (!error) { + + [_logger appendFormat:@"Response <#%d>\nDuration: %lu msec\nSize: %d kB\nResponse Body:\n%@\n\n", + [_logger loggerSerialNumber], + [FBUtility currentTimeInMilliseconds] - _requestStartTime, + [data length], + results]; + + } else { + + [_logger appendFormat:@"Response <#%d> :\n%@\n\n", + [_logger loggerSerialNumber], + [error localizedDescription]]; + + } + [_logger emitToNSLog]; + + if (self.deprecatedRequest) { + [self completeDeprecatedWithData:data results:results orError:error]; + } else { + [self completeWithResults:results orError:error]; + } + + self.connection = nil; + self.urlResponse = (NSHTTPURLResponse *)response; +} + +// +// If there is one request, the JSON is the response. +// If there are multiple requests, the JSON has an array of dictionaries whose +// body property is the response. +// [{ "code":200, +// "body":"JSON-response-as-a-string" }, +// { "code":200, +// "body":"JSON-response-as-a-string" }] +// +// In both cases, this function returns an NSArray containing the results. +// The NSArray looks just like the multiple request case except the body +// value is converted from a string to parsed JSON. +// +- (NSArray *)parseJSONResponse:(NSData *)data + error:(NSError **)error + statusCode:(int)statusCode; +{ + // Graph API can return "true" or "false", which is not valid JSON. + // Translate that before asking JSON parser to look at it. + NSString *responseUTF8 = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSArray *results = nil; + id response = [self parseJSONOrOtherwise:responseUTF8 error:error]; + + if (*error) { + // no-op + } else if ([self.requests count] == 1) { + // response is the entry, so put it in a dictionary under "body" and add + // that to array of responses. + NSMutableDictionary *result = [[[NSMutableDictionary alloc] init] autorelease]; + [result setObject:[NSNumber numberWithInt:statusCode] forKey:@"code"]; + [result setObject:response forKey:@"body"]; + + NSMutableArray *mutableResults = [[[NSMutableArray alloc] init] autorelease]; + [mutableResults addObject:result]; + results = mutableResults; + } else if ([response isKindOfClass:[NSArray class]]) { + // response is the array of responses, but the body element of each needs + // to be decoded from JSON. + NSMutableArray *mutableResults = [[[NSMutableArray alloc] init] autorelease]; + for (id item in response) { + // Don't let errors parsing one response stop us from parsing another. + NSError *batchResultError = nil; + if (![item isKindOfClass:[NSDictionary class]]) { + [mutableResults addObject:item]; + } else { + NSDictionary *itemDictionary = (NSDictionary *)item; + NSMutableDictionary *result = [[[NSMutableDictionary alloc] init] autorelease]; + for (NSString *key in [itemDictionary keyEnumerator]) { + id value = [itemDictionary objectForKey:key]; + if ([key isEqualToString:@"body"]) { + id body = [self parseJSONOrOtherwise:value error:&batchResultError]; + [result setObject:body forKey:key]; + } else { + [result setObject:value forKey:key]; + } + } + [mutableResults addObject:result]; + } + if (batchResultError) { + // We'll report back the last error we saw. + *error = batchResultError; + } + } + results = mutableResults; + } else { + *error = [self errorWithCode:FBErrorProtocolMismatch + statusCode:statusCode + parsedJSONResponse:results + innerError:nil + message:nil]; + } + + [responseUTF8 release]; + return results; +} + +- (id)parseJSONOrOtherwise:(NSString *)utf8 + error:(NSError **)error +{ + id parsed = nil; + if (!(*error)) { + FBSBJSON *parser = [[FBSBJSON alloc] init]; + parsed = [parser objectWithString:utf8 error:error]; + // if we fail parse we attemp a reparse of a modified input to support results in the form "foo=bar", "true", etc. + if (*error) { + // we round-trip our hand-wired response through the parser in order to remain + // consistent with the rest of the output of this function (note, if perf turns out + // to be a problem -- unlikely -- we can return the following dictionary outright) + NSDictionary *original = [NSDictionary dictionaryWithObjectsAndKeys: + utf8, FBNonJSONResponseProperty, + nil]; + NSString *jsonrep = [parser stringWithObject:original]; + NSError *reparseError = nil; + parsed = [parser objectWithString:jsonrep error:&reparseError]; + if (!reparseError) { + *error = nil; + } + } + [parser release]; + } + return parsed; +} + +- (void)completeDeprecatedWithData:(NSData *)data + results:(NSArray *)results + orError:(NSError *)error +{ + id result = [results objectAtIndex:0]; + if ([result isKindOfClass:[NSDictionary class]]) { + NSDictionary *resultDictionary = (NSDictionary *)result; + result = [resultDictionary objectForKey:@"body"]; + } + + id delegate = [self.deprecatedRequest delegate]; + + if (!error) { + if ([delegate respondsToSelector:@selector(request:didReceiveResponse:)]) { + [delegate request:self.deprecatedRequest + didReceiveResponse:self.urlResponse]; + } + if ([delegate respondsToSelector:@selector(request:didLoadRawResponse:)]) { + [delegate request:self.deprecatedRequest didLoadRawResponse:data]; + } + + error = [self errorFromResult:result]; + } + + if (!error) { + if ([delegate respondsToSelector:@selector(request:didLoad:)]) { + [delegate request:self.deprecatedRequest didLoad:result]; + } + } else { + if ([self isInvalidSessionError:error resultIndex:0]) { + [self.deprecatedRequest setSessionDidExpire:YES]; + [self.deprecatedRequest.session close]; + } + + [self.deprecatedRequest setError:error]; + if ([delegate respondsToSelector:@selector(request:didFailWithError:)]) { + [delegate request:self.deprecatedRequest didFailWithError:error]; + } + } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + [self.deprecatedRequest setState:kFBRequestStateComplete]; +#pragma GCC diagnostic pop +} + +- (void)completeWithResults:(NSArray *)results + orError:(NSError *)error +{ + int count = [self.requests count]; + for (int i = 0; i < count; i++) { + FBRequestMetadata *metadata = [self.requests objectAtIndex:i]; + id result = error ? nil : [results objectAtIndex:i]; + NSError *itemError = error ? error : [self errorFromResult:result]; + + id body = nil; + if (!itemError && [result isKindOfClass:[NSDictionary class]]) { + NSDictionary *resultDictionary = (NSDictionary *)result; + body = [FBGraphObject graphObjectWrappingDictionary:[resultDictionary objectForKey:@"body"]]; + } + + // if we lack permissions, use this as a cue to refresh the + // OS's understanding of current permissions + if ((metadata.request.session.loginType == FBSessionLoginTypeSystemAccount) && + [self isInsufficientPermissionError:error + resultIndex:error == itemError ? i : 0]) { + [FBSession renewSystemAuthorization]; + } + + if ([self isInvalidSessionError:itemError + resultIndex:error == itemError ? i : 0]) { + [metadata.request.session closeAndClearTokenInformation:itemError]; + if (metadata.request.session.loginType == FBSessionLoginTypeSystemAccount){ + [FBSession renewSystemAuthorization]; + } + } else if ([metadata.request.session shouldExtendAccessToken]) { + // If we have not had the opportunity to piggyback a token-extension request, + // but we need to, do so now as a separate request. + FBRequestConnection *connection = [[FBRequestConnection alloc] init]; + [FBRequestConnection addRequestToExtendTokenForSession:metadata.request.session + connection:connection]; + [connection start]; + [connection release]; + } + + if (metadata.completionHandler) { + // task #1256476: in the current implementation, FBErrorParsedJSONResponseKey has two + // semantics; both of which are used by the implementation; the right fix is to break the meaning into + // two throughout, and surface both in the public API; the following fix is a lower risk and also + // less correct solution that improves the public API surface for this release + // Unpack FBErrorParsedJSONResponseKey array if present + id parsedResponse; + if ((parsedResponse = itemError.userInfo) && // do we have an error with userInfo + (parsedResponse = [parsedResponse objectForKey:FBErrorParsedJSONResponseKey]) && // response present? + ([parsedResponse isKindOfClass:[NSArray class]])) { // array? + id newValue = nil; + // if we successfully spelunk this far, then we don't want to return FBErrorParsedJSONResponseKey as is + // but if there is an empty array here, then we are better off nil-ing the key + if ([parsedResponse count]) { + newValue = [parsedResponse objectAtIndex:0]; + } + itemError = [self errorWithCode:itemError.code + statusCode:[[itemError.userInfo objectForKey:FBErrorHTTPStatusCodeKey] intValue] + parsedJSONResponse:newValue + innerError:[itemError.userInfo objectForKey:FBErrorInnerErrorKey] + message:[itemError.userInfo objectForKey:NSLocalizedDescriptionKey]]; + } + + metadata.completionHandler(self, body, itemError); + } + } +} + +- (NSError *)errorFromResult:(id)idResult +{ + if ([idResult isKindOfClass:[NSDictionary class]]) { + NSDictionary *dictionary = (NSDictionary *)idResult; + + if ([dictionary objectForKey:@"error"] || + [dictionary objectForKey:@"error_code"] || + [dictionary objectForKey:@"error_msg"] || + [dictionary objectForKey:@"error_reason"]) { + + NSMutableDictionary *userInfo = [[[NSMutableDictionary alloc] init] autorelease]; + [userInfo addEntriesFromDictionary:dictionary]; + return [self errorWithCode:FBErrorRequestConnectionApi + statusCode:200 + parsedJSONResponse:idResult + innerError:nil + message:nil]; + } + + NSNumber *code = [dictionary valueForKey:@"code"]; + if (code) { + return [self checkConnectionError:nil + statusCode:[code intValue] + parsedJSONResponse:idResult]; + } + } + + return nil; +} + +- (NSError *)errorWithCode:(FBErrorCode)code + statusCode:(int)statusCode + parsedJSONResponse:(id)response + innerError:(NSError*)innerError + message:(NSString*)message { + NSMutableDictionary *userInfo = [[[NSMutableDictionary alloc] init] autorelease]; + [userInfo setObject:[NSNumber numberWithInt:statusCode] forKey:FBErrorHTTPStatusCodeKey]; + + if (response) { + [userInfo setObject:response forKey:FBErrorParsedJSONResponseKey]; + } + + if (innerError) { + [userInfo setObject:innerError forKey:FBErrorInnerErrorKey]; + } + + if (message) { + [userInfo setObject:message + forKey:NSLocalizedDescriptionKey]; + } + + NSError *error = [[[NSError alloc] + initWithDomain:FacebookSDKDomain + code:code + userInfo:userInfo] + autorelease]; + + return error; +} + +- (NSError *)checkConnectionError:(NSError *)innerError + statusCode:(int)statusCode + parsedJSONResponse:response +{ + // We don't want to re-wrap our own errors. + if (innerError && + [innerError.domain isEqualToString:FacebookSDKDomain]) { + return innerError; + } + NSError *result = nil; + if (innerError || ((statusCode < 200) || (statusCode >= 300))) { + NSLog(@"Error: HTTP status code: %d", statusCode); + result = [self errorWithCode:FBErrorHTTPError + statusCode:statusCode + parsedJSONResponse:response + innerError:innerError + message:nil]; + } + return result; +} + +- (BOOL)getCodeValueForError:(NSError *)error + resultIndex:(int)index + value:(int *)pvalue { + + // does this error have a response? that is an array? + id response = [error.userInfo objectForKey:FBErrorParsedJSONResponseKey]; + if (response && [response isKindOfClass:[NSArray class]]) { + + // spelunking a JSON array & nested objects (eg. response[index].body.error.code) + id item, body, error, code; + if ((item = [response objectAtIndex:index]) && // response[index] + [item isKindOfClass:[NSDictionary class]] && + (body = [item objectForKey:@"body"]) && // response[index].body + [body isKindOfClass:[NSDictionary class]] && + (error = [body objectForKey:@"error"]) && // response[index].body.error + [error isKindOfClass:[NSDictionary class]] && + (code = [error objectForKey:@"code"]) && // response[index].body.error.code + [code isKindOfClass:[NSNumber class]]) { + // is it a 190 packaged in the original response, then YES + if (pvalue) { + *pvalue = [code intValue]; + } + return YES; + } + } + // else NO + return NO; +} + +- (BOOL)isInsufficientPermissionError:(NSError *)error + resultIndex:(int)index { + + int value; + if ([self getCodeValueForError:error + resultIndex:index + value:&value]) { + return value == kRESTAPIPermissionErrorCode; + } + return NO; +} + +- (BOOL)isInvalidSessionError:(NSError *)error + resultIndex:(int)index { + + int value; + if ([self getCodeValueForError:error + resultIndex:index + value:&value]) { + return value == kRESTAPIAccessTokenErrorCode || value == kAPISessionNoLongerActiveErrorCode; + } + return NO; +} + +- (void)registerTokenToOmitFromLog:(NSString *)token +{ + if (![[FBSettings loggingBehavior] containsObject:FBLoggingBehaviorAccessTokens]) { + [FBLogger registerStringToReplace:token replaceWith:@"ACCESS_TOKEN_REMOVED"]; + } +} + ++ (NSString *)userAgent +{ + static NSString *agent = nil; + + if (!agent) { + agent = [[NSString stringWithFormat:@"%@.%@", kUserAgentBase, FB_IOS_SDK_VERSION_STRING] retain]; + } + + return agent; +} + +- (void)addPiggybackRequests +{ + // Get the set of sessions used by our requests + NSMutableSet *sessions = [[NSMutableSet alloc] init]; + for (FBRequestMetadata *requestMetadata in self.requests) { + // Have we seen this session yet? If not, assume we'll extend its token if it wants us to. + if (requestMetadata.request.session) { + [sessions addObject:requestMetadata.request.session]; + } + } + + for (FBSession *session in sessions) { + if (self.requests.count >= kMaximumBatchSize) { + break; + } + if ([session shouldExtendAccessToken]) { + [FBRequestConnection addRequestToExtendTokenForSession:session connection:self]; + } + } + + [sessions release]; +} + ++ (void)addRequestToExtendTokenForSession:(FBSession*)session connection:(FBRequestConnection*)connection +{ + FBRequest *request = [[FBRequest alloc] initWithSession:session + restMethod:kExtendTokenRestMethod + parameters:nil + HTTPMethod:nil]; + [connection addRequest:request + completionHandler:^(FBRequestConnection *connection, id result, NSError *error) { + // extract what we care about + id token = [result objectForKey:@"access_token"]; + id expireTime = [result objectForKey:@"expires_at"]; + + // if we have a token and it is not a string (?) punt + if (token && ![token isKindOfClass:[NSString class]]) { + expireTime = nil; + } + + // get a date if possible + NSDate *expirationDate = nil; + if (expireTime) { + NSTimeInterval timeInterval = [expireTime doubleValue]; + if (timeInterval != 0) { + expirationDate = [NSDate dateWithTimeIntervalSince1970:timeInterval]; + } + } + + // if we ended up with at least a date (and maybe a token) refresh the session token + if (expirationDate) { + [session refreshAccessToken:token + expirationDate:expirationDate]; + } + }]; + [request release]; +} + +#pragma mark Debugging helpers + +- (NSString*)description { + NSMutableString *result = [NSMutableString stringWithFormat:@"<%@: %p, %d request(s): (\n", + NSStringFromClass([self class]), + self, + self.requests.count]; + BOOL comma = NO; + for (FBRequestMetadata *metadata in self.requests) { + FBRequest *request = metadata.request; + if (comma) { + [result appendString:@",\n"]; + } + [result appendString:[request description]]; + comma = YES; + } + [result appendString:@"\n)>"]; + return result; + +} + +#pragma mark - + +@end diff --git a/src/ios/facebook/FBSDKVersion.h b/src/ios/facebook/FBSDKVersion.h new file mode 100644 index 000000000..c46faaf77 --- /dev/null +++ b/src/ios/facebook/FBSDKVersion.h @@ -0,0 +1,2 @@ +#define FB_IOS_SDK_VERSION_STRING @"3.1.1" +#define FB_IOS_SDK_MIGRATION_BUNDLE @"fbsdk:20121003" diff --git a/src/ios/facebook/FBSession+Internal.h b/src/ios/facebook/FBSession+Internal.h new file mode 100644 index 000000000..187fc1f5a --- /dev/null +++ b/src/ios/facebook/FBSession+Internal.h @@ -0,0 +1,31 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBSession.h" + +@interface FBSession (Internal) + +- (void)refreshAccessToken:(NSString*)token expirationDate:(NSDate*)expireDate; +- (BOOL)shouldExtendAccessToken; +- (void)closeAndClearTokenInformation:(NSError*) error; + ++ (FBSession*)activeSessionIfOpen; + ++ (void)deleteFacebookCookies; ++ (NSDate*)expirationDateFromExpirationTimeString:(NSString*)expirationTime; ++ (void)renewSystemAuthorization; + +@end diff --git a/src/ios/facebook/FBSession+Protected.h b/src/ios/facebook/FBSession+Protected.h new file mode 100644 index 000000000..eabf38c78 --- /dev/null +++ b/src/ios/facebook/FBSession+Protected.h @@ -0,0 +1,43 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBSession.h" + +// Methods here are meant to be used only by internal subclasses of FBSession +// and not by any other classes, external or internal. +@interface FBSession (Protected) + +- (BOOL)transitionToState:(FBSessionState)state + andUpdateToken:(NSString*)token + andExpirationDate:(NSDate*)date + shouldCache:(BOOL)shouldCache + loginType:(FBSessionLoginType)loginType; +- (void)transitionAndCallHandlerWithState:(FBSessionState)status + error:(NSError*)error + token:(NSString*)token + expirationDate:(NSDate*)date + shouldCache:(BOOL)shouldCache + loginType:(FBSessionLoginType)loginType; +- (void)authorizeWithPermissions:(NSArray*)permissions + behavior:(FBSessionLoginBehavior)behavior + defaultAudience:(FBSessionDefaultAudience)audience + isReauthorize:(BOOL)isReauthorize; + ++ (NSError*)errorLoginFailedWithReason:(NSString*)errorReason + errorCode:(NSString*)errorCode + innerError:(NSError*)innerError; + +@end diff --git a/src/ios/facebook/FBSession.h b/src/ios/facebook/FBSession.h new file mode 100644 index 000000000..209fa2893 --- /dev/null +++ b/src/ios/facebook/FBSession.h @@ -0,0 +1,618 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +// up-front decl's +@class FBSession; +@class FBSessionTokenCachingStrategy; + +#define FB_SESSIONSTATETERMINALBIT (1 << 8) + +#define FB_SESSIONSTATEOPENBIT (1 << 9) + +/* + * Constants used by NSNotificationCenter for active session notification + */ + +/*! NSNotificationCenter name indicating that a new active session was set */ +extern NSString *const FBSessionDidSetActiveSessionNotification; + +/*! NSNotificationCenter name indicating that an active session was unset */ +extern NSString *const FBSessionDidUnsetActiveSessionNotification; + +/*! NSNotificationCenter name indicating that the active session is open */ +extern NSString *const FBSessionDidBecomeOpenActiveSessionNotification; + +/*! NSNotificationCenter name indicating that there is no longer an open active session */ +extern NSString *const FBSessionDidBecomeClosedActiveSessionNotification; + +/*! + @typedef FBSessionState enum + + @abstract Passed to handler block each time a session state changes + + @discussion + */ +typedef enum { + /*! One of two initial states indicating that no valid cached token was found */ + FBSessionStateCreated = 0, + /*! One of two initial session states indicating that a cached token was loaded; + when a session is in this state, a call to open* will result in an open session, + without UX or app-switching*/ + FBSessionStateCreatedTokenLoaded = 1, + /*! One of three pre-open session states indicating that an attempt to open the session + is underway*/ + FBSessionStateCreatedOpening = 2, + + /*! Open session state indicating user has logged in or a cached token is available */ + FBSessionStateOpen = 1 | FB_SESSIONSTATEOPENBIT, + /*! Open session state indicating token has been extended */ + FBSessionStateOpenTokenExtended = 2 | FB_SESSIONSTATEOPENBIT, + + /*! Closed session state indicating that a login attempt failed */ + FBSessionStateClosedLoginFailed = 1 | FB_SESSIONSTATETERMINALBIT, // NSError obj w/more info + /*! Closed session state indicating that the session was closed, but the users token + remains cached on the device for later use */ + FBSessionStateClosed = 2 | FB_SESSIONSTATETERMINALBIT, // " +} FBSessionState; + +/*! helper macro to test for states that imply an open session */ +#define FB_ISSESSIONOPENWITHSTATE(state) (0 != (state & FB_SESSIONSTATEOPENBIT)) + +/*! helper macro to test for states that are terminal */ +#define FB_ISSESSIONSTATETERMINAL(state) (0 != (state & FB_SESSIONSTATETERMINALBIT)) + +/*! + @typedef FBSessionLoginBehavior enum + + @abstract + Passed to open to indicate whether Facebook Login should allow for fallback to be attempted. + + @discussion + Facebook Login authorizes the application to act on behalf of the user, using the user's + Facebook account. Usually a Facebook Login will rely on an account maintained outside of + the application, by the native Facebook application, the browser, or perhaps the device + itself. This avoids the need for a user to enter their username and password directly, and + provides the most secure and lowest friction way for a user to authorize the application to + interact with Facebook. If a Facebook Login is not possible, a fallback Facebook Login may be + attempted, where the user is prompted to enter their credentials in a web-view hosted directly + by the application. + + The `FBSessionLoginBehavior` enum specifies whether to allow fallback, disallow fallback, or + force fallback login behavior. Most applications will use the default, which attempts a normal + Facebook Login, and only falls back if needed. In rare cases, it may be preferable to disallow + fallback Facebook Login completely, or to force a fallback login. + */ +typedef enum { + /*! Attempt Facebook Login, ask user for credentials if necessary */ + FBSessionLoginBehaviorWithFallbackToWebView = 0, + /*! Attempt Facebook Login, no direct request for credentials will be made */ + FBSessionLoginBehaviorWithNoFallbackToWebView = 1, + /*! Only attempt WebView Login; ask user for credentials */ + FBSessionLoginBehaviorForcingWebView = 2, + /*! Attempt Facebook Login, prefering system account and falling back to fast app switch if necessary */ + FBSessionLoginBehaviorUseSystemAccountIfPresent = 3, +} FBSessionLoginBehavior; + +/*! + @typedef FBSessionDefaultAudience enum + + @abstract + Passed to open to indicate which default audience to use for sessions that post data to Facebook. + + @discussion + Certain operations such as publishing a status or publishing a photo require an audience. When the user + grants an application permission to perform a publish operation, a default audience is selected as the + publication ceiling for the application. This enumerated value allows the application to select which + audience to ask the user to grant publish permission for. + */ +typedef enum { + /*! No audience needed; this value is useful for cases where data will only be read from Facebook */ + FBSessionDefaultAudienceNone = 0, + /*! Indicates that only the user is able to see posts made by the application */ + FBSessionDefaultAudienceOnlyMe = 10, + /*! Indicates that the user's friends are able to see posts made by the application */ + FBSessionDefaultAudienceFriends = 20, + /*! Indicates that all Facebook users are able to see posts made by the application */ + FBSessionDefaultAudienceEveryone = 30, +} FBSessionDefaultAudience; + +/*! + @typedef FBSessionLoginType enum + + @abstract + Used as the type of the loginType property in order to specify what underlying technology was used to + login the user. + + @discussion + The FBSession object is an abstraction over five distinct mechanisms. This enum allows an application + to test for the mechanism used by a particular instance of FBSession. Usually the mechanism used for a + given login does not matter, however for certain capabilities, the type of login can impact the behavior + of other Facebook functionality. + */ +typedef enum { + /*! A login type has not yet been established */ + FBSessionLoginTypeNone = 0, + /*! A system integrated account was used to log the user into the application */ + FBSessionLoginTypeSystemAccount = 1, + /*! The Facebook native application was used to log the user into the application */ + FBSessionLoginTypeFacebookApplication = 2, + /*! Safari was used to log the user into the application */ + FBSessionLoginTypeFacebookViaSafari = 3, + /*! A web view was used to log the user into the application */ + FBSessionLoginTypeWebView = 4, + /*! A test user was used to create an open session */ + FBSessionLoginTypeTestUser = 5, +} FBSessionLoginType; + +/*! + @typedef + + @abstract Block type used to define blocks called by for state updates + @discussion + */ +typedef void (^FBSessionStateHandler)(FBSession *session, + FBSessionState status, + NSError *error); + +/*! + @typedef + + @abstract Block type used to define blocks called by <[FBSession reauthorizeWithPermissions]>/. + + @discussion + */ +typedef void (^FBSessionReauthorizeResultHandler)(FBSession *session, + NSError *error); + +/*! + @class FBSession + + @abstract + The `FBSession` object is used to authenticate a user and manage the user's session. After + initializing a `FBSession` object the Facebook App ID and desired permissions are stored. + Opening the session will initiate the authentication flow after which a valid user session + should be available and subsequently cached. Closing the session can optionally clear the + cache. + + If an request requires user authorization then an `FBSession` object should be used. + + + @discussion + Instances of the `FBSession` class provide notification of state changes in the following ways: + + 1. Callers of certain `FBSession` methods may provide a block that will be called + back in the course of state transitions for the session (e.g. login or session closed). + + 2. The object supports Key-Value Observing (KVO) for property changes. + */ +@interface FBSession : NSObject + +/*! + @methodgroup Creating a session + */ + +/*! + @method + + @abstract + Returns a newly initialized Facebook session with default values for the parameters + to . + */ +- (id)init; + +/*! + @method + + @abstract + Returns a newly initialized Facebook session with the specified permissions and other + default values for parameters to . + + @param permissions An array of strings representing the permissions to request during the + authentication flow. A value of nil indicates basic permissions. The default is nil. + + @discussion + It is required that any single permission request request (including initial log in) represent read-only permissions + or publish permissions only; not both. The permissions passed here should reflect this requirement. + + */ +- (id)initWithPermissions:(NSArray*)permissions; + +/*! + @method + + @abstract + Following are the descriptions of the arguments along with their + defaults when ommitted. + + @param permissions An array of strings representing the permissions to request during the + authentication flow. A value of nil indicates basic permissions. The default is nil. + @param appID The Facebook App ID for the session. If nil is passed in the default App ID will be obtained from a call to <[FBSession defaultAppID]>. The default is nil. + @param urlSchemeSuffix The URL Scheme Suffix to be used in scenarious where multiple iOS apps use one Facebook App ID. A value of nil indicates that this information should be pulled from the plist. The default is nil. + @param tokenCachingStrategy Specifies a key name to use for cached token information in NSUserDefaults, nil + indicates a default value of @"FBAccessTokenInformationKey". + + @discussion + It is required that any single permission request request (including initial log in) represent read-only permissions + or publish permissions only; not both. The permissions passed here should reflect this requirement. + */ +- (id)initWithAppID:(NSString*)appID + permissions:(NSArray*)permissions + urlSchemeSuffix:(NSString*)urlSchemeSuffix + tokenCacheStrategy:(FBSessionTokenCachingStrategy*)tokenCachingStrategy; + +/*! + @method + + @abstract + Following are the descriptions of the arguments along with their + defaults when ommitted. + + @param permissions An array of strings representing the permissions to request during the + authentication flow. A value of nil indicates basic permissions. The default is nil. + @param defaultAudience Most applications use FBSessionDefaultAudienceNone here, only specifying an audience when using reauthorize to request publish permissions. + @param appID The Facebook App ID for the session. If nil is passed in the default App ID will be obtained from a call to <[FBSession defaultAppID]>. The default is nil. + @param urlSchemeSuffix The URL Scheme Suffix to be used in scenarious where multiple iOS apps use one Facebook App ID. A value of nil indicates that this information should be pulled from the plist. The default is nil. + @param tokenCachingStrategy Specifies a key name to use for cached token information in NSUserDefaults, nil + indicates a default value of @"FBAccessTokenInformationKey". + + @discussion + It is required that any single permission request request (including initial log in) represent read-only permissions + or publish permissions only; not both. The permissions passed here should reflect this requirement. If publish permissions + are used, then the audience must also be specified. + */ +- (id)initWithAppID:(NSString*)appID + permissions:(NSArray*)permissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience + urlSchemeSuffix:(NSString*)urlSchemeSuffix + tokenCacheStrategy:(FBSessionTokenCachingStrategy*)tokenCachingStrategy; + +// instance readonly properties + +/*! @abstract Indicates whether the session is open and ready for use. */ +@property(readonly) BOOL isOpen; + +/*! @abstract Detailed session state */ +@property(readonly) FBSessionState state; + +/*! @abstract Identifies the Facebook app which the session object represents. */ +@property(readonly, copy) NSString *appID; + +/*! @abstract Identifies the URL Scheme Suffix used by the session. This is used when multiple iOS apps share a single Facebook app ID. */ +@property(readonly, copy) NSString *urlSchemeSuffix; + +/*! @abstract The access token for the session object. */ +@property(readonly, copy) NSString *accessToken; + +/*! @abstract The expiration date of the access token for the session object. */ +@property(readonly, copy) NSDate *expirationDate; + +/*! @abstract The permissions granted to the access token during the authentication flow. */ +@property(readonly, copy) NSArray *permissions; + +/*! @abstract Specifies the login type used to authenticate the user. */ +@property(readonly) FBSessionLoginType loginType; + +/*! + @methodgroup Instance methods + */ + +/*! + @method + + @abstract Opens a session for the Facebook. + + @discussion + A session may not be used with and other classes in the SDK until it is open. If, prior + to calling open, the session is in the state, then no UX occurs, and + the session becomes available for use. If the session is in the state, prior + to calling open, then a call to open causes login UX to occur, either via the Facebook application + or via mobile Safari. + + Open may be called at most once and must be called after the `FBSession` is initialized. Open must + be called before the session is closed. Calling an open method at an invalid time will result in + an exception. The open session methods may be passed a block that will be called back when the session + state changes. The block will be released when the session is closed. + + @param handler A block to call with the state changes. The default is nil. +*/ +- (void)openWithCompletionHandler:(FBSessionStateHandler)handler; + +/*! + @method + + @abstract Logs a user on to Facebook. + + @discussion + A session may not be used with and other classes in the SDK until it is open. If, prior + to calling open, the session is in the state, then no UX occurs, and + the session becomes available for use. If the session is in the state, prior + to calling open, then a call to open causes login UX to occur, either via the Facebook application + or via mobile Safari. + + The method may be called at most once and must be called after the `FBSession` is initialized. It must + be called before the session is closed. Calling the method at an invalid time will result in + an exception. The open session methods may be passed a block that will be called back when the session + state changes. The block will be released when the session is closed. + + @param behavior Controls whether to allow, force, or prohibit Facebook Login or Inline Facebook Login. The default + is to allow Facebook Login, with fallback to Inline Facebook Login. + @param handler A block to call with session state changes. The default is nil. + */ +- (void)openWithBehavior:(FBSessionLoginBehavior)behavior + completionHandler:(FBSessionStateHandler)handler; + +/*! + @abstract + Closes the local in-memory session object, but does not clear the persisted token cache. + */ +- (void)close; + +/*! + @abstract + Closes the in-memory session, and clears any persisted cache related to the session. +*/ +- (void)closeAndClearTokenInformation; + +/*! + @abstract + Reauthorizes the session, with additional permissions. + + @param permissions An array of strings representing the permissions to request during the + authentication flow. A value of nil indicates basic permissions. The default is nil. + @param behavior Controls whether to allow, force, or prohibit Facebook Login. The default + is to allow Facebook Login and fall back to Inline Facebook Login if needed. + @param handler A block to call with session state changes. The default is nil. + + @discussion Methods and properties that specify permissions without a read or publish + qualification are deprecated; use of a read-qualified or publish-qualified alternative is preferred + (e.g. reauthorizeWithReadPermissions or reauthorizeWithPublishPermissions) + */ +- (void)reauthorizeWithPermissions:(NSArray*)permissions + behavior:(FBSessionLoginBehavior)behavior + completionHandler:(FBSessionReauthorizeResultHandler)handler + __attribute__((deprecated)); + +/*! + @abstract + Reauthorizes the session, with additional permissions. + + @param readPermissions An array of strings representing the permissions to request during the + authentication flow. A value of nil indicates basic permissions. + + @param handler A block to call with session state changes. The default is nil. + */ +- (void)reauthorizeWithReadPermissions:(NSArray*)readPermissions + completionHandler:(FBSessionReauthorizeResultHandler)handler; + +/*! + @abstract + Reauthorizes the session, with additional permissions. + + @param writePermissions An array of strings representing the permissions to request during the + authentication flow. + + @param defaultAudience Specifies the audience for posts. + + @param handler A block to call with session state changes. The default is nil. + */ +- (void)reauthorizeWithPublishPermissions:(NSArray*)writePermissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience + completionHandler:(FBSessionReauthorizeResultHandler)handler; + +/*! + @abstract + A helper method that is used to provide an implementation for + [UIApplicationDelegate application:openURL:sourceApplication:annotation:]. It should be invoked during + the Facebook Login flow and will update the session information based on the incoming URL. + + @param url The URL as passed to [UIApplicationDelegate application:openURL:sourceApplication:annotation:]. +*/ +- (BOOL)handleOpenURL:(NSURL*)url; + +/*! + @abstract + A helper method that is used to provide an implementation for + [UIApplicationDelegate applicationDidBecomeActive:] to properly resolve session state for + the Facebook Login flow, specifically to support app-switch login. +*/ +- (void)handleDidBecomeActive; + +/*! + @methodgroup Class methods + */ + +/*! + @abstract + This is the simplest method for opening a session with Facebook. Using sessionOpen logs on a user, + and sets the static activeSession which becomes the default session object for any Facebook UI widgets + used by the application. This session becomes the active session, whether open succeeds or fails. + + Note, if there is not a cached token available, this method will present UI to the user in order to + open the session via explicit login by the user. + + @param allowLoginUI Sometimes it is useful to attempt to open a session, but only if + no login UI will be required to accomplish the operation. For example, at application startup it may not + be disirable to transition to login UI for the user, and yet an open session is desired so long as a cached + token can be used to open the session. Passing NO to this argument, assures the method will not present UI + to the user in order to open the session. + + @discussion + Returns YES if the session was opened synchronously without presenting UI to the user. This occurs + when there is a cached token available from a previous run of the application. If NO is returned, this indicates + that the session was not immediately opened, via cache. However, if YES was passed as allowLoginUI, then it is + possible that the user will login, and the session will become open asynchronously. The primary use for + this return value is to switch-on facebook capabilities in your UX upon startup, in the case were the session + is opened via cache. + */ ++ (BOOL)openActiveSessionWithAllowLoginUI:(BOOL)allowLoginUI; + +/*! + @abstract + This is a simple method for opening a session with Facebook. Using sessionOpen logs on a user, + and sets the static activeSession which becomes the default session object for any Facebook UI widgets + used by the application. This session becomes the active session, whether open succeeds or fails. + + @param permissions An array of strings representing the permissions to request during the + authentication flow. A value of nil indicates basic permissions. A nil value specifies + default permissions. + + @param allowLoginUI Sometimes it is useful to attempt to open a session, but only if + no login UI will be required to accomplish the operation. For example, at application startup it may not + be desirable to transition to login UI for the user, and yet an open session is desired so long as a cached + token can be used to open the session. Passing NO to this argument, assures the method will not present UI + to the user in order to open the session. + + @param handler Many applications will benefit from notification when a session becomes invalid + or undergoes other state transitions. If a block is provided, the FBSession + object will call the block each time the session changes state. + + @discussion + Returns true if the session was opened synchronously without presenting UI to the user. This occurs + when there is a cached token available from a previous run of the application. If NO is returned, this indicates + that the session was not immediately opened, via cache. However, if YES was passed as allowLoginUI, then it is + possible that the user will login, and the session will become open asynchronously. The primary use for + this return value is to switch-on facebook capabilities in your UX upon startup, in the case were the session + is opened via cache. + + It is required that initial permissions requests represent read-only permissions only. If publish + permissions are needed, you may use reauthorizeWithPermissions to specify additional permissions as + well as an audience. Use of this method will result in a legacy fast-app-switch Facebook Login due to + the requirement to seperate read and publish permissions for newer applications. Methods and properties + that specify permissions without a read or publish qualification are deprecated; use of a read-qualified + or publish-qualified alternative is preferred. + */ ++ (BOOL)openActiveSessionWithPermissions:(NSArray*)permissions + allowLoginUI:(BOOL)allowLoginUI + completionHandler:(FBSessionStateHandler)handler + __attribute__((deprecated)); + +/*! + @abstract + This is a simple method for opening a session with Facebook. Using sessionOpen logs on a user, + and sets the static activeSession which becomes the default session object for any Facebook UI widgets + used by the application. This session becomes the active session, whether open succeeds or fails. + + @param readPermissions An array of strings representing the read permissions to request during the + authentication flow. A value of nil indicates basic permissions. It is not allowed to pass publish + permissions to this method. + + @param allowLoginUI Sometimes it is useful to attempt to open a session, but only if + no login UI will be required to accomplish the operation. For example, at application startup it may not + be desirable to transition to login UI for the user, and yet an open session is desired so long as a cached + token can be used to open the session. Passing NO to this argument, assures the method will not present UI + to the user in order to open the session. + + @param handler Many applications will benefit from notification when a session becomes invalid + or undergoes other state transitions. If a block is provided, the FBSession + object will call the block each time the session changes state. + + @discussion + Returns true if the session was opened synchronously without presenting UI to the user. This occurs + when there is a cached token available from a previous run of the application. If NO is returned, this indicates + that the session was not immediately opened, via cache. However, if YES was passed as allowLoginUI, then it is + possible that the user will login, and the session will become open asynchronously. The primary use for + this return value is to switch-on facebook capabilities in your UX upon startup, in the case were the session + is opened via cache. + + */ ++ (BOOL)openActiveSessionWithReadPermissions:(NSArray*)readPermissions + allowLoginUI:(BOOL)allowLoginUI + completionHandler:(FBSessionStateHandler)handler; + +/*! + @abstract + This is a simple method for opening a session with Facebook. Using sessionOpen logs on a user, + and sets the static activeSession which becomes the default session object for any Facebook UI widgets + used by the application. This session becomes the active session, whether open succeeds or fails. + + @param publishPermissions An array of strings representing the publish permissions to request during the + authentication flow. + + @param defaultAudience Anytime an app publishes on behalf of a user, the post must have an audience (e.g. me, my friends, etc.) + The default audience is used to notify the user of the cieling that the user agrees to grant to the app for the provided permissions. + + @param allowLoginUI Sometimes it is useful to attempt to open a session, but only if + no login UI will be required to accomplish the operation. For example, at application startup it may not + be desirable to transition to login UI for the user, and yet an open session is desired so long as a cached + token can be used to open the session. Passing NO to this argument, assures the method will not present UI + to the user in order to open the session. + + @param handler Many applications will benefit from notification when a session becomes invalid + or undergoes other state transitions. If a block is provided, the FBSession + object will call the block each time the session changes state. + + @discussion + Returns true if the session was opened synchronously without presenting UI to the user. This occurs + when there is a cached token available from a previous run of the application. If NO is returned, this indicates + that the session was not immediately opened, via cache. However, if YES was passed as allowLoginUI, then it is + possible that the user will login, and the session will become open asynchronously. The primary use for + this return value is to switch-on facebook capabilities in your UX upon startup, in the case were the session + is opened via cache. + + */ ++ (BOOL)openActiveSessionWithPublishPermissions:(NSArray*)publishPermissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience + allowLoginUI:(BOOL)allowLoginUI + completionHandler:(FBSessionStateHandler)handler; + +/*! + @abstract + An appication may get or set the current active session. Certain high-level components in the SDK + will use the activeSession to set default session (e.g. `FBLoginView`, `FBFriendPickerViewController`) + + @discussion + If sessionOpen* is called, the resulting `FBSession` object also becomes the activeSession. If another + session was active at the time, it is closed automatically. If activeSession is called when no session + is active, a session object is instatiated and returned; in this case open must be called on the session + in order for it to be useable for communication with Facebook. + */ ++ (FBSession*)activeSession; + +/*! + @abstract + An appication may get or set the current active session. Certain high-level components in the SDK + will use the activeSession to set default session (e.g. `FBLoginView`, `FBFriendPickerViewController`) + + @param session The FBSession object to become the active session + + @discussion + If an application prefers the flexibilility of directly instantiating a session object, an active + session can be set directly. + */ ++ (FBSession*)setActiveSession:(FBSession*)session; + +/*! + @method + + @abstract Set the default Facebook App ID to use for sessions. The app ID may be + overridden on a per session basis. + + @param appID The default Facebook App ID to use for methods. + */ ++ (void)setDefaultAppID:(NSString*)appID; + +/*! + @method + + @abstract Get the default Facebook App ID to use for sessions. If not explicitly + set, the default will be read from the application's plist. The app ID may be + overridden on a per session basis. + */ ++ (NSString*)defaultAppID; + +@end diff --git a/src/ios/facebook/FBSession.m b/src/ios/facebook/FBSession.m new file mode 100644 index 000000000..eb9b54f8c --- /dev/null +++ b/src/ios/facebook/FBSession.m @@ -0,0 +1,1761 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import +#import "FBSession.h" +#import "FBSession+Internal.h" +#import "FBSession+Protected.h" +#import "FBSessionTokenCachingStrategy.h" +#import "FBSettings.h" +#import "FBSettings+Internal.h" +#import "FBError.h" +#import "FBLogger.h" +#import "FBUtility.h" +#import "FBDataDiskCache.h" + +// the sooner we can remove these the better +#import "Facebook.h" +#import "FBLoginDialog.h" + +// these are helpful macros for testing various login methods, should always checkin as NO/NO +#define TEST_DISABLE_MULTITASKING_LOGIN NO +#define TEST_DISABLE_FACEBOOKLOGIN NO + +// extern const strings +NSString *const FBErrorLoginFailedReasonInlineCancelledValue = @"com.facebook.sdk:InlineLoginCancelled"; +NSString *const FBErrorLoginFailedReasonInlineNotCancelledValue = @"com.facebook.sdk:ErrorLoginNotCancelled"; + +// const strings +static NSString *const FBPLISTAppIDKey = @"FacebookAppID"; +// for unit testing mode only (DO NOT store application secrets in a published application plist) +static NSString *const FBPLISTAppSecretKey = @"FacebookAppSecret"; +static NSString *const FBAuthURLScheme = @"fbauth"; +static NSString *const FBAuthURLPath = @"authorize"; +static NSString *const FBRedirectURL = @"fbconnect://success"; +static NSString *const FBDialogBaseURL = @"https://m." FB_BASE_URL @"/dialog/"; +static NSString *const FBLoginDialogMethod = @"oauth"; +static NSString *const FBLoginUXClientID = @"client_id"; +static NSString *const FBLoginUXUserAgent = @"user_agent"; +static NSString *const FBLoginUXType = @"type"; +static NSString *const FBLoginUXRedirectURI = @"redirect_uri"; +static NSString *const FBLoginUXTouch = @"touch"; +static NSString *const FBLoginUXDisplay = @"display"; +static NSString *const FBLoginUXIOS = @"ios"; +static NSString *const FBLoginUXSDK = @"sdk"; + +// the following constant strings are used by NSNotificationCenter +NSString *const FBSessionDidSetActiveSessionNotification = @"com.facebook.sdk:FBSessionDidSetActiveSessionNotification"; +NSString *const FBSessionDidUnsetActiveSessionNotification = @"com.facebook.sdk:FBSessionDidUnsetActiveSessionNotification"; +NSString *const FBSessionDidBecomeOpenActiveSessionNotification = @"com.facebook.sdk:FBSessionDidBecomeOpenActiveSessionNotification"; +NSString *const FBSessionDidBecomeClosedActiveSessionNotification = @"com.facebook.sdk:FBSessionDidBecomeClosedActiveSessionNotification"; + +// the following const strings name properties for which KVO is manually handled +// if name changes occur, these strings must be modified to match, else KVO will fail +static NSString *const FBisOpenPropertyName = @"isOpen"; +static NSString *const FBstatusPropertyName = @"status"; +static NSString *const FBaccessTokenPropertyName = @"accessToken"; +static NSString *const FBexpirationDatePropertyName = @"expirationDate"; + +static int const FBTokenExtendThresholdSeconds = 24 * 60 * 60; // day +static int const FBTokenRetryExtendSeconds = 60 * 60; // hour + +// module scoped globals +static NSString *g_defaultAppID = nil; +static FBSession *g_activeSession = nil; + +@interface FBSession () { + @private + // public-property ivars + NSString *_urlSchemeSuffix; + + // private property and non-property ivars + BOOL _isInStateTransition; + BOOL _isFacebookLoginToken; + BOOL _isOSIntegratedFacebookLoginToken; + FBSessionLoginType _loginTypeOfPendingOpenUrlCallback; + FBSessionDefaultAudience _defaultDefaultAudience; +} + +// private setters +@property(readwrite) FBSessionState state; +@property(readwrite, copy) NSString *appID; +@property(readwrite, copy) NSString *urlSchemeSuffix; +@property(readwrite, copy) NSString *accessToken; +@property(readwrite, copy) NSDate *expirationDate; +@property(readwrite, copy) NSArray *permissions; +@property(readwrite) FBSessionLoginType loginType; + +// private properties +@property(readwrite, retain) FBSessionTokenCachingStrategy *tokenCachingStrategy; +@property(readwrite, copy) NSDate *refreshDate; +@property(readwrite, copy) NSDate *attemptedRefreshDate; +@property(readwrite, copy) FBSessionStateHandler loginHandler; +@property(readwrite, copy) FBSessionReauthorizeResultHandler reauthorizeHandler; +@property(readwrite, copy) NSArray *reauthorizePermissions; +@property(readonly) NSString *appBaseUrl; +@property(readwrite, retain) FBLoginDialog *loginDialog; +@property(readwrite, retain) NSThread *affinitizedThread; + +// private members +- (void)authorizeWithPermissions:(NSArray*)permissions + defaultAudience:(FBSessionDefaultAudience)audience + integratedAuth:(BOOL)tryIntegratedAuth + FBAppAuth:(BOOL)tryFBAppAuth + safariAuth:(BOOL)trySafariAuth + fallback:(BOOL)tryFallback + isReauthorize:(BOOL)isReauthorize; +- (void)authorizeUsingSystemAccountStore:(id)accountStore + accountType:(id)accountType + permissions:(NSArray*)permissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience + isReauthorize:(BOOL)isReauthorize; +- (BOOL)handleOpenURLPreOpen:(NSDictionary*)parameters + accessToken:(NSString*)accessToken + loginType:(FBSessionLoginType)loginType; +- (BOOL)handleOpenURLReauthorize:(NSDictionary*)parameters + accessToken:(NSString*)accessToken; +- (void)completeReauthorizeWithAccessToken:(NSString*)accessToken + expirationDate:(NSDate*)expirationDate + permissions:(NSArray*)permissions; +- (void)reauthorizeWithPermissions:(NSArray*)permissions + isRead:(BOOL)isRead + behavior:(FBSessionLoginBehavior)behavior + defaultAudience:(FBSessionDefaultAudience)audience + completionHandler:(FBSessionReauthorizeResultHandler)handler; +- (void)callReauthorizeHandlerAndClearState:(NSError*)error; + +// class members ++ (BOOL)areRequiredPermissions:(NSArray*)requiredPermissions + aSubsetOfPermissions:(NSArray*)cachedPermissions; ++ (NSString *)sessionStateDescription:(FBSessionState)sessionState; ++ (BOOL)openActiveSessionWithPermissions:(NSArray*)permissions + allowLoginUI:(BOOL)allowLoginUI + allowSystemAccount:(BOOL)allowSystemAccount + isRead:(BOOL)isRead + defaultAudience:(FBSessionDefaultAudience)defaultAudience + completionHandler:(FBSessionStateHandler)handler; ++ (void)validateRequestForPermissions:(NSArray*)permissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience + allowSystemAccount:(BOOL)allowSystemAccount + isRead:(BOOL)isRead; ++ (BOOL)logIfFoundUnexpectedPermissions:(NSArray*)permissions + isRead:(BOOL)isRead; ++ (NSArray*)addBasicInfoPermission:(NSArray*)permissions; ++ (BOOL)isPublishPermission:(NSString*)permission; ++ (BOOL)areAllPermissionsReadPermissions:(NSArray*)permissions; + +@end + +@implementation FBSession : NSObject + +@synthesize + // public properties + appID = _appID, + permissions = _permissions, + loginType = _loginType, + + // following properties use manual KVO -- changes to names require + // changes to static property name variables (e.g. FBisOpenPropertyName) + state = _state, + accessToken = _accessToken, + expirationDate = _expirationDate, + + // private properties + tokenCachingStrategy = _tokenCachingStrategy, + refreshDate = _refreshDate, + attemptedRefreshDate = _attemptedRefreshDate, + loginDialog = _loginDialog, + affinitizedThread = _affinitizedThread, + loginHandler = _loginHandler, + reauthorizeHandler = _reauthorizeHandler, + reauthorizePermissions = _reauthorizePermissions; + +#pragma mark Lifecycle + +- (id)init { + return [self initWithAppID:nil + permissions:nil + urlSchemeSuffix:nil + tokenCacheStrategy:nil]; +} + +- (id)initWithPermissions:(NSArray*)permissions { + return [self initWithAppID:nil + permissions:permissions + urlSchemeSuffix:nil + tokenCacheStrategy:nil]; +} + +- (id)initWithAppID:(NSString*)appID + permissions:(NSArray*)permissions + urlSchemeSuffix:(NSString*)urlSchemeSuffix + tokenCacheStrategy:(FBSessionTokenCachingStrategy*)tokenCachingStrategy { + return [self initWithAppID:appID + permissions:permissions + defaultAudience:FBSessionDefaultAudienceNone + urlSchemeSuffix:urlSchemeSuffix + tokenCacheStrategy:tokenCachingStrategy]; +} + +- (id)initWithAppID:(NSString*)appID + permissions:(NSArray*)permissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience + urlSchemeSuffix:(NSString*)urlSchemeSuffix + tokenCacheStrategy:(FBSessionTokenCachingStrategy*)tokenCachingStrategy { + self = [super init]; + if (self) { + + // setup values where nil implies a default + if (!appID) { + appID = [FBSession defaultAppID]; + } + if (!permissions) { + permissions = [NSArray array]; + } + if (!tokenCachingStrategy) { + tokenCachingStrategy = [FBSessionTokenCachingStrategy defaultInstance]; + } + + // if we don't have an appID by here, fail -- this is almost certainly an app-bug + if (!appID) { + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBSession: No AppID provided; either pass an " + @"AppID to init, or add a string valued key with the " + @"appropriate id named FacebookAppID to the bundle *.plist" + userInfo:nil] + raise]; + } + + // assign arguments; + self.appID = appID; + self.permissions = permissions; + self.urlSchemeSuffix = urlSchemeSuffix; + self.tokenCachingStrategy = tokenCachingStrategy; + + // additional setup + _isInStateTransition = NO; + _isFacebookLoginToken = NO; + _loginTypeOfPendingOpenUrlCallback = FBSessionLoginTypeNone; + _isOSIntegratedFacebookLoginToken = NO; + _defaultDefaultAudience = defaultAudience; + self.attemptedRefreshDate = [NSDate distantPast]; + self.refreshDate = nil; + self.state = FBSessionStateCreated; + self.loginType = FBSessionLoginTypeNone; + self.affinitizedThread = [NSThread currentThread]; + [FBLogger registerCurrentTime:FBLoggingBehaviorPerformanceCharacteristics + withTag:self]; + + // use cached token if present + NSDictionary *tokenInfo = [tokenCachingStrategy fetchTokenInformation]; + if ([FBSessionTokenCachingStrategy isValidTokenInformation:tokenInfo]) { + NSDate *cachedTokenExpirationDate = [tokenInfo objectForKey:FBTokenInformationExpirationDateKey]; + NSString *cachedToken = [tokenInfo objectForKey:FBTokenInformationTokenKey]; + + // get the cached permissions, and do a subset check + NSArray *cachedPermissions = [tokenInfo objectForKey:FBTokenInformationPermissionsKey]; + BOOL isSubset = [FBSession areRequiredPermissions:permissions + aSubsetOfPermissions:cachedPermissions]; + + if (isSubset && + // check to see if expiration date is later than now + (NSOrderedDescending == [cachedTokenExpirationDate compare:[NSDate date]])) { + // if we had cached anything at all, use those + if (cachedPermissions) { + self.permissions = cachedPermissions; + } + + // if we have cached an optional refresh date or Facebook Login indicator, pick them up here + self.refreshDate = [tokenInfo objectForKey:FBTokenInformationRefreshDateKey]; + _isFacebookLoginToken = [[tokenInfo objectForKey:FBTokenInformationIsFacebookLoginKey] boolValue]; + FBSessionLoginType loginType = [[tokenInfo objectForKey:FBTokenInformationLoginTypeLoginKey] intValue]; + _isOSIntegratedFacebookLoginToken = loginType == FBSessionLoginTypeSystemAccount; + + // set the state and token info + [self transitionToState:FBSessionStateCreatedTokenLoaded + andUpdateToken:cachedToken + andExpirationDate:cachedTokenExpirationDate + shouldCache:NO + loginType:loginType]; + } else { + // else this token is expired and should be cleared from cache + [tokenCachingStrategy clearToken]; + } + } + + [FBSettings autoPublishInstall:self.appID]; + } + return self; +} + +- (void)dealloc { + [_loginDialog release]; + [_attemptedRefreshDate release]; + [_refreshDate release]; + [_reauthorizeHandler release]; + [_loginHandler release]; + [_reauthorizePermissions release]; + [_appID release]; + [_urlSchemeSuffix release]; + [_accessToken release]; + [_expirationDate release]; + [_permissions release]; + [_tokenCachingStrategy release]; + [_affinitizedThread release]; + + [super dealloc]; +} + +#pragma mark - +#pragma mark Public Members + +- (void)openWithCompletionHandler:(FBSessionStateHandler)handler { + [self openWithBehavior:FBSessionLoginBehaviorWithFallbackToWebView completionHandler:handler]; +} + +- (void)openWithBehavior:(FBSessionLoginBehavior)behavior + completionHandler:(FBSessionStateHandler)handler { + + NSAssert(self.affinitizedThread == [NSThread currentThread], @"FBSession: should only be used from a single thread"); + + if (!(self.state == FBSessionStateCreated || + self.state == FBSessionStateCreatedTokenLoaded)) { + // login may only be called once, and only from one of the two initial states + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBSession: an attempt was made to open an already opened or closed session" + userInfo:nil] + raise]; + } + self.loginHandler = handler; + + // normal login depends on the availability of a valid cached token + if (self.state == FBSessionStateCreated) { + + // set the state and token info + [self transitionToState:FBSessionStateCreatedOpening + andUpdateToken:nil + andExpirationDate:nil + shouldCache:NO + loginType:FBSessionLoginTypeNone]; + + [self authorizeWithPermissions:self.permissions + behavior:behavior + defaultAudience:_defaultDefaultAudience + isReauthorize:NO]; + + } else { // self.status == FBSessionStateLoadedValidToken + + // this case implies that a valid cached token was found, and preserves the + // "1-session-1-identity" rule, by transitioning to logged in, without a transition to login UX + [self transitionAndCallHandlerWithState:FBSessionStateOpen + error:nil + token:nil + expirationDate:nil + shouldCache:NO + loginType:FBSessionLoginTypeNone]; + } +} + +- (void)reauthorizeWithPermissions:(NSArray*)permissions + behavior:(FBSessionLoginBehavior)behavior + completionHandler:(FBSessionReauthorizeResultHandler)handler { + [self reauthorizeWithPermissions:permissions + isRead:NO + behavior:behavior + defaultAudience:FBSessionDefaultAudienceNone + completionHandler:handler]; +} + +- (void)reauthorizeWithReadPermissions:(NSArray*)readPermissions + completionHandler:(FBSessionReauthorizeResultHandler)handler { + [self reauthorizeWithPermissions:readPermissions + isRead:YES + behavior:FBSessionLoginBehaviorUseSystemAccountIfPresent + defaultAudience:FBSessionDefaultAudienceNone + completionHandler:handler]; +} + +- (void)reauthorizeWithPublishPermissions:(NSArray*)writePermissions + defaultAudience:(FBSessionDefaultAudience)audience + completionHandler:(FBSessionReauthorizeResultHandler)handler { + [self reauthorizeWithPermissions:writePermissions + isRead:NO + behavior:FBSessionLoginBehaviorUseSystemAccountIfPresent + defaultAudience:audience + completionHandler:handler]; +} + +- (void)close { + NSAssert(self.affinitizedThread == [NSThread currentThread], @"FBSession: should only be used from a single thread"); + + FBSessionState state; + if (self.state == FBSessionStateCreatedOpening) { + state = FBSessionStateClosedLoginFailed; + } else { + state = FBSessionStateClosed; + } + + [self transitionAndCallHandlerWithState:state + error:nil + token:nil + expirationDate:nil + shouldCache:NO + loginType:FBSessionLoginTypeNone]; +} + +- (void)closeAndClearTokenInformation { + [self closeAndClearTokenInformation:nil]; +} + +- (BOOL)handleOpenURL:(NSURL *)url { + NSAssert(self.affinitizedThread == [NSThread currentThread], @"FBSession: should only be used from a single thread"); + + // if the URL's structure doesn't match the structure used for Facebook authorization, abort. + if (![[url absoluteString] hasPrefix:self.appBaseUrl]) { + return NO; + } + FBSessionLoginType loginType = _loginTypeOfPendingOpenUrlCallback; + _loginTypeOfPendingOpenUrlCallback = FBSessionLoginTypeNone; + + // version 3.2.3 of the Facebook app encodes the parameters in the query but + // version 3.3 and above encode the parameters in the fragment; check first for + // fragment, and if missing fall back to query + NSString *query = [url fragment]; + if (!query) { + query = [url query]; + } + + NSDictionary *params = [FBUtility dictionaryByParsingURLQueryPart:query]; + NSString *accessToken = [params objectForKey:@"access_token"]; + + switch (self.state) { + case FBSessionStateCreatedOpening: + return [self handleOpenURLPreOpen:params + accessToken:accessToken + loginType:loginType]; + case FBSessionStateOpen: + case FBSessionStateOpenTokenExtended: + return [self handleOpenURLReauthorize:params + accessToken:accessToken]; + default: + FBConditionalLog(NO, @"handleOpenURL should not be called once a session has closed"); + return NO; + } +} + +- (void)handleDidBecomeActive{ + //Unexpected to calls to app delegate's applicationDidBecomeActive are + // handled by this method. If a pending fast-app-switch [re]authorization + // is in flight, it is cancelled. Otherwise, this method is a no-op. + + const FBSessionState state = FBSession.activeSession.state; + + if (state == FBSessionStateCreated || + state == FBSessionStateClosed || + state == FBSessionStateClosedLoginFailed){ + return; + } + + if (_loginTypeOfPendingOpenUrlCallback != FBSessionLoginTypeNone){ + if (state == FBSessionStateCreatedOpening){ + //if we're here, user had declined a fast app switch login. + [FBSession.activeSession close]; + } else { + //this means the user declined a 'reauthorization' so we need + // to clean out the in-flight request. + NSError *error = [FBSession errorLoginFailedWithReason:FBErrorReauthorizeFailedReasonUserCancelled + errorCode:nil + innerError:nil]; + [self callReauthorizeHandlerAndClearState:error]; + } + _loginTypeOfPendingOpenUrlCallback = FBSessionLoginTypeNone; + } +} + +- (BOOL)isOpen { + return FB_ISSESSIONOPENWITHSTATE(self.state); +} + +- (NSString*)urlSchemeSuffix { + NSAssert(self.affinitizedThread == [NSThread currentThread], @"FBSession: should only be used from a single thread"); + return _urlSchemeSuffix ? _urlSchemeSuffix : @""; +} + +// actually a private member, but wanted to be close to its public colleague +- (void)setUrlSchemeSuffix:(NSString*)newValue { + if (_urlSchemeSuffix != newValue) { + [_urlSchemeSuffix release]; + _urlSchemeSuffix = [(newValue ? newValue : @"") copy]; + } +} + +#pragma mark - +#pragma mark Class Methods + ++ (BOOL)openActiveSessionWithAllowLoginUI:(BOOL)allowLoginUI { + return [FBSession openActiveSessionWithPermissions:nil + allowLoginUI:allowLoginUI + allowSystemAccount:YES + isRead:YES + defaultAudience:FBSessionDefaultAudienceNone + completionHandler:nil]; +} + ++ (BOOL)openActiveSessionWithPermissions:(NSArray*)permissions + allowLoginUI:(BOOL)allowLoginUI + completionHandler:(FBSessionStateHandler)handler { + return [FBSession openActiveSessionWithPermissions:permissions + allowLoginUI:allowLoginUI + allowSystemAccount:NO + isRead:NO + defaultAudience:FBSessionDefaultAudienceNone + completionHandler:handler]; +} + ++ (BOOL)openActiveSessionWithReadPermissions:(NSArray*)readPermissions + allowLoginUI:(BOOL)allowLoginUI + completionHandler:(FBSessionStateHandler)handler { + return [FBSession openActiveSessionWithPermissions:readPermissions + allowLoginUI:allowLoginUI + allowSystemAccount:YES + isRead:YES + defaultAudience:FBSessionDefaultAudienceNone + completionHandler:handler]; +} + ++ (BOOL)openActiveSessionWithPublishPermissions:(NSArray*)publishPermissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience + allowLoginUI:(BOOL)allowLoginUI + completionHandler:(FBSessionStateHandler)handler { + return [FBSession openActiveSessionWithPermissions:publishPermissions + allowLoginUI:allowLoginUI + allowSystemAccount:YES + isRead:NO + defaultAudience:defaultAudience + completionHandler:handler]; +} + ++ (FBSession*)activeSession { + if (!g_activeSession) { + FBSession *session = [[FBSession alloc] init]; + [FBSession setActiveSession:session]; + [session release]; + } + return [[g_activeSession retain] autorelease]; +} + ++ (FBSession*)setActiveSession:(FBSession*)session { + + if (session != g_activeSession) { + // we will close this, but we want any resulting + // handlers to see the new active session + FBSession *toRelease = g_activeSession; + + // if we are being replaced, then we close you + [toRelease close]; + + // set the new session + g_activeSession = [session retain]; + + // some housekeeping needs to happen if we had a previous session + if (toRelease) { + // now the notification/release of the prior active + [[NSNotificationCenter defaultCenter] postNotificationName:FBSessionDidUnsetActiveSessionNotification + object:toRelease]; + [toRelease release]; + } + + // we don't notify nil sets + if (session) { + [[NSNotificationCenter defaultCenter] postNotificationName:FBSessionDidSetActiveSessionNotification + object:session]; + + if (session.isOpen) { + [[NSNotificationCenter defaultCenter] postNotificationName:FBSessionDidBecomeOpenActiveSessionNotification + object:session]; + } + } + } + + return session; +} + ++ (void)setDefaultAppID:(NSString*)appID { + NSString *oldValue = g_defaultAppID; + g_defaultAppID = [appID copy]; + [oldValue release]; +} + ++ (NSString*)defaultAppID { + if (!g_defaultAppID) { + NSBundle* bundle = [NSBundle mainBundle]; + g_defaultAppID = [bundle objectForInfoDictionaryKey:FBPLISTAppIDKey]; + } + return g_defaultAppID; +} + +//calls ios6 renewCredentialsForAccount in order to update ios6's worldview of authorization state. +// if not using ios6 system auth, this is a no-op. ++ (void)renewSystemAuthorization { + id accountStore = nil; + id accountTypeFB = nil; + + if ((accountStore = [[[ACAccountStore alloc] init] autorelease]) && + (accountTypeFB = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook] ) ){ + + NSArray *fbAccounts = [accountStore accountsWithAccountType:accountTypeFB]; + id account; + if (fbAccounts && [fbAccounts count] > 0 && + (account = [fbAccounts objectAtIndex:0])){ + + [accountStore renewCredentialsForAccount:account completion:^(ACAccountCredentialRenewResult renewResult, NSError *error) { + //we don't actually need to inspect renewResult or error. + if (error){ + [FBLogger singleShotLogEntry:FBLoggingBehaviorAccessTokens + logEntry:[NSString stringWithFormat:@"renewCredentialsForAccount result:%d, error: %@", + renewResult, + error]]; + } + }]; + } + } +} + +#pragma mark - +#pragma mark Private Members + +// private methods are broken into two categories: core session and helpers + +// core session members + +// core member that owns all state transitions as well as property setting for status and isOpen +- (BOOL)transitionToState:(FBSessionState)state + andUpdateToken:(NSString*)token + andExpirationDate:(NSDate*)date + shouldCache:(BOOL)shouldCache + loginType:(FBSessionLoginType)loginType { + + // is this a valid transition? + BOOL isValidTransition; + FBSessionState statePrior; + + statePrior = self.state; + switch (state) { + default: + case FBSessionStateCreated: + isValidTransition = NO; + break; + case FBSessionStateOpen: + isValidTransition = ( + statePrior == FBSessionStateCreatedTokenLoaded || + statePrior == FBSessionStateCreatedOpening + ); + break; + case FBSessionStateCreatedOpening: + case FBSessionStateCreatedTokenLoaded: + isValidTransition = statePrior == FBSessionStateCreated; + break; + case FBSessionStateClosedLoginFailed: + isValidTransition = statePrior == FBSessionStateCreatedOpening; + break; + case FBSessionStateOpenTokenExtended: + isValidTransition = ( + statePrior == FBSessionStateOpen || + statePrior == FBSessionStateOpenTokenExtended + ); + break; + case FBSessionStateClosed: + isValidTransition = ( + statePrior == FBSessionStateOpen || + statePrior == FBSessionStateOpenTokenExtended || + statePrior == FBSessionStateCreatedTokenLoaded + ); + break; + } + + // if we are just about to transition to open or token loaded, and the caller + // wants to specify a login type other than none, then we set the login type + if (isValidTransition && + (state == FBSessionStateOpen || state == FBSessionStateCreatedTokenLoaded) && + loginType != FBSessionLoginTypeNone) { + self.loginType = loginType; + } + + // invalid transition short circuits + if (!isValidTransition) { + [FBLogger singleShotLogEntry:FBLoggingBehaviorSessionStateTransitions + logEntry:[NSString stringWithFormat:@"FBSession **INVALID** transition from %@ to %@", + [FBSession sessionStateDescription:statePrior], + [FBSession sessionStateDescription:state]]]; + return NO; + } + + // if this is yes, someone called a method on FBSession from within a KVO will change handler + if (_isInStateTransition) { + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBSession: An attempt to change an FBSession object was " + @"made while a change was in flight; this is most likely due to " + @"a KVO observer calling a method on FBSession while handling a " + @"NSKeyValueObservingOptionPrior notification" + userInfo:nil] + raise]; + } + + // valid transitions notify + NSString *logString = [NSString stringWithFormat:@"FBSession transition from %@ to %@ ", + [FBSession sessionStateDescription:statePrior], + [FBSession sessionStateDescription:state]]; + [FBLogger singleShotLogEntry:FBLoggingBehaviorSessionStateTransitions logEntry:logString]; + + [FBLogger singleShotLogEntry:FBLoggingBehaviorPerformanceCharacteristics + timestampTag:self + formatString:@"%@", logString]; + + // Re-start session transition timer for the next time around. + [FBLogger registerCurrentTime:FBLoggingBehaviorPerformanceCharacteristics + withTag:self]; + + // identify whether we will update token and date, and what the values will be + BOOL changingTokenAndDate = NO; + if (token && date) { + changingTokenAndDate = YES; + } else if (!FB_ISSESSIONOPENWITHSTATE(state) && + FB_ISSESSIONOPENWITHSTATE(statePrior)) { + changingTokenAndDate = YES; + token = nil; + date = nil; + } + + BOOL changingIsOpen = FB_ISSESSIONOPENWITHSTATE(state) != FB_ISSESSIONOPENWITHSTATE(statePrior); + + // should only ever be YES from here... + _isInStateTransition = YES; + + // KVO property will change notifications, for state change + [self willChangeValueForKey:FBstatusPropertyName]; + if (changingIsOpen) { + [self willChangeValueForKey:FBisOpenPropertyName]; + } + + if (changingTokenAndDate) { + // KVO property will-change notifications for token and date + [self willChangeValueForKey:FBaccessTokenPropertyName]; + [self willChangeValueForKey:FBexpirationDatePropertyName]; + + // change the token and date values, should be kept near to state change following the conditional + self.accessToken = token; + self.expirationDate = date; + } + + // change the actual state + // note: we should not inject any callbacks between this and the token/date changes above + self.state = state; + + // ... to here -- if YES + _isInStateTransition = NO; + + if (changingTokenAndDate) { + // update the cache + if (shouldCache) { + NSMutableDictionary *tokenInfo = [NSMutableDictionary dictionaryWithCapacity:4]; + // we don't consider it a valid cache without these two values + [tokenInfo setObject:token forKey:FBTokenInformationTokenKey]; + [tokenInfo setObject:date forKey:FBTokenInformationExpirationDateKey]; + + // but these following values are optional + if (self.refreshDate) { + [tokenInfo setObject:self.refreshDate forKey:FBTokenInformationRefreshDateKey]; + } + + if (_isFacebookLoginToken) { + [tokenInfo setObject:[NSNumber numberWithBool:YES] forKey:FBTokenInformationIsFacebookLoginKey]; + } + + [tokenInfo setObject:[NSNumber numberWithInt:self.loginType] forKey:FBTokenInformationLoginTypeLoginKey]; + + if (self.permissions) { + [tokenInfo setObject:self.permissions forKey:FBTokenInformationPermissionsKey]; + } + + [self.tokenCachingStrategy cacheTokenInformation:tokenInfo]; + } + + // KVO property change notifications token and date + [self didChangeValueForKey:FBexpirationDatePropertyName]; + [self didChangeValueForKey:FBaccessTokenPropertyName]; + } + + // KVO property did change notifications, for state change + if (changingIsOpen) { + [self didChangeValueForKey:FBisOpenPropertyName]; + } + [self didChangeValueForKey:FBstatusPropertyName]; + + // if we are the active session, and we changed is-valid, notify + if (changingIsOpen && g_activeSession == self) { + if (FB_ISSESSIONOPENWITHSTATE(state)) { + [[NSNotificationCenter defaultCenter] postNotificationName:FBSessionDidBecomeOpenActiveSessionNotification + object:self]; + } else { + [[NSNotificationCenter defaultCenter] postNotificationName:FBSessionDidBecomeClosedActiveSessionNotification + object:self]; + } + } + + // Note! It is important that no processing occur after the KVO notifications have been raised, in order to + // assure the state is cohesive in common reintrant scenarios + + // the NO case short-circuits after the state switch/case + return YES; +} + +// core authorization UX flow +- (void)authorizeWithPermissions:(NSArray*)permissions + behavior:(FBSessionLoginBehavior)behavior + defaultAudience:(FBSessionDefaultAudience)audience + isReauthorize:(BOOL)isReauthorize { + BOOL tryIntegratedAuth = behavior == FBSessionLoginBehaviorUseSystemAccountIfPresent; + BOOL tryFacebookLogin = (behavior == FBSessionLoginBehaviorUseSystemAccountIfPresent) || + (behavior == FBSessionLoginBehaviorWithFallbackToWebView) || + (behavior == FBSessionLoginBehaviorWithNoFallbackToWebView); + BOOL tryFallback = (behavior == FBSessionLoginBehaviorWithFallbackToWebView) || + (behavior == FBSessionLoginBehaviorForcingWebView); + + [self authorizeWithPermissions:(NSArray*)permissions + defaultAudience:audience + integratedAuth:tryIntegratedAuth + FBAppAuth:tryFacebookLogin + safariAuth:tryFacebookLogin + fallback:tryFallback + isReauthorize:isReauthorize]; +} + +- (void)authorizeWithPermissions:(NSArray*)permissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience + integratedAuth:(BOOL)tryIntegratedAuth + FBAppAuth:(BOOL)tryFBAppAuth + safariAuth:(BOOL)trySafariAuth + fallback:(BOOL)tryFallback + isReauthorize:(BOOL)isReauthorize { + // setup parameters for either the safari or inline login + NSMutableDictionary* params = [NSMutableDictionary dictionaryWithObjectsAndKeys: + self.appID, FBLoginUXClientID, + FBLoginUXUserAgent, FBLoginUXType, + FBRedirectURL, FBLoginUXRedirectURI, + FBLoginUXTouch, FBLoginUXDisplay, + FBLoginUXIOS, FBLoginUXSDK, + nil]; + + NSString *loginDialogURL = [FBDialogBaseURL stringByAppendingString:FBLoginDialogMethod]; + + if (permissions != nil) { + NSString* scope = [permissions componentsJoinedByString:@","]; + [params setValue:scope forKey:@"scope"]; + } + + if (_urlSchemeSuffix) { + [params setValue:_urlSchemeSuffix forKey:@"local_client_id"]; + } + + // To avoid surprises, delete any cookies we currently have. + [FBSession deleteFacebookCookies]; + + // we prefer OS-integrated Facebook login if supported by the device + // attempt to open an account store with the type Facebook; and if successful authorize + // using the OS + BOOL didAuthNWithSystemAccount = NO; + + id accountStore = nil; + id accountTypeFB = nil; + // do we want and have the ability to attempt integrated authn + if (tryIntegratedAuth && + (!isReauthorize || _isOSIntegratedFacebookLoginToken) && + (accountStore = [[[NSClassFromString(@"ACAccountStore") alloc] init] autorelease]) && + (accountTypeFB = [accountStore accountTypeWithAccountTypeIdentifier:@"com.apple.facebook"])) { + + // looks like we will get to attempt a login with integrated authn + didAuthNWithSystemAccount = YES; + + [self authorizeUsingSystemAccountStore:accountStore + accountType:accountTypeFB + permissions:permissions + defaultAudience:defaultAudience + isReauthorize:isReauthorize]; + } + + // if the device is running a version of iOS that supports multitasking, + // try to obtain the access token from the Facebook app installed + // on the device. + // If the Facebook app isn't installed or it doesn't support + // the fbauth:// URL scheme, fall back on Safari for obtaining the access token. + // This minimizes the chance that the user will have to enter his or + // her credentials in order to authorize the application. + UIDevice *device = [UIDevice currentDevice]; + if (!didAuthNWithSystemAccount && + [device respondsToSelector:@selector(isMultitaskingSupported)] && + [device isMultitaskingSupported] && + !TEST_DISABLE_MULTITASKING_LOGIN) { + if (tryFBAppAuth && + !TEST_DISABLE_FACEBOOKLOGIN) { + NSString *scheme = FBAuthURLScheme; + if (_urlSchemeSuffix) { + scheme = [scheme stringByAppendingString:@"2"]; + } + NSString *urlPrefix = [NSString stringWithFormat:@"%@://%@", scheme, FBAuthURLPath]; + NSString *fbAppUrl = [FBRequest serializeURL:urlPrefix params:params]; + + _loginTypeOfPendingOpenUrlCallback = FBSessionLoginTypeFacebookApplication; + didAuthNWithSystemAccount = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:fbAppUrl]]; + } + + if (trySafariAuth && !didAuthNWithSystemAccount) { + NSString *nextUrl = self.appBaseUrl; + [params setValue:nextUrl forKey:@"redirect_uri"]; + + NSString *fbAppUrl = [FBRequest serializeURL:loginDialogURL params:params]; + _loginTypeOfPendingOpenUrlCallback = FBSessionLoginTypeFacebookViaSafari; + didAuthNWithSystemAccount = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:fbAppUrl]]; + } + //In case openURL failed, make sure we don't still expect a openURL callback. + if (!didAuthNWithSystemAccount){ + _loginTypeOfPendingOpenUrlCallback = FBSessionLoginTypeNone; + } + } + + // If single sign-on failed, see if we should attempt to fallback + if (!didAuthNWithSystemAccount) { + if (tryFallback) { + // open an inline login dialog. This will require the user to enter his or her credentials. + self.loginDialog = [[[FBLoginDialog alloc] initWithURL:loginDialogURL + loginParams:params + delegate:self] + autorelease]; + [self.loginDialog show]; + } else { + // Can't fallback and Facebook Login failed, so transition to an error state + NSError *error = [FBSession errorLoginFailedWithReason:FBErrorLoginFailedReasonInlineNotCancelledValue + errorCode:nil + innerError:nil]; + + // state transition, and call the handler if there is one + [self transitionAndCallHandlerWithState:FBSessionStateClosedLoginFailed + error:error + token:nil + expirationDate:nil + shouldCache:NO + loginType:FBSessionLoginTypeNone]; + } + } +} + +- (void)authorizeUsingSystemAccountStore:(ACAccountStore*)accountStore + accountType:(ACAccountType*)accountType + permissions:(NSArray*)permissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience + isReauthorize:(BOOL)isReauthorize { + + // app may be asking for nothing, but we will always have an array here + NSArray *permissionsToUse = permissions ? permissions : [NSArray array]; + if ([FBSession areAllPermissionsReadPermissions:permissions]) { + // If we have only read permissions being requested, ensure that basic info + // is among the permissions requested. + permissionsToUse = [FBSession addBasicInfoPermission:permissionsToUse]; + } + + NSString *audience; + switch (defaultAudience) { + case FBSessionDefaultAudienceOnlyMe: + audience = ACFacebookAudienceOnlyMe; + break; + case FBSessionDefaultAudienceFriends: + audience = ACFacebookAudienceFriends; + break; + case FBSessionDefaultAudienceEveryone: + audience = ACFacebookAudienceEveryone; + break; + default: + audience = nil; + } + + // no publish_* permissions are permitted with a nil audience + if (!audience && isReauthorize) { + for (NSString *p in permissions) { + if ([p hasPrefix:@"publish"]) { + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBSession: One or more publish permission was requested " + @"without specifying an audience; use FBSessionDefaultAudienceJustMe, " + @"FBSessionDefaultAudienceFriends, or FBSessionDefaultAudienceEveryone" + userInfo:nil] + raise]; + } + } + } + + // construct access options + NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: + self.appID, ACFacebookAppIdKey, + permissionsToUse, ACFacebookPermissionsKey, + audience, ACFacebookAudienceKey, // must end on this key/value due to audience possibly being nil + nil]; + + // we will attempt an iOS integrated facebook login + [accountStore requestAccessToAccountsWithType:accountType + options:options + completion:^(BOOL granted, NSError *error) { + FBConditionalLog(granted || error.code != ACErrorPermissionDenied || + [error.description rangeOfString: + @"remote_app_id does not match stored id"].location == NSNotFound, + @"System authorization failed:'%@'. This may be caused by a mismatch between" + @" the bundle identifier and your app configuration on the server" + @" at developers.facebook.com/apps.", + error.localizedDescription); + + // this means the user has not signed-on to Facebook via the OS + BOOL isUntosedDevice = (!granted && error.code == ACErrorAccountNotFound); + + // requestAccessToAccountsWithType:options:completion: completes on an + // arbitrary thread; let's process this back on our main thread + dispatch_async( dispatch_get_main_queue(), ^{ + NSString *oauthToken = nil; + if (granted) { + NSArray *fbAccounts = [accountStore accountsWithAccountType:accountType]; + id account = [fbAccounts objectAtIndex:0]; + id credential = [account credential]; + + oauthToken = [credential oauthToken]; + } + + // initial auth case + if (!isReauthorize) { + if (oauthToken) { + _isFacebookLoginToken = YES; + _isOSIntegratedFacebookLoginToken = YES; + + // we received a token just now + self.refreshDate = [NSDate date]; + + // set token and date, state transition, and call the handler if there is one + [self transitionAndCallHandlerWithState:FBSessionStateOpen + error:nil + token:oauthToken + // BUG: we need a means for fetching the expiration date of the token + expirationDate:[NSDate distantFuture] + shouldCache:YES + loginType:FBSessionLoginTypeSystemAccount]; + } else if (isUntosedDevice) { + // even when OS integrated auth is possible we use native-app/safari + // login if the user has not signed on to Facebook via the OS + [self authorizeWithPermissions:permissions + defaultAudience:defaultAudience + integratedAuth:NO + FBAppAuth:YES + safariAuth:YES + fallback:YES + isReauthorize:NO]; + } else { + // create an error object with additional info regarding failed login + NSError *err = [FBSession errorLoginFailedWithReason:FBErrorLoginFailedReason + errorCode:nil + innerError:error]; + + // state transition, and call the handler if there is one + [self transitionAndCallHandlerWithState:FBSessionStateClosedLoginFailed + error:err + token:nil + expirationDate:nil + shouldCache:NO + loginType:FBSessionLoginTypeNone]; + } + } else { // reauth case + if (oauthToken) { + // union the requested permissions with the already granted permissions + NSMutableSet *set = [NSMutableSet setWithArray:self.permissions]; + [set addObjectsFromArray:permissions]; + + // complete the operation: success + [self completeReauthorizeWithAccessToken:oauthToken + expirationDate:[NSDate distantFuture] + permissions:[set allObjects]]; + } else { + // no token in this case implies that the user cancelled the permissions upgrade + NSError *error = [FBSession errorLoginFailedWithReason:FBErrorReauthorizeFailedReasonUserCancelled + errorCode:nil + innerError:nil]; + // complete the operation: failed + [self callReauthorizeHandlerAndClearState:error]; + + // if we made it this far into the reauth case with an untosed device, then + // it is time to invalidate the session + if (isUntosedDevice) { + [self closeAndClearTokenInformation]; + } + } + } + }); + }]; + +} + +- (BOOL)handleOpenURLPreOpen:(NSDictionary*)parameters + accessToken:(NSString*)accessToken + loginType:(FBSessionLoginType)loginType { + // if the URL doesn't contain the access token, an error has occurred. + if (!accessToken) { + NSString *errorReason = [parameters objectForKey:@"error"]; + + // if the error response indicates that we should try again using Safari, open + // the authorization dialog in Safari. + if (errorReason && [errorReason isEqualToString:@"service_disabled_use_browser"]) { + [self authorizeWithPermissions:self.permissions + defaultAudience:_defaultDefaultAudience + integratedAuth:NO + FBAppAuth:NO + safariAuth:YES + fallback:NO + isReauthorize:NO]; + return YES; + } + + // if the error response indicates that we should try the authorization flow + // in an inline dialog, do that. + if (errorReason && [errorReason isEqualToString:@"service_disabled"]) { + [self authorizeWithPermissions:self.permissions + defaultAudience:_defaultDefaultAudience + integratedAuth:NO + FBAppAuth:NO + safariAuth:NO + fallback:NO + isReauthorize:NO]; + return YES; + } + + // the facebook app may return an error_code parameter in case it + // encounters a UIWebViewDelegate error + NSString *errorCode = [parameters objectForKey:@"error_code"]; + + // create an error object with additional info regarding failed login + NSError *error = [FBSession errorLoginFailedWithReason:errorReason + errorCode:errorCode + innerError:nil]; + + // state transition, and call the handler if there is one + [self transitionAndCallHandlerWithState:FBSessionStateClosedLoginFailed + error:error + token:nil + expirationDate:nil + shouldCache:NO + loginType:FBSessionLoginTypeNone]; + } else { + + // we have an access token, so parse the expiration date. + NSString *expTime = [parameters objectForKey:@"expires_in"]; + NSDate *expirationDate = [FBSession expirationDateFromExpirationTimeString:expTime]; + if (!expirationDate) { + expirationDate = [NSDate distantFuture]; + } + + _isFacebookLoginToken = YES; + _isOSIntegratedFacebookLoginToken = NO; + + // we received a token just now + self.refreshDate = [NSDate date]; + + // set token and date, state transition, and call the handler if there is one + [self transitionAndCallHandlerWithState:FBSessionStateOpen + error:nil + token:accessToken + expirationDate:expirationDate + shouldCache:YES + loginType:loginType]; + } + return YES; +} + +- (BOOL)handleOpenURLReauthorize:(NSDictionary*)parameters + accessToken:(NSString*)accessToken { + // if the URL doesn't contain the access token, an error has occurred. + if (!accessToken) { + // no token in this case implies that the user cancelled the permissions upgrade + NSError *error = [FBSession errorLoginFailedWithReason:FBErrorReauthorizeFailedReasonUserCancelled + errorCode:nil + innerError:nil]; + [self callReauthorizeHandlerAndClearState:error]; + } else { + + // we have an access token, so parse the expiration date. + NSString *expTime = [parameters objectForKey:@"expires_in"]; + NSDate *expirationDate = [FBSession expirationDateFromExpirationTimeString:expTime]; + if (!expirationDate) { + expirationDate = [NSDate distantFuture]; + } + + // now we are going to kick-off a batch request, where we confirm that the new token + // refers to the same fbid as the old, and if so we will succeed the reauthorize call + FBRequest *requestSessionMe = [FBRequest requestForGraphPath:@"me"]; + [requestSessionMe setSession:self]; + FBRequest *requestNewTokenMe = [[[FBRequest alloc] initWithSession:nil + graphPath:@"me" + parameters:[NSDictionary dictionaryWithObjectsAndKeys: + accessToken, @"access_token", + nil] + HTTPMethod:nil] + autorelease]; + + FBRequest *requestPermissions = [FBRequest requestForGraphPath:@"me/permissions"]; + [requestPermissions setSession:self]; + + // we create a block here with related state -- which will be the main handler block for all + // three requests -- wrapped by smaller blocks to provide context + + // we will use these to compare fbid's + __block id fbid = nil; + __block id fbid2 = nil; + __block id permissionsRefreshed = nil; + // and this to assure we notice when we have been called three times + __block int callsPending = 3; + + void (^handleBatch)(id,id) = [^(id user, + id permissions) { + + // here we accumulate state from the various callbacks + if (user && !fbid) { + fbid = [[user objectForKey:@"id"] retain]; + } else if (user && !fbid2) { + fbid2 = [[user objectForKey:@"id"] retain]; + } else if (permissions) { + permissionsRefreshed = [permissions retain]; + } + + // if this was our last call, then complete the operation + if (!--callsPending) { + if ([fbid isEqual:fbid2]) { + id newPermissions = [[permissionsRefreshed objectAtIndex:0] allKeys]; + if (![newPermissions isKindOfClass:[NSArray class]]) { + newPermissions = nil; + } + [self completeReauthorizeWithAccessToken:accessToken + expirationDate:expirationDate + permissions:newPermissions]; + } else { + // no we don't have matching FBIDs, then we fail on these grounds + NSError *error = [FBSession errorLoginFailedWithReason:FBErrorReauthorizeFailedReasonWrongUser + errorCode:nil + innerError:nil]; + [self callReauthorizeHandlerAndClearState:error]; + } + + // because these are __block, we manually handle their lifetime + [fbid release]; + [fbid2 release]; + [permissionsRefreshed release]; + } + } copy]; + + FBRequestConnection *connection = [[[FBRequestConnection alloc] init] autorelease]; + [connection addRequest:requestSessionMe + completionHandler:^(FBRequestConnection *connection, id user, NSError *error) { + handleBatch(user, nil); + }]; + + [connection addRequest:requestNewTokenMe + completionHandler:^(FBRequestConnection *connection, id user, NSError *error) { + handleBatch(user, nil); + }]; + + [connection addRequest:requestPermissions + completionHandler:^(FBRequestConnection *connection, id result, NSError *error) { + handleBatch(nil, [result objectForKey:@"data"]); + }]; + + [connection start]; + } + return YES; +} + +- (void)reauthorizeWithPermissions:(NSArray*)permissions + isRead:(BOOL)isRead + behavior:(FBSessionLoginBehavior)behavior + defaultAudience:(FBSessionDefaultAudience)audience + completionHandler:(FBSessionReauthorizeResultHandler)handler { + + if (!self.isOpen) { + // session must be open in order to reauthorize + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBSession: an attempt was made reauthorize permissions on an unopened session" + userInfo:nil] + raise]; + } + + if (self.reauthorizeHandler) { + // block must be cleared (meaning it has been called back) before a reauthorize can happen again + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBSession: It is not valid to reauthorize while a previous " + @"reauthorize call has not yet completed." + userInfo:nil] + raise]; + } + + // is everything in good order argument-wise? + [FBSession validateRequestForPermissions:permissions + defaultAudience:audience + allowSystemAccount:behavior == FBSessionLoginBehaviorUseSystemAccountIfPresent + isRead:isRead]; + + // setup handler and permissions and perform the actual reauthorize + self.reauthorizePermissions = permissions; + self.reauthorizeHandler = handler; + [self authorizeWithPermissions:permissions + behavior:behavior + defaultAudience:audience + isReauthorize:YES]; +} + +- (void)completeReauthorizeWithAccessToken:(NSString*)accessToken + expirationDate:(NSDate*)expirationDate + permissions:(NSArray*)permissions { + // we received a token just now + self.refreshDate = [NSDate date]; + + if (permissions) { + self.permissions = permissions; + } + + // set token and date, state transition, and call the handler if there is one + [self transitionAndCallHandlerWithState:FBSessionStateOpenTokenExtended + error:nil + token:accessToken + expirationDate:expirationDate + shouldCache:YES + loginType:FBSessionLoginTypeNone]; + + // no error, ack a completed permission upgrade + [self callReauthorizeHandlerAndClearState:nil]; +} + +- (void)refreshAccessToken:(NSString*)token + expirationDate:(NSDate*)expireDate { + // refreshing now + self.refreshDate = [NSDate date]; + + // refresh token and date, state transition, and call the handler if there is one + [self transitionAndCallHandlerWithState:FBSessionStateOpenTokenExtended + error:nil + token:token ? token : self.accessToken + expirationDate:expireDate + shouldCache:YES + loginType:FBSessionLoginTypeNone]; +} + +- (BOOL)shouldExtendAccessToken { + BOOL result = NO; + NSDate *now = [NSDate date]; + if (self.isOpen && + _isFacebookLoginToken && + [now timeIntervalSinceDate:self.attemptedRefreshDate] > FBTokenRetryExtendSeconds && + [now timeIntervalSinceDate:self.refreshDate] > FBTokenExtendThresholdSeconds) { + result = YES; + self.attemptedRefreshDate = now; + } + return result; +} + +// core handler for inline UX flow +- (void)fbDialogLogin:(NSString *)accessToken expirationDate:(NSDate *)expirationDate { + // no reason to keep this object + self.loginDialog = nil; + + // though this is not Facebook Login our policy is to cache the refresh date if we have it + self.refreshDate = [NSDate date]; + + // set token and date, state transition, and call the handler if there is one + [self transitionAndCallHandlerWithState:FBSessionStateOpen + error:nil + token:accessToken + expirationDate:expirationDate + shouldCache:YES + loginType:FBSessionLoginTypeWebView]; +} + +// core handler for inline UX flow +- (void)fbDialogNotLogin:(BOOL)cancelled { + // done with this + self.loginDialog = nil; + + // manually set the reason string for inline dialog + NSString *reason = + cancelled ? FBErrorLoginFailedReasonInlineCancelledValue : FBErrorLoginFailedReasonInlineNotCancelledValue; + + // create an error object with additional info regarding failed login + NSError *error = [FBSession errorLoginFailedWithReason:reason + errorCode:nil + innerError:nil]; + + // state transition, and call the handler if there is one + [self transitionAndCallHandlerWithState:FBSessionStateClosedLoginFailed + error:error + token:nil + expirationDate:nil + shouldCache:NO + loginType:FBSessionLoginTypeNone]; +} + +// private helpers + +// helper to wrap-up handler callback and state-change +- (void)transitionAndCallHandlerWithState:(FBSessionState)status + error:(NSError*)error + token:(NSString*)token + expirationDate:(NSDate*)date + shouldCache:(BOOL)shouldCache + loginType:(FBSessionLoginType)loginType { + + + // lets get the state transition out of the way + BOOL didTransition = [self transitionToState:status + andUpdateToken:token + andExpirationDate:date + shouldCache:shouldCache + loginType:loginType]; + + // if we are given a handler, we promise to call it once per transition from open to close + + // release the object's count on the handler, but copy (not retain, since it is a block) + // a stack ref to use as our callback outside of the lock + FBSessionStateHandler handler = [self.loginHandler retain]; + + // the moment we transition to a terminal state, we release our handlers, and possibly fail-call reauthorize + if (didTransition && FB_ISSESSIONSTATETERMINAL(self.state)) { + self.loginHandler = nil; + + NSError *error = [FBSession errorLoginFailedWithReason:FBErrorReauthorizeFailedReasonSessionClosed + errorCode:nil + innerError:nil]; + [self callReauthorizeHandlerAndClearState:error]; + } + + // if we have a handler, call it and release our + // final retain on the handler + if (handler) { + @try { + // unsuccessful transitions don't change state and don't propagate the error object + handler(self, + self.state, + didTransition ? error : nil); + } + @finally { + // now release our stack reference + [handler release]; + } + } +} + +- (void)callReauthorizeHandlerAndClearState:(NSError*)error { + + // clear state and call handler + FBSessionReauthorizeResultHandler reauthorizeHandler = [self.reauthorizeHandler retain]; + self.reauthorizeHandler = nil; + self.reauthorizePermissions = nil; + if (reauthorizeHandler) { + reauthorizeHandler(self, error); + } + [reauthorizeHandler release]; +} + +- (NSString *)appBaseUrl { + return [NSString stringWithFormat:@"fb%@%@://authorize", + self.appID, + self.urlSchemeSuffix]; +} + ++ (NSError*)errorLoginFailedWithReason:(NSString*)errorReason + errorCode:(NSString*)errorCode + innerError:(NSError*)innerError { + // capture reason and nested code as user info + NSMutableDictionary* userinfo = [[NSMutableDictionary alloc] init]; + if (errorReason) { + [userinfo setObject:errorReason + forKey:FBErrorLoginFailedReason]; + } + if (errorCode) { + [userinfo setObject:errorCode + forKey:FBErrorLoginFailedOriginalErrorCode]; + } + if (innerError) { + [userinfo setObject:innerError + forKey:FBErrorInnerErrorKey]; + } + + // create error object + NSError *err = [NSError errorWithDomain:FacebookSDKDomain + code:FBErrorLoginFailedOrCancelled + userInfo:userinfo]; + [userinfo release]; + return err; +} + ++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { + // these properties must manually notify for KVO + if ([key isEqualToString:FBisOpenPropertyName] || + [key isEqualToString:FBaccessTokenPropertyName] || + [key isEqualToString:FBexpirationDatePropertyName] || + [key isEqualToString:FBstatusPropertyName]) { + return NO; + } else { + return [super automaticallyNotifiesObserversForKey:key]; + } +} + ++ (BOOL)areRequiredPermissions:(NSArray*)requiredPermissions + aSubsetOfPermissions:(NSArray*)cachedPermissions { + NSSet *required = [NSSet setWithArray:requiredPermissions]; + NSSet *cached = [NSSet setWithArray:cachedPermissions]; + return [required isSubsetOfSet:cached]; +} + +#pragma mark - +#pragma mark Internal members + ++ (BOOL)openActiveSessionWithPermissions:(NSArray*)permissions + allowLoginUI:(BOOL)allowLoginUI + allowSystemAccount:(BOOL)allowSystemAccount + isRead:(BOOL)isRead + defaultAudience:(FBSessionDefaultAudience)defaultAudience + completionHandler:(FBSessionStateHandler)handler { + // is everything in good order? + [FBSession validateRequestForPermissions:permissions + defaultAudience:defaultAudience + allowSystemAccount:allowSystemAccount + isRead:isRead]; + BOOL result = NO; + FBSession *session = [[[FBSession alloc] initWithAppID:nil + permissions:permissions + defaultAudience:defaultAudience + urlSchemeSuffix:nil + tokenCacheStrategy:nil] + autorelease]; + if (allowLoginUI || session.state == FBSessionStateCreatedTokenLoaded) { + [FBSession setActiveSession:session]; + // we open after the fact, in order to avoid overlapping close + // and open handler calls for blocks + FBSessionLoginBehavior howToBehave = allowSystemAccount ? + FBSessionLoginBehaviorUseSystemAccountIfPresent : + FBSessionLoginBehaviorWithFallbackToWebView; + [session openWithBehavior:howToBehave + completionHandler:handler]; + result = session.isOpen; + } + return result; +} + ++ (FBSession*)activeSessionIfOpen { + if (g_activeSession.isOpen) { + return FBSession.activeSession; + } + return nil; +} + ++ (void)validateRequestForPermissions:(NSArray*)permissions + defaultAudience:(FBSessionDefaultAudience)defaultAudience + allowSystemAccount:(BOOL)allowSystemAccount + isRead:(BOOL)isRead { + // validate audience argument + if ([permissions count]) { + if (allowSystemAccount && !isRead) { + switch (defaultAudience) { + case FBSessionDefaultAudienceEveryone: + case FBSessionDefaultAudienceFriends: + case FBSessionDefaultAudienceOnlyMe: + break; + default: + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBSession: Publish permissions were requested " + @"without specifying an audience; use FBSessionDefaultAudienceJustMe, " + @"FBSessionDefaultAudienceFriends, or FBSessionDefaultAudienceEveryone" + userInfo:nil] + raise]; + break; + } + } + // log unexpected permissions, and throw on read w/publish permissions + if (allowSystemAccount && + [FBSession logIfFoundUnexpectedPermissions:permissions isRead:isRead] && + isRead) { + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBSession: Publish or manage permissions are not permited to " + @"to be requested with read permissions." + userInfo:nil] + raise]; + } + } +} + ++ (BOOL)isPublishPermission:(NSString*)permission { + return [permission hasPrefix:@"publish"] || + [permission hasPrefix:@"manage"] || + [permission isEqualToString:@"ads_management"] || + [permission isEqualToString:@"create_event"] || + [permission isEqualToString:@"rsvp_event"]; +} + ++ (BOOL)areAllPermissionsReadPermissions:(NSArray*)permissions { + for (NSString *permission in permissions) { + if ([self isPublishPermission:permission]) { + return NO; + } + } + return YES; +} + ++ (BOOL)logIfFoundUnexpectedPermissions:(NSArray*)permissions + isRead:(BOOL)isRead { + BOOL publishPermissionFound = NO; + BOOL readPermissionFound = NO; + BOOL result = NO; + for (NSString *p in permissions) { + if ([self isPublishPermission:p]) { + publishPermissionFound = YES; + } else { + readPermissionFound = YES; + } + + // If we've found one of each we can stop looking. + if (publishPermissionFound && readPermissionFound) { + break; + } + } + + if (!isRead && readPermissionFound) { + FBConditionalLog(NO, @"FBSession: a permission request for publish or manage permissions contains unexpected read permissions"); + result = YES; + } + if (isRead && publishPermissionFound) { + FBConditionalLog(NO, @"FBSession: a permission request for read permissions contains unexpected publish or manage permissions"); + result = YES; + } + + return result; +} + ++ (NSArray*)addBasicInfoPermission:(NSArray*)permissions { + // When specifying read permissions, be sure basic info is included; "email" is used + // as a proxy for basic info permission. + for (NSString *p in permissions) { + if ([p isEqualToString:@"email"]) { + // Already requested, don't need to add it again. + return permissions; + } + } + + NSMutableArray *newPermissions = [NSMutableArray arrayWithArray:permissions]; + [newPermissions addObject:@"email"]; + return newPermissions; +} + ++ (void)deleteFacebookCookies { + NSHTTPCookieStorage* cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + NSArray* facebookCookies = [cookies cookiesForURL: + [NSURL URLWithString:@"http://login." FB_BASE_URL]]; + + for (NSHTTPCookie* cookie in facebookCookies) { + [cookies deleteCookie:cookie]; + } +} + ++ (NSDate*)expirationDateFromExpirationTimeString:(NSString*)expirationTime { + NSDate *expirationDate = nil; + if (expirationTime != nil) { + int expValue = [expirationTime intValue]; + if (expValue != 0) { + expirationDate = [NSDate dateWithTimeIntervalSinceNow:expValue]; + } + } + return expirationDate; +} + +- (void)closeAndClearTokenInformation:(NSError*) error { + NSAssert(self.affinitizedThread == [NSThread currentThread], @"FBSession: should only be used from a single thread"); + + [[FBDataDiskCache sharedCache] removeDataForSession:self]; + [self.tokenCachingStrategy clearToken]; + + // If we are not already in a terminal state, go to Closed. + if (!FB_ISSESSIONSTATETERMINAL(self.state)) { + [self transitionAndCallHandlerWithState:FBSessionStateClosed + error:error + token:nil + expirationDate:nil + shouldCache:NO + loginType:FBSessionLoginTypeNone]; + } +} + +#pragma mark - +#pragma mark Debugging helpers + ++ (NSString *)sessionStateDescription:(FBSessionState)sessionState { + NSString *stateDescription = nil; + switch (sessionState) { + case FBSessionStateCreated: + stateDescription = @"FBSessionStateCreated"; + break; + case FBSessionStateCreatedTokenLoaded: + stateDescription = @"FBSessionStateCreatedTokenLoaded"; + break; + case FBSessionStateCreatedOpening: + stateDescription = @"FBSessionStateCreatedOpening"; + break; + case FBSessionStateOpen: + stateDescription = @"FBSessionStateOpen"; + break; + case FBSessionStateOpenTokenExtended: + stateDescription = @"FBSessionStateOpenTokenExtended"; + break; + case FBSessionStateClosedLoginFailed: + stateDescription = @"FBSessionStateClosedLoginFailed"; + break; + case FBSessionStateClosed: + stateDescription = @"FBSessionStateClosed"; + break; + default: + stateDescription = @"[Unknown]"; + break; + } + + return stateDescription; +} + + +- (NSString*)description { + NSString *stateDescription = [FBSession sessionStateDescription:self.state]; + return [NSString stringWithFormat:@"<%@: %p, state: %@, loginHandler: %p, appID: %@, urlSchemeSuffix: %@, tokenCachingStrategy:%@, expirationDate: %@, refreshDate: %@, attemptedRefreshDate: %@, permissions:%@>", + NSStringFromClass([self class]), + self, + stateDescription, + self.loginHandler, + self.appID, + self.urlSchemeSuffix, + [self.tokenCachingStrategy description], + self.expirationDate, + self.refreshDate, + self.attemptedRefreshDate, + [self.permissions description]]; +} + +#pragma mark - + +@end diff --git a/src/ios/facebook/FBSessionManualTokenCachingStrategy.h b/src/ios/facebook/FBSessionManualTokenCachingStrategy.h new file mode 100644 index 000000000..e4d7f368b --- /dev/null +++ b/src/ios/facebook/FBSessionManualTokenCachingStrategy.h @@ -0,0 +1,32 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBSessionTokenCachingStrategy.h" + +// FBSessionManualTokenCachingStrategy +// +// Summary: +// Internal use only, this class enables migration logic for the Facebook class, by providing +// a means to directly provide the access token to a FBSession object +// +@interface FBSessionManualTokenCachingStrategy : FBSessionTokenCachingStrategy + +// set the properties before instantiating the FBSession object in order to seed a token +@property (readwrite, copy) NSString* accessToken; +@property (readwrite, copy) NSDate* expirationDate; + +@end + diff --git a/src/ios/facebook/FBSessionManualTokenCachingStrategy.m b/src/ios/facebook/FBSessionManualTokenCachingStrategy.m new file mode 100644 index 000000000..320137053 --- /dev/null +++ b/src/ios/facebook/FBSessionManualTokenCachingStrategy.m @@ -0,0 +1,50 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBSessionManualTokenCachingStrategy.h" + + +@implementation FBSessionManualTokenCachingStrategy + +@synthesize accessToken = _accessToken, + expirationDate = _expirationDate; + +- (void)dealloc { + [_accessToken release]; + [_expirationDate release]; + [super dealloc]; +} + +- (void)cacheTokenInformation:(NSDictionary*)tokenInformation { + self.accessToken = [tokenInformation objectForKey:FBTokenInformationTokenKey]; + self.expirationDate = [tokenInformation objectForKey:FBTokenInformationExpirationDateKey]; +} + +- (NSDictionary*)fetchTokenInformation; +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + self.accessToken, FBTokenInformationTokenKey, + self.expirationDate, FBTokenInformationExpirationDateKey, + nil]; +} + +- (void)clearToken +{ + self.accessToken = nil; + self.expirationDate = nil; +} + +@end \ No newline at end of file diff --git a/src/ios/facebook/FBSessionTokenCachingStrategy.h b/src/ios/facebook/FBSessionTokenCachingStrategy.h new file mode 100644 index 000000000..ded1a6829 --- /dev/null +++ b/src/ios/facebook/FBSessionTokenCachingStrategy.h @@ -0,0 +1,121 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +/*! + @class + + @abstract + The `FBSessionTokenCachingStrategy` class is responsible for persisting and retrieving cached data related to + an object, including the user's Facebook access token. + + @discussion + `FBSessionTokenCachingStrategy` is designed to be instantiated directly or used as a base class. Usually default + token caching behavior is sufficient, and you do not need to interface directly with `FBSessionTokenCachingStrategy` objects. + However, if you need to control where or how `FBSession` information is cached, then you may take one of two approaches. + + The first and simplest approach is to instantiate an instance of `FBSessionTokenCachingStrategy`, and then pass + the instance to `FBSession` class' `init` method. This enables your application to control the key name used in + `NSUserDefaults` to store session information. You may consider this approach if you plan to cache session information + for multiple users. + + The second and more advanced approached is to derive a custom class from `FBSessionTokenCachingStrategy`, which will + be responsible for caching behavior of your application. This approach is useful if you need to change where the + information is cached, for example if you prefer to use the filesystem or make a network connection to fetch and + persist cached tokens. Inheritors should override the cacheTokenInformation, fetchTokenInformation, and clearToken methods. + Doing this enables your application to implement any token caching scheme, including no caching at all. + + Direct use of `FBSessionTokenCachingStrategy`is an advanced technique. Most applications use objects without + passing an `FBSessionTokenCachingStrategy`, which yields default caching to `NSUserDefaults`. + */ +@interface FBSessionTokenCachingStrategy : NSObject + +/*! + @abstract Initializes and returns an instance + */ +- (id)init; + +/*! + @abstract + Initializes and returns an instance + + @param tokenInformationKeyName Specifies a key name to use for cached token information in NSUserDefaults, nil + indicates a default value of @"FBAccessTokenInformationKey" + */ +- (id)initWithUserDefaultTokenInformationKeyName:(NSString*)tokenInformationKeyName; + +/*! + @abstract + Called by (and overridden by inheritors), in order to cache token information. + + @param tokenInformation Dictionary containing token information to be cached by the method + */ +- (void)cacheTokenInformation:(NSDictionary*)tokenInformation; + +/*! + @abstract + Called by (and overridden by inheritors), in order to fetch cached token information + + @discussion + An overriding implementation should only return a token if it + can also return an expiration date, otherwise return nil + */ +- (NSDictionary*)fetchTokenInformation; + +/*! + @abstract + Called by (and overridden by inheritors), in order delete any cached information for the current token + */ +- (void)clearToken; + +/*! + @abstract + Helper function called by the SDK as well as apps, in order to fetch the default strategy instance. + */ ++ (FBSessionTokenCachingStrategy*)defaultInstance; + +/*! + @abstract + Helper function called by the SDK as well as application code, used to determine whether a given dictionary + contains the minimum token information usable by the . + + @param tokenInformation Dictionary containing token information to be validated + */ ++ (BOOL)isValidTokenInformation:(NSDictionary*)tokenInformation; + +@end + +// The key to use with token information dictionaries to get and set the token value +extern NSString *const FBTokenInformationTokenKey; + +// The to use with token information dictionaries to get and set the expiration date +extern NSString *const FBTokenInformationExpirationDateKey; + +// The to use with token information dictionaries to get and set the refresh date +extern NSString *const FBTokenInformationRefreshDateKey; + +// The key to use with token information dictionaries to get the related user's fbid +extern NSString *const FBTokenInformationUserFBIDKey; + +// The key to use with token information dictionaries to determine whether the token was fetched via Facebook Login +extern NSString *const FBTokenInformationIsFacebookLoginKey; + +// The key to use with token information dictionaries to determine whether the token was fetched via the OS +extern NSString *const FBTokenInformationLoginTypeLoginKey; + +// The key to use with token information dictionaries to get the latest known permissions +extern NSString *const FBTokenInformationPermissionsKey; \ No newline at end of file diff --git a/src/ios/facebook/FBSessionTokenCachingStrategy.m b/src/ios/facebook/FBSessionTokenCachingStrategy.m new file mode 100644 index 000000000..d798c9fb9 --- /dev/null +++ b/src/ios/facebook/FBSessionTokenCachingStrategy.m @@ -0,0 +1,103 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBSessionTokenCachingStrategy.h" + +// const strings +static NSString *const FBAccessTokenInformationKeyName = @"FBAccessTokenInformationKey"; + +NSString *const FBTokenInformationTokenKey = @"com.facebook.sdk:TokenInformationTokenKey"; +NSString *const FBTokenInformationExpirationDateKey = @"com.facebook.sdk:TokenInformationExpirationDateKey"; +NSString *const FBTokenInformationRefreshDateKey = @"com.facebook.sdk:TokenInformationRefreshDateKey"; +NSString *const FBTokenInformationUserFBIDKey = @"com.facebook.sdk:TokenInformationUserFBIDKey"; +NSString *const FBTokenInformationIsFacebookLoginKey = @"com.facebook.sdk:TokenInformationIsFacebookLoginKey"; +NSString *const FBTokenInformationLoginTypeLoginKey = @"com.facebook.sdk:TokenInformationLoginTypeLoginKey"; +NSString *const FBTokenInformationPermissionsKey = @"com.facebook.sdk:TokenInformationPermissionsKey"; + +@implementation FBSessionTokenCachingStrategy { + NSString *_accessTokenInformationKeyName; +} + +#pragma mark Lifecycle + +- (id)init { + return [self initWithUserDefaultTokenInformationKeyName:nil]; +} + +- (id)initWithUserDefaultTokenInformationKeyName:(NSString*)tokenInformationKeyName { + + self = [super init]; + if (self) { + // get-em + _accessTokenInformationKeyName = tokenInformationKeyName ? tokenInformationKeyName : FBAccessTokenInformationKeyName; + + // keep-em + [_accessTokenInformationKeyName retain]; + } + return self; +} + +- (void)dealloc { + // let-em go + [_accessTokenInformationKeyName release]; + [super dealloc]; +} + +#pragma mark - +#pragma mark Public Members + +- (void)cacheTokenInformation:(NSDictionary*)tokenInformation { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setObject:tokenInformation forKey:_accessTokenInformationKeyName]; + [defaults synchronize]; +} + +- (NSDictionary*)fetchTokenInformation { + // fetch values from defaults + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + return [defaults objectForKey:_accessTokenInformationKeyName]; +} + +- (void)clearToken { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults removeObjectForKey:_accessTokenInformationKeyName]; + [defaults synchronize]; +} + ++ (BOOL)isValidTokenInformation:(NSDictionary*)tokenInformation { + id token = [tokenInformation objectForKey:FBTokenInformationTokenKey]; + id expirationDate = [tokenInformation objectForKey:FBTokenInformationExpirationDateKey]; + return [token isKindOfClass:[NSString class]] && + ([token length] > 0) && + [expirationDate isKindOfClass:[NSDate class]]; +} + ++ (FBSessionTokenCachingStrategy*)defaultInstance { + // static state to assure a single default instance here + static FBSessionTokenCachingStrategy *sharedDefaultInstance = nil; + static dispatch_once_t onceToken; + + // assign once to the static, if called + dispatch_once(&onceToken, ^{ + sharedDefaultInstance = [[FBSessionTokenCachingStrategy alloc] init]; + }); + return sharedDefaultInstance; +} + + +#pragma mark - + +@end diff --git a/src/ios/facebook/FBSettings+Internal.h b/src/ios/facebook/FBSettings+Internal.h new file mode 100644 index 000000000..8ea155e0b --- /dev/null +++ b/src/ios/facebook/FBSettings+Internal.h @@ -0,0 +1,23 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBSettings.h" + +@interface FBSettings (Internal) + ++ (void)autoPublishInstall:(NSString *)appID; + +@end diff --git a/src/ios/facebook/FBSettings.h b/src/ios/facebook/FBSettings.h new file mode 100644 index 000000000..23bc1fb50 --- /dev/null +++ b/src/ios/facebook/FBSettings.h @@ -0,0 +1,84 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +/* + * Constants defining logging behavior. Use with <[FBSettings setLoggingBehavior]>. + */ + +/*! Log requests from FBRequest* classes */ +extern NSString *const FBLoggingBehaviorFBRequests; + +/*! Log requests from FBURLConnection* classes */ +extern NSString *const FBLoggingBehaviorFBURLConnections; + +/*! Include access token in logging. */ +extern NSString *const FBLoggingBehaviorAccessTokens; + +/*! Log session state transitions. */ +extern NSString *const FBLoggingBehaviorSessionStateTransitions; + +/*! Log performance characteristics */ +extern NSString *const FBLoggingBehaviorPerformanceCharacteristics; + +@interface FBSettings : NSObject + +/*! + @method + + @abstract Retrieve the current Facebook SDK logging behavior. + + */ ++ (NSSet *)loggingBehavior; + +/*! + @method + + @abstract Set the current Facebook SDK logging behavior. This should consist of strings defined as + constants with FBLogBehavior*, and can be constructed with [NSSet initWithObjects:]. + + @param loggingBehavior A set of strings indicating what information should be logged. + */ ++ (void)setLoggingBehavior:(NSSet *)loggingBehavior; + +/*! @abstract Retreive the current auto publish behavior. Defaults to YES. */ ++ (BOOL)shouldAutoPublishInstall; + +/*! + @method + + @abstract Sets whether the SDK will automatically publish an install to Facebook during first FBSession init + or on first network request to Facebook. + + @param autoPublishInstall If YES, automatically publish the install; if NO, do not. + */ ++ (void)setShouldAutoPublishInstall:(BOOL)autoPublishInstall; + +// For best results, call this function during app activation. +/*! + @method + + @abstract Manually publish an attributed install to the facebook graph. Use this method if you have disabled + auto publish and wish to manually send an install from your code. This method acquires the current attribution + id from the facebook application, queries the graph API to determine if the application has install + attribution enabled, publishes the id, and records success to avoid reporting more than once. + + @param appID A specific appID to publish an install for. If nil, uses [FBSession defaultAppID]. + */ ++ (void) publishInstall:(NSString *)appID; + +@end diff --git a/src/ios/facebook/FBSettings.m b/src/ios/facebook/FBSettings.m new file mode 100644 index 000000000..e86a0ac22 --- /dev/null +++ b/src/ios/facebook/FBSettings.m @@ -0,0 +1,160 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBRequest.h" +#import "FBSession.h" +#import "FBSettings.h" +#import "FBSettings+Internal.h" + +#import +#import + +NSString *const FBLoggingBehaviorFBRequests = @"fb_requests"; +NSString *const FBLoggingBehaviorFBURLConnections = @"fburl_connections"; +NSString *const FBLoggingBehaviorAccessTokens = @"include_access_tokens"; +NSString *const FBLoggingBehaviorSessionStateTransitions = @"state_transitions"; +NSString *const FBLoggingBehaviorPerformanceCharacteristics = @"perf_characteristics"; + +NSString *const FBLastAttributionPing = @"com.facebook.sdk:lastAttributionPing%@"; +NSString *const FBSupportsAttributionPath = @"%@?fields=supports_attribution"; +NSString *const FBPublishActivityPath = @"%@/activities"; +NSString *const FBMobileInstallEvent = @"MOBILE_APP_INSTALL"; +NSString *const FBAttributionPasteboard = @"fb_app_attribution"; +NSString *const FBSupportsAttribution = @"supports_attribution"; + +NSTimeInterval const FBPublishDelay = 0.1; + +@implementation FBSettings + +static NSSet *g_loggingBehavior; +static BOOL g_autoPublishInstall = YES; +static dispatch_once_t g_publishInstallOnceToken; + ++ (NSSet *)loggingBehavior { + return g_loggingBehavior; +} + ++ (void)setLoggingBehavior:(NSSet *)newValue { + [newValue retain]; + [g_loggingBehavior release]; + g_loggingBehavior = newValue; +} + ++ (BOOL)shouldAutoPublishInstall { + return g_autoPublishInstall; +} + ++ (void)setShouldAutoPublishInstall:(BOOL)newValue { + g_autoPublishInstall = newValue; +} + ++ (void)autoPublishInstall:(NSString *)appID { + if ([FBSettings shouldAutoPublishInstall]) { + dispatch_once(&g_publishInstallOnceToken, ^{ + // dispatch_once is great, but not re-entrant. Inside publishInstall we use FBRequest, which will + // cause this function to get invoked a second time. By scheduling the work, we can sidestep the problem. + [[FBSettings class] performSelector:@selector(publishInstall:) withObject:appID afterDelay:FBPublishDelay]; + }); + } +} + + +#pragma mark - +#pragma mark proto-activity publishing code + ++ (void)publishInstall:(NSString *)appID { + @try { + if (!appID) { + appID = [FBSession defaultAppID]; + } + + if (!appID) { + // if the appID is still nil, exit early. + return; + } + + // look for a previous ping & grab the facebook app's current attribution id. + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *pingKey = [NSString stringWithFormat:FBLastAttributionPing, appID, nil]; + NSDate *lastPing = [defaults objectForKey:pingKey]; + NSString *attributionID = [[UIPasteboard pasteboardWithName:FBAttributionPasteboard create:NO] string]; + + NSString *advertiserID = nil; + if ([ASIdentifierManager class]) { + ASIdentifierManager *manager = [ASIdentifierManager sharedManager]; + advertiserID = [[manager advertisingIdentifier] UUIDString]; + } + + if ((attributionID || advertiserID) && !lastPing) { + FBRequestHandler publishCompletionBlock = ^(FBRequestConnection *connection, + id result, + NSError *error) { + @try { + if (!error) { + // if server communication was successful, take note of the current time. + [defaults setObject:[NSDate date] forKey:pingKey]; + [defaults synchronize]; + } else { + // there was a problem. allow a repeat execution. + g_publishInstallOnceToken = 0; + } + } @catch (NSException *ex1) { + NSLog(@"Failure after install publish: %@", ex1.reason); + } + }; + + FBRequestHandler pingCompletionBlock = ^(FBRequestConnection *connection, + id result, + NSError *error) { + if (!error) { + @try { + if ([result respondsToSelector:@selector(objectForKey:)] && + [[result objectForKey:FBSupportsAttribution] boolValue]) { + // set up the HTTP POST to publish the attribution ID. + NSString *publishPath = [NSString stringWithFormat:FBPublishActivityPath, appID, nil]; + NSMutableDictionary *installActivity = [FBGraphObject graphObject]; + [installActivity setObject:FBMobileInstallEvent forKey:@"event"]; + + if (attributionID) { + [installActivity setObject:attributionID forKey:@"attribution"]; + } + if (advertiserID) { + [installActivity setObject:advertiserID forKey:@"advertiser_id"]; + } + + FBRequest *publishRequest = [[[FBRequest alloc] initForPostWithSession:nil graphPath:publishPath graphObject:installActivity] autorelease]; + [publishRequest startWithCompletionHandler:publishCompletionBlock]; + } else { + // the app has turned off install insights. prevent future attempts. + [defaults setObject:[NSDate date] forKey:pingKey]; + [defaults synchronize]; + } + } @catch (NSException *ex2) { + NSLog(@"Failure during install publish: %@", ex2.reason); + } + } + }; + + NSString *pingPath = [NSString stringWithFormat:FBSupportsAttributionPath, appID, nil]; + FBRequest *pingRequest = [[[FBRequest alloc] initWithSession:nil graphPath:pingPath] autorelease]; + [pingRequest startWithCompletionHandler:pingCompletionBlock]; + } + } @catch (NSException *ex3) { + NSLog(@"Failure before/during install ping: %@", ex3.reason); + } +} + +@end diff --git a/src/ios/facebook/FBTestSession+Internal.h b/src/ios/facebook/FBTestSession+Internal.h new file mode 100644 index 000000000..4a1d95a1e --- /dev/null +++ b/src/ios/facebook/FBTestSession+Internal.h @@ -0,0 +1,25 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBSession.h" + +@interface FBTestSession (Internal) + +// Can be used during testing to force a request for an access token refresh. This affects only the next +// connection, when this flag is reset. +@property (readwrite) BOOL forceAccessTokenRefresh; + +@end diff --git a/src/ios/facebook/FBTestSession.h b/src/ios/facebook/FBTestSession.h new file mode 100644 index 000000000..12a98b6ab --- /dev/null +++ b/src/ios/facebook/FBTestSession.h @@ -0,0 +1,135 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBSession.h" + +#if defined (DEBUG) + #define SAFE_TO_USE_FBTESTSESSION +#endif + +#if !defined(SAFE_TO_USE_FBTESTSESSION) + #pragma message ("warning: using FBTestSession, which is designed for unit-testing uses only, in non-DEBUG code -- ensure this is what you really want") +#endif + +/*! + Consider using this tag to pass to sessionWithSharedUserWithPermissions:uniqueUserTag: when + you need a second unique test user in a test case. Using the same tag each time reduces + the proliferation of test users. + */ +extern NSString *kSecondTestUserTag; +/*! + Consider using this tag to pass to sessionWithSharedUserWithPermissions:uniqueUserTag: when + you need a third unique test user in a test case. Using the same tag each time reduces + the proliferation of test users. + */ +extern NSString *kThirdTestUserTag; + +/*! + @class FBTestSession + + @abstract + Implements an FBSession subclass that knows about test users for a particular + application. This should never be used from a real application, but may be useful + for writing unit tests, etc. + + @discussion + Facebook allows developers to create test accounts for testing their applications' + Facebook integration (see https://developers.facebook.com/docs/test_users/). This class + simplifies use of these accounts for writing unit tests. It is not designed for use in + production application code. + + The main use case for this class is using sessionForUnitTestingWithPermissions:mode: + to create a session for a test user. Two modes are supported. In "shared" mode, an attempt + is made to find an existing test user that has the required permissions and, if it is not + currently in use by another FBTestSession, just use that user. If no such user is available, + a new one is created with the required permissions. In "private" mode, designed for + scenarios which require a new user in a known clean state, a new test user will always be + created, and it will be automatically deleted when the FBTestSession is closed. + + Note that the shared test user functionality depends on a naming convention for the test users. + It is important that any testing of functionality which will mutate the permissions for a + test user NOT use a shared test user, or this scheme will break down. If a shared test user + seems to be in an invalid state, it can be deleted manually via the Web interface at + https://developers.facebook.com/apps/APP_ID/permissions?role=test+users. + */ +@interface FBTestSession : FBSession + +/// The app access token (composed of app ID and secret) to use for accessing test users. +@property (readonly, copy) NSString *appAccessToken; +/// The ID of the test user associated with this session. +@property (readonly, copy) NSString *testUserID; +/// The App ID of the test app as configured in the plist. +@property (readonly, copy) NSString *testAppID; +/// The App Secret of the test app as configured in the plist. +@property (readonly, copy) NSString *testAppSecret; + +/*! + @abstract + Constructor helper to create a session for use in unit tests + + @discussion + This method creates a session object which uses a shared test user with the right permissions, + creating one if necessary on open (but not deleting it on close, so it can be re-used in later + tests). Calling this method multiple times may return sessions with the same user. If this is not + desired, use the variant sessionWithSharedUserWithPermissions:uniqueUserTag:. + + This method should not be used in application code -- but is useful for creating unit tests + that use the Facebook SDK. + + @param permissions array of strings naming permissions to authorize; nil indicates + a common default set of permissions should be used for unit testing + */ ++ (id)sessionWithSharedUserWithPermissions:(NSArray*)permissions; + +/*! + @abstract + Constructor helper to create a session for use in unit tests + + @discussion + This method creates a session object which uses a shared test user with the right permissions, + creating one if necessary on open (but not deleting it on close, so it can be re-used in later + tests). + + This method should not be used in application code -- but is useful for creating unit tests + that use the Facebook SDK. + + @param permissions array of strings naming permissions to authorize; nil indicates + a common default set of permissions should be used for unit testing + + @param uniqueUserTag a string which will be used to make this user unique among other + users with the same permissions. Useful for tests which require two or more users to interact + with each other, and which therefore must have sessions associated with different users. For + this case, consider using kSecondTestUserTag and kThirdTestUserTag so these users can be shared + with other, similar, tests. + */ ++ (id)sessionWithSharedUserWithPermissions:(NSArray*)permissions + uniqueUserTag:(NSString*)uniqueUserTag; + +/*! + @abstract + Constructor helper to create a session for use in unit tests + + @discussion + This method creates a session object which creates a test user on open, and destroys the user on + close; This method should not be used in application code -- but is useful for creating unit tests + that use the Facebook SDK. + + @param permissions array of strings naming permissions to authorize; nil indicates + a common default set of permissions should be used for unit testing + */ ++ (id)sessionWithPrivateUserWithPermissions:(NSArray*)permissions; + +@end diff --git a/src/ios/facebook/FBTestSession.m b/src/ios/facebook/FBTestSession.m new file mode 100644 index 000000000..70dc317c1 --- /dev/null +++ b/src/ios/facebook/FBTestSession.m @@ -0,0 +1,583 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#define SAFE_TO_USE_FBTESTSESSION + +#import "FBTestSession.h" +#import "FBTestSession+Internal.h" +#import "FBSessionManualTokenCachingStrategy.h" +#import "FBError.h" +#import "FBSession+Protected.h" +#import "FBSession+Internal.h" +#import "FBRequest.h" +#import +#import "FBSBJSON.h" +#import "FBGraphUser.h" + +/* + Indicates whether the test user for an FBTestSession should be shared + (created only if necessary, not deleted automatically) or private (created specifically + for this session, deleted automatically upon close). + */ +typedef enum { + // Create and delete a new test user for this session. + FBTestSessionModePrivate = 0, + // Use an existing available test user with the right permissions, or create + // a new one if none are available. Not automatically deleted. + FBTestSessionModeShared = 1, +} FBTestSessionMode; + + +static NSString *const FBPLISTAppIDKey = @"FacebookAppID"; +static NSString *const FBPLISTAppSecretKey = @"FacebookAppSecret"; +static NSString *const FBPLISTUniqueUserTagKey = @"UniqueUserTag"; +static NSString *const FBLoginAuthTestUserURLPath = @"oauth/access_token"; +static NSString *const FBLoginAuthTestUserCreatePathFormat = @"%@/accounts/test-users"; +static NSString *const FBLoginTestUserClientID = @"client_id"; +static NSString *const FBLoginTestUserClientSecret = @"client_secret"; +static NSString *const FBLoginTestUserGrantType = @"grant_type"; +static NSString *const FBLoginTestUserGrantTypeClientCredentials = @"client_credentials"; +static NSString *const FBLoginTestUserAccessToken = @"access_token"; +static NSString *const FBLoginTestUserID = @"id"; +static NSString *const FBLoginTestUserName = @"name"; + +NSString *kSecondTestUserTag = @"Second"; +NSString *kThirdTestUserTag = @"Third"; + +NSString *const FBErrorLoginFailedReasonUnitTestResponseUnrecognized = @"com.facebook.sdk:UnitTestResponseUnrecognized"; + +#pragma mark Module scoped global variables + +static NSMutableDictionary *testUsers = nil; +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; + +#pragma mark - + +#pragma mark Private interface + +@interface FBTestSession () +{ + BOOL _forceAccessTokenRefresh; +} + +@property (readwrite, copy) NSString *appAccessToken; +@property (readwrite, copy) NSString *testUserID; +@property (readwrite, copy) NSString *testAppID; +@property (readwrite, copy) NSString *testAppSecret; +@property (readwrite, copy) NSString *machineUniqueUserTag; +@property (readwrite, copy) NSString *sessionUniqueUserTag; +@property (readonly, copy) NSString *permissionsString; +@property (readonly, copy) NSString *sharedTestUserIdentifier; +@property (readwrite) FBTestSessionMode mode; + +- (id)initWithAppID:(NSString*)appID + appSecret:(NSString*)appSecret +machineUniqueUserTag:(NSString*)uniqueUserTag +sessionUniqueUserTag:(NSString*)sessionUniqueUserTag + mode:(FBTestSessionMode)mode + permissions:(NSArray*)permissions +tokenCachingStrategy:(FBSessionTokenCachingStrategy*)tokenCachingStrategy; +- (void)createNewTestUser; +- (void)retrieveTestUsersForApp; +- (void)findOrCreateSharedUser; +- (void)transitionToOpenWithToken:(NSString*)token; +- (NSString*)validNameStringFromInteger:(NSUInteger)input; +- (void)raiseException:(NSError*)innerError; + ++ (void)deleteUnitTestUser:(NSString*)userID accessToken:(NSString*)accessToken; ++ (id)sessionForUnitTestingWithPermissions:(NSArray*)permissions mode:(FBTestSessionMode)mode sessionUniqueUserTag:(NSString*)sessionUniqueUserTag; + +@end + +#pragma mark - + +@implementation FBTestSession + +@synthesize appAccessToken = _appAccessToken; +@synthesize testUserID = _testUserID; +@synthesize testAppID = _testAppID; +@synthesize testAppSecret = _testAppSecret; +@synthesize mode = _mode; +@synthesize machineUniqueUserTag = _machineUniqueUserKey; +@synthesize sessionUniqueUserTag = _sessionUniqueUserTag; + +#pragma mark Lifecycle + +- (id)initWithAppID:(NSString*)appID + appSecret:(NSString*)appSecret +machineUniqueUserTag:(NSString*)machineUniqueUserTag +sessionUniqueUserTag:(NSString*)sessionUniqueUserTag + mode:(FBTestSessionMode)mode + permissions:(NSArray*)permissions +tokenCachingStrategy:(FBSessionTokenCachingStrategy*)tokenCachingStrategy +{ + if (self = [super initWithAppID:appID + permissions:permissions + urlSchemeSuffix:nil + tokenCacheStrategy:tokenCachingStrategy]) { + self.testAppID = appID; + self.testAppSecret = appSecret; + self.machineUniqueUserTag = machineUniqueUserTag; + self.sessionUniqueUserTag = sessionUniqueUserTag; + self.appAccessToken = [NSString stringWithFormat:@"%@|%@", appID, appSecret]; + self.mode = mode; + } + + return self; +} + +- (void)dealloc +{ + [_appAccessToken release]; + [_testUserID release]; + [_testAppID release]; + [_testAppSecret release]; + [_machineUniqueUserKey release]; + [_sessionUniqueUserTag release]; + + [super dealloc]; +} + +#pragma mark - +#pragma mark Private methods + +- (NSString*)permissionsString { + return [self.permissions componentsJoinedByString:@","]; +} + +- (void)createNewTestUser +{ + NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"true", @"installed", + [self permissionsString], @"permissions", + @"post", @"method", + self.appAccessToken, @"access_token", + nil]; + + // We don't get the user name back on create, so if we want it later, remember it now. + NSString *newName = nil; + if (self.mode == FBTestSessionModeShared) { + // Rename the user with a hashed representation of our permissions, so we can find it + // again later. + newName = [NSString stringWithFormat:@"Shared %@ Testuser", self.sharedTestUserIdentifier]; + [parameters setObject:newName forKey:@"name"]; + } + + // fetch a test user and token + // note, this fetch uses a manually constructed app token using the appid|appsecret approach, + // if there is demand for support for apps for which this will not work, we may consider handling + // failure by falling back and fetching an app-token via a request; the current approach reduces + // traffic for common unit testing configuration, which seems like the right tradeoff to start with + FBRequest *request = [[[FBRequest alloc] initWithSession:nil + graphPath:[NSString stringWithFormat:FBLoginAuthTestUserCreatePathFormat, self.appID] + parameters:parameters + HTTPMethod:nil] + autorelease]; + [request startWithCompletionHandler: + ^(FBRequestConnection *connection, id result, NSError *error) { + id userToken; + id userID; + if (!error && + [result isKindOfClass:[NSDictionary class]] && + (userToken = [result objectForKey:FBLoginTestUserAccessToken]) && + [userToken isKindOfClass:[NSString class]] && + (userID = [result objectForKey:FBLoginTestUserID]) && + [userID isKindOfClass:[NSString class]]) { + // capture the id for future use + self.testUserID = userID; + + // Remember this user if it is going to be shared. + if (self.mode == FBTestSessionModeShared) { + NSDictionary *user = [NSDictionary dictionaryWithObjectsAndKeys: + userID, FBLoginTestUserID, + userToken, FBLoginTestUserAccessToken, + newName, FBLoginTestUserName, + nil]; + + pthread_mutex_lock(&mutex); + + [testUsers setObject:user forKey:userID]; + + pthread_mutex_unlock(&mutex); + } + + [self transitionToOpenWithToken:userToken]; + } else { + if (error) { + NSLog(@"Error: [FBSession createNewTestUserAndRename:] failed with error: %@", error.description); + } else { + // we fetched something unexpected when requesting an app token + error = [FBSession errorLoginFailedWithReason:FBErrorLoginFailedReasonUnitTestResponseUnrecognized + errorCode:nil + innerError:nil]; + } + // state transition, and call the handler if there is one + [self transitionAndCallHandlerWithState:FBSessionStateClosedLoginFailed + error:error + token:nil + expirationDate:nil + shouldCache:NO + loginType:FBSessionLoginTypeNone]; + } + }]; +} + +- (void)transitionToOpenWithToken:(NSString*)token +{ + [self transitionAndCallHandlerWithState:FBSessionStateOpen + error:nil + token:token + expirationDate:[NSDate distantFuture] + shouldCache:NO + loginType:FBSessionLoginTypeTestUser]; +} + +// We raise exceptions when things go wrong here, because this is intended for use only +// in unit tests and we want things to stop as soon as something bad happens. +- (void)raiseException:(NSError*)innerError +{ + NSDictionary *userInfo = nil; + if (innerError) { + userInfo = [NSDictionary dictionaryWithObjectsAndKeys: + innerError, FBErrorInnerErrorKey, + nil]; + } + + [[NSException exceptionWithName:FBInvalidOperationException + reason:@"FBTestSession encountered an error" + userInfo:userInfo] + raise]; + +} + +- (void)populateTestUsers:(NSArray*)users testAccounts:(NSArray*)testAccounts +{ + pthread_mutex_lock(&mutex); + + // Map user IDs to test_accounts + for (NSDictionary *testAccount in testAccounts) { + id uid = [[testAccount objectForKey:FBLoginTestUserID] stringValue]; + [testUsers setObject:[NSMutableDictionary dictionaryWithDictionary:testAccount] + forKey:uid]; + } + + // Add the user name to the test_account data. + for (NSDictionary *user in users) { + id uid = [[user objectForKey:@"uid"] stringValue]; + NSMutableDictionary *testUser = [testUsers objectForKey:uid]; + [testUser setObject:[user objectForKey:FBLoginTestUserName] forKey:FBLoginTestUserName]; + } + + pthread_mutex_unlock(&mutex); +} + +- (void)retrieveTestUsersForApp +{ + // We need three pieces of data: id, access_token, and name (which we use to + // encode permissions). We get access_token from the test_account FQL table and + // name from the user table; they share an id. Use FQL multiquery to get it all + // in one go. + NSString *testAccountQuery = [NSString stringWithFormat: + @"SELECT id,access_token FROM test_account WHERE app_id = %@", + self.testAppID]; + NSString *userQuery = @"SELECT uid,name FROM user WHERE uid IN (SELECT id FROM #test_accounts)"; + NSDictionary *multiquery = [NSDictionary dictionaryWithObjectsAndKeys: + testAccountQuery, @"test_accounts", + userQuery, @"users", + nil]; + + FBSBJSON *writer = [[FBSBJSON alloc] init]; + NSString *jsonMultiquery = [writer stringWithObject:multiquery]; + [writer release]; + + NSDictionary *parameters = [NSDictionary dictionaryWithObjectsAndKeys: + jsonMultiquery, @"q", + self.appAccessToken, @"access_token", + nil]; + FBRequest *request = [[[FBRequest alloc] initWithSession:nil + graphPath:@"fql" + parameters:parameters + HTTPMethod:nil] + autorelease]; + [request startWithCompletionHandler: + ^(FBRequestConnection *connection, id result, NSError *error) { + if (error || + !result) { + [self raiseException:error]; + } + id data = [result objectForKey:@"data"]; + if (![data isKindOfClass:[NSArray class]] || + [data count] != 2) { + [self raiseException:nil]; + } + + // We get back two sets of results. The first is from the test_accounts + // query, the second from the users query. + id testAccounts = [[data objectAtIndex:0] objectForKey:@"fql_result_set"]; + id users = [[data objectAtIndex:1] objectForKey:@"fql_result_set"]; + if (![testAccounts isKindOfClass:[NSArray class]] || + ![users isKindOfClass:[NSArray class]]) { + [self raiseException:nil]; + } + + // Use both sets of results to populate our static array of accounts. + [self populateTestUsers:users testAccounts:testAccounts]; + + // Now that we've populated all test users, we can continue looking for + // the matching user, which started this all off. + [self findOrCreateSharedUser]; + }]; + +} + +// Given a long string, generate its hash value, and then convert that to a string that +// we can use as part of a Facebook test user name (i.e., no digits). +- (NSString*)validNameStringFromInteger:(NSUInteger)input +{ + NSString *hashAsString = [NSString stringWithFormat:@"%u", input]; + NSMutableString *result = [NSMutableString stringWithString:@"Perm"]; + + // We know each character is a digit. Convert it into a letter starting with 'a'. + for (int i = 0; i < hashAsString.length; ++i) { + NSString *ch = [NSString stringWithFormat:@"%C", + (unsigned short)([hashAsString characterAtIndex:i] + 'a' - '0')]; + [result appendString:ch]; + } + + return result; +} + +- (NSString*)sharedTestUserIdentifier +{ + NSUInteger permissionsHash = self.permissionsString.hash; + NSUInteger machineTagHash = self.machineUniqueUserTag.hash; + NSUInteger sessionTagHash = self.sessionUniqueUserTag.hash; + + NSUInteger combinedHash = permissionsHash ^ machineTagHash ^ sessionTagHash; + return [self validNameStringFromInteger:combinedHash]; +} + +- (void)findOrCreateSharedUser +{ + pthread_mutex_lock(&mutex); + + NSString *userIdentifier = self.sharedTestUserIdentifier; + + id matchingTestUser = nil; + for (id testUser in [testUsers allValues]) { + NSString *userName = [testUser objectForKey:FBLoginTestUserName]; + // Does this user have the right permissions and is it not in use? + if ([userName rangeOfString:userIdentifier].length > 0) { + matchingTestUser = testUser; + break; + } + } + + pthread_mutex_unlock(&mutex); + + if (matchingTestUser) { + // We can use this user. IDs come back as numbers, make sure we return as a string. + self.testUserID = [[matchingTestUser objectForKey:FBLoginTestUserID] description]; + + [self transitionToOpenWithToken:[matchingTestUser objectForKey:FBLoginTestUserAccessToken]]; + } else { + // Need to create a user. Do so, and rename it using our hashed permissions string. + [self createNewTestUser]; + } +} + +- (void)setForceAccessTokenRefresh:(BOOL)forceAccessTokenRefresh { + _forceAccessTokenRefresh = forceAccessTokenRefresh; +} + +- (BOOL)forceAccessTokenRefresh { + return _forceAccessTokenRefresh; +} + +#pragma mark - +#pragma mark Overrides + +- (BOOL)transitionToState:(FBSessionState)state + andUpdateToken:(NSString*)token + andExpirationDate:(NSDate*)date + shouldCache:(BOOL)shouldCache + loginType:(FBSessionLoginType)loginType { + // in case we need these after the transition + NSString *userID = self.testUserID; + + BOOL didTransition = [super transitionToState:state + andUpdateToken:token + andExpirationDate:date + shouldCache:shouldCache + loginType:loginType]; + + if (didTransition && FB_ISSESSIONSTATETERMINAL(self.state)) { + if (self.mode == FBTestSessionModePrivate) { + [FBTestSession deleteUnitTestUser:userID accessToken:self.appAccessToken]; + } + } + + return didTransition; +} +- (void)authorizeWithPermissions:(NSArray*)permissions + behavior:(FBSessionLoginBehavior)behavior + defaultAudience:(FBSessionDefaultAudience)audience + isReauthorize:(BOOL)isReauthorize { + + // We ignore behavior, since we aren't going to present UI. + + if (self.mode == FBTestSessionModePrivate) { + // If we aren't wanting a shared user, just create a user. Don't waste time renaming it since + // we will be deleting it when done. + [self createNewTestUser]; + } else { + // We need to see if there are any test users that fit the bill. + + // Did we already get the test users? + pthread_mutex_lock(&mutex); + if (testUsers) { + pthread_mutex_unlock(&mutex); + + // Yes, look for one that we can use. + [self findOrCreateSharedUser]; + } else { + // No, populate the list and then continue. + // We never release testUsers. We should only populate it once. + testUsers = [[NSMutableDictionary alloc] init]; + + pthread_mutex_unlock(&mutex); + + [self retrieveTestUsersForApp]; + } + } +} + +- (BOOL)shouldExtendAccessToken { + // Note: we reset the flag each time we are queried. Tests should set it as needed for more complicated logic. + BOOL extend = self.forceAccessTokenRefresh || [super shouldExtendAccessToken]; + self.forceAccessTokenRefresh = NO; + return extend; +} + +#pragma mark - +#pragma mark Class methods + ++ (id)sessionWithSharedUserWithPermissions:(NSArray*)permissions + uniqueUserTag:(NSString*)uniqueUserTag +{ + return [self sessionForUnitTestingWithPermissions:permissions + mode:FBTestSessionModeShared + sessionUniqueUserTag:uniqueUserTag]; + +} + ++ (id)sessionWithSharedUserWithPermissions:(NSArray*)permissions +{ + return [self sessionWithSharedUserWithPermissions:permissions uniqueUserTag:nil]; +} + ++ (id)sessionWithPrivateUserWithPermissions:(NSArray*)permissions +{ + return [self sessionForUnitTestingWithPermissions:permissions + mode:FBTestSessionModePrivate + sessionUniqueUserTag:nil]; +} + ++ (id)sessionForUnitTestingWithPermissions:(NSArray*)permissions + mode:(FBTestSessionMode)mode + sessionUniqueUserTag:(NSString*)sessionUniqueUserTag +{ + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths objectAtIndex:0]; + + // fetch config contents + NSString *configFilename = [documentsDirectory stringByAppendingPathComponent:@"FacebookSDK-UnitTestConfig.plist"]; + NSDictionary *configSettings = [NSDictionary dictionaryWithContentsOfFile:configFilename]; + + NSString *appID = [configSettings objectForKey:FBPLISTAppIDKey]; + NSString *appSecret = [configSettings objectForKey:FBPLISTAppSecretKey]; + if (!appID || !appSecret) { + [[NSException exceptionWithName:FBInvalidOperationException + reason: + @"FBSession: Missing AppID or AppSecret; FacebookSDK-UnitTestConfig.plist is " + @"is missing or invalid; to create a Facebook AppID, " + @"visit https://developers.facebook.com/apps" + userInfo:nil] + raise]; + } + + NSString *machineUniqueUserTag = [configSettings objectForKey:FBPLISTUniqueUserTagKey]; + + FBSessionManualTokenCachingStrategy *tokenCachingStrategy = + [[FBSessionManualTokenCachingStrategy alloc] init]; + + if (!permissions.count) { + permissions = [NSArray arrayWithObjects:@"email", @"publish_actions", nil]; + } + + // call our internal designated initializer to create a unit-testing instance + FBTestSession *session = [[[FBTestSession alloc] + initWithAppID:appID + appSecret:appSecret + machineUniqueUserTag:machineUniqueUserTag + sessionUniqueUserTag:sessionUniqueUserTag + mode:mode + permissions:permissions + tokenCachingStrategy:tokenCachingStrategy] + autorelease]; + + [tokenCachingStrategy release]; + + return session; +} + ++ (void)deleteUnitTestUser:(NSString*)userID + accessToken:(NSString*)accessToken +{ + if (userID && accessToken) { + // use FBRequest/FBRequestConnection to create an NSURLRequest + FBRequest *temp = [[FBRequest alloc ] initWithSession:nil + graphPath:userID + parameters:[NSDictionary dictionaryWithObjectsAndKeys: + @"delete", @"method", + accessToken, @"access_token", + nil] + HTTPMethod:nil]; + FBRequestConnection *connection = [[FBRequestConnection alloc] init]; + [connection addRequest:temp completionHandler:nil]; + NSURLRequest *request = connection.urlRequest; + [temp release]; + [connection release]; + + // synchronously delete the user + NSURLResponse *response; + NSError *error = nil; + NSData *data; + data = [NSURLConnection sendSynchronousRequest:request + returningResponse:&response + error:&error]; + // if !data or if data == false, log + NSString *body = !data ? nil : [[[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding] + autorelease]; + if (!data || [body isEqualToString:@"false"]) { + NSLog(@"FBSession !delete test user with id:%@ error:%@", userID, error ? error : body); + } + } +} + +#pragma mark - + +@end diff --git a/src/ios/facebook/FBURLConnection.h b/src/ios/facebook/FBURLConnection.h new file mode 100644 index 000000000..bd6837a6c --- /dev/null +++ b/src/ios/facebook/FBURLConnection.h @@ -0,0 +1,36 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +@class FBURLConnection; +typedef void (^FBURLConnectionHandler)(FBURLConnection *connection, + NSError *error, + NSURLResponse *response, + NSData *responseData); + +@interface FBURLConnection : NSObject + +- (FBURLConnection *)initWithURL:(NSURL *)url + completionHandler:(FBURLConnectionHandler)handler; + +- (FBURLConnection *)initWithRequest:(NSURLRequest *)request + skipRoundTripIfCached:(BOOL)skipRoundtripIfCached + completionHandler:(FBURLConnectionHandler)handler; + +- (void)cancel; + +@end diff --git a/src/ios/facebook/FBURLConnection.m b/src/ios/facebook/FBURLConnection.m new file mode 100644 index 000000000..2aa2fc7a7 --- /dev/null +++ b/src/ios/facebook/FBURLConnection.m @@ -0,0 +1,282 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBURLConnection.h" +#import "FBError.h" +#import "FBDataDiskCache.h" +#import "FBSession.h" +#import "FBLogger.h" +#import "FBUtility.h" +#import "FBSettings.h" +#import "FBSettings+Internal.h" + +static NSArray* _cdnHosts; + +@interface FBURLConnection () + +@property (nonatomic, retain) NSURLConnection *connection; +@property (nonatomic, retain) NSMutableData *data; +@property (nonatomic, copy) FBURLConnectionHandler handler; +@property (nonatomic, retain) NSURLResponse *response; +@property (nonatomic) unsigned long requestStartTime; +@property (nonatomic, readonly) NSUInteger loggerSerialNumber; +@property (nonatomic) BOOL skipRoundtripIfCached; + +- (BOOL)isCDNURL:(NSURL *)url; + +- (void)invokeHandler:(FBURLConnectionHandler)handler + error:(NSError *)error + response:(NSURLResponse *)response + responseData:(NSData *)responseData; + +@end + +@implementation FBURLConnection + +@synthesize connection = _connection; +@synthesize data = _data; +@synthesize handler = _handler; +@synthesize loggerSerialNumber = _loggerSerialNumber; +@synthesize requestStartTime = _requestStartTime; +@synthesize response = _response; +@synthesize skipRoundtripIfCached = _skipRoundtripIfCached; + +#pragma mark - Lifecycle + ++ (void)initialize +{ + if (_cdnHosts == nil) { + _cdnHosts = [[NSArray arrayWithObjects: + @"akamaihd.net", + @"fbcdn.net", + nil] retain]; + } +} + +- (FBURLConnection *)initWithURL:(NSURL *)url + completionHandler:(FBURLConnectionHandler)handler +{ + NSURLRequest *request = [[[NSURLRequest alloc] initWithURL:url] autorelease]; + return [self initWithRequest:request + skipRoundTripIfCached:YES + completionHandler:handler]; +} + +- (FBURLConnection *)initWithRequest:(NSURLRequest *)request + skipRoundTripIfCached:(BOOL)skipRoundtripIfCached + completionHandler:(FBURLConnectionHandler)handler +{ + if (self = [super init]) { + self.skipRoundtripIfCached = skipRoundtripIfCached; + + // Check if this url is cached + NSURL* url = request.URL; + NSData* cachedData = skipRoundtripIfCached ? [[FBDataDiskCache sharedCache] dataForURL:url] : nil; + + if (cachedData) { + [FBLogger singleShotLogEntry:FBLoggingBehaviorFBURLConnections + formatString:@"FBUrlConnection: <#%d>. Cached response %d kB\n", + [url absoluteString], + [cachedData length] / 1024]; + + // TODO: It seems wrong to call this within init. There are cases + // with UI where this is not ideal. We should talk about this. + handler(self, nil, nil, cachedData); + + } else { + + _requestStartTime = [FBUtility currentTimeInMilliseconds]; + _loggerSerialNumber = [FBLogger newSerialNumber]; + _connection = [[NSURLConnection alloc] + initWithRequest:request + delegate:self]; + _data = [[NSMutableData alloc] init]; + + [FBLogger singleShotLogEntry:FBLoggingBehaviorFBURLConnections + formatString:@"FBURLConnection <#%d>:\n URL: '%@'\n\n", + _loggerSerialNumber, + [url absoluteString]]; + + self.handler = handler; + } + + // always attempt to autoPublish. this function internally + // handles only executing once. + [FBSettings autoPublishInstall:nil]; + } + return self; +} + +- (void)invokeHandler:(FBURLConnectionHandler)handler + error:(NSError *)error + response:(NSURLResponse *)response + responseData:(NSData *)responseData +{ + if (self.handler == nil) { + return; + } + + NSString *logEntry; + + if (error) { + + logEntry = [NSString + stringWithFormat:@"FBURLConnection <#%d>:\n Error: '%@'", + _loggerSerialNumber, + [error localizedDescription]]; + + } else { + + // Basic FBURLConnection logging just prints out the URL. FBRequest logging provides more details. + NSString *mimeType = [response MIMEType]; + NSMutableString *mutableLogEntry = [NSMutableString stringWithFormat:@"FBURLConnection <#%d>:\n Duration: %lu msec\nResponse Size: %d kB\n MIME type: %@\n", + _loggerSerialNumber, + [FBUtility currentTimeInMilliseconds] - _requestStartTime, + [responseData length] / 1024, + mimeType]; + + if ([mimeType isEqualToString:@"text/javascript"]) { + NSString *responseUTF8 = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; + [mutableLogEntry appendFormat:@" Response:\n%@\n\n", responseUTF8]; + [responseUTF8 release]; + } + + logEntry = mutableLogEntry; + } + + [FBLogger singleShotLogEntry:FBLoggingBehaviorFBURLConnections + logEntry:logEntry]; + + handler(self, error, response, responseData); +} + +- (void)dealloc +{ + [_response release]; + [_connection release]; + [_data release]; + [_handler release]; + [super dealloc]; +} + +- (void)cancel +{ + [self.connection cancel]; + if (self.handler == nil) { + return; + } + + NSError *error = [[NSError alloc] initWithDomain:FacebookSDKDomain + code:FBErrorOperationCancelled + userInfo:nil]; + + // We are retaining ourselves (and releasing explicitly) because unlike the + // other cases where we call the handler, we are not being held by anyone + // else. + [self retain]; + FBURLConnectionHandler handler = [self.handler retain]; + self.handler = nil; + @try { + [self invokeHandler:handler error:error response:nil responseData:nil]; + } @finally { + [handler release]; + [self release]; + [error release]; + } +} + +- (void)connection:(NSURLConnection *)connection +didReceiveResponse:(NSURLResponse *)response +{ + self.response = response; + [self.data setLength:0]; +} + +- (void)connection:(NSURLResponse *)connection + didReceiveData:(NSData *)data +{ + [self.data appendData:data]; +} + +- (void)connection:(NSURLConnection *)connection + didFailWithError:(NSError *)error +{ + @try { + [self invokeHandler:self.handler error:error response:nil responseData:nil]; + } @finally { + self.handler = nil; + } +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection +{ + NSURL* dataURL = self.response.URL; + if ([self isCDNURL:dataURL]) { + // Cache this data + [[FBDataDiskCache sharedCache] setData:self.data forURL:dataURL]; + } + + @try { + [self invokeHandler:self.handler error:nil response:self.response responseData:self.data]; + } @finally { + self.handler = nil; + } +} + +-(NSURLRequest *)connection:(NSURLConnection *)connection + willSendRequest:(NSURLRequest *)request + redirectResponse:(NSURLResponse *)redirectResponse +{ + if (redirectResponse && self.skipRoundtripIfCached) { + NSURL* redirectURL = request.URL; + + // Check for cache and short-circuit + NSData* cachedData = + [[FBDataDiskCache sharedCache] dataForURL:redirectURL]; + if (cachedData) { + @try { + // Fake a response + NSURLResponse* cacheResponse = + [[NSURLResponse alloc] initWithURL:redirectURL + MIMEType:@"application/octet-stream" + expectedContentLength:cachedData.length + textEncodingName:@"utf8"]; + [self invokeHandler:self.handler error:nil response:cacheResponse responseData:cachedData]; + [cacheResponse release]; + } @finally { + self.handler = nil; + } + + return nil; + } + } + + return request; +} + +- (BOOL)isCDNURL:(NSURL *)url +{ + NSString* urlHost = url.host; + for (NSString* host in _cdnHosts) { + if ([urlHost hasSuffix:host]) { + return YES; + } + } + + return NO; +} + +@end diff --git a/src/ios/facebook/FBUserSettingsViewController.h b/src/ios/facebook/FBUserSettingsViewController.h new file mode 100644 index 000000000..27b99b600 --- /dev/null +++ b/src/ios/facebook/FBUserSettingsViewController.h @@ -0,0 +1,125 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "FBSession.h" +#import "FBViewController.h" + +/*! + @protocol + + @abstract + The `FBUserSettingsDelegate` protocol defines the methods called by a . + */ +@protocol FBUserSettingsDelegate + +@optional + +/*! + @abstract + Called when the view controller will log the user out in response to a button press. + + @param sender The view controller sending the message. + */ +- (void)loginViewControllerWillLogUserOut:(id)sender; + +/*! + @abstract + Called after the view controller logged the user out in response to a button press. + + @param sender The view controller sending the message. + */ +- (void)loginViewControllerDidLogUserOut:(id)sender; + +/*! + @abstract + Called when the view controller will log the user in in response to a button press. + Note that logging in can fail for a number of reasons, so there is no guarantee that this + will be followed by a call to loginViewControllerDidLogUserIn:. Callers wanting more granular + notification of the session state changes can use KVO or the NSNotificationCenter to observe them. + + @param sender The view controller sending the message. + */ +- (void)loginViewControllerWillAttemptToLogUserIn:(id)sender; + +/*! + @abstract + Called after the view controller successfully logged the user in in response to a button press. + + @param sender The view controller sending the message. + */ +- (void)loginViewControllerDidLogUserIn:(id)sender; + +/*! + @abstract + Called if the view controller encounters an error while trying to log a user in. + + @param sender The view controller sending the message. + @param error The error encountered. + */ +- (void)loginViewController:(id)sender receivedError:(NSError *)error; + +@end + + +/*! + @class FBUserSettingsViewController + + @abstract + The `FBUserSettingsViewController` class provides a user interface exposing a user's + Facebook-related settings. Currently, this is limited to whether they are logged in or out + of Facebook. + + Because of the size of some graphics used in this view, its resources are packaged as a separate + bundle. In order to use `FBUserSettingsViewController`, drag the `FBUserSettingsViewResources.bundle` + from the SDK directory into your Xcode project. + */ +@interface FBUserSettingsViewController : FBViewController + +/*! + @abstract + The permissions to request if the user logs in via this view. + */ +@property (nonatomic, copy) NSArray *permissions __attribute__((deprecated)); + +/*! + @abstract + The read permissions to request if the user logs in via this view. + + @discussion + Note, that if read permissions are specified, then publish permissions should not be specified. + */ +@property (nonatomic, copy) NSArray *readPermissions; + +/*! + @abstract + The publish permissions to request if the user logs in via this view. + + @discussion + Note, that a defaultAudience value of FBSessionDefaultAudienceOnlyMe, FBSessionDefaultAudienceEveryone, or + FBSessionDefaultAudienceFriends should be set if publish permissions are specified. Additionally, when publish + permissions are specified, then read should not be specified. + */ +@property (nonatomic, copy) NSArray *publishPermissions; + +/*! + @abstract + The default audience to use, if publish permissions are requested at login time. + */ +@property (nonatomic, assign) FBSessionDefaultAudience defaultAudience; + +@end + diff --git a/src/ios/facebook/FBUserSettingsViewController.m b/src/ios/facebook/FBUserSettingsViewController.m new file mode 100644 index 000000000..137c590c7 --- /dev/null +++ b/src/ios/facebook/FBUserSettingsViewController.m @@ -0,0 +1,373 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBUserSettingsViewController.h" +#import "FBProfilePictureView.h" +#import "FBGraphUser.h" +#import "FBSession.h" +#import "FBRequest.h" +#import "FBViewController+Internal.h" +#import "FBUtility.h" + +@interface FBUserSettingsViewController () + +@property (nonatomic, retain) FBProfilePictureView *profilePicture; +@property (nonatomic, retain) UIImageView *backgroundImageView; +@property (nonatomic, retain) UILabel *connectedStateLabel; +@property (nonatomic, retain) id me; +@property (nonatomic, retain) UIButton *loginLogoutButton; +@property (nonatomic) BOOL attemptingLogin; +@property (nonatomic, retain) NSBundle *bundle; + +- (void)loginLogoutButtonPressed:(id)sender; +- (void)sessionStateChanged:(FBSession *)session + state:(FBSessionState)state + error:(NSError *)error; +- (void)openSession; +- (void)updateControls; +- (void)updateBackgroundImage; + +@end + +@implementation FBUserSettingsViewController + +@synthesize profilePicture = _profilePicture; +@synthesize connectedStateLabel = _connectedStateLabel; +@synthesize me = _me; +@synthesize loginLogoutButton = _loginLogoutButton; +@synthesize permissions = _permissions; +@synthesize readPermissions = _readPermissions; +@synthesize publishPermissions = _publishPermissions; +@synthesize defaultAudience = _defaultAudience; +@synthesize attemptingLogin = _attemptingLogin; +@synthesize backgroundImageView = _backgroundImageView; +@synthesize bundle = _bundle; + +#pragma mark View controller lifecycle + +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + self.cancelButton = nil; + self.attemptingLogin = NO; + + NSString *path = [[NSBundle mainBundle] pathForResource:@"FBUserSettingsViewResources" + ofType:@"bundle"]; + self.bundle = [NSBundle bundleWithPath:path]; + if (self.bundle == nil) { + NSLog(@"WARNING: FBUserSettingsViewController could not find FBUserSettingsViewResources.bundle"); + } + } + return self; +} + +- (void)dealloc { + [super dealloc]; + + [_profilePicture release]; + [_connectedStateLabel release]; + [_me release]; + [_loginLogoutButton release]; + [_permissions release]; + [_backgroundImageView release]; + [_bundle release]; +} + +#pragma mark View lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + // If we are not being presented modally, we don't need a Done button. + if (self.compatiblePresentingViewController == nil) { + self.doneButton = nil; + } + + // If you remove the background images from the resource bundle in order to save space, + // this allows the background to still be rendered in Facebook blue. + UIColor *facebookBlue = [UIColor colorWithRed:(59.0 / 255.0) + green:(89.0 / 255.0) + blue:(152.0 / 255.0) + alpha:1.0]; + self.view.backgroundColor = facebookBlue; + + CGRect usableBounds = self.canvasView.bounds; + + self.backgroundImageView = [[[UIImageView alloc] init] autorelease]; + self.backgroundImageView.frame = usableBounds; + self.backgroundImageView.userInteractionEnabled = NO; + self.backgroundImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.canvasView addSubview:self.backgroundImageView]; + [self updateBackgroundImage]; + + UIImageView *logo = [[[UIImageView alloc] + initWithImage:[UIImage imageNamed:@"FBUserSettingsViewResources.bundle/images/facebook-logo.png"]] autorelease]; + CGPoint center = CGPointMake(CGRectGetMidX(usableBounds), 68); + logo.center = center; + logo.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [self.canvasView addSubview:logo]; + + // We want the profile picture control and label to be grouped together when autoresized, + // so we put them in a subview. + UIView *containerView = [[[UIView alloc] init] autorelease]; + containerView.frame = CGRectMake(0, + 135, + usableBounds.size.width, + 110); + containerView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + + // Add profile picture control + self.profilePicture = [[[FBProfilePictureView alloc] initWithProfileID:nil + pictureCropping:FBProfilePictureCroppingSquare] + autorelease]; + self.profilePicture.frame = CGRectMake(containerView.frame.size.width / 2 - 32, 0, 64, 64); + [containerView addSubview:self.profilePicture]; + + // Add connected state/name control + self.connectedStateLabel = [[[UILabel alloc] init] autorelease]; + self.connectedStateLabel.frame = CGRectMake(0, + self.profilePicture.frame.size.height + 14.0, + containerView.frame.size.width, + 20); + self.connectedStateLabel.backgroundColor = [UIColor clearColor]; + self.connectedStateLabel.textAlignment = UITextAlignmentCenter; + self.connectedStateLabel.numberOfLines = 0; + self.connectedStateLabel.font = [UIFont boldSystemFontOfSize:16.0]; + self.connectedStateLabel.shadowColor = [UIColor blackColor]; + self.connectedStateLabel.shadowOffset = CGSizeMake(0.0, -1.0); + [containerView addSubview:self.connectedStateLabel]; + [self.canvasView addSubview:containerView]; + + // Add the login/logout button + self.loginLogoutButton = [UIButton buttonWithType:UIButtonTypeCustom]; + UIImage *image = [UIImage imageNamed:@"FBUserSettingsViewResources.bundle/images/silver-button-normal.png"]; + [self.loginLogoutButton setBackgroundImage:image forState:UIControlStateNormal]; + image = [UIImage imageNamed:@"FBUserSettingsViewResources.bundle/images/silver-button-pressed.png"]; + [self.loginLogoutButton setBackgroundImage:image forState:UIControlStateHighlighted]; + self.loginLogoutButton.frame = CGRectMake((int)((usableBounds.size.width - image.size.width) / 2), + 285, + image.size.width, + image.size.height); + [self.loginLogoutButton addTarget:self + action:@selector(loginLogoutButtonPressed:) + forControlEvents:UIControlEventTouchUpInside]; + self.loginLogoutButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + UIColor *loginTitleColor = [UIColor colorWithRed:75.0 / 255.0 + green:81.0 / 255.0 + blue:100.0 / 255.0 + alpha:1.0]; + [self.loginLogoutButton setTitleColor:loginTitleColor forState:UIControlStateNormal]; + self.loginLogoutButton.titleLabel.font = [UIFont boldSystemFontOfSize:18.0]; + + UIColor *loginShadowColor = [UIColor colorWithRed:212.0 / 255.0 + green:218.0 / 255.0 + blue:225.0 / 255.0 + alpha:1.0]; + [self.loginLogoutButton setTitleShadowColor:loginShadowColor forState:UIControlStateNormal]; + self.loginLogoutButton.titleLabel.shadowOffset = CGSizeMake(0.0, 1.0); + [self.canvasView addSubview:self.loginLogoutButton]; + + // We need to know when the active session changes state. + // We use the same handler for both, because we don't actually care about distinguishing between them. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleActiveSessionStateChanged:) + name:FBSessionDidBecomeOpenActiveSessionNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleActiveSessionStateChanged:) + name:FBSessionDidBecomeClosedActiveSessionNotification + object:nil]; + + [self updateControls]; +} + +- (void)updateBackgroundImage { + NSString *orientation = UIInterfaceOrientationIsPortrait(self.interfaceOrientation) ? @"Portrait" : @"Landscape"; + NSString *idiom = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) ? @"IPhone" : @"IPad"; + NSString *imagePath = [NSString stringWithFormat:@"FBUserSettingsViewResources.bundle/images/loginBackground%@%@.jpg", idiom, orientation]; + self.backgroundImageView.image = [UIImage imageNamed:imagePath]; +} + +- (void)viewDidUnload { + [super viewDidUnload]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { + [self updateBackgroundImage]; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) || UIInterfaceOrientationIsPortrait(interfaceOrientation); +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; +} +#pragma mark Implementation + +- (void)updateControls { + if (FBSession.activeSession.isOpen) { + NSString *loginLogoutText = [FBUtility localizedStringForKey:@"FBUSVC:LogOut" + withDefault:@"Log Out" + inBundle:self.bundle]; + [self.loginLogoutButton setTitle:loginLogoutText forState:UIControlStateNormal]; + + // Label should be white with a shadow + self.connectedStateLabel.textColor = [UIColor whiteColor]; + self.connectedStateLabel.shadowColor = [UIColor blackColor]; + + // Move the label back below the profile view and show the profile view + self.connectedStateLabel.frame = CGRectMake(0, + self.profilePicture.frame.size.height + 16.0, + self.connectedStateLabel.frame.size.width, + 20); + self.profilePicture.hidden = NO; + + // Do we know the user's name? If not, request it. + if (self.me != nil) { + self.connectedStateLabel.text = self.me.name; + self.profilePicture.profileID = [self.me objectForKey:@"id"]; + } else { + self.connectedStateLabel.text = [FBUtility localizedStringForKey:@"FBUSVC:LoggedIn" + withDefault:@"Logged in" + inBundle:self.bundle]; + self.profilePicture.profileID = nil; + + [[FBRequest requestForMe] startWithCompletionHandler:^(FBRequestConnection *connection, id result, NSError *error) { + if (result) { + self.me = result; + [self updateControls]; + } + }]; + } + } else { + self.me = nil; + + // Label should be gray and centered in its superview; hide the profile view + self.connectedStateLabel.textColor = [UIColor colorWithRed:166.0 / 255.0 + green:174.0 / 255.0 + blue:215.0 / 255.0 + alpha:1.0]; + self.connectedStateLabel.shadowColor = nil; + + CGRect parentBounds = self.connectedStateLabel.superview.bounds; + self.connectedStateLabel.center = CGPointMake(CGRectGetMidX(parentBounds), + CGRectGetMidY(parentBounds)); + self.profilePicture.hidden = YES; + + self.connectedStateLabel.text = [FBUtility localizedStringForKey:@"FBUSVC:NotLoggedIn" + withDefault:@"Not logged in" + inBundle:self.bundle]; + self.profilePicture.profileID = nil; + NSString *loginLogoutText = [FBUtility localizedStringForKey:@"FBUSVC:LogIn" + withDefault:@"Log In..." + inBundle:self.bundle]; + [self.loginLogoutButton setTitle:loginLogoutText forState:UIControlStateNormal]; + } +} + +- (void)sessionStateChanged:(FBSession *)session + state:(FBSessionState)state + error:(NSError *)error +{ + if (error && + [self.delegate respondsToSelector:@selector(loginViewController:receivedError:)]) { + [(id)self.delegate loginViewController:self receivedError:error]; + } + + if (self.attemptingLogin) { + if (FB_ISSESSIONOPENWITHSTATE(state)) { + self.attemptingLogin = NO; + + if ([self.delegate respondsToSelector:@selector(loginViewControllerDidLogUserIn:)]) { + [(id)self.delegate loginViewControllerDidLogUserIn:self]; + } + } else if (FB_ISSESSIONSTATETERMINAL(state)) { + self.attemptingLogin = NO; + } + } +} + +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +- (void)openSession { + if ([self.delegate respondsToSelector:@selector(loginViewControllerWillAttemptToLogUserIn:)]) { + [(id)self.delegate loginViewControllerWillAttemptToLogUserIn:self]; + } + + self.attemptingLogin = YES; + + // all of our open calls use the same handler + FBSessionStateHandler handler = ^(FBSession *session, FBSessionState state, NSError *error) { + [self sessionStateChanged:session state:state error:error]; + }; + + // the policy here is: + // 1) if you provide unspecified permissions, then we fall back on legacy fast-app-switch + // 2) if you provide only read permissions, then we call a read-based open method that will use integrated auth + // 3) if you provide any publish permissions, then we combine the read-set and publish-set and call the publish-based + // method that will use integrated auth when availab le + // 4) if you provide any publish permissions, and don't specify a valid audience, the control will throw an exception + // when the user presses login + if (self.permissions) { + [FBSession openActiveSessionWithPermissions:self.permissions + allowLoginUI:YES + completionHandler:handler]; + } else if (![self.publishPermissions count]) { + [FBSession openActiveSessionWithReadPermissions:self.publishPermissions + allowLoginUI:YES + completionHandler:handler]; + } else { + // combined read and publish permissions will usually fail, but if the app wants us to + // try it here, then we will pass the aggregate set to the server + NSArray *permissions = self.publishPermissions; + if ([self.readPermissions count]) { + NSMutableSet *set = [NSMutableSet setWithArray:self.publishPermissions]; + [set addObjectsFromArray:self.readPermissions]; + permissions = [set allObjects]; + } + [FBSession openActiveSessionWithPublishPermissions:permissions + defaultAudience:self.defaultAudience + allowLoginUI:YES + completionHandler:handler]; + } +} +#pragma GCC diagnostic warning "-Wdeprecated-declarations" + +#pragma mark Handlers + +- (void)loginLogoutButtonPressed:(id)sender { + if (FBSession.activeSession.isOpen) { + if ([self.delegate respondsToSelector:@selector(loginViewControllerWillLogUserOut:)]) { + [(id)self.delegate loginViewControllerWillLogUserOut:self]; + } + + [FBSession.activeSession closeAndClearTokenInformation]; + + if ([self.delegate respondsToSelector:@selector(loginViewControllerDidLogUserOut:)]) { + [(id)self.delegate loginViewControllerDidLogUserOut:self]; + } + } else { + [self openSession]; + } +} + +- (void)handleActiveSessionStateChanged:(NSNotification *)notification { + [self updateControls]; +} + +@end diff --git a/src/ios/facebook/FBUtility.h b/src/ios/facebook/FBUtility.h new file mode 100644 index 000000000..60244aaa8 --- /dev/null +++ b/src/ios/facebook/FBUtility.h @@ -0,0 +1,56 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import + +@class FBSession; + +@protocol FBGraphObject; + +@interface FBUtility : NSObject + ++ (NSDictionary*)dictionaryByParsingURLQueryPart:(NSString *)encodedString; ++ (NSString *)stringByURLDecodingString:(NSString*)escapedString; ++ (NSString*)stringByURLEncodingString:(NSString*)unescapedString; ++ (id)graphObjectInArray:(NSArray*)array withSameIDAs:(id)item; + ++ (unsigned long)currentTimeInMilliseconds; ++ (NSTimeInterval)randomTimeInterval:(NSTimeInterval)minValue withMaxValue:(NSTimeInterval)maxValue; ++ (void)centerView:(UIView*)view tableView:(UITableView*)tableView; ++ (NSString *)stringFBIDFromObject:(id)object; + ++ (NSBundle *)facebookSDKBundle; ++ (NSString *)localizedStringForKey:(NSString *)key + withDefault:(NSString *)value; ++ (NSString *)localizedStringForKey:(NSString *)key + withDefault:(NSString *)value + inBundle:(NSBundle *)bundle; + +@end + +#define FBConditionalLog(condition, desc, ...) \ +do { \ + if (!(condition)) { \ + NSString *msg = [NSString stringWithFormat:(desc), ##__VA_ARGS__]; \ + NSLog(@"FBConditionalLog: %@", msg); \ + } \ +} while(NO) + +#define FB_BASE_URL @"facebook.com" + + + diff --git a/src/ios/facebook/FBUtility.m b/src/ios/facebook/FBUtility.m new file mode 100644 index 000000000..0a79b6bbe --- /dev/null +++ b/src/ios/facebook/FBUtility.m @@ -0,0 +1,148 @@ +/* + * Copyright 2012 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBUtility.h" +#import "FBGraphObject.h" +#import "FBRequest.h" +#import "FBSBJSON.h" +#import "FBSession.h" +#include + +@implementation FBUtility + +// finishes the parsing job that NSURL starts ++ (NSDictionary*)dictionaryByParsingURLQueryPart:(NSString *)encodedString { + + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + NSArray *parts = [encodedString componentsSeparatedByString:@"&"]; + + for (NSString *part in parts) { + if ([part length] == 0) { + continue; + } + + NSRange index = [part rangeOfString:@"="]; + NSString *key; + NSString *value; + + if (index.location == NSNotFound) { + key = part; + value = @""; + } else { + key = [part substringToIndex:index.location]; + value = [part substringFromIndex:index.location + index.length]; + } + + if (key && value) { + [result setObject:[FBUtility stringByURLDecodingString:value] + forKey:[FBUtility stringByURLDecodingString:key]]; + } + } + return result; +} + +// the reverse of url encoding ++ (NSString*)stringByURLDecodingString:(NSString*)escapedString { + return [[escapedString stringByReplacingOccurrencesOfString:@"+" withString:@" "] + stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; +} + ++ (NSString*)stringByURLEncodingString:(NSString*)unescapedString { + NSString* result = (NSString *)CFURLCreateStringByAddingPercentEscapes( + kCFAllocatorDefault, + (CFStringRef)unescapedString, + NULL, // characters to leave unescaped + (CFStringRef)@":!*();@/&?#[]+$,='%’\"", + kCFStringEncodingUTF8); + [result autorelease]; + return result; +} + ++ (unsigned long)currentTimeInMilliseconds { + struct timeval time; + gettimeofday(&time, NULL); + return (time.tv_sec * 1000) + (time.tv_usec / 1000); +} + ++ (NSTimeInterval)randomTimeInterval:(NSTimeInterval)minValue withMaxValue:(NSTimeInterval)maxValue { + return minValue + (maxValue - minValue) * (double)arc4random() / UINT32_MAX; +} + ++ (id)graphObjectInArray:(NSArray*)array withSameIDAs:(id)item { + for (id obj in array) { + if ([FBGraphObject isGraphObjectID:obj sameAs:item]) { + return obj; + } + } + return nil; +} + +// The assumption here is that the view and the tableView share a common parent. ++ (void)centerView:(UIView*)view tableView:(UITableView*)tableView { + // We want to center the view in the table as much as possible, but we also want to center it + // within a cell so it is visually appealing. + CGRect bounds = tableView.bounds; + CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); + + CGFloat rowHeight = tableView.rowHeight; + int numRows = bounds.size.height / rowHeight; + int centerRow = numRows / 2; + center.y = rowHeight * centerRow + rowHeight / 2; + + center = [view.superview convertPoint:center fromView:tableView]; + view.center = center; +} + ++ (NSString *)stringFBIDFromObject:(id)object { + if ([object isKindOfClass:[NSDictionary class]]) { + id val = [object objectForKey:@"id"]; + if ([val isKindOfClass:[NSString class]]) { + return val; + } + } + return [object description]; +} + ++ (NSBundle *)facebookSDKBundle { + static dispatch_once_t fetchBundleOnce; + static NSBundle *bundle = nil; + + dispatch_once(&fetchBundleOnce, ^{ + NSString *path = [[NSBundle mainBundle] pathForResource:@"FacebookSDKResources" + ofType:@"bundle"]; + bundle = [NSBundle bundleWithPath:path]; + }); + return bundle; +} + ++ (NSString *)localizedStringForKey:(NSString *)key + withDefault:(NSString *)value { + return [self localizedStringForKey:key withDefault:value inBundle:FBUtility.facebookSDKBundle]; +} + ++ (NSString *)localizedStringForKey:(NSString *)key + withDefault:(NSString *)value + inBundle:(NSBundle *)bundle { + NSString *result = value; + if (bundle) { + result = [bundle localizedStringForKey:key + value:value + table:nil]; + } + return result; +} + +@end diff --git a/src/ios/facebook/FBViewController+Internal.h b/src/ios/facebook/FBViewController+Internal.h new file mode 100644 index 000000000..71b17d4a4 --- /dev/null +++ b/src/ios/facebook/FBViewController+Internal.h @@ -0,0 +1,22 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@interface FBViewController (Internal) + +@property (nonatomic, readonly) UIViewController *compatiblePresentingViewController; + +@end + diff --git a/src/ios/facebook/FBViewController.h b/src/ios/facebook/FBViewController.h new file mode 100644 index 000000000..4139a660b --- /dev/null +++ b/src/ios/facebook/FBViewController.h @@ -0,0 +1,123 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +@class FBViewController; + +/*! + @typedef FBModalCompletionHandler + + @abstract + A block that is passed to [FBViewController presentModallyInViewController:animated:handler:] + and called when the view controller is dismissed via either Done or Cancel. + + @discussion + Pass a block of this type when calling addRequest. This will be called once + the request completes. The call occurs on the UI thread. + + @param sender The that is being dismissed. + + @param donePressed If YES, Done was pressed. If NO, Cancel was pressed. + */ +typedef void (^FBModalCompletionHandler)(FBViewController *sender, BOOL donePressed); + +/*! + @protocol + + @abstract + The `FBViewControllerDelegate` protocol defines the methods called when the Cancel or Done + buttons are pressed in a . + */ +@protocol FBViewControllerDelegate + +@optional + +/*! + @abstract + Called when the Cancel button is pressed on a modally-presented . + + @param sender The view controller sending the message. + */ +- (void)facebookViewControllerCancelWasPressed:(id)sender; + +/*! + @abstract + Called when the Done button is pressed on a modally-presented . + + @param sender The view controller sending the message. + */ +- (void)facebookViewControllerDoneWasPressed:(id)sender; + +@end + + +/*! + @class FBViewController + + @abstract + The `FBViewController` class is a base class encapsulating functionality common to several + other view controller classes. Specifically, it provides UI when a view controller is presented + modally, in the form of optional Cancel and Done buttons. + */ +@interface FBViewController : UIViewController + +/*! + @abstract + The Cancel button to display when presented modally. If nil, no Cancel button is displayed. + If this button is provided, its target and action will be redirected to internal handlers, replacing + any previous target that may have been set. + */ +@property (nonatomic, retain) IBOutlet UIBarButtonItem *cancelButton; + +/*! + @abstract + The Done button to display when presented modally. If nil, no Done button is displayed. + If this button is provided, its target and action will be redirected to internal handlers, replacing + any previous target that may have been set. + */ +@property (nonatomic, retain) IBOutlet UIBarButtonItem *doneButton; + +/*! + @abstract + The delegate that will be called when Cancel or Done is pressed. Derived classes may specify + derived types for their delegates that provide additional functionality. + */ +@property (nonatomic, assign) IBOutlet id delegate; + +/*! + @abstract + The view into which derived classes should put their subviews. This view will be resized correctly + depending on whether or not a toolbar is displayed. + */ +@property (nonatomic, readonly, retain) UIView *canvasView; + +/*! + @abstract + Provides a wrapper that presents the view controller modally and automatically dismisses it + when either the Done or Cancel button is pressed. If Done is pressed, the block provided by the + doneHandler parameter is called. + + @param viewController The view controller that is presenting this view controller. + @param animated If YES, presenting and dismissing the view controller is animated. + @param handler The block called when the Done or Cancel button is pressed. + */ +- (void)presentModallyFromViewController:(UIViewController*)viewController + animated:(BOOL)animated + handler:(FBModalCompletionHandler)handler; + +@end + diff --git a/src/ios/facebook/FBViewController.m b/src/ios/facebook/FBViewController.m new file mode 100644 index 000000000..aa484185a --- /dev/null +++ b/src/ios/facebook/FBViewController.m @@ -0,0 +1,324 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBViewController.h" +#import "FBViewController+Internal.h" + +@interface FBViewController () + +@property (nonatomic, retain) UIToolbar *toolbar; +@property (nonatomic, retain) UIView *canvasView; +@property (nonatomic, retain) UIBarButtonItem *titleLabel; +@property (nonatomic, copy) FBModalCompletionHandler handler; +@property (nonatomic) BOOL autoDismiss; +@property (nonatomic) BOOL dismissAnimated; + +- (void)cancelButtonPressed:(id)sender; +- (void)doneButtonPressed:(id)sender; +- (void)updateBarForPresentedMode; +- (void)updateBarForNavigationMode; +- (void)updateBar; + +@end + +@implementation FBViewController + +@synthesize cancelButton = _cancelButton; +@synthesize doneButton = _doneButton; +@synthesize delegate = _delegate; +@synthesize toolbar = _toolbar; +@synthesize canvasView = _canvasView; +@synthesize titleLabel = _titleLabel; +@synthesize handler = _handler; +@synthesize autoDismiss = _autoDismiss; +@synthesize dismissAnimated = _dismissAnimated; + +#pragma mark View controller lifecycle + +- (id)init { + self = [super init]; + if (self) { + // We do this at init-time rather than in viewDidLoad so the caller can change the buttons if + // they want prior to the view loading. + self.cancelButton = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel + target:self + action:@selector(cancelButtonPressed:)] + autorelease]; + self.doneButton = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(doneButtonPressed:)] + autorelease]; + } + return self; +} + +- (void)dealloc { + [super dealloc]; + + [_cancelButton release]; + [_doneButton release]; + [_toolbar release]; + [_canvasView release]; + [_titleLabel release]; + [_handler release]; +} + +#pragma mark View lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.view.autoresizesSubviews = YES; + + self.canvasView = [[[UIView alloc] init] autorelease]; + [self.canvasView setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight]; + + self.canvasView.frame = self.view.bounds; + [self.view addSubview:self.canvasView]; + [self.view sendSubviewToBack:self.canvasView]; + + self.autoDismiss = NO; + + self.doneButton.target = self; + self.doneButton.action = @selector(doneButtonPressed:); + self.cancelButton.target = self; + self.cancelButton.action = @selector(cancelButtonPressed:); + + [self updateBar]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + + // If the view goes away for any reason, nil out the handler to avoid a retain cycle. + self.handler = nil; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + return YES; +} + +#pragma mark Public methods + +- (void)presentModallyFromViewController:(UIViewController*)viewController + animated:(BOOL)animated + handler:(FBModalCompletionHandler)handler { + self.handler = handler; + // Assumption: we want to dismiss with the same animated-ness as we present. + self.dismissAnimated = animated; + + if ([viewController respondsToSelector:@selector(presentViewController:animated:completion:)]) { + [viewController presentViewController:self animated:animated completion:nil]; + } else { + [viewController presentModalViewController:self animated:animated]; + } + + // Set this here because we always revert to NO in viewDidLoad. + self.autoDismiss = YES; +} + +#pragma mark Implementation + +- (void)updateBar { + if (self.compatiblePresentingViewController != nil) { + [self updateBarForPresentedMode]; + } else if (self.navigationController != nil) { + [self updateBarForNavigationMode]; + } +} + +- (void)updateBarForPresentedMode { + BOOL needBar = (self.doneButton != nil) || (self.cancelButton != nil); + if (needBar) { + // If we need a bar but don't have one, create it. + if (self.toolbar == nil) { + self.toolbar = [[[UIToolbar alloc] init] autorelease]; + self.toolbar.barStyle = UIBarStyleDefault; + + [self.toolbar setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; + + [self.view addSubview:self.toolbar]; + } + } else { + // If we have a bar but don't need one, get rid of it. + if (self.toolbar != nil) { + [self.toolbar removeFromSuperview]; + self.toolbar = nil; + + self.canvasView.frame = self.view.bounds; + } + return; + } + + NSMutableArray *buttons = [NSMutableArray array]; + if (self.cancelButton != nil) { + [buttons addObject:self.cancelButton]; + } else { + // No cancel button, but if we have a done and a title, add some space at the beginning to help center the title. + if (self.doneButton != nil && self.title.length > 0) { + UIBarButtonItem *space = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:nil] + autorelease]; + [buttons addObject:space]; + } + } + if (self.title.length > 0) { + UIBarButtonItem *space = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:nil] + autorelease]; + [buttons addObject:space]; + + if (self.titleLabel == nil) { + UILabel *label = [[[UILabel alloc] init] autorelease]; + label.font = [UIFont fontWithName:@"Helvetica-Bold" size:20]; + label.backgroundColor = [UIColor clearColor]; + label.textColor = [UIColor colorWithRed:255.0/255.0 green:255.0/255.0 blue:255.0/255.0 alpha:1.0]; + label.textAlignment = UITextAlignmentCenter; + label.shadowColor = [UIColor colorWithWhite:0.0 alpha:0.5]; + + self.titleLabel = [[[UIBarButtonItem alloc] initWithCustomView:label] autorelease]; + } + [(UILabel*)self.titleLabel.customView setText:self.title]; + [self.titleLabel.customView sizeToFit]; + + [buttons addObject:self.titleLabel]; + + space = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:nil] + autorelease]; + [buttons addObject:space]; + + } + + if (self.doneButton != nil) { + // If no title, we need a space to right-align + if (self.title.length == 0) { + UIBarButtonItem *space = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:nil] + autorelease]; + [buttons addObject:space]; + } + [buttons addObject:self.doneButton]; + } else { + // No done button, but if we have a cancel and a title, add some space at the end to help center the title. + if (self.cancelButton != nil && self.title.length > 0) { + UIBarButtonItem *space = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:nil] + autorelease]; + [buttons addObject:space]; + } + } + + [self.toolbar sizeToFit]; + CGRect bounds = self.toolbar.bounds; + bounds = CGRectMake(0, 0, self.view.bounds.size.width, bounds.size.height); + self.toolbar.bounds = bounds; + + // Make the canvas shorter to account for the toolbar. + bounds = self.view.bounds; + CGFloat toolbarHeight = self.toolbar.bounds.size.height; + bounds.origin.y += toolbarHeight; + bounds.size.height -= toolbarHeight; + self.canvasView.frame = bounds; + + self.toolbar.items = buttons; +} + +- (void)updateBarForNavigationMode { + self.navigationItem.rightBarButtonItem = self.doneButton; +} + +- (void)setCancelButton:(UIBarButtonItem *)cancelButton { + if (_cancelButton != cancelButton) { + [_cancelButton release]; + _cancelButton = [cancelButton retain]; + [self updateBar]; + } +} + +- (void)setDoneButton:(UIBarButtonItem *)doneButton { + if (_doneButton != doneButton) { + [_doneButton release]; + _doneButton = [doneButton retain]; + [self updateBar]; + } +} + +- (void)setTitle:(NSString *)title { + [super setTitle:title]; + [self updateBar]; +} + +- (UIViewController *)compatiblePresentingViewController { + if ([self respondsToSelector:@selector(presentingViewController)]) { + return [self presentingViewController]; + } else { + UIViewController *parentViewController = [self parentViewController]; + if (self == [parentViewController modalViewController]) { + return parentViewController; + } + } + return nil; +} + +#pragma mark Handlers + +- (void)cancelButtonPressed:(id)sender { + if ([self.delegate respondsToSelector:@selector(facebookViewControllerCancelWasPressed:)]) { + [self.delegate facebookViewControllerCancelWasPressed:self]; + } + + UIViewController *presentingViewController = [self compatiblePresentingViewController]; + if (self.autoDismiss && presentingViewController) { + if ([presentingViewController respondsToSelector:@selector(dismissViewControllerAnimated:completion:)]) { + [presentingViewController dismissViewControllerAnimated:self.dismissAnimated completion:nil]; + } else { + [presentingViewController dismissModalViewControllerAnimated:self.dismissAnimated]; + } + + if (self.handler) { + self.handler(self, NO); + } + } +} + +- (void)doneButtonPressed:(id)sender { + if ([self.delegate respondsToSelector:@selector(facebookViewControllerDoneWasPressed:)]) { + [self.delegate facebookViewControllerDoneWasPressed:self]; + } + + UIViewController *presentingViewController = [self compatiblePresentingViewController]; + if (self.autoDismiss && presentingViewController) { + if ([presentingViewController respondsToSelector:@selector(dismissViewControllerAnimated:completion:)]) { + [presentingViewController dismissViewControllerAnimated:self.dismissAnimated completion:nil]; + } else { + [presentingViewController dismissModalViewControllerAnimated:self.dismissAnimated]; + } + + if (self.handler) { + self.handler(self, YES); + } + } +} + + +@end diff --git a/src/ios/facebook/Facebook.h b/src/ios/facebook/Facebook.h new file mode 100644 index 000000000..8818cfa0c --- /dev/null +++ b/src/ios/facebook/Facebook.h @@ -0,0 +1,281 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FBLoginDialog.h" +#import "FBRequest.h" +#import "FBSessionManualTokenCachingStrategy.h" +#import "FBFrictionlessRequestSettings.h" +#import "FacebookSDK.h" + +//////////////////////////////////////////////////////////////////////////////// +// deprecated API +// +// Summary +// The classes, protocols, etc. in this header are provided for backward +// compatibility and migration; for new code, use FacebookSDK.h, and/or the +// public headers that it imports; for existing code under active development, +// Facebook.h imports FacebookSDK.h, and updates should favor the new interfaces +// whenever possible + +// up-front decl's +@class FBFrictionlessRequestSettings; +@protocol FBRequestDelegate; +@protocol FBSessionDelegate; + +/** + * Main Facebook interface for interacting with the Facebook developer API. + * Provides methods to log in and log out a user, make requests using the REST + * and Graph APIs, and start user interface interactions (such as + * pop-ups promoting for credentials, permissions, stream posts, etc.) + */ +@interface Facebook : NSObject{ + id _sessionDelegate; + NSMutableSet* _requests; + FBSession* _session; + FBSessionManualTokenCachingStrategy *_tokenCaching; + FBDialog* _fbDialog; + NSString* _appId; + NSString* _urlSchemeSuffix; + BOOL _isExtendingAccessToken; + FBRequest *_requestExtendingAccessToken; + NSDate* _lastAccessTokenUpdate; + FBFrictionlessRequestSettings* _frictionlessRequestSettings; +} + +@property(nonatomic, copy) NSString* accessToken; +@property(nonatomic, copy) NSDate* expirationDate; +@property(nonatomic, assign) id sessionDelegate; +@property(nonatomic, copy) NSString* urlSchemeSuffix; +@property(nonatomic, readonly) BOOL isFrictionlessRequestsEnabled; +@property(nonatomic, readonly, retain) FBSession *session; + +- (id)initWithAppId:(NSString *)appId + andDelegate:(id)delegate; + +- (id)initWithAppId:(NSString *)appId + urlSchemeSuffix:(NSString *)urlSchemeSuffix + andDelegate:(id)delegate; + +- (void)authorize:(NSArray *)permissions; + +- (void)extendAccessToken; + +- (void)extendAccessTokenIfNeeded; + +- (BOOL)shouldExtendAccessToken; + +- (BOOL)handleOpenURL:(NSURL *)url; + +- (void)logout; + +- (void)logout:(id)delegate; + +- (FBRequest*)requestWithParams:(NSMutableDictionary *)params + andDelegate:(id )delegate; + +- (FBRequest*)requestWithMethodName:(NSString *)methodName + andParams:(NSMutableDictionary *)params + andHttpMethod:(NSString *)httpMethod + andDelegate:(id )delegate; + +- (FBRequest*)requestWithGraphPath:(NSString *)graphPath + andDelegate:(id )delegate; + +- (FBRequest*)requestWithGraphPath:(NSString *)graphPath + andParams:(NSMutableDictionary *)params + andDelegate:(id )delegate; + +- (FBRequest*)requestWithGraphPath:(NSString *)graphPath + andParams:(NSMutableDictionary *)params + andHttpMethod:(NSString *)httpMethod + andDelegate:(id )delegate; + +- (void)dialog:(NSString *)action + andDelegate:(id)delegate; + +- (void)dialog:(NSString *)action + andParams:(NSMutableDictionary *)params + andDelegate:(id )delegate; + +- (BOOL)isSessionValid; + +- (void)enableFrictionlessRequests; + +- (void)reloadFrictionlessRecipientCache; + +- (BOOL)isFrictionlessEnabledForRecipient:(id)fbid; + +- (BOOL)isFrictionlessEnabledForRecipients:(NSArray*)fbids; + +@end + +//////////////////////////////////////////////////////////////////////////////// + +/** + * Your application should implement this delegate to receive session callbacks. + */ +@protocol FBSessionDelegate + +/** + * Called when the user successfully logged in. + */ +- (void)fbDidLogin; + +/** + * Called when the user dismissed the dialog without logging in. + */ +- (void)fbDidNotLogin:(BOOL)cancelled; + +/** + * Called after the access token was extended. If your application has any + * references to the previous access token (for example, if your application + * stores the previous access token in persistent storage), your application + * should overwrite the old access token with the new one in this method. + * See extendAccessToken for more details. + */ +- (void)fbDidExtendToken:(NSString*)accessToken + expiresAt:(NSDate*)expiresAt; + +/** + * Called when the user logged out. + */ +- (void)fbDidLogout; + +/** + * Called when the current session has expired. This might happen when: + * - the access token expired + * - the app has been disabled + * - the user revoked the app's permissions + * - the user changed his or her password + */ +- (void)fbSessionInvalidated; + +@end + +@protocol FBRequestDelegate; + +enum { + kFBRequestStateReady, + kFBRequestStateLoading, + kFBRequestStateComplete, + kFBRequestStateError +}; + +// FBRequest(Deprecated) +// +// Summary +// The deprecated category is used to maintain back compat and ease migration +// to the revised SDK for iOS + +/** + * Do not use this interface directly, instead, use method in Facebook.h + */ +@interface FBRequest(Deprecated) + +@property(nonatomic,assign) id delegate; + +/** + * The URL which will be contacted to execute the request. + */ +@property(nonatomic,copy) NSString* url; + +/** + * The API method which will be called. + */ +@property(nonatomic,copy) NSString* httpMethod; + +/** + * The dictionary of parameters to pass to the method. + * + * These values in the dictionary will be converted to strings using the + * standard Objective-C object-to-string conversion facilities. + */ +@property(nonatomic,retain) NSMutableDictionary* params; +@property(nonatomic,retain) NSURLConnection* connection; +@property(nonatomic,retain) NSMutableData* responseText; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +@property(nonatomic) FBRequestState state; +#pragma GCC diagnostic pop +@property(nonatomic) BOOL sessionDidExpire; + +/** + * Error returned by the server in case of request's failure (or nil otherwise). + */ +@property(nonatomic,retain) NSError* error; + +- (BOOL) loading; + ++ (NSString *)serializeURL:(NSString *)baseUrl + params:(NSDictionary *)params; + ++ (NSString*)serializeURL:(NSString *)baseUrl + params:(NSDictionary *)params + httpMethod:(NSString *)httpMethod; + +@end + +//////////////////////////////////////////////////////////////////////////////// + +/* + *Your application should implement this delegate + */ +@protocol FBRequestDelegate + +@optional + +/** + * Called just before the request is sent to the server. + */ +- (void)requestLoading:(FBRequest *)request; + +/** + * Called when the Facebook API request has returned a response. + * + * This callback gives you access to the raw response. It's called before + * (void)request:(FBRequest *)request didLoad:(id)result, + * which is passed the parsed response object. + */ +- (void)request:(FBRequest *)request didReceiveResponse:(NSURLResponse *)response; + +/** + * Called when an error prevents the request from completing successfully. + */ +- (void)request:(FBRequest *)request didFailWithError:(NSError *)error; + +/** + * Called when a request returns and its response has been parsed into + * an object. + * + * The resulting object may be a dictionary, an array or a string, depending + * on the format of the API response. If you need access to the raw response, + * use: + * + * (void)request:(FBRequest *)request + * didReceiveResponse:(NSURLResponse *)response + */ +- (void)request:(FBRequest *)request didLoad:(id)result; + +/** + * Called when a request returns a response. + * + * The result object is the raw response from the server of type NSData + */ +- (void)request:(FBRequest *)request didLoadRawResponse:(NSData *)data; + +@end + + diff --git a/src/ios/facebook/Facebook.m b/src/ios/facebook/Facebook.m new file mode 100644 index 000000000..cd4673ab4 --- /dev/null +++ b/src/ios/facebook/Facebook.m @@ -0,0 +1,826 @@ +/* + * Copyright 2010 Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Facebook.h" +#import "FBFrictionlessRequestSettings.h" +#import "FBLoginDialog.h" +#import "FBRequest.h" +#import "FBError.h" +#import "FBSessionManualTokenCachingStrategy.h" +#import "FBSBJSON.h" +#import "FBSession+Internal.h" +#import "FBUtility.h" + +static NSString* kDialogBaseURL = @"https://m." FB_BASE_URL "/dialog/"; +static NSString* kGraphBaseURL = @"https://graph." FB_BASE_URL "/"; +static NSString* kRestserverBaseURL = @"https://api." FB_BASE_URL "/method/"; + +static NSString* kFBAppAuthURLScheme = @"fbauth"; +static NSString* kFBAppAuthURLPath = @"authorize"; +static NSString* kRedirectURL = @"fbconnect://success"; + +static NSString* kLogin = @"oauth"; +static NSString* kApprequests = @"apprequests"; +static NSString* kSDKVersion = @"2"; + +// If the last time we extended the access token was more than 24 hours ago +// we try to refresh the access token again. +static const int kTokenExtendThreshold = 24; + +static NSString *requestFinishedKeyPath = @"state"; +static void *finishedContext = @"finishedContext"; +static void *tokenContext = @"tokenContext"; + +// the following const strings name properties for which KVO is manually handled +static NSString *const FBaccessTokenPropertyName = @"accessToken"; +static NSString *const FBexpirationDatePropertyName = @"expirationDate"; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface Facebook () + +// private properties +@property(nonatomic, copy) NSString* appId; +// session and tokenCaching object implement login logic and token state in Facebook class +@property(nonatomic, readwrite, retain) FBSession *session; +@property(nonatomic) BOOL hasUpdatedAccessToken; +@property(nonatomic, retain) FBSessionManualTokenCachingStrategy *tokenCaching; + +@end + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation Facebook + +@synthesize sessionDelegate = _sessionDelegate, + urlSchemeSuffix = _urlSchemeSuffix, + appId = _appId, + session = _session, + hasUpdatedAccessToken = _hasUpdatedAccessToken, + tokenCaching = _tokenCaching; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// private + + +- (id)initWithAppId:(NSString *)appId + andDelegate:(id)delegate { + self = [self initWithAppId:appId urlSchemeSuffix:nil andDelegate:delegate]; + return self; +} + +/** + * Initialize the Facebook object with application ID. + * + * @param appId the facebook app id + * @param urlSchemeSuffix + * urlSchemeSuffix is a string of lowercase letters that is + * appended to the base URL scheme used for Facebook Login. For example, + * if your facebook ID is "350685531728" and you set urlSchemeSuffix to + * "abcd", the Facebook app will expect your application to bind to + * the following URL scheme: "fb350685531728abcd". + * This is useful if your have multiple iOS applications that + * share a single Facebook application id (for example, if you + * have a free and a paid version on the same app) and you want + * to use Facebook Login with both apps. Giving both apps different + * urlSchemeSuffix values will allow the Facebook app to disambiguate + * their URL schemes and always redirect the user back to the + * correct app, even if both the free and the app is installed + * on the device. + * urlSchemeSuffix is supported on version 3.4.1 and above of the Facebook + * app. If the user has an older version of the Facebook app + * installed and your app uses urlSchemeSuffix parameter, the SDK will + * proceed as if the Facebook app isn't installed on the device + * and redirect the user to Safari. + * @param delegate the FBSessionDelegate + */ +- (id)initWithAppId:(NSString *)appId + urlSchemeSuffix:(NSString *)urlSchemeSuffix + andDelegate:(id)delegate { + + self = [super init]; + if (self) { + _requests = [[NSMutableSet alloc] init]; + _lastAccessTokenUpdate = [[NSDate distantPast] retain]; + _frictionlessRequestSettings = [[FBFrictionlessRequestSettings alloc] init]; + _tokenCaching = [[FBSessionManualTokenCachingStrategy alloc] init]; + self.appId = appId; + self.sessionDelegate = delegate; + self.urlSchemeSuffix = urlSchemeSuffix; + + // observe tokenCaching properties so we can forward KVO + [self.tokenCaching addObserver:self + forKeyPath:FBaccessTokenPropertyName + options:NSKeyValueObservingOptionPrior + context:tokenContext]; + [self.tokenCaching addObserver:self + forKeyPath:FBexpirationDatePropertyName + options:NSKeyValueObservingOptionPrior + context:tokenContext]; + } + return self; +} + +/** + * Override NSObject : free the space + */ +- (void)dealloc { + + // this is the one case where the delegate is this object + _requestExtendingAccessToken.delegate = nil; + + [_session release]; + [_tokenCaching release]; + + for (FBRequest* _request in _requests) { + [_request removeObserver:self forKeyPath:requestFinishedKeyPath]; + } + [_lastAccessTokenUpdate release]; + [_requests release]; + _fbDialog.delegate = nil; + [_fbDialog release]; + [_appId release]; + [_urlSchemeSuffix release]; + [_frictionlessRequestSettings release]; + [super dealloc]; +} + +- (void)invalidateSession { + + [self.session close]; + [self.tokenCaching clearToken]; + + [FBSession deleteFacebookCookies]; + + // setting to nil also terminates any active request for whitelist + [_frictionlessRequestSettings updateRecipientCacheWithRecipients:nil]; +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +- (void)observeFinishedContextValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change { + FBRequest* _request = (FBRequest*)object; + FBRequestState requestState = [_request state]; + if (requestState == kFBRequestStateComplete) { + if ([_request sessionDidExpire]) { + [self invalidateSession]; + if ([self.sessionDelegate respondsToSelector:@selector(fbSessionInvalidated)]) { + [self.sessionDelegate fbSessionInvalidated]; + } + } + [_request removeObserver:self forKeyPath:requestFinishedKeyPath]; + [_requests removeObject:_request]; + } + +} +#pragma GCC diagnostic pop + +- (void)observeTokenContextValueForKeyPath:(NSString *)keyPath + change:(NSDictionary *)change { + // here we are forwarding KVO notifications from an inner object + if ([change objectForKey:NSKeyValueChangeNotificationIsPriorKey]) { + [self willChangeValueForKey:keyPath]; + } else { + [self didChangeValueForKey:keyPath]; + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + // dispatch for various observe cases + if (context == finishedContext) { + [self observeFinishedContextValueForKeyPath:keyPath + ofObject:object + change:change]; + } else if (context == tokenContext) { + [self observeTokenContextValueForKeyPath:keyPath + change:change]; + } else { + [super observeValueForKeyPath:keyPath + ofObject:object + change:change + context:context]; + } +} + +/** + * A private function for getting the app's base url. + */ +- (NSString *)getOwnBaseUrl { + return [NSString stringWithFormat:@"fb%@%@://authorize", + _appId, + _urlSchemeSuffix ? _urlSchemeSuffix : @""]; +} + +/** + * A function for parsing URL parameters. + */ +- (NSDictionary*)parseURLParams:(NSString *)query { + NSArray *pairs = [query componentsSeparatedByString:@"&"]; + NSMutableDictionary *params = [[[NSMutableDictionary alloc] init] autorelease]; + for (NSString *pair in pairs) { + NSArray *kv = [pair componentsSeparatedByString:@"="]; + NSString *val = + [[kv objectAtIndex:1] + stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + + [params setObject:val forKey:[kv objectAtIndex:0]]; + } + return params; +} + +- (void)updateSessionIfTokenUpdated { + if (self.hasUpdatedAccessToken) { + self.hasUpdatedAccessToken = NO; + + // invalidate current session and create a new one with the same permissions + NSArray *permissions = self.session.permissions; + [self.session close]; + self.session = [[[FBSession alloc] initWithAppID:_appId + permissions:permissions + urlSchemeSuffix:_urlSchemeSuffix + tokenCacheStrategy:self.tokenCaching] + autorelease]; + + // get the session into a valid state + [self.session openWithCompletionHandler:nil]; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +//public + +/** + * Starts a dialog which prompts the user to log in to Facebook and grant + * the requested permissions to the application. + * + * If the device supports multitasking, we use fast app switching to show + * the dialog in the Facebook app or, if the Facebook app isn't installed, + * in Safari (this enables Facebook Login by allowing multiple apps on + * the device to share the same user session). + * When the user grants or denies the permissions, the app that + * showed the dialog (the Facebook app or Safari) redirects back to + * the calling application, passing in the URL the access token + * and/or any other parameters the Facebook backend includes in + * the result (such as an error code if an error occurs). + * + * See http://developers.facebook.com/docs/authentication/ for more details. + * + * Also note that requests may be made to the API without calling + * authorize() first, in which case only public information is returned. + * + * @param permissions + * A list of permission required for this application: e.g. + * "read_stream", "publish_stream", or "offline_access". see + * http://developers.facebook.com/docs/authentication/permissions + * This parameter should not be null -- if you do not require any + * permissions, then pass in an empty String array. + * @param delegate + * Callback interface for notifying the calling application when + * the user has logged in. + */ +- (void)authorize:(NSArray *)permissions { + + // if we already have a session, git rid of it + [self.session close]; + self.session = nil; + [self.tokenCaching clearToken]; + + self.session = [[[FBSession alloc] initWithAppID:_appId + permissions:permissions + urlSchemeSuffix:_urlSchemeSuffix + tokenCacheStrategy:self.tokenCaching] + autorelease]; + + [self.session openWithCompletionHandler:^(FBSession *session, FBSessionState status, NSError *error) { + switch (status) { + case FBSessionStateOpen: + // call the legacy session delegate + [self fbDialogLogin:session.accessToken expirationDate:session.expirationDate]; + break; + case FBSessionStateClosedLoginFailed: + { // prefer to keep decls near to their use + + // unpack the error code and reason in order to compute cancel bool + NSString *errorCode = [[error userInfo] objectForKey:FBErrorLoginFailedOriginalErrorCode]; + NSString *errorReason = [[error userInfo] objectForKey:FBErrorLoginFailedReason]; + BOOL userDidCancel = !errorCode && (!errorReason || + [errorReason isEqualToString:FBErrorLoginFailedReasonInlineCancelledValue]); + + // call the legacy session delegate + [self fbDialogNotLogin:userDidCancel]; + } + break; + // presently extension, log-out and invalidation are being implemented in the Facebook class + default: + break; // so we do nothing in response to those state transitions + } + }]; + } + +-(NSString*)accessToken { + return self.tokenCaching.accessToken; +} + +-(void)setAccessToken:(NSString *)accessToken { + self.tokenCaching.accessToken = accessToken; + self.hasUpdatedAccessToken = YES; +} + +-(NSDate*)expirationDate { + return self.tokenCaching.expirationDate; +} + +-(void)setExpirationDate:(NSDate *)expirationDate { + self.tokenCaching.expirationDate = expirationDate; + self.hasUpdatedAccessToken = YES; +} + +/** + * Attempt to extend the access token. + * + * Access tokens typically expire within 30-60 days. When the user uses the + * app, the app should periodically try to obtain a new access token. Once an + * access token has expired, the app can no longer renew it. The app then has + * to ask the user to re-authorize it to obtain a new access token. + * + * To ensure your app always has a fresh access token for active users, it's + * recommended that you call extendAccessTokenIfNeeded in your application's + * applicationDidBecomeActive: UIApplicationDelegate method. + */ +- (void)extendAccessToken { + if (_isExtendingAccessToken) { + return; + } + _isExtendingAccessToken = YES; + NSMutableDictionary* params = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"auth.extendSSOAccessToken", @"method", + nil]; + _requestExtendingAccessToken = [self requestWithParams:params andDelegate:self]; +} + +/** + * Calls extendAccessToken if shouldExtendAccessToken returns YES. + */ +- (void)extendAccessTokenIfNeeded { + if ([self shouldExtendAccessToken]) { + [self extendAccessToken]; + } +} + +/** + * Returns YES if the last time a new token was obtained was over 24 hours ago. + */ +- (BOOL)shouldExtendAccessToken { + if ([self isSessionValid]){ + NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease]; + NSDateComponents *components = [calendar components:NSHourCalendarUnit + fromDate:_lastAccessTokenUpdate + toDate:[NSDate date] + options:0]; + + if (components.hour >= kTokenExtendThreshold) { + return YES; + } + } + return NO; +} + +/** + * This function processes the URL the Facebook application or Safari used to + * open your application during a Facebook Login flow. + * + * You MUST call this function in your UIApplicationDelegate's handleOpenURL + * method (see + * http://developer.apple.com/library/ios/#documentation/uikit/reference/UIApplicationDelegate_Protocol/Reference/Reference.html + * for more info). + * + * This will ensure that the authorization process will proceed smoothly once the + * Facebook application or Safari redirects back to your application. + * + * @param URL the URL that was passed to the application delegate's handleOpenURL method. + * + * @return YES if the URL starts with 'fb[app_id]://authorize and hence was handled + * by SDK, NO otherwise. + */ +- (BOOL)handleOpenURL:(NSURL *)url { + return [self.session handleOpenURL:url]; +} + +/** + * Invalidate the current user session by removing the access token in + * memory and clearing the browser cookie. + * + * Note that this method dosen't unauthorize the application -- + * it just removes the access token. To unauthorize the application, + * the user must remove the app in the app settings page under the privacy + * settings screen on facebook.com. + */ +- (void)logout { + [self invalidateSession]; + + if ([self.sessionDelegate respondsToSelector:@selector(fbDidLogout)]) { + [self.sessionDelegate fbDidLogout]; + } +} + +/** + * Invalidate the current user session by removing the access token in + * memory and clearing the browser cookie. + * + * @deprecated Use of a single session delegate, set at app init, is preferred + */ +- (void)logout:(id)delegate { + [self logout]; + // preserve deprecated callback behavior, but leave cached delegate intact + // avoid calling twice if the passed and cached delegates are the same + if (delegate != self.sessionDelegate && + [delegate respondsToSelector:@selector(fbDidLogout)]) { + [delegate fbDidLogout]; + } +} + +/** + * Make a request to Facebook's REST API with the given + * parameters. One of the parameter keys must be "method" and its value + * should be a valid REST server API method. + * + * See http://developers.facebook.com/docs/reference/rest/ + * + * @param parameters + * Key-value pairs of parameters to the request. Refer to the + * documentation: one of the parameters must be "method". + * @param delegate + * Callback interface for notifying the calling application when + * the request has received response + * @return FBRequest* + * Returns a pointer to the FBRequest object. + */ +- (FBRequest*)requestWithParams:(NSMutableDictionary *)params + andDelegate:(id )delegate { + if ([params objectForKey:@"method"] == nil) { + NSLog(@"API Method must be specified"); + return nil; + } + + NSString * methodName = [params objectForKey:@"method"]; + [params removeObjectForKey:@"method"]; + + return [self requestWithMethodName:methodName + andParams:params + andHttpMethod:@"GET" + andDelegate:delegate]; +} + +/** + * Make a request to Facebook's REST API with the given method name and + * parameters. + * + * See http://developers.facebook.com/docs/reference/rest/ + * + * + * @param methodName + * a valid REST server API method. + * @param parameters + * Key-value pairs of parameters to the request. Refer to the + * documentation: one of the parameters must be "method". To upload + * a file, you should specify the httpMethod to be "POST" and the + * “params” you passed in should contain a value of the type + * (UIImage *) or (NSData *) which contains the content that you + * want to upload + * @param delegate + * Callback interface for notifying the calling application when + * the request has received response + * @return FBRequest* + * Returns a pointer to the FBRequest object. + */ +- (FBRequest*)requestWithMethodName:(NSString *)methodName + andParams:(NSMutableDictionary *)params + andHttpMethod:(NSString *)httpMethod + andDelegate:(id )delegate { + [self updateSessionIfTokenUpdated]; + [self extendAccessTokenIfNeeded]; + + FBRequest *request = [[FBRequest alloc] initWithSession:self.session + restMethod:methodName + parameters:params + HTTPMethod:httpMethod]; + [request setDelegate:delegate]; + [request startWithCompletionHandler:nil]; + + return request; +} + +/** + * Make a request to the Facebook Graph API without any parameters. + * + * See http://developers.facebook.com/docs/api + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param delegate + * Callback interface for notifying the calling application when + * the request has received response + * @return FBRequest* + * Returns a pointer to the FBRequest object. + */ +- (FBRequest*)requestWithGraphPath:(NSString *)graphPath + andDelegate:(id )delegate { + + return [self requestWithGraphPath:graphPath + andParams:[NSMutableDictionary dictionary] + andHttpMethod:@"GET" + andDelegate:delegate]; +} + +/** + * Make a request to the Facebook Graph API with the given string + * parameters using an HTTP GET (default method). + * + * See http://developers.facebook.com/docs/api + * + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param parameters + * key-value string parameters, e.g. the path "search" with + * parameters "q" : "facebook" would produce a query for the + * following graph resource: + * https://graph.facebook.com/search?q=facebook + * @param delegate + * Callback interface for notifying the calling application when + * the request has received response + * @return FBRequest* + * Returns a pointer to the FBRequest object. + */ +- (FBRequest*)requestWithGraphPath:(NSString *)graphPath + andParams:(NSMutableDictionary *)params + andDelegate:(id )delegate { + + return [self requestWithGraphPath:graphPath + andParams:params + andHttpMethod:@"GET" + andDelegate:delegate]; +} + +/** + * Make a request to the Facebook Graph API with the given + * HTTP method and string parameters. Note that binary data parameters + * (e.g. pictures) are not yet supported by this helper function. + * + * See http://developers.facebook.com/docs/api + * + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param parameters + * key-value string parameters, e.g. the path "search" with + * parameters {"q" : "facebook"} would produce a query for the + * following graph resource: + * https://graph.facebook.com/search?q=facebook + * To upload a file, you should specify the httpMethod to be + * "POST" and the “params” you passed in should contain a value + * of the type (UIImage *) or (NSData *) which contains the + * content that you want to upload + * @param httpMethod + * http verb, e.g. "GET", "POST", "DELETE" + * @param delegate + * Callback interface for notifying the calling application when + * the request has received response + * @return FBRequest* + * Returns a pointer to the FBRequest object. + */ +- (FBRequest*)requestWithGraphPath:(NSString *)graphPath + andParams:(NSMutableDictionary *)params + andHttpMethod:(NSString *)httpMethod + andDelegate:(id )delegate { + [self updateSessionIfTokenUpdated]; + [self extendAccessTokenIfNeeded]; + + FBRequest *request = [[FBRequest alloc] initWithSession:self.session + graphPath:graphPath + parameters:params + HTTPMethod:httpMethod]; + [request setDelegate:delegate]; + [request startWithCompletionHandler:nil]; + + return request; +} + +/** + * Generate a UI dialog for the request action. + * + * @param action + * String representation of the desired method: e.g. "login", + * "feed", ... + * @param delegate + * Callback interface to notify the calling application when the + * dialog has completed. + */ +- (void)dialog:(NSString *)action + andDelegate:(id)delegate { + NSMutableDictionary * params = [NSMutableDictionary dictionary]; + [self dialog:action andParams:params andDelegate:delegate]; +} + +/** + * Generate a UI dialog for the request action with the provided parameters. + * + * @param action + * String representation of the desired method: e.g. "login", + * "feed", ... + * @param parameters + * key-value string parameters + * @param delegate + * Callback interface to notify the calling application when the + * dialog has completed. + */ +- (void)dialog:(NSString *)action + andParams:(NSMutableDictionary *)params + andDelegate:(id )delegate { + + [_fbDialog release]; + + NSString *dialogURL = [kDialogBaseURL stringByAppendingString:action]; + [params setObject:@"touch" forKey:@"display"]; + [params setObject:kSDKVersion forKey:@"sdk"]; + [params setObject:kRedirectURL forKey:@"redirect_uri"]; + + if ([action isEqualToString:kLogin]) { + [params setObject:@"user_agent" forKey:@"type"]; + _fbDialog = [[FBLoginDialog alloc] initWithURL:dialogURL loginParams:params delegate:self]; + } else { + [params setObject:_appId forKey:@"app_id"]; + if ([self isSessionValid]) { + [params setValue:[self.accessToken stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] + forKey:@"access_token"]; + [self extendAccessTokenIfNeeded]; + } + + // by default we show dialogs, frictionless cases may have a hidden view + BOOL invisible = NO; + + // frictionless handling for application requests + if ([action isEqualToString:kApprequests]) { + // if frictionless requests are enabled + if (self.isFrictionlessRequestsEnabled) { + // 1. show the "Don't show this again for these friends" checkbox + // 2. if the developer is sending a targeted request, then skip the loading screen + [params setValue:@"1" forKey:@"frictionless"]; + // 3. request the frictionless recipient list encoded in the success url + [params setValue:@"1" forKey:@"get_frictionless_recipients"]; + } + + // set invisible if all recipients are enabled for frictionless requests + id fbid = [params objectForKey:@"to"]; + if (fbid != nil) { + // if value parses as a json array expression get the list that way + FBSBJsonParser *parser = [[[FBSBJsonParser alloc] init] autorelease]; + id fbids = [parser objectWithString:fbid]; + if (![fbids isKindOfClass:[NSArray class]]) { + // otherwise seperate by commas (handles the singleton case too) + fbids = [fbid componentsSeparatedByString:@","]; + } + invisible = [self isFrictionlessEnabledForRecipients:fbids]; + } + } + + _fbDialog = [[FBDialog alloc] initWithURL:dialogURL + params:params + isViewInvisible:invisible + frictionlessSettings:_frictionlessRequestSettings + delegate:delegate]; + } + + [_fbDialog show]; +} + +- (BOOL)isFrictionlessRequestsEnabled { + return _frictionlessRequestSettings.enabled; +} + +- (void)enableFrictionlessRequests { + [_frictionlessRequestSettings enableWithFacebook:self]; +} + +- (void)reloadFrictionlessRecipientCache { + [_frictionlessRequestSettings reloadRecipientCacheWithFacebook:self]; +} + +- (BOOL)isFrictionlessEnabledForRecipient:(NSString*)fbid { + return [_frictionlessRequestSettings isFrictionlessEnabledForRecipient:fbid]; +} + +- (BOOL)isFrictionlessEnabledForRecipients:(NSArray*)fbids { + return [_frictionlessRequestSettings isFrictionlessEnabledForRecipients:fbids]; +} + +/** + * @return boolean - whether this object has an non-expired session token + */ +- (BOOL)isSessionValid { + return (self.accessToken != nil && self.expirationDate != nil + && NSOrderedDescending == [self.expirationDate compare:[NSDate date]]); + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +//FBLoginDialogDelegate + +/** + * Set the authToken and expirationDate after login succeed + */ +- (void)fbDialogLogin:(NSString *)token expirationDate:(NSDate *)expirationDate { + [_lastAccessTokenUpdate release]; + _lastAccessTokenUpdate = [[NSDate date] retain]; + [self reloadFrictionlessRecipientCache]; + if ([self.sessionDelegate respondsToSelector:@selector(fbDidLogin)]) { + [self.sessionDelegate fbDidLogin]; + } +} + +/** + * Did not login call the not login delegate + */ +- (void)fbDialogNotLogin:(BOOL)cancelled { + if ([self.sessionDelegate respondsToSelector:@selector(fbDidNotLogin:)]) { + [self.sessionDelegate fbDidNotLogin:cancelled]; + } +} + +#pragma mark - FBRequestDelegate Methods +// These delegate methods are only called for requests that extendAccessToken initiated + +- (void)request:(FBRequest *)request didFailWithError:(NSError *)error { + _isExtendingAccessToken = NO; + _requestExtendingAccessToken = nil; +} + +- (void)request:(FBRequest *)request didLoad:(id)result { + _isExtendingAccessToken = NO; + _requestExtendingAccessToken = nil; + NSString* accessToken = [result objectForKey:@"access_token"]; + NSString* expTime = [result objectForKey:@"expires_at"]; + + if (accessToken == nil || expTime == nil) { + return; + } + + self.accessToken = accessToken; + + NSTimeInterval timeInterval = [expTime doubleValue]; + NSDate *expirationDate = [NSDate distantFuture]; + if (timeInterval != 0) { + expirationDate = [NSDate dateWithTimeIntervalSince1970:timeInterval]; + } + self.expirationDate = expirationDate; + [_lastAccessTokenUpdate release]; + _lastAccessTokenUpdate = [[NSDate date] retain]; + + [self updateSessionIfTokenUpdated]; + + if ([self.sessionDelegate respondsToSelector:@selector(fbDidExtendToken:expiresAt:)]) { + [self.sessionDelegate fbDidExtendToken:accessToken expiresAt:expirationDate]; + } +} + +- (void)request:(FBRequest *)request didLoadRawResponse:(NSData *)data { +} + +- (void)request:(FBRequest *)request didReceiveResponse:(NSURLResponse *)response{ +} + +- (void)requestLoading:(FBRequest *)request{ +} + ++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { + // these properties must manually notify for KVO + if ([key isEqualToString:FBaccessTokenPropertyName] || + [key isEqualToString:FBexpirationDatePropertyName]) { + return NO; + } else { + return [super automaticallyNotifiesObserversForKey:key]; + } +} + +@end diff --git a/src/ios/facebook/FacebookSDK.h b/src/ios/facebook/FacebookSDK.h new file mode 100644 index 000000000..5a10d5b52 --- /dev/null +++ b/src/ios/facebook/FacebookSDK.h @@ -0,0 +1,134 @@ +/* + * Copyright 2012 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// core +#import "FBSession.h" +#import "FBRequest.h" +#import "FBError.h" +#import "FBSettings.h" + +// ux +#import "FBLoginView.h" +#import "FBProfilePictureView.h" +#import "FBUserSettingsViewController.h" +#import "FBPlacePickerViewController.h" +#import "FBFriendPickerViewController.h" +#import "FBCacheDescriptor.h" + +// graph +#import "FBGraphUser.h" +#import "FBGraphPlace.h" +#import "FBGraphLocation.h" +#import "FBGraphObject.h" // + design summary for graph component-group +#import "FBOpenGraphAction.h" + +// ux +#import "FBLoginView.h" +#import "FBProfilePictureView.h" +#import "FBPlacePickerViewController.h" +#import "FBFriendPickerViewController.h" +#import "FBCacheDescriptor.h" +#import "FBNativeDialogs.h" + +/*! + @header + + @abstract Library header, import this to import all of the public types + in the Facebook SDK + + @discussion + +//////////////////////////////////////////////////////////////////////////////// + + + Summary: this header summarizes the structure and goals of the Facebook SDK for iOS. + Goals: + * Leverage and work well with modern features of iOS (e.g. blocks, ARC, etc.) + * Patterned after best of breed iOS frameworks (e.g. naming, pattern-use, etc.) + * Common integration experience is simple & easy to describe + * Factored to enable a growing list of scenarios over time + + Notes on approaches: + 1. We use a key scenario to drive prioritization of work for a given update + 2. We are building-atop and refactoring, rather than replacing, existing iOS SDK releases + 3. We use take an incremental approach where we can choose to maintain as little or as much compatibility with the existing SDK needed + a) and so we will be developing to this approach + b) and then at push-time for a release we will decide when/what to break + on a feature by feature basis + 4. Some light but critical infrastructure is needed to support both the goals + and the execution of this change (e.g. a build/package/deploy process) + + Design points: + We will move to a more object-oriented approach, in order to facilitate the + addition of a different class of objects, such as controls and visual helpers + (e.g. FBLikeView, FBPersonView), as well as sub-frameworks to enable scenarios + such (e.g. FBOpenGraphEntity, FBLocalEntityCache, etc.) + + As we add features, it will no longer be appropriate to host all functionality + in the Facebook class, though it will be maintained for some time for migration + purposes. Instead functionality lives in related collections of classes. + +
+ @textblock
+ 
+               *------------* *----------*  *----------------* *---*
+  Scenario --> |FBPersonView| |FBLikeView|  | FBPlacePicker  | | F |
+               *------------* *----------*  *----------------* | a |
+               *-------------------*  *----------*  *--------* | c |
+ Component --> |   FBGraphObject   |  | FBDialog |  | FBView | | e |
+               *-------------------*  *----------*  *--------* | b |
+               *---------* *---------* *---------------------* | o |
+      Core --> |FBSession| |FBRequest| |Utilities (e.g. JSON)| | o |
+               *---------* *---------* *---------------------* * k *
+
+ @/textblock
+ 
+ + The figure above describes three layers of functionality, with the existing + Facebook on the side as a helper proxy to a subset of the overall SDK. The + layers loosely organize the SDK into *Core Objects* necessary to interface + with Facebook, higher-level *Framework Components* that feel like natural + extensions to existing frameworks such as UIKit and Foundation, but which + surface behavior broadly applicable to Facebook, and finally the + *Scenario Objects*, which provide deeper turn-key capibilities for useful + mobile scenarios. + + Use example (low barrier use case): + +
+ @textblock
+
+// log on to Facebook
+[FBSession sessionOpenWithPermissions:nil
+                    completionHandler:^(FBSession *session, 
+                                        FBSessionState status, 
+                                        NSError *error) {
+                        if (session.isOpen) {
+                            // request basic information for the user
+                            [FBRequestConnection startWithGraphPath:@"me"
+                                                  completionHandler:^void(FBRequestConnection *request, 
+                                                                          id result,
+                                                                          NSError *error) {
+                                                      if (!error) {
+                                                          // get json from result
+                                                      }
+                                                  }];
+                        }
+                    }];
+ @/textblock
+ 
+ + */ diff --git a/src/ios/facebook/JSON/FBSBJSON.h b/src/ios/facebook/JSON/FBSBJSON.h new file mode 100644 index 000000000..64ad57587 --- /dev/null +++ b/src/ios/facebook/JSON/FBSBJSON.h @@ -0,0 +1,75 @@ +/* + Copyright (C) 2007-2009 Stig Brautaset. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import +#import "FBSBJsonParser.h" +#import "FBSBJsonWriter.h" + +/** + @brief Facade for SBJsonWriter/SBJsonParser. + + Requests are forwarded to instances of SBJsonWriter and SBJsonParser. + */ +@interface FBSBJSON : FBSBJsonBase { + +@private + FBSBJsonParser *jsonParser; + FBSBJsonWriter *jsonWriter; +} + + +/// Return the fragment represented by the given string +- (id)fragmentWithString:(NSString*)jsonrep + error:(NSError**)error; + +/// Return the object represented by the given string +- (id)objectWithString:(NSString*)jsonrep + error:(NSError**)error; + +/// Parse the string and return the represented object (or scalar) +- (id)objectWithString:(id)value + allowScalar:(BOOL)x + error:(NSError**)error; + + +/// Return JSON representation of an array or dictionary +- (NSString*)stringWithObject:(id)value + error:(NSError**)error; + +/// Return JSON representation of any legal JSON value +- (NSString*)stringWithFragment:(id)value + error:(NSError**)error; + +/// Return JSON representation (or fragment) for the given object +- (NSString*)stringWithObject:(id)value + allowScalar:(BOOL)x + error:(NSError**)error; + + +@end diff --git a/src/ios/facebook/JSON/FBSBJSON.m b/src/ios/facebook/JSON/FBSBJSON.m new file mode 100644 index 000000000..f9c8db5a5 --- /dev/null +++ b/src/ios/facebook/JSON/FBSBJSON.m @@ -0,0 +1,212 @@ +/* + Copyright (C) 2007-2009 Stig Brautaset. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "FBSBJSON.h" + +@implementation FBSBJSON + +- (id)init { + self = [super init]; + if (self) { + jsonWriter = [FBSBJsonWriter new]; + jsonParser = [FBSBJsonParser new]; + [self setMaxDepth:512]; + + } + return self; +} + +- (void)dealloc { + [jsonWriter release]; + [jsonParser release]; + [super dealloc]; +} + +#pragma mark Writer + + +- (NSString *)stringWithObject:(id)obj { + NSString *repr = [jsonWriter stringWithObject:obj]; + if (repr) + return repr; + + [errorTrace release]; + errorTrace = [[jsonWriter errorTrace] mutableCopy]; + return nil; +} + +/** + Returns a string containing JSON representation of the passed in value, or nil on error. + If nil is returned and @p error is not NULL, @p *error can be interrogated to find the cause of the error. + + @param value any instance that can be represented as a JSON fragment + @param allowScalar wether to return json fragments for scalar objects + @param error used to return an error by reference (pass NULL if this is not desired) + +@deprecated Given we bill ourselves as a "strict" JSON library, this method should be removed. + */ +- (NSString*)stringWithObject:(id)value allowScalar:(BOOL)allowScalar error:(NSError**)error { + + NSString *json = allowScalar ? [jsonWriter stringWithFragment:value] : [jsonWriter stringWithObject:value]; + if (json) + return json; + + [errorTrace release]; + errorTrace = [[jsonWriter errorTrace] mutableCopy]; + + if (error) + *error = [errorTrace lastObject]; + return nil; +} + +/** + Returns a string containing JSON representation of the passed in value, or nil on error. + If nil is returned and @p error is not NULL, @p error can be interrogated to find the cause of the error. + + @param value any instance that can be represented as a JSON fragment + @param error used to return an error by reference (pass NULL if this is not desired) + + @deprecated Given we bill ourselves as a "strict" JSON library, this method should be removed. + */ +- (NSString*)stringWithFragment:(id)value error:(NSError**)error { + return [self stringWithObject:value + allowScalar:YES + error:error]; +} + +/** + Returns a string containing JSON representation of the passed in value, or nil on error. + If nil is returned and @p error is not NULL, @p error can be interrogated to find the cause of the error. + + @param value a NSDictionary or NSArray instance + @param error used to return an error by reference (pass NULL if this is not desired) + */ +- (NSString*)stringWithObject:(id)value error:(NSError**)error { + return [self stringWithObject:value + allowScalar:NO + error:error]; +} + +#pragma mark Parsing + +- (id)objectWithString:(NSString *)repr { + id obj = [jsonParser objectWithString:repr]; + if (obj) + return obj; + + [errorTrace release]; + errorTrace = [[jsonParser errorTrace] mutableCopy]; + + return nil; +} + +/** + Returns the object represented by the passed-in string or nil on error. The returned object can be + a string, number, boolean, null, array or dictionary. + + @param value the json string to parse + @param allowScalar whether to return objects for JSON fragments + @param error used to return an error by reference (pass NULL if this is not desired) + + @deprecated Given we bill ourselves as a "strict" JSON library, this method should be removed. + */ +- (id)objectWithString:(id)value allowScalar:(BOOL)allowScalar error:(NSError**)error { + + id obj = allowScalar ? [jsonParser fragmentWithString:value] : [jsonParser objectWithString:value]; + if (obj) + return obj; + + [errorTrace release]; + errorTrace = [[jsonParser errorTrace] mutableCopy]; + + if (error) + *error = [errorTrace lastObject]; + return nil; +} + +/** + Returns the object represented by the passed-in string or nil on error. The returned object can be + a string, number, boolean, null, array or dictionary. + + @param repr the json string to parse + @param error used to return an error by reference (pass NULL if this is not desired) + + @deprecated Given we bill ourselves as a "strict" JSON library, this method should be removed. + */ +- (id)fragmentWithString:(NSString*)repr error:(NSError**)error { + return [self objectWithString:repr + allowScalar:YES + error:error]; +} + +/** + Returns the object represented by the passed-in string or nil on error. The returned object + will be either a dictionary or an array. + + @param repr the json string to parse + @param error used to return an error by reference (pass NULL if this is not desired) + */ +- (id)objectWithString:(NSString*)repr error:(NSError**)error { + return [self objectWithString:repr + allowScalar:NO + error:error]; +} + + + +#pragma mark Properties - parsing + +- (NSUInteger)maxDepth { + return jsonParser.maxDepth; +} + +- (void)setMaxDepth:(NSUInteger)d { + jsonWriter.maxDepth = jsonParser.maxDepth = d; +} + + +#pragma mark Properties - writing + +- (BOOL)humanReadable { + return jsonWriter.humanReadable; +} + +- (void)setHumanReadable:(BOOL)x { + jsonWriter.humanReadable = x; +} + +- (BOOL)sortKeys { + return jsonWriter.sortKeys; +} + +- (void)setSortKeys:(BOOL)x { + jsonWriter.sortKeys = x; +} + +@end diff --git a/src/ios/facebook/JSON/FBSBJsonBase.h b/src/ios/facebook/JSON/FBSBJsonBase.h new file mode 100644 index 000000000..495dc5d90 --- /dev/null +++ b/src/ios/facebook/JSON/FBSBJsonBase.h @@ -0,0 +1,86 @@ +/* + Copyright (C) 2009 Stig Brautaset. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + +extern NSString * FBSBJSONErrorDomain; + + +enum { + EUNSUPPORTED = 1, + EPARSENUM, + EPARSE, + EFRAGMENT, + ECTRL, + EUNICODE, + EDEPTH, + EESCAPE, + ETRAILCOMMA, + ETRAILGARBAGE, + EEOF, + EINPUT +}; + +/** + @brief Common base class for parsing & writing. + + This class contains the common error-handling code and option between the parser/writer. + */ +@interface FBSBJsonBase : NSObject { + NSMutableArray *errorTrace; + +@protected + NSUInteger depth, maxDepth; +} + +/** + @brief The maximum recursing depth. + + Defaults to 512. If the input is nested deeper than this the input will be deemed to be + malicious and the parser returns nil, signalling an error. ("Nested too deep".) You can + turn off this security feature by setting the maxDepth value to 0. + */ +@property NSUInteger maxDepth; + +/** + @brief Return an error trace, or nil if there was no errors. + + Note that this method returns the trace of the last method that failed. + You need to check the return value of the call you're making to figure out + if the call actually failed, before you know call this method. + */ + @property(copy,readonly) NSArray* errorTrace; + +/// @internal for use in subclasses to add errors to the stack trace +- (void)addErrorWithCode:(NSUInteger)code description:(NSString*)str; + +/// @internal for use in subclasess to clear the error before a new parsing attempt +- (void)clearErrorTrace; + +@end diff --git a/src/ios/facebook/JSON/FBSBJsonBase.m b/src/ios/facebook/JSON/FBSBJsonBase.m new file mode 100644 index 000000000..47ba01c8f --- /dev/null +++ b/src/ios/facebook/JSON/FBSBJsonBase.m @@ -0,0 +1,78 @@ +/* + Copyright (C) 2009 Stig Brautaset. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "FBSBJsonBase.h" +NSString * FBSBJSONErrorDomain = @"org.brautaset.JSON.ErrorDomain"; + + +@implementation FBSBJsonBase + +@synthesize errorTrace; +@synthesize maxDepth; + +- (id)init { + self = [super init]; + if (self) + self.maxDepth = 512; + return self; +} + +- (void)dealloc { + [errorTrace release]; + [super dealloc]; +} + +- (void)addErrorWithCode:(NSUInteger)code description:(NSString*)str { + NSDictionary *userInfo; + if (!errorTrace) { + errorTrace = [NSMutableArray new]; + userInfo = [NSDictionary dictionaryWithObject:str forKey:NSLocalizedDescriptionKey]; + + } else { + userInfo = [NSDictionary dictionaryWithObjectsAndKeys: + str, NSLocalizedDescriptionKey, + [errorTrace lastObject], NSUnderlyingErrorKey, + nil]; + } + + NSError *error = [NSError errorWithDomain:FBSBJSONErrorDomain code:code userInfo:userInfo]; + + [self willChangeValueForKey:@"errorTrace"]; + [errorTrace addObject:error]; + [self didChangeValueForKey:@"errorTrace"]; +} + +- (void)clearErrorTrace { + [self willChangeValueForKey:@"errorTrace"]; + [errorTrace release]; + errorTrace = nil; + [self didChangeValueForKey:@"errorTrace"]; +} + +@end diff --git a/src/ios/facebook/JSON/FBSBJsonParser.h b/src/ios/facebook/JSON/FBSBJsonParser.h new file mode 100644 index 000000000..0b895445e --- /dev/null +++ b/src/ios/facebook/JSON/FBSBJsonParser.h @@ -0,0 +1,87 @@ +/* + Copyright (C) 2009 Stig Brautaset. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import +#import "FBSBJsonBase.h" + +/** + @brief Options for the parser class. + + This exists so the SBJSON facade can implement the options in the parser without having to re-declare them. + */ +@protocol FBSBJsonParser + +/** + @brief Return the object represented by the given string. + + Returns the object represented by the passed-in string or nil on error. The returned object can be + a string, number, boolean, null, array or dictionary. + + @param repr the json string to parse + */ +- (id)objectWithString:(NSString *)repr; + +@end + + +/** + @brief The JSON parser class. + + JSON is mapped to Objective-C types in the following way: + + @li Null -> NSNull + @li String -> NSMutableString + @li Array -> NSMutableArray + @li Object -> NSMutableDictionary + @li Boolean -> NSNumber (initialised with -initWithBool:) + @li Number -> NSDecimalNumber + + Since Objective-C doesn't have a dedicated class for boolean values, these turns into NSNumber + instances. These are initialised with the -initWithBool: method, and + round-trip back to JSON properly. (They won't silently suddenly become 0 or 1; they'll be + represented as 'true' and 'false' again.) + + JSON numbers turn into NSDecimalNumber instances, + as we can thus avoid any loss of precision. (JSON allows ridiculously large numbers.) + + */ +@interface FBSBJsonParser : FBSBJsonBase { + +@private + const char *c; +} + +@end + +// don't use - exists for backwards compatibility with 2.1.x only. Will be removed in 2.3. +@interface FBSBJsonParser (Private) +- (id)fragmentWithString:(id)repr; +@end + + diff --git a/src/ios/facebook/JSON/FBSBJsonParser.m b/src/ios/facebook/JSON/FBSBJsonParser.m new file mode 100644 index 000000000..c1a550c8f --- /dev/null +++ b/src/ios/facebook/JSON/FBSBJsonParser.m @@ -0,0 +1,475 @@ +/* + Copyright (C) 2009 Stig Brautaset. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "FBSBJsonParser.h" + +@interface FBSBJsonParser () + +- (BOOL)scanValue:(NSObject **)o; + +- (BOOL)scanRestOfArray:(NSMutableArray **)o; +- (BOOL)scanRestOfDictionary:(NSMutableDictionary **)o; +- (BOOL)scanRestOfNull:(NSNull **)o; +- (BOOL)scanRestOfFalse:(NSNumber **)o; +- (BOOL)scanRestOfTrue:(NSNumber **)o; +- (BOOL)scanRestOfString:(NSMutableString **)o; + +// Cannot manage without looking at the first digit +- (BOOL)scanNumber:(NSNumber **)o; + +- (BOOL)scanHexQuad:(unichar *)x; +- (BOOL)scanUnicodeChar:(unichar *)x; + +- (BOOL)scanIsAtEnd; + +@end + +#define skipWhitespace(c) while (isspace(*c)) c++ +#define skipDigits(c) while (isdigit(*c)) c++ + + +@implementation FBSBJsonParser + +static char ctrl[0x22]; + + ++ (void)initialize { + ctrl[0] = '\"'; + ctrl[1] = '\\'; + for (int i = 1; i < 0x20; i++) + ctrl[i+1] = i; + ctrl[0x21] = 0; +} + +/** + @deprecated This exists in order to provide fragment support in older APIs in one more version. + It should be removed in the next major version. + */ +- (id)fragmentWithString:(id)repr { + [self clearErrorTrace]; + + if (!repr) { + [self addErrorWithCode:EINPUT description:@"Input was 'nil'"]; + return nil; + } + + depth = 0; + c = [repr UTF8String]; + + id o; + if (![self scanValue:&o]) { + return nil; + } + + // We found some valid JSON. But did it also contain something else? + if (![self scanIsAtEnd]) { + [self addErrorWithCode:ETRAILGARBAGE description:@"Garbage after JSON"]; + return nil; + } + + NSAssert1(o, @"Should have a valid object from %@", repr); + return o; +} + +- (id)objectWithString:(NSString *)repr { + + id o = [self fragmentWithString:repr]; + if (!o) + return nil; + + // Check that the object we've found is a valid JSON container. + if (![o isKindOfClass:[NSDictionary class]] && ![o isKindOfClass:[NSArray class]]) { + [self addErrorWithCode:EFRAGMENT description:@"Valid fragment, but not JSON"]; + return nil; + } + + return o; +} + +/* + In contrast to the public methods, it is an error to omit the error parameter here. + */ +- (BOOL)scanValue:(NSObject **)o +{ + skipWhitespace(c); + + switch (*c++) { + case '{': + return [self scanRestOfDictionary:(NSMutableDictionary **)o]; + break; + case '[': + return [self scanRestOfArray:(NSMutableArray **)o]; + break; + case '"': + return [self scanRestOfString:(NSMutableString **)o]; + break; + case 'f': + return [self scanRestOfFalse:(NSNumber **)o]; + break; + case 't': + return [self scanRestOfTrue:(NSNumber **)o]; + break; + case 'n': + return [self scanRestOfNull:(NSNull **)o]; + break; + case '-': + case '0'...'9': + c--; // cannot verify number correctly without the first character + return [self scanNumber:(NSNumber **)o]; + break; + case '+': + [self addErrorWithCode:EPARSENUM description: @"Leading + disallowed in number"]; + return NO; + break; + case 0x0: + [self addErrorWithCode:EEOF description:@"Unexpected end of string"]; + return NO; + break; + default: + [self addErrorWithCode:EPARSE description: @"Unrecognised leading character"]; + return NO; + break; + } + + NSAssert(0, @"Should never get here"); + return NO; +} + +- (BOOL)scanRestOfTrue:(NSNumber **)o +{ + if (!strncmp(c, "rue", 3)) { + c += 3; + *o = [NSNumber numberWithBool:YES]; + return YES; + } + [self addErrorWithCode:EPARSE description:@"Expected 'true'"]; + return NO; +} + +- (BOOL)scanRestOfFalse:(NSNumber **)o +{ + if (!strncmp(c, "alse", 4)) { + c += 4; + *o = [NSNumber numberWithBool:NO]; + return YES; + } + [self addErrorWithCode:EPARSE description: @"Expected 'false'"]; + return NO; +} + +- (BOOL)scanRestOfNull:(NSNull **)o { + if (!strncmp(c, "ull", 3)) { + c += 3; + *o = [NSNull null]; + return YES; + } + [self addErrorWithCode:EPARSE description: @"Expected 'null'"]; + return NO; +} + +- (BOOL)scanRestOfArray:(NSMutableArray **)o { + if (maxDepth && ++depth > maxDepth) { + [self addErrorWithCode:EDEPTH description: @"Nested too deep"]; + return NO; + } + + *o = [NSMutableArray arrayWithCapacity:8]; + + for (; *c ;) { + id v; + + skipWhitespace(c); + if (*c == ']' && c++) { + depth--; + return YES; + } + + if (![self scanValue:&v]) { + [self addErrorWithCode:EPARSE description:@"Expected value while parsing array"]; + return NO; + } + + [*o addObject:v]; + + skipWhitespace(c); + if (*c == ',' && c++) { + skipWhitespace(c); + if (*c == ']') { + [self addErrorWithCode:ETRAILCOMMA description: @"Trailing comma disallowed in array"]; + return NO; + } + } + } + + [self addErrorWithCode:EEOF description: @"End of input while parsing array"]; + return NO; +} + +- (BOOL)scanRestOfDictionary:(NSMutableDictionary **)o +{ + if (maxDepth && ++depth > maxDepth) { + [self addErrorWithCode:EDEPTH description: @"Nested too deep"]; + return NO; + } + + *o = [NSMutableDictionary dictionaryWithCapacity:7]; + + for (; *c ;) { + id k, v; + + skipWhitespace(c); + if (*c == '}' && c++) { + depth--; + return YES; + } + + if (!(*c == '\"' && c++ && [self scanRestOfString:&k])) { + [self addErrorWithCode:EPARSE description: @"Object key string expected"]; + return NO; + } + + skipWhitespace(c); + if (*c != ':') { + [self addErrorWithCode:EPARSE description: @"Expected ':' separating key and value"]; + return NO; + } + + c++; + if (![self scanValue:&v]) { + NSString *string = [NSString stringWithFormat:@"Object value expected for key: %@", k]; + [self addErrorWithCode:EPARSE description: string]; + return NO; + } + + [*o setObject:v forKey:k]; + + skipWhitespace(c); + if (*c == ',' && c++) { + skipWhitespace(c); + if (*c == '}') { + [self addErrorWithCode:ETRAILCOMMA description: @"Trailing comma disallowed in object"]; + return NO; + } + } + } + + [self addErrorWithCode:EEOF description: @"End of input while parsing object"]; + return NO; +} + +- (BOOL)scanRestOfString:(NSMutableString **)o +{ + *o = [NSMutableString stringWithCapacity:16]; + do { + // First see if there's a portion we can grab in one go. + // Doing this caused a massive speedup on the long string. + size_t len = strcspn(c, ctrl); + if (len) { + // check for + id t = [[NSString alloc] initWithBytesNoCopy:(char*)c + length:len + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + if (t) { + [*o appendString:t]; + [t release]; + c += len; + } + } + + if (*c == '"') { + c++; + return YES; + + } else if (*c == '\\') { + unichar uc = *++c; + switch (uc) { + case '\\': + case '/': + case '"': + break; + + case 'b': uc = '\b'; break; + case 'n': uc = '\n'; break; + case 'r': uc = '\r'; break; + case 't': uc = '\t'; break; + case 'f': uc = '\f'; break; + + case 'u': + c++; + if (![self scanUnicodeChar:&uc]) { + [self addErrorWithCode:EUNICODE description: @"Broken unicode character"]; + return NO; + } + c--; // hack. + break; + default: + [self addErrorWithCode:EESCAPE description: [NSString stringWithFormat:@"Illegal escape sequence '0x%x'", uc]]; + return NO; + break; + } + CFStringAppendCharacters((CFMutableStringRef)*o, &uc, 1); + c++; + + } else if (*c < 0x20) { + [self addErrorWithCode:ECTRL description: [NSString stringWithFormat:@"Unescaped control character '0x%x'", *c]]; + return NO; + + } else { + NSLog(@"should not be able to get here"); + } + } while (*c); + + [self addErrorWithCode:EEOF description:@"Unexpected EOF while parsing string"]; + return NO; +} + +- (BOOL)scanUnicodeChar:(unichar *)x +{ + unichar hi, lo; + + if (![self scanHexQuad:&hi]) { + [self addErrorWithCode:EUNICODE description: @"Missing hex quad"]; + return NO; + } + + if (hi >= 0xd800) { // high surrogate char? + if (hi < 0xdc00) { // yes - expect a low char + + if (!(*c == '\\' && ++c && *c == 'u' && ++c && [self scanHexQuad:&lo])) { + [self addErrorWithCode:EUNICODE description: @"Missing low character in surrogate pair"]; + return NO; + } + + if (lo < 0xdc00 || lo >= 0xdfff) { + [self addErrorWithCode:EUNICODE description:@"Invalid low surrogate char"]; + return NO; + } + + hi = (hi - 0xd800) * 0x400 + (lo - 0xdc00) + 0x10000; + + } else if (hi < 0xe000) { + [self addErrorWithCode:EUNICODE description:@"Invalid high character in surrogate pair"]; + return NO; + } + } + + *x = hi; + return YES; +} + +- (BOOL)scanHexQuad:(unichar *)x +{ + *x = 0; + for (int i = 0; i < 4; i++) { + unichar uc = *c; + c++; + int d = (uc >= '0' && uc <= '9') + ? uc - '0' : (uc >= 'a' && uc <= 'f') + ? (uc - 'a' + 10) : (uc >= 'A' && uc <= 'F') + ? (uc - 'A' + 10) : -1; + if (d == -1) { + [self addErrorWithCode:EUNICODE description:@"Missing hex digit in quad"]; + return NO; + } + *x *= 16; + *x += d; + } + return YES; +} + +- (BOOL)scanNumber:(NSNumber **)o +{ + const char *ns = c; + + // The logic to test for validity of the number formatting is relicensed + // from JSON::XS with permission from its author Marc Lehmann. + // (Available at the CPAN: http://search.cpan.org/dist/JSON-XS/ .) + + if ('-' == *c) + c++; + + if ('0' == *c && c++) { + if (isdigit(*c)) { + [self addErrorWithCode:EPARSENUM description: @"Leading 0 disallowed in number"]; + return NO; + } + + } else if (!isdigit(*c) && c != ns) { + [self addErrorWithCode:EPARSENUM description: @"No digits after initial minus"]; + return NO; + + } else { + skipDigits(c); + } + + // Fractional part + if ('.' == *c && c++) { + + if (!isdigit(*c)) { + [self addErrorWithCode:EPARSENUM description: @"No digits after decimal point"]; + return NO; + } + skipDigits(c); + } + + // Exponential part + if ('e' == *c || 'E' == *c) { + c++; + + if ('-' == *c || '+' == *c) + c++; + + if (!isdigit(*c)) { + [self addErrorWithCode:EPARSENUM description: @"No digits after exponent"]; + return NO; + } + skipDigits(c); + } + + id str = [[NSString alloc] initWithBytesNoCopy:(char*)ns + length:c - ns + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; + [str autorelease]; + if (str && (*o = [NSDecimalNumber decimalNumberWithString:str])) + return YES; + + [self addErrorWithCode:EPARSENUM description: @"Failed creating decimal instance"]; + return NO; +} + +- (BOOL)scanIsAtEnd +{ + skipWhitespace(c); + return !*c; +} + + +@end diff --git a/src/ios/facebook/JSON/FBSBJsonWriter.h b/src/ios/facebook/JSON/FBSBJsonWriter.h new file mode 100644 index 000000000..7fbab84eb --- /dev/null +++ b/src/ios/facebook/JSON/FBSBJsonWriter.h @@ -0,0 +1,129 @@ +/* + Copyright (C) 2009 Stig Brautaset. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import +#import "FBSBJsonBase.h" + +/** + @brief Options for the writer class. + + This exists so the SBJSON facade can implement the options in the writer without having to re-declare them. + */ +@protocol FBSBJsonWriter + +/** + @brief Whether we are generating human-readable (multiline) JSON. + + Set whether or not to generate human-readable JSON. The default is NO, which produces + JSON without any whitespace. (Except inside strings.) If set to YES, generates human-readable + JSON with linebreaks after each array value and dictionary key/value pair, indented two + spaces per nesting level. + */ +@property BOOL humanReadable; + +/** + @brief Whether or not to sort the dictionary keys in the output. + + If this is set to YES, the dictionary keys in the JSON output will be in sorted order. + (This is useful if you need to compare two structures, for example.) The default is NO. + */ +@property BOOL sortKeys; + +/** + @brief Return JSON representation (or fragment) for the given object. + + Returns a string containing JSON representation of the passed in value, or nil on error. + If nil is returned and @p error is not NULL, @p *error can be interrogated to find the cause of the error. + + @param value any instance that can be represented as a JSON fragment + + */ +- (NSString*)stringWithObject:(id)value; + +@end + + +/** + @brief The JSON writer class. + + Objective-C types are mapped to JSON types in the following way: + + @li NSNull -> Null + @li NSString -> String + @li NSArray -> Array + @li NSDictionary -> Object + @li NSNumber (-initWithBool:) -> Boolean + @li NSNumber -> Number + + In JSON the keys of an object must be strings. NSDictionary keys need + not be, but attempting to convert an NSDictionary with non-string keys + into JSON will throw an exception. + + NSNumber instances created with the +initWithBool: method are + converted into the JSON boolean "true" and "false" values, and vice + versa. Any other NSNumber instances are converted to a JSON number the + way you would expect. + + */ +@interface FBSBJsonWriter : FBSBJsonBase { + +@private + BOOL sortKeys, humanReadable; +} + +@end + +// don't use - exists for backwards compatibility. Will be removed in 2.3. +@interface FBSBJsonWriter (Private) +- (NSString*)stringWithFragment:(id)value; +@end + +/** + @brief Allows generation of JSON for otherwise unsupported classes. + + If you have a custom class that you want to create a JSON representation for you can implement + this method in your class. It should return a representation of your object defined + in terms of objects that can be translated into JSON. For example, a Person + object might implement it like this: + + @code + - (id)jsonProxyObject { + return [NSDictionary dictionaryWithObjectsAndKeys: + name, @"name", + phone, @"phone", + email, @"email", + nil]; + } + @endcode + + */ +@interface NSObject (SBProxyForJson) +- (id)proxyForJson; +@end + diff --git a/src/ios/facebook/JSON/FBSBJsonWriter.m b/src/ios/facebook/JSON/FBSBJsonWriter.m new file mode 100644 index 000000000..66ae2cfed --- /dev/null +++ b/src/ios/facebook/JSON/FBSBJsonWriter.m @@ -0,0 +1,237 @@ +/* + Copyright (C) 2009 Stig Brautaset. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "FBSBJsonWriter.h" + +@interface FBSBJsonWriter () + +- (BOOL)appendValue:(id)fragment into:(NSMutableString*)json; +- (BOOL)appendArray:(NSArray*)fragment into:(NSMutableString*)json; +- (BOOL)appendDictionary:(NSDictionary*)fragment into:(NSMutableString*)json; +- (BOOL)appendString:(NSString*)fragment into:(NSMutableString*)json; + +- (NSString*)indent; + +@end + +@implementation FBSBJsonWriter + +static NSMutableCharacterSet *kEscapeChars; + ++ (void)initialize { + kEscapeChars = [[NSMutableCharacterSet characterSetWithRange: NSMakeRange(0,32)] retain]; + [kEscapeChars addCharactersInString: @"\"\\"]; +} + + +@synthesize sortKeys; +@synthesize humanReadable; + +/** + @deprecated This exists in order to provide fragment support in older APIs in one more version. + It should be removed in the next major version. + */ +- (NSString*)stringWithFragment:(id)value { + [self clearErrorTrace]; + depth = 0; + NSMutableString *json = [NSMutableString stringWithCapacity:128]; + + if ([self appendValue:value into:json]) + return json; + + return nil; +} + + +- (NSString*)stringWithObject:(id)value { + + if ([value isKindOfClass:[NSDictionary class]] || [value isKindOfClass:[NSArray class]]) { + return [self stringWithFragment:value]; + } + + if ([value respondsToSelector:@selector(proxyForJson)]) { + NSString *tmp = [self stringWithObject:[value proxyForJson]]; + if (tmp) + return tmp; + } + + + [self clearErrorTrace]; + [self addErrorWithCode:EFRAGMENT description:@"Not valid type for JSON"]; + return nil; +} + + +- (NSString*)indent { + return [@"\n" stringByPaddingToLength:1 + 2 * depth withString:@" " startingAtIndex:0]; +} + +- (BOOL)appendValue:(id)fragment into:(NSMutableString*)json { + if ([fragment isKindOfClass:[NSDictionary class]]) { + if (![self appendDictionary:fragment into:json]) + return NO; + + } else if ([fragment isKindOfClass:[NSArray class]]) { + if (![self appendArray:fragment into:json]) + return NO; + + } else if ([fragment isKindOfClass:[NSString class]]) { + if (![self appendString:fragment into:json]) + return NO; + + } else if ([fragment isKindOfClass:[NSNumber class]]) { + if ('c' == *[fragment objCType]) + [json appendString:[fragment boolValue] ? @"true" : @"false"]; + else + [json appendString:[fragment stringValue]]; + + } else if ([fragment isKindOfClass:[NSNull class]]) { + [json appendString:@"null"]; + } else if ([fragment respondsToSelector:@selector(proxyForJson)]) { + [self appendValue:[fragment proxyForJson] into:json]; + + } else { + [self addErrorWithCode:EUNSUPPORTED description:[NSString stringWithFormat:@"JSON serialisation not supported for %@", [fragment class]]]; + return NO; + } + return YES; +} + +- (BOOL)appendArray:(NSArray*)fragment into:(NSMutableString*)json { + if (maxDepth && ++depth > maxDepth) { + [self addErrorWithCode:EDEPTH description: @"Nested too deep"]; + return NO; + } + [json appendString:@"["]; + + BOOL addComma = NO; + for (id value in fragment) { + if (addComma) + [json appendString:@","]; + else + addComma = YES; + + if ([self humanReadable]) + [json appendString:[self indent]]; + + if (![self appendValue:value into:json]) { + return NO; + } + } + + depth--; + if ([self humanReadable] && [fragment count]) + [json appendString:[self indent]]; + [json appendString:@"]"]; + return YES; +} + +- (BOOL)appendDictionary:(NSDictionary*)fragment into:(NSMutableString*)json { + if (maxDepth && ++depth > maxDepth) { + [self addErrorWithCode:EDEPTH description: @"Nested too deep"]; + return NO; + } + [json appendString:@"{"]; + + NSString *colon = [self humanReadable] ? @" : " : @":"; + BOOL addComma = NO; + NSArray *keys = [fragment allKeys]; + if (self.sortKeys) + keys = [keys sortedArrayUsingSelector:@selector(compare:)]; + + for (id value in keys) { + if (addComma) + [json appendString:@","]; + else + addComma = YES; + + if ([self humanReadable]) + [json appendString:[self indent]]; + + if (![value isKindOfClass:[NSString class]]) { + [self addErrorWithCode:EUNSUPPORTED description: @"JSON object key must be string"]; + return NO; + } + + if (![self appendString:value into:json]) + return NO; + + [json appendString:colon]; + if (![self appendValue:[fragment objectForKey:value] into:json]) { + [self addErrorWithCode:EUNSUPPORTED description:[NSString stringWithFormat:@"Unsupported value for key %@ in object", value]]; + return NO; + } + } + + depth--; + if ([self humanReadable] && [fragment count]) + [json appendString:[self indent]]; + [json appendString:@"}"]; + return YES; +} + +- (BOOL)appendString:(NSString*)fragment into:(NSMutableString*)json { + + [json appendString:@"\""]; + + NSRange esc = [fragment rangeOfCharacterFromSet:kEscapeChars]; + if ( !esc.length ) { + // No special chars -- can just add the raw string: + [json appendString:fragment]; + + } else { + NSUInteger length = [fragment length]; + for (NSUInteger i = 0; i < length; i++) { + unichar uc = [fragment characterAtIndex:i]; + switch (uc) { + case '"': [json appendString:@"\\\""]; break; + case '\\': [json appendString:@"\\\\"]; break; + case '\t': [json appendString:@"\\t"]; break; + case '\n': [json appendString:@"\\n"]; break; + case '\r': [json appendString:@"\\r"]; break; + case '\b': [json appendString:@"\\b"]; break; + case '\f': [json appendString:@"\\f"]; break; + default: + if (uc < 0x20) { + [json appendFormat:@"\\u%04x", uc]; + } else { + CFStringAppendCharacters((CFMutableStringRef)json, &uc, 1); + } + break; + + } + } + } + + [json appendString:@"\""]; + return YES; +} + + +@end diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/Contents/Resources/en.lproj/Localizable.strings b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/Contents/Resources/en.lproj/Localizable.strings new file mode 100644 index 000000000..7fd7575ee Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/Contents/Resources/en.lproj/Localizable.strings differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/Contents/Resources/he.lproj/Localizable.strings b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/Contents/Resources/he.lproj/Localizable.strings new file mode 100644 index 000000000..bb3ba24dd Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/Contents/Resources/he.lproj/Localizable.strings differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/facebook-logo.png b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/facebook-logo.png new file mode 100644 index 000000000..be1dccd25 Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/facebook-logo.png differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/facebook-logo@2x.png b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/facebook-logo@2x.png new file mode 100644 index 000000000..4b03929cb Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/facebook-logo@2x.png differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadLandscape.jpg b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadLandscape.jpg new file mode 100644 index 000000000..07e75dd07 Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadLandscape.jpg differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadLandscape@2x.jpg b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadLandscape@2x.jpg new file mode 100644 index 000000000..399e3f5c9 Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadLandscape@2x.jpg differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadPortrait.jpg b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadPortrait.jpg new file mode 100644 index 000000000..523ac67a9 Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadPortrait.jpg differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadPortrait@2x.jpg b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadPortrait@2x.jpg new file mode 100644 index 000000000..45a8cc2b4 Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadPortrait@2x.jpg differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPhonePortrait.jpg b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPhonePortrait.jpg new file mode 100644 index 000000000..1fba77f3f Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPhonePortrait.jpg differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPhonePortrait@2x.jpg b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPhonePortrait@2x.jpg new file mode 100644 index 000000000..fe1d11aea Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/loginBackgroundIPhonePortrait@2x.jpg differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-normal.png b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-normal.png new file mode 100644 index 000000000..892419f35 Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-normal.png differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-normal@2x.png b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-normal@2x.png new file mode 100644 index 000000000..daa4ba694 Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-normal@2x.png differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed.png b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed.png new file mode 100644 index 000000000..3f862c850 Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed.png differ diff --git a/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed@2x.png b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed@2x.png new file mode 100644 index 000000000..7866e3da7 Binary files /dev/null and b/src/ios/facebook/resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed@2x.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/Contents/Resources/en.lproj/Localizable.strings b/src/ios/facebook/resources/FacebookSDKResources.bundle/Contents/Resources/en.lproj/Localizable.strings new file mode 100644 index 000000000..81ab63cb0 Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/Contents/Resources/en.lproj/Localizable.strings differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/Contents/Resources/he.lproj/Localizable.strings b/src/ios/facebook/resources/FacebookSDKResources.bundle/Contents/Resources/he.lproj/Localizable.strings new file mode 100644 index 000000000..7e305581e Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/Contents/Resources/he.lproj/Localizable.strings differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBDialog/images/close.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBDialog/images/close.png new file mode 100644 index 000000000..ad0147460 Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBDialog/images/close.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBDialog/images/close@2x.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBDialog/images/close@2x.png new file mode 100644 index 000000000..e3aff5ae5 Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBDialog/images/close@2x.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBDialog/images/fbicon.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBDialog/images/fbicon.png new file mode 100644 index 000000000..413396be6 Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBDialog/images/fbicon.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBFriendPickerView/images/default.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBFriendPickerView/images/default.png new file mode 100755 index 000000000..9762eb85c Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBFriendPickerView/images/default.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/bluetint.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/bluetint.png new file mode 100644 index 000000000..adef9c05b Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/bluetint.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/f_logo.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/f_logo.png new file mode 100644 index 000000000..9443a881d Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/f_logo.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/facebook.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/facebook.png new file mode 100755 index 000000000..daf8097b1 Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/facebook.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small-pressed.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small-pressed.png new file mode 100644 index 000000000..31c69fe57 Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small-pressed.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small-pressed@2x.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small-pressed@2x.png new file mode 100644 index 000000000..bd2696592 Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small-pressed@2x.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small.png new file mode 100644 index 000000000..22aa4fad6 Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small@2x.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small@2x.png new file mode 100644 index 000000000..857c489bf Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBLoginView/images/login-button-small@2x.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBPlacePickerView/images/fb_generic_place.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBPlacePickerView/images/fb_generic_place.png new file mode 100755 index 000000000..97ec3e608 Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBPlacePickerView/images/fb_generic_place.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBProfilePictureView/images/fb_blank_profile_portrait.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBProfilePictureView/images/fb_blank_profile_portrait.png new file mode 100755 index 000000000..1ea13d41a Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBProfilePictureView/images/fb_blank_profile_portrait.png differ diff --git a/src/ios/facebook/resources/FacebookSDKResources.bundle/FBProfilePictureView/images/fb_blank_profile_square.png b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBProfilePictureView/images/fb_blank_profile_square.png new file mode 100755 index 000000000..bf10aebff Binary files /dev/null and b/src/ios/facebook/resources/FacebookSDKResources.bundle/FBProfilePictureView/images/fb_blank_profile_square.png differ diff --git a/test/pg-plugin-fb-connect-tests.js b/test/pg-plugin-fb-connect-tests.js deleted file mode 100644 index d910d7e2b..000000000 --- a/test/pg-plugin-fb-connect-tests.js +++ /dev/null @@ -1,45 +0,0 @@ -// replace this with your own APP_ID -var APP_ID = '126462174095513' // <----( this is the PhoneGap-Facebook app id -FB.initWithAppId(APP_ID) - -test('login', function loginToFacebook () { - QUnit.stop() - FB.onLogin = function login (e) { - QUnit.start() - ok(true, 'user can login') - } - FB.authorize('email read_stream publish_stream offline_access'.split(' ')) -}) - -test('failed login', function() { - QUnit.stop() - FB.onDidNotLogin = function loginFailed () { - QUnit.start() - ok(true, 'can capture failed login attempt') - } -}) - -test('get friends', function() { - QUnit.stop() - var req = facebook.getFriends() - req.onload = function friends (e) { - QUnit.start() - var friends = JSON.parse(e.target.responseText).data - ok(friends, 'returned friends array') - console.log('found ' + friends.length + ' friends!') - for(var i=0, l=friends.length; i < l; i++) { - var id = friends[i].id - , name = friends[i].name - console.log(name) - } - } -}) - -test('logout',function() { - QUnit.stop() - FB.onLogout = function logout (e) { - QUnit.start() - ok(true, 'user can logout') - } - FB.logout(); -}) diff --git a/lib/facebook_js_sdk.js b/www/facebook-js-sdk.js similarity index 100% rename from lib/facebook_js_sdk.js rename to www/facebook-js-sdk.js