<NME> upload.php
<BEF> <div class="headerbar">
	Upload <!-- function bar for e.g. sharing function -->
</div>

<div class="message upload-complete"><img src="<?php getTheme()->asset('image', 'checkmark-big.gif'); ?>" align="absmiddle">All done! Go have a <a href="<?php Url::photosView(); ?>">look at your photos</a>.</div>
<div class="message upload-progress"><img src="<?php getTheme()->asset('image', 'upload-big.gif'); ?>" align="absmiddle">Currently uploading <span class="completed">0</span> of <span class="total">0</span> photos.</div>
<form class="upload">
  <div class="options">
    <h2>Apply these settings to my photos.</h2>
    <div class="row">
      <br>
      <div class="span8">
        <h3>Upload from your computer</h3>
        <div id="uploader">
          <div class="insufficient">
            <h1>Unfortunately, it doesn't look like we support your browser. :(</h1>
            <p>
              Try using <a href="http://www.mozilla.org/en-US/firefox/new/">Firefox</a>.
            </p>
          </div>
        </div>
        <?php if(!empty($token)) { ?>
          <div class="btn-toolbar visible-phone">
            <button type="submit" class="btn btn-brand upload-button addSpinner wide">Start uploading</button>
          </div>
        <?php } ?>
        <em class="poweredby hidden-phone">Powered by <a href="http://www.plupload.com">Plupload</a>.</em>
      </div>
      <div class="span4 <?php if(!empty($token)) { ?>hide<?php } ?>">
        <h3>Use these settings.</h3>
        <br>
        <label for="tags">Tags</label>
        <input type="search" name="tags" class="typeahead tags" autocomplete="off" placeholder="Optional comma separated list">

        <div class="control-group">
          <label class="control-label">Albums <small>(<a href="#" class="showBatchForm album" data-action="albums">create new</a>)</small></label>
          <select data-placeholder="Select albums for these photos" name="albums" class="typeahead">
            <option value="">If you'd like, choose an album</option>
            <?php foreach($albums as $album) { ?>
              <option value="<?php $this->utility->safe($album['id']); ?>"><?php $this->utility->safe($album['name']); ?></option>
            <?php } ?>
          </select>
        </div>

        <div class="control-group">
          <label for="tags">Permission</label>
          <div class="controls">
            <label class="radio inline private">
              <input type="radio" name="permission" value="0"<?php if($preferences['permission'] === false || $preferences['permission'] === '0') { ?> checked="checked"<?php } ?>>
              <span>Private</span>
            </label>
            <label class="radio inline">
              <input type="radio" name="permission" value="1"<?php if($preferences['permission'] === '1') { ?> checked="checked"<?php } ?>>
              <span>Public</span>
            </label>
          </div>
        </div>

        <?php if(empty($token)) { ?>
          <label for="license">License</label>
          <select name="license" class="license">
            <?php foreach($licenses as $code => $license) { ?>
              <option value="<?php $this->utility->safe($code); ?>" <?php if($license['selected']) { ?> selected="selected" <?php } ?>><?php $this->utility->licenseName($code); ?></option>
            <?php } ?>
          </select>
        <?php } else { ?>
          <input type="hidden" name="license" value="<?php reset($licenses); $this->utility->safe(key($licenses)); ?>">
        <?php } ?>

        <input type="hidden" name="crumb" value="<?php $this->utility->safe($crumb); ?>">
        
        <?php if(empty($token)) { ?>
          <div class="btn-toolbar visible-phone">
            <button type="submit" class="btn btn-brand upload-button addSpinner wide">Start uploading</button>
          </div>
        <?php } ?>
      </div>
    </div>
  </form>
</div>

<MSG> Merge commit 'upstream/master'
Should also address issue #352

Conflicts:

	src/libraries/models/Photo.php

<DFF> @@ -1,9 +1,5 @@
-<div class="headerbar">
-	Upload <!-- function bar for e.g. sharing function -->
-</div>
-
-<div class="message upload-complete"><img src="<?php getTheme()->asset('image', 'checkmark-big.gif'); ?>" align="absmiddle">All done! Go have a <a href="<?php Url::photosView(); ?>">look at your photos</a>.</div>
-<div class="message upload-progress"><img src="<?php getTheme()->asset('image', 'upload-big.gif'); ?>" align="absmiddle">Currently uploading <span class="completed">0</span> of <span class="total">0</span> photos.</div>
+<div class="message upload-complete"><img src="/assets/images/checkmark-big.gif" align="absmiddle">All done! Go have a <a href="<?php Url::photosView(); ?>">look at your photos</a>.</div>
+<div class="message upload-progress"><img src="/assets/images/upload-big.gif" align="absmiddle">Currently uploading <span class="completed">0</span> of <span class="total">0</span> photos.</div>
 <form class="upload">
   <div class="options">
     <h2>Apply these settings to my photos.</h2>

{'commit': 'diff --git a/Readme.markdown b/Readme.markdown\nindex 855d27a5..830c2b36 100644\n--- a/Readme.markdown\n+++ b/Readme.markdown\n@@ -3,10 +3,16 @@\n \n ### What is OpenPhoto?\n \n-1.  [FAQ][faq], Answers to the most common questions.\n-1.  [API][api], Documentation to get started building applications on OpenPhoto.\n-1.  [Schemas][schemas], Description of what the different objects in OpenPhoto look like (i.e. a photo or a comment).\n-1.  [Guides][guides], Detailed guides to help you get the OpenPhoto software installed on various systems.\n+1.  [FAQ](http://theopenphotoproject.org/documentation/faq/Faq), Answers to the most common questions.\n+1.  [API &amp; Guides](http://theopenphotoproject.org/documentation), Documentation to get started building applications on OpenPhoto.\n+\n+### Install OpenPhoto in under 3 minutes\n+\n+    # Installing OpenPhoto on Ubuntu and Apache\n+    # Run this from the command line as root.\n+    # As always, view any script before running it ;).\n+\n+    curl https://raw.github.com/openphoto/frontend/master/documentation/guides/InstallationUbuntuApache.sh | /bin/bash\n \n ----------------------------------------\n \n@@ -47,7 +53,3 @@ There are importing tools available to make the switch easy.\n [aws]: http://aws.amazon.com/\n [s3]: http://aws.amazon.com/s3/\n [simpledb]: http://aws.amazon.com/simpledb/\n-[api]: https://github.com/openphoto/frontend/blob/master/documentation/api/Api.markdown\n-[faq]: https://github.com/openphoto/frontend/blob/master/documentation/faq/Faq.markdown\n-[schemas]: https://github.com/openphoto/frontend/blob/master/documentation/schemas/Schemas.markdown\n-[guides]: https://github.com/openphoto/frontend/blob/master/documentation/guides/Guides.markdown\ndiff --git a/documentation/Readme.markdown b/documentation/Readme.markdown\nindex ee558019..189b5678 100644\n--- a/documentation/Readme.markdown\n+++ b/documentation/Readme.markdown\n@@ -1,19 +1,5 @@\n OpenPhoto / Documentation\n =======================\n-#### OpenPhoto, a photo service for the masses\n \n-1.  [FAQ][faq]\n-1.  [API][api]\n-1.  [Schemas][schemas]\n-1.  [Guides][guides]\n-  * [Installation on Ubuntu][guideinstallubuntu]\n-  * [Installation on OSX using MacPorts][guideinstallosxmacports]\n-  * [Installation on SUSE][guideinstallsuse]\n+You can find the documentation at http://theopenphotoproject.org/documentation. It\'s a mirror of what is here but easier to follow.\n \n-[api]: documentation/api/Api.markdown\n-[faq]: documentation/faq/Faq.markdown\n-[schemas]: documentation/schemas/Schemas.markdown\n-[guides]: documentation/guides/Guides.markdown\n-[guideinstallubuntu]: documentation/guides/InstallationUbuntu.markdown\n-[guideinstallosxmacports]: documentation/guides/InstallationOSXMacports.markdown\n-[guideinstallsuse]: documentation/guides/InstallationSUSE.markdown\ndiff --git a/documentation/api/Api.markdown b/documentation/api/Api.markdown\ndeleted file mode 100644\nindex 6ae9d86d..00000000\n--- a/documentation/api/Api.markdown\n+++ /dev/null\n@@ -1,99 +0,0 @@\n-Open Photo API\n-=======================\n-#### OpenPhoto, a photo service for the masses\n-\n-----------------------------------------\n-\n-### How do I authenticate?\n-\n-The Open Photo API uses [OAuth1.0a][oauth1.0a] for authentication. See the complete [guide on authentication][authentication] for details.\n-\n-### What\'s the response format?\n-\n-Every API endpoint returns a JSON response in a [standard envelope][Envelope].\n-\n-    {\n-      message: "A string describing the response",\n-      code: 200,\n-      result: {\n-        foo: "bar"\n-      }\n-    }\n-\n-### API Endpoints\n-\n-#### Test / diagnostics endpoints\n-1.  [GET /hello.json][GetHelloWorld]\n-    Test endpoint.\n-\n-#### Action endpoings (comments, favorites, etc)\n-1.  [POST /action/:id/:type/create.json][PostActionCreate]\n-    Create an action.\n-1.  [POST /action/:id/delete.json][PostActionDelete]\n-    Delete an action.\n-\n-#### Photo endpoints\n-1.  [POST /photos/:id/delete.json][PostPhotoDelete]\n-    Delete a user\'s photo.\n-1.  [POST /photos/:id/update.json][PostPhotoUpdate]\n-    Update data on a user\'s photo.\n-1.  [GET /photo/:id/view.json][GetPhoto]\n-    Get a user\'s photo.\n-1.  [GET /photos/list.json][GetPhotos]\n-    Get a list of the user\'s photos.\n-1.  [GET /photo/:id/nextprevious.json][GetPhotoNextPrevious]\n-    Get the next and previous photo.\n-1.  [POST /photo/upload.json][PostPhotoUpload]\n-    Upload a new photo.\n-\n-#### Tag endpoints\n-1.  [GET /tags/list.json][GetTags]\n-    Get a user\'s tags.\n-1.  [POST /tag/:id/create.json][PostTagCreate]\n-    Create a tag for the user.\n-1.  [POST /tag/:id/update.json][PostTagUpdate]\n-    Modify meta data for a user\'s tag.\n-\n-#### Group endpoints\n-1.  [GET /group/:id/view.json][GetGroup]\n-    Get a group.\n-1.  [GET /groups/list.json][GetGroups]\n-    Get a listing of a user\'s groups.\n-1.  [POST /group/create.json][PostGroupCreate]\n-    Create a group.\n-1.  [POST /group/delete.json][PostGroupDelete]\n-    Delete a group.\n-1.  [POST /group/update.json][PostGroupUpdate]\n-    Update a group.\n-\n-#### Webhook endpoints\n-1.  [POST /webhook/subscribe][PostWebHookSubscribe]\n-    Update an eixsting webhook.\n-1.  [GET /webhook/:id/view.json][GetWebhook]\n-    Get a user\'s webhook by id.\n-1.  [POST /webhook/:id/delete.json][PostWebHookDelete]\n-    Delete an existing webhook.\n-\n-[Envelope]: Envelope.markdown\n-[GetHelloWorld]: GetHelloWorld.markdown\n-[GetPhotos]: GetPhotos.markdown\n-[GetPhoto]: GetPhoto.markdown\n-[GetPhotoNextPrevious]: GetPhotoNextPrevious.markdown\n-[PostPhotoDelete]: PostPhotoDelete.markdown\n-[PostPhotoUpdate]: PostPhotoUpdate.markdown\n-[PostPhotoUpload]: PostPhotoUpload.markdown\n-[PostActionCreate]: PostActionCreate.markdown\n-[PostActionDelete]: PostActionDelete.markdown\n-[GetTags]: GetTags.markdown\n-[PostTagCreate]: PostTagCreate.markdown\n-[PostTagUpdate]: PostTagUpdate.markdown\n-[GetGroup]: GetGroup.markdown\n-[GetGroups]: GetGroups.markdown\n-[PostGroupCreate]: PostGroupCreate.markdown\n-[PostGroupUpdate]: PostGroupUpdate.markdown\n-[PostGroupDelete]: PostGroupDelete.markdown\n-[GetWebhook]: GetWebhook.markdown\n-[PostWebhookSubscribe]: PostWebhookSubscribe.markdown\n-[PostWebhookDelete]: PostWebhookDelete.markdown\n-[authentication]: Authentication.markdown\n-[oauth1.0a]: http://oauth.net/core/1.0a/\ndiff --git a/documentation/api/Authentication.markdown b/documentation/api/Authentication.markdown\nindex d641aaa9..c5329473 100644\n--- a/documentation/api/Authentication.markdown\n+++ b/documentation/api/Authentication.markdown\n@@ -1,8 +1,5 @@\n-Open Photo API / Authentication\n+Authentication using OAuth 1.0a\n =======================\n-#### OpenPhoto, a photo service for the masses\n-\n-----------------------------------------\n \n ### Using OAuth (1.0a)\n \n@@ -10,6 +7,7 @@ A full introduction to OAuth is beyond the scope of the OpenPhoto documentation.\n In all reality you probably don\'t need to understand the ins and outs of OAuth, just grab one of our libraries.\n \n * <a href="https://github.com/openphoto/openphoto-php">openphoto/openphoto-php</a> - Our PHP language binding.\n+* <a href="https://github.com/openphoto/openphoto-java">openphoto/openphoto-java</a> - Our Java language binding.\n * More coming soon, <a href="mailto:hello@openphoto.me">contact us</a> if you\'d like to write bindings in an unlisted language.\n \n ### Obtaining a consumer key and secret\ndiff --git a/documentation/api/Envelope.markdown b/documentation/api/Envelope.markdown\nindex dd5f7ab4..de334a04 100644\n--- a/documentation/api/Envelope.markdown\n+++ b/documentation/api/Envelope.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Response Envelope\n+Response Envelope\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \ndiff --git a/documentation/api/GetGroup.markdown b/documentation/api/GetGroup.markdown\nindex a677d153..41ad8e4d 100644\n--- a/documentation/api/GetGroup.markdown\n+++ b/documentation/api/GetGroup.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photo\n+Get Group\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -56,7 +56,7 @@ _None_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -80,7 +80,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Group]: ../schemas/Group.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/GetGroups.markdown b/documentation/api/GetGroups.markdown\nindex 3b8ad877..6bc31500 100644\n--- a/documentation/api/GetGroups.markdown\n+++ b/documentation/api/GetGroups.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photo\n+Get Groups\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -56,7 +56,7 @@ _None_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -102,7 +102,7 @@ The response is in a standard [response envelope][Envelope].\n       ]\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Group]: ../schemas/Group.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/GetHelloWorld.markdown b/documentation/api/GetHelloWorld.markdown\nindex f4ab6bea..94cd928c 100644\n--- a/documentation/api/GetHelloWorld.markdown\n+++ b/documentation/api/GetHelloWorld.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Hello World\n+Hello World\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -64,7 +64,7 @@ _Authentication: optional_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -82,7 +82,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [purpose]: #purpose\n [endpoint]: #endpoint\n [parameters]: #parameters\ndiff --git a/documentation/api/GetPhoto.markdown b/documentation/api/GetPhoto.markdown\nindex c192c32e..0bc9025d 100644\n--- a/documentation/api/GetPhoto.markdown\n+++ b/documentation/api/GetPhoto.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photo\n+Get Photo\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -20,7 +20,7 @@ Open Photo API / Get Photo\n \n Use this API to get a for a user\'s photo.\n \n-_NOTE:_ Always pass in the `returnSizes` parameter for sizes you plan on using. It\'s the only way to guarantee that a URL for that size will be present in the response. See [Photo Generation][photogeneration] for details.\n+_NOTE:_ Always pass in the `returnSizes` parameter for sizes you plan on using. It\'s the only way to guarantee that a URL for that size will be present in the response. See [Photo Generation](http://theopenphotoproject.org/documentation/faq/PhotoGeneration) for details.\n \n ----------------------------------------\n \n@@ -34,7 +34,8 @@ _Authentication: optional_\n <a name="parameters"></a>\n ### Parameters\n \n-1.  returnSizes (optional), The photo sizes you\'d like in the response. Specify every size you plan on using. [Docs for this parameter][ReturnSizes]\n+1.  returnSizes (optional), (i.e. 20x20 or 30x30xCR,40x40) The photo sizes you\'d like in the response. Specify every size you plan on using. [Docs for this parameter](http://theopenphotoproject.org/documentation/faq/ReturnSizes)\n+1.  generate (optional), (i.e. true or false) Tells the API to generate the sizes from `returnSizes` instead of returning a _create_ URL. [Docs for this parameter](http://theopenphotoproject.org/documentation/faq/ReturnSizes)\n \n ----------------------------------------\n \n@@ -57,7 +58,7 @@ _Authentication: optional_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -100,7 +101,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Photo]: ../schemas/Photo.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/GetPhotoNextPrevious.markdown b/documentation/api/GetPhotoNextPrevious.markdown\nindex ab5e355c..9db52d2d 100644\n--- a/documentation/api/GetPhotoNextPrevious.markdown\n+++ b/documentation/api/GetPhotoNextPrevious.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photo\n+Get Next/Previous Photo\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -20,7 +20,7 @@ Open Photo API / Get Photo\n \n Use this API to get the next and previous photo given a photo in the middle.\n \n-_NOTE:_ Always pass in the `returnSizes` parameter for sizes you plan on using. It\'s the only way to guarantee that a URL for that size will be present in the response. See [Photo Generation][photogeneration] for details.\n+_NOTE:_ Always pass in the `returnSizes` parameter for sizes you plan on using. It\'s the only way to guarantee that a URL for that size will be present in the response. See [Photo Generation](http://theopenphotoproject.org/documentation/faq/PhotoGeneration) for details.\n \n ----------------------------------------\n \n@@ -34,7 +34,8 @@ _Authentication: optional_\n <a name="parameters"></a>\n ### Parameters\n \n-1.  returnSizes (optional), The photo sizes you\'d like in the response. Specify every size you plan on using. [Docs for this parameter][ReturnSizes]\n+1.  returnSizes (optional), (i.e. 20x20 or 30x30xCR,40x40) The photo sizes you\'d like in the response. Specify every size you plan on using. [Docs for this parameter](http://theopenphotoproject.org/documentation/faq/ReturnSizes)\n+1.  generate (optional), (i.e. true or false) Tells the API to generate the sizes from `returnSizes` instead of returning a _create_ URL. [Docs for this parameter](http://theopenphotoproject.org/documentation/faq/ReturnSizes)\n \n ----------------------------------------\n \n@@ -57,7 +58,7 @@ _Authentication: optional_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -143,7 +144,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Photo]: ../schemas/Photo.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/GetPhotos.markdown b/documentation/api/GetPhotos.markdown\nindex cda55e8b..6bd14f16 100644\n--- a/documentation/api/GetPhotos.markdown\n+++ b/documentation/api/GetPhotos.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photos\n+Get Photos\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -20,7 +20,7 @@ Open Photo API / Get Photos\n \n Use this API to get a set of photos for a user.\n \n-_NOTE:_ Always pass in the `returnSizes` parameter for sizes you plan on using. It\'s the only way to guarantee that a URL for that size will be present in the response. See [Photo Generation][photogeneration] for details.\n+_NOTE:_ Always pass in the `returnSizes` parameter for sizes you plan on using. It\'s the only way to guarantee that a URL for that size will be present in the response. See [Photo Generation](http://theopenphotoproject.org/documentation/faq/PhotoGeneration) for details.\n \n ----------------------------------------\n \n@@ -34,7 +34,8 @@ _Authentication: optional_\n <a name="parameters"></a>\n ### Parameters\n \n-1.  returnSizes (optional), The photo sizes you\'d like in the response. Specify every size you plan on using. [Docs for this parameter][ReturnSizes]\n+1.  returnSizes (optional), (i.e. 20x20 or 30x30xCR,40x40) The photo sizes you\'d like in the response. Specify every size you plan on using. [Docs for this parameter](http://theopenphotoproject.org/documentation/faq/ReturnSizes)\n+1.  generate (optional), (i.e. true or false) Tells the API to generate the sizes from `returnSizes` instead of returning a _create_ URL. [Docs for this parameter](http://theopenphotoproject.org/documentation/faq/ReturnSizes)\n 1.  pageSize (optional), Number of photos to return per request.\n 1.  page (optional), Page number when browsing through photos. Starts at 1.\n 1.  tags (optional), _i.e. dog,cat_ - A comma delimited string of strings.\n@@ -60,7 +61,7 @@ _Authentication: optional_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -133,7 +134,7 @@ The response is in a standard [response envelope][Envelope].\n       ]\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Photo]: ../schemas/Photo.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/GetTags.markdown b/documentation/api/GetTags.markdown\nindex 47581c2c..9f179070 100644\n--- a/documentation/api/GetTags.markdown\n+++ b/documentation/api/GetTags.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photos\n+Get Tags\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -55,7 +55,7 @@ _None_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -94,7 +94,7 @@ The response is in a standard [response envelope][Envelope].\n       ]\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Tag]: ../schemas/Tag.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/GetWebhook.markdown b/documentation/api/GetWebhook.markdown\nindex afa056e9..b934c8c2 100644\n--- a/documentation/api/GetWebhook.markdown\n+++ b/documentation/api/GetWebhook.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photo\n+Get Webhook\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -56,7 +56,7 @@ _None_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -79,7 +79,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Webhook]: ../schemas/Webhook.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/PostActionCreate.markdown b/documentation/api/PostActionCreate.markdown\nindex 93661f83..56be23d4 100644\n--- a/documentation/api/PostActionCreate.markdown\n+++ b/documentation/api/PostActionCreate.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photos\n+Create Action\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -61,7 +61,7 @@ _Authentication: required_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -85,8 +85,9 @@ The response is in a standard [response envelope][Envelope].\n         datePosted: "1318281477",\n         status: 1\n       }\n+    }\n+\n \n-[Envelope]: Envelope.markdown\n [Action]: ../schemas/Action.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/PostActionDelete.markdown b/documentation/api/PostActionDelete.markdown\nindex 40173eab..a393d588 100644\n--- a/documentation/api/PostActionDelete.markdown\n+++ b/documentation/api/PostActionDelete.markdown\n@@ -1 +1,83 @@\n-Not yet documented\n+Delete Action\n+=======================\n+\n+\n+----------------------------------------\n+\n+1. [Purpose][purpose]\n+1. [Endpoint][endpoint]\n+1. [Parameters][parameters]\n+1. [Examples][examples]\n+  * [Command line][example-cli]\n+  * [PHP][example-php]\n+1. [Response][response]\n+  * [Sample][sample]\n+\n+----------------------------------------\n+\n+<a name="purpose"></a>\n+### Purpose of the delete action API\n+\n+Use this API to delete an action on a photo. This includes comments and favorites.\n+\n+----------------------------------------\n+\n+<a name="endpoint"></a>\n+### Endpoint\n+\n+_Authentication: required_\n+\n+    POST /action/photo/:targetId/delete.json\n+\n+<a name="parameters"></a>\n+### Parameters\n+\n+_N/A_\n+\n+----------------------------------------\n+\n+<a name="examples"></a>\n+### Examples\n+\n+<a name="example-cli"></a>\n+#### Command Line (using [openphoto-php][openphoto-php])\n+\n+    ./openphoto -p -X POST -h current.openphoto.me -e /action/photo/a/delete.json\n+\n+<a name="example-php"></a>\n+#### PHP (using [openphoto-php][openphoto-php])\n+\n+    $client = new OpenPhotoOAuth($host, $consumerKey, $consumerSecret, $oauthToken, $oauthTokenSecret);\n+    $response = $client->post("/action/photo/a/delete.json");\n+\n+----------------------------------------\n+\n+<a name="response"></a>\n+### Response\n+\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n+\n+* _message_, A string describing the result. Don\'t use this for anything but reading.\n+* _code_, _200_ on success\n+* _result_, boolean\n+\n+<a name="sample"></a>\n+#### Sample\n+\n+    {\n+      "message":"",\n+      "code":200,\n+      "result": true\n+    }\n+\n+\n+[Action]: ../schemas/Action.markdown\n+[purpose]: #purpose\n+[endpoint]: #endpoint\n+[parameters]: #parameters\n+[examples]: #examples\n+[example-cli]: #example-cli\n+[example-php]: #example-php\n+[response]: #response\n+[sample]: #sample\n+[openphoto-php]: https://github.com/openphoto/openphoto-php\ndiff --git a/documentation/api/PostGroupCreate.markdown b/documentation/api/PostGroupCreate.markdown\nindex e247f9e6..837a0060 100644\n--- a/documentation/api/PostGroupCreate.markdown\n+++ b/documentation/api/PostGroupCreate.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photos\n+Create Group\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -56,7 +56,7 @@ _Authentication: required_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -77,7 +77,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Group]: ../schemas/Group.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/PostGroupDelete.markdown b/documentation/api/PostGroupDelete.markdown\nindex 71f9ac4a..da6d0b53 100644\n--- a/documentation/api/PostGroupDelete.markdown\n+++ b/documentation/api/PostGroupDelete.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photos\n+Delete Group\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -55,7 +55,7 @@ _None_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -70,7 +70,7 @@ The response is in a standard [response envelope][Envelope].\n       "result": true\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Group]: ../schemas/Group.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/PostGroupUpdate.markdown b/documentation/api/PostGroupUpdate.markdown\nindex cc2f7b34..3b6c9ea3 100644\n--- a/documentation/api/PostGroupUpdate.markdown\n+++ b/documentation/api/PostGroupUpdate.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photos\n+Update Group\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -56,7 +56,7 @@ _Authentication: required_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -77,7 +77,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Group]: ../schemas/Group.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/PostPhotoDelete.markdown b/documentation/api/PostPhotoDelete.markdown\nindex bca3c527..e819c866 100644\n--- a/documentation/api/PostPhotoDelete.markdown\n+++ b/documentation/api/PostPhotoDelete.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photos\n+Delete Photo\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -55,7 +55,7 @@ _None_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -70,7 +70,7 @@ The response is in a standard [response envelope][Envelope].\n       "result":true\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Action]: ../schemas/Action.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/PostPhotoUpdate.markdown b/documentation/api/PostPhotoUpdate.markdown\nindex 9dc886e8..e7e97f52 100644\n--- a/documentation/api/PostPhotoUpdate.markdown\n+++ b/documentation/api/PostPhotoUpdate.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Photo Upload\n+Update Photo\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -64,7 +64,7 @@ _Authentication: required_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _202_ on success\n@@ -110,7 +110,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Photo]: ../schemas/Photo.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/PostPhotoUpload.markdown b/documentation/api/PostPhotoUpload.markdown\nindex 17efd625..6a6b01cf 100644\n--- a/documentation/api/PostPhotoUpload.markdown\n+++ b/documentation/api/PostPhotoUpload.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Photo Upload\n+Upload Photo\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -32,6 +32,8 @@ _Authentication: required_\n <a name="parameters"></a>\n ### Parameters\n \n+(Due to a [bug in PHP\'s PECL OAuth extension](https://github.com/openphoto/frontend/issues/289) all parameters except for the photo must be passed urlencoded in the query string. This only applies for multipart and not application/x-www-form-urlencoded requests).\n+\n 1.  photo (required), The photo to be uploaded.\n     * This can be the binary photo in multipart/formdata\n     * This can be the base64 encoded value of the photo in application/x-www-form-urlencoded\n@@ -78,7 +80,7 @@ _Authentication: required_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _202_ on success\n@@ -124,7 +126,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Photo]: ../schemas/Photo.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/PostTagCreate.markdown b/documentation/api/PostTagCreate.markdown\nindex 4f247e0e..6da515e6 100644\n--- a/documentation/api/PostTagCreate.markdown\n+++ b/documentation/api/PostTagCreate.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photos\n+Creae Tag\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -59,7 +59,7 @@ _Authentication: required_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -78,7 +78,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Tag]: ../schemas/Tag.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/PostTagUpdate.markdown b/documentation/api/PostTagUpdate.markdown\nindex c27baba5..af9fc488 100644\n--- a/documentation/api/PostTagUpdate.markdown\n+++ b/documentation/api/PostTagUpdate.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photos\n+Update Tag\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -58,7 +58,7 @@ _Authentication: required_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -77,7 +77,7 @@ The response is in a standard [response envelope][Envelope].\n       }\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Tag]: ../schemas/Tag.markdown\n [purpose]: #purpose\n [endpoint]: #endpoint\ndiff --git a/documentation/api/PostWebhookDelete.markdown b/documentation/api/PostWebhookDelete.markdown\nindex de8ffc61..c32ab9ce 100644\n--- a/documentation/api/PostWebhookDelete.markdown\n+++ b/documentation/api/PostWebhookDelete.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photo\n+Delete Webhook\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -56,7 +56,7 @@ _None_\n <a name="response"></a>\n ### Response\n \n-The response is in a standard [response envelope][Envelope].\n+The response is in a standard [response envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n \n * _message_, A string describing the result. Don\'t use this for anything but reading.\n * _code_, _200_ on success\n@@ -71,7 +71,7 @@ The response is in a standard [response envelope][Envelope].\n       "result" : true\n     }\n \n-[Envelope]: Envelope.markdown\n+\n [Webhook]: ../schemas/Webhook.markdown\n [webhookverification]: ../faq/WebhookVerification.markdown\n [purpose]: #purpose\ndiff --git a/documentation/api/PostWebhookSubscribe.markdown b/documentation/api/PostWebhookSubscribe.markdown\nindex 546de183..48968808 100644\n--- a/documentation/api/PostWebhookSubscribe.markdown\n+++ b/documentation/api/PostWebhookSubscribe.markdown\n@@ -1,6 +1,6 @@\n-Open Photo API / Get Photo\n+Create/Subscribe to Webhook\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \n@@ -15,7 +15,7 @@ Open Photo API / Get Photo\n \n Use this API to create a new webhook for a user.\n \n-This API differs from our others in that it\'s both interactive and does not return a [Standard Envelope][Envelope]. These are the steps required to complete a webhook subscription.\n+This API differs from our others in that it\'s both interactive and does not return a [Standard Envelope](http://theopenphotoproject.org/documentation/api/Envelope). These are the steps required to complete a webhook subscription.\n \n <a name="verification"></a>\n \n@@ -43,7 +43,7 @@ _Authentication: required_\n \n ----------------------------------------\n \n-[Envelope]: Envelope.markdown\n+\n [purpose]: #purpose\n [endpoint]: #endpoint\n [parameters]: #parameters\ndiff --git a/documentation/api/Readme.markdown b/documentation/api/Readme.markdown\nnew file mode 100644\nindex 00000000..6f5d5ea1\n--- /dev/null\n+++ b/documentation/api/Readme.markdown\n@@ -0,0 +1,75 @@\n+Open Photo API\n+=======================\n+\n+\n+----------------------------------------\n+\n+### How do I authenticate?\n+\n+The Open Photo API uses [OAuth1.0a](http://oauth.net/core/1.0a/) for authentication. See the complete [guide on authentication](http://theopenphotoproject.org/documentation/api/Authentication) for details.\n+\n+### What\'s the response format?\n+\n+Every API endpoint returns a JSON response in a [standard envelope](http://theopenphotoproject.org/documentation/api/Envelope).\n+\n+    {\n+      message: "A string describing the response",\n+      code: 200,\n+      result: {\n+        foo: "bar"\n+      }\n+    }\n+\n+### API Endpoints\n+\n+#### Test / diagnostics endpoints\n+1.  [GET /hello.json](http://theopenphotoproject.org/documentation/api/GetHelloWorld)\n+    Test endpoint.\n+\n+#### Action endpoings (comments, favorites, etc)\n+1.  [POST /action/:id/:type/create.json](http://theopenphotoproject.org/documentation/api/PostActionCreate)\n+    Create an action.\n+1.  [POST /action/:id/delete.json](http://theopenphotoproject.org/documentation/api/PostActionDelete)\n+    Delete an action.\n+\n+#### Photo endpoints\n+1.  [POST /photos/:id/delete.json](http://theopenphotoproject.org/documentation/api/PostPhotoDelete)\n+    Delete a user\'s photo.\n+1.  [POST /photos/:id/update.json](http://theopenphotoproject.org/documentation/api/PostPhotoUpdate)\n+    Update data on a user\'s photo.\n+1.  [GET /photo/:id/view.json](http://theopenphotoproject.org/documentation/api/GetPhoto)\n+    Get a user\'s photo.\n+1.  [GET /photos/list.json](http://theopenphotoproject.org/documentation/api/GetPhotos)\n+    Get a list of the user\'s photos.\n+1.  [GET /photo/:id/nextprevious.json](http://theopenphotoproject.org/documentation/api/GetPhotoNextPrevious)\n+    Get the next and previous photo.\n+1.  [POST /photo/upload.json](http://theopenphotoproject.org/documentation/api/PostPhotoUpload)\n+    Upload a new photo.\n+\n+#### Tag endpoints\n+1.  [GET /tags/list.json](http://theopenphotoproject.org/documentation/api/GetTags)\n+    Get a user\'s tags.\n+1.  [POST /tag/:id/create.json](http://theopenphotoproject.org/documentation/api/PostTagCreate)\n+    Create a tag for the user.\n+1.  [POST /tag/:id/update.json](http://theopenphotoproject.org/documentation/api/PostTagUpdate)\n+    Modify meta data for a user\'s tag.\n+\n+#### Group endpoints\n+1.  [GET /group/:id/view.json](http://theopenphotoproject.org/documentation/api/GetGroup)\n+    Get a group.\n+1.  [GET /groups/list.json](http://theopenphotoproject.org/documentation/api/GetGroups)\n+    Get a listing of a user\'s groups.\n+1.  [POST /group/create.json](http://theopenphotoproject.org/documentation/api/PostGroupCreate)\n+    Create a group.\n+1.  [POST /group/delete.json](http://theopenphotoproject.org/documentation/api/PostGroupDelete)\n+    Delete a group.\n+1.  [POST /group/update.json](http://theopenphotoproject.org/documentation/api/PostGroupUpdate)\n+    Update a group.\n+\n+#### Webhook endpoints\n+1.  [POST /webhook/subscribe](http://theopenphotoproject.org/documentation/api/PostWebHookSubscribe)\n+    Update an eixsting webhook.\n+1.  [GET /webhook/:id/view.json](http://theopenphotoproject.org/documentation/api/GetWebhook)\n+    Get a user\'s webhook by id.\n+1.  [POST /webhook/:id/delete.json](http://theopenphotoproject.org/documentation/api/PostWebHookDelete)\n+    Delete an existing webhook.\ndiff --git a/documentation/faq/Faq.markdown b/documentation/faq/Faq.markdown\nindex 23832f19..30f80a32 100644\n--- a/documentation/faq/Faq.markdown\n+++ b/documentation/faq/Faq.markdown\n@@ -1,6 +1,5 @@\n-Documentation\n+Frequently asked questions\n =======================\n-#### OpenPhoto, a photo service for the masses\n \n This is a list of common questions that people have about OpenPhoto.\n If you have one which is not on this list send us a message via <a href="http://twitter.com/openphoto">Twitter</a> or <a href="mailto:hello@openphoto.me">email</a>.\n@@ -55,13 +54,10 @@ It depends on who you use but here are some guidelines.\n \n ### When will the OpenPhoto software be completed?\n \n-You can set up the OpenPhoto software using [any of the guides][guides].\n+You can set up the OpenPhoto software using guides.\n \n ----------------------------------------\n \n ### When will the hosted version of OpenPhoto be available?\n \n We are aiming to have it ready by the end of November (2011).\n-\n-[guides]: ../guides/Guides.markdown\n-\ndiff --git a/documentation/faq/Glossary.markdown b/documentation/faq/Glossary.markdown\nindex 9496b6cd..f63c338f 100644\n--- a/documentation/faq/Glossary.markdown\n+++ b/documentation/faq/Glossary.markdown\n@@ -1,6 +1,6 @@\n OpenPhoto / Glossary\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n The OpenPhoto stack consists of various components. Below is a glossary of all the terms you need to know so you can understand the overall system.\n \ndiff --git a/documentation/faq/PhotoGeneration.markdown b/documentation/faq/PhotoGeneration.markdown\nindex 086e204d..88f8ecd9 100644\n--- a/documentation/faq/PhotoGeneration.markdown\n+++ b/documentation/faq/PhotoGeneration.markdown\n@@ -1,14 +1,11 @@\n-Open Photo API / Photo Upload\n+How photos are generated\n =======================\n-#### OpenPhoto, a photo service for the masses\n-\n-----------------------------------------\n \n ### Overview of how and when versions of photos are generated\n \n By default, when a photo is uploaded there are two versions of that photo which are stored. The original version (`pathOriginal`) and a slightly lower resolution version (`pathBase`).\n \n-The API is capable of returning URLs for any size version of every photo. This is acheived by passing in a parameter named `returnSizes` to the [GET Photos][GetPhotos] and [GET Photo][GetPhoto] APIs. This ensures that the response for the photo(s) will include a URL for the size(s) you specify. Here is an example.\n+The API is capable of returning URLs for any size version of every photo. This is acheived by passing in a parameter named `returnSizes` to the [GET Photos](http://theopenphotoproject.org/documentation/api/GetPhotos) and [GET Photo](http://theopenphotoproject.org/documentation/api/GetPhoto) APIs. This ensures that the response for the photo(s) will include a URL for the size(s) you specify. Here is an example.\n \n     curl "http://current.openphoto.me/photo/63.json?returnSizes=123x123"\n \n@@ -106,8 +103,4 @@ The `returnSizes` parameter takes values in the form of _WxH[[xA]xB]_ which mean\n \n Additional options include `CR` and `BW`. `CR` tells the API to crop the photo to be exactly _width_ by _height_. It does a center crop and minimizes the portion of the photo that is cropped. `BW` applies a greyscale filter to the photo. `100x100xCRxBW` means the photo will have a key of `path100x100xCRxBW` and will be exactly _100_ by _100_, cropped and greyscale.\n \n-For more information on the `returnSizes` parameter see our [documentation on returnSizes][ReturnSizes].\n-\n-[GetPhotos]: ../api/GetPhotos.markdown\n-[GetPhoto]: ../api/GetPhoto.markdown\n-[ReturnSizes]: ../faq/ReturnSizes.markdown\n+For more information on the `returnSizes` parameter see our [documentation on returnSizes](http://theopenphotoproject.org/documentation/faq/ReturnSizes).\ndiff --git a/documentation/faq/ReturnSizes.markdown b/documentation/faq/ReturnSizes.markdown\nindex 478a26f7..d58d3f8d 100644\n--- a/documentation/faq/ReturnSizes.markdown\n+++ b/documentation/faq/ReturnSizes.markdown\n@@ -1,14 +1,11 @@\n-Open Photo API / Photo Upload\n+Getting specific sizes of a photo\n =======================\n-#### OpenPhoto, a photo service for the masses\n-\n-----------------------------------------\n \n ### Understanding the `returnSizes` parameter and how to use it\n-For the [GET /photo/view.json][GetPhoto] and [GET /photos/list.json][GetPhotos] you can pass in an optional `returnSizes` parameter.\n+For the [GET /photo/view.json](http://theopenphotoproject.org/documentation/api/GetPhoto) and [GET /photos/list.json](http://theopenphotoproject.org/documentation/api/GetPhotos) you can pass in an optional `returnSizes` parameter.\n If you need a path to a photo then you\'ll want to make sure you pass this parameter in else your response won\'t contain any URLs.\n \n-We also have a more detailed look at how [photos are generated][photogeneration].\n+We also have a more detailed look at how [photos are generated](http://theopenphotoproject.org/documentation/faq/PhotoGeneration).\n \n ### Specifying the width and height of photos you want\n The first decision you need to make is the size or sizes of photos you want in the response.\n@@ -35,7 +32,3 @@ Adding `xCR` means that the photo will be exactly _200px_ by _200px_ and cropped\n \n ### Accessing the paths in the response\n The name of the URL attribute in the response is, by convention, `pathWxH` where `WxH` is the value passed in to returnSizes.\n-\n-[GetPhoto]: ../api/GetPhoto.markdown\n-[GetPhotos]: ../api/GetPhotos.markdown\n-[photogeneration]: ../faq/PhotoGeneration.markdown\ndiff --git a/documentation/guides/InstallationOSXMacports.markdown b/documentation/guides/InstallationOSXMacports.markdown\nindex ed52041c..8102a619 100644\n--- a/documentation/guides/InstallationOSXMacports.markdown\n+++ b/documentation/guides/InstallationOSXMacports.markdown\n@@ -41,24 +41,6 @@ This guide assumes you have [get it here][Macports installed]. If not you can .\n \n Download and install the source code. We recommend `~/Sites/yourdomain.com` but you can use any directory you\'d like.\n \n-#### Using git clone\n-\n-    apt-get install git-core\n-    git clone git@github.com:openphoto/frontend.git /var/www/yourdomain.com\n-    chown -R www-data:www-data /var/www/yourdomain.com\n-\n-#### Using tar\n-\n-    cd /var/www\n-    wget https://github.com/openphoto/frontend/tarball/master -O openphoto.tar.gz\n-    tar -zxvf openphoto.tar.gz\n-    mv openphoto-frontend-* yourdomain.com\n-    chown -R www-data:www-data yourdomain.com\n-\n-Assuming that this is a development machine you can make the config writable by the user Apache runs as. Most likely `www-data`.\n-\n-    mkdir /var/www/yourdomain.com/src/userdata\n-    chown www-data:www-data /var/www/yourdomain.com/src/configs/generated\n #### Using git clone\n \n     # install git if you don\'t have it already\ndiff --git a/documentation/guides/InstallationUsingOpenPhotoEC2-AMI.markdown b/documentation/guides/InstallationUsingOpenPhotoEC2-AMI.markdown\nindex 1d11ac1e..e2ae476d 100644\n--- a/documentation/guides/InstallationUsingOpenPhotoEC2-AMI.markdown\n+++ b/documentation/guides/InstallationUsingOpenPhotoEC2-AMI.markdown\n@@ -1,4 +1,4 @@\n-OpenPhoto / Installation for Ubuntu\n+OpenPhoto / Installation for AWS EC2 using an AMI\n =======================\n #### OpenPhoto, a photo service for the masses\n \ndiff --git a/documentation/schemas/Action.markdown b/documentation/schemas/Action.markdown\nindex bdeb8e2d..a3dd3b4b 100644\n--- a/documentation/schemas/Action.markdown\n+++ b/documentation/schemas/Action.markdown\n@@ -1,6 +1,6 @@\n Documentation\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \ndiff --git a/documentation/schemas/Credential.markdown b/documentation/schemas/Credential.markdown\nindex be6a0d71..0c30b978 100644\n--- a/documentation/schemas/Credential.markdown\n+++ b/documentation/schemas/Credential.markdown\n@@ -1,6 +1,6 @@\n Documentation\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \ndiff --git a/documentation/schemas/Group.markdown b/documentation/schemas/Group.markdown\nindex 3a6c0493..20ba2b65 100644\n--- a/documentation/schemas/Group.markdown\n+++ b/documentation/schemas/Group.markdown\n@@ -1,6 +1,6 @@\n Documentation\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \ndiff --git a/documentation/schemas/Photo.markdown b/documentation/schemas/Photo.markdown\nindex a110ee97..4519615a 100644\n--- a/documentation/schemas/Photo.markdown\n+++ b/documentation/schemas/Photo.markdown\n@@ -1,6 +1,6 @@\n Documentation\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \ndiff --git a/documentation/schemas/Schemas.markdown b/documentation/schemas/Schemas.markdown\nindex c70aa213..153f10f7 100644\n--- a/documentation/schemas/Schemas.markdown\n+++ b/documentation/schemas/Schemas.markdown\n@@ -1,6 +1,6 @@\n OpenPhoto\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \ndiff --git a/documentation/schemas/Tag.markdown b/documentation/schemas/Tag.markdown\nindex 0913dd13..22a2a48b 100644\n--- a/documentation/schemas/Tag.markdown\n+++ b/documentation/schemas/Tag.markdown\n@@ -1,6 +1,6 @@\n Documentation\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \ndiff --git a/documentation/schemas/User.markdown b/documentation/schemas/User.markdown\nindex a70095ce..9c1b2c24 100644\n--- a/documentation/schemas/User.markdown\n+++ b/documentation/schemas/User.markdown\n@@ -1,6 +1,6 @@\n Documentation\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \ndiff --git a/documentation/schemas/Webhook.markdown b/documentation/schemas/Webhook.markdown\nindex 20536a96..5fde05e0 100644\n--- a/documentation/schemas/Webhook.markdown\n+++ b/documentation/schemas/Webhook.markdown\n@@ -1,6 +1,6 @@\n Documentation\n =======================\n-#### OpenPhoto, a photo service for the masses\n+\n \n ----------------------------------------\n \ndiff --git a/files/creative/logo.png b/files/creative/logo.png\nindex 966e5ce2..ccad0e7f 100644\nBinary files a/files/creative/logo.png and b/files/creative/logo.png differ\ndiff --git a/src/configs/defaults.ini b/src/configs/defaults.ini\nindex 071bcf92..c7b3515e 100644\n--- a/src/configs/defaults.ini\n+++ b/src/configs/defaults.ini\n@@ -1,4 +1,4 @@\n [defaults]\n theme=default\n lastCodeVersion=0.0.0\n-currentCodeVersion=1.3.0\n+currentCodeVersion=1.3.1\ndiff --git a/src/configs/upgrade/db/mysql/mysql-base.php b/src/configs/upgrade/db/mysql/mysql-base.php\nindex 07ac713c..bdd871db 100644\n--- a/src/configs/upgrade/db/mysql/mysql-base.php\n+++ b/src/configs/upgrade/db/mysql/mysql-base.php\n@@ -193,7 +193,7 @@ SQL;\n     INSERT INTO `{$this->mySqlTablePrefix}admin` (`key`,`value`) \n     VALUES (:key, :value)\n SQL;\n-  mysql_base($sql, array(\':key\' => \'version\', \':value\' => \'1.2.1\'));\n+  mysql_base($sql, array(\':key\' => \'version\', \':value\' => \'1.3.1\'));\n \n   return true;\n }\ndiff --git a/src/html/assets/themes/beisel/images/backgrounds.gif b/src/html/assets/images/backgrounds.gif\nsimilarity index 100%\nrename from src/html/assets/themes/beisel/images/backgrounds.gif\nrename to src/html/assets/images/backgrounds.gif\ndiff --git a/src/html/assets/themes/beisel/images/buttons-disabled.png b/src/html/assets/images/buttons-disabled.png\nsimilarity index 100%\nrename from src/html/assets/themes/beisel/images/buttons-disabled.png\nrename to src/html/assets/images/buttons-disabled.png\ndiff --git a/src/html/assets/themes/beisel/images/buttons.png b/src/html/assets/images/buttons.png\nsimilarity index 100%\nrename from src/html/assets/themes/beisel/images/buttons.png\nrename to src/html/assets/images/buttons.png\ndiff --git a/src/html/assets/themes/beisel/images/checkmark-big.gif b/src/html/assets/images/checkmark-big.gif\nsimilarity index 100%\nrename from src/html/assets/themes/beisel/images/checkmark-big.gif\nrename to src/html/assets/images/checkmark-big.gif\ndiff --git a/src/html/assets/images/logo.png b/src/html/assets/images/logo.png\nindex c232c552..fb6d7052 100644\nBinary files a/src/html/assets/images/logo.png and b/src/html/assets/images/logo.png differ\ndiff --git a/src/html/assets/themes/beisel/images/transp50.png b/src/html/assets/images/transp50.png\nsimilarity index 100%\nrename from src/html/assets/themes/beisel/images/transp50.png\nrename to src/html/assets/images/transp50.png\ndiff --git a/src/html/assets/themes/beisel/images/upload-big.gif b/src/html/assets/images/upload-big.gif\nsimilarity index 100%\nrename from src/html/assets/themes/beisel/images/upload-big.gif\nrename to src/html/assets/images/upload-big.gif\ndiff --git a/src/html/assets/themes/beisel/javascripts/jquery.plupload.queue.js b/src/html/assets/javascripts/jquery.plupload.queue.js\nsimilarity index 100%\nrename from src/html/assets/themes/beisel/javascripts/jquery.plupload.queue.js\nrename to src/html/assets/javascripts/jquery.plupload.queue.js\ndiff --git a/src/html/assets/javascripts/minification.txt b/src/html/assets/javascripts/minification.txt\nnew file mode 100644\nindex 00000000..0a81a0be\n--- /dev/null\n+++ b/src/html/assets/javascripts/minification.txt\n@@ -0,0 +1 @@\n+minify plupload.js plupload.html5.js jquery.plupload.queue.js openphoto-upload.js openphoto-upload.min.js --force\ndiff --git a/src/html/assets/javascripts/openphoto-upload.js b/src/html/assets/javascripts/openphoto-upload.js\nnew file mode 100644\nindex 00000000..76f69071\n--- /dev/null\n+++ b/src/html/assets/javascripts/openphoto-upload.js\n@@ -0,0 +1,81 @@\n+/**\n+* Utility methods for OpenPhoto - this will include the entire framework\n+* And ability to swap frameworks (YUI or jQuery) in and out.\n+*/\n+OPU = (function() {\n+  return {\n+      init: function() {\n+        var uploaderEl = $("#uploader");\n+        if(uploaderEl.length == 0)\n+          return;\n+\n+        uploaderEl.pluploadQueue({\n+            // General settings\n+            runtimes : \'html5\',\n+            url : \'/photo/upload.json\',\n+            max_file_size : \'20mb\',\n+            //chunk_size : \'1mb\',\n+            unique_names : true,\n+     \n+            // Specify what files to browse for\n+            filters : [\n+                {title : "Photos", extensions : "jpg,jpeg,gif,png"}\n+            ],\n+     \n+            // Flash settings\n+            flash_swf_url : \'plupload.flash.swf\',\n+            multipart_params:{\n+              crumb: $("form.upload input.crumb").val()\n+            },\n+            preinit: {\n+              BeforeUpload: function() {\n+                var uploader = $("#uploader").pluploadQueue();\n+                $(".upload-progress .total").html(uploader.files.length);\n+                $(".upload-progress .completed").html(uploader.total.uploaded+1);\n+                $(".upload-progress").slideDown(\'fast\');\n+              },\n+              UploadComplete: function() {\n+                $(".upload-progress").fadeOut(\'fast\', function() { $(".upload-complete").fadeIn(\'fast\'); });\n+              },\n+              UploadFile: function() {\n+                var uploader = $("#uploader").pluploadQueue(), license, permission, tags;\n+                license = $("form.upload select[name=\'license\'] :selected").val();\n+                if(license.length == 0)\n+                  license = $("form.upload input[name=\'custom\']").val();\n+                tags = $("form.upload input[name=\'tags\']").val();\n+                permission = $("form.upload input[name=\'permission\']:checked").val();\n+                \n+                uploader.settings.multipart_params.license = license;\n+                uploader.settings.multipart_params.tags = tags;\n+                uploader.settings.multipart_params.permission = permission;\n+              }\n+            }\n+        });\n+     \n+        // Client side form validation\n+        var uploadForm = $("form.upload");\n+        uploadForm.submit(function(e) {\n+          var uploader = $(\'#uploader\').pluploadQueue({});\n+          // Files in queue upload them first\n+          if (uploader.files.length > 0) {\n+            // When all files are uploaded submit form\n+            uploader.bind(\'StateChanged\', function() {\n+              if (uploader.files.length === (uploader.total.uploaded + uploader.total.failed)) {\n+                $("form.upload")[0].submit();\n+              }\n+            }); \n+            uploader.start();\n+          } else {\n+            // TODO something that doesn\'t suck\n+            alert(\'Please select at least one photo to upload.\');\n+          }\n+   \n+          return false;\n+        });\n+\n+        var insufficient = $("#uploader .insufficient");\n+        if(insufficient.length == 1)\n+          insufficient.show();\n+      }\n+    };\n+}());\ndiff --git a/src/html/assets/javascripts/openphoto-upload.min.js b/src/html/assets/javascripts/openphoto-upload.min.js\nnew file mode 100644\nindex 00000000..9127bccc\n--- /dev/null\n+++ b/src/html/assets/javascripts/openphoto-upload.min.js\n@@ -0,0 +1,67 @@\n+(function(){function l(){this.returnValue=false}function h(){this.cancelBubble=true}var j=0,s=[],r={},t={},v={"<":"lt",">":"gt","&":"amp",\'"\':"quot","\'":"#39"},u=/[<>&\\"\\\']/g,p=window.setTimeout,o={},m;(function(a){var a=a.split(/,/),b,e,d;for(b=0;b<a.length;b+=2){d=a[b+1].split(/ /);for(e=0;e<d.length;e++)t[d[e]]=a[b]}})("application/msword,doc dot,application/pdf,pdf,application/pgp-signature,pgp,application/postscript,ps ai eps,application/rtf,rtf,application/vnd.ms-excel,xls xlb,application/vnd.ms-powerpoint,ppt pps pot,application/zip,zip,application/x-shockwave-flash,swf swfl,application/vnd.openxmlformats,docx pptx xlsx,audio/mpeg,mpga mpega mp2 mp3,audio/x-wav,wav,audio/mp4,m4a,image/bmp,bmp,image/gif,gif,image/jpeg,jpeg jpg jpe,image/photoshop,psd,image/png,png,image/svg+xml,svg svgz,image/tiff,tiff tif,text/html,htm html xhtml,text/rtf,rtf,video/mpeg,mpeg mpg mpe,video/quicktime,qt mov,video/mp4,mp4,video/x-m4v,m4v,video/x-flv,flv,video/x-ms-wmv,wmv,video/avi,avi,video/webm,webm,video/vnd.rn-realvideo,rv,text/csv,csv,text/plain,asc txt text diff log,application/octet-stream,exe");\n+var f={VERSION:"1.5.1.1",STOPPED:1,STARTED:2,QUEUED:1,UPLOADING:2,FAILED:4,DONE:5,GENERIC_ERROR:-100,HTTP_ERROR:-200,IO_ERROR:-300,SECURITY_ERROR:-400,INIT_ERROR:-500,FILE_SIZE_ERROR:-600,FILE_EXTENSION_ERROR:-601,IMAGE_FORMAT_ERROR:-700,IMAGE_MEMORY_ERROR:-701,IMAGE_DIMENSIONS_ERROR:-702,mimeTypes:t,ua:function(){var a=navigator,b=a.userAgent,e=a.vendor,d,g;g=(d=/WebKit/.test(b))&&e.indexOf("Apple")!==-1;e=window.opera&&window.opera.buildNumber;return{windows:navigator.platform.indexOf("Win")!==\n+-1,ie:!d&&!e&&/MSIE/gi.test(b)&&/Explorer/gi.test(a.appName),webkit:d,gecko:!d&&/Gecko/.test(b),safari:g,opera:!!e}}(),extend:function(a){f.each(arguments,function(b,e){e>0&&f.each(b,function(b,g){a[g]=b})});return a},cleanName:function(a){var b,e;e=[/[\\300-\\306]/g,"A",/[\\340-\\346]/g,"a",/\\307/g,"C",/\\347/g,"c",/[\\310-\\313]/g,"E",/[\\350-\\353]/g,"e",/[\\314-\\317]/g,"I",/[\\354-\\357]/g,"i",/\\321/g,"N",/\\361/g,"n",/[\\322-\\330]/g,"O",/[\\362-\\370]/g,"o",/[\\331-\\334]/g,"U",/[\\371-\\374]/g,"u"];for(b=0;b<e.length;b+=\n+2)a=a.replace(e[b],e[b+1]);a=a.replace(/\\s+/g,"_");return a=a.replace(/[^a-z0-9_\\-\\.]+/gi,"")},addRuntime:function(a,b){b.name=a;s[a]=b;s.push(b);return b},guid:function(){var a=(new Date).getTime().toString(32),b;for(b=0;b<5;b++)a+=Math.floor(Math.random()*65535).toString(32);return(f.guidPrefix||"p")+a+(j++).toString(32)},buildUrl:function(a,b){var e="";f.each(b,function(a,g){e+=(e?"&":"")+encodeURIComponent(g)+"="+encodeURIComponent(a)});e&&(a+=(a.indexOf("?")>0?"&":"?")+e);return a},each:function(a,\n+b){var e,d;if(a)if(e=a.length,e===void 0)for(d in a){if(a.hasOwnProperty(d)&&b(a[d],d)===false)break}else for(d=0;d<e;d++)if(b(a[d],d)===false)break},formatSize:function(a){return a===void 0||/\\D/.test(a)?f.translate("N/A"):a>1073741824?Math.round(a/1073741824,1)+" GB":a>1048576?Math.round(a/1048576,1)+" MB":a>1024?Math.round(a/1024,1)+" KB":a+" b"},getPos:function(a,b){function e(a){var c,g=0;c=0;a&&(c=a.getBoundingClientRect(),a=q.compatMode==="CSS1Compat"?q.documentElement:q.body,g=c.left+a.scrollLeft,\n+c=c.top+a.scrollTop);return{x:g,y:c}}var d=0,g=0,c,q=document,b=b||q.body;if(a&&a.getBoundingClientRect&&navigator.userAgent.indexOf("MSIE")>0&&q.documentMode!==8)return d=e(a),g=e(b),{x:d.x-g.x,y:d.y-g.y};for(c=a;c&&c!=b&&c.nodeType;)d+=c.offsetLeft||0,g+=c.offsetTop||0,c=c.offsetParent;for(c=a.parentNode;c&&c!=b&&c.nodeType;)d-=c.scrollLeft||0,g-=c.scrollTop||0,c=c.parentNode;return{x:d,y:g}},getSize:function(a){return{w:a.offsetWidth||a.clientWidth,h:a.offsetHeight||a.clientHeight}},parseSize:function(a){var b;\n+typeof a=="string"&&(a=/^([0-9]+)([mgk]?)$/.exec(a.toLowerCase().replace(/[^0-9mkg]/g,"")),b=a[2],a=+a[1],b=="g"&&(a*=1073741824),b=="m"&&(a*=1048576),b=="k"&&(a*=1024));return a},xmlEncode:function(a){return a?(""+a).replace(u,function(a){return v[a]?"&"+v[a]+";":a}):a},toArray:function(a){var b,e=[];for(b=0;b<a.length;b++)e[b]=a[b];return e},addI18n:function(a){return f.extend(r,a)},translate:function(a){return r[a]||a},isEmptyObj:function(a){if(a===void 0)return true;for(var b in a)return false;\n+return true},hasClass:function(a,b){return a.className==""?false:RegExp("(^|\\\\s+)"+b+"(\\\\s+|$)").test(a.className)},addClass:function(a,b){if(!f.hasClass(a,b))a.className=a.className==""?b:a.className.replace(/\\s+$/,"")+" "+b},removeClass:function(a,b){a.className=a.className.replace(RegExp("(^|\\\\s+)"+b+"(\\\\s+|$)"),function(a,b,g){return b===" "&&g===" "?" ":""})},getStyle:function(a,b){if(a.currentStyle)return a.currentStyle[b];else if(window.getComputedStyle)return window.getComputedStyle(a,null)[b]},\n+addEvent:function(a,b,e,d){var g,b=b.toLowerCase();m===void 0&&(m="Plupload_"+f.guid());a.addEventListener?(g=e,a.addEventListener(b,g,false)):a.attachEvent&&(g=function(){var a=window.event;if(!a.target)a.target=a.srcElement;a.preventDefault=l;a.stopPropagation=h;e(a)},a.attachEvent("on"+b,g));a[m]===void 0&&(a[m]=f.guid());o.hasOwnProperty(a[m])||(o[a[m]]={});a=o[a[m]];a.hasOwnProperty(b)||(a[b]=[]);a[b].push({func:g,orig:e,key:d})},removeEvent:function(a,b,e){var d,g;typeof e=="function"?d=e:g=\n+e;b=b.toLowerCase();if(a[m]&&o[a[m]]&&o[a[m]][b]){for(var e=o[a[m]][b],c=e.length-1;c>=0;c--)if(e[c].key===g||e[c].orig===d)if(a.detachEvent?a.detachEvent("on"+b,e[c].func):a.removeEventListener&&a.removeEventListener(b,e[c].func,false),e[c].orig=null,e[c].func=null,e.splice(c,1),d!==void 0)break;e.length||delete o[a[m]][b];if(f.isEmptyObj(o[a[m]])){delete o[a[m]];try{delete a[m]}catch(q){a[m]=void 0}}}},removeAllEvents:function(a,b){a[m]!==void 0&&a[m]&&f.each(o[a[m]],function(e,d){f.removeEvent(a,\n+d,b)})}};f.Uploader=function(a){function b(){var a,g=0,b;if(this.state==f.STARTED){for(b=0;b<c.length;b++)!a&&c[b].status==f.QUEUED?(a=c[b],a.status=f.UPLOADING,this.trigger("BeforeUpload",a)&&this.trigger("UploadFile",a)):g++;g==c.length&&(this.stop(),this.trigger("UploadComplete",c))}}function e(){var a,b;g.reset();for(a=0;a<c.length;a++)b=c[a],b.size!==void 0?(g.size+=b.size,g.loaded+=b.loaded):g.size=void 0,b.status==f.DONE?g.uploaded++:b.status==f.FAILED?g.failed++:g.queued++;g.size===void 0?\n+g.percent=c.length>0?Math.ceil(g.uploaded/c.length*100):0:(g.bytesPerSec=Math.ceil(g.loaded/((+new Date-q||1)/1E3)),g.percent=g.size>0?Math.ceil(g.loaded/g.size*100):0)}var d={},g,c=[],q;g=new f.QueueProgress;a=f.extend({chunk_size:0,multipart:true,multi_selection:true,file_data_name:"photo",filters:[]},a);f.extend(this,{state:f.STOPPED,runtime:"",features:{},files:c,settings:a,total:g,id:f.guid(),init:function(){function g(){var a=j[l++],c,b,e;if(a){c=a.getFeatures();if(b=d.settings.required_features){b=\n+b.split(",");for(e=0;e<b.length;e++)if(!c[b[e]]){g();return}}a.init(d,function(b){b&&b.success?(d.features=c,d.runtime=a.name,d.trigger("Init",{runtime:a.name}),d.trigger("PostInit"),d.refresh()):g()})}else d.trigger("Error",{code:f.INIT_ERROR,message:f.translate("Init error.")})}var d=this,k,j,l=0,n;typeof a.preinit=="function"?a.preinit(d):f.each(a.preinit,function(a,c){d.bind(c,a)});a.page_url=a.page_url||document.location.pathname.replace(/\\/[^\\/]+$/g,"/");if(!/^(\\w+:\\/\\/|\\/)/.test(a.url))a.url=\n+a.page_url+a.url;a.chunk_size=f.parseSize(a.chunk_size);a.max_file_size=f.parseSize(a.max_file_size);d.bind("FilesAdded",function(b,g){var e,k,x=0,q;if((e=a.filters)&&e.length)q=[],f.each(e,function(a){f.each(a.extensions.split(/,/),function(a){/^\\s*\\*\\s*$/.test(a)?q.push("\\\\.*"):q.push("\\\\."+a.replace(RegExp("["+"/^$.*+?|()[]{}\\\\".replace(/./g,"\\\\$&")+"]","g"),"\\\\$&"))})}),q=RegExp(q.join("|")+"$","i");for(e=0;e<g.length;e++)k=g[e],k.loaded=0,k.percent=0,k.status=f.QUEUED,q&&!q.test(k.name)?b.trigger("Error",\n+{code:f.FILE_EXTENSION_ERROR,message:f.translate("File extension error."),file:k}):k.size!==void 0&&k.size>a.max_file_size?b.trigger("Error",{code:f.FILE_SIZE_ERROR,message:f.translate("File size error."),file:k}):(c.push(k),x++);if(x)p(function(){d.trigger("QueueChanged");d.refresh()},1);else return false});a.unique_names&&d.bind("UploadFile",function(a,c){var b=c.name.match(/\\.([^.]+)$/),g="tmp";b&&(g=b[1]);c.target_name=c.id+"."+g});d.bind("UploadProgress",function(a,c){c.percent=c.size>0?Math.ceil(c.loaded/\n+c.size*100):100;e()});d.bind("StateChanged",function(a){if(a.state==f.STARTED)q=+new Date;else if(a.state==f.STOPPED)for(k=a.files.length-1;k>=0;k--)if(a.files[k].status==f.UPLOADING)a.files[k].status=f.QUEUED,e()});d.bind("QueueChanged",e);d.bind("Error",function(a,c){if(c.file)c.file.status=f.FAILED,e(),a.state==f.STARTED&&p(function(){b.call(d)},1)});d.bind("FileUploaded",function(a,c){c.status=f.DONE;c.loaded=c.size;a.trigger("UploadProgress",c);p(function(){b.call(d)},1)});if(a.runtimes){j=[];\n+n=a.runtimes.split(/\\s?,\\s?/);for(k=0;k<n.length;k++)s[n[k]]&&j.push(s[n[k]])}else j=s;g();typeof a.init=="function"?a.init(d):f.each(a.init,function(a,c){d.bind(c,a)})},refresh:function(){this.trigger("Refresh")},start:function(){if(this.state!=f.STARTED)this.state=f.STARTED,this.trigger("StateChanged"),b.call(this)},stop:function(){if(this.state!=f.STOPPED)this.state=f.STOPPED,this.trigger("StateChanged")},getFile:function(a){var b;for(b=c.length-1;b>=0;b--)if(c[b].id===a)return c[b]},removeFile:function(a){var b;\n+for(b=c.length-1;b>=0;b--)if(c[b].id===a.id)return this.splice(b,1)[0]},splice:function(a,b){var g;g=c.splice(a===void 0?0:a,b===void 0?c.length:b);this.trigger("FilesRemoved",g);this.trigger("QueueChanged");return g},trigger:function(a){var c=d[a.toLowerCase()],b,g;if(c){g=Array.prototype.slice.call(arguments);g[0]=this;for(b=0;b<c.length;b++)if(c[b].func.apply(c[b].scope,g)===false)return false}return true},hasEventListener:function(a){return!!d[a.toLowerCase()]},bind:function(a,c,b){var g,a=a.toLowerCase();\n+g=d[a]||[];g.push({func:c,scope:b||this});d[a]=g},unbind:function(a,c){var a=a.toLowerCase(),b=d[a],g;if(b){if(c!==void 0)for(g=b.length-1;g>=0;g--){if(b[g].func===c){b.splice(g,1);break}}else b=[];b.length||delete d[a]}},unbindAll:function(){var a=this;f.each(d,function(c,b){a.unbind(b)})},destroy:function(){this.trigger("Destroy");this.unbindAll()}})};f.File=function(a,b,e){this.id=a;this.name=b;this.size=e;this.status=this.percent=this.loaded=0};f.Runtime=function(){this.getFeatures=function(){};\n+this.init=function(){}};f.QueueProgress=function(){var a=this;a.size=0;a.loaded=0;a.uploaded=0;a.failed=0;a.queued=0;a.percent=0;a.bytesPerSec=0;a.reset=function(){a.size=a.loaded=a.uploaded=a.failed=a.queued=a.percent=a.bytesPerSec=0}};f.runtimes={};window.plupload=f})();\n+(function(l,h,j,s){function r(a,b){var e;if("FileReader"in l)e=new FileReader,e.readAsDataURL(a),e.onload=function(){b(e.result)};else return b(a.getAsDataURL())}function t(a,b){var e;if("FileReader"in l)e=new FileReader,e.readAsBinaryString(a),e.onload=function(){b(e.result)};else return b(a.getAsBinary())}function v(a,b,e,d){var g,c,q,f,j=this;r(m[a.id],function(k){g=h.createElement("canvas");g.style.display="none";h.body.appendChild(g);c=g.getContext("2d");q=new Image;q.onerror=q.onabort=function(){d({success:false})};\n+q.onload=function(){var l,h,n,m;if(!b.width)b.width=q.width;if(!b.height)b.height=q.height;f=Math.min(b.width/q.width,b.height/q.height);if(f<1||f===1&&e==="image/jpeg"){l=Math.round(q.width*f);h=Math.round(q.height*f);g.width=l;g.height=h;c.drawImage(q,0,0,l,h);if(e==="image/jpeg"){if(n=new p(atob(k.substring(k.indexOf("base64,")+7))),n.headers&&n.headers.length&&(m=new o,m.init(n.get("exif")[0])&&(m.setExif("PixelXDimension",l),m.setExif("PixelYDimension",h),n.set("exif",m.getBinary()),j.hasEventListener("ExifData")&&\n+j.trigger("ExifData",a,m.EXIF()),j.hasEventListener("GpsData")&&j.trigger("GpsData",a,m.GPS()))),b.quality)try{k=g.toDataURL(e,b.quality/100)}catch(r){k=g.toDataURL(e)}}else k=g.toDataURL(e);k=k.substring(k.indexOf("base64,")+7);k=atob(k);n&&n.headers&&n.headers.length&&(k=n.restore(k),n.purge());g.parentNode.removeChild(g);d({success:true,data:k})}else d({success:false})};q.src=k})}function u(){function a(a,c){var b=e?0:-8*(c-1),f=0,j;for(j=0;j<c;j++)f|=d.charCodeAt(a+j)<<Math.abs(b+j*8);return f}\n+function b(a,c,b){b=arguments.length===3?b:d.length-c-1;d=d.substr(0,c)+a+d.substr(b+c)}var e=false,d;return{II:function(a){if(a===s)return e;else e=a},init:function(a){e=false;d=a},SEGMENT:function(a,c,e){switch(arguments.length){case 1:return d.substr(a,d.length-a-1);case 2:return d.substr(a,c);case 3:b(e,a,c);break;default:return d}},BYTE:function(b){return a(b,1)},SHORT:function(b){return a(b,2)},LONG:function(g,c){if(c===s)return a(g,4);else{var d="",j=e?0:-24,f;for(f=0;f<4;f++)d+=String.fromCharCode(c>>\n+Math.abs(j+f*8)&255);b(d,g,4)}},SLONG:function(b){b=a(b,4);return b>2147483647?b-4294967296:b},STRING:function(b,c){var d="";for(c+=b;b<c;b++)d+=String.fromCharCode(a(b,1));return d}}}function p(a){var b={65505:{app:"EXIF",name:"APP1",signature:"Exif\\x00"},65506:{app:"ICC",name:"APP2",signature:"ICC_PROFILE\\x00"},65517:{app:"IPTC",name:"APP13",signature:"Photoshop 3.0\\x00"}},e=[],d,g,c=s,f=0;d=new u;d.init(a);if(d.SHORT(0)===65496){g=2;for(a=Math.min(1048576,a.length);g<=a;)if(c=d.SHORT(g),c>=65488&&\n+c<=65495)g+=2;else{if(c===65498||c===65497)break;f=d.SHORT(g+2)+2;b[c]&&d.STRING(g+4,b[c].signature.length)===b[c].signature&&e.push({hex:c,app:b[c].app.toUpperCase(),name:b[c].name.toUpperCase(),start:g,length:f,segment:d.SEGMENT(g,f)});g+=f}d.init(null);return{headers:e,restore:function(a){d.init(a);var b=new p(a);if(!b.headers)return false;for(a=b.headers.length;a>0;a--){var c=b.headers[a-1];d.SEGMENT(c.start,c.length,"")}b.purge();g=d.SHORT(2)==65504?4+d.SHORT(4):2;a=0;for(b=e.length;a<b;a++)d.SEGMENT(g,\n+0,e[a].segment),g+=e[a].length;return d.SEGMENT()},get:function(a){for(var b=[],c=0,g=e.length;c<g;c++)e[c].app===a.toUpperCase()&&b.push(e[c].segment);return b},set:function(a,b){var c=[];typeof b==="string"?c.push(b):c=b;for(var g=ii=0,d=e.length;g<d;g++){if(e[g].app===a.toUpperCase())e[g].segment=c[ii],e[g].length=c[ii].length,ii++;if(ii>=c.length)break}},purge:function(){e=[];d.init(null)}}}}function o(){function a(a,e){var f=b.SHORT(a),j,k,l,h,n,m=[],p={};for(j=0;j<f;j++)if(n=a+12*j+2,l=e[b.SHORT(n)],\n+l!==s){k=b.SHORT(n+=2);h=b.LONG(n+=2);n+=4;m=[];switch(k){case 1:case 7:h>4&&(n=b.LONG(n)+d.tiffHeader);for(k=0;k<h;k++)m[k]=b.BYTE(n+k);break;case 2:h>4&&(n=b.LONG(n)+d.tiffHeader);p[l]=b.STRING(n,h-1);continue;case 3:h>2&&(n=b.LONG(n)+d.tiffHeader);for(k=0;k<h;k++)m[k]=b.SHORT(n+k*2);break;case 4:h>1&&(n=b.LONG(n)+d.tiffHeader);for(k=0;k<h;k++)m[k]=b.LONG(n+k*4);break;case 5:n=b.LONG(n)+d.tiffHeader;for(k=0;k<h;k++)m[k]=b.LONG(n+k*4)/b.LONG(n+k*4+4);break;case 9:n=b.LONG(n)+d.tiffHeader;for(k=0;k<\n+h;k++)m[k]=b.SLONG(n+k*4);break;case 10:n=b.LONG(n)+d.tiffHeader;for(k=0;k<h;k++)m[k]=b.SLONG(n+k*4)/b.SLONG(n+k*4+4);break;default:continue}h=h==1?m[0]:m;p[l]=g.hasOwnProperty(l)&&typeof h!="object"?g[l][h]:h}return p}var b,e,d={},g;b=new u;e={tiff:{274:"Orientation",34665:"ExifIFDPointer",34853:"GPSInfoIFDPointer"},exif:{36864:"ExifVersion",40961:"ColorSpace",40962:"PixelXDimension",40963:"PixelYDimension",36867:"DateTimeOriginal",33434:"ExposureTime",33437:"FNumber",34855:"ISOSpeedRatings",37377:"ShutterSpeedValue",\n+37378:"ApertureValue",37383:"MeteringMode",37384:"LightSource",37385:"Flash",41986:"ExposureMode",41987:"WhiteBalance",41990:"SceneCaptureType",41988:"DigitalZoomRatio",41992:"Contrast",41993:"Saturation",41994:"Sharpness"},gps:{0:"GPSVersionID",1:"GPSLatitudeRef",2:"GPSLatitude",3:"GPSLongitudeRef",4:"GPSLongitude"}};g={ColorSpace:{1:"sRGB",0:"Uncalibrated"},MeteringMode:{0:"Unknown",1:"Average",2:"CenterWeightedAverage",3:"Spot",4:"MultiSpot",5:"Pattern",6:"Partial",255:"Other"},LightSource:{1:"Daylight",\n+2:"Fliorescent",3:"Tungsten",4:"Flash",9:"Fine weather",10:"Cloudy weather",11:"Shade",12:"Daylight fluorescent (D 5700 - 7100K)",13:"Day white fluorescent (N 4600 -5400K)",14:"Cool white fluorescent (W 3900 - 4500K)",15:"White fluorescent (WW 3200 - 3700K)",17:"Standard light A",18:"Standard light B",19:"Standard light C",20:"D55",21:"D65",22:"D75",23:"D50",24:"ISO studio tungsten",255:"Other"},Flash:{0:"Flash did not fire.",1:"Flash fired.",5:"Strobe return light not detected.",7:"Strobe return light detected.",\n+9:"Flash fired, compulsory flash mode",13:"Flash fired, compulsory flash mode, return light not detected",15:"Flash fired, compulsory flash mode, return light detected",16:"Flash did not fire, compulsory flash mode",24:"Flash did not fire, auto mode",25:"Flash fired, auto mode",29:"Flash fired, auto mode, return light not detected",31:"Flash fired, auto mode, return light detected",32:"No flash function",65:"Flash fired, red-eye reduction mode",69:"Flash fired, red-eye reduction mode, return light not detected",\n+71:"Flash fired, red-eye reduction mode, return light detected",73:"Flash fired, compulsory flash mode, red-eye reduction mode",77:"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",79:"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",89:"Flash fired, auto mode, red-eye reduction mode",93:"Flash fired, auto mode, return light not detected, red-eye reduction mode",95:"Flash fired, auto mode, return light detected, red-eye reduction mode"},\n+ExposureMode:{0:"Auto exposure",1:"Manual exposure",2:"Auto bracket"},WhiteBalance:{0:"Auto white balance",1:"Manual white balance"},SceneCaptureType:{0:"Standard",1:"Landscape",2:"Portrait",3:"Night scene"},Contrast:{0:"Normal",1:"Soft",2:"Hard"},Saturation:{0:"Normal",1:"Low saturation",2:"High saturation"},Sharpness:{0:"Normal",1:"Soft",2:"Hard"},GPSLatitudeRef:{N:"North latitude",S:"South latitude"},GPSLongitudeRef:{E:"East longitude",W:"West longitude"}};return{init:function(c){d={tiffHeader:10};\n+if(c===s||!c.length)return false;b.init(c);b.SHORT(0)===65505&&b.STRING(4,5).toUpperCase()==="EXIF\\x00"?(c=s,c=d.tiffHeader,b.II(b.SHORT(c)==18761),b.SHORT(c+=2)!==42?c=false:(d.IFD0=d.tiffHeader+b.LONG(c+2),c=a(d.IFD0,e.tiff),d.exifIFD="ExifIFDPointer"in c?d.tiffHeader+c.ExifIFDPointer:s,d.gpsIFD="GPSInfoIFDPointer"in c?d.tiffHeader+c.GPSInfoIFDPointer:s,c=true)):c=false;return c},EXIF:function(){var b;b=a(d.exifIFD,e.exif);if(b.ExifVersion)b.ExifVersion=String.fromCharCode(b.ExifVersion[0],b.ExifVersion[1],\n+b.ExifVersion[2],b.ExifVersion[3]);return b},GPS:function(){var b;b=a(d.gpsIFD,e.gps);if(b.GPSVersionID)b.GPSVersionID=b.GPSVersionID.join(".");return b},setExif:function(a,g){var f;if(a!=="PixelXDimension"&&a!=="PixelYDimension")f=false;else{f=a;var j,k,l,h=0;if(typeof f==="string")for(hex in j=e.exif,j)if(j[hex]===f){f=hex;break}j=d.exifIFD;k=b.SHORT(j);for(i=0;i<k;i++)if(l=j+12*i+2,b.SHORT(l)==f){h=l+8;break}h?(b.LONG(h,g),f=true):f=false}return f},getBinary:function(){return b.SEGMENT()}}}var m=\n+{},f;j.runtimes.Html5=j.addRuntime("html5",{getFeatures:function(){var a,b,e,d,g,c;b=e=g=c=false;l.XMLHttpRequest&&(a=new XMLHttpRequest,e=!!a.upload,b=!(!a.sendAsBinary&&!a.upload));b&&(d=!!(a.sendAsBinary||l.Uint8Array&&l.ArrayBuffer),g=!(!File||!File.prototype.getAsDataURL&&!l.FileReader||!d),c=!(!File||!File.prototype.mozSlice&&!File.prototype.webkitSlice&&!File.prototype.slice));f=j.ua.safari&&j.ua.windows;a=b;b=h.createElement("div");return{html5:a,dragdrop:"draggable"in b||"ondragstart"in b&&\n+"ondrop"in b,jpgresize:g,pngresize:g,multipart:g||!!l.FileReader||!!l.FormData,canSendBinary:d,cantSendBlobInFormData:!(!j.ua.gecko||!l.FormData||!l.FileReader||FileReader.prototype.readAsArrayBuffer),progress:e,chunks:c,multi_selection:!(j.ua.safari&&j.ua.windows),triggerDialog:j.ua.gecko&&l.FormData||j.ua.webkit}},init:function(a,b){function e(b){var c,d,e=[],f,k={};for(d=0;d<b.length;d++)c=b[d],k[c.name]||(k[c.name]=true,f=j.guid(),m[f]=c,e.push(new j.File(f,c.fileName||c.name,c.fileSize||c.size)));\n+e.length&&a.trigger("FilesAdded",e)}var d;d=this.getFeatures();d.html5?(a.bind("Init",function(b){var c,d,f=[],l,k,m=b.settings.filters,p,n;l=h.body;c=h.createElement("div");c.id=b.id+"_html5_container";j.extend(c.style,{position:"absolute",background:a.settings.shim_bgcolor||"transparent",width:"100px",height:"100px",overflow:"hidden",zIndex:99999,opacity:a.settings.shim_bgcolor?"":0});c.className="plupload html5";if(a.settings.container&&(l=h.getElementById(a.settings.container),j.getStyle(l,"position")===\n+"static"))l.style.position="relative";l.appendChild(c);l=0;a:for(;l<m.length;l++){p=m[l].extensions.split(/,/);for(k=0;k<p.length;k++){if(p[k]==="*"){f=[];break a}(n=j.mimeTypes[p[k]])&&f.push(n)}}c.innerHTML=\'<input id="\'+a.id+\'_html5"  style="font-size:999px" type="file" accept="\'+f.join(",")+\'" \'+(a.settings.multi_selection&&a.features.multi_selection?\'multiple="multiple"\':"")+" />";c.scrollTop=100;f=h.getElementById(a.id+"_html5");b.features.triggerDialog?j.extend(f.style,{position:"absolute",\n+width:"100%",height:"100%"}):j.extend(f.style,{cssFloat:"right",styleFloat:"right"});f.onchange=function(){e(this.files);this.value=""};if(d=h.getElementById(b.settings.browse_button)){var o=b.settings.browse_button_hover,r=b.settings.browse_button_active;c=b.features.triggerDialog?d:c;o&&(j.addEvent(c,"mouseover",function(){j.addClass(d,o)},b.id),j.addEvent(c,"mouseout",function(){j.removeClass(d,o)},b.id));r&&(j.addEvent(c,"mousedown",function(){j.addClass(d,r)},b.id),j.addEvent(h.body,"mouseup",\n+function(){j.removeClass(d,r)},b.id));b.features.triggerDialog&&j.addEvent(d,"click",function(a){h.getElementById(b.id+"_html5").click();a.preventDefault()},b.id)}}),a.bind("PostInit",function(){var b=h.getElementById(a.settings.drop_element);b&&(f?j.addEvent(b,"dragenter",function(){var c,d;c=h.getElementById(a.id+"_drop");c||(c=h.createElement("input"),c.setAttribute("type","file"),c.setAttribute("id",a.id+"_drop"),c.setAttribute("multiple","multiple"),j.addEvent(c,"change",function(){e(this.files);\n+j.removeEvent(c,"change",a.id);c.parentNode.removeChild(c)},a.id),b.appendChild(c));j.getPos(b,h.getElementById(a.settings.container));d=j.getSize(b);j.getStyle(b,"position")==="static"&&j.extend(b.style,{position:"relative"});j.extend(c.style,{position:"absolute",display:"block",top:0,left:0,width:d.w+"px",height:d.h+"px",opacity:0})},a.id):(j.addEvent(b,"dragover",function(a){a.preventDefault()},a.id),j.addEvent(b,"drop",function(a){var b=a.dataTransfer;b&&b.files&&e(b.files);a.preventDefault()},\n+a.id)))}),a.bind("Refresh",function(b){var c,d,e;if(c=h.getElementById(a.settings.browse_button))d=j.getPos(c,h.getElementById(b.settings.container)),e=j.getSize(c),b=h.getElementById(a.id+"_html5_container"),j.extend(b.style,{top:d.y+"px",left:d.x+"px",width:e.w+"px",height:e.h+"px"}),a.features.triggerDialog&&(j.getStyle(c,"position")==="static"&&j.extend(c.style,{position:"relative"}),d=parseInt(j.getStyle(c,"z-index"),10),isNaN(d)&&(d=0),j.extend(c.style,{zIndex:d}),j.extend(b.style,{zIndex:d-\n+1}))}),a.bind("UploadFile",function(a,b){function e(a,b,c){var d;if(File.prototype.slice)try{return a.slice(),a.slice(b,c)}catch(g){return a.slice(b,c-b)}else return(d=File.prototype.webkitSlice||File.prototype.mozSlice)?d.call(a,b,c):null}function f(k){function m(){function f(e){var k=0,h=new XMLHttpRequest,o=h.upload,q="----pluploadboundary"+j.guid(),y,w="";if(o)o.onprogress=function(d){b.loaded=Math.min(b.size,p+d.loaded-k);a.trigger("UploadProgress",b)};h.onreadystatechange=function(){var d,f;\n+if(h.readyState==4){try{d=h.status}catch(k){d=0}if(d>=400)a.trigger("Error",{code:j.HTTP_ERROR,message:j.translate("HTTP Error."),file:b,status:d});else{if(s){f={chunk:n,chunks:s,response:h.responseText,status:d};a.trigger("ChunkUploaded",b,f);p+=v;if(f.cancelled){b.status=j.FAILED;return}b.loaded=Math.min(b.size,(n+1)*t)}else b.loaded=b.size;a.trigger("UploadProgress",b);e=r=y=w=null;!s||++n>=s?(b.status=j.DONE,a.trigger("FileUploaded",b,{response:h.responseText,status:d})):m()}h=null}};if(a.settings.multipart&&\n+d.multipart){u.name=b.target_name||b.name;h.open("post",A,true);j.each(a.settings.headers,function(a,b){h.setRequestHeader(b,a)});if(typeof e!=="string"&&l.FormData){y=new FormData;j.each(j.extend(u,a.settings.multipart_params),function(a,b){y.append(b,a)});y.append(a.settings.file_data_name,e);h.send(y);return}if(typeof e==="string"){h.setRequestHeader("Content-Type","multipart/form-data; boundary="+q);j.each(j.extend(u,a.settings.multipart_params),function(a,b){w+="--"+q+\'\\r\\nContent-Disposition: form-data; name="\'+\n+b+\'"\\r\\n\\r\\n\';w+=unescape(encodeURIComponent(a))+"\\r\\n"});x=j.mimeTypes[b.name.replace(/^.+\\.([^.]+)/,"$1").toLowerCase()]||"application/octet-stream";w+="--"+q+\'\\r\\nContent-Disposition: form-data; name="\'+a.settings.file_data_name+\'"; filename="\'+unescape(encodeURIComponent(b.name))+\'"\\r\\nContent-Type: \'+x+"\\r\\n\\r\\n"+e+"\\r\\n--"+q+"--\\r\\n";k=w.length-e.length;e=w;if(h.sendAsBinary)h.sendAsBinary(e);else if(d.canSendBinary){for(var o=new Uint8Array(e.length),z=0;z<e.length;z++)o[z]=e.charCodeAt(z)&\n+255;h.send(o.buffer)}return}}A=j.buildUrl(a.settings.url,j.extend(u,a.settings.multipart_params));h.open("post",A,true);h.setRequestHeader("Content-Type","application/octet-stream");j.each(a.settings.headers,function(a,b){h.setRequestHeader(b,a)});h.send(e)}var r,s,u,t,v,x,A=a.settings.url;if(!(b.status==j.DONE||b.status==j.FAILED||a.state==j.STOPPED))u={name:b.target_name||b.name},h.chunk_size&&b.size>h.chunk_size&&(d.chunks||typeof k=="string")?(t=h.chunk_size,s=Math.ceil(b.size/t),v=Math.min(t,\n+b.size-n*t),r=typeof k=="string"?k.substring(n*t,n*t+v):e(k,n*t,n*t+v),u.chunk=n,u.chunks=s):(v=b.size,r=k),typeof r!=="string"&&o&&d.cantSendBlobInFormData&&d.chunks&&a.settings.chunk_size?(o.onload=function(){f(o.result)},o.readAsBinaryString(r)):f(r)}var n=0,p=0,o="FileReader"in l?new FileReader:null;m()}var h=a.settings,k;k=m[b.id];d.jpgresize&&a.settings.resize&&/\\.(png|jpg|jpeg)$/i.test(b.name)?v.call(a,b,a.settings.resize,/\\.png$/i.test(b.name)?"image/png":"image/jpeg",function(a){a.success?\n+(b.size=a.data.length,f(a.data)):f(k)}):!d.chunks&&d.jpgresize?t(k,f):f(k)}),a.bind("Destroy",function(a){var b,d,e=h.body,f={inputContainer:a.id+"_html5_container",inputFile:a.id+"_html5",browseButton:a.settings.browse_button,dropElm:a.settings.drop_element};for(b in f)(d=h.getElementById(f[b]))&&j.removeAllEvents(d,a.id);j.removeAllEvents(h.body,a.id);a.settings.container&&(e=h.getElementById(a.settings.container));e.removeChild(h.getElementById(f.inputContainer))}),b({success:true})):b({success:false})}})})(window,\n+document,plupload);\n+(function(l){function h(h){return plupload.translate(h)||h}function j(j,s){s.contents().each(function(j,h){h=l(h);h.is(".plupload")||h.remove()});s.prepend(\'<div class="plupload_wrapper plupload_scroll"><div id="\'+j+\'_container" class="plupload_container"><div class="plupload"><div class="plupload_header"><div class="plupload_header_content"><div class="plupload_header_title">\'+h("Select photos")+\'</div><div class="plupload_header_text">\'+h("Add photos and click the start button.")+\'</div></div></div><div class="plupload_content"><div class="plupload_filelist_footer"> <div class="plupload_file_name"> <div class="plupload_buttons"><a href="#" class="plupload_button plupload_add">\'+\n+h("Add photos")+\'</a><a href="#" class="plupload_button plupload_start">\'+h("Start upload")+\'</a></div> <span class="plupload_upload_status"></span> </div> <div class="plupload_file_action"></div> <div class="plupload_file_status"><span class="plupload_total_status">0%</span></div> <div class="plupload_file_size"><span class="plupload_total_file_size">0 b</span></div> <div class="plupload_progress"> <div class="plupload_progress_container"> <div class="plupload_progress_bar"></div> </div> </div> <div class="plupload_clearer">&nbsp;</div> </div><div class="plupload_filelist_header"><div class="plupload_file_name">\'+\n+h("Filename")+\'</div><div class="plupload_file_action">&nbsp;</div><div class="plupload_file_status"><span>\'+h("Status")+\'</span></div><div class="plupload_file_size">\'+h("Size")+\'</div><div class="plupload_clearer">&nbsp;</div></div><ul id="\'+j+\'_filelist" class="plupload_filelist"></ul></div></div></div><input type="hidden" id="\'+j+\'_count" name="\'+j+\'_count" value="0" /></div>\')}var s={};l.fn.pluploadQueue=function(r){return r?(this.each(function(){function t(f){var a;f.status==plupload.DONE&&\n+(a="plupload_done");f.status==plupload.FAILED&&(a="plupload_failed");f.status==plupload.QUEUED&&(a="plupload_delete");f.status==plupload.UPLOADING&&(a="plupload_uploading");a=l("#"+f.id).attr("class",a).find("a").css("display","block");f.hint&&a.attr("title",f.hint)}function v(){l("span.plupload_total_status",o).html(p.total.percent+"%");l("div.plupload_progress_bar",o).css("width",p.total.percent+"%");l("span.plupload_upload_status",o).text(h("Uploaded %d/%d photos").replace(/%d\\/%d/,p.total.uploaded+\n+"/"+p.files.length))}function u(){var f=l("ul.plupload_filelist",o).html(""),a=0,b;l.each(p.files,function(e,d){b="";d.status==plupload.DONE&&(d.target_name&&(b+=\'<input type="hidden" name="\'+m+"_"+a+\'_tmpname" value="\'+plupload.xmlEncode(d.target_name)+\'" />\'),b+=\'<input type="hidden" name="\'+m+"_"+a+\'_name" value="\'+plupload.xmlEncode(d.name)+\'" />\',b+=\'<input type="hidden" name="\'+m+"_"+a+\'_status" value="\'+(d.status==plupload.DONE?"done":"failed")+\'" />\',a++,l("#"+m+"_count").val(a));f.append(\'<li id="\'+\n+d.id+\'"><div class="plupload_file_name"><span>\'+d.name+\'</span></div><div class="plupload_file_action"><a href="#"></a></div><div class="plupload_file_status">\'+d.percent+\'%</div><div class="plupload_file_size">\'+plupload.formatSize(d.size)+\'</div><div class="plupload_clearer">&nbsp;</div>\'+b+"</li>");t(d);l("#"+d.id+".plupload_delete a").click(function(a){l("#"+d.id).remove();p.removeFile(d);a.preventDefault()})});l("span.plupload_total_file_size",o).html(plupload.formatSize(p.total.size));p.total.queued===\n+0?l("span.plupload_add_text",o).text(h("Add photos.")):l("span.plupload_add_text",o).text(p.total.queued+" photos queued.");l("a.plupload_start",o).toggleClass("plupload_disabled",p.files.length==p.total.uploaded+p.total.failed);f[0].scrollTop=f[0].scrollHeight;v();!p.files.length&&p.features.dragdrop&&p.settings.dragdrop&&l("#"+m+"_filelist").append(\'<li class="plupload_droptext">\'+h("Drag and drop photos here.")+"</li>")}var p,o,m;o=l(this);m=o.attr("id");m||(m=plupload.guid(),o.attr("id",m));p=\n+new plupload.Uploader(l.extend({dragdrop:true,container:m},r));s[m]=p;p.bind("UploadFile",function(f,a){l("#"+a.id).addClass("plupload_current_file")});p.bind("Init",function(f,a){j(m,o);!r.unique_names&&r.rename&&l("#"+m+"_filelist div.plupload_file_name span",o).live("click",function(a){var e=l(a.target),d,g,c="";d=f.getFile(e.parents("li")[0].id);g=d.name;if(a=/^(.+)(\\.[^.]+)$/.exec(g))g=a[1],c=a[2];e.hide().after(\'<input type="text" />\');e.next().val(g).focus().blur(function(){e.show().next().remove()}).keydown(function(a){var b=\n+l(this);if(a.keyCode==13)a.preventDefault(),d.name=b.val()+c,e.text(d.name),b.blur()})});l("a.plupload_add",o).attr("id",m+"_browse");f.settings.browse_button=m+"_browse";if(f.features.dragdrop&&f.settings.dragdrop)f.settings.drop_element=m+"_filelist",l("#"+m+"_filelist").append(\'<li class="plupload_droptext">\'+h("Drag and drop photos here.")+"</li>");l("#"+m+"_container").attr("title","Using runtime: "+a.runtime);l("a.plupload_start",o).click(function(a){l(this).hasClass("plupload_disabled")||p.start();\n+a.preventDefault()});l("a.plupload_stop",o).click(function(a){a.preventDefault();p.stop()});l("a.plupload_start",o).addClass("plupload_disabled")});p.init();p.bind("Error",function(f,a){var b=a.file,e;if(b)e=a.message,a.details&&(e+=" ("+a.details+")"),a.code==plupload.FILE_SIZE_ERROR&&alert(h("Error: File too large: ")+b.name),a.code==plupload.FILE_EXTENSION_ERROR&&alert(h("Error: Invalid file extension: ")+b.name),b.hint=e,l("#"+b.id).attr("class","plupload_failed").find("a").css("display","block").attr("title",\n+e)});p.bind("StateChanged",function(){p.state===plupload.STARTED?(l("li.plupload_delete a,div.plupload_buttons",o).hide(),l("span.plupload_upload_status,div.plupload_progress,a.plupload_stop",o).css("display","block"),l("span.plupload_upload_status",o).text("Uploaded "+p.total.uploaded+"/"+p.files.length+" photos"),r.multiple_queues&&l("span.plupload_total_status,span.plupload_total_file_size",o).show()):(u(),l("a.plupload_stop,div.plupload_progress",o).hide(),l("a.plupload_delete",o).css("display",\n+"block"))});p.bind("QueueChanged",u);p.bind("FileUploaded",function(f,a){t(a)});p.bind("UploadProgress",function(f,a){l("#"+a.id+" div.plupload_file_status",o).html(a.percent+"%");t(a);v();r.multiple_queues&&p.total.uploaded+p.total.failed==p.files.length&&(l(".plupload_buttons,.plupload_upload_status",o).css("display","inline"),l(".plupload_start",o).addClass("plupload_disabled"),l("span.plupload_total_status,span.plupload_total_file_size",o).hide())});r.setup&&r.setup(p)}),this):s[l(this[0]).attr("id")]}})(jQuery);\n+OPU=function(){return{init:function(){var l=$("#uploader");l.length!=0&&(l.pluploadQueue({runtimes:"html5",url:"/photo/upload.json",max_file_size:"20mb",unique_names:true,filters:[{title:"Photos",extensions:"jpg,jpeg,gif,png"}],flash_swf_url:"plupload.flash.swf",multipart_params:{crumb:$("form.upload input.crumb").val()},preinit:{BeforeUpload:function(){var h=$("#uploader").pluploadQueue();$(".upload-progress .total").html(h.files.length);$(".upload-progress .completed").html(h.total.uploaded+1);\n+$(".upload-progress").slideDown("fast")},UploadComplete:function(){$(".upload-progress").fadeOut("fast",function(){$(".upload-complete").fadeIn("fast")})},UploadFile:function(){var h=$("#uploader").pluploadQueue(),j,l,r;j=$("form.upload select[name=\'license\'] :selected").val();j.length==0&&(j=$("form.upload input[name=\'custom\']").val());r=$("form.upload input[name=\'tags\']").val();l=$("form.upload input[name=\'permission\']:checked").val();h.settings.multipart_params.license=j;h.settings.multipart_params.tags=\n+r;h.settings.multipart_params.permission=l}}}),$("form.upload").submit(function(){var h=$("#uploader").pluploadQueue({});h.files.length>0?(h.bind("StateChanged",function(){h.files.length===h.total.uploaded+h.total.failed&&$("form.upload")[0].submit()}),h.start()):alert("Please select at least one photo to upload.");return false}),l=$("#uploader .insufficient"),l.length==1&&l.show())}}}();\ndiff --git a/src/html/assets/themes/beisel/javascripts/plupload.html5.js b/src/html/assets/javascripts/plupload.html5.js\nsimilarity index 100%\nrename from src/html/assets/themes/beisel/javascripts/plupload.html5.js\nrename to src/html/assets/javascripts/plupload.html5.js\ndiff --git a/src/html/assets/themes/beisel/javascripts/plupload.js b/src/html/assets/javascripts/plupload.js\nsimilarity index 100%\nrename from src/html/assets/themes/beisel/javascripts/plupload.js\nrename to src/html/assets/javascripts/plupload.js\ndiff --git a/src/html/assets/themes/beisel/javascripts/plupload-full-min.js b/src/html/assets/openphoto-upload.js\nsimilarity index 91%\nrename from src/html/assets/themes/beisel/javascripts/plupload-full-min.js\nrename to src/html/assets/openphoto-upload.js\nindex df88ae9c..09cefe19 100644\n--- a/src/html/assets/themes/beisel/javascripts/plupload-full-min.js\n+++ b/src/html/assets/openphoto-upload.js\n@@ -62,3 +62,84 @@ k(this);if(a.keyCode==13)a.preventDefault(),d.name=b.val()+c,e.text(d.name),b.bl\n a.preventDefault()});k("a.plupload_stop",o).click(function(a){a.preventDefault();p.stop()});k("a.plupload_start",o).addClass("plupload_disabled")});p.init();p.bind("Error",function(f,a){var b=a.file,e;if(b)e=a.message,a.details&&(e+=" ("+a.details+")"),a.code==plupload.FILE_SIZE_ERROR&&alert(l("Error: File too large: ")+b.name),a.code==plupload.FILE_EXTENSION_ERROR&&alert(l("Error: Invalid file extension: ")+b.name),b.hint=e,k("#"+b.id).attr("class","plupload_failed").find("a").css("display","block").attr("title",\n e)});p.bind("StateChanged",function(){p.state===plupload.STARTED?(k("li.plupload_delete a,div.plupload_buttons",o).hide(),k("span.plupload_upload_status,div.plupload_progress,a.plupload_stop",o).css("display","block"),k("span.plupload_upload_status",o).text("Uploaded "+p.total.uploaded+"/"+p.files.length+" photos"),s.multiple_queues&&k("span.plupload_total_status,span.plupload_total_file_size",o).show()):(u(),k("a.plupload_stop,div.plupload_progress",o).hide(),k("a.plupload_delete",o).css("display",\n "block"))});p.bind("QueueChanged",u);p.bind("FileUploaded",function(f,a){t(a)});p.bind("UploadProgress",function(f,a){k("#"+a.id+" div.plupload_file_status",o).html(a.percent+"%");t(a);v();s.multiple_queues&&p.total.uploaded+p.total.failed==p.files.length&&(k(".plupload_buttons,.plupload_upload_status",o).css("display","inline"),k(".plupload_start",o).addClass("plupload_disabled"),k("span.plupload_total_status,span.plupload_total_file_size",o).hide())});s.setup&&s.setup(p)}),this):r[k(this[0]).attr("id")]}})(jQuery);\n+/**\n+* Utility methods for OpenPhoto - this will include the entire framework\n+* And ability to swap frameworks (YUI or jQuery) in and out.\n+*/\n+OPU = (function() {\n+  return {\n+      init: function() {\n+        var uploaderEl = $("#uploader");\n+        if(uploaderEl.length == 0)\n+          return;\n+\n+        uploaderEl.pluploadQueue({\n+            // General settings\n+            runtimes : \'html5\',\n+            url : \'/photo/upload.json\',\n+            max_file_size : \'20mb\',\n+            //chunk_size : \'1mb\',\n+            unique_names : true,\n+     \n+            // Specify what files to browse for\n+            filters : [\n+                {title : "Photos", extensions : "jpg,jpeg,gif,png"}\n+            ],\n+     \n+            // Flash settings\n+            flash_swf_url : \'plupload.flash.swf\',\n+            multipart_params:{\n+              crumb: $("form.upload input.crumb").val()\n+            },\n+            preinit: {\n+              BeforeUpload: function() {\n+                var uploader = $("#uploader").pluploadQueue();\n+                $(".upload-progress .total").html(uploader.files.length);\n+                $(".upload-progress .completed").html(uploader.total.uploaded+1);\n+                $(".upload-progress").slideDown(\'fast\');\n+              },\n+              UploadComplete: function() {\n+                $(".upload-progress").fadeOut(\'fast\', function() { $(".upload-complete").fadeIn(\'fast\'); });\n+              },\n+              UploadFile: function() {\n+                var uploader = $("#uploader").pluploadQueue(), license, permission, tags;\n+                license = $("form.upload select[name=\'license\'] :selected").val();\n+                if(license.length == 0)\n+                  license = $("form.upload input[name=\'custom\']").val();\n+                tags = $("form.upload input[name=\'tags\']").val();\n+                permission = $("form.upload input[name=\'permission\']:checked").val();\n+                \n+                uploader.settings.multipart_params.license = license;\n+                uploader.settings.multipart_params.tags = tags;\n+                uploader.settings.multipart_params.permission = permission;\n+              }\n+            }\n+        });\n+     \n+        // Client side form validation\n+        var uploadForm = $("form.upload");\n+        uploadForm.submit(function(e) {\n+          var uploader = $(\'#uploader\').pluploadQueue({});\n+          // Files in queue upload them first\n+          if (uploader.files.length > 0) {\n+            // When all files are uploaded submit form\n+            uploader.bind(\'StateChanged\', function() {\n+              if (uploader.files.length === (uploader.total.uploaded + uploader.total.failed)) {\n+                $("form.upload")[0].submit();\n+              }\n+            }); \n+            uploader.start();\n+          } else {\n+            // TODO something that doesn\'t suck\n+            alert(\'Please select at least one photo to upload.\');\n+          }\n+   \n+          return false;\n+        });\n+\n+        var insufficient = $("#uploader .insufficient");\n+        if(insufficient.length == 1)\n+          insufficient.show();\n+      }\n+    };\n+}());\ndiff --git a/src/html/assets/stylesheets/upload.css b/src/html/assets/stylesheets/upload.css\nnew file mode 100644\nindex 00000000..c05711fa\n--- /dev/null\n+++ b/src/html/assets/stylesheets/upload.css\n@@ -0,0 +1,213 @@\n+/*  Upload ************************************************ */\n+body.upload .options {\n+  margin:10px;\n+}\n+body.upload .options div {\n+  margin:6px 0;\n+}\n+body.upload .options label {\n+  float:left;\n+  width:100px;\n+}\n+body.upload .poweredby {\n+  font-style:italic;\n+  font-size:12px;\n+  display:block;\n+  text-align:right;\n+}\n+body.upload .message {\n+  font-size:30px;\n+  margin:10px 0 30px;\n+  text-align:center;\n+  display:none;\n+}\n+body.upload .message img {\n+  position:relative;\n+  left:-5px;\n+  top:5px;\n+}\n+/* \n+   Plupload\n+------------------------------------------------------------------- */\n+.plupload{\n+  -moz-border-radius: 10px;\n+  -khtml-border-radius: 10px;\n+  -webkit-border-radius: 10px;\n+  border-radius: 10px;\n+}\n+.plupload_button {\n+\tdisplay: -moz-inline-box; /* FF < 3*/\n+\tdisplay: inline-block;\n+\tfont: normal 12px sans-serif;\n+\ttext-decoration: none;\n+  color: #42454a;\n+  border: 1px solid #bababa;\n+  padding: 2px 8px 3px 20px;\n+\tmargin-right: 4px;\n+  background: #f3f3f3 url(\'../images/buttons.png\') no-repeat 0 center;\n+  outline: 0;\n+\n+  /* Optional rounded corners for browsers that support it */\n+  -moz-border-radius: 3px;\n+  -khtml-border-radius: 3px;\n+  -webkit-border-radius: 3px;\n+  border-radius: 3px;\n+}\n+\n+.plupload_button:hover {\n+\tcolor: #000;\n+\ttext-decoration: none;\n+}\n+\n+.plupload_disabled, a.plupload_disabled:hover {\n+    color: #737373;\n+    border-color: #c5c5c5;\n+    background: #ededed url(\'../images/buttons-disabled.png\') no-repeat 0 center;\n+\tcursor: default;\n+}\n+\n+.plupload_add {\n+\tbackground-position: -181px center;\n+}\n+\n+.plupload_wrapper {\n+\tfont: normal 11px Verdana,sans-serif;\n+\twidth: 100%;\n+}\n+\n+.plupload_container {\n+\tpadding: 8px;\n+\tbackground: url(\'../images/transp50.png\');\n+\t/*-moz-border-radius: 5px;*/\n+}\n+\n+.plupload_container input {\n+\tborder: 1px solid #DDD;\n+\tfont: normal 11px Verdana,sans-serif;\n+\twidth: 98%;\n+}\n+\n+.plupload_header {background: #2A2C2E url(\'../images/backgrounds.gif\') repeat-x;}\n+.plupload_header_content {\n+\tbackground: url(\'../images/backgrounds.gif\') no-repeat 0 -317px;\n+\tmin-height: 56px;\n+\tpadding-left: 60px;\n+\tcolor: #FFF;\n+}\n+.plupload_header_title {\n+\tfont: normal 18px sans-serif;\n+\tpadding: 6px 0 3px;\n+}\n+.plupload_header_text {\n+\tfont: normal 12px sans-serif;\n+}\n+\n+.plupload_filelist {\n+\tmargin: 0;\n+\tpadding: 0;\n+\tlist-style: none;\n+}\n+\n+.plupload_scroll .plupload_filelist {\n+\t/*height: 185px;*/\n+\tbackground: #F5F5F5;\n+  clear:both;\n+\t/*overflow-y: scroll;*/\n+}\n+\n+.plupload_filelist li {\n+\tpadding: 10px 8px;\n+\tbackground: #F5F5F5 url(\'../images/backgrounds.gif\') repeat-x 0 -156px;\n+\tborder-bottom: 1px solid #DDD;\n+}\n+\n+.plupload_filelist_header, .plupload_filelist_footer {\n+\tbackground: #DFDFDF;\n+\tpadding: 8px 8px;\n+\tcolor: #42454A;\n+}\n+.plupload_filelist_header {\t\n+\tborder-top: 1px solid #EEE;\n+\tborder-bottom: 1px solid #CDCDCD;\n+}\n+\n+.plupload_filelist_footer {border-top: 1px solid #FFF; height: 22px; line-height: 20px; vertical-align: middle;}\n+.plupload_file_name {float: left; overflow: hidden}\n+.plupload_file_status {color: #777;}\n+.plupload_file_status span {color: #42454A;}\n+.plupload_file_size, .plupload_file_status, .plupload_progress {\n+\tfloat: right;\n+\twidth: 80px;\n+}\n+.plupload_file_size, .plupload_file_status, .plupload_file_action {text-align: right;}\n+\n+.plupload_filelist .plupload_file_name {width: 205px}\n+\n+.plupload_file_action {\n+\tfloat: right;\n+\twidth: 16px;\n+\theight: 16px;\n+\tmargin-left: 15px;\n+}\n+\n+.plupload_file_action * {\n+\tdisplay: none;\n+\twidth: 16px;\n+\theight: 16px;\n+}\n+\n+li.plupload_uploading {background: #ECF3DC url(\'../images/backgrounds.gif\') repeat-x 0 -238px;}\n+li.plupload_done {color:#AAA}\n+\n+li.plupload_delete a {\n+\tbackground: url(\'../images/delete.gif\');\n+}\n+\n+li.plupload_failed a {\n+\tbackground: url(\'../images/error.gif\');\n+\tcursor: default;\n+}\n+\n+li.plupload_done a {\n+\tbackground: url(\'../images/done.gif\');\n+\tcursor: default;\n+}\n+\n+.plupload_progress, .plupload_upload_status {\n+\tdisplay: none;\n+}\n+\n+.plupload_progress_container {\n+\tmargin-top: 3px;\n+\tborder: 1px solid #CCC;\n+\tbackground: #FFF;\n+\tpadding: 1px;\n+}\n+.plupload_progress_bar {\n+\twidth: 0px;\n+\theight: 7px;\n+\tbackground: #CDEB8B;\n+}\n+\n+.plupload_scroll .plupload_filelist_header .plupload_file_action, .plupload_scroll .plupload_filelist_footer .plupload_file_action {\n+\tmargin-right: 17px;\n+}\n+\n+/* Floats */\n+\n+.plupload_clear,.plupload_clearer {clear: both;}\n+.plupload_clearer, .plupload_progress_bar {\n+\tdisplay: block;\n+\tfont-size: 0;\n+\tline-height: 0;\t\n+}\n+\n+li.plupload_droptext {\n+\tbackground: transparent;\n+\ttext-align: center;\n+\tvertical-align: middle;\n+\tborder: 0;\n+  font-size:30px;\n+\tline-height: 165px;\n+}\n+\ndiff --git a/src/html/assets/themes/beisel/images/browserid.png b/src/html/assets/themes/beisel/images/browserid.png\nnew file mode 100644\nindex 00000000..919a5c7a\nBinary files /dev/null and b/src/html/assets/themes/beisel/images/browserid.png differ\ndiff --git a/src/html/assets/themes/beisel/images/facebook-icon.gif b/src/html/assets/themes/beisel/images/facebook-icon.gif\nnew file mode 100644\nindex 00000000..c0f82566\nBinary files /dev/null and b/src/html/assets/themes/beisel/images/facebook-icon.gif differ\ndiff --git a/src/html/assets/themes/beisel/images/favicon.ico b/src/html/assets/themes/beisel/images/favicon.ico\ndeleted file mode 100644\nindex c6c716b2..00000000\nBinary files a/src/html/assets/themes/beisel/images/favicon.ico and /dev/null differ\ndiff --git a/src/html/assets/themes/beisel/images/favicon.png b/src/html/assets/themes/beisel/images/favicon.png\nnew file mode 100644\nindex 00000000..9537b259\nBinary files /dev/null and b/src/html/assets/themes/beisel/images/favicon.png differ\ndiff --git a/src/html/assets/themes/beisel/images/openphoto.png b/src/html/assets/themes/beisel/images/openphoto.png\nindex 71ee274e..0449b9c8 100644\nBinary files a/src/html/assets/themes/beisel/images/openphoto.png and b/src/html/assets/themes/beisel/images/openphoto.png differ\ndiff --git a/src/html/assets/themes/beisel/javascripts/minification.txt b/src/html/assets/themes/beisel/javascripts/minification.txt\nindex bfc74a3a..cb25a7f8 100644\n--- a/src/html/assets/themes/beisel/javascripts/minification.txt\n+++ b/src/html/assets/themes/beisel/javascripts/minification.txt\n@@ -1,3 +1 @@\n minify jquery.scrollTo-1.4.2-min.js jquery.flexslider-min.js openphoto-theme.js openphoto-theme-full-min.js --force\n-\n-minify plupload.js plupload.html5.js jquery.plupload.queue.js plupload-full-min.js --force\ndiff --git a/src/html/assets/themes/beisel/javascripts/openphoto-theme-full-min.js b/src/html/assets/themes/beisel/javascripts/openphoto-theme-full-min.js\nindex 5c341dfb..44230aca 100644\n--- a/src/html/assets/themes/beisel/javascripts/openphoto-theme-full-min.js\n+++ b/src/html/assets/themes/beisel/javascripts/openphoto-theme-full-min.js\n@@ -19,21 +19,19 @@ slideshow:true,slideshowSpeed:7E3,animationDuration:600,directionNav:true,contro\n 1?c(this).find(".slides li").fadeIn(400):c(this).data("flexslider")!=true&&new c.flexslider(c(this),e)})}})(jQuery);\n var opTheme=function(){var c=function(b){console!==void 0&&console.log!==void 0&&console.log(b)},e=void 0;return{callback:{keyboard:function(){c("keyboard!!!!")},actionDelete:function(b){b.preventDefault();var b=$(b.target),a=b.attr("href")+".json",c=b.attr("data-id");OP.Util.makeRequest(a,b.parent().serializeArray(),function(a){a.code===200?$(".action-container-"+c).hide("medium",function(){$(this).remove()}):opTheme.message.error("Could not delete the photo.")});return false},commentJump:function(b){b.preventDefault();\n $.scrollTo($("div.comment-form"),200);return false},credentailDelete:function(b){b.preventDefault();var a=$(b.target),b=a.attr("href")+".json";OP.Util.makeRequest(b,{},function(b){b.code===200?(a.parent().remove(),opTheme.message.confirm("Credential successfully deleted.")):opTheme.message.error("Could not delete credential.")});return false},groupCheckbox:function(b){b=$(b.target);b.hasClass("none")&&b.is(":checked")?$("input.group-checkbox:not(.none)").removeAttr("checked"):b.is(":checked")&&$("input.group-checkbox.none").removeAttr("checked")},\n-groupPost:function(b){b.preventDefault();var b=$(b.target).parent(),a=b.attr("action")+".json",c=a.search("create")>-1;OP.Util.makeRequest(a,b.serializeArray(),function(a){a.code===200?c?location.href=location.href:opTheme.message.confirm("Group updated successfully."):opTheme.message.error("Could not update group.")});return false},login:function(){navigator.id.getVerifiedEmail(function(b){b?opTheme.user.loginSuccess(b):opTheme.user.loginFailure(b)})},photoDelete:function(b){b.preventDefault();var a=\n-$(b.target),b=a.parent().attr("action")+".json";OP.Util.makeRequest(b,a.parent().serializeArray(),function(b){b.code===200?(a.html("This photo has been deleted"),opTheme.message.confirm("This photo has been deleted.")):opTheme.message.error("Could not delete the photo.")});return false},photoEdit:function(b){b.preventDefault();b=$(b.target).attr("href")+".json";$("div.owner-edit").length==1?$.scrollTo($("div.owner-edit"),200):OP.Util.makeRequest(b,{},function(a){a.code===200?($("#main").append(a.result.markup),\n-$.scrollTo($("div.owner-edit"),200)):opTheme.message.error("Could not load the form to edit this photo.")},"json","get");return false},searchByTags:function(b){b.preventDefault();var a=$(b.target).parent(),b=$(a.find("input[name=tags]")[0]).val(),a=$(a).attr("action");location.href=b.length>0?a.replace("/list","")+"/tags-"+b+"/list":a;return false},settings:function(){$("ul#settingsbar").slideToggle("medium");$("li#nav-signin").toggleClass("active");return false},keyBrowseNext:function(){var b;if(b=\n-$(".image-pagination .next a").attr("href"))location.href=b},keyBrowsePrevious:function(){var b;if(b=$(".image-pagination .previous a").attr("href"))location.href=b},webhookDelete:function(b){b.preventDefault();var a=$(b.target),b=a.attr("href")+".json";OP.Util.makeRequest(b,{},function(b){b.code===200?(a.parent().remove(),opTheme.message.confirm("Credential successfully deleted.")):opTheme.message.error("Could not delete credential.")});return false}},formHandlers:{hasErrors:function(b,a){var c=\n-[];b.children("input, textarea").each(function(){var d=$(this);d.prev().removeClass("error");var e=d.attr(a);if(e!=void 0)for(var e=e.split(" "),h=0;h<e.length;h++){if(e[h]=="date"&&!opTheme.formHandlers.passesDate(d)){var g=d.prev().html()+" is not a valid date";c.push([d,g])}e[h]=="email"&&!opTheme.formHandlers.passesEmail(d)&&(g=d.prev().html()+" is not a valid email address",c.push([d,g]));e[h]=="ifexists"&&d.val()!=""&&d.val()!=void 0&&$.merge(c,opTheme.formHandlers.hasErrors(b,"data-ifexists"));\n-e[h]=="integer"&&!opTheme.formHandlers.passesInteger(d)&&(g=d.prev().html()+" is not a number",c.push([d,g]));e[h]=="match"&&(g=d.attr("data-match"),opTheme.formHandlers.passesMatch(d,g)||(g=d.prev().html()+" does not match "+$("#"+g).prev().html(),c.push([d,g])));e[h]=="required"&&!opTheme.formHandlers.passesRequired(d)&&(g=d.prev().html()+" is required",c.push([d,g]));e[h]=="alphanumeric"&&!opTheme.formHandlers.passesAlphaNumeric(d)&&(g=d.prev().html()+" can only contain alpha-numeric characters",\n-c.push([d,g]))}});return c},init:function(){$(this).submit(opTheme.submitHandlers.siteForm);opTheme.formHandlers.showPlaceholders();$("input[data-placeholder]").live("focus",opTheme.formHandlers.placeholderFocus);$("input[data-placeholder]").live("blur",opTheme.formHandlers.placeholderBlur)},passesAlphaNumeric:function(b){return/^[a-zA-Z0-9]+$/.test(b.val())},passesDate:function(b){return/^\\d{1,2}\\/\\d{1,2}\\/\\d{4}$/.test(b.val())},passesEmail:function(b){return/^([\\w-\\.+]+@([\\w-]+\\.)+[\\w-]{2,4})?$/.test(b.val())},\n-passesInteger:function(b){return/^\\d+$/.test(b.val())},passesMatch:function(b,a){return b.val()==$("#"+a).val()},passesRequired:function(b){return b.is("textarea")||b.is("input")&&(b.attr("type")=="text"||b.attr("type")=="password")?b.val()!=""&&b.val()!=void 0:b.is("checkbox")?b.is(":checked"):true},placeholderBlur:function(){var b=$(this);b.val()==""&&(b.val(b.attr("data-placeholder")),b.addClass("placeholder"))},placeholderFocus:function(){var b=$(this);b.val()==b.attr("data-placeholder")&&(b.val(""),\n-b.removeClass("placeholder"))},removePlaceholders:function(){$("input[data-placeholder]").each(function(){var b=$(this);b.val()==b.attr("data-placeholder")&&(b.val(""),b.removeClass("placeholder"))})},showPlaceholders:function(){$("input[data-placeholder]").each(function(){var b=$(this);b.val()==""&&(b.val(b.attr("data-placeholder")),b.addClass("placeholder"))})}},front:{init:function(b){b.length>0&&b.cycle({fx:"fade"}).find("img").click(function(a){location.href=$(a.target).attr("data-origin")})}},\n-init:{load:function(){$("section#slideshow").length>0&&$(window).load(function(){$(".flexslider").flexslider({animation:"slide",controlsContainer:".flex-container",controlNav:true,pausePlay:false,directionNav:true,nextText:"<span title=\'Next\'>Next</span>",prevText:"<span title=\'Previous\'>Previous</span>"})})},attach:function(){OP.Util.on("click:action-delete",opTheme.callback.actionDelete);OP.Util.on("click:action-jump",opTheme.callback.commentJump);OP.Util.on("click:credential-delete",opTheme.callback.credentailDelete);\n-OP.Util.on("click:group-checkbox",opTheme.callback.groupCheckbox);OP.Util.on("click:group-update",opTheme.callback.groupPost);OP.Util.on("click:login",opTheme.callback.login);OP.Util.on("click:photo-delete",opTheme.callback.photoDelete);OP.Util.on("click:photo-edit",opTheme.callback.photoEdit);OP.Util.on("click:nav-item",opTheme.callback.searchBarToggle);OP.Util.on("click:search",opTheme.callback.searchByTags);OP.Util.on("click:settings",opTheme.callback.settings);OP.Util.on("click:webhook-delete",\n-opTheme.callback.webhookDelete);OP.Util.on("keydown:browse-next",opTheme.callback.keyBrowseNext);OP.Util.on("keydown:browse-previous",opTheme.callback.keyBrowsePrevious);opTheme.front.init($("div.front-slideshow"));opTheme.upload.init();$("form.validate").each(opTheme.formHandlers.init)}},message:{close:function(){e!=void 0&&(clearTimeout(e),e=void 0,$("#message-box").animate({height:"toggle"},500,function(){$("#message-box").remove()}))},confirm:function(b){opTheme.message.show(b,"confirm")},error:function(b){opTheme.message.show(b,\n-"error")},show:function(b,a){var c=a=="error"?"confirm":"error";e!=void 0?(clearTimeout(e),e=void 0,$("#message-box").removeClass(c).addClass(a).html(\'<div><a class="message-close">close</a>\'+b+"</div>"),e=setTimeout(function(){$("#message-box").animate({height:"toggle"},500,function(){$("#message-box").remove();e=void 0})},7E3)):($("html").append(\'<section id="message-box" style="display:none;"><div><a class="message-close">close</a>\'+b+"</div></section>"),$("#message-box").removeClass(c).addClass(a).animate({height:"toggle"},\n-500,function(){e=setTimeout(function(){$("#message-box").animate({height:"toggle"},500,function(){$("#message-box").remove();e=void 0})},7E3)}));$("a.message-close").click(opTheme.message.close)}},submitHandlers:{siteForm:function(b){var a=$(this);b.preventDefault();opTheme.formHandlers.removePlaceholders();b=opTheme.formHandlers.hasErrors(a,"data-validation");opTheme.formHandlers.showPlaceholders();if(b.length==0)this.submit();else{for(var a="<ul>",c=0;c<b.length;c++)b[c][0].prev().addClass("error"),\n-a+="<li>"+b[c][1]+"</li>";a+="</ul>";$("html").animate({scrollTop:b[0][0].offset().top-30},500);b[0][0].focus();opTheme.message.error(a)}}},upload:{init:function(){var b=$("#uploader");b.length!=0&&(b.pluploadQueue({runtimes:"html5",url:"/photo/upload.json",max_file_size:"20mb",unique_names:true,filters:[{title:"Photos",extensions:"jpg,jpeg,gif,png"}],flash_swf_url:"plupload.flash.swf",multipart_params:{crumb:$("form.upload input.crumb").val()},preinit:{BeforeUpload:function(){var a=$("#uploader").pluploadQueue();\n-$(".upload-progress .total").html(a.files.length);$(".upload-progress .completed").html(a.total.uploaded+1);$(".upload-progress").slideDown("fast")},UploadComplete:function(){$(".upload-progress").fadeOut("fast",function(){$(".upload-complete").fadeIn("fast")})},UploadFile:function(){var a=$("#uploader").pluploadQueue(),b,c,e;b=$("form.upload select[name=\'license\'] :selected").val();b.length==0&&(b=$("form.upload input[name=\'custom\']").val());e=$("form.upload input[name=\'tags\']").val();c=$("form.upload input[name=\'permission\']:checked").val();\n-a.settings.multipart_params.license=b;a.settings.multipart_params.tags=e;a.settings.multipart_params.permission=c}}}),$("form.upload").submit(function(){var a=$("#uploader").pluploadQueue({});a.files.length>0?(a.bind("StateChanged",function(){a.files.length===a.total.uploaded+a.total.failed&&$("form.upload")[0].submit()}),a.start()):alert("Please select at least one photo to upload.");return false}),b=$("#uploader .insufficient"),b.length==1&&b.show())}},user:{loginFailure:function(){c("login failed")},\n-loginProcessed:function(b){b.code!=200?c("processing of login failed"):(c("login processing succeeded"),window.location.reload())},loginSuccess:function(b){OP.Util.makeRequest("/user/login.json",{assertion:b},opTheme.user.loginProcessed)}}}}();\n+groupPost:function(b){b.preventDefault();var b=$(b.target).parent(),a=b.attr("action")+".json",c=a.search("create")>-1;OP.Util.makeRequest(a,b.serializeArray(),function(a){a.code===200?c?location.href=location.href:opTheme.message.confirm("Group updated successfully."):opTheme.message.error("Could not update group.")});return false},login:function(b){b=$(b.target);b.hasClass("browserid")?navigator.id.getVerifiedEmail(function(a){a?opTheme.user.browserid.loginSuccess(a):opTheme.user.browserid.loginFailure(a)}):\n+b.hasClass("facebook")&&FB.login(function(a){a.authResponse?(console.log("User logged in, posting to openphoto host."),OP.Util.makeRequest("/user/facebook/login.json",opTheme.user.base.loginProcessed)):console.log("User cancelled login or did not fully authorize.")},{scope:"email"})},photoDelete:function(b){b.preventDefault();var a=$(b.target),b=a.parent().attr("action")+".json";OP.Util.makeRequest(b,a.parent().serializeArray(),function(b){b.code===200?(a.html("This photo has been deleted"),opTheme.message.confirm("This photo has been deleted.")):\n+opTheme.message.error("Could not delete the photo.")});return false},photoEdit:function(b){b.preventDefault();b=$(b.target).attr("href")+".json";$("div.owner-edit").length==1?$.scrollTo($("div.owner-edit"),200):OP.Util.makeRequest(b,{},function(a){a.code===200?($("#main").append(a.result.markup),$.scrollTo($("div.owner-edit"),200)):opTheme.message.error("Could not load the form to edit this photo.")},"json","get");return false},searchByTags:function(b){b.preventDefault();var a=$(b.target).parent(),\n+b=$(a.find("input[name=tags]")[0]).val(),a=$(a).attr("action");location.href=b.length>0?a.replace("/list","")+"/tags-"+b+"/list":a;return false},settings:function(){$("ul#settingsbar").slideToggle("medium");$("li#nav-signin").toggleClass("active");return false},keyBrowseNext:function(){var b;if(b=$(".image-pagination .next a").attr("href"))location.href=b},keyBrowsePrevious:function(){var b;if(b=$(".image-pagination .previous a").attr("href"))location.href=b},webhookDelete:function(b){b.preventDefault();\n+var a=$(b.target),b=a.attr("href")+".json";OP.Util.makeRequest(b,{},function(b){b.code===200?(a.parent().remove(),opTheme.message.confirm("Credential successfully deleted.")):opTheme.message.error("Could not delete credential.")});return false}},formHandlers:{hasErrors:function(b,a){var c=[];b.children("input, textarea").each(function(){var d=$(this);d.prev().removeClass("error");var e=d.attr(a);if(e!=void 0)for(var e=e.split(" "),h=0;h<e.length;h++){if(e[h]=="date"&&!opTheme.formHandlers.passesDate(d)){var g=\n+d.prev().html()+" is not a valid date";c.push([d,g])}e[h]=="email"&&!opTheme.formHandlers.passesEmail(d)&&(g=d.prev().html()+" is not a valid email address",c.push([d,g]));e[h]=="ifexists"&&d.val()!=""&&d.val()!=void 0&&$.merge(c,opTheme.formHandlers.hasErrors(b,"data-ifexists"));e[h]=="integer"&&!opTheme.formHandlers.passesInteger(d)&&(g=d.prev().html()+" is not a number",c.push([d,g]));e[h]=="match"&&(g=d.attr("data-match"),opTheme.formHandlers.passesMatch(d,g)||(g=d.prev().html()+" does not match "+\n+$("#"+g).prev().html(),c.push([d,g])));e[h]=="required"&&!opTheme.formHandlers.passesRequired(d)&&(g=d.prev().html()+" is required",c.push([d,g]));e[h]=="alphanumeric"&&!opTheme.formHandlers.passesAlphaNumeric(d)&&(g=d.prev().html()+" can only contain alpha-numeric characters",c.push([d,g]))}});return c},init:function(){$(this).submit(opTheme.submitHandlers.siteForm);opTheme.formHandlers.showPlaceholders();$("input[data-placeholder]").live("focus",opTheme.formHandlers.placeholderFocus);$("input[data-placeholder]").live("blur",\n+opTheme.formHandlers.placeholderBlur)},passesAlphaNumeric:function(b){return/^[a-zA-Z0-9]+$/.test(b.val())},passesDate:function(b){return/^\\d{1,2}\\/\\d{1,2}\\/\\d{4}$/.test(b.val())},passesEmail:function(b){return/^([\\w-\\.+]+@([\\w-]+\\.)+[\\w-]{2,4})?$/.test(b.val())},passesInteger:function(b){return/^\\d+$/.test(b.val())},passesMatch:function(b,a){return b.val()==$("#"+a).val()},passesRequired:function(b){return b.is("textarea")||b.is("input")&&(b.attr("type")=="text"||b.attr("type")=="password")?b.val()!=\n+""&&b.val()!=void 0:b.is("checkbox")?b.is(":checked"):true},placeholderBlur:function(){var b=$(this);b.val()==""&&(b.val(b.attr("data-placeholder")),b.addClass("placeholder"))},placeholderFocus:function(){var b=$(this);b.val()==b.attr("data-placeholder")&&(b.val(""),b.removeClass("placeholder"))},removePlaceholders:function(){$("input[data-placeholder]").each(function(){var b=$(this);b.val()==b.attr("data-placeholder")&&(b.val(""),b.removeClass("placeholder"))})},showPlaceholders:function(){$("input[data-placeholder]").each(function(){var b=\n+$(this);b.val()==""&&(b.val(b.attr("data-placeholder")),b.addClass("placeholder"))})}},front:{init:function(b){b.length>0&&b.cycle({fx:"fade"}).find("img").click(function(a){location.href=$(a.target).attr("data-origin")})}},init:{load:function(){$("section#slideshow").length>0&&$(window).load(function(){$(".flexslider").flexslider({animation:"slide",controlsContainer:".flex-container",controlNav:true,pausePlay:false,directionNav:true,nextText:"<span title=\'Next\'>Next</span>",prevText:"<span title=\'Previous\'>Previous</span>"})})},\n+attach:function(){OP.Util.on("click:action-delete",opTheme.callback.actionDelete);OP.Util.on("click:action-jump",opTheme.callback.commentJump);OP.Util.on("click:credential-delete",opTheme.callback.credentailDelete);OP.Util.on("click:group-checkbox",opTheme.callback.groupCheckbox);OP.Util.on("click:group-update",opTheme.callback.groupPost);OP.Util.on("click:login",opTheme.callback.login);OP.Util.on("click:photo-delete",opTheme.callback.photoDelete);OP.Util.on("click:photo-edit",opTheme.callback.photoEdit);\n+OP.Util.on("click:nav-item",opTheme.callback.searchBarToggle);OP.Util.on("click:search",opTheme.callback.searchByTags);OP.Util.on("click:settings",opTheme.callback.settings);OP.Util.on("click:webhook-delete",opTheme.callback.webhookDelete);OP.Util.on("keydown:browse-next",opTheme.callback.keyBrowseNext);OP.Util.on("keydown:browse-previous",opTheme.callback.keyBrowsePrevious);opTheme.front.init($("div.front-slideshow"));OPU.init();$("form.validate").each(opTheme.formHandlers.init)}},message:{close:function(){e!=\n+void 0&&(clearTimeout(e),e=void 0,$("#message-box").animate({height:"toggle"},500,function(){$("#message-box").remove()}))},confirm:function(b){opTheme.message.show(b,"confirm")},error:function(b){opTheme.message.show(b,"error")},show:function(b,a){var c=a=="error"?"confirm":"error";e!=void 0?(clearTimeout(e),e=void 0,$("#message-box").removeClass(c).addClass(a).html(\'<div><a class="message-close">close</a>\'+b+"</div>"),e=setTimeout(function(){$("#message-box").animate({height:"toggle"},500,function(){$("#message-box").remove();\n+e=void 0})},7E3)):($("html").append(\'<section id="message-box" style="display:none;"><div><a class="message-close">close</a>\'+b+"</div></section>"),$("#message-box").removeClass(c).addClass(a).animate({height:"toggle"},500,function(){e=setTimeout(function(){$("#message-box").animate({height:"toggle"},500,function(){$("#message-box").remove();e=void 0})},7E3)}));$("a.message-close").click(opTheme.message.close)}},submitHandlers:{siteForm:function(b){var a=$(this);b.preventDefault();opTheme.formHandlers.removePlaceholders();\n+b=opTheme.formHandlers.hasErrors(a,"data-validation");opTheme.formHandlers.showPlaceholders();if(b.length==0)this.submit();else{for(var a="<ul>",c=0;c<b.length;c++)b[c][0].prev().addClass("error"),a+="<li>"+b[c][1]+"</li>";a+="</ul>";$("html").animate({scrollTop:b[0][0].offset().top-30},500);b[0][0].focus();opTheme.message.error(a)}}},user:{base:{loginProcessed:function(b){b.code!=200?c("processing of login failed"):(c("login processing succeeded"),window.location.reload())}},browserid:{loginFailure:function(){c("login failed")},\n+loginSuccess:function(b){OP.Util.makeRequest("/user/browserid/login.json",{assertion:b},opTheme.user.base.loginProcessed)}}}}}();\ndiff --git a/src/html/assets/themes/beisel/javascripts/openphoto-theme.js b/src/html/assets/themes/beisel/javascripts/openphoto-theme.js\nindex ec2c1683..a003522d 100644\n--- a/src/html/assets/themes/beisel/javascripts/openphoto-theme.js\n+++ b/src/html/assets/themes/beisel/javascripts/openphoto-theme.js\n@@ -74,13 +74,25 @@ var opTheme = (function() {\n         return false;\n       },\n       login: function(ev) {\n-        navigator.id.getVerifiedEmail(function(assertion) {\n-            if (assertion) {\n-              opTheme.user.loginSuccess(assertion);\n+        var el = $(ev.target);\n+        if(el.hasClass(\'browserid\')) {\n+          navigator.id.getVerifiedEmail(function(assertion) {\n+              if (assertion) {\n+                opTheme.user.browserid.loginSuccess(assertion);\n+              } else {\n+                opTheme.user.browserid.loginFailure(assertion);\n+              }\n+          });\n+        } else if(el.hasClass(\'facebook\')) {\n+          FB.login(function(response) {\n+            if (response.authResponse) {\n+              console.log(\'User logged in, posting to openphoto host.\');\n+              OP.Util.makeRequest(\'/user/facebook/login.json\', opTheme.user.base.loginProcessed);\n             } else {\n-              opTheme.user.loginFailure(assertion);\n+              console.log(\'User cancelled login or did not fully authorize.\');\n             }\n-        });\n+          }, {scope: \'email\'});\n+        }\n       },\n       photoDelete: function(ev) {\n       \n@@ -355,7 +367,8 @@ var opTheme = (function() {\n         OP.Util.on(\'keydown:browse-previous\', opTheme.callback.keyBrowsePrevious);\n \n         opTheme.front.init($(\'div.front-slideshow\'));\n-        opTheme.upload.init();\n+        if(typeof OPU === \'object\')\n+          OPU.init();\n         // TODO standardize this somehow\n         $(\'form.validate\').each(opTheme.formHandlers.init);\n       }\n@@ -439,100 +452,29 @@ var opTheme = (function() {\n \t\t\t\t\topTheme.message.error(messageHtml);\n \t\t\t\t}\n \t\t\t}\n-\t\t},\n-    upload: {\n-      init: function() {\n-        var uploaderEl = $("#uploader");\n-        if(uploaderEl.length == 0)\n-          return;\n-\n-        uploaderEl.pluploadQueue({\n-            // General settings\n-            runtimes : \'html5\',\n-            url : \'/photo/upload.json\',\n-            max_file_size : \'20mb\',\n-            //chunk_size : \'1mb\',\n-            unique_names : true,\n-     \n-            // Specify what files to browse for\n-            filters : [\n-                {title : "Photos", extensions : "jpg,jpeg,gif,png"}\n-            ],\n-     \n-            // Flash settings\n-            flash_swf_url : \'plupload.flash.swf\',\n-            multipart_params:{\n-              crumb: $("form.upload input.crumb").val()\n-            },\n-            preinit: {\n-              BeforeUpload: function() {\n-                var uploader = $("#uploader").pluploadQueue();\n-                $(".upload-progress .total").html(uploader.files.length);\n-                $(".upload-progress .completed").html(uploader.total.uploaded+1);\n-                $(".upload-progress").slideDown(\'fast\');\n-              },\n-              UploadComplete: function() {\n-                $(".upload-progress").fadeOut(\'fast\', function() { $(".upload-complete").fadeIn(\'fast\'); });\n-              },\n-              UploadFile: function() {\n-                var uploader = $("#uploader").pluploadQueue(), license, permission, tags;\n-                license = $("form.upload select[name=\'license\'] :selected").val();\n-                if(license.length == 0)\n-                  license = $("form.upload input[name=\'custom\']").val();\n-                tags = $("form.upload input[name=\'tags\']").val();\n-                permission = $("form.upload input[name=\'permission\']:checked").val();\n-                \n-                uploader.settings.multipart_params.license = license;\n-                uploader.settings.multipart_params.tags = tags;\n-                uploader.settings.multipart_params.permission = permission;\n-              }\n-            }\n-        });\n-     \n-        // Client side form validation\n-        var uploadForm = $("form.upload");\n-        uploadForm.submit(function(e) {\n-          var uploader = $(\'#uploader\').pluploadQueue({});\n-          // Files in queue upload them first\n-          if (uploader.files.length > 0) {\n-            // When all files are uploaded submit form\n-            uploader.bind(\'StateChanged\', function() {\n-              if (uploader.files.length === (uploader.total.uploaded + uploader.total.failed)) {\n-                $("form.upload")[0].submit();\n-              }\n-            }); \n-            uploader.start();\n-          } else {\n-            // TODO something that doesn\'t suck\n-            alert(\'Please select at least one photo to upload.\');\n+\t\t}, \n+    user: {\n+      base: {\n+        loginProcessed: function(response) {\n+          if(response.code != 200) {\n+            log(\'processing of login failed\');\n+            // TODO do something here to handle failed login\n+            return;\n           }\n-   \n-          return false;\n-        });\n \n-        var insufficient = $("#uploader .insufficient");\n-        if(insufficient.length == 1)\n-          insufficient.show();\n-      }\n-    },\n-    user: {\n-      loginFailure: function(assertion) {\n-        log(\'login failed\');\n-        // TODO something here to handle failed login\n-      },\n-      loginProcessed: function(response) {\n-        if(response.code != 200) {\n-          log(\'processing of login failed\');\n-          // TODO do something here to handle failed login\n-          return;\n+          log(\'login processing succeeded\');\n+          window.location.reload();\n         }\n-\n-        log(\'login processing succeeded\');\n-        window.location.reload();\n       },\n-      loginSuccess: function(assertion) {\n-        var params = {assertion: assertion};\n-        OP.Util.makeRequest(\'/user/login.json\', params, opTheme.user.loginProcessed);\n+      browserid: {\n+        loginFailure: function(assertion) {\n+          log(\'login failed\');\n+          // TODO something here to handle failed login\n+        },\n+        loginSuccess: function(assertion) {\n+          var params = {assertion: assertion};\n+          OP.Util.makeRequest(\'/user/browserid/login.json\', params, opTheme.user.base.loginProcessed);\n+        }\n       }\n     }\n   };\ndiff --git a/src/html/assets/themes/beisel/stylesheets/main.css b/src/html/assets/themes/beisel/stylesheets/main.css\nindex 93620dd5..7d1a9066 100644\n--- a/src/html/assets/themes/beisel/stylesheets/main.css\n+++ b/src/html/assets/themes/beisel/stylesheets/main.css\n@@ -525,9 +525,9 @@ nav ul li#nav-search form button:active {\n \n nav ul li#nav-signin,\n nav ul li#nav-browserid {\n+  cursor:inherit;\n   border-left:1px solid #eee;\n   color:#666;\n-  cursor:pointer;\n   display:inline-block;\n   float:right;\n   margin:0;\n@@ -537,17 +537,21 @@ nav ul li#nav-browserid {\n \n nav ul li#nav-browserid {\n   height:40px;\n-  width:100px;\n+  width:150px;\n }\n \n nav ul li#nav-browserid a {\n   border:none;\n   display:block;\n   margin:9px 0 3px 0;\n-  padding:0 10px 0 10px;\n+  padding:0 15px;\n   position:relative;\n   width:auto;\n }\n+nav ul li#nav-browserid a img{\n+  margin-left:10px;\n+  cursor:pointer;\n+}\n \n nav ul li#nav-signin:hover,\n nav ul li#nav-browserid:hover,\n@@ -1946,35 +1950,6 @@ div#setup h3 {\n   margin:10px 0 0 164px;\n }\n \n-/*  Upload ************************************************ */\n-body.upload .options {\n-  margin:10px;\n-}\n-body.upload .options div {\n-  margin:6px 0;\n-}\n-body.upload .options label {\n-  float:left;\n-  width:100px;\n-}\n-body.upload .poweredby {\n-  font-style:italic;\n-  font-size:12px;\n-  display:block;\n-  text-align:right;\n-}\n-body.upload .message {\n-  font-size:30px;\n-  margin:10px 0 30px;\n-  text-align:center;\n-  display:none;\n-}\n-body.upload .message img {\n-  position:relative;\n-  left:-5px;\n-  top:5px;\n-}\n-\n /* 04.09 Settings ************************************************ */\n \n .settings form label {\n@@ -2035,183 +2010,3 @@ footer a:hover {\n a.message-close {text-decoration:underline; cursor:pointer; color:#43c2fd; position:absolute; top:5px; right:45px;}\n #message-box.error {color:#ca1616;}\n #message-box.confirm {color:#6c9e6d;}\n-\n-/* \n-   Plupload\n-------------------------------------------------------------------- */\n-\n-.plupload_button {\n-\tdisplay: -moz-inline-box; /* FF < 3*/\n-\tdisplay: inline-block;\n-\tfont: normal 12px sans-serif;\n-\ttext-decoration: none;\n-  color: #42454a;\n-  border: 1px solid #bababa;\n-  padding: 2px 8px 3px 20px;\n-\tmargin-right: 4px;\n-  background: #f3f3f3 url(\'../images/buttons.png\') no-repeat 0 center;\n-  outline: 0;\n-\n-  /* Optional rounded corners for browsers that support it */\n-  -moz-border-radius: 3px;\n-  -khtml-border-radius: 3px;\n-  -webkit-border-radius: 3px;\n-  border-radius: 3px;\n-}\n-\n-.plupload_button:hover {\n-\tcolor: #000;\n-\ttext-decoration: none;\n-}\n-\n-.plupload_disabled, a.plupload_disabled:hover {\n-    color: #737373;\n-    border-color: #c5c5c5;\n-    background: #ededed url(\'../images/buttons-disabled.png\') no-repeat 0 center;\n-\tcursor: default;\n-}\n-\n-.plupload_add {\n-\tbackground-position: -181px center;\n-}\n-\n-.plupload_wrapper {\n-\tfont: normal 11px Verdana,sans-serif;\n-\twidth: 100%;\n-}\n-\n-.plupload_container {\n-\tpadding: 8px;\n-\tbackground: url(\'../images/transp50.png\');\n-\t/*-moz-border-radius: 5px;*/\n-}\n-\n-.plupload_container input {\n-\tborder: 1px solid #DDD;\n-\tfont: normal 11px Verdana,sans-serif;\n-\twidth: 98%;\n-}\n-\n-.plupload_header {background: #2A2C2E url(\'../images/backgrounds.gif\') repeat-x;}\n-.plupload_header_content {\n-\tbackground: url(\'../images/backgrounds.gif\') no-repeat 0 -317px;\n-\tmin-height: 56px;\n-\tpadding-left: 60px;\n-\tcolor: #FFF;\n-}\n-.plupload_header_title {\n-\tfont: normal 18px sans-serif;\n-\tpadding: 6px 0 3px;\n-}\n-.plupload_header_text {\n-\tfont: normal 12px sans-serif;\n-}\n-\n-.plupload_filelist {\n-\tmargin: 0;\n-\tpadding: 0;\n-\tlist-style: none;\n-}\n-\n-.plupload_scroll .plupload_filelist {\n-\t/*height: 185px;*/\n-\tbackground: #F5F5F5;\n-  clear:both;\n-\t/*overflow-y: scroll;*/\n-}\n-\n-.plupload_filelist li {\n-\tpadding: 10px 8px;\n-\tbackground: #F5F5F5 url(\'../images/backgrounds.gif\') repeat-x 0 -156px;\n-\tborder-bottom: 1px solid #DDD;\n-}\n-\n-.plupload_filelist_header, .plupload_filelist_footer {\n-\tbackground: #DFDFDF;\n-\tpadding: 8px 8px;\n-\tcolor: #42454A;\n-}\n-.plupload_filelist_header {\t\n-\tborder-top: 1px solid #EEE;\n-\tborder-bottom: 1px solid #CDCDCD;\n-}\n-\n-.plupload_filelist_footer {border-top: 1px solid #FFF; height: 22px; line-height: 20px; vertical-align: middle;}\n-.plupload_file_name {float: left; overflow: hidden}\n-.plupload_file_status {color: #777;}\n-.plupload_file_status span {color: #42454A;}\n-.plupload_file_size, .plupload_file_status, .plupload_progress {\n-\tfloat: right;\n-\twidth: 80px;\n-}\n-.plupload_file_size, .plupload_file_status, .plupload_file_action {text-align: right;}\n-\n-.plupload_filelist .plupload_file_name {width: 205px}\n-\n-.plupload_file_action {\n-\tfloat: right;\n-\twidth: 16px;\n-\theight: 16px;\n-\tmargin-left: 15px;\n-}\n-\n-.plupload_file_action * {\n-\tdisplay: none;\n-\twidth: 16px;\n-\theight: 16px;\n-}\n-\n-li.plupload_uploading {background: #ECF3DC url(\'../images/backgrounds.gif\') repeat-x 0 -238px;}\n-li.plupload_done {color:#AAA}\n-\n-li.plupload_delete a {\n-\tbackground: url(\'../images/delete.gif\');\n-}\n-\n-li.plupload_failed a {\n-\tbackground: url(\'../images/error.gif\');\n-\tcursor: default;\n-}\n-\n-li.plupload_done a {\n-\tbackground: url(\'../images/done.gif\');\n-\tcursor: default;\n-}\n-\n-.plupload_progress, .plupload_upload_status {\n-\tdisplay: none;\n-}\n-\n-.plupload_progress_container {\n-\tmargin-top: 3px;\n-\tborder: 1px solid #CCC;\n-\tbackground: #FFF;\n-\tpadding: 1px;\n-}\n-.plupload_progress_bar {\n-\twidth: 0px;\n-\theight: 7px;\n-\tbackground: #CDEB8B;\n-}\n-\n-.plupload_scroll .plupload_filelist_header .plupload_file_action, .plupload_scroll .plupload_filelist_footer .plupload_file_action {\n-\tmargin-right: 17px;\n-}\n-\n-/* Floats */\n-\n-.plupload_clear,.plupload_clearer {clear: both;}\n-.plupload_clearer, .plupload_progress_bar {\n-\tdisplay: block;\n-\tfont-size: 0;\n-\tline-height: 0;\t\n-}\n-\n-li.plupload_droptext {\n-\tbackground: transparent;\n-\ttext-align: center;\n-\tvertical-align: middle;\n-\tborder: 0;\n-  font-size:30px;\n-\tline-height: 165px;\n-}\ndiff --git a/src/html/assets/themes/beisel/templates/partials/navigation.php b/src/html/assets/themes/beisel/templates/partials/navigation.php\nindex 2763dd94..cbfda019 100644\n--- a/src/html/assets/themes/beisel/templates/partials/navigation.php\n+++ b/src/html/assets/themes/beisel/templates/partials/navigation.php\n@@ -23,7 +23,10 @@\n \t</li>\n <?php } else { ?>\n \t<li id="nav-browserid">\n-\t\t<a class="login-click" title="Signin to OpenPhoto"><img src="https://browserid.org/i/sign_in_blue.png" class="login-click" /></a>\n+    <?php if(getPlugin()->isActive(\'FacebookConnect\')) { ?>\n+      <a class="login-click facebook" title="Signin using Facebook"><img src="<?php getTheme()->asset(\'image\', \'facebook-icon.gif\'); ?>" class="login-click facebook" hspace="5" /></a>\n+    <?php } ?>\n+    <a class="login-click browserid" title="Signin using BrowserID"><img src="<?php getTheme()->asset(\'image\', \'browserid.png\'); ?>" class="login-click browserid" /></a>\n \t</li>\n <?php } ?>\n \t</li>\ndiff --git a/src/html/assets/themes/beisel/templates/photo-details.php b/src/html/assets/themes/beisel/templates/photo-details.php\nindex 21d0c946..627f8f14 100644\n--- a/src/html/assets/themes/beisel/templates/photo-details.php\n+++ b/src/html/assets/themes/beisel/templates/photo-details.php\n@@ -3,7 +3,7 @@\n </div>\n \n <section class="photo-column">\n-  <img class="photo" width="<?php Utility::safe($photo[\'thisWidth\']); ?>" height="<?php Utility::safe($photo[\'thisHeight\']); ?>" src="<?php Url::photoUrl($photo, getConfig()->get(\'photoSizes\')->detail); ?>" alt="<?php Utility::safe($photo[\'title\']); ?>">\n+  <img class="photo" width="<?php Utility::safe($photo[\'photo\'.getConfig()->get(\'photoSizes\')->detail][1]); ?>" height="<?php Utility::safe($photo[\'photo\'.getConfig()->get(\'photoSizes\')->detail][2]); ?>" src="<?php Url::photoUrl($photo, getConfig()->get(\'photoSizes\')->detail); ?>" alt="<?php Utility::safe($photo[\'title\']); ?>">\n   <h1><?php Utility::safe($photo[\'title\']); ?></h1>\n   <p class="description"><?php Utility::safe($photo[\'description\']); ?></p>\n   <?php if(count($photo[\'actions\']) > 0) { ?>\ndiff --git a/src/html/assets/themes/beisel/templates/photos.php b/src/html/assets/themes/beisel/templates/photos.php\nindex c1cdcec4..3885cbe8 100644\n--- a/src/html/assets/themes/beisel/templates/photos.php\n+++ b/src/html/assets/themes/beisel/templates/photos.php\n@@ -5,14 +5,14 @@\n     <?php foreach($photos as $photo) { ?>\n       <li class="grid-item id-<?php Utility::safe($photo[\'id\']); ?>">\n         <a href="<?php Url::photoView($photo[\'id\'], $options); ?>">\n-\t\t\t<img src="<?php Url::photoUrl($photo, $thumbnailSize); ?>" alt="<?php Utility::safe($photo[\'title\']); ?>" />\n-\t        <ul class="meta">\n-\t          <li class="age"><?php Utility::timeAsText($photo[\'dateTaken\'], \'Taken\'); ?></li>\n-\t          <li class="permission"><span class="<?php Utility::permissionAsText($photo[\'permission\']); ?>" title="Photo is <?php Utility::permissionAsText($photo[\'permission\']); ?>"></li>\n-\t\t\t  <?php if(isset($photo[\'tags\']) && !empty($photo[\'tags\'])) { ?><li class="tags"><span title="Photo has tags"></span></li><?php } ?>\n-\t\t\t  <?php if(isset($photo[\'latitude\']) && !empty($photo[\'latitude\'])) { ?><li class="geo"><span title="Photo has geo informations"></span></li><?php } ?>\n-\t        </ul>\n-\t\t</a>\n+          <img src="<?php Url::photoUrl($photo, $thumbnailSize); ?>" alt="<?php Utility::safe($photo[\'title\']); ?>" />\n+          <ul class="meta">\n+            <li class="age"><?php Utility::timeAsText($photo[\'dateTaken\'], \'Taken\'); ?></li>\n+            <li class="permission"><span class="<?php Utility::permissionAsText($photo[\'permission\']); ?>" title="Photo is <?php Utility::permissionAsText($photo[\'permission\']); ?>"></li>\n+            <?php if(isset($photo[\'tags\']) && !empty($photo[\'tags\'])) { ?><li class="tags"><span title="Photo has tags"></span></li><?php } ?>\n+            <?php if(isset($photo[\'latitude\']) && !empty($photo[\'latitude\'])) { ?><li class="geo"><span title="Photo has geo informations"></span></li><?php } ?>\n+          </ul>\n+        </a>\n       </li>\n     <?php } ?>\n   </ul>\ndiff --git a/src/html/assets/themes/beisel/templates/tags.php b/src/html/assets/themes/beisel/templates/tags.php\nindex 9cfe8faf..51fbcb4f 100644\n--- a/src/html/assets/themes/beisel/templates/tags.php\n+++ b/src/html/assets/themes/beisel/templates/tags.php\n@@ -8,7 +8,7 @@\n   <ol class="tag-cloud">\n     <?php foreach($tags as $tag) { ?>\n     <li class="size-<?php Utility::safe($tag[\'weight\']); ?>">\n-      <a href="<?php Url::photosView("tags-{$tag[\'id\']}"); ?>" title="<?php Utility::safe($tag[$tagField]); ?> photos"><?php Utility::safe($tag[\'id\']); ?></a>\n+      <a href="<?php Url::photosView("tags-{$tag[\'id\']}"); ?>" title="<?php Utility::safe($tag[\'count\']); ?> photos"><?php Utility::safe($tag[\'id\']); ?></a>\n     </li>\n     <?php } ?>\n   </ol>\ndiff --git a/src/html/assets/themes/beisel/templates/template.php b/src/html/assets/themes/beisel/templates/template.php\nindex 2814fe39..8a74a073 100644\n--- a/src/html/assets/themes/beisel/templates/template.php\n+++ b/src/html/assets/themes/beisel/templates/template.php\n@@ -9,13 +9,15 @@\n     <meta name="keywords" content="<?php getTheme()->meta(\'keywords\', $page); ?>">\n     <meta name="author" content="openphoto.me">\n     <meta name="viewport" content="width=device-width, initial-scale=1.0">\n-    <link rel="shortcut icon" href="<?php getTheme()->asset(\'image\', \'favicon.ico\'); ?>">\n+    <link rel="shortcut icon" href="<?php getTheme()->asset(\'image\', \'favicon.png\'); ?>">\n     <link rel="apple-touch-icon" href="<?php getTheme()->asset(\'image\', \'apple-touch-icon.png\'); ?>">\n     <link rel="stylesheet" href="<?php getTheme()->asset(\'stylesheet\', \'main.css\'); ?>">\n+    <link rel="stylesheet" href="/assets/stylesheets/upload.css">\n     <?php getPlugin()->invoke(\'onHead\', array(\'page\' => $page)); ?>\n </head>\n \n <body class="<?php echo $page; ?>">\n+  <?php getPlugin()->invoke(\'onBodyBegin\', array(\'page\' => $page)); ?>\n \n   <div id="wrapper">\n \n@@ -44,8 +46,9 @@\n               \'<?php getTheme()->asset(\'javascript\', \'plupload.js\'); ?>\',\n               \'<?php getTheme()->asset(\'javascript\', \'plupload.html5.js\'); ?>\',\n               \'<?php getTheme()->asset(\'javascript\', \'jquery.plupload.queue.js\'); ?>\',\n+              \'/assets/javascripts/openphoto-upload.js\',\n             <?php } else { ?>\n-              \'<?php getTheme()->asset(\'javascript\', \'plupload-full-min.js\'); ?>\',\n+              \'/assets/javascripts/openphoto-upload.min.js\',\n             <?php } ?>\n           <?php } ?>\n \n@@ -64,6 +67,6 @@\n       }\n     });\n   </script>\n-  <?php getPlugin()->invoke(\'onBody\', array(\'page\' => $page)); ?>\n+  <?php getPlugin()->invoke(\'onBodyEnd\', array(\'page\' => $page)); ?>\n </body>\n </html>\ndiff --git a/src/html/assets/themes/default/images/favicon.ico b/src/html/assets/themes/default/images/favicon.ico\ndeleted file mode 100644\nindex c6c716b2..00000000\nBinary files a/src/html/assets/themes/default/images/favicon.ico and /dev/null differ\ndiff --git a/src/html/assets/themes/default/images/favicon.png b/src/html/assets/themes/default/images/favicon.png\nnew file mode 100644\nindex 00000000..9537b259\nBinary files /dev/null and b/src/html/assets/themes/default/images/favicon.png differ\ndiff --git a/src/html/assets/themes/default/images/header-logo.png b/src/html/assets/themes/default/images/header-logo.png\nindex c232c552..fb6d7052 100644\nBinary files a/src/html/assets/themes/default/images/header-logo.png and b/src/html/assets/themes/default/images/header-logo.png differ\ndiff --git a/src/html/assets/themes/default/javascripts/openphoto-theme.js b/src/html/assets/themes/default/javascripts/openphoto-theme.js\nindex 4604e34f..784b7f1b 100644\n--- a/src/html/assets/themes/default/javascripts/openphoto-theme.js\n+++ b/src/html/assets/themes/default/javascripts/openphoto-theme.js\n@@ -323,125 +323,6 @@ var opTheme = (function() {\n       }\n     },\n \n-    upload: {\n-      init: function() {\n-        var that = this; // that references upload\n-        if ($("body.upload").length) {\n-          that.options.$dropZone = $("#drop-zone");\n-          that.options.crumb = $("#uploader-frame").attr("crumb");\n-          that.options.dragEnterCallback = that.dragEnter;\n-          that.options.dragLeaveCallback = that.dragLeave;\n-          that.options.dragDropCallback = that.dragDrop;\n-          that.options.duplicateCallback = that.duplicate;\n-          that.options.notImageCallback = that.notImage;\n-          that.options.pushToUICallback = that.pushToUI;\n-          that.options.uploadStartCallback = that.uploadStart;\n-          that.options.uploadProgressCallback = that.uploadProgress;\n-          that.options.uploadFinishedCallback = that.uploadFinished;\n-          that.options.permission = that.permission;\n-          that.options.photoTags = that.photoTags;\n-          that.options.photoLicense = that.photoLicense;\n-          OP.Util.upload.init(that.options);\n-          that.licenseChange();\n-        }\n-      },\n-\n-      options : {\n-        simultaneousUploadLimit : 3,\n-        frameId : "uploader-frame",\n-        dropZoneId : "drop-zone",\n-        uploadPath : \'/photo/upload.json\',\n-        returnSizes : "50x50xCR",\n-        allowDuplicates : false\n-      },\n-\n-      licenseChange : function() {\n-        $("#uploader-frame .license").bind("change", function() {\n-          if ($(this).val() == "_custom_") {\n-            $("#uploader-frame .custom").fadeIn();\n-          } else {\n-            if ($(this).is(":visible")) {\n-              $("#uploader-frame .custom").fadeOut();\n-            }\n-          }\n-        });\n-      },\n-\n-      /**\n-      * !! REMINDER !!\n-      * These functions are going to be called from within the\n-      * opTheme.upload.options object so \'this\' points back to\n-      * the options object\n-      **/\n-      dragEnter : function() {\n-        this.$dropZone.removeClass("waiting active").addClass("hover");\n-      },\n-\n-      dragLeave : function() {\n-        this.$dropZone.removeClass("hover").addClass("active");\n-      },\n-\n-      dragDrop : function() {\n-        this.$dropZone.removeClass("hover").addClass("active");\n-      },\n-\n-      duplicate : function() {\n-        opTheme.message.error("duplicate image");\n-      },\n-\n-      notImage : function() {\n-        opTheme.message.error("not an image file");\n-      },\n-\n-      pushToUI : function(files) {\n-        // get current tags and license data to apply to each photo\n-        var tags = $("#uploader-frame .tags").val();\n-        var permission = $("#uploader-frame input[name=permission]").val();\n-        var license = $("#uploader-frame .license").val();\n-        if (license == "_custom_") {\n-          license = $("#uploader-frame .custom input").val();\n-        }\n-        var html = [];\n-        for (var i=0; i < files.length; i++) {\n-          var size = (parseInt(files[i].size) / 1048576).toFixed(2) + "MB";\n-          html.push("<div id=\'file-",files[i]["queueIndex"],"\' class=\'photo waiting\' tags=\'",tags,"\' license=\'",license,"\' permission=\'",permission,"\'><span class=\'name\'>",files[i].name,"</span><span class=\'size\'>",size,"</span><span class=\'progress\'></span></div>");\n-        }\n-        this.$dropZone.append(html.join(""));\n-        OP.Util.upload.kickOffUploads();\n-      },\n-\n-      uploadStart : function(queueIndex) {\n-        $("#file-"+queueIndex).removeClass("waiting").addClass("uploading");\n-      },\n-\n-      uploadProgress : function(queueIndex, percent) {\n-        $("#file-"+queueIndex+" .progress").animate({\n-          "width":percent+"%"\n-        }, 500);\n-      },\n-\n-      uploadFinished : function(queueIndex, status, response) {\n-        var thisClass = response.code == 202 ? \'finished\' : \'error\';\n-        $("#file-"+queueIndex+" .progress").addClass(thisClass);\n-        $("#file-"+queueIndex).removeClass("uploading").addClass("finished");\n-        if(response.code == 202)\n-          $("#file-"+queueIndex).append("<img class=\'thumb\' src=\'"+response.result.path50x50xCR+"\'/>");\n-      },\n-\n-      permission : function(queueIndex) {\n-        return $("#file-"+queueIndex).attr("permission");\n-      },\n-\n-      photoLicense : function(queueIndex) {\n-        return $("#file-"+queueIndex).attr("license");\n-      },\n-\n-      photoTags : function(queueIndex) {\n-        return $("#file-"+queueIndex).attr("tags");\n-      }\n-\n-    }, // upload\n-\n     init: {\n       attach: function() {\n         OP.Util.on(\'click:action-delete\', opTheme.callback.actionDelete);\n@@ -460,7 +341,7 @@ var opTheme = (function() {\n         OP.Util.on(\'keydown:browse-previous\', opTheme.callback.keyBrowsePrevious);\n \n         opTheme.front.init($(\'div.front-slideshow\'));\n-        opTheme.upload.init();\n+        OPU.init();\n         // TODO standardize this somehow\n         $(\'form.validate\').each(opTheme.formHandlers.init);\n       }\n@@ -562,7 +443,7 @@ var opTheme = (function() {\n       },\n       loginSuccess: function(assertion) {\n         var params = {assertion: assertion};\n-        OP.Util.makeRequest(\'/user/login.json\', params, opTheme.user.loginProcessed, \'json\');\n+        OP.Util.makeRequest(\'/user/browserid/login.json\', params, opTheme.user.loginProcessed, \'json\');\n       }\n     },\n   };\ndiff --git a/src/html/assets/themes/default/templates/photo-details.php b/src/html/assets/themes/default/templates/photo-details.php\nindex e5f9eaf1..a53b226c 100644\n--- a/src/html/assets/themes/default/templates/photo-details.php\n+++ b/src/html/assets/themes/default/templates/photo-details.php\n@@ -3,7 +3,7 @@\n     <div class="photo-column">\n       <h1><?php Utility::safe($photo[\'title\']); ?></h1>\n       <p class="description"><?php Utility::safe($photo[\'description\']); ?></p>\n-      <img class="photo" width="<?php Utility::safe($photo[\'thisWidth\']); ?>" height="<?php Utility::safe($photo[\'thisHeight\']); ?>" src="<?php Url::photoUrl($photo, getConfig()->get(\'photoSizes\')->detail); ?>" alt="<?php Utility::safe($photo[\'title\']); ?>">\n+      <img class="photo" width="<?php Utility::safe($photo[\'photo\'.getConfig()->get(\'photoSizes\')->detail][1]); ?>" height="<?php Utility::safe($photo[\'photo\'.getConfig()->get(\'photoSizes\')->detail][1]); ?>" src="<?php Url::photoUrl($photo, getConfig()->get(\'photoSizes\')->detail); ?>" alt="<?php Utility::safe($photo[\'title\']); ?>">\n     </div>\n     <div class="sidebar">\n       <div class="image-pagination">\ndiff --git a/src/html/assets/themes/default/templates/tags.php b/src/html/assets/themes/default/templates/tags.php\nindex 38df3ce0..5de38897 100644\n--- a/src/html/assets/themes/default/templates/tags.php\n+++ b/src/html/assets/themes/default/templates/tags.php\n@@ -5,7 +5,7 @@\n   <ol class="tag-cloud">\n     <?php foreach($tags as $tag) { ?>\n     <li class="size-<?php Utility::safe($tag[\'weight\']); ?>">\n-      <a href="<?php Url::photosView("tags-{$tag[\'id\']}"); ?>" title="<?php Utility::safe($tag[$tagField]); ?> photos"><?php Utility::safe($tag[\'id\']); ?></a>\n+      <a href="<?php Url::photosView("tags-{$tag[\'id\']}"); ?>" title="<?php Utility::safe($tag[\'count\']); ?> photos"><?php Utility::safe($tag[\'id\']); ?></a>\n     </li>\n     <?php } ?>\n   </ol>\ndiff --git a/src/html/assets/themes/default/templates/template.php b/src/html/assets/themes/default/templates/template.php\nindex 6c499a43..f68169da 100644\n--- a/src/html/assets/themes/default/templates/template.php\n+++ b/src/html/assets/themes/default/templates/template.php\n@@ -15,7 +15,7 @@\n     <meta name="author" content="">\n \n     <meta name="viewport" content="width=device-width, initial-scale=1.0">\n-    <link rel="shortcut icon" href="<?php getTheme()->asset(\'image\', \'favicon.ico\'); ?>">\n+    <link rel="shortcut icon" href="<?php getTheme()->asset(\'image\', \'favicon.png\'); ?>">\n     <link rel="apple-touch-icon" href="<?php getTheme()->asset(\'image\', \'apple-touch-icon.png\'); ?>">\n     <link rel="stylesheet" href="<?php getTheme()->asset(\'stylesheet\', \'html5_boilerplate_style.css\'); ?>">\n     <link rel="stylesheet" href="<?php getTheme()->asset(\'stylesheet\', \'basics.css\'); ?>">\n@@ -30,6 +30,7 @@\n     <link rel="stylesheet" href="<?php getTheme()->asset(\'stylesheet\', \'settings.css\'); ?>">\n     <link rel="stylesheet" href="<?php getTheme()->asset(\'stylesheet\', \'setup.css\'); ?>">\n     <link rel="stylesheet" href="<?php getTheme()->asset(\'stylesheet\', \'message-box.css\'); ?>">\n+    <link rel="stylesheet" href="/assets/stylesheets/upload.css">\n     <?php getPlugin()->invoke(\'onHead\', array(\'page\' => $page)); ?>\n </head>\n \n@@ -58,6 +59,7 @@\n           \'<?php getTheme()->asset(\'javascript\', \'jquery.scrollTo-1.4.2-min.js\'); ?>\',\n           \'<?php getTheme()->asset(\'javascript\', \'jquery.fileupload.min.js\'); ?>\',\n           \'<?php getTheme()->asset(\'javascript\', \'jquery.cycle.min.js \'); ?>\',\n+          \'/assets/javascripts/openphoto-upload.min.js\',\n           \'<?php getTheme()->asset(\'javascript\', \'openphoto-theme.js\'); ?>\'\n         ],\n         onComplete: function(){ opTheme.init.attach(); }\ndiff --git a/src/libraries/adapters/DatabaseMySql.php b/src/libraries/adapters/DatabaseMySql.php\nindex c9620c0a..28d113cf 100644\n--- a/src/libraries/adapters/DatabaseMySql.php\n+++ b/src/libraries/adapters/DatabaseMySql.php\n@@ -1086,9 +1086,19 @@ class DatabaseMySql implements DatabaseInterface\n         $stmt[\'vals\'] .= ",";\n       $stmt[\'cols\'] .= $key;\n       if(!empty($bindings) && array_key_exists($value, $bindings))\n-        $stmt[\'vals\'] .= "{$value}";\n+      {\n+        if(is_null($value))\n+          $stmt[\'vals\'] .= \'NULL\';\n+        else\n+          $stmt[\'vals\'] .= $value;\n+      }\n       else\n-        $stmt[\'vals\'] .= sprintf("\'%s\'", $this->_($value));\n+      {\n+        if(is_null($value))\n+          $stmt[\'vals\'] .= \'NULL\';\n+        else\n+          $stmt[\'vals\'] .= sprintf("\'%s\'", $this->_($value));\n+      }\n     }\n     return $stmt;\n   }\n@@ -1235,6 +1245,9 @@ class DatabaseMySql implements DatabaseInterface\n         case \'extra\':\n           break;\n         default:\n+          // skip empty lat/long Gh-313\n+          if(($key == \'latitude\' || $key == \'longitude\') && empty($value))\n+            $value = null;\n           $bindings[":{$key}"] = $value;\n           $paramsOut[$key] = ":{$key}";\n           break;\ndiff --git a/src/libraries/adapters/FileSystemDropboxBase.php b/src/libraries/adapters/FileSystemDropboxBase.php\nindex 58c51f87..59dedcb8 100644\n--- a/src/libraries/adapters/FileSystemDropboxBase.php\n+++ b/src/libraries/adapters/FileSystemDropboxBase.php\n@@ -17,7 +17,7 @@ class FileSystemDropboxBase\n       \'token\' => Utility::decrypt(getConfig()->get(\'credentials\')->dropboxToken),\n       \'token_secret\' => Utility::decrypt(getConfig()->get(\'credentials\')->dropboxTokenSecret)\n     ));\n-    $this->dropbox = new Dropbox_API($oauth);\n+    $this->dropbox = new Dropbox_API($oauth, Dropbox_API::ROOT_SANDBOX);\n     $this->dropboxFolder = getConfig()->get(\'dropbox\')->dropboxFolder;\n     $this->parent = $parent;\n   }\ndiff --git a/src/libraries/adapters/FileSystemLocal.php b/src/libraries/adapters/FileSystemLocal.php\nindex 399e3781..1213434e 100644\n--- a/src/libraries/adapters/FileSystemLocal.php\n+++ b/src/libraries/adapters/FileSystemLocal.php\n@@ -121,9 +121,17 @@ class FileSystemLocal implements FileSystemInterface\n   public function initialize()\n   {\n     if(!file_exists($this->root)) {\n-      mkdir($this->root, 0775, true);\n+      @mkdir($this->root, 0775, true);\n+    }\n+    if(file_exists($this->root))\n+    {\n+      return true;\n+    }\n+    else\n+    {\n+      getLogger()->crit("Could not create {$this->root}");\n+      return false;\n     }\n-    return file_exists($this->root);\n   }\n \n   /**\n@@ -140,4 +148,9 @@ class FileSystemLocal implements FileSystemInterface\n   {\n     return $this->root . $path;\n   }\n+\n+  public function getRoot()\n+  {\n+    return $this->root;\n+  }\n }\ndiff --git a/src/libraries/adapters/Login.php b/src/libraries/adapters/Login.php\nnew file mode 100644\nindex 00000000..7c2ab8b4\n--- /dev/null\n+++ b/src/libraries/adapters/Login.php\n@@ -0,0 +1,27 @@\n+<?php\n+/**\n+ * Interface for the Login models.\n+ *\n+ * This defines the interface for any model that wants to connect to authenticate a user.\n+ * @author Jaisen Mathai <jaisen@jmathai.com>\n+ */\n+interface LoginInterface\n+{\n+  public function __construct();\n+  public function verifyEmail($args);\n+}\n+\n+function getLogin($provider)\n+{\n+  switch($provider)\n+  {\n+    case \'facebook\':\n+      return new LoginFacebook;\n+      break;\n+    case \'browserid\':\n+    default:\n+      return new LoginBrowserId;\n+      break;\n+  }\n+}\n+\ndiff --git a/src/libraries/adapters/LoginBrowserId.php b/src/libraries/adapters/LoginBrowserId.php\nnew file mode 100644\nindex 00000000..a5d061e1\n--- /dev/null\n+++ b/src/libraries/adapters/LoginBrowserId.php\n@@ -0,0 +1,34 @@\n+<?php\n+/**\n+ * BrowserId implementation\n+ *\n+ * This class defines the functionality defined by LoginInterface for BrowserId.\n+ * @author Jaisen Mathai <jaisen@jmathai.com>\n+ */\n+class LoginBrowserId implements LoginInterface\n+{\n+  private $audience;\n+  public function __construct()\n+  {\n+    $this->audience = $_SERVER[\'HTTP_HOST\'];\n+  }\n+\n+  public function verifyEmail($args)\n+  {\n+    $ch = curl_init(\'https://browserid.org/verify\');\n+    $params = array(\'assertion\' => $args[\'assertion\'], \'audience\' => $this->audience);\n+    curl_setopt($ch, CURLOPT_POST, 1);\n+    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));\n+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n+    curl_setopt($ch, CURLOPT_CAINFO, getConfig()->get(\'paths\')->configs.\'/ca-bundle.crt\');\n+    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);\n+    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);\n+\n+    $response = curl_exec($ch);\n+    $response = json_decode($response, 1);\n+    if(!isset($response[\'status\']) || $response[\'status\'] != \'okay\')\n+      return false;\n+\n+    return $response[\'email\'];\n+  }\n+}\ndiff --git a/src/libraries/adapters/LoginFacebook.php b/src/libraries/adapters/LoginFacebook.php\nnew file mode 100644\nindex 00000000..a69185e1\n--- /dev/null\n+++ b/src/libraries/adapters/LoginFacebook.php\n@@ -0,0 +1,35 @@\n+<?php\n+/**\n+ * BrowserId implementation\n+ *\n+ * This class defines the functionality defined by LoginInterface for BrowserId.\n+ * @author Jaisen Mathai <jaisen@jmathai.com>\n+ */\n+if(!class_exists(\'Facebook\'))\n+  require getConfig()->get(\'paths\')->external . \'/facebook/facebook.php\';\n+\n+class LoginFacebook implements LoginInterface\n+{\n+  private $fb, $appId, $appSecret;\n+  public function __construct()\n+  {\n+    // test app, no one cares\n+    $this->appId = \'232147993517254\';\n+    $this->appSecret = \'5816281c38851a25544e316bb1c64084\';\n+    $this->fb = new Facebook(array(\'appId\' => $this->appId, \'secret\' => $this->appSecret));\n+  }\n+\n+  public function verifyEmail($args)\n+  {\n+    $user = $this->fb->getUser();\n+    if(!$user)\n+      return false;\n+\n+    $response = $this->fb->api(\'/me\');\n+    if(!isset($response[\'email\']))\n+      return false;\n+\n+    return $response[\'email\'];\n+  }\n+}\n+\ndiff --git a/src/libraries/controllers/ApiPhotoController.php b/src/libraries/controllers/ApiPhotoController.php\nindex 0b72a35b..7a005122 100644\n--- a/src/libraries/controllers/ApiPhotoController.php\n+++ b/src/libraries/controllers/ApiPhotoController.php\n@@ -58,42 +58,6 @@ class ApiPhotoController extends BaseController\n     return self::error(\'Photo edit markup failure\', false);\n   }\n \n-  /**\n-   * Remove all the size keys from the photo but the one in list in $sizes\n-   *\n-   * @param $photo the photo object to prune.\n-   * @param array $sizes the sizes to keep.\n-   * @return the photo\n-   */\n-  private static function pruneSizes($photo, $sizes)\n-  {\n-    if(isset($sizes) && !empty($sizes))\n-    {\n-      foreach($sizes as $size)\n-      {\n-        $sizekeys["path{$size}"] = 1;\n-      }\n-    }\n-\n-    foreach($photo as $photoKey => $photoValue)\n-    {\n-      if(preg_match(\'/path(\\d+x\\d+)/\', $photoKey))\n-      {\n-        $prune = true;\n-        if(isset($sizekeys) && isset($sizekeys[$photoKey]))\n-        {\n-          $prune = false;\n-          break;\n-        }\n-        if($prune)\n-        {\n-          unset($photo[$photoKey]);\n-        }\n-      }\n-    }\n-    return $photo;\n-  }\n-\n   /**\n     * Retrieve the next and previous photo given photo $id\n     *\n@@ -108,14 +72,14 @@ class ApiPhotoController extends BaseController\n       return self::error(\'Could not get next/previous photo\', false);\n \n     if(isset($_GET[\'returnSizes\']))\n-    {\n       $sizes = (array)explode(\',\', $_GET[\'returnSizes\']);\n-    }\n-    // if specific sizes are requested then make sure we return them\n+\n     foreach($nextPrevious as $key => $photo)\n-    {\n       $nextPrevious[$key] = self::pruneSizes($photo, $sizes);\n-    }\n+\n+    $generate = $requery = false;\n+    if(isset($_GET[\'generate\']) && $_GET[\'generate\'] == \'true\')\n+      $generate = true;\n \n     // if specific sizes are requested then make sure we return them\n     if(isset($sizes))\n@@ -129,7 +93,35 @@ class ApiPhotoController extends BaseController\n         foreach($nextPrevious as $key => $photo)\n         {\n           $options = Photo::generateFragmentReverse($size);\n-          $nextPrevious[$key]["path{$size}"] = Photo::generateUrlPublic($photo, $options[\'width\'], $options[\'height\'], $options[\'options\'], $protocol);\n+          if($generate && !isset($nextPrevious[$key]["path{$size}"]))\n+          {\n+            $hash = Photo::generateHash($photo[\'id\'], $options[\'width\'], $options[\'height\'], $options[\'options\']);\n+            Photo::generate($photo[\'id\'], $hash, $options[\'width\'], $options[\'width\'], $options[\'options\']);\n+            $requery = true;\n+          }\n+          else\n+          {\n+            // we have to merge to retain multiple sizes else the last one overwrites the rest\n+            // we also can\'t pass in $photo since it doesn\'t persist over iterations and removes returnSizes\n+            $nextPrevious[$key] = Photo::addApiUrls($nextPrevious[$key], $options, $protocol);\n+          }\n+        }\n+      }\n+\n+      // requery to get generated paths\n+      if($requery)\n+      {\n+        $nextPrevious = getDb()->getPhotoNextPrevious($id, $filters);\n+        foreach($nextPrevious as $key => $photo)\n+        {\n+          $nextPrevious[$key] = self::pruneSizes($photo, $sizes);\n+          foreach($sizes as $size)\n+          {\n+            // we have to merge to retain multiple sizes else the last one overwrites the rest\n+            // we also can\'t pass in $photo since it doesn\'t persist over iterations and removes returnSizes\n+            $options = Photo::generateFragmentReverse($size);\n+            $nextPrevious[$key] = Photo::addApiUrls($nextPrevious[$key], $options, $protocol);\n+          }\n         }\n       }\n     }\n@@ -153,7 +145,7 @@ class ApiPhotoController extends BaseController\n \n   /*public static function dynamic($id, $hash, $width, $height, $options = null)\n   {\n-    $photo = Photo::generateImage($id, $hash, $width, $height, $options);\n+    $photo = Photo::generate($id, $hash, $width, $height, $options);\n     return self::success(\'\', $photo);\n   }*/\n \n@@ -179,6 +171,10 @@ class ApiPhotoController extends BaseController\n     if(isset($filters[\'returnSizes\']))\n       $sizes = (array)explode(\',\', $filters[\'returnSizes\']);\n \n+    $generate = $requery = false;\n+    if(isset($_GET[\'generate\']) && $_GET[\'generate\'] == \'true\')\n+      $generate = true;\n+\n     foreach($photos as $key => $photo)\n     {\n       // we remove all path* entries to keep the interface clean and only return sizes explicitly requested\n@@ -190,9 +186,37 @@ class ApiPhotoController extends BaseController\n         foreach($sizes as $size)\n         {\n           // TODO call API\n-          // here we put a previously deleted key back in - ah, the things we do for consistency\n+          // we do this to put a previously deleted key (pruneSizes) back in - ah, the things we do for consistency\n           $options = Photo::generateFragmentReverse($size);\n-          $photos[$key]["path{$size}"] = Photo::generateUrlPublic($photo, $options[\'width\'], $options[\'height\'], $options[\'options\'], $protocol);\n+          if($generate && !isset($photo["path{$size}"]))\n+          {\n+            $hash = Photo::generateHash($photo[\'id\'], $options[\'width\'], $options[\'height\'], $options[\'options\']);\n+            Photo::generate($photo[\'id\'], $hash, $options[\'width\'], $options[\'width\'], $options[\'options\']);\n+            $requery = true;\n+          }\n+          else\n+          {\n+            // we have to merge to retain multiple sizes else the last one overwrites the rest\n+            // we also can\'t pass in $photo since it doesn\'t persist over iterations and removes returnSizes\n+            $photos[$key] = Photo::addApiUrls($photos[$key], $options, $protocol);\n+          }\n+        }\n+      }\n+    }\n+\n+    // requery to get generated paths\n+    if($requery)\n+    {\n+      $photos = $db->getPhotos($filters, $pageSize);\n+      foreach($photos as $key => $photo)\n+      {\n+        $photos[$key] = self::pruneSizes($photo, $sizes);\n+        foreach($sizes as $size)\n+        {\n+          // we have to merge to retain multiple sizes else the last one overwrites the rest\n+          // we also can\'t pass in $photo since it doesn\'t persist over iterations and removes returnSizes\n+          $options = Photo::generateFragmentReverse($size);\n+          $photos[$key] = Photo::addApiUrls($photos[$key], $options, $protocol);\n         }\n       }\n     }\n@@ -254,7 +278,7 @@ class ApiPhotoController extends BaseController\n         {\n           $options = Photo::generateFragmentReverse($size);\n           $hash = Photo::generateHash($photoId, $options[\'width\'], $options[\'height\'], $options[\'options\']);\n-          Photo::generateImage($photoId, $hash, $options[\'width\'], $options[\'height\'], $options[\'options\']);\n+          Photo::generate($photoId, $hash, $options[\'width\'], $options[\'height\'], $options[\'options\']);\n         }\n       }\n \n@@ -331,7 +355,8 @@ class ApiPhotoController extends BaseController\n     */\n   public static function view($id)\n   {\n-    if(isset($_GET[\'actions\']) && $_GET[\'actions\'] == \'true\')\n+    $getActions = isset($_GET[\'actions\']) && $_GET[\'actions\'] == \'true\';\n+    if($getActions)\n       $photo = getDb()->getPhotoWithActions($id);\n     else\n       $photo = getDb()->getPhoto($id);\n@@ -380,10 +405,39 @@ class ApiPhotoController extends BaseController\n       if(isset($_GET[\'protocol\']))\n         $protocol = $_GET[\'protocol\'];\n \n+      $generate = $requery = false;\n+      if(isset($_GET[\'generate\']) && $_GET[\'generate\'] == \'true\')\n+        $generate = true;\n+\n       foreach($sizes as $size)\n       {\n         $options = Photo::generateFragmentReverse($size);\n-        $photo["path{$size}"] = Photo::generateUrlPublic($photo, $options[\'width\'], $options[\'height\'], $options[\'options\'], $protocol);\n+        if($generate && !isset($photo["path{$size}"]))\n+        {\n+          $hash = Photo::generateHash($id, $options[\'width\'], $options[\'height\'], $options[\'options\']);\n+          Photo::generate($id, $hash, $options[\'width\'], $options[\'width\'], $options[\'options\']);\n+          $requery = true;\n+        }\n+        else\n+        {\n+          $photo = Photo::addApiUrls($photo, $options, $protocol);\n+        }\n+      }\n+\n+      // requery to get generated paths\n+      if($requery)\n+      {\n+        if($getActions)\n+          $photo = getDb()->getPhotoWithActions($id);\n+        else\n+          $photo = getDb()->getPhoto($id);\n+\n+        $photo = self::pruneSizes($photo, $sizes);\n+        foreach($sizes as $size)\n+        {\n+          $options = Photo::generateFragmentReverse($size);\n+          $photo = Photo::addApiUrls($photo, $options, $protocol);\n+        }\n       }\n     }\n \n@@ -460,4 +514,33 @@ class ApiPhotoController extends BaseController\n \n     return array(\'filters\' => $filters, \'pageSize\' => $pageSize, \'protocol\' => $protocol, \'page\' => $page);\n   }\n+\n+  /**\n+   * Remove all the size keys from the photo but the one in list in $sizes\n+   *\n+   * @param $photo the photo object to prune.\n+   * @param array $sizes the sizes to keep.\n+   * @return the photo\n+   */\n+  private static function pruneSizes($photo, $sizes)\n+  {\n+    if(isset($sizes) && !empty($sizes))\n+    {\n+      foreach($sizes as $size)\n+      {\n+        $sizekeys["path{$size}"] = 1;\n+      }\n+    }\n+\n+    foreach($photo as $photoKey => $photoValue)\n+    {\n+      if(preg_match(\'/path(\\d+x\\d+)/\', $photoKey))\n+      {\n+        if(isset($sizekeys) && isset($sizekeys[$photoKey]))\n+          continue;\n+        unset($photo[$photoKey]);\n+      }\n+    }\n+    return $photo;\n+  }\n }\ndiff --git a/src/libraries/controllers/ApiTagController.php b/src/libraries/controllers/ApiTagController.php\nindex 0c495fe7..ce0bdb7c 100644\n--- a/src/libraries/controllers/ApiTagController.php\n+++ b/src/libraries/controllers/ApiTagController.php\n@@ -67,7 +67,15 @@ class ApiTagController extends BaseController\n     $filters = array();\n     if(User::isOwner())\n       $filters[\'permission\'] = 0;\n+\n+    $tagField = User::isOwner() ? \'countPrivate\' : \'countPublic\';\n+\n     $tags = getDb()->getTags($filters);\n+    foreach($tags as $key => $tag)\n+    {\n+      $tags[$key][\'count\'] = $tag[$tagField];\n+      unset($tags[$key][\'countPublic\'], $tags[$key][\'countPrivate\'], $tags[$key][\'owner\']);\n+    }\n     return self::success(\'Tags for the user\', $tags);\n   }\n }\ndiff --git a/src/libraries/controllers/ApiUserController.php b/src/libraries/controllers/ApiUserController.php\nindex 06dd08e7..facd21c0 100644\n--- a/src/libraries/controllers/ApiUserController.php\n+++ b/src/libraries/controllers/ApiUserController.php\n@@ -11,9 +11,9 @@ class ApiUserController extends BaseController\n     *\n     * @return string Standard JSON envelope\n     */\n-  public static function login()\n+  public static function login($provider = null)\n   {\n-    $wasUserLoggedIn = User::login($_POST[\'assertion\'], $_SERVER[\'HTTP_HOST\']);\n+    $wasUserLoggedIn = User::login($provider, $_POST);\n     if($wasUserLoggedIn)\n       return self::success(\'User was logged in successfully\', array(\'email\' => getSession()->get(\'email\')));\n     else\n@@ -36,7 +36,7 @@ class ApiUserController extends BaseController\n     User::setEmail($email);\n     User::setMobilePassphrase(true); // unset\n     if(isset($_POST[\'redirect\']))\n-      getRoute()->redirect($_POST[\'redirect\']);\n+      getRoute()->redirect($_POST[\'redirect\'], null, true);\n     else\n       return self::success(\'User was logged in successfully\', array(\'email\' => getSession()->get(\'email\')));\n   }\ndiff --git a/src/libraries/controllers/PhotoController.php b/src/libraries/controllers/PhotoController.php\nindex d68f116a..1109ad2c 100644\n--- a/src/libraries/controllers/PhotoController.php\n+++ b/src/libraries/controllers/PhotoController.php\n@@ -20,7 +20,7 @@ class PhotoController extends BaseController\n   {\n     $args = func_get_args();\n     // TODO, this should call a method in the API\n-    $photo = Photo::generateImage($id, $hash, $width, $height, $options);\n+    $photo = Photo::generate($id, $hash, $width, $height, $options);\n     // TODO return 404 graphic\n     if($photo)\n     {\n@@ -130,7 +130,8 @@ class PhotoController extends BaseController\n       return;\n     }\n     $crumb = getSession()->get(\'crumb\');\n-    $body = getTheme()->get(\'upload.php\', array(\'crumb\' => $crumb, \'licenses\' => Utility::getLicenses()));\n+    $template = sprintf(\'%s/upload.php\', getConfig()->get(\'paths\')->templates);\n+    $body = getTemplate()->get($template, array(\'crumb\' => $crumb, \'licenses\' => Utility::getLicenses()));\n     getTheme()->display(\'template.php\', array(\'body\' => $body, \'page\' => \'upload\'));\n   }\n \n@@ -153,16 +154,6 @@ class PhotoController extends BaseController\n       else\n         $apiNextPrevious = getApi()->invoke("/photo/{$id}/nextprevious/{$options}.json", EpiRoute::httpGet, array(\'_GET\' => array(\'returnSizes\' => getConfig()->get(\'photoSizes\')->nextPrevious)));\n       $photo = $apiResp[\'result\'];\n-      if($photo[\'width\'] <= $detailDimensions[0])\n-      {\n-        $photo[\'thisWidth\'] = $photo[\'width\'];\n-        $photo[\'thisHeight\'] = $photo[\'height\'];\n-      }\n-      else\n-      {\n-        $photo[\'thisWidth\'] = $detailDimensions[0];\n-        $photo[\'thisHeight\'] = intval($photo[\'height\']/$photo[\'width\']*$detailDimensions[0]);\n-      }\n       $photo[\'previous\'] = isset($apiNextPrevious[\'result\'][\'previous\']) ? $apiNextPrevious[\'result\'][\'previous\'] : null;\n       $photo[\'next\'] = isset($apiNextPrevious[\'result\'][\'next\']) ? $apiNextPrevious[\'result\'][\'next\'] : null;\n       $crumb = getSession()->get(\'crumb\');\ndiff --git a/src/libraries/controllers/SetupController.php b/src/libraries/controllers/SetupController.php\nindex 50fab92e..01457196 100644\n--- a/src/libraries/controllers/SetupController.php\n+++ b/src/libraries/controllers/SetupController.php\n@@ -542,7 +542,7 @@ class SetupController\n         if($usesAws)\n           $fsErrors[] = \'We were unable to initialize your S3 bucket.<ul><li>Make sure you\\\'re <a href="http://aws.amazon.com/s3/">signed up for AWS S3</a>.</li><li>Double check your AWS credentials.</li><li>S3 bucket names are globally unique, make sure yours isn\\\'t already in use by someone else.</li><li>S3 bucket names can\\\'t have certain special characters. Try using just alpha-numeric characters and periods.</li></ul>\';\n         else if($usesLocalFs)\n-          $fsErrors[] = "We were unable to set up your local file system using <em>{$fsObj->fsRoot}</em>. Make sure that the following user has proper permissions ({$serverUser}).";\n+          $fsErrors[] = "We were unable to set up your local file system using <em>{$fsObj->getRoot()}</em>. Make sure that the following user has proper permissions ({$serverUser}).";\n         else\n           $fsErrors[] = \'An unknown error occurred while setting up your file system. Check your error logs to see if there\\\'s more information about the error.\';\n       }\ndiff --git a/src/libraries/controllers/TagController.php b/src/libraries/controllers/TagController.php\nindex be63f01b..ba243a84 100644\n--- a/src/libraries/controllers/TagController.php\n+++ b/src/libraries/controllers/TagController.php\n@@ -15,9 +15,7 @@ class TagController extends BaseController\n   {\n     $tags = getApi()->invoke("/tags/list.json");\n     $groupedTags = Tag::groupByWeight($tags[\'result\']);\n-    $tagField = User::isOwner() ? \'countPrivate\' : \'countPublic\';\n-\n-    $body = getTheme()->get(Utility::getTemplate(\'tags.php\'), array(\'tags\' => $groupedTags, \'tagField\' => $tagField));\n+    $body = getTheme()->get(Utility::getTemplate(\'tags.php\'), array(\'tags\' => $groupedTags));\n     getTheme()->display(Utility::getTemplate(\'template.php\'), array(\'body\' => $body, \'page\' => \'tags\'));\n   }\n }\ndiff --git a/src/libraries/dependencies.php b/src/libraries/dependencies.php\nindex 66866562..39f9f409 100644\n--- a/src/libraries/dependencies.php\n+++ b/src/libraries/dependencies.php\n@@ -26,6 +26,9 @@ require getConfig()->get(\'paths\')->controllers . \'/WebhookController.php\';\n require getConfig()->get(\'paths\')->external . \'/aws/sdk.class.php\';\n require getConfig()->get(\'paths\')->external . \'/Dropbox/autoload.php\';\n require getConfig()->get(\'paths\')->external . \'/Mobile_Detect/Mobile_Detect.php\';\n+require getConfig()->get(\'paths\')->libraries . \'/functions.php\';\n+\n+// adapters\n require getConfig()->get(\'paths\')->adapters . \'/Database.php\';\n require getConfig()->get(\'paths\')->adapters . \'/DatabaseSimpleDb.php\';\n require getConfig()->get(\'paths\')->adapters . \'/DatabaseMySql.php\';\n@@ -35,7 +38,9 @@ require getConfig()->get(\'paths\')->adapters . \'/FileSystemS3Dropbox.php\';\n require getConfig()->get(\'paths\')->adapters . \'/FileSystemLocal.php\';\n require getConfig()->get(\'paths\')->adapters . \'/FileSystemLocalDropbox.php\';\n require getConfig()->get(\'paths\')->adapters . \'/FileSystemDropboxBase.php\';\n-require getConfig()->get(\'paths\')->libraries . \'/functions.php\';\n+require getConfig()->get(\'paths\')->adapters . \'/Login.php\';\n+require getConfig()->get(\'paths\')->adapters . \'/LoginBrowserId.php\';\n+require getConfig()->get(\'paths\')->adapters . \'/LoginFacebook.php\';\n \n // models\n require getConfig()->get(\'paths\')->models . \'/Utility.php\';\ndiff --git a/src/libraries/external/Dropbox/API.php b/src/libraries/external/Dropbox/API.php\nindex b91da914..7a072fc6 100644\n--- a/src/libraries/external/Dropbox/API.php\n+++ b/src/libraries/external/Dropbox/API.php\n@@ -28,7 +28,7 @@ class Dropbox_API {\n     /**\n      * Content API URl\n      */\n-    protected $api_content_url = \'api-content.dropbox.com/0/\';\n+    protected $api_content_url = \'api-content.dropbox.com/1/\';\n \n     /**\n      * OAuth object\ndiff --git a/src/libraries/external/facebook/base_facebook.php b/src/libraries/external/facebook/base_facebook.php\nnew file mode 100644\nindex 00000000..fcf3bff1\n--- /dev/null\n+++ b/src/libraries/external/facebook/base_facebook.php\n@@ -0,0 +1,1143 @@\n+<?php\n+/**\n+ * Copyright 2011 Facebook, Inc.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the "License"); you may\n+ * not use this file except in compliance with the License. You may obtain\n+ * 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, WITHOUT\n+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n+ * License for the specific language governing permissions and limitations\n+ * under the License.\n+ */\n+\n+if (!function_exists(\'curl_init\')) {\n+  throw new Exception(\'Facebook needs the CURL PHP extension.\');\n+}\n+if (!function_exists(\'json_decode\')) {\n+  throw new Exception(\'Facebook needs the JSON PHP extension.\');\n+}\n+\n+/**\n+ * Thrown when an API call returns an exception.\n+ *\n+ * @author Naitik Shah <naitik@facebook.com>\n+ */\n+class FacebookApiException extends Exception\n+{\n+  /**\n+   * The result from the API server that represents the exception information.\n+   */\n+  protected $result;\n+\n+  /**\n+   * Make a new API Exception with the given result.\n+   *\n+   * @param array $result The result from the API server\n+   */\n+  public function __construct($result) {\n+    $this->result = $result;\n+\n+    $code = isset($result[\'error_code\']) ? $result[\'error_code\'] : 0;\n+\n+    if (isset($result[\'error_description\'])) {\n+      // OAuth 2.0 Draft 10 style\n+      $msg = $result[\'error_description\'];\n+    } else if (isset($result[\'error\']) && is_array($result[\'error\'])) {\n+      // OAuth 2.0 Draft 00 style\n+      $msg = $result[\'error\'][\'message\'];\n+    } else if (isset($result[\'error_msg\'])) {\n+      // Rest server style\n+      $msg = $result[\'error_msg\'];\n+    } else {\n+      $msg = \'Unknown Error. Check getResult()\';\n+    }\n+\n+    parent::__construct($msg, $code);\n+  }\n+\n+  /**\n+   * Return the associated result object returned by the API server.\n+   *\n+   * @return array The result from the API server\n+   */\n+  public function getResult() {\n+    return $this->result;\n+  }\n+\n+  /**\n+   * Returns the associated type for the error. This will default to\n+   * \'Exception\' when a type is not available.\n+   *\n+   * @return string\n+   */\n+  public function getType() {\n+    if (isset($this->result[\'error\'])) {\n+      $error = $this->result[\'error\'];\n+      if (is_string($error)) {\n+        // OAuth 2.0 Draft 10 style\n+        return $error;\n+      } else if (is_array($error)) {\n+        // OAuth 2.0 Draft 00 style\n+        if (isset($error[\'type\'])) {\n+          return $error[\'type\'];\n+        }\n+      }\n+    }\n+\n+    return \'Exception\';\n+  }\n+\n+  /**\n+   * To make debugging easier.\n+   *\n+   * @return string The string representation of the error\n+   */\n+  public function __toString() {\n+    $str = $this->getType() . \': \';\n+    if ($this->code != 0) {\n+      $str .= $this->code . \': \';\n+    }\n+    return $str . $this->message;\n+  }\n+}\n+\n+/**\n+ * Provides access to the Facebook Platform.  This class provides\n+ * a majority of the functionality needed, but the class is abstract\n+ * because it is designed to be sub-classed.  The subclass must\n+ * implement the four abstract methods listed at the bottom of\n+ * the file.\n+ *\n+ * @author Naitik Shah <naitik@facebook.com>\n+ */\n+abstract class BaseFacebook\n+{\n+  /**\n+   * Version.\n+   */\n+  const VERSION = \'3.1.1\';\n+\n+  /**\n+   * Default options for curl.\n+   */\n+  public static $CURL_OPTS = array(\n+    CURLOPT_CONNECTTIMEOUT => 10,\n+    CURLOPT_RETURNTRANSFER => true,\n+    CURLOPT_TIMEOUT        => 60,\n+    CURLOPT_USERAGENT      => \'facebook-php-3.1\',\n+  );\n+\n+  /**\n+   * List of query parameters that get automatically dropped when rebuilding\n+   * the current URL.\n+   */\n+  protected static $DROP_QUERY_PARAMS = array(\n+    \'code\',\n+    \'state\',\n+    \'signed_request\',\n+  );\n+\n+  /**\n+   * Maps aliases to Facebook domains.\n+   */\n+  public static $DOMAIN_MAP = array(\n+    \'api\'       => \'https://api.facebook.com/\',\n+    \'api_video\' => \'https://api-video.facebook.com/\',\n+    \'api_read\'  => \'https://api-read.facebook.com/\',\n+    \'graph\'     => \'https://graph.facebook.com/\',\n+    \'www\'       => \'https://www.facebook.com/\',\n+  );\n+\n+  /**\n+   * The Application ID.\n+   *\n+   * @var string\n+   */\n+  protected $appId;\n+\n+  /**\n+   * The Application API Secret.\n+   *\n+   * @var string\n+   */\n+  protected $apiSecret;\n+\n+  /**\n+   * The ID of the Facebook user, or 0 if the user is logged out.\n+   *\n+   * @var integer\n+   */\n+  protected $user;\n+\n+  /**\n+   * The data from the signed_request token.\n+   */\n+  protected $signedRequest;\n+\n+  /**\n+   * A CSRF state variable to assist in the defense against CSRF attacks.\n+   */\n+  protected $state;\n+\n+  /**\n+   * The OAuth access token received in exchange for a valid authorization\n+   * code.  null means the access token has yet to be determined.\n+   *\n+   * @var string\n+   */\n+  protected $accessToken = null;\n+\n+  /**\n+   * Indicates if the CURL based @ syntax for file uploads is enabled.\n+   *\n+   * @var boolean\n+   */\n+  protected $fileUploadSupport = false;\n+\n+  /**\n+   * Initialize a Facebook Application.\n+   *\n+   * The configuration:\n+   * - appId: the application ID\n+   * - secret: the application secret\n+   * - fileUpload: (optional) boolean indicating if file uploads are enabled\n+   *\n+   * @param array $config The application configuration\n+   */\n+  public function __construct($config) {\n+    $this->setAppId($config[\'appId\']);\n+    $this->setApiSecret($config[\'secret\']);\n+    if (isset($config[\'fileUpload\'])) {\n+      $this->setFileUploadSupport($config[\'fileUpload\']);\n+    }\n+\n+    $state = $this->getPersistentData(\'state\');\n+    if (!empty($state)) {\n+      $this->state = $this->getPersistentData(\'state\');\n+    }\n+  }\n+\n+  /**\n+   * Set the Application ID.\n+   *\n+   * @param string $appId The Application ID\n+   * @return BaseFacebook\n+   */\n+  public function setAppId($appId) {\n+    $this->appId = $appId;\n+    return $this;\n+  }\n+\n+  /**\n+   * Get the Application ID.\n+   *\n+   * @return string the Application ID\n+   */\n+  public function getAppId() {\n+    return $this->appId;\n+  }\n+\n+  /**\n+   * Set the API Secret.\n+   *\n+   * @param string $apiSecret The API Secret\n+   * @return BaseFacebook\n+   */\n+  public function setApiSecret($apiSecret) {\n+    $this->apiSecret = $apiSecret;\n+    return $this;\n+  }\n+\n+  /**\n+   * Get the API Secret.\n+   *\n+   * @return string the API Secret\n+   */\n+  public function getApiSecret() {\n+    return $this->apiSecret;\n+  }\n+\n+  /**\n+   * Set the file upload support status.\n+   *\n+   * @param boolean $fileUploadSupport The file upload support status.\n+   * @return BaseFacebook\n+   */\n+  public function setFileUploadSupport($fileUploadSupport) {\n+    $this->fileUploadSupport = $fileUploadSupport;\n+    return $this;\n+  }\n+\n+  /**\n+   * Get the file upload support status.\n+   *\n+   * @return boolean true if and only if the server supports file upload.\n+   */\n+  public function useFileUploadSupport() {\n+    return $this->fileUploadSupport;\n+  }\n+\n+  /**\n+   * Sets the access token for api calls.  Use this if you get\n+   * your access token by other means and just want the SDK\n+   * to use it.\n+   *\n+   * @param string $access_token an access token.\n+   * @return BaseFacebook\n+   */\n+  public function setAccessToken($access_token) {\n+    $this->accessToken = $access_token;\n+    return $this;\n+  }\n+\n+  /**\n+   * Determines the access token that should be used for API calls.\n+   * The first time this is called, $this->accessToken is set equal\n+   * to either a valid user access token, or it\'s set to the application\n+   * access token if a valid user access token wasn\'t available.  Subsequent\n+   * calls return whatever the first call returned.\n+   *\n+   * @return string The access token\n+   */\n+  public function getAccessToken() {\n+    if ($this->accessToken !== null) {\n+      // we\'ve done this already and cached it.  Just return.\n+      return $this->accessToken;\n+    }\n+\n+    // first establish access token to be the application\n+    // access token, in case we navigate to the /oauth/access_token\n+    // endpoint, where SOME access token is required.\n+    $this->setAccessToken($this->getApplicationAccessToken());\n+    $user_access_token = $this->getUserAccessToken();\n+    if ($user_access_token) {\n+      $this->setAccessToken($user_access_token);\n+    }\n+\n+    return $this->accessToken;\n+  }\n+\n+  /**\n+   * Determines and returns the user access token, first using\n+   * the signed request if present, and then falling back on\n+   * the authorization code if present.  The intent is to\n+   * return a valid user access token, or false if one is determined\n+   * to not be available.\n+   *\n+   * @return string A valid user access token, or false if one\n+   *                could not be determined.\n+   */\n+  protected function getUserAccessToken() {\n+    // first, consider a signed request if it\'s supplied.\n+    // if there is a signed request, then it alone determines\n+    // the access token.\n+    $signed_request = $this->getSignedRequest();\n+    if ($signed_request) {\n+      // apps.facebook.com hands the access_token in the signed_request\n+      if (array_key_exists(\'oauth_token\', $signed_request)) {\n+        $access_token = $signed_request[\'oauth_token\'];\n+        $this->setPersistentData(\'access_token\', $access_token);\n+        return $access_token;\n+      }\n+\n+      // the JS SDK puts a code in with the redirect_uri of \'\'\n+      if (array_key_exists(\'code\', $signed_request)) {\n+        $code = $signed_request[\'code\'];\n+        $access_token = $this->getAccessTokenFromCode($code, \'\');\n+        if ($access_token) {\n+          $this->setPersistentData(\'code\', $code);\n+          $this->setPersistentData(\'access_token\', $access_token);\n+          return $access_token;\n+        }\n+      }\n+\n+      // signed request states there\'s no access token, so anything\n+      // stored should be cleared.\n+      $this->clearAllPersistentData();\n+      return false; // respect the signed request\'s data, even\n+                    // if there\'s an authorization code or something else\n+    }\n+\n+    $code = $this->getCode();\n+    if ($code && $code != $this->getPersistentData(\'code\')) {\n+      $access_token = $this->getAccessTokenFromCode($code);\n+      if ($access_token) {\n+        $this->setPersistentData(\'code\', $code);\n+        $this->setPersistentData(\'access_token\', $access_token);\n+        return $access_token;\n+      }\n+\n+      // code was bogus, so everything based on it should be invalidated.\n+      $this->clearAllPersistentData();\n+      return false;\n+    }\n+\n+    // as a fallback, just return whatever is in the persistent\n+    // store, knowing nothing explicit (signed request, authorization\n+    // code, etc.) was present to shadow it (or we saw a code in $_REQUEST,\n+    // but it\'s the same as what\'s in the persistent store)\n+    return $this->getPersistentData(\'access_token\');\n+  }\n+\n+  /**\n+   * Retrieve the signed request, either from a request parameter or,\n+   * if not present, from a cookie.\n+   *\n+   * @return string the signed request, if available, or null otherwise.\n+   */\n+  public function getSignedRequest() {\n+    if (!$this->signedRequest) {\n+      if (isset($_REQUEST[\'signed_request\'])) {\n+        $this->signedRequest = $this->parseSignedRequest(\n+          $_REQUEST[\'signed_request\']);\n+      } else if (isset($_COOKIE[$this->getSignedRequestCookieName()])) {\n+        $this->signedRequest = $this->parseSignedRequest(\n+          $_COOKIE[$this->getSignedRequestCookieName()]);\n+      }\n+    }\n+    return $this->signedRequest;\n+  }\n+\n+  /**\n+   * Get the UID of the connected user, or 0\n+   * if the Facebook user is not connected.\n+   *\n+   * @return string the UID if available.\n+   */\n+  public function getUser() {\n+    if ($this->user !== null) {\n+      // we\'ve already determined this and cached the value.\n+      return $this->user;\n+    }\n+\n+    return $this->user = $this->getUserFromAvailableData();\n+  }\n+\n+  /**\n+   * Determines the connected user by first examining any signed\n+   * requests, then considering an authorization code, and then\n+   * falling back to any persistent store storing the user.\n+   *\n+   * @return integer The id of the connected Facebook user,\n+   *                 or 0 if no such user exists.\n+   */\n+  protected function getUserFromAvailableData() {\n+    // if a signed request is supplied, then it solely determines\n+    // who the user is.\n+    $signed_request = $this->getSignedRequest();\n+    if ($signed_request) {\n+      if (array_key_exists(\'user_id\', $signed_request)) {\n+        $user = $signed_request[\'user_id\'];\n+        $this->setPersistentData(\'user_id\', $signed_request[\'user_id\']);\n+        return $user;\n+      }\n+\n+      // if the signed request didn\'t present a user id, then invalidate\n+      // all entries in any persistent store.\n+      $this->clearAllPersistentData();\n+      return 0;\n+    }\n+\n+    $user = $this->getPersistentData(\'user_id\', $default = 0);\n+    $persisted_access_token = $this->getPersistentData(\'access_token\');\n+\n+    // use access_token to fetch user id if we have a user access_token, or if\n+    // the cached access token has changed.\n+    $access_token = $this->getAccessToken();\n+    if ($access_token &&\n+        $access_token != $this->getApplicationAccessToken() &&\n+        !($user && $persisted_access_token == $access_token)) {\n+      $user = $this->getUserFromAccessToken();\n+      if ($user) {\n+        $this->setPersistentData(\'user_id\', $user);\n+      } else {\n+        $this->clearAllPersistentData();\n+      }\n+    }\n+\n+    return $user;\n+  }\n+\n+  /**\n+   * Get a Login URL for use with redirects. By default, full page redirect is\n+   * assumed. If you are using the generated URL with a window.open() call in\n+   * JavaScript, you can pass in display=popup as part of the $params.\n+   *\n+   * The parameters:\n+   * - redirect_uri: the url to go to after a successful login\n+   * - scope: comma separated list of requested extended perms\n+   *\n+   * @param array $params Provide custom parameters\n+   * @return string The URL for the login flow\n+   */\n+  public function getLoginUrl($params=array()) {\n+    $this->establishCSRFTokenState();\n+    $currentUrl = $this->getCurrentUrl();\n+\n+    // if \'scope\' is passed as an array, convert to comma separated list\n+    $scopeParams = isset($params[\'scope\']) ? $params[\'scope\'] : null;\n+    if ($scopeParams && is_array($scopeParams)) {\n+      $params[\'scope\'] = implode(\',\', $scopeParams);\n+    }\n+\n+    return $this->getUrl(\n+      \'www\',\n+      \'dialog/oauth\',\n+      array_merge(array(\n+                    \'client_id\' => $this->getAppId(),\n+                    \'redirect_uri\' => $currentUrl, // possibly overwritten\n+                    \'state\' => $this->state),\n+                  $params));\n+  }\n+\n+  /**\n+   * Get a Logout URL suitable for use with redirects.\n+   *\n+   * The parameters:\n+   * - next: the url to go to after a successful logout\n+   *\n+   * @param array $params Provide custom parameters\n+   * @return string The URL for the logout flow\n+   */\n+  public function getLogoutUrl($params=array()) {\n+    return $this->getUrl(\n+      \'www\',\n+      \'logout.php\',\n+      array_merge(array(\n+        \'next\' => $this->getCurrentUrl(),\n+        \'access_token\' => $this->getAccessToken(),\n+      ), $params)\n+    );\n+  }\n+\n+  /**\n+   * Get a login status URL to fetch the status from Facebook.\n+   *\n+   * The parameters:\n+   * - ok_session: the URL to go to if a session is found\n+   * - no_session: the URL to go to if the user is not connected\n+   * - no_user: the URL to go to if the user is not signed into facebook\n+   *\n+   * @param array $params Provide custom parameters\n+   * @return string The URL for the logout flow\n+   */\n+  public function getLoginStatusUrl($params=array()) {\n+    return $this->getUrl(\n+      \'www\',\n+      \'extern/login_status.php\',\n+      array_merge(array(\n+        \'api_key\' => $this->getAppId(),\n+        \'no_session\' => $this->getCurrentUrl(),\n+        \'no_user\' => $this->getCurrentUrl(),\n+        \'ok_session\' => $this->getCurrentUrl(),\n+        \'session_version\' => 3,\n+      ), $params)\n+    );\n+  }\n+\n+  /**\n+   * Make an API call.\n+   *\n+   * @return mixed The decoded response\n+   */\n+  public function api(/* polymorphic */) {\n+    $args = func_get_args();\n+    if (is_array($args[0])) {\n+      return $this->_restserver($args[0]);\n+    } else {\n+      return call_user_func_array(array($this, \'_graph\'), $args);\n+    }\n+  }\n+\n+  /**\n+   * Constructs and returns the name of the cookie that\n+   * potentially houses the signed request for the app user.\n+   * The cookie is not set by the BaseFacebook class, but\n+   * it may be set by the JavaScript SDK.\n+   *\n+   * @return string the name of the cookie that would house\n+   *         the signed request value.\n+   */\n+  protected function getSignedRequestCookieName() {\n+    return \'fbsr_\'.$this->getAppId();\n+  }\n+\n+  /**\n+   * Get the authorization code from the query parameters, if it exists,\n+   * and otherwise return false to signal no authorization code was\n+   * discoverable.\n+   *\n+   * @return mixed The authorization code, or false if the authorization\n+   *               code could not be determined.\n+   */\n+  protected function getCode() {\n+    if (isset($_REQUEST[\'code\'])) {\n+      if ($this->state !== null &&\n+          isset($_REQUEST[\'state\']) &&\n+          $this->state === $_REQUEST[\'state\']) {\n+\n+        // CSRF state has done its job, so clear it\n+        $this->state = null;\n+        $this->clearPersistentData(\'state\');\n+        return $_REQUEST[\'code\'];\n+      } else {\n+        self::errorLog(\'CSRF state token does not match one provided.\');\n+        return false;\n+      }\n+    }\n+\n+    return false;\n+  }\n+\n+  /**\n+   * Retrieves the UID with the understanding that\n+   * $this->accessToken has already been set and is\n+   * seemingly legitimate.  It relies on Facebook\'s Graph API\n+   * to retrieve user information and then extract\n+   * the user ID.\n+   *\n+   * @return integer Returns the UID of the Facebook user, or 0\n+   *                 if the Facebook user could not be determined.\n+   */\n+  protected function getUserFromAccessToken() {\n+    try {\n+      $user_info = $this->api(\'/me\');\n+      return $user_info[\'id\'];\n+    } catch (FacebookApiException $e) {\n+      return 0;\n+    }\n+  }\n+\n+  /**\n+   * Returns the access token that should be used for logged out\n+   * users when no authorization code is available.\n+   *\n+   * @return string The application access token, useful for gathering\n+   *                public information about users and applications.\n+   */\n+  protected function getApplicationAccessToken() {\n+    return $this->appId.\'|\'.$this->apiSecret;\n+  }\n+\n+  /**\n+   * Lays down a CSRF state token for this process.\n+   *\n+   * @return void\n+   */\n+  protected function establishCSRFTokenState() {\n+    if ($this->state === null) {\n+      $this->state = md5(uniqid(mt_rand(), true));\n+      $this->setPersistentData(\'state\', $this->state);\n+    }\n+  }\n+\n+  /**\n+   * Retrieves an access token for the given authorization code\n+   * (previously generated from www.facebook.com on behalf of\n+   * a specific user).  The authorization code is sent to graph.facebook.com\n+   * and a legitimate access token is generated provided the access token\n+   * and the user for which it was generated all match, and the user is\n+   * either logged in to Facebook or has granted an offline access permission.\n+   *\n+   * @param string $code An authorization code.\n+   * @return mixed An access token exchanged for the authorization code, or\n+   *               false if an access token could not be generated.\n+   */\n+  protected function getAccessTokenFromCode($code, $redirect_uri = null) {\n+    if (empty($code)) {\n+      return false;\n+    }\n+\n+    if ($redirect_uri === null) {\n+      $redirect_uri = $this->getCurrentUrl();\n+    }\n+\n+    try {\n+      // need to circumvent json_decode by calling _oauthRequest\n+      // directly, since response isn\'t JSON format.\n+      $access_token_response =\n+        $this->_oauthRequest(\n+          $this->getUrl(\'graph\', \'/oauth/access_token\'),\n+          $params = array(\'client_id\' => $this->getAppId(),\n+                          \'client_secret\' => $this->getApiSecret(),\n+                          \'redirect_uri\' => $redirect_uri,\n+                          \'code\' => $code));\n+    } catch (FacebookApiException $e) {\n+      // most likely that user very recently revoked authorization.\n+      // In any event, we don\'t have an access token, so say so.\n+      return false;\n+    }\n+\n+    if (empty($access_token_response)) {\n+      return false;\n+    }\n+\n+    $response_params = array();\n+    parse_str($access_token_response, $response_params);\n+    if (!isset($response_params[\'access_token\'])) {\n+      return false;\n+    }\n+\n+    return $response_params[\'access_token\'];\n+  }\n+\n+  /**\n+   * Invoke the old restserver.php endpoint.\n+   *\n+   * @param array $params Method call object\n+   *\n+   * @return mixed The decoded response object\n+   * @throws FacebookApiException\n+   */\n+  protected function _restserver($params) {\n+    // generic application level parameters\n+    $params[\'api_key\'] = $this->getAppId();\n+    $params[\'format\'] = \'json-strings\';\n+\n+    $result = json_decode($this->_oauthRequest(\n+      $this->getApiUrl($params[\'method\']),\n+      $params\n+    ), true);\n+\n+    // results are returned, errors are thrown\n+    if (is_array($result) && isset($result[\'error_code\'])) {\n+      $this->throwAPIException($result);\n+    }\n+\n+    if ($params[\'method\'] === \'auth.expireSession\' ||\n+        $params[\'method\'] === \'auth.revokeAuthorization\') {\n+      $this->destroySession();\n+    }\n+\n+    return $result;\n+  }\n+\n+  /**\n+   * Invoke the Graph API.\n+   *\n+   * @param string $path The path (required)\n+   * @param string $method The http method (default \'GET\')\n+   * @param array $params The query/post data\n+   *\n+   * @return mixed The decoded response object\n+   * @throws FacebookApiException\n+   */\n+  protected function _graph($path, $method = \'GET\', $params = array()) {\n+    if (is_array($method) && empty($params)) {\n+      $params = $method;\n+      $method = \'GET\';\n+    }\n+    $params[\'method\'] = $method; // method override as we always do a POST\n+\n+    $result = json_decode($this->_oauthRequest(\n+      $this->getUrl(\'graph\', $path),\n+      $params\n+    ), true);\n+\n+    // results are returned, errors are thrown\n+    if (is_array($result) && isset($result[\'error\'])) {\n+      $this->throwAPIException($result);\n+    }\n+\n+    return $result;\n+  }\n+\n+  /**\n+   * Make a OAuth Request.\n+   *\n+   * @param string $url The path (required)\n+   * @param array $params The query/post data\n+   *\n+   * @return string The decoded response object\n+   * @throws FacebookApiException\n+   */\n+  protected function _oauthRequest($url, $params) {\n+    if (!isset($params[\'access_token\'])) {\n+      $params[\'access_token\'] = $this->getAccessToken();\n+    }\n+\n+    // json_encode all params values that are not strings\n+    foreach ($params as $key => $value) {\n+      if (!is_string($value)) {\n+        $params[$key] = json_encode($value);\n+      }\n+    }\n+\n+    return $this->makeRequest($url, $params);\n+  }\n+\n+  /**\n+   * Makes an HTTP request. This method can be overridden by subclasses if\n+   * developers want to do fancier things or use something other than curl to\n+   * make the request.\n+   *\n+   * @param string $url The URL to make the request to\n+   * @param array $params The parameters to use for the POST body\n+   * @param CurlHandler $ch Initialized curl handle\n+   *\n+   * @return string The response text\n+   */\n+  protected function makeRequest($url, $params, $ch=null) {\n+    if (!$ch) {\n+      $ch = curl_init();\n+    }\n+\n+    $opts = self::$CURL_OPTS;\n+    if ($this->useFileUploadSupport()) {\n+      $opts[CURLOPT_POSTFIELDS] = $params;\n+    } else {\n+      $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, \'&\');\n+    }\n+    $opts[CURLOPT_URL] = $url;\n+\n+    // disable the \'Expect: 100-continue\' behaviour. This causes CURL to wait\n+    // for 2 seconds if the server does not support this header.\n+    if (isset($opts[CURLOPT_HTTPHEADER])) {\n+      $existing_headers = $opts[CURLOPT_HTTPHEADER];\n+      $existing_headers[] = \'Expect:\';\n+      $opts[CURLOPT_HTTPHEADER] = $existing_headers;\n+    } else {\n+      $opts[CURLOPT_HTTPHEADER] = array(\'Expect:\');\n+    }\n+\n+    curl_setopt_array($ch, $opts);\n+    $result = curl_exec($ch);\n+\n+    if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT\n+      self::errorLog(\'Invalid or no certificate authority found, \'.\n+                     \'using bundled information\');\n+      curl_setopt($ch, CURLOPT_CAINFO,\n+                  dirname(__FILE__) . \'/fb_ca_chain_bundle.crt\');\n+      $result = curl_exec($ch);\n+    }\n+\n+    if ($result === false) {\n+      $e = new FacebookApiException(array(\n+        \'error_code\' => curl_errno($ch),\n+        \'error\' => array(\n+        \'message\' => curl_error($ch),\n+        \'type\' => \'CurlException\',\n+        ),\n+      ));\n+      curl_close($ch);\n+      throw $e;\n+    }\n+    curl_close($ch);\n+    return $result;\n+  }\n+\n+  /**\n+   * Parses a signed_request and validates the signature.\n+   *\n+   * @param string $signed_request A signed token\n+   * @return array The payload inside it or null if the sig is wrong\n+   */\n+  protected function parseSignedRequest($signed_request) {\n+    list($encoded_sig, $payload) = explode(\'.\', $signed_request, 2);\n+\n+    // decode the data\n+    $sig = self::base64UrlDecode($encoded_sig);\n+    $data = json_decode(self::base64UrlDecode($payload), true);\n+\n+    if (strtoupper($data[\'algorithm\']) !== \'HMAC-SHA256\') {\n+      self::errorLog(\'Unknown algorithm. Expected HMAC-SHA256\');\n+      return null;\n+    }\n+\n+    // check sig\n+    $expected_sig = hash_hmac(\'sha256\', $payload,\n+                              $this->getApiSecret(), $raw = true);\n+    if ($sig !== $expected_sig) {\n+      self::errorLog(\'Bad Signed JSON signature!\');\n+      return null;\n+    }\n+\n+    return $data;\n+  }\n+\n+  /**\n+   * Build the URL for api given parameters.\n+   *\n+   * @param $method String the method name.\n+   * @return string The URL for the given parameters\n+   */\n+  protected function getApiUrl($method) {\n+    static $READ_ONLY_CALLS =\n+      array(\'admin.getallocation\' => 1,\n+            \'admin.getappproperties\' => 1,\n+            \'admin.getbannedusers\' => 1,\n+            \'admin.getlivestreamvialink\' => 1,\n+            \'admin.getmetrics\' => 1,\n+            \'admin.getrestrictioninfo\' => 1,\n+            \'application.getpublicinfo\' => 1,\n+            \'auth.getapppublickey\' => 1,\n+            \'auth.getsession\' => 1,\n+            \'auth.getsignedpublicsessiondata\' => 1,\n+            \'comments.get\' => 1,\n+            \'connect.getunconnectedfriendscount\' => 1,\n+            \'dashboard.getactivity\' => 1,\n+            \'dashboard.getcount\' => 1,\n+            \'dashboard.getglobalnews\' => 1,\n+            \'dashboard.getnews\' => 1,\n+            \'dashboard.multigetcount\' => 1,\n+            \'dashboard.multigetnews\' => 1,\n+            \'data.getcookies\' => 1,\n+            \'events.get\' => 1,\n+            \'events.getmembers\' => 1,\n+            \'fbml.getcustomtags\' => 1,\n+            \'feed.getappfriendstories\' => 1,\n+            \'feed.getregisteredtemplatebundlebyid\' => 1,\n+            \'feed.getregisteredtemplatebundles\' => 1,\n+            \'fql.multiquery\' => 1,\n+            \'fql.query\' => 1,\n+            \'friends.arefriends\' => 1,\n+            \'friends.get\' => 1,\n+            \'friends.getappusers\' => 1,\n+            \'friends.getlists\' => 1,\n+            \'friends.getmutualfriends\' => 1,\n+            \'gifts.get\' => 1,\n+            \'groups.get\' => 1,\n+            \'groups.getmembers\' => 1,\n+            \'intl.gettranslations\' => 1,\n+            \'links.get\' => 1,\n+            \'notes.get\' => 1,\n+            \'notifications.get\' => 1,\n+            \'pages.getinfo\' => 1,\n+            \'pages.isadmin\' => 1,\n+            \'pages.isappadded\' => 1,\n+            \'pages.isfan\' => 1,\n+            \'permissions.checkavailableapiaccess\' => 1,\n+            \'permissions.checkgrantedapiaccess\' => 1,\n+            \'photos.get\' => 1,\n+            \'photos.getalbums\' => 1,\n+            \'photos.gettags\' => 1,\n+            \'profile.getinfo\' => 1,\n+            \'profile.getinfooptions\' => 1,\n+            \'stream.get\' => 1,\n+            \'stream.getcomments\' => 1,\n+            \'stream.getfilters\' => 1,\n+            \'users.getinfo\' => 1,\n+            \'users.getloggedinuser\' => 1,\n+            \'users.getstandardinfo\' => 1,\n+            \'users.hasapppermission\' => 1,\n+            \'users.isappuser\' => 1,\n+            \'users.isverified\' => 1,\n+            \'video.getuploadlimits\' => 1);\n+    $name = \'api\';\n+    if (isset($READ_ONLY_CALLS[strtolower($method)])) {\n+      $name = \'api_read\';\n+    } else if (strtolower($method) == \'video.upload\') {\n+      $name = \'api_video\';\n+    }\n+    return self::getUrl($name, \'restserver.php\');\n+  }\n+\n+  /**\n+   * Build the URL for given domain alias, path and parameters.\n+   *\n+   * @param $name string The name of the domain\n+   * @param $path string Optional path (without a leading slash)\n+   * @param $params array Optional query parameters\n+   *\n+   * @return string The URL for the given parameters\n+   */\n+  protected function getUrl($name, $path=\'\', $params=array()) {\n+    $url = self::$DOMAIN_MAP[$name];\n+    if ($path) {\n+      if ($path[0] === \'/\') {\n+        $path = substr($path, 1);\n+      }\n+      $url .= $path;\n+    }\n+    if ($params) {\n+      $url .= \'?\' . http_build_query($params, null, \'&\');\n+    }\n+\n+    return $url;\n+  }\n+\n+  /**\n+   * Returns the Current URL, stripping it of known FB parameters that should\n+   * not persist.\n+   *\n+   * @return string The current URL\n+   */\n+  protected function getCurrentUrl() {\n+    if (isset($_SERVER[\'HTTPS\']) && ($_SERVER[\'HTTPS\'] == \'on\' || $_SERVER[\'HTTPS\'] == 1)\n+      || isset($_SERVER[\'HTTP_X_FORWARDED_PROTO\']) && $_SERVER[\'HTTP_X_FORWARDED_PROTO\'] == \'https\'\n+    ) {\n+      $protocol = \'https://\';\n+    }\n+    else {\n+      $protocol = \'http://\';\n+    }\n+    $currentUrl = $protocol . $_SERVER[\'HTTP_HOST\'] . $_SERVER[\'REQUEST_URI\'];\n+    $parts = parse_url($currentUrl);\n+\n+    $query = \'\';\n+    if (!empty($parts[\'query\'])) {\n+      // drop known fb params\n+      $params = explode(\'&\', $parts[\'query\']);\n+      $retained_params = array();\n+      foreach ($params as $param) {\n+        if ($this->shouldRetainParam($param)) {\n+          $retained_params[] = $param;\n+        }\n+      }\n+\n+      if (!empty($retained_params)) {\n+        $query = \'?\'.implode($retained_params, \'&\');\n+      }\n+    }\n+\n+    // use port if non default\n+    $port =\n+      isset($parts[\'port\']) &&\n+      (($protocol === \'http://\' && $parts[\'port\'] !== 80) ||\n+       ($protocol === \'https://\' && $parts[\'port\'] !== 443))\n+      ? \':\' . $parts[\'port\'] : \'\';\n+\n+    // rebuild\n+    return $protocol . $parts[\'host\'] . $port . $parts[\'path\'] . $query;\n+  }\n+\n+  /**\n+   * Returns true if and only if the key or key/value pair should\n+   * be retained as part of the query string.  This amounts to\n+   * a brute-force search of the very small list of Facebook-specific\n+   * params that should be stripped out.\n+   *\n+   * @param string $param A key or key/value pair within a URL\'s query (e.g.\n+   *                     \'foo=a\', \'foo=\', or \'foo\'.\n+   *\n+   * @return boolean\n+   */\n+  protected function shouldRetainParam($param) {\n+    foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) {\n+      if (strpos($param, $drop_query_param.\'=\') === 0) {\n+        return false;\n+      }\n+    }\n+\n+    return true;\n+  }\n+\n+  /**\n+   * Analyzes the supplied result to see if it was thrown\n+   * because the access token is no longer valid.  If that is\n+   * the case, then the persistent store is cleared.\n+   *\n+   * @param $result array A record storing the error message returned\n+   *                      by a failed API call.\n+   */\n+  protected function throwAPIException($result) {\n+    $e = new FacebookApiException($result);\n+    switch ($e->getType()) {\n+      // OAuth 2.0 Draft 00 style\n+      case \'OAuthException\':\n+        // OAuth 2.0 Draft 10 style\n+      case \'invalid_token\':\n+        // REST server errors are just Exceptions\n+      case \'Exception\':\n+        $message = $e->getMessage();\n+      if ((strpos($message, \'Error validating access token\') !== false) ||\n+          (strpos($message, \'Invalid OAuth access token\') !== false)) {\n+        $this->setAccessToken(null);\n+        $this->user = 0;\n+        $this->clearAllPersistentData();\n+      }\n+    }\n+\n+    throw $e;\n+  }\n+\n+\n+  /**\n+   * Prints to the error log if you aren\'t in command line mode.\n+   *\n+   * @param string $msg Log message\n+   */\n+  protected static function errorLog($msg) {\n+    // disable error log if we are running in a CLI environment\n+    // @codeCoverageIgnoreStart\n+    if (php_sapi_name() != \'cli\') {\n+      error_log($msg);\n+    }\n+    // uncomment this if you want to see the errors on the page\n+    // print \'error_log: \'.$msg."\\n";\n+    // @codeCoverageIgnoreEnd\n+  }\n+\n+  /**\n+   * Base64 encoding that doesn\'t need to be urlencode()ed.\n+   * Exactly the same as base64_encode except it uses\n+   *   - instead of +\n+   *   _ instead of /\n+   *\n+   * @param string $input base64UrlEncoded string\n+   * @return string\n+   */\n+  protected static function base64UrlDecode($input) {\n+    return base64_decode(strtr($input, \'-_\', \'+/\'));\n+  }\n+\n+  /**\n+   * Destroy the current session\n+   */\n+  public function destroySession() {\n+    $this->setAccessToken(null);\n+    $this->user = 0;\n+    $this->clearAllPersistentData();\n+  }\n+\n+  /**\n+   * Each of the following four methods should be overridden in\n+   * a concrete subclass, as they are in the provided Facebook class.\n+   * The Facebook class uses PHP sessions to provide a primitive\n+   * persistent store, but another subclass--one that you implement--\n+   * might use a database, memcache, or an in-memory cache.\n+   *\n+   * @see Facebook\n+   */\n+\n+  /**\n+   * Stores the given ($key, $value) pair, so that future calls to\n+   * getPersistentData($key) return $value. This call may be in another request.\n+   *\n+   * @param string $key\n+   * @param array $value\n+   *\n+   * @return void\n+   */\n+  abstract protected function setPersistentData($key, $value);\n+\n+  /**\n+   * Get the data for $key, persisted by BaseFacebook::setPersistentData()\n+   *\n+   * @param string $key The key of the data to retrieve\n+   * @param boolean $default The default value to return if $key is not found\n+   *\n+   * @return mixed\n+   */\n+  abstract protected function getPersistentData($key, $default = false);\n+\n+  /**\n+   * Clear the data with $key from the persistent storage\n+   *\n+   * @param string $key\n+   * @return void\n+   */\n+  abstract protected function clearPersistentData($key);\n+\n+  /**\n+   * Clear all data from the persistent storage\n+   *\n+   * @return void\n+   */\n+  abstract protected function clearAllPersistentData();\n+}\ndiff --git a/src/libraries/external/facebook/facebook.php b/src/libraries/external/facebook/facebook.php\nnew file mode 100644\nindex 00000000..c577c2aa\n--- /dev/null\n+++ b/src/libraries/external/facebook/facebook.php\n@@ -0,0 +1,93 @@\n+<?php\n+/**\n+ * Copyright 2011 Facebook, Inc.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the "License"); you may\n+ * not use this file except in compliance with the License. You may obtain\n+ * 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, WITHOUT\n+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n+ * License for the specific language governing permissions and limitations\n+ * under the License.\n+ */\n+\n+require_once "base_facebook.php";\n+\n+/**\n+ * Extends the BaseFacebook class with the intent of using\n+ * PHP sessions to store user ids and access tokens.\n+ */\n+class Facebook extends BaseFacebook\n+{\n+  /**\n+   * Identical to the parent constructor, except that\n+   * we start a PHP session to store the user ID and\n+   * access token if during the course of execution\n+   * we discover them.\n+   *\n+   * @param Array $config the application configuration.\n+   * @see BaseFacebook::__construct in facebook.php\n+   */\n+  public function __construct($config) {\n+    if (!session_id()) {\n+      session_start();\n+    }\n+    parent::__construct($config);\n+  }\n+\n+  protected static $kSupportedKeys =\n+    array(\'state\', \'code\', \'access_token\', \'user_id\');\n+\n+  /**\n+   * Provides the implementations of the inherited abstract\n+   * methods.  The implementation uses PHP sessions to maintain\n+   * a store for authorization codes, user ids, CSRF states, and\n+   * access tokens.\n+   */\n+  protected function setPersistentData($key, $value) {\n+    if (!in_array($key, self::$kSupportedKeys)) {\n+      self::errorLog(\'Unsupported key passed to setPersistentData.\');\n+      return;\n+    }\n+\n+    $session_var_name = $this->constructSessionVariableName($key);\n+    $_SESSION[$session_var_name] = $value;\n+  }\n+\n+  protected function getPersistentData($key, $default = false) {\n+    if (!in_array($key, self::$kSupportedKeys)) {\n+      self::errorLog(\'Unsupported key passed to getPersistentData.\');\n+      return $default;\n+    }\n+\n+    $session_var_name = $this->constructSessionVariableName($key);\n+    return isset($_SESSION[$session_var_name]) ?\n+      $_SESSION[$session_var_name] : $default;\n+  }\n+\n+  protected function clearPersistentData($key) {\n+    if (!in_array($key, self::$kSupportedKeys)) {\n+      self::errorLog(\'Unsupported key passed to clearPersistentData.\');\n+      return;\n+    }\n+\n+    $session_var_name = $this->constructSessionVariableName($key);\n+    unset($_SESSION[$session_var_name]);\n+  }\n+\n+  protected function clearAllPersistentData() {\n+    foreach (self::$kSupportedKeys as $key) {\n+      $this->clearPersistentData($key);\n+    }\n+  }\n+\n+  protected function constructSessionVariableName($key) {\n+    return implode(\'_\', array(\'fb\',\n+                              $this->getAppId(),\n+                              $key));\n+  }\n+}\ndiff --git a/src/libraries/models/ImageGD.php b/src/libraries/models/ImageGD.php\nindex 02b3264f..04537b0c 100644\n--- a/src/libraries/models/ImageGD.php\n+++ b/src/libraries/models/ImageGD.php\n@@ -34,7 +34,13 @@ class ImageGD implements ImageInterface\n     */\n   public function __construct($filename)\n   {\n-    $this->image = @imagecreatefromjpeg($filename);\n+    if(preg_match(\'/\\.png^/\', $filename))\n+      $this->image = @imagecreatefrompng($filename);\n+    elseif(preg_match(\'/\\.gif^/\', $filename))\n+      $this->image = @imagecreatefromgif($filename);\n+    else\n+      $this->image = @imagecreatefromjpeg($filename);\n+\n     if(!$this->image)\n       OPException::raise(new OPInvalidImageException(\'Could not create jpeg with GD library\'));\n \t\t$this->width = imagesx($this->image);\n@@ -134,6 +140,11 @@ class ImageGD implements ImageInterface\n     */\n   public function write($outputFile)\n   {\n-    imagejpeg($this->image, $outputFile, 90);\n+    if(preg_match(\'/\\.png^/\', $outputFile))\n+      imagepng($this->image, $outputFile, 90);\n+    elseif(preg_match(\'/\\.gif^/\', $outputFile))\n+      imagegif($this->image, $outputFile, 90);\n+    else\n+      imagejpeg($this->image, $outputFile, 90);\n   }\n }\ndiff --git a/src/libraries/models/Photo.php b/src/libraries/models/Photo.php\nindex cad04669..83d6dbbf 100644\n--- a/src/libraries/models/Photo.php\n+++ b/src/libraries/models/Photo.php\n@@ -8,6 +8,33 @@\n  */\n class Photo\n {\n+  /**\n+    * Adds the urls for the photo.\n+    * pathWxH is a static resource.\n+    * photoWxH is an enumerated array [path, width, height]\n+    *\n+    * @param array $photo the photo object\n+    * @param array $options Options for the photo such as crop (CR) and greyscale (BW)\n+    * @param string $protocol http or https\n+    * @return array\n+    */\n+  public static function addApiUrls($photo, $options, $protocol)\n+  {\n+    $size = self::generateFragment($options[\'width\'], $options[\'height\'], $options[\'options\']);\n+    $path = self::generateUrlPublic($photo, $options[\'width\'], $options[\'height\'], $options[\'options\'], $protocol);\n+    $photo["path{$size}"] = $path;\n+    if(strstr($size, \'xCR\') === false)\n+    {\n+      $dimensions = self::getRealDimensions($photo[\'width\'], $photo[\'height\'], $options[\'width\'], $options[\'height\']);\n+      $photo["photo{$size}"] = array($path, $dimensions[\'width\'], $dimensions[\'height\']);\n+    }\n+    else\n+    {\n+      $photo["photo{$size}"] = array($path, $options[\'width\'], $options[\'height\']);\n+    }\n+    return $photo;\n+  }\n+\n   /**\n     * Delete a photo from the remote database and remote filesystem.\n     * This deletes the original photo and all versions.\n@@ -57,6 +84,58 @@ class Photo\n     return $fragment;\n   }\n \n+  /**\n+    * Generate a version of the photo as specified by the width, height and options.\n+    * This method requres the $hash ve validated to keep random versions of images to be created.\n+    * The photo is generated, uploaded to the remote file system and added to the database.\n+    * Operations are done in place on a downloaded version of the base photo and this file name is returned.\n+    *\n+    * @param string $id The id of the photo.\n+    * @param int $width The width of the requested photo.\n+    * @param int $height The height of the requested photo.\n+    * @param string $options Optional options to be applied on the photo\n+    * @return mixed string on success, false on failure\n+    */\n+  public static function generate($id, $hash, $width, $height, $options = null)\n+  {\n+    if(!self::isValidateHash($hash, $id, $width, $height, $options))\n+      return false;\n+\n+    $photo = getDb()->getPhoto($id);\n+    $filename = getFs()->getPhoto($photo[\'pathBase\']);\n+    $image = getImage($filename);\n+    $maintainAspectRatio = true;\n+    if(!empty($options))\n+    {\n+      $optionsArray = (array)explode(\'x\', $options);\n+      foreach($optionsArray as $option)\n+      {\n+        switch($option)\n+        {\n+          case \'BW\':\n+            $image->greyscale();\n+            break;\n+          case \'CR\':\n+            $maintainAspectRatio = false;\n+            break;\n+        }\n+      }\n+    }\n+\n+    $image->scale($width, $height, $maintainAspectRatio);\n+\n+    $image->write($filename);\n+    $customPath = self::generateCustomUrl($photo[\'pathBase\'], $width, $height, $options);\n+    $key = self::generateCustomKey($width, $height, $options);\n+    $resFs = getFs()->putPhoto($filename, $customPath);\n+    $resDb = getDb()->postPhoto($id, array($key => $customPath));\n+    // TODO unlink $filename\n+    if($resFs && $resDb)\n+      return $filename;\n+\n+    return false;\n+  }\n+\n   /**\n     * Does the opposite of self::generateFragment.\n     * Given a string fragment this will return it\'s parts as an array.\n@@ -153,56 +232,33 @@ class Photo\n   }\n \n   /**\n-    * Generate a version of the photo as specified by the width, height and options.\n-    * This method requres the $hash ve validated to keep random versions of images to be created.\n-    * The photo is generated, uploaded to the remote file system and added to the database.\n-    * Operations are done in place on a downloaded version of the base photo and this file name is returned.\n+    * Calculate the width and height of a scaled photo\n     *\n-    * @param string $id The id of the photo.\n-    * @param int $width The width of the requested photo.\n-    * @param int $height The height of the requested photo.\n-    * @param string $options Optional options to be applied on the photo\n-    * @return mixed string on success, false on failure\n+    * @param int $originalWidth The width of the original photo.\n+    * @param int $originalHeight The height of the original photo.\n+    * @param int $newWidth The width of the new photo.\n+    * @param int $newHeight The height of the new photo.\n+    * @return array\n     */\n-  // TODO change name to generate()\n-  public static function generateImage($id, $hash, $width, $height, $options = null)\n+  public static function getRealDimensions($originalWidth, $originalHeight, $newWidth, $newHeight)\n   {\n-    if(!self::isValidateHash($hash, $id, $width, $height, $options))\n-      return false;\n+    if(empty($originalWidth) || empty($originalHeight))\n+      return array(\'width\' => $newWidth, \'height\' => $newHeight);\n \n-    $photo = getDb()->getPhoto($id);\n-    $filename = getFs()->getPhoto($photo[\'pathBase\']);\n-    $image = getImage($filename);\n-    $maintainAspectRatio = true;\n-    if(!empty($options))\n+    $originalRatio = $originalWidth / $originalHeight;\n+    $newRatio = $newWidth / $newHeight;\n+\n+    if($originalRatio <= $newRatio)\n     {\n-      $optionsArray = (array)explode(\'x\', $options);\n-      foreach($optionsArray as $option)\n-      {\n-        switch($option)\n-        {\n-          case \'BW\':\n-            $image->greyscale();\n-            break;\n-          case \'CR\':\n-            $maintainAspectRatio = false;\n-            break;\n-        }\n-      }\n+      $height = $newHeight;\n+      $width = $newHeight / (1 / $originalRatio);\n     }\n-\n-    $image->scale($width, $height, $maintainAspectRatio);\n-\n-    $image->write($filename);\n-    $customPath = self::generateCustomUrl($photo[\'pathBase\'], $width, $height, $options);\n-    $key = self::generateCustomKey($width, $height, $options);\n-    $resFs = getFs()->putPhoto($filename, $customPath);\n-    $resDb = getDb()->postPhoto($id, array($key => $customPath));\n-    // TODO unlink $filename\n-    if($resFs && $resDb)\n-      return $filename;\n-\n-    return false;\n+    else\n+    {\n+      $width = $newWidth;\n+      $height = $newWidth / $originalRatio;\n+    }\n+    return array(\'width\' => $width, \'height\' => $height);\n   }\n \n   /** \n@@ -233,7 +289,8 @@ class Photo\n   {\n     $exif = @exif_read_data($photo);\n     if(!$exif)\n-      return null;\n+      $exif = array();\n+\n     $size = getimagesize($photo);\n     // DateTimeOriginal is the right thing. If it is not there\n     // use DateTime which might be the date the photo was modified\n@@ -242,7 +299,12 @@ class Photo\n     {\n         $parsedDate = self::parseExifDate($exif, \'DateTime\');    \n \tif($parsedDate === false)\n-\t    $parsedDate = $exif[\'FileDateTime\'];\n+\t{\n+\t    if(array_key_exists(\'FileDateTime\', $exif))\n+\t        $parsedDate = $exif[\'FileDateTime\'];\n+            else\n+                $parsedDate = time();\n+        }\n     }\n     $dateTaken = $parsedDate;    \n \ndiff --git a/src/libraries/models/Plugin.php b/src/libraries/models/Plugin.php\nindex 31530319..237c960b 100644\n--- a/src/libraries/models/Plugin.php\n+++ b/src/libraries/models/Plugin.php\n@@ -18,10 +18,25 @@ class Plugin\n \n   public function invoke($action, $params = null)\n   {\n+    $output = \'\';\n     foreach($this->pluginInstances as $instance)\n     {\n-      $instance->$action($params);\n+      $output .= (string)$instance->$action($params);\n     }\n+\n+    if($output != \'\')\n+      echo $output;\n+  }\n+\n+  public function isActive($plugin)\n+  {\n+    foreach($this->getActive() as $p)\n+    {\n+      if($plugin == $p)\n+        return true;\n+    }\n+\n+    return false;\n   }\n \n   private function getActive()\n@@ -47,7 +62,6 @@ class Plugin\n   {\n     if(empty($this->pluginDir) || !is_dir($this->pluginDir))\n       return array();\n-\n     $dir = dir($this->pluginDir);\n     $plugins = array();\n     while (($name = $dir->read()) !== false)\ndiff --git a/src/libraries/models/PluginBase.php b/src/libraries/models/PluginBase.php\nindex a543b78c..8ed90373 100644\n--- a/src/libraries/models/PluginBase.php\n+++ b/src/libraries/models/PluginBase.php\n@@ -16,9 +16,14 @@ class PluginBase\n     getLogger()->info(\'Plugin onAction called\');\n   }\n \n-  public function onBody()\n+  public function onBodyBegin()\n   {\n-    getLogger()->info(\'Plugin onBody called\');\n+    getLogger()->info(\'Plugin onBodyBegin called\');\n+  }\n+\n+  public function onBodyBeginEnd()\n+  {\n+    getLogger()->info(\'Plugin onBodyEnd called\');\n   }\n \n   public function onHead()\ndiff --git a/src/libraries/models/Tag.php b/src/libraries/models/Tag.php\nindex 8e8d081a..71c79fb7 100644\n--- a/src/libraries/models/Tag.php\n+++ b/src/libraries/models/Tag.php\n@@ -105,7 +105,6 @@ class Tag\n     */\n   public static function groupByWeight($tags = null)\n   {\n-    $tagField = User::isOwner() ? \'countPrivate\' : \'countPublic\';\n     if($tags === null)\n     {\n       $tags = getApi()->invoke("/tags/list.json");\n@@ -118,10 +117,10 @@ class Tag\n     {\n       foreach($tags as $tag)\n       {\n-        if($tag[$tagField] < $minTags)\n-          $minTags = $tag[$tagField];\n-        if($tag[$tagField] > $maxTags)\n-          $maxTags = $tag[$tagField];\n+        if($tag[\'count\'] < $minTags)\n+          $minTags = $tag[\'count\'];\n+        if($tag[\'count\'] > $maxTags)\n+          $maxTags = $tag[\'count\'];\n       }\n \n       // we create 10 groups based on count using %s\n@@ -129,7 +128,7 @@ class Tag\n       // step needs to be float so we don\'t divide by zero\n       $step = floatval($range / 9);\n       foreach($tags as $key => $tag)\n-        $tags[$key][\'weight\'] = intval(($tag[$tagField]-$minTags) / $step)+1;\n+        $tags[$key][\'weight\'] = intval(($tag[\'count\']-$minTags) / $step)+1;\n     }\n     return $tags;\n   }\ndiff --git a/src/libraries/models/User.php b/src/libraries/models/User.php\nindex 702adbdd..f9d4e1aa 100644\n--- a/src/libraries/models/User.php\n+++ b/src/libraries/models/User.php\n@@ -168,23 +168,13 @@ class User\n     * @param string $audience Audience from BrowserID.org\n     * @return boolean\n     */\n-  public static function login($assertion, $audience)\n+  public static function login($provider, $params)\n   {\n-    $ch = curl_init(\'https://browserid.org/verify\');\n-    $params = array(\'assertion\' => $assertion, \'audience\' => $audience);\n-    curl_setopt($ch, CURLOPT_POST, 1);\n-    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));\n-    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n-    curl_setopt($ch, CURLOPT_CAINFO, getConfig()->get(\'paths\')->configs.\'/ca-bundle.crt\');\n-    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);\n-    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);\n-\n-    $response = curl_exec($ch);\n-    $response = json_decode($response, 1);\n-    if(!isset($response[\'status\']) || $response[\'status\'] != \'okay\')\n+    $email = getLogin($provider)->verifyEmail($params);\n+    if($email === false)\n       return false;\n \n-    self::setEmail($response[\'email\']);\n+    self::setEmail($email);\n     return true;\n   }\n \ndiff --git a/src/libraries/plugins/FacebookConnectPlugin.php b/src/libraries/plugins/FacebookConnectPlugin.php\nnew file mode 100644\nindex 00000000..fc4499f8\n--- /dev/null\n+++ b/src/libraries/plugins/FacebookConnectPlugin.php\n@@ -0,0 +1,45 @@\n+<?php\n+/**\n+ * FacebookConnect\n+ *\n+ * @author Jaisen Mathai <jaisen@jmathai.com>\n+ */\n+class FacebookConnectPlugin extends PluginBase\n+{\n+  private $appId;\n+  public function __construct()\n+  {\n+    parent::__construct();\n+    $this->appId = \'232147993517254\'; // test account, no one cares\n+  }\n+\n+  public function onBodyEnd($params)\n+  {\n+    parent::onAction($params);\n+    return <<<MKP\n+  <div id="fb-root"></div>\n+  <script>\n+    window.fbAsyncInit = function() {\n+      FB.init({\n+        appId      : \'{$this->appId}\', // App ID\n+        status     : true, // check login status\n+        cookie     : true, // enable cookies to allow the server to access the session\n+        oauth      : true, // enable OAuth 2.0\n+        xfbml      : true  // parse XFBML\n+      });\n+\n+      // Additional initialization code here\n+    };\n+\n+    // Load the SDK Asynchronously\n+    (function(d){\n+       var js, id = \'facebook-jssdk\'; if (d.getElementById(id)) {return;}\n+       js = d.createElement(\'script\'); js.id = id; js.async = true;\n+       js.src = "//connect.facebook.net/en_US/all.js";\n+       d.getElementsByTagName(\'head\')[0].appendChild(js);\n+     }(document));\n+  </script>\n+MKP;\n+  }\n+}\n+\ndiff --git a/src/libraries/routes-api.php b/src/libraries/routes-api.php\nindex 04c225cb..52f5fa1f 100644\n--- a/src/libraries/routes-api.php\n+++ b/src/libraries/routes-api.php\n@@ -46,9 +46,9 @@ getApi()->get(\'/tags/list.json\', array(\'ApiTagController\', \'list_\'), EpiApi::ext\n  * User endpoints\n  * All user endpoints follow the same convention.\n  * Everything in []\'s are optional\n- * /user/{action}.json\n+ * /user[/{provider}]/{action}.json\n  */\n-getApi()->post(\'/user/login.json\', array(\'ApiUserController\', \'login\'), EpiApi::external);\n+getApi()->post(\'/user/([a-z0-9]+)/login.json\', array(\'ApiUserController\', \'login\'), EpiApi::external);\n getApi()->post(\'/user/login/mobile.json\', array(\'ApiUserController\', \'loginMobile\'), EpiApi::external);\n getApi()->get(\'/user/logout.json\', array(\'ApiUserController\', \'logout\'), EpiApi::external);\n \ndiff --git a/src/html/assets/themes/beisel/templates/upload.php b/src/templates/upload.php\nsimilarity index 74%\nrename from src/html/assets/themes/beisel/templates/upload.php\nrename to src/templates/upload.php\nindex 20d99496..ddef2d0d 100644\n--- a/src/html/assets/themes/beisel/templates/upload.php\n+++ b/src/templates/upload.php\n@@ -1,9 +1,5 @@\n-<div class="headerbar">\n-\tUpload <!-- function bar for e.g. sharing function -->\n-</div>\n-\n-<div class="message upload-complete"><img src="<?php getTheme()->asset(\'image\', \'checkmark-big.gif\'); ?>" align="absmiddle">All done! Go have a <a href="<?php Url::photosView(); ?>">look at your photos</a>.</div>\n-<div class="message upload-progress"><img src="<?php getTheme()->asset(\'image\', \'upload-big.gif\'); ?>" align="absmiddle">Currently uploading <span class="completed">0</span> of <span class="total">0</span> photos.</div>\n+<div class="message upload-complete"><img src="/assets/images/checkmark-big.gif" align="absmiddle">All done! Go have a <a href="<?php Url::photosView(); ?>">look at your photos</a>.</div>\n+<div class="message upload-progress"><img src="/assets/images/upload-big.gif" align="absmiddle">Currently uploading <span class="completed">0</span> of <span class="total">0</span> photos.</div>\n <form class="upload">\n   <div class="options">\n     <h2>Apply these settings to my photos.</h2>\n', 'commit_subject': "Merge commit 'upstream/master' Should also address issue #352", 'repo_name': 'photo/frontend', 'committer': {'name': 'Hubert Figuiere', 'email': '65acf0a7ced564a4880cf946224e60b745d3d631@figuiere.net', 'time_sec': '1322985143', 'tz_offset': '-480', 'date': {'seconds': '1322985143'}}, 'commit_tree': 'd318b256ad68edcd1f96d19da3d976e58b1e2127', 'commit_message': "Merge commit 'upstream/master'\nShould also address issue #352\n\nConflicts:\n\n\tsrc/libraries/models/Photo.php\n", 'language_name': 'Shell', 'license': 'apache-2.0', 'hunks': '@@ -1,9 +1,5 @@\n-<div class="headerbar">\n-\tUpload <!-- function bar for e.g. sharing function -->\n-</div>\n-\n-<div class="message upload-complete"><img src="<?php getTheme()->asset(\'image\', \'checkmark-big.gif\'); ?>" align="absmiddle">All done! Go have a <a href="<?php Url::photosView(); ?>">look at your photos</a>.</div>\n-<div class="message upload-progress"><img src="<?php getTheme()->asset(\'image\', \'upload-big.gif\'); ?>" align="absmiddle">Currently uploading <span class="completed">0</span> of <span class="total">0</span> photos.</div>\n+<div class="message upload-complete"><img src="/assets/images/checkmark-big.gif" align="absmiddle">All done! Go have a <a href="<?php Url::photosView(); ?>">look at your photos</a>.</div>\n+<div class="message upload-progress"><img src="/assets/images/upload-big.gif" align="absmiddle">Currently uploading <span class="completed">0</span> of <span class="total">0</span> photos.</div>\n <form class="upload">\n   <div class="options">\n     <h2>Apply these settings to my photos.</h2>\n', 'addition_count': 2, 'deletion_count': 6, 'src_file': 'a/src/html/assets/themes/beisel/templates/upload.php', 'tgt_file': 'b/src/templates/upload.php', 'file_extension': '.php', 'before_file': '<div class="headerbar">\n\tUpload <!-- function bar for e.g. sharing function -->\n</div>\n\n<div class="message upload-complete"><img src="<?php getTheme()->asset(\'image\', \'checkmark-big.gif\'); ?>" align="absmiddle">All done! Go have a <a href="<?php Url::photosView(); ?>">look at your photos</a>.</div>\n<div class="message upload-progress"><img src="<?php getTheme()->asset(\'image\', \'upload-big.gif\'); ?>" align="absmiddle">Currently uploading <span class="completed">0</span> of <span class="total">0</span> photos.</div>\n<form class="upload">\n  <div class="options">\n    <h2>Apply these settings to my photos.</h2>\n    <div class="row">\n      <br>\n      <div class="span8">\n        <h3>Upload from your computer</h3>\n        <div id="uploader">\n          <div class="insufficient">\n            <h1>Unfortunately, it doesn\'t look like we support your browser. :(</h1>\n            <p>\n              Try using <a href="http://www.mozilla.org/en-US/firefox/new/">Firefox</a>.\n            </p>\n          </div>\n        </div>\n        <?php if(!empty($token)) { ?>\n          <div class="btn-toolbar visible-phone">\n            <button type="submit" class="btn btn-brand upload-button addSpinner wide">Start uploading</button>\n          </div>\n        <?php } ?>\n        <em class="poweredby hidden-phone">Powered by <a href="http://www.plupload.com">Plupload</a>.</em>\n      </div>\n      <div class="span4 <?php if(!empty($token)) { ?>hide<?php } ?>">\n        <h3>Use these settings.</h3>\n        <br>\n        <label for="tags">Tags</label>\n        <input type="search" name="tags" class="typeahead tags" autocomplete="off" placeholder="Optional comma separated list">\n\n        <div class="control-group">\n          <label class="control-label">Albums <small>(<a href="#" class="showBatchForm album" data-action="albums">create new</a>)</small></label>\n          <select data-placeholder="Select albums for these photos" name="albums" class="typeahead">\n            <option value="">If you\'d like, choose an album</option>\n            <?php foreach($albums as $album) { ?>\n              <option value="<?php $this->utility->safe($album[\'id\']); ?>"><?php $this->utility->safe($album[\'name\']); ?></option>\n            <?php } ?>\n          </select>\n        </div>\n\n        <div class="control-group">\n          <label for="tags">Permission</label>\n          <div class="controls">\n            <label class="radio inline private">\n              <input type="radio" name="permission" value="0"<?php if($preferences[\'permission\'] === false || $preferences[\'permission\'] === \'0\') { ?> checked="checked"<?php } ?>>\n              <span>Private</span>\n            </label>\n            <label class="radio inline">\n              <input type="radio" name="permission" value="1"<?php if($preferences[\'permission\'] === \'1\') { ?> checked="checked"<?php } ?>>\n              <span>Public</span>\n            </label>\n          </div>\n        </div>\n\n        <?php if(empty($token)) { ?>\n          <label for="license">License</label>\n          <select name="license" class="license">\n            <?php foreach($licenses as $code => $license) { ?>\n              <option value="<?php $this->utility->safe($code); ?>" <?php if($license[\'selected\']) { ?> selected="selected" <?php } ?>><?php $this->utility->licenseName($code); ?></option>\n            <?php } ?>\n          </select>\n        <?php } else { ?>\n          <input type="hidden" name="license" value="<?php reset($licenses); $this->utility->safe(key($licenses)); ?>">\n        <?php } ?>\n\n        <input type="hidden" name="crumb" value="<?php $this->utility->safe($crumb); ?>">\n        \n        <?php if(empty($token)) { ?>\n          <div class="btn-toolbar visible-phone">\n            <button type="submit" class="btn btn-brand upload-button addSpinner wide">Start uploading</button>\n          </div>\n        <?php } ?>\n      </div>\n    </div>\n  </form>\n</div>\n', 'message': "Merge commit 'upstream/master'\nShould also address issue #352\n\nConflicts:\n\n\tsrc/libraries/models/Photo.php\n", 'author_name': 'Hubert Figuiere', 'lang': 'php'}