Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Contentstack.Core/Configuration/LivePreviewConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json.Linq;
using System;
using Newtonsoft.Json.Linq;

namespace Contentstack.Core.Configuration
{
Expand All @@ -12,7 +13,24 @@ public class LivePreviewConfig
internal string ContentTypeUID { get; set; }
internal string EntryUID { get; set; }
internal JObject PreviewResponse { get; set; }

/// <summary>
/// Snapshot of preview_timestamp / release_id / live_preview when <see cref="PreviewResponse"/> was set (prefetch).
/// Prevents Entry.Fetch from short-circuiting with a draft from a previous Live Preview query.
/// </summary>
internal string PreviewResponseFingerprintPreviewTimestamp { get; set; }
internal string PreviewResponseFingerprintReleaseId { get; set; }
internal string PreviewResponseFingerprintLivePreview { get; set; }

public string ReleaseId {get; set;}
public string PreviewTimestamp {get; set;}

internal bool IsCachedPreviewForCurrentQuery()
{
if (PreviewResponse == null) return false;
return string.Equals(PreviewTimestamp ?? "", PreviewResponseFingerprintPreviewTimestamp ?? "", StringComparison.Ordinal)
&& string.Equals(ReleaseId ?? "", PreviewResponseFingerprintReleaseId ?? "", StringComparison.Ordinal)
&& string.Equals(LivePreview ?? "", PreviewResponseFingerprintLivePreview ?? "", StringComparison.Ordinal);
}
}
}
145 changes: 137 additions & 8 deletions Contentstack.Core/ContentstackClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,94 @@
private string currentContenttypeUid = null;
private string currentEntryUid = null;
public List<IContentstackPlugin> Plugins { get; set; } = new List<IContentstackPlugin>();

private static LivePreviewConfig CloneLivePreviewConfig(LivePreviewConfig source)
{
if (source == null) return null;

return new LivePreviewConfig
{
Enable = source.Enable,
Host = source.Host,
ManagementToken = source.ManagementToken,
PreviewToken = source.PreviewToken,
ReleaseId = source.ReleaseId,
PreviewTimestamp = source.PreviewTimestamp,

// internal state (same assembly)
LivePreview = source.LivePreview,
ContentTypeUID = source.ContentTypeUID,
EntryUID = source.EntryUID,
PreviewResponse = source.PreviewResponse,
PreviewResponseFingerprintPreviewTimestamp = source.PreviewResponseFingerprintPreviewTimestamp,
PreviewResponseFingerprintReleaseId = source.PreviewResponseFingerprintReleaseId,
PreviewResponseFingerprintLivePreview = source.PreviewResponseFingerprintLivePreview
};
}

private static ContentstackOptions CloneOptions(ContentstackOptions source)
{
if (source == null) return null;

return new ContentstackOptions
{
ApiKey = source.ApiKey,
AccessToken = source.AccessToken,

Check warning on line 96 in Contentstack.Core/ContentstackClient.cs

View workflow job for this annotation

GitHub Actions / unit-test

'ContentstackOptions.AccessToken' is obsolete: 'We have deprecated AccessToken and we will stop supporting it in the near future. We strongly recommend using DeliveryToken.'

Check warning on line 96 in Contentstack.Core/ContentstackClient.cs

View workflow job for this annotation

GitHub Actions / unit-test

'ContentstackOptions.AccessToken' is obsolete: 'We have deprecated AccessToken and we will stop supporting it in the near future. We strongly recommend using DeliveryToken.'

Check warning on line 96 in Contentstack.Core/ContentstackClient.cs

View workflow job for this annotation

GitHub Actions / unit-test

'ContentstackOptions.AccessToken' is obsolete: 'We have deprecated AccessToken and we will stop supporting it in the near future. We strongly recommend using DeliveryToken.'

Check warning on line 96 in Contentstack.Core/ContentstackClient.cs

View workflow job for this annotation

GitHub Actions / unit-test

'ContentstackOptions.AccessToken' is obsolete: 'We have deprecated AccessToken and we will stop supporting it in the near future. We strongly recommend using DeliveryToken.'

Check warning on line 96 in Contentstack.Core/ContentstackClient.cs

View workflow job for this annotation

GitHub Actions / unit-test

'ContentstackOptions.AccessToken' is obsolete: 'We have deprecated AccessToken and we will stop supporting it in the near future. We strongly recommend using DeliveryToken.'

Check warning on line 96 in Contentstack.Core/ContentstackClient.cs

View workflow job for this annotation

GitHub Actions / unit-test

'ContentstackOptions.AccessToken' is obsolete: 'We have deprecated AccessToken and we will stop supporting it in the near future. We strongly recommend using DeliveryToken.'

Check warning on line 96 in Contentstack.Core/ContentstackClient.cs

View workflow job for this annotation

GitHub Actions / unit-test

'ContentstackOptions.AccessToken' is obsolete: 'We have deprecated AccessToken and we will stop supporting it in the near future. We strongly recommend using DeliveryToken.'
DeliveryToken = source.DeliveryToken,
Environment = source.Environment,
Host = source.Host,
Proxy = source.Proxy,
Region = source.Region,
Version = source.Version,
Branch = source.Branch,
Timeout = source.Timeout,
EarlyAccessHeader = source.EarlyAccessHeader,
LivePreview = CloneLivePreviewConfig(source.LivePreview)
};
}

/// <summary>
/// Clears any in-memory Live Preview context (hash, release, timestamp, content type, entry).
/// Useful when switching back to the Delivery API after using Live Preview / Timeline preview.
/// </summary>
public void ResetLivePreview()
{
if (this.LivePreviewConfig == null) return;

this.LivePreviewConfig.LivePreview = null;
this.LivePreviewConfig.ReleaseId = null;
this.LivePreviewConfig.PreviewTimestamp = null;
this.LivePreviewConfig.ContentTypeUID = null;
this.LivePreviewConfig.EntryUID = null;
this.LivePreviewConfig.PreviewResponse = null;
this.LivePreviewConfig.PreviewResponseFingerprintPreviewTimestamp = null;
this.LivePreviewConfig.PreviewResponseFingerprintReleaseId = null;
this.LivePreviewConfig.PreviewResponseFingerprintLivePreview = null;
}

/// <summary>
/// Creates a new client instance with the same configuration but isolated in-memory state.
/// Use this to safely perform Timeline comparisons (left/right) without shared Live Preview context.
/// </summary>
public ContentstackClient Fork()
{
var forked = new ContentstackClient(CloneOptions(_options));

// Preserve any runtime header mutations (e.g., custom headers added via SetHeader).
if (this._LocalHeaders != null)
{
foreach (var kvp in this._LocalHeaders)
{
forked.SetHeader(kvp.Key, kvp.Value?.ToString());
}
}

// Carry over current content type / entry hints (used when live preview query omits them)
forked.currentContenttypeUid = this.currentContenttypeUid;
forked.currentEntryUid = this.currentEntryUid;

return forked;
}
/// <summary>
/// Initializes a instance of the <see cref="ContentstackClient"/> class.
/// </summary>
Expand Down Expand Up @@ -115,7 +203,7 @@
this.SetConfig(cnfig);
if (_options.LivePreview != null)
{
this.LivePreviewConfig = _options.LivePreview;
this.LivePreviewConfig = CloneLivePreviewConfig(_options.LivePreview);
}
else
{
Expand Down Expand Up @@ -341,6 +429,11 @@
}
}

/// <summary>
/// Fetches draft entry JSON from the Live Preview host (Java Stack.livePreviewQuery equivalent).
/// Always uses the configured preview host so the call succeeds even when the delivery base URL
/// would still point at CDN (e.g. live_preview hash is "init").
/// </summary>
private async Task<JObject> GetLivePreviewData()
{

Expand Down Expand Up @@ -386,11 +479,25 @@
try
{
HttpRequestHandler RequestHandler = new HttpRequestHandler(this);
//string branch = this.Config.Branch ? this.Config.Branch : "main";
string URL = String.Format("{0}/content_types/{1}/entries/{2}", this.Config.getBaseUrl(this.LivePreviewConfig, this.LivePreviewConfig.ContentTypeUID), this.LivePreviewConfig.ContentTypeUID, this.LivePreviewConfig.EntryUID);
string basePreview = this.Config.getLivePreviewUrl(this.LivePreviewConfig);
string URL = String.Format("{0}/content_types/{1}/entries/{2}", basePreview, this.LivePreviewConfig.ContentTypeUID, this.LivePreviewConfig.EntryUID);
var outputResult = await RequestHandler.ProcessRequest(URL, headerAll, mainJson, Branch: this.Config.Branch, isLivePreview: true, timeout: this.Config.Timeout, proxy: this.Config.Proxy);
JObject data = JsonConvert.DeserializeObject<JObject>(outputResult.Replace("\r\n", ""), this.SerializerSettings);
return (JObject)data["entry"];
if (data == null) return null;
if (data["entry"] is JObject single && single.HasValues)
return single;
if (data["entries"] is JArray arr && arr.Count > 0)
{
string targetUid = this.LivePreviewConfig.EntryUID;
foreach (var token in arr)
{
if (token is JObject jo && jo["uid"] != null
&& string.Equals(jo["uid"].ToString(), targetUid, StringComparison.Ordinal))
return jo;
}
return arr[0] as JObject;
}
return null;
}
catch (Exception ex)
{
Expand Down Expand Up @@ -612,6 +719,10 @@
this.LivePreviewConfig.LivePreview = null;
this.LivePreviewConfig.PreviewTimestamp = null;
this.LivePreviewConfig.ReleaseId = null;
this.LivePreviewConfig.PreviewResponse = null;
this.LivePreviewConfig.PreviewResponseFingerprintPreviewTimestamp = null;
this.LivePreviewConfig.PreviewResponseFingerprintReleaseId = null;
this.LivePreviewConfig.PreviewResponseFingerprintLivePreview = null;
if (query.Keys.Contains("content_type_uid"))
{
string contentTypeUID = null;
Expand Down Expand Up @@ -655,10 +766,28 @@
query.TryGetValue("preview_timestamp", out PreviewTimestamp);
this.LivePreviewConfig.PreviewTimestamp = PreviewTimestamp;
}
//if (!string.IsNullOrEmpty(this.LivePreviewConfig.LivePreview))
//{
// this.LivePreviewConfig.PreviewResponse = await GetLivePreviewData();
//}

if (this.LivePreviewConfig.Enable
&& !string.IsNullOrEmpty(this.LivePreviewConfig.Host)
&& !string.IsNullOrEmpty(this.LivePreviewConfig.ContentTypeUID)
&& !string.IsNullOrEmpty(this.LivePreviewConfig.EntryUID))
{
try
{
var draft = await GetLivePreviewData();
if (draft != null && draft.Type == JTokenType.Object && draft.HasValues)
{
this.LivePreviewConfig.PreviewResponse = draft;
this.LivePreviewConfig.PreviewResponseFingerprintPreviewTimestamp = this.LivePreviewConfig.PreviewTimestamp;
this.LivePreviewConfig.PreviewResponseFingerprintReleaseId = this.LivePreviewConfig.ReleaseId;
this.LivePreviewConfig.PreviewResponseFingerprintLivePreview = this.LivePreviewConfig.LivePreview;
}
}
catch
{
// Prefetch failed: Entry.Fetch still uses preview headers on the network path.
}
}
}

/// <summary>
Expand Down
71 changes: 58 additions & 13 deletions Contentstack.Core/Models/Entry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1401,39 +1401,84 @@ public async Task<T> Fetch<T>()

//Dictionary<string, object> urlQueries = new Dictionary<string, object>();

var livePreviewConfig = this.ContentTypeInstance?.StackInstance?.LivePreviewConfig;
if (livePreviewConfig != null
&& livePreviewConfig.Enable
&& livePreviewConfig.PreviewResponse != null
&& livePreviewConfig.PreviewResponse.Type == JTokenType.Object
&& livePreviewConfig.PreviewResponse.HasValues
&& !string.IsNullOrEmpty(this.Uid)
&& string.Equals(livePreviewConfig.EntryUID, this.Uid, StringComparison.Ordinal)
&& this.ContentTypeInstance != null
&& string.Equals(
livePreviewConfig.ContentTypeUID,
this.ContentTypeInstance.ContentTypeId,
StringComparison.OrdinalIgnoreCase)
&& livePreviewConfig.IsCachedPreviewForCurrentQuery())
{
try
{
var serializedFromPreview = livePreviewConfig.PreviewResponse.ToObject<T>(
this.ContentTypeInstance.StackInstance.Serializer);
if (serializedFromPreview != null && serializedFromPreview.GetType() == typeof(Entry))
{
(serializedFromPreview as Entry).ContentTypeInstance = this.ContentTypeInstance;
}
return serializedFromPreview;
}
catch
{
// Fall through to network fetch.
}
}

if (headers != null && headers.Count() > 0)
{
foreach (var header in headers)
{
if (this.ContentTypeInstance.StackInstance.LivePreviewConfig.Enable == true
&& this.ContentTypeInstance.StackInstance.LivePreviewConfig.ContentTypeUID == this.ContentTypeInstance.ContentTypeId
&& header.Key == "access_token" && !string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.LivePreview))
if (this.ContentTypeInstance != null
&& livePreviewConfig != null
&& livePreviewConfig.Enable
&& livePreviewConfig.ContentTypeUID == this.ContentTypeInstance.ContentTypeId
&& header.Key == "access_token"
&& !string.IsNullOrEmpty(livePreviewConfig.LivePreview))
{
continue;
}
headerAll.Add(header.Key, (String)header.Value);
}
}
bool isLivePreview = false;
if (this.ContentTypeInstance.StackInstance.LivePreviewConfig.Enable == true && this.ContentTypeInstance.StackInstance.LivePreviewConfig.ContentTypeUID == this.ContentTypeInstance.ContentTypeId)
var hasLivePreviewContext =
this.ContentTypeInstance != null
&& livePreviewConfig != null
&& livePreviewConfig.Enable
&& livePreviewConfig.ContentTypeUID == this.ContentTypeInstance.ContentTypeId
&& (
!string.IsNullOrEmpty(livePreviewConfig.LivePreview)
|| !string.IsNullOrEmpty(livePreviewConfig.ReleaseId)
|| !string.IsNullOrEmpty(livePreviewConfig.PreviewTimestamp)
);

if (hasLivePreviewContext)
{
mainJson.Add("live_preview", string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.LivePreview)? "init" : this.ContentTypeInstance.StackInstance.LivePreviewConfig.LivePreview);
mainJson.Add("live_preview", string.IsNullOrEmpty(livePreviewConfig.LivePreview)? "init" : livePreviewConfig.LivePreview);

if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.ManagementToken)) {
headerAll["authorization"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.ManagementToken;
} else if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewToken)) {
headerAll["preview_token"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewToken;
if (!string.IsNullOrEmpty(livePreviewConfig.ManagementToken)) {
headerAll["authorization"] = livePreviewConfig.ManagementToken;
} else if (!string.IsNullOrEmpty(livePreviewConfig.PreviewToken)) {
headerAll["preview_token"] = livePreviewConfig.PreviewToken;
} else {
throw new LivePreviewException();
}

if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.ReleaseId))
if (!string.IsNullOrEmpty(livePreviewConfig.ReleaseId))
{
headerAll["release_id"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.ReleaseId;
headerAll["release_id"] = livePreviewConfig.ReleaseId;
}
if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewTimestamp))
if (!string.IsNullOrEmpty(livePreviewConfig.PreviewTimestamp))
{
headerAll["preview_timestamp"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewTimestamp;
headerAll["preview_timestamp"] = livePreviewConfig.PreviewTimestamp;
}

isLivePreview = true;
Expand Down
Loading
Loading