While it may seem like a strange requirement, I recently came across the request to be able to set Content Items in Sitecore to be only published via a Sitecore Scheduled Task. While the client still wanted the flexibility of allowing their content authors to publish as often as they required, there were certain items in the content tree that would cause a relatively large amount of traffic during a publish. While considering a solution to this I realised, while relatively simple, we would be customising some interesting parts of the system:
- Publishing Agent – The out of the box scheduled task that runs the publisher to a schedule
- Process Queue – The pipeline that handles the queued publishing request
- Publisher – The class that processes publishing requests
- item:publish – The command for publishing a single item in the content tree
- item:publishnow – The command for immediate publishing of an item
- Set Publishing Form – The dialog where we change publishing restrictions
- Schedule publish editor warnings – The warning/information displayed at the top of the content window in the content editor
- Gutter Publishing Warnings – The warning that displays as an icon in the gutter of the content tree
First things first, before we get started we need to setup a boolean value on our system publishing template in order to track the items we want to only publish with the scheduler. This data template can be found at /templates/system/templates/sections/publishing. Add a checkbox field to the Publishing section and call it __Publish Only in Schedule or anything else you like. After creating the field you can also change the Title of it under the data section of the new field to be something more readable: Publish Only in Schedule
Publisher
The first place to start in terms of coding is customising the Publisher itself, namely adding some small changes to the Sitecore.Publishing.Publisher class that will allow us to identify when this is being run by the scheduler rather than as a manual publish. One way to accomplish this is to take advantage of the JobManager classes ability to associate parameters with the function being called when the job is run. Utilising the JobManager is default behaviour of the Publisher however in order to add parameters when initialising the JobOptions of the job being created we have to have a method statement that matches the parameters we are passing. The easiest way to achieve this is to inherit the Publisher and make some small modifications:
...
//create a method where we can associate parameters for tracking the origin of a publish call
public virtual void Publish(bool empty)
{
base.Publish();
}
public virtual Job PublishAsync(bool schedule)
{
this.AssertState();
JobOptions options;
if (!schedule)
{
//standard JobOptions declaration
options = new JobOptions(this.GetJobName(), "publish", "publisher", this, "Publish");
}
else
{
//if call is from a schedule, add a boolean parameter that is set to true
options = new JobOptions(this.GetJobName(), "publish", "publisher", this, "Publish", new object[]{true});
}
options.ContextUser = Context.User;
options.AfterLife = TimeSpan.FromMinutes(1.0);
options.AtomicExecution = true;
options.CustomData = new PublishStatus();
return JobManager.Start(options);
}
...
Publishing Agent
Next step is to customise the publishing agent. This is the class that is run by the scheduler and is specified in the web.config. Using reflector we can copy the functionality of the original class and make a small change so that it is calling the new Publisher class we just created.
private void StartPublish(Language language)
{
Assert.ArgumentNotNull(language, "language");
Log.Info(string.Concat(new object[] { "PublishAgent started (source: ", this.SourceDatabase, ", target: ",
this.TargetDatabase, ", mode: ", this.Mode, ")" }), this);
Database database = Factory.GetDatabase(this.SourceDatabase);
Database database2 = Factory.GetDatabase(this.TargetDatabase);
Assert.IsNotNull(database, "Unknown database: {0}", new object[] { this.SourceDatabase });
Assert.IsNotNull(database2, "Unknown database: {0}", new object[] { this.TargetDatabase });
PublishOptions options = new PublishOptions(database, database2, this.Mode, language, DateTime.Now);
//use custom publisher
CustomPublisher publisher = new CustomPublisher(options);
bool willBeQueued = publisher.WillBeQueued;
//pass true bool value to indicate call is coming from the scheduler
publisher.PublishAsync(true);
Log.Info("Asynchronous publishing " + (willBeQueued ? "queued" : "started") + ". Job name: " + publisher.GetJobName(), this);
TaskCounters.Publishings.Increment();
}
We also need to change the configuration in the web.config to point to this class when the schedule is run. The interval needs to be changed from zero in order for the schedule to run.
<!--<agent type="Sitecore.Tasks.PublishAgent" method="Run" interval="00:00:00">-->
<agent type="Sitecore.Starterkit.Classes.CustomPublishingAgent" method="Run" interval="00:15:00">
<param desc="source database">master</param>
<param desc="target database">web</param>
<param desc="mode (full or incremental)">incremental</param>
<param desc="languages">en, da</param>
</agent>
Process Queue
Next is to customise the process queue, here we need to check for a bool parameter for the job that is being run in the pipeline to determine if the current request is an automatic publish or a manual one. If it is an automatic publish we can skip the checking of the __Publish Only in Schedule field otherwise we need to check this field and skip the publish if it is set to true.
protected virtual void ProcessEntries(IEnumerable<PublishingCandidate> entries, PublishContext context)
{
//get parameters from current job
object[] obj = context.Job.Options.Parameters;
bool schedule = false;
if (obj != null && obj.Count() > 0)
{
try
{
//if the parameter is true we know that this job has come from the scheduler
schedule = (bool)obj[0];
}
catch { }
}
foreach (PublishingCandidate candidate in entries)
{
PublishItemContext publishItem = this.CreateItemContext(candidate, context);
if (schedule || !this.SkipScheduledOnly(publishItem, context))
{
PublishItemResult result = PublishItemPipeline.Run(publishItem);
if (!this.SkipReferrers(result, context))
{
this.ProcessEntries(result.ReferredItems, context);
}
if (!this.SkipChildren(result, candidate, context))
{
this.ProcessEntries(candidate.ChildEntries, context);
}
}
}
}
protected virtual bool SkipScheduledOnly(PublishItemContext result, PublishContext context)
{
Item itm = context.PublishOptions.SourceDatabase.GetItem(result.ItemId);
if (itm != null && itm.Fields["__Publish Only in Schedule"] != null && itm.Fields["__Publish Only in Schedule"].Value == "1") return true;
return false;
}
Again we need to update the web.config and change the publishing pipeline to point to our new queue processer.
<publish help="Processors should derive from Sitecore.Publishing.Pipelines.Publish.PublishProcessor">
<processor type="Sitecore.Publishing.Pipelines.Publish.AddLanguagesToQueue, Sitecore.Kernel"/>
<processor type="Sitecore.Publishing.Pipelines.Publish.AddItemsToQueue, Sitecore.Kernel"/>
<!--<processor type="Sitecore.Publishing.Pipelines.Publish.ProcessQueue, Sitecore.Kernel"/>-->
<processor type="Sitecore.Starterkit.ProcessQueue, Sitecore.Starterkit"/>
</publish>
Now all we have left to do is to make some customisations to the content editor to give the option for enabling this setting and to give the content editor some visual feedback when this restriction is in place.
item:publish; item:publishnow
Due to Sitecore’s flexibility customising the commands from the ribbon is an easy process. These commands are all configured in the Commands.Config file located in the App_Config directory. We need to change this command to give a visual alert when an editor tries to publish an item that has this restriction in place. First some simple code that checks an item for the restriction that will be used in both commands:
public static bool CheckScheduleOnly(ClientPipelineArgs args, Item item)
{
Assert.ArgumentNotNull(args, "args");
Assert.ArgumentNotNull(item, "item");
if (item.Fields["__Publish Only in Schedule"] == null
|| string.IsNullOrEmpty(item.Fields["__Publish Only in Schedule"].Value.ToString())
|| item.Fields["__Publish Only in Schedule"].Value == "0") return true;
SheerResponse.Alert(Translate.Text
("The current item \"{0}\" can only be published by the publishing scheduler. It will be published during next scheduled publish."
, item.DisplayName));
return false;
}
Then we need to add the checking to the Run method of both commands.
item:publish
new protected void Run(ClientPipelineArgs args)
{
Assert.ArgumentNotNull(args, "args");
string str = args.Parameters["id"];
string name = args.Parameters["language"];
string str3 = args.Parameters["version"];
if (SheerResponse.CheckModified())
{
Item item = Context.ContentDatabase.Items[str, Language.Parse(name), Sitecore.Data.Version.Parse(str3)];
if (item == null)
{
SheerResponse.Alert("Item not found.", new string[0]);
}
//check for schedule only flag
else if (CheckScheduleOnly(args, item) && CheckWorkflow(args, item))
{
Log.Audit(this, "Publish item: {0}", new string[] { AuditFormatter.FormatItem(item) });
Items.Publish(item);
}
}
}
item:publishnow
new protected void Run(ClientPipelineArgs args)
{
Assert.ArgumentNotNull(args, "args");
string str = args.Parameters["id"];
string name = args.Parameters["language"];
string str3 = args.Parameters["version"];
Item item = Client.ContentDatabase.Items[str, Language.Parse(name), Sitecore.Data.Version.Parse(str3)];
if (item == null)
{
SheerResponse.Alert("Item not found.", new string[0]);
}
//check for schedule only flag
else if (CheckScheduleOnly(args, item) && CheckWorkflow(args, item) && SheerResponse.CheckModified())
{
if (args.IsPostBack)
{
if (args.Result == "yes")
{
Database[] targets = GetTargets();
if (targets.Length == 0)
{
SheerResponse.Alert("No target databases were found for publishing.", new string[0]);
}
else
{
LanguageCollection languages = LanguageManager.GetLanguages(Context.ContentDatabase);
if ((languages == null) || (languages.Count == 0))
{
SheerResponse.Alert("No languages were found for publishing.", new string[0]);
}
else
{
Log.Audit(this, "Publish item now: {0}", new string[] { AuditFormatter.FormatItem(item) });
PublishManager.PublishItem(item, targets, languages.ToArray(), false, true);
SheerResponse.Alert("The item is being published.", new string[0]);
}
}
}
}
else
{
SheerResponse.Confirm(Translate.Text("Are you sure you want to publish \"{0}\"\nin every language to every publishing target?",
new object[] { item.DisplayName }));
args.WaitForPostBack();
}
}
}
And the 2 line changes to Commands.Config to point to our new commands
<!--<command name="item:publish" type="Sitecore.Shell.Framework.Commands.PublishItem,Sitecore.Kernel" />-->
<command name="item:publish" type="Sitecore.Starterkit.Classes.CustomPublishItem,Sitecore.Starterkit" />
<!--<command name="item:publishnow" type="Sitecore.Shell.Framework.Commands.PublishNow,Sitecore.Kernel" />-->
<command name="item:publishnow" type="Sitecore.Starterkit.Classes.CustomPublishNow,Sitecore.Starterkit" />
And the result:
Set Publishing Form
We also need a nice way of enabling this feature for items. To do this we will extend the set publishing dialogue form that is shown when the change publishing restrictions button is pressed. The xml page that builds this form can be found in /sitecore/shell/Applications/Content Manager/Dialogs/Set Publishing/Set Publishing.xml. We also want to extend the CodeBeside class for this file to incorporate the logic for our new option.
<CodeBeside Type="Sitecore.Starterkit.Classes.CustomSetPublishingForm,Sitecore.Starterkit"/>
<!--<CodeBeside Type="Sitecore.Shell.Applications.ContentManager.Dialogs.SetPublishing.SetPublishingForm,Sitecore.Client"/>-->
...
<Border Padding="8px 8px 4px 8px">
<Checkbox ID="SchedulePublish" Header="Publish to Schedule Only" />
</Border>
public class CustomSetPublishingForm : DialogForm
{
...
protected override void OnOK(object sender, EventArgs args)
{
...
itemFromQueryString.Publishing.NeverPublish = !this.NeverPublish.Checked;
if (itemFromQueryString.Fields["__Publish Only in Schedule"] != null)
itemFromQueryString.Fields["__Publish Only in Schedule"].Value = this.SchedulePublish.Checked ? "1" : "0";
...
}
private void RenderItemTab(Item item)
{
Assert.ArgumentNotNull(item, "item");
this.NeverPublish.Checked = !item.Publishing.NeverPublish;
if (item.Fields["__Publish Only in Schedule"] == null)
{
SchedulePublish.Visible = false;
}
else
{
this.SchedulePublish.Checked = item.Fields["__Publish Only in Schedule"].Value == "1";
}
...
}
Result:
Publishing Warnings
So after doing all the hard work the only thing left is to display some nice info/warning messages in the content editor. The two places where publishing warnings occur are in the gutter and the warning panel and while the warnings themselves are very similar (if not identical) the area to customise these are in very different places.
Editor warnings
This is fairly simple to achieve by creating a class that is called in the getContentEditorWarnings pipeline. First of all make the simple class that will be called:
public class SchedulePublishEditorWarning
{
// Methods
public void Process(GetContentEditorWarningsArgs args)
{
Item item = args.Item;
if ((item != null) && item.Fields["__Publish Only in Schedule"] != null && item.Fields["__Publish Only in Schedule"].Value == "1")
{
args.Add(Translate.Text("This item can only be published by the publishing scheduler. It will be published during next scheduled publish."),
Translate.Text("Only the scheduled publisher can publish this item"));
}
}
}
From here all we need to do is add this class into the pipeline
<getContentEditorWarnings>
...
<processor type="Sitecore.Pipelines.GetContentEditorWarnings.NeverPublish, Sitecore.Kernel"/>
<processor type="Sitecore.Pipelines.GetContentEditorWarnings.ItemPublishingRestricted, Sitecore.Kernel"/>
<processor type="Sitecore.Pipelines.GetContentEditorWarnings.VersionPublishingRestricted, Sitecore.Kernel"/>
<processor type="Sitecore.Starterkit.Classes.SchedulePublishEditorWarning, Sitecore.Starterkit" />
...
</getContentEditorWarnings>
This gives us a message in the content editor:
Gutter Warnings
The last thing for us to do is to set the warning in the gutter when the editor has Publishing Warnings enabled in the gutter. To find this setting we need to switch to the core database on the desktop and open up the content editor. The relevant setting can be found at /sitecore/content/Applications/Content Editor/Gutters/Publishing Warnings. Here in the type field of the data section we can add in our own class for these warnings. So again using reflector we can copy the original class and add our own logic to it.
private static string GetTooltip(Item item)
{
...
Item validVersion = item.Publishing.GetValidVersion(now, requireApproved);
if ((item.Publishing.IsPublishable(now, false)
&& item.Publishing.IsValid(now, requireApproved))
&& ((validVersion != null) && (item.Version == validVersion.Version))
&& (item.Fields["__Publish Only in Schedule"] == null
|| string.IsNullOrEmpty(item.Fields["__Publish Only in Schedule"].Value)
|| item.Fields["__Publish Only in Schedule"].Value == "0"))
{
return null;
}
...
if (item.Fields["__Publish Only in Schedule"] != null && item.Fields["__Publish Only in Schedule"].Value == "1")
{
return "You cannot publish this item as it is restricted to scheduled publish only. It will be published by the publishing scheduler";
}
return "If you publish now, the latest version will not be visible on the web site as it is unpublishable.";
}
Now all we need to do is change the application setting in the core database and we get the following:
Hopefully that has covered all the bases for restricting publishing in such a way. While I agree that this is a very abstract requirement, most of these customisations will be handy in some other context along the way.