Merge branch 'ScottIsAFool-Blogger-Categories'

This commit is contained in:
Will Duff 2016-01-22 08:11:30 -08:00
commit b293feecc7
11 changed files with 181 additions and 32 deletions

View File

@ -23,9 +23,9 @@ using Google.Apis.Util;
using System.Globalization;
using System.Diagnostics;
using Google.Apis.Blogger.v3.Data;
using System.Net.Http.Headers;
using OpenLiveWriter.Controls;
using System.Windows.Forms;
using Newtonsoft.Json;
namespace OpenLiveWriter.BlogClient.Clients
{
@ -88,7 +88,7 @@ namespace OpenLiveWriter.BlogClient.Clients
Permalink = post.Url,
Contents = post.Content,
DatePublished = post.Published.Value,
Keywords = string.Join(new string(LabelDelimiter,1), post.Labels)
Categories = post.Labels?.Select(x => new BlogPostCategory(x)).ToArray() ?? new BlogPostCategory[0]
};
}
@ -106,10 +106,12 @@ namespace OpenLiveWriter.BlogClient.Clients
private static Post ConvertToGoogleBloggerPost(BlogPost post)
{
var labels = post.Categories?.Select(x => x.Name).ToList();
labels?.AddRange(post.NewCategories?.Select(x => x.Name) ?? new List<string>());
return new Post()
{
Content = post.Contents,
Labels = post.Keywords?.Split(new char[] { LabelDelimiter }, StringSplitOptions.RemoveEmptyEntries).Select(k => k.Trim()).ToList(),
Labels = labels ?? new List<string>(),
// TODO:OLW - DatePublishedOverride didn't work quite right. Either the date published override was off by several hours,
// needs to be normalized to UTC or the Blogger website thinks I'm in the wrong time zone.
Published = post.HasDatePublishedOverride ? post?.DatePublishedOverride : null,
@ -123,6 +125,8 @@ namespace OpenLiveWriter.BlogClient.Clients
return new PageInfo(page.Id, page.Title, page.Published.GetValueOrDefault(DateTime.Now), string.Empty);
}
private const int MaxRetries = 5;
private const string ENTRY_CONTENT_TYPE = "application/atom+xml;type=entry";
private const string XHTML_NS = "http://www.w3.org/1999/xhtml";
private const string FEATURES_NS = "http://purl.org/atompub/features/1.0";
@ -140,15 +144,15 @@ namespace OpenLiveWriter.BlogClient.Clients
{
// configure client options
BlogClientOptions clientOptions = new BlogClientOptions();
clientOptions.SupportsCategories = false;
clientOptions.SupportsMultipleCategories = false;
clientOptions.SupportsNewCategories = false;
clientOptions.SupportsCategories = true;
clientOptions.SupportsMultipleCategories = true;
clientOptions.SupportsNewCategories = true;
clientOptions.SupportsCustomDate = true;
clientOptions.SupportsExcerpt = false;
clientOptions.SupportsSlug = false;
clientOptions.SupportsFileUpload = true;
clientOptions.SupportsKeywords = true;
clientOptions.SupportsGetKeywords = true;
clientOptions.SupportsKeywords = false;
clientOptions.SupportsGetKeywords = false;
clientOptions.SupportsPages = true;
clientOptions.SupportsExtendedEntries = true;
_clientOptions = clientOptions;
@ -197,6 +201,7 @@ namespace OpenLiveWriter.BlogClient.Clients
var transientCredentials = Credentials.TransientCredentials as TransientCredentials ??
new TransientCredentials(Credentials.Username, Credentials.Password, null);
VerifyAndRefreshCredentials(transientCredentials);
Credentials.TransientCredentials = transientCredentials;
return transientCredentials;
}
@ -266,6 +271,14 @@ namespace OpenLiveWriter.BlogClient.Clients
tc.Token = userCredential;
}
private void RefreshAccessToken(TransientCredentials transientCredentials)
{
// Using the BloggerService automatically refreshes the access token, but we call the Picasa endpoint
// directly and therefore need to force refresh the access token on occasion.
var userCredential = transientCredentials.Token as UserCredential;
userCredential?.RefreshTokenAsync(CancellationToken.None).Wait();
}
private HttpRequestFilter CreateAuthorizationFilter()
{
var transientCredentials = Login();
@ -289,13 +302,33 @@ namespace OpenLiveWriter.BlogClient.Clients
public BlogInfo[] GetUsersBlogs()
{
var blogList = GetService().Blogs.ListByUser("self").Execute();
return blogList.Items.Select(b => new BlogInfo(b.Id, b.Name, b.Url)).ToArray();
return blogList.Items?.Select(b => new BlogInfo(b.Id, b.Name, b.Url)).ToArray() ?? new BlogInfo[0];
}
private const string CategoriesEndPoint = "/feeds/posts/summary?alt=json&max-results=0";
public BlogPostCategory[] GetCategories(string blogId)
{
// Google Blogger does not support categories
return new BlogPostCategory[] { };
var categories = new BlogPostCategory[0];
var blog = GetService().Blogs.Get(blogId).Execute();
if (blog != null)
{
var categoriesUrl = string.Concat(blog.Url, CategoriesEndPoint);
var response = SendAuthenticatedHttpRequest(categoriesUrl, 30, CreateAuthorizationFilter());
if (response != null)
{
using (var reader = new StreamReader(response.GetResponseStream()))
{
var json = reader.ReadToEnd();
var item = JsonConvert.DeserializeObject<CategoryResponse>(json);
var cats = item?.Feed?.CategoryArray.Select(x => new BlogPostCategory(x.Term));
categories = cats?.ToArray() ?? new BlogPostCategory[0];
}
}
}
return categories;
}
public BlogPostKeyword[] GetKeywords(string blogId)
@ -317,7 +350,7 @@ namespace OpenLiveWriter.BlogClient.Clients
recentPostsRequest.Status = PostsResource.ListRequest.StatusEnum.Live;
var recentPosts = recentPostsRequest.Execute();
return recentPosts.Items.Select(p => ConvertToBlogPost(p)).ToArray();
return recentPosts.Items?.Select(ConvertToBlogPost).ToArray() ?? new BlogPost[0];
}
public string NewPost(string blogId, BlogPost post, INewCategoryContext newCategoryContext, bool publish, out string etag, out XmlDocument remotePost)
@ -383,7 +416,7 @@ namespace OpenLiveWriter.BlogClient.Clients
var getPagesRequest = GetService().Pages.List(blogId);
var pageList = getPagesRequest.Execute();
return pageList.Items.Select(p => ConvertToPageInfo(p)).ToArray();
return pageList.Items?.Select(ConvertToPageInfo).ToArray() ?? new PageInfo[0];
}
public BlogPost[] GetPages(string blogId, int maxPages)
@ -392,7 +425,7 @@ namespace OpenLiveWriter.BlogClient.Clients
getPagesRequest.MaxResults = maxPages;
var pageList = getPagesRequest.Execute();
return pageList.Items.Select(p => ConvertToBlogPost(p)).ToArray();
return pageList.Items?.Select(ConvertToBlogPost).ToArray() ?? new BlogPost[0];
}
public string NewPage(string blogId, BlogPost page, bool publish, out string etag, out XmlDocument remotePost)
@ -672,18 +705,44 @@ namespace OpenLiveWriter.BlogClient.Clients
private void PostNewImage(string albumName, string filename, out string srcUrl, out string editUri)
{
Login();
for (int retry = 0; retry < MaxRetries; retry++)
{
var transientCredentials = Login();
try
{
string albumUrl = GetBlogImagesAlbum(albumName);
HttpWebResponse response = RedirectHelper.GetResponse(albumUrl, new RedirectHelper.RequestFactory(new UploadFileRequestFactory(this, filename, "POST").Create));
using (Stream s = response.GetResponseStream())
{
ParseMediaEntry(s, out srcUrl, out editUri);
return;
}
}
catch (WebException we)
{
if (retry < MaxRetries - 1 &&
we.Response as HttpWebResponse != null &&
((HttpWebResponse)we.Response).StatusCode == HttpStatusCode.Forbidden)
{
// HTTP 403 Forbidden means our OAuth access token is not valid.
RefreshAccessToken(transientCredentials);
}
else
{
throw;
}
}
}
string albumUrl = GetBlogImagesAlbum(albumName);
HttpWebResponse response = RedirectHelper.GetResponse(albumUrl, new RedirectHelper.RequestFactory(new UploadFileRequestFactory(this, filename, "POST").Create));
using (Stream s = response.GetResponseStream())
ParseMediaEntry(s, out srcUrl, out editUri);
Trace.Fail("Should never get here");
throw new ApplicationException("Should never get here");
}
private void UpdateImage(string editUri, string filename, out string srcUrl, out string newEditUri)
{
for (int retry = 5; retry > 0; retry--)
for (int retry = 0; retry < MaxRetries; retry++)
{
var transientCredentials = Login();
HttpWebResponse response;
bool conflict = false;
try
@ -692,20 +751,35 @@ namespace OpenLiveWriter.BlogClient.Clients
}
catch (WebException we)
{
if (retry > 1
&& we.Response as HttpWebResponse != null
&& ((HttpWebResponse)we.Response).StatusCode == HttpStatusCode.Conflict)
if (retry < MaxRetries - 1 &&
we.Response as HttpWebResponse != null)
{
response = (HttpWebResponse)we.Response;
conflict = true;
if (((HttpWebResponse)we.Response).StatusCode == HttpStatusCode.Conflict)
{
response = (HttpWebResponse)we.Response;
conflict = true;
}
else if (((HttpWebResponse)we.Response).StatusCode == HttpStatusCode.Forbidden)
{
// HTTP 403 Forbidden means our OAuth access token is not valid.
RefreshAccessToken(transientCredentials);
continue;
}
}
else
throw;
throw;
}
using (Stream s = response.GetResponseStream())
{
ParseMediaEntry(s, out srcUrl, out newEditUri);
}
if (!conflict)
{
return; // success!
}
editUri = newEditUri;
}
@ -782,5 +856,22 @@ namespace OpenLiveWriter.BlogClient.Clients
#endregion
public class Category
{
[JsonProperty("term")]
public string Term { get; set; }
}
public class Feed
{
[JsonProperty("category")]
public Category[] CategoryArray { get; set; }
}
public class CategoryResponse
{
[JsonProperty("feed")]
public Feed Feed { get; set; }
}
}
}

View File

@ -10,6 +10,7 @@ using OpenLiveWriter.Api;
using OpenLiveWriter.CoreServices;
using OpenLiveWriter.Extensibility.BlogClient;
using OpenLiveWriter.BlogClient.Providers;
using System.Web;
// TODO: WP on outstanding issues
/*
@ -53,5 +54,48 @@ namespace OpenLiveWriter.BlogClient.Clients
clientOptions.SupportsAuthor = true;
}
public override BlogInfo[] GetUsersBlogs()
{
TransientCredentials tc = Login();
return GetWPUsersBlogs(tc.Username, tc.Password);
}
private BlogInfo[] GetWPUsersBlogs(string username, string password)
{
// call method
XmlNode result = CallMethod("wp.getUsersBlogs",
new XmlRpcString(username),
new XmlRpcString(password, true));
try
{
// parse results
ArrayList blogs = new ArrayList();
XmlNodeList dataValues = result.SelectNodes("array/data");
foreach (XmlNode dataValue in dataValues)
{
XmlNodeList blogNodes = dataValue.SelectNodes("value/struct");
foreach (XmlNode blogNode in blogNodes)
{
// get node values
XmlNode idNode = blogNode.SelectSingleNode("member[name='blogid']/value");
XmlNode nameNode = blogNode.SelectSingleNode("member[name='blogName']/value");
XmlNode urlNode = blogNode.SelectSingleNode("member[name='url']/value");
// add to our list of blogs
blogs.Add(new BlogInfo(idNode.InnerText, HttpUtility.HtmlDecode(NodeToText(nameNode)), urlNode.InnerText));
}
}
// return list of blogs
return (BlogInfo[])blogs.ToArray(typeof(BlogInfo));
}
catch (Exception ex)
{
string response = result != null ? result.OuterXml : "(empty response)";
Trace.Fail("Exception occurred while parsing GetUsersBlogs response: " + response + "\r\n" + ex.ToString());
throw new BlogClientInvalidServerResponseException("wp.getUsersBlogs", ex.Message, response);
}
}
}
}

View File

@ -130,7 +130,7 @@
<futurePublishDateWarning>No</futurePublishDateWarning>
<defaultView>WebLayout</defaultView>
<invalidPostIdFaultCodePattern>^404$</invalidPostIdFaultCodePattern>
<supportsMultipleCategories>No</supportsMultipleCategories>
<supportsMultipleCategories>Yes</supportsMultipleCategories>
<supportsEmptyTitles>Yes</supportsEmptyTitles>
<requiresHtmlTitles>Yes</requiresHtmlTitles>
<usePicasaImgMaxAlways>Yes</usePicasaImgMaxAlways>

View File

@ -88,7 +88,7 @@ namespace OpenLiveWriter.CoreServices.Settings
private bool haveLoggedFailedGetKey = false;
/// <summary>
/// Low-level get (returns null if the value doesn't exist).
/// Low-level get (returns null if the value doesn't exist).
/// </summary>
/// <param name="name">name</param>
/// <returns>value (null if not present)</returns>
@ -183,6 +183,10 @@ namespace OpenLiveWriter.CoreServices.Settings
{
using (RegistryKey key = GetKey(false))
{
if (key == null)
{
return false;
}
using (RegistryKey subSettingsKey = key.OpenSubKey(subSettingsName))
{
return subSettingsKey != null;

View File

@ -232,6 +232,10 @@ namespace OpenLiveWriter.CoreServices
/// <param name="progressHost">The progressHost to provide feedback to</param>
private void DownloadReference(ReferenceToDownload reference, FileBasedSiteStorage fileStorage, IProgressHost progressHost)
{
if (IsBase64EmbededImage(reference.AbsoluteUrl))
{
return;
}
UrlDownloadToFile downloader;
string fullPath;
@ -346,6 +350,12 @@ namespace OpenLiveWriter.CoreServices
}
}
private bool IsBase64EmbededImage(string url)
{
return url.StartsWith("data:image/", StringComparison.InvariantCultureIgnoreCase) &&
url.ToLowerInvariant().Contains("base64");
}
/// <summary>
/// Used to actually commit the HTML to disk
/// </summary>

View File

@ -1186,7 +1186,7 @@ namespace OpenLiveWriter.HtmlEditor
OnDocumentComplete(e);
// Remove null-attributed font tags, e.g. <p><font>blah</font></p> --> <p>blah</p>
if (DocumentIsReady)
if (DocumentIsReady && PostBodyElement != null)
{
MarkupRange bodyRange = MarkupServices.CreateMarkupRange(PostBodyElement);
bodyRange.RemoveElementsByTagId(_ELEMENT_TAG_ID.TAGID_FONT, true);

View File

@ -141,7 +141,7 @@ namespace OpenLiveWriter.PostEditor
//ToDo: OLW Spell Checker
// NOTE: hardcode to sentry spelling engine -- need to unroll this if we switch engines
private static readonly string CONTEXT_DICTIONARY_FILE = string.Empty; // SentrySpellingChecker.ContextDictionaryFileName ;
private static readonly string CONTEXT_DICTIONARY_FILE = "context.tlx";
private DirectoryInfo _storageDirectory;
}

View File

@ -1 +1 @@
0.5.1.2
0.5.1.3