Simple CMS with Linq to SQL

We’ll need to add another utility helper for basic formatting and such. This is really to save time typing and to unclutter the rest of your class. There are some methods here that you really won’t be using for this example, but I’ll let them stay there in case you’ll find it helpful for another purpose.

Some of these were actually used for another project currently in the works. What you’ll really need are the last few methods in this. In your solution explorer, add a new code file and fill it with the following…

using System;
using System.IO;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Web;

namespace SimpleCMS.Helpers
{
	/// <summary>
	/// Validation, formatting and string generation method helpers
	/// </summary>
	public static class Util
	{
		public static string GenerateKeyCode()
		{
			string dt = string.Format("{0:yyyy-MM-dd}", DateTime.Now);
			string r = GenerateRandomString(5, "346789ABCDEFGHKMNPQRTUVWXYZ");

			// We use our date and the above random string and date to generate an
			// MD5 hash

			byte[] b = Encoding.ASCII.GetBytes(dt + r);
			StringBuilder ck = new StringBuilder();

			using (MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider())
			{
				b = md5.ComputeHash(b);
			}

			for (int i = 0; i < b.Length; i++)
				ck.Append(b[i].ToString("x2").ToUpper());

			// Replace those pesky confusing letters and numbers and take the
			// first 5 characters
			string c = ck.ToString()
				.Replace("0", "")
				.Replace("1", "")
				.Replace("2", "")
				.Replace("5", "")
				.Replace("I", "")
				.Replace("J", "")
				.Replace("L", "")
				.Replace("O", "")
				.Replace("S", "")
				.Substring(0, 5);

			return dt + "-" + c + "-" + r;
		}

		public static bool CheckKeyCode(string key)
		{
			// No need to check an empty key
			if (string.IsNullOrEmpty(key))
				return false;

			// No dashes means we can't check this key anyway
			if (key.IndexOf('-') < 0)
				return false;

			key = key.ToUpper();

			string[] k = key.Split('-');

			// Every single dash is needed. The order number needs to be split into
			// 5 parts.
			if (k.Length < 5)
				return false;

			// Every length has to be just right. Year is 4, month is 2, day is 2,
			// checksum is 5 and order number is 5.
			if (k[0].Length < 4 || k[1].Length < 2 || k[2].Length < 2 || 
				k[3].Length < 5 || k[4].Length < 5)
				return false;

			string dt = k[0] + "-" + k[1] + "-" + k[2];

			byte[] b = Encoding.ASCII.GetBytes(dt + k[4]);

			using (MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider())
			{
				b = md5.ComputeHash(b);
			}

			StringBuilder ck = new StringBuilder();
			for (int i = 0; i < b.Length; i++)
				ck.Append(b[i].ToString("x2").ToUpper());

			string ckv = ck.ToString()
				.Replace("0", "")
				.Replace("1", "")
				.Replace("2", "")
				.Replace("5", "")
				.Replace("I", "")
				.Replace("J", "")
				.Replace("L", "")
				.Replace("O", "")
				.Replace("S", "")
				.Substring(0, 5);

			// Match the first 5 characters of the MD5 to the given checksum
			if (ckv == k[3])
				return true;

			return false;
		}

		public static string GeneratePassword(int len)
		{
			string range = "~!@#$%_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
			return GenerateRandomString(len, range);
		}

		/// <summary>
		///  Generates a random string of given length
		/// </summary>
		/// <param name="len">Length of the generated string</param>
		/// <param name="range">Character pool</param>
		/// <returns>Generated string</returns>
		public static string GenerateRandomString(int len, string range)
		{
			Byte[] _bytes = new Byte[len];
			char[] _chars = new char[len];

			using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
			{
				rng.GetBytes(_bytes);

				for (int i = 0; i < len; i++)
					_chars[i] = range[(int)_bytes[i] % range.Length];
			}

			return new string(_chars);
		}


		public static bool VerifyChecksum(string chk, string file)
		{
			if (chk == GetChecksum(file))
				return true;

			return false;
		}

		/// <summary>
		/// Get the checksum of a local file. This is used to verify downloaded updates
		/// </summary>
		/// <param name="file">Path to file</param>
		/// <returns>Completed checksum</returns>
		public static string GetChecksum(string file)
		{
			string ret = "";
			try
			{
				using (var fs = new BufferedStream(File.OpenRead(file), 120000))
				{
					MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
					byte[] check = md5.ComputeHash(fs);
					ret = BitConverter.ToString(check).Replace("-", String.Empty);
				}
			}
			catch { }
			return ret;
		}


		/// <summary>
		/// Checks whether string has value or sets default it doesn't or is at 0
		/// </summary>
		public static int DefaultInt(string val, int d, int? min)
		{
			int tmp = 0;

			if (!Int32.TryParse(val, out tmp))
				tmp = d;

			if(min.HasValue)
				if (tmp <= min.Value) tmp = d;

			return tmp;
		}

		/// <summary>
		/// Checks whether nullable int has value or sets default it doesn't or is at 0
		/// </summary>
		public static int DefaultInt(int? val, int d)
		{
			val = val ?? d;
			if (val.Value <= 0 && val.Value != d) val = d;

			return val.Value;
		}

		/// <summary>
		/// Checks whether nullable bool has value or sets default it doesn't
		/// </summary>
		public static bool DefaultBool(bool? val, bool d)
		{
			val = val ?? d;
			return val.Value;
		}

		/// <summary>
		/// Checks whether nullable bool has value or sets default it doesn't
		/// </summary>
		public static bool DefaultBool(string val, bool d)
		{
			bool tmp = d;

			if (Boolean.TryParse(val, out tmp))
				return tmp;

			return d;
		}

		/// <summary>
		/// Checks whether nullable string has value or sets default it doesn't or is at empty
		/// </summary>
		public static string DefaultString(string val, string d)
		{
			if (string.IsNullOrEmpty(val)) val = d;

			return val;
		}

		/// <summary>
		/// Converts a string value to a DateTime object or returns the default value on failure
		/// </summary>
		public static DateTime DefaultDate(string val, DateTime d)
		{
			DateTime dt;
			if (DateTime.TryParse(val, out dt))
				return dt;

			return d;
		}
		

		public static bool ValidateFormHash(string hash)
		{
			return false;
		}

		/// <summary>
		/// Gets the current user's IP address
		/// </summary>
		public static string GetUserIP()
		{
			// Connecting through a proxy?
			string ip = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

			// None found
			if (string.IsNullOrEmpty(ip) || ip.ToLower() == "unknown")
				ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];

			return ip;
		}
	}
}

Now, right click on the Controllers folder and select Add > Controller. We’ll call this the PagesController.

Be sure to check the “Add acion methods for Create, Update…etc…” option is checked because we’re going to be extending those. Plus I’m too lazy to retype them ;)

In the newly added PagesController, change the Index ActionResult to the following…

public ActionResult Index(int? id, int? page)
{
	id = id ?? 0;
	page = page ?? 1;

	PagedList<ContentPage> pages;

	using (SimpleCMSDataContext db = new SimpleCMSDataContext())
	{
		pages = (from p in db.ContentPages
				where p.ParentId == id.Value
				select p).ToPagedList(page.Value, 10);
	}

	ViewData.Model = new PageView(pages);
	return View();
}

This is basically your index list of pages when no parent page is specified. Notice the page defaults to 1 if no page number is given. This is to make it easier on our PagedList class.

But for this page number to work (sorry for jumping around like this) go into your Global.asax and change the default route to match the following…

routes.MapRoute(
	"Default",
	"{controller}/{action}/{id}/{page}",
	new
	{
		controller = "Home",
		action = "Index",
		id = UrlParameter.Optional,
		page = UrlParameter.Optional
	}
);

Notice that all we really did was add another {page} route value. So when you’re reading a page it will be like /Pages/Read/2/5. Basically reading PageId 2 and the 5th page.

Now back in our PagesController change the Details ActionResult into Read and add the following code…

public ActionResult Read(int? id, int? page)
{
	id = id ?? 0;
	page = page ?? 1;

	ContentPage content;
	PagedList<ContentComment> comments;

	using (SimpleCMSDataContext db = new SimpleCMSDataContext())
	{
		content = (from p in db.ContentPages
			 where p.PageId == id.Value
			 select p).SingleOrDefault();

		// Increment view count
		content.ViewCount = Util.DefaultInt(content.ViewCount, 0) + 1;

		comments = (from c in db.ContentComments
				where c.PageId == id.Value
				orderby c.CreatedDate ascending
				select c).ToPagedList(page.Value, 1);

		db.SubmitChanges();
	}

	ViewData.Model = new PageView(content, comments);
	return View();
}

Notice we’re not going to be flooding the ViewData array as is the usual method for a lot of simple projects. We’re using that PageView model class we created earlier. Also note, how much it simplifies things in your controller when you don’t have ViewData[“blah”] = somedata stuff in it.

The next steps involve input from the user and we should all know the golden rule : Never trust input from the user.

But since I also want some formatting ability in the content, I opted to use Markdown by John Gruber for this. Specfically, the MarkdownSharp port of it courtesy of Jeff Atwood.

Just download the package and extract the Markdown.cs and Escape.cs code files to the Lib/Helper folder we created earlier. Then, at the top of your Controller class, add using MarkdownSharp; and you’re ready to go.

Onward to step 5…

Advertisement

8 thoughts on “Simple CMS with Linq to SQL

  1. Pingback: Simple CMS with Linq to SQL part II « This page intentionally left ugly

  2. Pingback: Forum Tables « This page intentionally left ugly

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s