Quantcast
Channel: Project Lifecycle
Viewing all 178 articles
Browse latest View live

Inspecting Sitecore Packages Before You Install Them

$
0
0

So if you've worked with Sitecore for any length of time you've probably created your own data packages to move to a remote server or installed a module from Sitecore shared source. You may have wondered what you were installing out of curiosity or fear of blowing up your installation. It's also good to know all the files created or items that you'll need to publish. One way to go about  inspecting a package is to open it first in the package designer. First open the package designer:

open package designer

Then at the top if you click on the "New" text you'll see an option to open "From Existing":

from existing

You'll be give an dialog window to choose the packages uploaded to your site:

openPackage.jpg

Here I'll select the Sitemap XML module and we'll see the package elements:

package details

From the list on the left you can select the item and view the contents of that section like you would from a package you created yourself:

item detail

You don't want to edit the package at this point but this will help you a lot if you're recieving content from a third party vendor or other questionable source and you want to make sure you're not going to be overwriting a large swath of your content tree.


Rich Text Editor Profiles

$
0
0

This article was updated on 2014.5.16 to cover how to set the profile for Sitecore 7.

While doing research for another article I came across an article on Stack Overflow asking about Rich Text Editor Profiles. The author noted that nothing had been written on the topic so I thought I might go ahead and fill in the blank. So to start from the top for anyone new to Sitecore, when you're creating fields on a template you select a Rich Text Editor as a field type. This will allow content editors to use the Telerik wysiwyg editor when they click to edit that field. If you use the stock Rich Text Editor field you'll notice that there are very few buttons to use and might wonder why it's so very bare. Sitecore does provide for a more robust toolbar but this is just one of may settings that you will have to configure to get the most use out of it. Let's walk through how you might go about doing that.

First  expand the template you're working on so that you can see the child node that has the Rich Text  Editor type selected. You'll see below that the Source field below is blank.

Rich Text Editor Field Source Blank

You'll want to click on the "Insert link" text above the Source field so that you can set it. When you do you'll be given a dialog window that will ask you to browse the content tree to select the source item. Here you'll need to drill down into the tree to /sitecore/system/Settings/Html Editor Profiles I'm going to select Rich Text Editor Medium. Now looking at the tree you may think this is the master database but it's not you're actually browsing the core, but I'll go more into that later.

Select Rich Text Editor Field Source Dialog Window

 Once you hit the "Open" button you'll be brought back to the field source and now see that there is a value selected. Hit save to store this value.

fieldSource.jpg

Now when you go to an instance of the template you're working on and open the wysiwyg editor you'll see that the toolbar has changed. I've detailed the different profile types below with their corresponding images.

RTE Default

Rich Text Editor Default

RTE Medium

 Rich Text Editor Medium

RTE IDE

 Rich Text Editor IDE

RTE FULL

 Rich Text Editor Full

You'll see that each progressive choice gives you more options. Now let's go back to the core and see what's working behind the scenes a bit. Switch to the core database and browse to /sitecore/system/Settings/Html Editor Profiles also provided in the image below.

Core Profile Item Location

 If you open the Rich Text Default folder you'll see a lot of folder and items. The most relevant is the Toolbar 1 item. If you expand that you'll see the buttons that you are provided in the editor. It's important to note what the template types of the different nodes are if/when you decide to work with them since if you don't use the provided system templates you're new custom widget likely won't load.

core default toolbar buttons

Now drilling down just a little further if you select the "Insert Sitecore Link" item, you'll see the field are few.

core insert link button

The "Click" field is really the only thing populated and you'll probably be left guessing what that actually links to. Well it's actually a reference in a javascript files located at /website/sitecore/shell/controls/rich text editor/RichText Commands.js. I've copied a snippet from the file below so that you can see what the reference looks like.

 rich text commands js file

Now you might be asking what about if I need to customize a profile or create my own buttons. Well depending on what you want to do you could just duplicate the RTE Full folder and then trim what you don't want. If on the other hand you want to create some custom buttons you may want to refer to my other article about adding custom buttons. Well hopefully you're a little bit more knowledgable than before and if you have questions hit me up on the comments section.

*Update for Sitecore 7

In Sitecore 7 the HTML editor profiles have been moved from the master db to the core. Unfortunately this means that you can no longer browse to the settings using the source button on the field item. What you will need to do is paste the path into the source field on the template item on the builder tab. Here's a screenshot:

Rich text editor builder

and here are the available paths:

  • /sitecore/system/Settings/Html Editor Profiles/Rich Text Default
  • /sitecore/system/Settings/Html Editor Profiles/Rich Text Full
  • /sitecore/system/Settings/Html Editor Profiles/Rich Text IDE
  • /sitecore/system/Settings/Html Editor Profiles/Rich Text Medium

Using Brightcove in Sitecore

$
0
0

 

update on 9.13.2011 - Sukiyoshi's open source license has changed to the MIT open source license.

update on 3.8.2012 - Sukiyoshi has been updated to work on Sitecore 6.4 and up and has a specific release for it you can jump over to the Sukiyoshi open source repositoryfor the specifics.

Over the past few years I've been fortunate enough to have had the opportunity to work with Brightcove and was given a development account for their platform that I could work against. In that time I went and took the existing .NET SDK Tanaris that had implemented the video read methods and blew out the rest of the read and write methods for the videos and playlists. I ended up learning a lot and had started my own branch of the code. I named it Sukiyoshi, after my wife. I did a lot of work on Sitecore sites at the same time and after having built More Interop with Douglas Couto, which used a lot of video, I realized that being able to outsource the video management aspect of a Sitecore website could have greatly simplified our task.

This brings us to the point of our story: Brightcove in Sitecore. There are of course a thousand ways to do this and I've probably tried half of them. My current job gives me a unique opportunity to match the potential of this project with an actual need I feel like what I've come up with is a pretty good solution. I will say there is a good deal of customization that you must be comfortable with, but it will allow you some good flexibility when it comes to multilingual, multisite installations.

The installation relies on two open source libraries and a sitecore package. I'll walk through what's in the package that you should be aware of and a bit of what you'll expect when you start working with it. Let's start with the files that get installed with the sitecore package. There's two bin files: BrightcoveSDK.dll and BrightcoveSDK.SitecoreUtil.dll. They contain the bulk of the labor. The first is just the library you need to connect to Brightcove in a .NET site without Sitecore. The other will add a lot of functionality specific to Sitecore.

bin files

Next are a handful of files that live deep in the Sitecore folder. The path is provided in the image. To add buttons and dialog windows to the Rich Text Editor there is a RichText Commands-Brightcove.js file whose content must be copied into the RichText Commands.js file. Then there is an InsertVideo folder which contains the SheerUI XmlControls and some javascript that allow you to select the player/video/playlists.

rich text editor

There's some files in the sitecore modules folder that are the what is loaded into the custom editor tabs.

sitecore module files

There is also a handful of changes that you must add to your web.config file so in the root website folder is a Brightcove.config file with those specific changes including the very important Brightcove account information, a handler definition, pipeline events etc.

brightcove config file

Inside Sitecore itself there are also a good number of items that you should know about. First in the core are the content editor tabs. These are pretty essential to giving the end user a good UI to work with when they want to view a preview a video in a player or edit an item or even add images to a video. I'll go more into what those look like further into the article.

core editor tabs

There are also two items that provide the buttons in the wysiwyg that trigger the Sheer UI XmlControl dialog windows to open. You should notice that this is only set by default to the "Full" profile and you will need to duplicate the buttons and place them in other profiles should you need to.

core html buttons

There are also a few templates that support the Video Library structure. Most of the fields on these items are also shared so that it's not languages specific and will be the same no matter what language you happen to be using.

brightcove templates

The system also supports multiple accounts so when you create each account there is a branch template that prepopulates it with the subfolders you'll need to get started.

branch templates

One of the features that is supported is the ability to link text to a popup of a video. To do this you'll be relying on a myriad of javascript libraries. To allow developers the flexibility to use the package of their choice, you can define a profile for each type of js library. There is a default, which is populated with prettyPhoto because that's what I use, but you can create others and define which domain name each should be used for. This is so that in the event that you want one of the ten sites your running to work with the newest prettiest thing, you will be able to do just that. There are two types of settings: attributes and querystring params. It allows you enough leverage to create customized profiles.

brightcove system settings

The only thing left that is provided on installation is the Brightcove Media library section. This is installed under the content node not under the media library node because it's not actual media, it's just content. It's semantic really and if you don't like the idea of it, feel free to download the source and change it to suit your needs. After installation one of the first things you'll need to do is create an account. Just right-click on the Brightcove Media node and insert and account folder.

create account

I created one named "Mark" since this will be pointing to my developer account.

new name

Once created you'll need to go to the content tab and add in your publisher id. This can be found in brightcove under the Home section if you click on the "profile" link. Punch it in and hit save.

add publisher id

Then click on the Brightcove Account tab and click on the "Update Library". I've provided some other options so that as you're library grows you're not overwriting something you may be working on or you want to limit what it is you're trying to do. For now you should have nothing so you'll just want to add new items. When it's finished, assuming you have videos and playlists in your account, you'll see the number of them displayed. There are some videos that are being transcoded that may not show up immediately. There are also other errors that could have occurred with your videos that may not allow them to be available from the API method calls so if the number of videos doesn't exactly match what you have in your account don't freak out just look at Brightcove first and see if there's something that might be blocking it first. At this point it's worth noting that Brightove's API only supports read/write for videos and playlists but not players. You will have to enter those by hand. I apologize if you have a lot of players and I've had to do a lot of these myself and I would have automated them but I don't make the rules or the API.

account updated

Okay so now we've got some media. What else is there?

account media list

Well on the Video Library item you'll see that you also have the option of updating just your videos. I thought it would be nice to have update items in a more granular way. There is also a video upload control that has it's visibility hidden. You can opt to use this if you trust yourself to do so. There is an upper limit to what can be uploaded this way. I've found that anything below 5MB is fine but there is no feedback to tell you how long it will take. Anything over 5MB will take so long it may stall and you'll never know. Also you don't really want your server killing itself trying to upload videos when it should be serving up pages, but it's there if you want or need it.

video library tab

When you click on a Video item you'll be presented with the Video tab. I've created a form that allows you the ability to update the fields that the API allows you to update. The others that can't be updated are still visible though at the bottom. If you're going to update your videos through Sitecore you should use this tab because it updates both Brightcove and the Sitecore item. You shouldn't update the content fields in the content tab because when you do an update those values, if you have update existing items, the data will be overwritten because it was never written back to Brightcove. I'm using Sitecore only to store the information locally but the master copy is Brightcove itself and will always take priority over what is stored. You can also update the video from this tab. Again a very granular level of control. From this tab you can also delete the video. It's there but don't kill yourself with it.

video tab

Also on a video is the Image Tab. This allows you to view and upload new images for the video to Brightcove. This is actually quite useful. The API doesn't query for as many image types as you can create but what I could get I do.

video image tab

On to the Playlist Library. Here you'll find the same as on the Video Library. You can update just the playlists and there are a few options. Similiarly to the Video Library Tab there is also a control that is not visible that will allow you to create playlists. Again it's up to you to uncomment it but it does work I just don't provide it by default.

playlist library tab

On the playlist item itself I've provided a form for you to update the fields and like the equivalent on the Video Tab you should do updates to your playlists through this interface because it updates both Brightcove and Sitecore. You can also update and delete the playlist from this tab. You will have to reload the children on the parent node once you do this so the tree is updated for you or you'll get null exceptions. (fun times).

playlist tab

The playlist also has the ability to add a thumbnail image. I don't know why but you can so I built it.

playlist image tab

Again as I noted the API doesn't support querying for players so you have to create them yourself. Just right-click on the Video Player Library  item and insert a Brightcove Video Player.

create a player

Once you create it you'll need to fill out a few fields. You'll need to fill out the Name, Player ID, Playlist Type, the Height and Width. The description does nothing and is just there for you to keep track. Another thing to note is that you can set the Height and Width to whatever you want. So you can create two player items that point to the same Player ID and have different dimensions. Just consider it. So once you populate the fields click save.

player item fields

Now jump over to the Video Preview Tab and you'll be able to choose, from a drop down, the videos you've got in Sitecore. You can also select some other options and preview it on the tab itself. This good if you just want to test a few things out before you go banging video items into your content fields.

 player preview tab

 

Here's a final note on the media library: you can create folders. If you've used Brightcove for any length of time you'll notice that you just have one giant list of videos. You can search but at times you just want to be able to sort the enormous volume of videos you've accumulated. I provided folders so that at least in Sitecore you can do just that. It won't affect when you update and you can add security to the folders. All new videos/playlists added to the system will show up in the root and if you enforce strict permissions someone will have to move new videos to their respective locations. I didn't use that much but if you do need it it's there.

Okay so you've installed and setup all your content and now you're ready to start adding things to your pages go to a Rich Text Field that is either has an HTML Profile with Full access which I explained earlier.

field blank

Once you open the editor you'll see two buttons: Embed Video and Insert Video Link.

brightcove rich text editor buttons

When you click on these you'll be given a dialog window. Here you can set a number of settings that will determine how the video will display.

embed window 

You should at least start by selecting a player.

select player

What you choose next will depend on the type of player you have. You can select either a video...

select video

or you can select a playlist or multiple playlists.

select playlist

Some player types will need to have playlists selected (tabbed, combo, single list). Some will need to have only a video selected (single video). If you line up the wrong choice, when you select the "Link" button you will get a warning telling you that the video player doesn't support what you tried to do.

error for video

error for playlist

Now assuming you selected the right type and didn't get a warning you'll see what is known as a web control in the wysiwyg editor. Better detail of this is give by a man named John West. Embed and object tags get stripped but storing the data in what essentially is a server control tag is the best way to get the wysiwyg editor to play nice. If you inspect the html you'll see the tag is a custom server control in the BrightcoveSDK.SitecoreUtil library. There is a pipeline event that will cause this to be rendered at runtime. It is selectable and if it is selected when you hit the embed button it will pre-load the settings. You can also double-click on it and change the values as key-value pairs. The link is created using the settings from /system/modules. This is where it determines what site you're on and which settings to use. If you are running a multisite installation, this does use the current url hostname which relies on the user being logged into the specific domain which he is working on. I believe this is preferred behavior so it shouldn't be a problem but it could given there are a lot of custom variations of Sitecore out there.

 field filled

This is the markup you'll see when you inspect the source. You'll see the server control and the anchor tag that are populated with the data from the dialog window. I'm also storing all Brightcove ID's instead of Sitecore ID's and this is to prevent content pointing to an ID in one environment from failing when it's package and installed on another environment. Notice that the url for the anchor tag is a handler url. This is because there is no formal file or location. I'm just catching the variables and displaying embed code within the window you've called from. The javascript window should be using an IFrame or something like it to load the page.

 Rich Text Editor Source

When you view the page this is what you'll see.

 

When you click on the link, here I'm using prettyPhoto, you'll see the popup window that will render the video. I would've setup a link from the wysiwyg I'm using but Sitecore Express on a hosted environment is fighting me right now so I took a screenshot instead.

 pretty photo

 So that's about it. Where do I go next you might ask. It has been suggested that I should make a custom field type or forgo the media library altogether and use a DataProvider or pull from Brightcove directly. I might do that eventually, unfortunately I don't have a need for it yet so it's not likely to happen soon. If you end up using this or thinking of some aspects that could improve it, please let me know in the comments. I'd like to hear how it gets used. If you do use it and have issues please use the issue tracker at Sukiyoshi. It's just easier for me to have it all it one place.

Cache Manager

$
0
0

So if you've ever setup caching on a Sitecore site you'd be familiar with the admin cache page. I recently had to set it up on a fairly large existing site and though there were a lot of other issues, one was that I was overwhelmed by the length of the list I had to view to get the information I needed. So I spent a little time and built a new admin cache page so that I could search just for what I wanted and clear out the excess noise. I built it to be a drop in place setup so that there are no external libraries or files. You can literally just copy these files into the same folder the original is in and you should be able to start working with it. You can get the files on the Cache Manager UI or checkout the source on Github.

Some things to note before I dive into what this does: You should see "Toggle" buttons at the top of each form region. This allows you to expand/collapse sections of the page below it. The more sites and results you'll get the more important this will become. There are also four distinct details forms. Sitecore stores cache objects by name and you'll see, on the original admin cache page that there are some names that appear several times (like sites) with slight variations (html, xsl, viewstate, etc.). The detail forms separate these values so that you have the ability to get just the html cache or just the xsl cache. The DB caches have a different subset of types and Miscellaneous are all unique by name. There is also one that doesn't really fit which is the AccessResultCache. This is storing security information and the cache keys are stored as a private class type so there's no way to know what they are. Ok let's get to some of the changes that I made and what you can do with it.

So first when you hit the page you'll see the default information such as total entries, total size and clear all button as you'd expect. Below that the first thing you'll see is a text search form.

Page Load

If you run a search for "sitecore" you'll get a few hits. This will check all caches for the text (I blanked out the names of the sites I'm working on). This doesn't support regex or anything special. At it's core compares using a contains method so you can do partial id's or paths and get results. If you enter nothing you should get all cache entries.

Search

The details form has three parts: the form, the summary and the cache entries. The form is your filter. You can say you just want to get the html cache for a single site or all sites.

 Toggle Form

Once you've decided on what to filter when you click the "Get Summary" button you will see the number of entries and size for each cache object. Basically what the original admin cache page would tell you. 

Get Summary

You can collapse the summary and click now on "Get Cache Entries". This will enumerate the individual cache keys in each of the cache objects. Be aware that if you try to get all the cache keys for all caches or a very large cache you could well be waiting a while for the results.   

Get Cache Entries

You'll also see that there are clear cache buttons. Each section has it's own and will basically run the same search with the exception that it will clear the cache on any results. On a text search you can clear individual cache keys, but on the detail forms you'll be clearing all cache keys on any resulting cache objects.

Alright that's it. Hopefully you'll get as much mileage out of this as I did.

 

Sitecore Sublayout Parameters and Datasources

$
0
0

Some of the most useful features in Sitecore are also some of the least well known. The longer you work with anything, the more you begin to understand it. With something as complex as Sitecore you really have to spend a lot of time learning what it can do before you start to really see how well it's suited to solving a lot of different problems.

Sitecore's sublayouts, for example, contain a wealth of technology that is not really documented, but it's the bedrock of an effective solution. The sublayout definitions in a presentation definition are where you determine where it appears on a page. The cache settings will really help you scale up in terms of traffic. Then there are the datasource and parameter fields. Now at first I largely ignored them because I'm often in a race to solve a dozen problems and I'm just limiting the scope of what I need to research but eventually you start to question what all these other features can do and surprisingly they can do quite a bit.

So enough gum flapping. Let's see what they do and why you'd want to use them. Depending on which version of Sitecore you're using the presentation details window may differ but you'll see something similar to what's in the image below.

 sublayout properties dialog window

The datasource is a LinkField and will be used to store the Sitecore ID of an item in your tree. Okay so if you're relatively new you may say "I'm used to expecting the datasource for a sublayout to be the context item, that's where I store all the information and that's how I like it." Well yes, often times the context item will contain a lot of the information that you'll want to display on a page but not necessarily all of it and of course you might want to setup a way to share data across a large number of pages efficiently. Big shout out to the development engineers for thinking so far ahead and providing this feature. Let's take one of I'm sure many use cases for this kind of feature. Let's say for instance you want to have a contact information list on the sidebar of a page and that information should be global. Now along with all good inheritance schemes you'll start the setup at a template's standard values level. Here you'll assign the sublayout and set the datasource to the global contact information. This will ensure that 1). you won't have to implement a library method to traverse the tree and find the global contact information on every page load and 2). that if and when you or you're client decides that some small subset of pages should have a different source for the contact information you can go to those pages and override the datasource value. Now let it sink in for a while and I'm sure you'll end up finding at least a dozen other uses for this. Really, really powerful feature here. Okay now you're all fired up and you set this up and switch over to Visual Studio and you open the code behind and realize, "How do I access that datasource?" Excellent question, and thanks to the contributions of none other than Mark Ursino here's what you'll need to do just that:

using Sitecore.Data.Items;using Sitecore.Web.UI.WebControls;private Item _dataSource = null;	/// /// Access the Item assigned in the DataSource field on a sublayout/// public Item DataSource {	get {		if (_dataSource == null)			if(Parent is Sublayout)				_dataSource = Sitecore.Context.Database.GetItem(((Sublayout)Parent).DataSource);		return _dataSource;	}}

For reference the "Parent" object is a reference to the control.Parent of a System.Web.UI.UserControl class and the Sublayout object is a reference to a Sitecore.Web.UI.WebControls class. What you'll notice is that we're accessing the DataSource property on the sublayout and retrieving a

Now on to the parameters. What you'll find here is a list of key/value pairs that you can seemingly add at infinitum. I haven't tested it but you should be able to add enough pairs to satisfy you, unless of course you're a jerk and you just have to break it to find out. In that case nothing I say will matter so forget you. For the rest of you you're probably asking why do you talk to yourself so much in your articles. Because. Just because. Anyway you're likely looking for a good use case for this kind of feature to really understand the implications of such a feature. So let's say you've got form on this sublayout and you want to popup a message indicating a privacy policy to the user before they submit information. Ok that's useful but sometimes you need to, legally, warn them every time they visit the page and other times you may only need to warn them once. You don't want to go duplicating all the functionality in the sublayout to another sublayout so that one sets a cookie and one doesn't. You'd rather just set a default to true/false parameter and then override the individual pages that differ. You'll get a lot more mileage from that single sublayout without all the fuss. Now you're again all ready to get the values and suddenly realize you have no idea how to access them. Well this time I've got you covered and since I love extension methods, that's how I've prepared the code for you. Boom.

using System.Collections.Specialized;using Sitecore.Web.UI.WebControls;public static class SublayoutExtensions{		public static NameValueCollection ParameterList(this Sublayout s) {				return Sitecore.Web.WebUtil.ParseUrlParameters(s.Parameters);		}} public static class NameValueCollectionExtensions {	public static bool HasKey(this NameValueCollection QString, string Key) {		foreach (string key in QString.Keys) {    			if (key.Equals(Key)) {     				return true;    			}   		}	return false;  	}}NameValueCollection nvc = ((Sublayout)Parent).ParameterList();if (nvc.HasKey("TestKey")) {	Response.Write(nvc["TestKey"]);}

Again like the datasource field you're going to be accessing the UserControl parent Sublayout object to get the parameters which will give you the values in a querystring format. Sitecore also provides a nifty utility class to convert that querystring into a NameValueCollection for you and I've provided an extension method for the NameValueCollection called "HasKey" because for some unforseen reason it doesn't exist.

So hopefully if you don't know about these features you'll spend a lot less time than I did figuring them out and if you have some better use cases feel free to share them in the comments.

Multi Site Rich Text Editor Stylesheet in Sitecore 6.4

$
0
0

So while doing a dry run of an upgrade from Sitecore 6.2 to Sitecore 6.4, I noticed something that stopped working in my rich text editor: dynamic stylesheets. I have a large multi-site platform and it's using code from the dynamic stylesheets SDN Article to set a unique stylesheet in the wysiwyg editor for each site. The Telerik codebase has changed a bit and the rich text editor has also. So I'll go through what I did to get it working so you can shortcut the hassle. The first thing you'll need to know is that the file loading the rich text editor is now located at EditorPage.aspx instead of Default.aspx. The class supporting this page (Sitecore.Shell.Controls.RichTextEditor.EditorPage) is also different from it's predecessor (Sitecore.Shell.Controls.RADEditor.RADEditor). I've stubbed out the page directive you'll need to update below:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="MultiSiteEditorPage.aspx.cs" Inherits="YourNamespace.MultiSiteEditorPage" %>

I've also pulled the EditorPage codebehind class together that you'll need to add to your library to set the EdtiorPage.aspx to. This class is mostly just a copy of Sitecore.Shell.Controls.RichTextEditor.EditorPage with a few specific additions. One such change is in the "OnLoad" method there is a method call to the "this.AddSiteSpecificCSS();". This method is custom coded for you to add in your own stylesheet. There are a number of settings you can customize so you may not just use this method for adding stylesheets and want to rename it. There's nothing special about the name so change it if it fits your paradigm. One way or the other you'll want to work within that method to get your specific site css files loaded.

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Web.UI;using System.Web;using Sitecore.StringExtensions;using System.Web.UI.HtmlControls;using Telerik.Web.UI;using Sitecore.Web.UI.XamlSharp.Ajax;using Sitecore.Web;using Sitecore.Diagnostics;using Sitecore.Shell.Controls.RichTextEditor.Pipelines.LoadRichTextContent;using Sitecore.Pipelines;using Sitecore.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent;using Sitecore.Shell.Applications.ContentEditor.RichTextEditor;using Sitecore.Web.UI.Sheer;using Sitecore;using Sitecore.Web.UI.WebControls;using Sitecore.Shell.Controls.RichTextEditor;using Sitecore.Security.Accounts;using Sitecore.SecurityModel;using Sitecore.Data.Items;using Sitecore.Configuration;using Sitecore.Resources.Media;using System.Web.UI.WebControls;using System.Diagnostics;using Sitecore.Sites;using System.IO;namespace Sitecore.Customization{	public class MultiSiteEditorPage : Page {		// Fields		protected Sitecore.Web.UI.HtmlControls.Button CancelButton;		protected RadEditor Editor;		protected PlaceHolder EditorClientScripts;		protected PlaceHolder EditorStyles;		protected RadFormDecorator formDecorator;		protected HtmlForm mainForm;		protected Sitecore.Web.UI.HtmlControls.Button OkButton;		protected PlaceHolder ScriptConstants;		protected RadScriptManager ScriptManager1;		protected UpdatePanel UpdatePanel1;				// Methods		private void AjaxScriptManager_OnExecute(object sender, AjaxCommandEventArgs args)		{			if (args.Name == "editorpage:accept")			{				this.OnAccept();			}			else if (args.Name == "editorpage:reject")			{				this.OnReject();			}		}		private void LoadHtml()		{			string queryString = WebUtil.GetQueryString("hdl");			Assert.IsNotNullOrEmpty(queryString, "html value handle");			string sessionString = WebUtil.GetSessionString(queryString);			WebUtil.RemoveSessionValue(queryString);			LoadRichTextContentArgs args = new LoadRichTextContentArgs(sessionString);			using (new LongRunningOperationWatcher(250, "loadRichTextContent", new string[0]))			{				CorePipeline.Run("loadRichTextContent", args);			}			this.Editor.Content = args.Content;		}		protected void OnAccept()		{			string content = base.Request.Form["EditorValue"];			SaveRichTextContentArgs args = new SaveRichTextContentArgs(content);			using (new LongRunningOperationWatcher(250, "saveRichTextContent", new string[0]))			{				CorePipeline.Run("saveRichTextContent", args);			}			if (!RichTextEditorUrl.Parse(this.Context.Request.RawUrl).IsPageEdit)			{				SheerResponse.Eval(string.Format("scRichText.saveRichText({0})", StringUtil.EscapeJavascriptString(args.Content)));			}			else			{				SheerResponse.SetDialogValue(args.Content);			}			SheerResponse.Eval("scCloseEditor();");		}		protected override void OnInit(EventArgs e)		{			base.OnInit(e);			Client.AjaxScriptManager.OnExecute += new AjaxScriptManager.ExecuteDelegate(this.AjaxScriptManager_OnExecute);		}		protected override void OnLoad(EventArgs e)		{			base.OnLoad(e);			if (!base.IsPostBack && !string.IsNullOrEmpty(WebUtil.GetQueryString("hdl")))			{				EditorConfigurationResult result;				Sitecore.Security.Accounts.User user = Sitecore.Context.User;				if (!user.IsAuthenticated)				{					user = Sitecore.Security.Accounts.User.FromName(WebUtil.GetQueryString("us"), true);				}				using (new UserSwitcher(user))				{					using (new SecurityEnabler())					{						string queryString = WebUtil.GetQueryString("so", Settings.HtmlEditor.DefaultProfile);						Item profile = Sitecore.Context.Database.GetItem(queryString);						Assert.IsTrue(profile != null, string.Format("HTML editor profile "{0}" not found.", queryString));						EditorConfiguration configuration = EditorConfiguration.Create(profile);						configuration.Language = WebUtil.GetQueryString("la");						result = configuration.Apply(this.Editor);					}				}				this.RegisterMediaPrefixes();				this.EditorClientScripts.Controls.Add(new LiteralControl(result.Scripts.ToString()));				this.EditorStyles.Controls.Add(new LiteralControl(result.Styles.ToString()));				this.RenderScriptConstants();				//call the extra function for site specific settings				this.AddSiteSpecificCSS();				this.LoadHtml();			}		}		//this is stubbed out as a sample but you'll need to implement you're own logic here.		protected void AddSiteSpecificCSS() {						// get instance of item that is edited, and its corresponding site item			Item item = Sitecore.Context.ContentDatabase.GetItem(WebUtil.GetQueryString("id"));						List<siteinfo></siteinfo> si = Factory.GetSiteInfoList().Where(siteInfo => Sitecore.Context.Item.Paths.ContentPath.Contains(siteInfo.StartItem)).ToList();			if (si.Any()) {				SiteInfo site = si.First();				if (site != null) {					//you'll want to change this to match your css file path convention					string yourLocalPath = "/css/global.css";					string siteCssPath = SitecoreUtility.PathCombine(site.PhysicalFolder, yourLocalPath);					if (File.Exists(Server.MapPath(siteCssPath)) == true)						this.Editor.CssFiles.Add(new EditorCssFile(siteCssPath));				}			}		}		protected void OnReject()		{			SheerResponse.Eval("scCloseEditor();");		}		private void RegisterMediaPrefixes()		{			StringBuilder builder = new StringBuilder();			foreach (string str in MediaManager.Provider.Config.MediaPrefixes)			{				builder.Append("|" + str.Replace(@"", @"\").Replace("/", @"/"));			}			base.ClientScript.RegisterClientScriptBlock(base.GetType(), "mediaPrefixes", "var prefixes = '" + builder + "';", true);		}		private void RenderScriptConstants()		{			string text = "
            var scClientID = '{0}';
            var scItemID = '{1}';
            var scLanguage = '{2}';".FormatWith(new object[] { this.Editor.ClientID, WebUtil.GetQueryString("id"), WebUtil.GetQueryString("la") });			this.ScriptConstants.Controls.Add(new LiteralControl(text));		}	}}

As I mentioned earlier there are a number of other settings that can be configured for each of your individual sites so you may find it makes more sense to consolidate those settings in a Tools File. If you plan to use a Tools File to configure your editor settings you can do this by setting it on the editor property like so: {this.Edtior.ToolsFile = "your string value";}. If you're curious about how to configure a tools file to add features to the editor you can try to start by looking at the tools file that Sitecore uses at /sitecore/shell/Controls/Rich Text/Editor/ToolsFile.xml or you may be better served by getting started with at Telerik's RadEditor Demo page.

There is also another way to augment how a rich text editor adds stylesheets. In the web.config there is a setting, HtmlEditor.DefaultConfigurationType, that controls the class that is used to construct the wysiwyg editor and all it's settings. This class, Sitecore.Shell.Controls.RichTextEditor.EditorConfiguration, was recently changed by Sitecore. They set several methods type to virtual to allow you to extend the class and override them. One of those methods is the SetupStylesheets. In this method you could then try to determine which site is being edited and get the local stylesheets. This would be a fairly clean way to configure a dynamic stylesheet. I didn't use this method in the example because I learned of it later, but when I looked over it there was no clear way to determine what the context item was. It might be possible to get the item ID from the querystring like is done in the previous example like this:

Item item = Sitecore.Context.ContentDatabase.GetItem(WebUtil.GetQueryString("id"));

I don't know if the context is the same or if the querystring is available. If you do try to use this method please let me know if this works for you.

Update to Sitecore Stager

$
0
0

If you're on a Sitecore environment pre-6.3 and you're using the Sitecore Stager you may have noticed that each publish will clear the entire HTML for a site. This may be fine for you but I like to have more granular control over the cache so I've updated the source code a bit to clear only entries related to the published items. I'm not going to knock the Sitecore Stager, it's an excellent utility. Plus this why they release source code in the first place.

This idea may appeal to you and if it does you should be aware of the pro's and con's of this approach. Let's first go over how cache entries are stored. Each entry is just a string that starts with a path to the sublayout it is storing. There's also language and cache parameter information appended to make the entry key unique. Let's say I have a sublayout using "vary by data". The cache entry would look like this: /sublayouts/PageContent.ascx_#lang:EN_#data:/sitecore/content/Home/SomeSubPage. You'll notice that the data parameter is the path to the page item. This is really the only information about the page that's stored. What you won't see is information about subitems or items in link fields. This means that for you to clear that page's cache entry you'll have to publish that page not just the items releated to it, which is probably why the Sitecore Stager defaults just clearing all cache. You may now be questioning whether or not you're ready to go this route but if you are you can help yourself with some additional tools like the cache manager, which is what I do.

So if you've got the source code and you're still ready to take the plunge let's get into the "how" of getting this working. First browse to /SitecoreStager/Trunk/slave/Code/CacheManager.cs and replace the code with this:

 

using System;using System.Collections;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Hosting;using Sitecore.Caching;using Sitecore.Collections;using Sitecore.Configuration;using Sitecore.Data;using Sitecore.Data.Engines;using Sitecore.Web;using Sitecore.Data.Items;namespace Sitecore.Stager{	public static class CacheManager	{		static CacheManager() {			ConfigWatcher.ConfigChanged += ConfigWatcher_ConfigChanged;		}		private static void ConfigWatcher_ConfigChanged(object sender, ConfigChangedEventArgs e) {			Log.Info("STAGER Shutting down the application due to configuration change");			HostingEnvironment.InitiateShutdown();		}		private static readonly Dictionary<string, PivotCache> pivotCaches = new Dictionary<string, PivotCache>();		public static Cache GetSqlPrefetchCache(Database database) {			return Caching.CacheManager.FindCacheByName("SqlDataProvider - Prefetch data(" + database.Name + ")");		}		public static PivotCache GetPivotCache(Database database) {			if (!pivotCaches.ContainsKey(database.Name)) {				lock (pivotCaches) {					if (!pivotCaches.ContainsKey(database.Name)) {						pivotCaches.Add(database.Name, new PivotCache(database, Settings.PivotCacheSize));					}				}			}			return pivotCaches[database.Name];		}		public static void ClearPathCache(Database database, Set<ID> toClear) {			Log.Debug("STAGER Clearing path cache. Database: " + database.Name);			foreach (ID pubID in toClear) {				Item i = database.GetItem(pubID);				if (i != null) {					string path = i.Paths.Path;					Log.Info("STAGER Clearing " + path + " from AccessResultCache");					Caching.CacheManager.GetPathCache(database).RemoveKeysContaining(path);					Caching.CacheManager.GetPathCache(database).RemoveKeysContaining(i.ID.ToString());				}			}					}		public static void ClearAccessResultCache(Database database, Set<ID> toClear) {			Log.Debug("STAGER Clearing access result cache. Database: " + database.Name);			foreach (ID pubID in toClear) {				Item i = database.GetItem(pubID);				if (i != null) {					string path = i.Paths.Path;					Log.Info("STAGER Clearing " + path + " from AccessResultCache");					Caching.CacheManager.GetAccessResultCache().RemoveKeysContaining(path);					Caching.CacheManager.GetAccessResultCache().RemoveKeysContaining(i.ID.ToString());				}			}		}		public static void ClearHtmlCaches(Database database, Set<ID> toClear) {			Log.Debug("STAGER Clearing HTML caches");			//get the unique list of site infos so you only clear cache on those sites			foreach (ID pubID in toClear) {				//find the site info for this site				SiteInfo si = GetSiteInfoByItemID(database, pubID);				if (si != null) {					Item i = database.GetItem(pubID);					if(i != null){						string path = i.Paths.Path;						Log.Info("STAGER Clearing " + path + " from " + si.Name);						si.HtmlCache.RemoveKeysContaining(path);						si.HtmlCache.RemoveKeysContaining(pubID.ToString());					}				}			}		}		public static void ClearDataItemPrefetch(Database database, IEnumerable<ID> ids) {			Log.Debug("STAGER Clearing item, data, prefetch, std values caches. Database: " + database.Name);			PivotCache cache = GetPivotCache(database);			foreach (ID id in ids) {				cache.RemoveID(id);			}		}		public static void IssueCacheCleanup(DateTime lastPublishDate, Database database) {			Log.Info("STAGER Cache cleanup issued. Last publish date: {0}, database: {1}", lastPublishDate, database.Name);			bool clearAll;			Set<ID> ids = ProcessHistoryStorage(database, lastPublishDate, out clearAll);			if (clearAll) {				database.Engines.TemplateEngine.Reset();				Caching.CacheManager.ClearAllCaches();			} else {				if (ids.Count != 0) {					ClearHtmlCaches(database, ids);					ClearAccessResultCache(database, ids);					ClearPathCache(database, ids);					ClearDataItemPrefetch(database, ids);				}			}		}		private static SiteInfo GetSiteInfoByItemID(Database db, ID itemID) {			Item i = db.GetItem(itemID);			if (i != null) {				List<SiteInfo> si = Factory.GetSiteInfoList().Where(a => a.StartItem.Length > 0 && i.Paths.Path.Contains(a.StartItem)).ToList();				if (si.Any()) {					return si[0];				}			}			return null;		}		private static Set<ID> ProcessHistoryStorage(Database database, DateTime lastPublishDate, out bool clearAll) {			if (database.Engines.HistoryEngine.Storage == null) {				clearAll = true;				Log.Error(string.Format("STAGER History engine unavailable for '{0}' database. Full cleanup issued", database.Name));				return null;			}			HistoryEntryCollection history = database.Engines.HistoryEngine.GetHistory(lastPublishDate, DateTime.UtcNow.AddYears(1).Date);			Log.Debug("STAGER History storage entries: " + history.Count);			clearAll = false;			Set<ID> set = new Set<ID>();			foreach (HistoryEntry entry in history) {				set.Add(entry.ItemId);				if (entry.AdditionalInfo.StartsWith("#")) {					var ids = entry.AdditionalInfo.Split(new[] { '#' }, StringSplitOptions.RemoveEmptyEntries);					foreach (string id in ids) {						if (id.Equals("*")) {							clearAll = true;							break;						}						set.Add(ID.Parse(id));					}				}				if (clearAll) break;			}			if (clearAll) {				Log.Debug("STAGER History storage contains 'clear all' entries. Full cleanup issued");			} else {				Log.Debug("STAGER History storage entries grouped: " + set.Count);			}			return set;		}	}}

 

What you should notice is the "IssueCacheCleanup" method. This is where all the subsidiary methods are called from. What I've changed is that each method like "ClearHtmlCaches" now takes in the Set of ID's of items that were published. Then if you jump to the "ClearHtmlCaches" method you'll see a foreach loop that iterates through those ID's, retrieves the items and calls the "RemoveKeysContaining" method with the path and ID of that item.

So that's the meat of it and again I highly recommend the Sitecore Stager, but if you're looking to get in there and tweak a few things now you know where to look.

Partial Html Cache Clearing

$
0
0

*the code was updated 8/2/2011 with some insight from Mrunal Brahmbhatt. Much appreciated.

 So I recently upgraded my system to Sitecore 6.4 from Sitecore 6.2 and was pumped about a lot of the new features like multi-browser support, new Rich Text Editor but mostly, the new built-in multi-target cache management system. Now I have to say that when I heard the words "partial cache clearing" I completely misunderstood what it meant. I thought it was partial html cache clearing thinking that when a single page got published, just that item was removed from cache. The truth is that when anyone publishes anything all html cache for all sites defined under the web.config's "publish:end" or "publish:end:remote" event are cleared. Sitecore manages a lot more cache than just html cache so by their thinking when just the html cache is cleared, that is just part of all the cache they're working with. In this way they're right, but this strategy is a problem for my particular system because of the large number of sites and editors working on it at any given time. The continually growing number of sites means that I rely on the html cache a lot to minimize the workload on the servers and keep sites loading quickly. I had solved this same issue working with the Stager Module but doing it with this new system is a bit different. Before I go into details about how to do this I will say that I am expecting that you have already setup your system as a mutli-target platform and have properly configured your ScalabilitySettings.config file. If you're looking for more on how to setup a multi-target platform then I'd suggest first starting by reviewing the scaling guide on SDN first which answered all my question about how to get it working.

Okay so the first thing you'll need to do is add a new HtmlCacheClearer class to your library which I've copied and modified from the Sitecore.Publishing.HtmlCacheClearer class.

public class HtmlCacheClearer
	{
		// Fields
		private readonly ArrayList _sites = new ArrayList();

		// Methods
		public void ClearCache(object sender, EventArgs args) {
			Assert.ArgumentNotNull(sender, "sender");
			Assert.ArgumentNotNull(args, "args");

			//THIS WILL RUN ON THE REMOTE TARGETS
			if (args.GetType().ToString().Equals("Sitecore.Data.Events.PublishEndRemoteEventArgs")) {

				PublishEndRemoteEventArgs pargs = (PublishEndRemoteEventArgs)args;

				ID did = new ID(pargs.RootItemId);
				Assert.IsNotNull(did, "publish root item id");
				Database db = Sitecore.Configuration.Factory.GetDatabase(pargs.TargetDatabaseName);
				if (db != null) {
					Item rootItem = db.GetItem(did);
					if (rootItem != null) {
						List<SiteInfo> siList = GetSiteInfo(rootItem);
						foreach(SiteInfo si in siList){
							SiteContext sc = Factory.GetSite(si.Name)
							if (sc != null) {
								HtmlCache htmlCache = CacheManager.GetHtmlCache(sc);
								if (htmlCache != null) {
									htmlCache.Clear();
								}
							}
						}
					}
				}
			}
		}

		public List<SiteInfo> GetSiteInfo(Item i){
			return Factory.GetSiteInfoList().Where(siteInfo => Sitecore.Context.Item.Paths.ContentPath.Contains(siteInfo.StartItem)).ToList();
		}

		// Properties
		public ArrayList Sites {
			get {
				return this._sites;
			}
		}
	}
   

Now you're going to want to add a reference to this class in your web.config file's events section under the "publish:end:remote" event. You'll want to change the handler from:

<event name="publish:end:remote"><handler type="Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel" method="ClearCache"><sites hint="list"><site>website</site></sites></handler></event>

to:

<event name="publish:end:remote"><handler type="YourNamespace.Publishing.HtmlCacheClearer, YourLibrary" method="ClearCache"><sites hint="list"><site>website</site></sites></handler></event>

So what changed in this modified method is we take whatever root item was published to determine which site was affected and clear all of the html cache for that site. You could, and I may later, change it so that it tries to only remove references to that single item's path and id from the html cache keys, but for now it's an improvement and I don't have a lot of time to test the best way to do that. I would say that if you're looking to do it yourself you should start with determining whether or not the PublishEndRemoteEventArgs pargs'"PublishMode" property is set to "SingleItem" or not which would tell you whether or not to do an axes query for all descendants to remove their path and id from the cache keys as well. Also by using this method you won't need to add all new sites to the list of the sites in this event unless you're going to modify the code to use the Sites ArrayList in this class. For this example I just pulled sites from the Factory.GetSiteInfoList().

It should be noted that Sitecore is not running this event during a normal page lifecycle and there won't be any access to the Log. While the Sitecore application is running, it performs a check for changes to the event queue at an interval set in the web.config file and when it sees there was a publish event and determines that it's instance name doesn't match the name that published it, it will call this pipeline event. If you do attempt to modify this code you won't be able to rely on typical messaging methods to know if you're code is failing. I was able to at least write messages to an html cache key and read it using the Cache Manager. It's not pretty but it worked. So here's to making things work.


Fix Broken User Manager

$
0
0

I've long given up on using the User Manager paging buttons until yesterday when looking over the known issues, I found an article describing a solution to my problem on SDN. Knowing at least one other individual that has had the same issue I thought I'd do a write up to bring attention to it so that the anyone else with the same issue can get back use of their user manager.  

The problem was that on the UserManager, the paging buttons (shown below) simply didn't work.

 user manager paging buttons

With a long list of users I would often cringe at the thought of the day when I wouldn't know the name of the user I was trying to work with and simply had to use the search box to find them. Luckily that day never came and as it turns out the issue was caused by a setting in the web.config which I'd only first found about a year or so ago. I'm talking about the "AutomaticDataBind" setting. If you've ever used repeaters or datagrids and had to debug the page you may have wondered why exactly the event fired twice when a page loaded. This is why. Why Sitecore originally added this into the system is probably a long and mysterious tale, the likes of which we'll never know but it's there and you can use it or not. Apparently the UserManager is negatively affected by this setting and the article I found gives two solutions to resolve the issue: 1). set the AutomaticDataBind setting to false so that it's off, or if like in my case where there's too many custom pieces of code for me to say for sure that something won't break, you can 2). override the CodeBehind class that's running the user manager layout file. To do the latter you would first need to go to the file and change the CodeBehind class path. The file is located at /sitecore/shell/Applications/Security/UserManager/UserManager.aspx. Then you'll need to create a class in your library and compile it. The class you'll need is setup like this:

using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace YourNamespace{    public class UserManager : Sitecore.Shell.Applications.Security.UserManager.UserManager    {        protected override void OnInit(EventArgs e)        {            base.OnInit(e);            //this is used to reset the binding setting after the default was set in the base.OnInit            Sitecore.Context.State.DataBind = false;        }    }}

Now that I can browse through my user list I'm now free to find and purge users that aren't part of the company as well as a list of other housekeeping duties I have to work on. Honestly I'm just glad I don't feel like a utility that I use very often is hobbled anymore. I really like it when I can fix issues like this.

Sitecore Data Importer

$
0
0

Many of the projects I've worked on over the past several years have required importing data either from SQL or from an existing Sitecore installation itself. The original application was a page form that imported data from a database and was developed by Mark Graber, Sitecore MVP and employee at Agency Oasis. Each time I've used it I improve on it and have since retooled it as an internal Sitecore application. I've recently cleaned it up to be a reusable, customizable application and am releasing it as a shared source module. You can find it on Sitecore Shared Source, or by getting the SVN repo at http://svn.sitecore.net/DataImporter. If you want to test it out you'll just need the package install. You'll also want to look over the documentation, which largely reiterates the information in this article.

There are two main pieces of the importer. The first is the import mapping which you define and then there is the application which runs the mapping. This was set up in this way so that you could test run an import by just querying for a subset of the results and then change the query and re-run an import easily. After the initial installation you'll need to login under desktop mode in the master database and browse to /sitecore/System/Modules/DataImports.

new install

By right-clicking on the "Data Imports" folder you can create Import Map Folders, a Sitecore Import Map or a SQL Import Map as shown below.

insert map

The Import Map Folders are used to help organize a large set of imports which in my experience is common. One reason is that you will often find that there is a lot of information to import and it helps to break it up into several imports so that the import processing time can be broken up a bit. 

The Sitecore Import Map is used to create new content items from existing ones. If, for example, you were upgrading an old Sitecore site with a new set of templates and wanted to copy the information from the old template model to items using the new template model. This allows you to do that. The only field that is specific to the Sitecore Import Map is the "XPath Query". This will be used by the importer to get the set of items you want to copy over.

sitecore import map

The SQL Import Map is used to create content items from content in a SQL Database. This would be useful when you're rebuilding a site that was using another system other than in Sitecore. You will save a lot of time by importing content this way. Similar to the Sitecore Import Map the SQL Import Map only has one unique field which is the "SQL Query". This is used by the importer to query for the result set to be imported.

sql import map

There are also a set of fields shared by both the Sitecore Import Map and SQL Import Map. The Base Import Map section defines several fields that help you setup your import process. The "Import To Where" field is a DropLink field and allows you choose a folder where you want to import the content to. The "Import To What Template" field defines which template the new items will be created in. The "Pull Item Name from What Fields" field is used to create the new items with a name from the comma separated set of fields provided. The "Item Name Max Length" determines how long is too long for an item name.

base import map

There is also an Item Folder section that is shared by both map types. This is useful for importing articles or people since you'll likely want to folder them by date or name. You can select either folder by date or by name. If you choose to folder by date you must provide a "Date Field" value so that the importer knows where to look for the date value. You can also select a specific folder type to folder with by using the "Folder Template" field which is a DropLink used to select a template in Sitecores template section.

item foldering

That's all there is for the setup of an import map. So far you have defined enough information to pull a dataset and create a new item for each row or item in that dataset. Now we need to define how to pass the information from the existing row or item to the new item. Generally this is handled on a field by field basis. This means that if you are importing articles you would define how to import the date field, author, article title and the article body separately. There is also one difference between importing content from Sitecore than from SQL. When importing Sitecore items you will often times want to access properties of that item and not just fields. A path to the item or the url of the item would be considered a property. This is something a SQL DataRow will not have. So when you create a Sitecore Import Map you'll see that there is a folder specifically for the properties separate from the fields. 

map default folders

There are also a number of property and field types already defined. If you find that the existing set isn't enough to fill your needs you can also create your own custom import types, which I'll describe later in this article. When you right-click on a Fields folder you'll be given a set of types shown below.

insert fields

The list of provided Field types are:

ListToGuid

This Type is used to match the import value to the display name on a list of child items. You would set the list in the field and all the child item’s names will be compared. If there is a match then the GUID of the matched item will be stored in the new field. This is useful when you’ve imported an enumeration previously and you want to import items that point to the enumeration using link fields like DropLink.

ToDate

This is used to import date values into DateTime field types.

ToStaticValue

This is used when all the import items need to have a field populated with the same value. For example if you’re importing a list of blog articles and you want to set the value of a DropLink field to the same GUID

ToText

This is used to set the text value of the import to the TextField of the new item.

UrlToLink

This is used to set the import value which would be a url to the URL property of the LinkField..

When you right-click on a Properties folder you'll be given a set of types shown below.

insert properties

The list of provided Property types are:

PathToText

This will set the Item.Paths.Path property of the imported item to a TextField on the new item

UrlToText

This will set the url of the imported item to the TextField on the new item.

Both fields and properties share some definition fields. The To What Field is the name of the field on the new item you'll be inserting the value into. The Handler Class and Handler Assembly is the class and binary file that will do the importing. This is what eventually allows you to customize your own mappings/handlers. Again more on that later.

base mapping

The ToDate, ToText and UrlToLink fields use the Field Mapping section. The From What Fields allows you to select multiple fields to populate the new field value with.

field mapping

The ToStaticValue field type has a Value Mapping section whose lone field is Value. The Value field is used so that you can set to copy a static value into a field during import

value mapping

The ListToGuid field has a Droplink Mapping section. The Source List field allows you to define the parent of a list of items.

droplink mapping

Once you've fully defined an import map you're now ready to start importing that data. You'll want to open up the Sitecore start menu and go to Development Tools->Data Import.  

application start 

You'll be presented with an internal application. For SQL imports the Connection String section drop down list will populate with connection strings defined in your web.config or connectionstrings.config files. You'll want to select the database connection that you want to import from. The Import Setting drop down will populate with any SQL Import maps that you've defined. Once you've selected the import map you want to run just hit the Import button and wait for it to finish. Depending on the number of items to import you could be waiting for a while. I recommend you break up your imports if you can. I've imported close to 7 thousand items at a time but it took a good 30 - 45 minutes to run on a fairly powerful machine. Anything in the hundreds should be reasonably quick but on large sets you should consider whether or not you can wait so long to have your browser locked up.

import app

Custom Mappings

To create your own custom mappings you will need to create a template item and class to handle the template. You could also download the source code to build the class library with your custom mappings in them or add them to your own library and reference them properly as explained below. Property Mappings are only for Sitecore imports since properties are only related to Sitecore items.

Creating the Template

You would need to define a template and extend from either BaseField or BaseProperty depending on if you are creating a Field or Property Mapping. You could also inherit from another existing class. You can also add any fields to store information you want on this template. The field values on this template will be manipulated in the class you define later. Now you would need to create a Standard Values for that template. In the Standard Values item you will see there are two inherited fields: Handler Class and Handler Assembly. These are used to determine which class runs this mapping. The “Handler Assembly” field should be populated with the “Sitecore.SharedSource.DataImporter”. This assumes you will be downloading the source and adding your fields to this class library. If you are not you can override the value in this field and then set the “Handler Class” field to the fully qualified name of your class (namespace and class name).

Creating the Class

The class you create should inherit from the BaseField or BaseProperty depending on if you are creating a Field or Property Mapping. You could also inherit from existing classes. In the new class the constructor should call the constructor of the BaseField or BaseProperty class. It should also implement the two FillField methods. One is to handle SQL imports and the other is to handle Sitecore imports. They’re separated because the import rows are different types. The FillField methods can be used to manage one field or if you’re in a hurry many fields. I was trying to define types that would be reusable so I decided to break each field into its own class but since you have access to both the existing DataRow and the new item. You can do much more than just handle a single field. You could write an entirely custom import in this one class. It’s intended for you to be able to manage the process of transforming the data so do what you will.

Insert Options

Once you’ve created your mappings update the Fields or Properties Folder insert options. This will allow you to create the mappings in the system and run the import utility against them.

Custom Importer - Support for Oracle or XML

I know that some people use Oracle. I haven’t used Oracle so I don’t know what form a resulting row is like. If it is a standard .NET DataRow You should be able to use the SQL importer. If this is not the case you need to create a new DataMap Class and sitecore template similar to the field and property mappings above. You would do the same for importing XML as well. From here you can add fields to the template and add them as properties to the class. The class should inherit from BaseDataMap and call the constructor on the BaseDataMap on its own constructor. You will also need to implement a method called Process. You can largely just copy from the SQLDataMap.Process method and make changes to query the data and loop through the result set.

When you create a new import map type you will be iterating through a different object type instead of DataRow or Items. To support this type you'll want to add a FillField method to the BaseField class so that each of the existing field classes will be forced to implement it to handle consuming the custom row's specific object type.

To support the right-click insert options you'll also want to create a branch template with a Field Folder as a child item. Change the branch item icon to match the template icon and add the new branch template to the insert options of the Data Import Folder and Import Map Folder.

Add Css Classes to Sitecore Rich Text Editor

$
0
0

There are lots of ways to add css classes to sites and I've covered how to setup stylesheets for multi-site installations, but if you're only building a single site on Sitecore and you're looking to quickly populate the class drop down, or any of the other rich text editor settings for that matter, then this is for you.

The settings for the rich text editor are stored in the Telerik Tools File. The file is located under /website/sitecore/shell/controls/Rich Text Editor/ToolFile.xml.

file location

There are all the settings for the drop down fields stored here. I'm not going to go through them all but I'll quickly run through adding the css classes. You'll want to jump to the <classes></classes> section and add a <class> tag. Then add name and value attributes for the display name and css class name respectively as shown below.

tools file

Hit save and clear your browser cache and jump into a rich text editor and check what you've got in the classes drop down list.

wysiwyg class drop down

Boom.

 

Transform Web Controls In Sitecore Rich Text Editor

$
0
0

This article is a follow-up to an article written previously by John West about embedding web controls in Rich Text Editors.

I spent some good time using web controls in the Rich Text Editor to allow content editors to implement complex functionality inline without a lot of strain. It's a step up from using code snippets where you don't want a content editor to have to switch to html mode to set html attributes or other behind-the-scenes elements.

For those of you not familiar with web controls or server controls it's basically a custom asp.net tag in the form <customTagPrefix:customTagName>. I use dialog windows to create them and, as long as your web.config setting "HtmlEditor.SupportWebControls" is set to true, the Sitecore Rich Text editor will display them with an icon like so:

 web control

But to make sure the tag isn't just spit out and treated as if it were any other html tag you'll need to transform it.

As I mentioned John West has already fully explained how to render fields by setting up a custom pipeline event handler. I ended up recently finding out that this case doesn't cover when you place the content field value on the page without using a Field Renderer. Let's say you have a field that, for whatever reason, you decided to place on a page as plain text by using it's Field.Value property. You'll find that the web controls won't be rendered and if you look at the html source you'll see them there staring you in the face plain as day. What you can do is add into your vast library of code, a utility method (pulled and modified from John West's post) that takes in the string value and transforms the web controls then returns a string value. This will turn all those fun webcontrols from boring html into amazing displays of genius. 

The method is as follows:

public static string TransformWebControls(string args) {	System.Web.UI.Page page = new System.Web.UI.Page();	page.AppRelativeVirtualPath = "/";	System.Web.UI.Control control = page.ParseControl(args);	return Sitecore.Web.HtmlUtil.RenderControl(control);}

Ok...Carry on.

Using Short IDs

$
0
0

If you've ever worked with Sitecore for any length of time you've worked with an item ID. It's in the form: {7294ECFC-0C37-44AD-B728-ABA259B18889}. There are times when you need to pass the ID around through querystring values or other visible locations but don't want to deal with all the troubles of encoding/decoding the entity characters or just because it's damn ugly. You can, however, convert the ID to just it's alpha-numeric character sequence known in Sitecore as a ShortID. When you convert the previous ID to a ShortID and print the value, the result string will look like this: 7294ECFC0C3744ADB728ABA259B18889. It's still a long value but useful in other ways. The following code sample converts a ShortID string value to an item and then back to a ShortID string.

//get item from short idstring shortID = "7294ECFC0C3744ADB728ABA259B18889";			if (!ShortID.IsShortID(shortID)){	ID id = ShortID.DecodeID(shortID);	if (!id.IsNull){		Item someItem = Sitecore.Context.Database.GetItem(id);		//turn the id back into a short id string		string sid = id.ToShortID().ToString();	}}

I know this was a short article but there wasn't much out there in terms of information on the use of ShortID. I really just ended up poking around intellisense to figure it out so I thought although it might throw it out there to add a little value to the cruft.

Setting Up a Sitecore Extranet

$
0
0

*(updated on 5/30/2014) For those interested, I have released an Extranet Module to manage creating and removing an extranet for your site. 

I've recently needed to setup an extranet for several websites and began researching. There are several helpful articles on the topic. I found bolaky.net and blog.wojciech.org as well as using Sitecore's Security API and Security Administrator cookbooks. Although there was enough information to get me started, what I found was disjointed and there were a few specifics that weren't detailed. After some testing I was able to create a functioning extranet and in an effort to create a more comprehensive article I have detailed my results.

1. Create Extranet Sublayouts

The first thing you'll want to do is to create several sublayouts that will be required by the extranet. You can use what suits you for your project but I found these to be useful: login, register user, edit account, forgot password and account navigation. It's up to you whether or not you'll create separate templates for each of these but you'll need to apply them to different pages in your site. The login, register user, edit account and forgot password sublayouts will need to have their own individual pages and urls but the navigation will most likely be added to a global page for accessibility. This will serve your users as a method of logging out (or in, if you choose), seeing their status and accessing their account. I've added the code for each of the sublayouts below. I didn't go so far as to put any form validation or spam protection as I'm sure you will customize that yourself.

Login

The login page is going to be the main gateway for your extranet. This code also has a hardcoded link to the signup and forgot password pages. It would be best for you to manage these urls in content or at least update these urls to match the locations you put those pages in. Also you're going to want to manage the value of the role for each individual site since here it's hardcoded to "Extranet Users" to match the example. Here's the .aspx code:

<div><div><asp:Literal ID="ltlMessage" runat="server" /></div><div><span>Username:</span><asp:TextBox ID="txtUser" runat="server" /><br/><span>Password:</span><asp:TextBox ID="txtPass" TextMode="Password" runat="server"  /><br/><asp:Button ID="btnLogin" Text="Submit"  runat="server"/></div><div><a href="/extranet/forgot-password.aspx">Forgot Password</a> |<a href="/extranet/register.aspx">Signup</a></div></div>

And of course the code behind:

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Sitecore.Security.Authentication;
using Sitecore.Security.Accounts;
using Sitecore.Security;
using System.Collections.Specialized;

namespace YourNamespace.Extranet
{
	public partial class Login : System.Web.UI.UserControl
	{
		protected string returnURL;

		protected void Page_Load(object sender, EventArgs e) {

			//set the return url
			if (Request.QueryString.HasKey("returnUrl") && !Request.QueryString.HasKey("returnUrl").Equals("")) {
				returnURL = Request.QueryString["returnUrl"];
			}

			//if you're logged in and you've got permissions to this site then redirect to home
			if (Sitecore.Context.IsLoggedIn && Sitecore.Context.User.Roles.Where(a => a.Name.Contains("Extranet Users")).Any()) {
				if (string.IsNullOrEmpty(returnURL)) {
					Sitecore.Web.WebUtil.Redirect("/");
				} else {
					Sitecore.Web.WebUtil.Redirect(returnURL);
				}
			}

			//if you've been redirected from an activation then show messaging
			if (Request.QueryString.HasKey("activated") && !Request.QueryString.HasKey("activated").Equals("true")) {
				//show a message explaining the user what happened.
				ltlMessage.Text = "you've activated your account!";
			}
		}

		protected void btnLogin_Click(object sender, EventArgs e) {
			if (Page.IsValid) {
				try {
					Sitecore.Security.Domains.Domain domain = Sitecore.Context.Domain;
					string domainUser = domain.Name + @"" + txtUser.Text;
					if (Sitecore.Security.Authentication.AuthenticationManager.Login(domainUser, txtPass.Text, false)) {
						if (!string.IsNullOrEmpty(returnURL)) {
							Sitecore.Web.WebUtil.Redirect(returnURL);
						} else {
							Sitecore.Web.WebUtil.Redirect("/");
						}
					} else {
						//throw new System.Security.Authentication.AuthenticationException("Invalid username or password.");
						ltlMessage.Text = "Invalid username or password.";
					}
				} catch (System.Security.Authentication.AuthenticationException) {
					ltlMessage.Text = "Processing error. Please try again later.";
				}
			}
		}
	}
}

I'm also using an extension method "HasKey" for the Request.QueryString NameValueCollection here and on a few other pages:

public static class NameValueCollectionExtensions {
	public static bool HasKey(this NameValueCollection QString, string Key) {

	foreach (string key in QString.Keys) {
		if (key.Equals(Key)) {
			return true;
		}
	}

	return false;
}

Registration

The registration page will most likely require customization. You may need to customize the field inputs to your needs since what I provide here is very basic. Consider what your user looks like and what you'll need to be storing. There is more information about customizing the user and creating a class to handle these additional fields in the Security Cookbook chapter 3.5. There is also another hardcoded link to the login page that you should update also. Here's the .aspx code:

<div><asp:PlaceHolder ID="phChoose" runat="server"><asp:LinkButton ID="lnkPass"  runat="server">
			Change Password</asp:LinkButton><br /><br /><asp:LinkButton ID="lnkEmail"  runat="server">
			Change Email</asp:LinkButton></asp:PlaceHolder><asp:Placeholder ID="phPass" Visible="false" runat="server"><h3>Change Password</h3><div class="required"><asp:Literal ID="ltlMessagePass" runat="server" /></div><div><label>Old Password</label><asp:TextBox ID="txtPassOld" TextMode="Password" runat="server" /><label>New Password</label><asp:TextBox ID="txtPassNew" TextMode="Password" runat="server" /><label>New Password Confirm</label><asp:TextBox ID="txtPassConfirm" TextMode="Password" runat="server" /><asp:Button id="btnEditPass"  text="Change Password"/></div></asp:Placeholder><asp:Placeholder ID="phEmail" Visible="false" runat="server"><h3Change Email</h3><div class="required"><asp:Literal ID="ltlMessageEmail" runat="server" /></div><div><label>Old Email</label><asp:TextBox ID="txtEmailAddressOld" runat="server"/><label>New Email</label><asp:TextBox ID="txtEmailAddressNew" runat="server" /><label>New Email Confirm</label><asp:TextBox ID="txtEmailAddressConfirm" runat="server" /><asp:Button id=""  text="Change Email"/></div></asp:Placeholder></div>

Again, like in the login page, you'll have to change the hardcoded "Extranet Users" role that is being used to something managed. I am also sending an email to the user with a registration code. The user will be created but will not be able to view the hidden pages until the role has been added to their user. This only occurs when the user verifies they are who they say they are by clicking on the link in the email. Also note that to store the email property to the user profile requires you to call the save method. We are doing a simple validation to make sure the email and password match their confirmation fields. After the user is logged in I then redirect the user to a hardcoded url. This url should be updated to match your url values. And the code behind:

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Sitecore.Security.Accounts;
using System.Collections.Generic;
using Sitecore.Data.Items;

namespace YourNamespace.Extranet
{
	public partial class Registration : System.Web.UI.UserControl {
	protected void Page_Load(object sender, EventArgs e) {
		//handle case where the id exists from a registration link
		if (Request.QueryString.HasKey("code")) {
			Guid userKey = new Guid(Request.QueryString.Get("code"));
			if (ExtranetSecurity.RegisterUser(userKey)) {
				Sitecore.Web.WebUtil.Redirect(HttpContext.Current.Request.Url.Host + "/extranet/login.aspx?activated=true");
			}
		}

		//if you're logged in and you've got permissions to this site then redirect to home
		if (Sitecore.Context.IsLoggedIn && Sitecore.Context.User.Roles.Where(a => a.Name.Contains("Extranet Users")).Any()) {
			Sitecore.Web.WebUtil.Redirect("/");
		}
	}

	protected void btnRegister_Click(object sender, EventArgs e) {
		if (Page.IsValid &&
			txtPass.Text.Equals(txtConfirmPass.Text) &&
			txtEmailAddress.Text.Equals(txtConfirmEmailAddress.Text)) {
			//create user
			string domainUser = Sitecore.Context.Domain.GetFullName(txtUser.Text);
			if (System.Web.Security.Membership.GetUser(domainUser) == null && Sitecore.Security.Accounts.User.Exists(domainUser)) {
				try {
					//create user
					User u = Sitecore.Security.Accounts.User.Create(domainUser, txtPass.Text);
					if (u == null) {
						ltlMessage.Text = "null user";
					} else {
						using (new Sitecore.SecurityModel.SecurityStateSwitcher(Sitecore.SecurityModel.SecurityState.Disabled)) {
							//add this user to the site role
							List roles = Sitecore.Context.Domain.GetRoles().Where(a => a.Name.Contains("Extranet Users")).ToList();
							if (roles.Any()) {
								//could also loop through them all if there are multiple
								//need to make sure there is a convention for knowing which to add.
								u.Roles.Add(roles.First());
							}
							//u.Profile.FullName = "some name";
							//u.Profile.Comment = "some comment";
							u.Profile.Email = txtEmailAddress.Text;
							u.Profile.Save();

							HttpRequest req = HttpContext.Current.Request;
							string body = "Hi " + txtUser.Text + ",
 Thanks for registering. " + req.Url.Host + "
 Your new password is: : " + password);
							body += "Click on the link to register and login: http://" + req.Url.Host + "/extranet/register.aspx?code=" + ((Guid)mu.ProviderUserKey).ToString() + ".");

							EmailUtility.SendMail("from@from.com", "to@to.com", "email subject - registration", body.ToString());
						}
						phForm.Visible = false;
						phMessage.Visible = true;
						ltlMessage.Text = "Your user was created and a registration email was sent to you.";
					}
				} catch (System.Web.Security.MembershipCreateUserException ex) {
					ltlMessage.Text = "Processing error. Please try again later.";// +ex.ToString();
				}
			}
		} else {
			//error
			if(!txtPass.Text.Equals(txtConfirmPass.Text)){
				ltlMessage.Text = "Passwords don't match";
			} else if(txtEmailAddress.Text.Equals(txtConfirmEmailAddress.Text)) {
				ltlMessage.Text = "Email Addresses don't match";
			}
		}
	}

	public static bool RegisterUser(Guid userKey) {

		MembershipUser newUser = Membership.GetUser(userKey);
		if (newUser != null) {
			User u = (User)User.FromName(newUser.UserName, AccountType.User);
			using (new Sitecore.SecurityModel.SecurityStateSwitcher(Sitecore.SecurityModel.SecurityState.Disabled)) {
				//add this user to the site role
				//also check if the role contains "extranet" to make sure they don't get added to the reader/editor/manager roles
				if (HasExtranetRole()) {
					List<Role> roles = Sitecore.Context.Domain.GetRoles().Where(a => a.Name.Equals("extranetYourNewRole")).ToList();
					if (roles.Any()) {
						//could also loop through them all if there are multiple
						//need to make sure there is a convention for knowing which to add.
						u.Roles.Add(roles.First());
						return true;
					}
				}
			}
		}
		return false;
	}
}

Edit Account

The edit account page is only accounting for the base level fields and anything you customize could end up here. I've only provided the ability to update the email and password but it could be used for much more. Here's the .aspx code

<div class=""><h3>Change Your Password</h3><div><asp:Literal ID="ltlMessagePass" runat="server" /></div><div><span>Old Password</span><asp:TextBox ID="txtPassOld" TextMode="Password" runat="server" /><br/><span>New Password</span><asp:TextBox ID="txtPassNew" TextMode="Password" runat="server" /><br/><span>Confirm New Password</span><asp:TextBox ID="txtPassConfirm" TextMode="Password" runat="server" /><br/><asp:Button ID="btnEditPass" Text="Submit"  runat="server"/></div><br /><br /><h3>Change Your Email Address</h3><div><asp:Literal ID="ltlMessageEmail" runat="server" /></div><div><span>Old Email Address</span><asp:TextBox ID="txtEmailAddressOld" runat="server" /><br/><span>New Email Address</span><asp:TextBox ID="txtEmailAddressNew" runat="server" /><br/><span>Confirm New Email</span><asp:TextBox ID="txtEmailAddressConfirm" runat="server" /><br/><asp:Button ID="btnEditEmail" Text="Submit"  runat="server"/></div></div>

The code behind

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Sitecore.Configuration;
using Sitecore.SecurityModel.Cryptography;
using Sitecore.Web;
using Sitecore.Security.Authentication;
using Sitecore.Security.Accounts;

namespace YourNamespace.Extranet
{
	public partial class EditAccount : System.Web.UI.UserControl
	{
		protected void Page_Load(object sender, EventArgs e) {

			//if you're not logged in you shouldn't be on this page.
			if (!(Sitecore.Context.IsLoggedIn && Sitecore.Context.User.Roles.Where(a => a.Name.Contains("Extranet Users")).Any())) {
				Response.Redirect(Sitecore.Context.Site.LoginPage);
			}

			//set the current email to the form
			txtEmailAddressOld.Text = Sitecore.Context.User.Profile.Email;
		}

		protected void lnkPass_Click(object sender, EventArgs e) {
			phChoose.Visible = false;
			phPass.Visible = true;
		}

		protected void lnkEmail_Click(object sender, EventArgs e) {
			phChoose.Visible = false;
			phEmail.Visible = true;
		}

		protected void btnEditPass_Click(object sender, EventArgs e) {
			if (Page.IsValid && txtPassNew.Text.Equals(txtPassConfirm.Text)) {
				string message = "";
				bool updated = UpdatePassword(txtPassOld.Text, txtPassNew.Text, ref message);
				ltlMessagePass.Text = message;
			} else {
				ltlMessagePass.Text = "Password doesn't match.";
			}
		}

		protected void btnEditEmail_Click(object sender, EventArgs e) {
			string newEmail = txtEmailAddressNew.Text;
			if (Page.IsValid && newEmail.Equals(txtEmailAddressConfirm.Text)) {
				string message = "";
				if(UpdateEmail(newEmail, ref message)){
					txtEmailAddressOld.Text = newEmail;
					txtEmailAddressNew.Text = "";
					txtEmailAddressConfirm.Text = "";
				}
				ltlMessageEmail.Text = message;
			} else {
				ltlMessageEmail.Text = "Email doesn't match";
			}
		}

		public static bool UpdatePassword(string oldPassword, string newPassword, ref string message) {
			//get auth helper
			AuthenticationHelper authHelper = new AuthenticationHelper(Sitecore.Security.Authentication.AuthenticationManager.Provider);
			try {
				//check to see if the existing password is correct
				if (!authHelper.ValidateUser(Sitecore.Context.User.Name, oldPassword)) {
					//throw new System.Security.Authentication.AuthenticationException("Incorrect password.");
					message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/OldPasswordIsIncorrect", LanguageFallback.English);
				} else {
					//get the current user
					System.Web.Security.MembershipUser user = System.Web.Security.Membership.GetUser(Sitecore.Context.User.Name);
					if (user.ChangePassword(oldPassword, newPassword)) {
						message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/PasswordHasBeenChanged", LanguageFallback.English);
						return true;
					} else {
						//throw new System.Security.Authentication.AuthenticationException("Unable to change password");
						message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/UnableToChangePassword", LanguageFallback.English);
					}
				}
			} catch (System.Security.Authentication.AuthenticationException) {
				message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/AuthenticationError", LanguageFallback.English);
			}
			return false;
		}

		public static bool UpdateEmail(string newEmail, ref string message) {
			//get auth helper
			AuthenticationHelper authHelper = new AuthenticationHelper(Sitecore.Security.Authentication.AuthenticationManager.Provider);
			try {
				//get the current user
				User u = Sitecore.Context.User;
				u.Profile.Email = newEmail;
				u.Profile.Save();

				message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/EmailHasChanged", LanguageFallback.English);

				return true;
			} catch (System.Security.Authentication.AuthenticationException) {
				message = SitecoreUtility.GetStringContent("/Strings/Extranet/EditAccount/EmailWasntChanged", LanguageFallback.English);
			}
			return false;
		}
	}
}

Forgot Password

There are several different ways you could handle a user forgetting his password. I've opted to reset the password and email the new value to the user. By default your Sitecore installation will not support retrieving passwords. You can setup your environment so that you will be able to do this but it's past the scope of this article. If you want the ability to retrieve passwords and understand the security implications you can start reading up on it over at MSDN.

<div><div><asp:Literal ID="ltlMessage" runat="server" /></div><span>Username</span><asp:TextBox ID="txtUser" runat="server" /><asp:Button ID="btnForgot" Text="Submit"  runat="server"/></div>

The code behind

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Configuration;
using Sitecore.Web;
using Sitecore.SecurityModel.Cryptography;

namespace YourNamespace.Extranet
{
	public partial class ForgotPassword : System.Web.UI.UserControl
	{
		protected void Page_Load(object sender, EventArgs e) {

		}

		protected void btnForgot_Click(object sender, EventArgs e) {
			if (Page.IsValid) {
				try {
					string domainUser = Sitecore.Context.Domain.GetFullName(txtUser.Text);
					if (!Sitecore.Security.Accounts.User.Exists(domainUser)) {
						//throw new System.Security.Authentication.AuthenticationException(domainUser + " does not exist.");
						ltlMessage.Text = txtUser.Text + " does not exist.";
					} else {
						System.Web.Security.MembershipUser user = System.Web.Security.Membership.GetUser(domainUser);
						if (System.Web.Security.Membership.EnablePasswordRetrieval) {
							ltlMessage.Text = "Password for " + user.UserName + ": ";
							if (System.Web.Security.Membership.RequiresQuestionAndAnswer) {
								ltlMessage.Text += user.GetPassword("");
								//MainUtil.SendMail();
							} else {
								ltlMessage.Text += user.GetPassword();
								////MainUtil.SendMail();
							}
						} else {
							//throw new System.Configuration.ConfigurationErrorsException("Cannot retrieve or reset passwords.");
							ltlMessage.Text = "Cannot retrieve password.";
						}
					}
				} catch (System.Security.Authentication.AuthenticationException) {
					ltlMessage.Text = "Processing error.";
				} catch (System.Configuration.ConfigurationErrorsException) {
					ltlMessage.Text = "Configuration error.";
				}
			}
		}

		protected void btnReset_Click(object sender, EventArgs e) {
			try {
				string domainUser = Sitecore.Context.Domain.GetFullName(txtUser.Text);
				if (!Sitecore.Security.Accounts.User.Exists(domainUser)) {
					//throw new System.Security.Authentication.AuthenticationException(domainUser + " does not exist.");
					ltlMessage.Text = txtUser.Text + " does not exist.";
				} else {
					System.Web.Security.MembershipUser user = System.Web.Security.Membership.GetUser(domainUser);
					if (System.Web.Security.Membership.EnablePasswordReset) {
						ltlMessage.Text = "New password for " + user.UserName + ": ";
						if (System.Web.Security.Membership.RequiresQuestionAndAnswer) {
							ltlMessage.Text += user.ResetPassword("");
							//MainUtil.SendMail();
						} else {
							ltlMessage.Text += user.ResetPassword();
							//MainUtil.SendMail();
						}
					} else {
						//throw new System.Configuration.ConfigurationErrorsException("Cannot retrieve or reset passwords.");
						ltlMessage.Text = "Cannot reset password.";
					}
				}
			} catch (System.Security.Authentication.AuthenticationException) {
				ltlMessage.Text = "Processing error.";
			} catch (System.Configuration.ConfigurationErrorsException) {
				ltlMessage.Text = "Configuration error.";
			}
		}
	}
}

2. Setup Security Handler

Now that the page is being hidden what we want to do is add a pipeline handler that catches the conditions where a page not found is as a result of a permissions restriction not just becuase the page doesn't exist. To do this you'll want to add a security resolver processor to the <httpRequestBegin> event pipeline in the web.config after the LayoutResolver processor. The web.config entry will look like this

<processor type="YourLibrary.Pipelines.HttpRequest.ExtranetSecurityResolver, YourLibrary"></processor>

And the class will look like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Sites;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.SecurityModel;
using Sitecore.Web;
using System.Web;
using Sitecore.Pipelines.HttpRequest;
using System.Net;
using Sitecore.Security.AccessControl;
using Sitecore.Diagnostics;
using Sitecore.Security.Accounts;
using Sitecore;
using Sitecore.IO;
using Sitecore.Data.Managers;
using Sitecore.Globalization;

namespace YourLibrary.Pipelines.HttpRequest
{
	public class ExtranetSecurityResolver : HttpRequestProcessor {
		public override void Process(HttpRequestArgs args) {
			//make sure you're on the right domain and page mode
			if (!Sitecore.Context.Domain.Name.ToLower().Contains("sitecore") && Sitecore.Context.PageMode.IsNormal) {
				// Get the site context
				SiteContext site = Sitecore.Context.Site;

				// Check if the current user has sufficient rights to enter this page
				if (SiteManager.CanEnter(site.Name, Sitecore.Context.User)) {
					string prefix = args.StartPath;

					if (args.LocalPath.Contains(Sitecore.Context.Site.StartPath))
						prefix = String.Empty;

					if (Sitecore.Context.Database == null)
						return;

					// Get the item using securityDisabler for restricted items such as permission denied items
					Item contextItem = null;
					using (new SecurityDisabler()) {
						if (Context.Database != null && args.Url.ItemPath.Length != 0) {
							string path = MainUtil.DecodeName(args.Url.ItemPath);
							Item item = args.GetItem(path);
							if (item == null) {
								path = args.LocalPath;
								item = args.GetItem(path);
							}
							if (item == null) {
								path = MainUtil.DecodeName(args.LocalPath);
								item = args.GetItem(path);
							}
							string str2 = (site != null) ? site.RootPath : string.Empty;
							if (item == null) {
								path = FileUtil.MakePath(str2, args.LocalPath, '/');
								item = args.GetItem(path);
							}
							if (item == null) {
								path = MainUtil.DecodeName(FileUtil.MakePath(str2, args.LocalPath, '/'));
								item = args.GetItem(path);
							}
							if (item == null) {
								Item root = ItemManager.GetItem(site.RootPath, Language.Current, Sitecore.Data.Version.Latest, Context.Database, SecurityCheck.Disable);
								if (root != null) {
									string path2 = MainUtil.DecodeName(args.LocalPath);
									item = this.GetSubItem(path2, root);
								}
							}
							if (item == null) {
								int index = args.Url.ItemPath.IndexOf('/', 1);
								if (index >= 0) {
									Item root = ItemManager.GetItem(args.Url.ItemPath.Substring(0, index), Language.Current, Sitecore.Data.Version.Latest, Context.Database, SecurityCheck.Disable);
									if (root != null) {
										string path3 = MainUtil.DecodeName(args.Url.ItemPath.Substring(index));
										item = this.GetSubItem(path3, root);
									}
								}
							}
							if (((item == null) && args.UseSiteStartPath) && (site != null)) {
								item = args.GetItem(site.StartPath);
							}
							contextItem = item;
						}
					}

					//Item contextItem = Sitecore.Context.Item;
					if (contextItem != null) {
						User u = Sitecore.Context.User;
						bool isAllowed = AuthorizationManager.IsAllowed(contextItem, AccessRight.ItemRead, u);

						if (!isAllowed && (site.LoginPage.Length > 0)) {
							// Redirect the user
							WebUtil.Redirect(String.Format("{0}?returnUrl={1}", site.LoginPage, HttpContext.Current.Server.HtmlEncode(HttpContext.Current.Request.RawUrl)));
						}
					}
				}
			}
                }

		private Item GetSubItem(string path, Item root) {
			Item child = root;
			foreach (string str in path.Split(new char[] { '/' })) {
				if (str.Length != 0) {
					child = this.GetChild(child, str);
					if (child == null) {
						return null;
					}
				}
			}
			return child;
		}

		private Item GetChild(Item item, string itemName) {
			foreach (Item item2 in item.Children) {
				if (item2.DisplayName.Equals(itemName, StringComparison.OrdinalIgnoreCase)) {
					return item2;
				}
				if (item2.Name.Equals(itemName, StringComparison.OrdinalIgnoreCase)) {
					return item2;
				}
			}
			return null;
		}
	}
}

One of the things that the articles I read previously had done that I had to change was how they determined if the user had the proper credentials to view the current item. I ended up changing "contextItem.Access.CanRead()" to "AuthorizationManager.IsAllowed" which cleared up the issue for me.

3. Setup Login URL

Sitecore provides a location for you to store the url to your login on your site definition. You can set this up by adding the "loginUrl" attribute to your site node in your web.config or in the "loginUrl" field of your site node (if you're using the multi site manager). It's the path, relative to your home page, to the login page. This will be used by you later when the user doesn't have the credentials to view the hidden pages.

4. Create the Extranet Role

When approaching creating a role or set of roles for your visitors you should be aware of how Sitecore sees your visitors and defines the difference between Content Authoring users from web site visitors. Sitecore, by default, has two domains: Sitecore and Extranet. The sitecore domain is used when logging into sitecore and the extranet as you may have surmised is used when you're visiting the front-end of a website. You may, for many reasons, want to define your own domain to define a separate set of users and permissions. This is supported by Sitecore but for the purposes of this article it is unnecessary. When you visit a Sitecore website and you are not logged in the Sitecore.Context.User will define you as the "Extranet/Anonymous" user. You will want to discern the particular group of visitors you wish to target by creating a new role for them to use in the Extranet domain, or your custom domain if you chose to add one. If you log into Sitecore and open the Role Manager.

role manager

Then click on the "New" button you can create a new role.

create role

I've name mine "Extranet User" and selected from the drop down field "Extranet".

5. Hide Pages by Adding Security Through Permissions

When you've created the roles that you'll want for this site you'll want to apply some security permissions relating to your site's role for the pages you want to restrict access to. The Sitecore Security Administrator's Cookbook talks about how you shouldn't specifically deny read permissions. Instead you should break inheritance. Ok, what does that mean. Well there's no single button that's going to break inheritance. You're breaking inheritance on a role or user for a particular item. Which role or user will depend on the domain or domains you're trying to restrict. For this case, since the default role that will capture all web visitors is "ExtranetAnonymous", we'll be breaking inheritance on this role. To do this, select the item you wish to restrict visibility on, jump to the security tab and click on the assign button.

security tab

The following image shows the dialog window where you will need to add the "ExtranetEveryone" role and add the following permission values to break the inheritance.

assign security extranet everyone

What this will do is break the default read for web visitors and the page will not be accessible. Then you will want to allow the read permissions for your new extranet role that registered users will have added to their account. This will allow you to set the permissions once and handle for all users. The image below shows the permissions applied to the extranet role you want to have access to these pages.

assign security extranet users

When you're finished you'll want to publish the item so that these permissions will be applied to the live content.

6. Test It!

Browse to the site and try to go to a page that is now restricted. You should end up with a Page Not Found error. This is because you've just restricted the access and Sitecore is behaving in the way you'd expect. Now you'll want to jump to the registration page and register a user. You should receive an email which then let's you register/login. Now try the page again. Now you should be able to both see the page in the navigation and be able to access it.

7. Check the Users

It's worth mentioning that you can now browse through the users in the user manager in Sitecore. This allows you to manually setup and manage users or if need be, disable users.

8. Additional Considerations

Navigation Permission

Now that you've got everything ready to go you're going to run into the inevitable issue of directing users to the hidden pages. You may perhaps only show links to the hidden pages once a user logs in and have a dedicated link to your extranet login. In this case you're ready to roll and start registering users. You may on the other hand want to have the navigation link directly to your hidden page and only when the user tries to go to that page will you force them to login. To do this when building your navigation you will want to disable the security when querying for the items so that there is still a link even though the current guest user is restricted from viewing these items.

Customization

This framework is a generic one that you will need to customize for your own installation. There are a few things that I will point out that you will want to consider before you deploy your installation.

Form Validation, Captcha and Honeypots
You will not want to deploy these forms without some form of protection against the internet-at-large. It's standard practice to force users to conform form values and it can be a vector of attack if you do not protect the values before you store them. Make sure if you're storing anything that you use parameterized insertion. The last thing you want is to have your data wiped when you can easily protect against it. Also consider that there are a lot of malicious individuals that you will want to protect yourself against by using some simple captcha forms and honeypots that can drastically filter spam and bots attempts from storming your castle.

Limit the number of login attempts for a given time period
This can be a very useful defense for automated attempts to prevent dictionary style login attacks. Any normal person really won't be trying to login any more than a few times before they go and lookup their password. If you're not allowing more than say, 6 attempts in a half hour, you're really limiting a brute force attack to take forever while really not inconveniencing your users at all.

Multiple Domain Support
If you have a multi-site installations you will obviously need separate roles to support these with multiple domains. You will want to consider storing the value of each role somewhere on the site node for each site so that you can do a lookup when it comes time to register a user. Another thing you should also consider is that if you're not going to be creating your own custom domains within Sitecore and plan to create all your users in the existing extranet domain, you will want to prefix user names with the site name or some other convention. This will allow you users to register separately on each different sites without the system saying that the user already exists.

Alright that about wraps it up. It took me a lot longer to write this article than I planned but hopefully you won't run into to many issues setting this up and can get things going a little quicker than I did.

Adding a button to the Rich Text Editor in Sitecore 6.4

$
0
0

Since there were some significant changes in Sitecore's RTE when they upgraded to Telerik's new libraries from Sitecore 6.3 to 6.4. I'd written previously about adding buttons to the RTE before and was lookin for a good reason to write a supplemental article. Thankfully the new editor actually simplifies a bit of what you'll need to do from the earlier version of Sitecore.

I was recently asked by a content editor how to insert a YouTube video on a page in Sitecore and after thinking about the best approach to respond with my options were to tell the user to change to HTML mode in the Rich Text Editor (RTE) or for me to install one of the Shared Source Modules. I looked over YouTube Integration and as thoughtfully designed as it was I thought it would require too much effort for something I thought could be much simpler. I decided to extend the RTE with an additional button that opened a dialog window you could paste the IFrame embed code into.

There were a few reasons I chose this route. First I wanted a way for editors to paste html code into the window without having to switch to HTML mode. Second I knew that just pasting in the IFrame html would support HTML5 and mobile browsers. This was important since we recently upgraded our Brightcove accounts to support HTML5 for mobile devices. Finally I just like the idea of content editors being able to add media inline with content, which is most often how editors ask for it.

It's important to understand a little about the lifecycle of the RTE and the buttons before you start so I'll give a quick overview and then fill in the rest of the specifics throughout the article. When you open RTE by clicking the "Show Editor" button, there are a number of css and js files and button items that are loaded. The buttons themselves are defined in the Core database, separated into profile folders so that you can setup different groups of controls for different fields. You'll find them under "/sitecore/system/Settings/Html Editor Profiles/". A button can define a Javascript click event in one of it's fields. This event will need to live in the RichText Editor.js file. It lives under "/sitecore/shell/controls/rich text editor". This click event handler method will make a call to a SheerUI control you define and can pass data through the querystring and the callback Javascript method. This SheerUI control, like most .NET pages has a class file backing it. You can get the querystring data and manipulate it and then pass it back to the javascript file that you include on the SheerUI control. This method will build you're return object and pass it back to the callback handler.

If you're still not too clear on the details don't worry I'll be walking you through the rest and I'll try to clear up the grey areas.

I started by adding a button to the rich text editor. Jump into Core to the location you want it to appear. Mine was: "/sitecore/system/Settings/Html Editor Profiles/Rich Text Default/Toolbar Links". Right-click and add an "__Html Editor Button".

insert button

I named it "Insert Iframe".

insert iframe

Under the "Configure" section at the top I changed the icon.

iframe button

Type in the name of the javascript click event. In my case it was "InsertIframe".

click event

Next we need to create the files that support the click event. You'll want to add a folder at "/sitecore/shell/controls/rich text editor". The folder I created was "InsertIframe" and in it I added two files: insertiframe.xml and insertiframe.js.

sitecore insert iframe

The InsertIframe.xml is the SheerUI control that controls the design of the popup window when you click the button. If you haven't heard of Sitecore's SheerUI already, Sitecore has a information and tutorials here. It will help you understand the basics. This file includes a Javascript file and the backing class reference. The Javascript include here DOES need to have a "." between the start and end tag to work. Why I don't know but trust me otherwise you'll lose precious daylight trying to figure out why it never loads. Here's the code for it:

<?xml version="1.0" encoding="utf-8" ?> 
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
<RichText.InsertIframe>
<FormDialog Icon="Network/32x32/link.png" Header="Insert an IFrame" Text="Insert the IFrame code to Insert." OKButton="Insert">
<script Type="text/javascript" Language="javascript" Src="InsertIframe/InsertIframe.js">.</script>
<CodeBeside Type="YourLibrary.XmlControls.InsertIframe, YourLibrary"/>
<GridPanel Width="100%" Height="100%" Style="table-layout:fixed">
<Memo ID="memCode" Style="height:100%;width:100%;border-top:1px solid #919b9c"></Memo>
</GridPanel>
</FormDialog>
</RichText.InsertIframe>
</control>

The InsertIframe.js file contains the return handler and cancel methods from the C# code. It's included in the InsertIframe.xml file. The "scClose" method builds the return object and sends it to the callback handler. Here's the code for that:

function scClose(text) {
    var returnValue = {
        Text: text
    };

    getRadWindow().close(returnValue);
}

function GetDialogArguments() {
    return getRadWindow().ClientParameters;
}

function getRadWindow() {
    if (window.radWindow) {
        return window.radWindow;
    }

    if (window.frameElement && window.frameElement.radWindow) {
        return window.frameElement.radWindow;
    }

    return null;
}

var isRadWindow = true;

var radWindow = getRadWindow();

if (radWindow) {
    if (window.dialogArguments) {
        radWindow.Window = window;
    }
}

function scCancel() {

    getRadWindow().close();
}

function scCloseWebEdit(embedTag) {
    window.returnValue = embedTag;
    window.close();
}

if (window.focus && Prototype.Browser.Gecko) {
    window.focus();
}

Then there's the class you'll need in your library. This handles any manipulation you'll want to do before pasting it back into the RTE. This is referenced on the InsertIframe.xml. OnLoad it sets the text with the querystring value. It also calls the Javascript methods at the end of the "OnOk" and "OnCancel" methods. I've labeled it InsertIframe.cs and you will need to build this class into your library. Here's the code:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Web.UI.Pages;
using Sitecore.Diagnostics;
using Sitecore;
using Sitecore.Web;
using Sitecore.Web.UI.Sheer;

namespace YourLibrary.XmlControls
{
	public class InsertIframe : DialogForm
	{
		// Fields
		protected Sitecore.Web.UI.HtmlControls.Memo memCode;

		//setup page
		protected override void OnLoad(EventArgs e) {
			Assert.ArgumentNotNull(e, "e");
			base.OnLoad(e);
			if (!Context.ClientPage.IsEvent) {
				this.Mode = WebUtil.GetQueryString("mo");
				string text = WebUtil.GetQueryString("selectedText");

				//set textbox text to selected text
				memCode.Value = text;
			}
		}

		//pressed ok
		protected override void OnOK(object sender, EventArgs args) {
			Assert.ArgumentNotNull(sender, "sender");
			Assert.ArgumentNotNull(args, "args");

			string code = memCode.Value;

			//encode it and send it back to the rich text editor
			if (this.Mode == "webedit") {
				SheerResponse.SetDialogValue(StringUtil.EscapeJavascriptString(code));
				base.OnOK(sender, args);
			} else {
				SheerResponse.Eval("scClose(" + StringUtil.EscapeJavascriptString(code) + ")");
			}
		}

		//cancelled
		protected override void OnCancel(object sender, EventArgs args) {
			Assert.ArgumentNotNull(sender, "sender");
			Assert.ArgumentNotNull(args, "args");
			if (this.Mode == "webedit") {
				base.OnCancel(sender, args);
			} else {
				SheerResponse.Eval("scCancel()");
			}
		}

		// Properties
		protected string Mode {
			get {
				string str = StringUtil.GetString(base.ServerProperties["Mode"]);
				if (!string.IsNullOrEmpty(str)) {
					return str;
				}
				return "shell";
			}
			set {
				Assert.ArgumentNotNull(value, "value");
				base.ServerProperties["Mode"] = value;
			}
		}
	}
}

Now you need to define the specific Javascript click event you referenced on the button you created in the Core database. This code needs to be added to the RichText Commands.js file. This file is loaded into the context when you open the RTE. Then when you click on the button it calls the method defined below which loads your SheerUI control. You'll notice that the SheerUI is called through a "showExternalDialog" method with querystring attached and the height, width and callback (scInsertFrame) defined in following parameters. I'm also passing in the selected text so that once you paste in an IFrame you can reselect it later and edit it. The callback method is also defined below this method and just pastes the returning value back into the RTE.

 RadEditorCommandList["InsertIframe"] = function (commandName, editor, args) {

    var html = editor.getSelectionHtml();

    scEditor = editor;

    editor.showExternalDialog(
		"/sitecore/shell/default.aspx?xmlcontrol=RichText.InsertIframe&la=" + scLanguage + "&selectedText=" + escape(html),
		null, //argument
		500, //width
		200, //height
		scInsertIframe, //callback
		null, // callback args"Insert IFrame",
		true, //modal
		Telerik.Web.UI.WindowBehaviors.Close, // behaviors
		false, //showStatusBar
		false //showTitleBar
	);
};

function scInsertIframe(sender, returnValue) {
    if (!returnValue) {
        return;
    }

    scEditor.pasteHtml(unescape(returnValue.Text), "DocumentManager");
}

Alright that's all the coding you'll need to do. Now you'll need to build the class, logout, clear your browser cache and log back in and when you open the RTE and click the button you'll see this.

 

From here you can paste in the IFrame code and change the dimensions to match and click "Insert".

 

Remember that the click event method also supports editing existing IFrame content, and well any html content, so you can select an existing item and, for example, change the dimensions.

 

So for closing remarks, this is a basic tool that really just pastes and loads HTML into a popup so you can edit it. You could use it as a tool to inspect small amounts of HTML like links, titles, images etc. instead of switching to HTML mode. Hopefully you'll at least learn a bit more about how the RTE works so that you can learn to harness it yourself and craft intuitive tools to ease the burden of content management for your editors.


Deprecated Sitecore Field Conversions

$
0
0

So while preparing my Sitecore system for a future upgrade to 6.5 I noticed that there were a lot of fields that were using deprecated field types. It's not a surprise since the original system was built on 5.3 but it's one of those things that's easily overlooked. It can seem daunting at first to update all the templates on a running system but the field types have more to do with how the fields are managed than whether or not the data gets removed from the database. The good news is that the deprecated fields all have an analogous field that it can be upgraded to easily. Well, except the server file field, which I have not found any matching field for but that's probably not being used by anyone but Sitecore. Here's what I've come up with as a cheatsheet that should speed up your upgrade a bit.

Conversion Fields:

  • html => Rich Text
  • link => General Link
  • lookup => Droplink
  • memo => Multi-Line Text
  • reference => Droptree
  • server file => I have no idea. If you use this field you'd know better than me
  • text => Single Line Text
  • tree => Droptree
  • treelist => Treelist
  • valuelookup => Droplist

So there really aren't many and like I mentioned before changing these types will only change how you interface with the stored data. It won't change the data until you edit the field. As a disclaimer, I changed and tested them locally before I pushed them live and I'd recommend the same to anyone attempting this. Please backup your db's first too, just in case.

Sitecore Symposium Kickoff

$
0
0

I'm at Sitecore Symposium in sunny Las Vegas and I'm going to be writing a series of articles covering the developer track that I'm attending. I'll be trying to live blog where I can.

So far I've met a lot of great people starting with the New England Sitecore user group that filled the plane I was on to get here. There's also a lot of great partners like Oasis, where I originally cut my teeth, and Velir. There's also a lot of great tech companies in attendance like Brightcove who is obiously near and dear to my heart.

It's Sitecore's largest most energetic showing. There's also a record number of people in attendance. All in all the event should be a standout for not only the developers who already love the platform but especially for the clients who are coalescing around Sitecore to power their growing web strategies. 

Personally I'm looking forward to catching the talks by Alex Shyba since I've never been disappointed by what he's been working on. I'll also be catching the talk about the Sitecore Brightcove connector by Oasis.

Sitecore Keynote

$
0
0

sitecore symposium kickoffImmediate: Relevant: Predictive. That's where Sitecore is taking us this year. The demo of the well known Nikam fake product website was telling of how they're looking towards a more integrated toolset. Sitecore isn't selling their product as a standalone utility, but more of a tool that helps you integrate your increasingly growing end points to help you draw a more complete image of your entity. 

Right off the bat the demo shows how Sitecore is helping "Nikam" engage the generic user by tying together their browsing, email and Facebook experience quickly and seamlessly. Of course as a developer I know there's a lot of work behind the scenes to make this magic work through DMS and the rules engine, but its great to see that Sitecore does have a vision for themselves in the future where a typical user is expecting more from a CMS than just content editing. 

Another area of Sitecore's core technology where they've greatly improved their usability is with the Page Editor. I personally have only had time to work with basic page editing, but they've crammed a lot of power into those pretty little pop ups and tricky transitions that make me want to learn it. Clearly their internal development team has been busy. 

Before I was about to post this article I was stopped by Sitecore's data visualization Path Analyzer tool. From a marketer's perspective this looks to be a go-to app. Who doesn't love being absorbed by data rich network graphs. I can see lots of people being excited by having a more focused approach to marketing which at times can seem like sharp shooting blind folded. So far I'm already seeing Sitecore bringing the heat. This should prove to be an exciting week. Well, for those of us who geek out over graphs and charts.

Developer Keynote

$
0
0

lars nielsenThe developer keynote was kicked off with the venerable Lars Nielsen whose presence implies Sitecore has been able to maintain its brain trust through it's explosive growth over recent years.

Getting into the nitty gritty, enhancement and stability are what Sitecore is focusing on this year. 

The integration with InDesign seems to be more on the enhancement side of the coin where marketers will be able to merge their print materials into their work stream.

Foundry: another technology that hasn't really received much vocal attention largely because of its targeted use for franchise syndication. 

Marketplace: This is the new and improved shared source modules destination. You can find it at http://marketplace.sitecore.net

Sitecore rocks: Sadly not a lot of developers are using it. It is a really great, free product that has a ton of power. If you haven't yet, check it out. 

6.6: I'm seeing the MVC support going to be a feature that will drive more developer interest than anything else. This could really expand the developer base by driving interest by developers of other languages like Java. There's really a lot of good use-cases for it and it has a ton of features that allow efficiency-focused developers to devise all sorts of new design patterns for solutions going forward.

Lucene: Sitecore's hidden gem. Upgraded and optimized, we're going to be seeing an even more powerful search solution. Since the first time I watched Alex Shyba give his presentation on the new Lucene I remember flexibility and speed being hallmarks of its usage. 

Page editor: what we're going to see is a much more refined UI. Inclusive with those improvements is a device simulator. This is definitely something I'm excited about this. Mobile has obviously arrived but the tools to manage this new world are only just starting to mature. Now I'm realistic about how accurate a simulator can be but it's a step in the right direction. 

Dictionary domains: Finally, mainstream multi-lingual text support within the CMS.

Version support for archive and recycle bin. Clearly the focus is on clearing up some of the pain points with the deeper management of content versioning.

Rules and workflow for user groups: What we're seeing is a deep maturation of the targeted marketing tools that have been growing over the last few years. 

Executive Insight Dashboard: This may prove to be a great tool but I can't say I've been able to spend enough time with it to judge one way or the other.

Database mirroring: Anyone relying on global scaling will be happy to know their replication needs have just gotten easier. The web.config will be the place where this is managed as you'd largely expect. 

Componetizing Sitecore's core product: They made a point to say that MVC can be added to the system as a module to prove the flexibility of the system as a whole.

Sheer UI: we're seeing a migration from a core legacy technology. I'm not shedding any tears.  The amount of control specifically built for Sheer UI that you needed to use to gain access to the powerful tools like the DropTree and MultiSelect fields are hopefully going to be ported to more readily accelerate API methods. The future looks a little brighter. New Media Library: Really not a full fledged application but more of an introduction to the new types of frontend UI code libraries that are in development and what's going to be possible in the future. This is a bit of a sleeper. I'd really like to see a lot more about this than almost anything else. Customization is, after all, the most important thing to me, as a developer.

Massive scaling: We haven't heard much about the scalability and key-value pairs for Sitecore since "Massive" being talked about a year or two ago. I'd be interested to know who's driving the support for these features and what they're using to do it. Sitecore is going with NoSQL for what they're saying is a gain in speed for these massive data sets but I'd bet you could change it given its controlled in the pipeline which is easily customizeable. 

This developer keynote was promised to be the "coolest" presentation and they pulled out all the stops. I can barely keep up with the amount of technology being talked about here. If I missed anything forgive me considering the deluge of information.

Sitecore Brightcove Connector

$
0
0

brightcove connectorI'm extremely excited about the number of people who are in attendance for the Brightcove Sitecore connector. This was as you may or may not know a labor of love for me since it's built off of my open source library Sukiyoshi on Google Code

Agency Oasis did add a number of features into the original connector such as automated system syncs, video sublayouts, patch configuration file, the analytical tracking for player events, tighter process queue control, the lucene search fields and the use of snippets instead of web controls.

This is of course a free module built on free open source software and that's really the best part about this which is how the developer community taking these tools and making them increasingly more powerful over time. 

when asked, the audience didn't have more than a handful of user so far, but given the amount of questions, from the audience, there appears to be a lot of interest in the connector. Hopefully the engagement will turn into users and that will in turn drive development of interesting new features.

If you're looking to take it for a test drive you can download the module here. It does have support for Sitecore 6.4 and 6.5. You also have access to the connector cheatsheet to get you started quickly.

Viewing all 178 articles
Browse latest View live