Click here to Skip to main content
15,881,455 members
Articles / Web Development / HTML

Gallery Server Pro - An ASP.NET Gallery for Sharing Photos, Video, Audio and Other Media

Rate me:
Please Sign up or sign in to vote.
4.86/5 (131 votes)
18 Oct 2013GPL331 min read 824.2K   539  
Gallery Server Pro is a complete, stable ASP.NET gallery for sharing photos, video, audio and other media. This article presents the overall architecture and major features.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Web;
using System.Web.SessionState;
using GalleryServerPro.Business;
using GalleryServerPro.Business.Interfaces;
using GalleryServerPro.ErrorHandler.CustomExceptions;
using GalleryServerPro.Web.Controller;
using Image = System.Drawing.Image;

namespace GalleryServerPro.Web.Handler
{
	/// <summary>
	/// Defines a handler that sends the specified media object to the output stream.
	/// </summary>
	[System.Web.Services.WebService(Namespace = "http://tempuri.org/")]
	[System.Web.Services.WebServiceBinding(ConformsTo = System.Web.Services.WsiProfiles.BasicProfile1_1)]
	public class getmediaobject : IHttpHandler, IReadOnlySessionState
	{
		#region Private Fields

		private static int _bufferSize;

		private HttpContext _context;
		private int _galleryIdInQueryString = int.MinValue;
		private int _galleryId = int.MinValue;
		private int _mediaObjectId;
		private DisplayObjectType _displayType;

		private IGalleryObject _mediaObject;
		private string _mediaObjectFilePath;
		private IGallerySettings _gallerySetting;

		#endregion

		#region Properties

		/// <summary>
		/// Gets the media object being requested. Guaranteed to not return null; returns <see cref="Business.NullObjects.NullGalleryObject" />
		/// when no media object is being requested or it is invalid. This property does not verify the user has permission to view the
		/// media object.
		/// </summary>
		/// <value>The media object being requested.</value>
		private IGalleryObject MediaObject
		{
			get
			{
				if (_mediaObject == null)
				{
					if (_mediaObjectId > int.MinValue)
					{
						try
						{
							_mediaObject = Factory.LoadMediaObjectInstance(_mediaObjectId);
						}
						catch (InvalidMediaObjectException)
						{
							_mediaObject = new Business.NullObjects.NullGalleryObject();
						}
					}
					else
					{
						_mediaObject = new Business.NullObjects.NullGalleryObject();
					}
				}

				return _mediaObject;
			}
		}

		/// <summary>
		/// Gets the file path to the requested media object. It will be the thumbnail, optimized, or original file depending
		/// on which version is being requested.
		/// </summary>
		/// <value>The file path to the requested media object.</value>
		private string MediaObjectFilePath
		{
			get
			{
				if (_mediaObjectFilePath == null)
				{
					switch (_displayType)
					{
						case DisplayObjectType.Thumbnail:
							_mediaObjectFilePath = MediaObject.Thumbnail.FileNamePhysicalPath;
							break;
						case DisplayObjectType.Optimized:
							_mediaObjectFilePath = MediaObject.Optimized.FileNamePhysicalPath;
							break;
						case DisplayObjectType.Original:
							_mediaObjectFilePath = MediaObject.Original.FileNamePhysicalPath;
							break;
					}
				}

				return _mediaObjectFilePath;
			}
		}

		/// <summary>
		/// Gets the gallery ID associated with the media object being requested. If no media object is available (perhaps an empty
		/// album thumbnail is being requested), then use the gallery ID specified in the query string.
		/// </summary>
		/// <value>The gallery ID.</value>
		private int GalleryId
		{
			get
			{
				if (_galleryId == int.MinValue)
				{
					if (!(MediaObject is Business.NullObjects.NullGalleryObject))
					{
						_galleryId = MediaObject.GalleryId;
					}
					else
					{
						_galleryId = _galleryIdInQueryString;
					}
				}

				return _galleryId;
			}
		}

		/// <summary>
		/// Gets the gallery settings for the gallery the requested media object is in.
		/// </summary>
		/// <value>The gallery settings.</value>
		private IGallerySettings GallerySettings
		{
			get
			{
				if (_gallerySetting == null)
				{
					_gallerySetting = Factory.LoadGallerySetting(GalleryId);
				}

				return _gallerySetting;
			}
		}

		/// <summary>
		/// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
		/// </summary>
		/// <value></value>
		/// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.
		/// </returns>
		public bool IsReusable
		{
			get
			{
				return false;
			}
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
		/// </summary>
		/// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
		public void ProcessRequest(HttpContext context)
		{
			// Send the specified media object to the output stream.
			// Expected format:
			// /dev/gs/handler/getmediaobject.ashx?moid=34&amp;dt=1&amp;g=1
			// moid: The media object ID. Is int.MinValue if an empty image is to be returned.
			// dt: The display type. It is an integer that maps to the enum GalleryServerPro.Business.DisplayObjectType.
			// (0=Unknown,1=Thumbnail,2=Optimized,3=Original) At present all values other than 3 (Original) are ignored. If 3,
			// a security check is done to make sure user is authorized to view original images.
			// g: The gallery ID.
			// If URL encryption is enabled, then the entire query string portion is encrypted.
			try
			{
				if (!GalleryController.IsInitialized)
				{
					GalleryController.InitializeGspApplication();
				}

				if (InitializeVariables(context))
				{
					if (!IsUserAuthorized())
					{
						this._context.Response.StatusCode = 403;
						this._context.Response.End();
					}

					if (IsMediaObjectRequest() && !DoesMediaObjectExist())
					{
						this._context.Response.StatusCode = 404;
						this._context.Response.End();
					}

					ShowMediaObject();
				}
			}
			catch (System.Threading.ThreadAbortException)
			{
				throw; // We don't want these to fall into the generic catch because we don't want them logged.
			}
			catch (Exception ex)
			{
				AppErrorController.LogError(ex);
			}
		}

		#endregion

		#region Private methods

		/// <summary>
		/// Initialize the class level variables with information from the query string. Returns false if the variables cannot 
		/// be properly initialized.
		/// </summary>
		/// <param name="context">The HttpContext for the current request.</param>
		/// <returns>Returns true if all variables were initialized; returns false if there was a problem and one or more variables
		/// could not be set.</returns>
		private bool InitializeVariables(HttpContext context)
		{
			this._context = context;

			if (!ExtractQueryStringParms(context.Request.Url.Query))
				return false;

			if (_bufferSize == 0)
			{
				_bufferSize = AppSetting.Instance.MediaObjectDownloadBufferSize;
			}

			if (DisplayObjectTypeEnumHelper.IsValidDisplayObjectType(this._displayType))
			{
				return true;
			}
			else
				return false;
		}

		/// <summary>
		/// Extract information from the query string and assign to our class level variables. Return false if something goes wrong
		/// and the variables cannot be set. This will happen when the query string is in an unexpected format.
		/// </summary>
		/// <param name="queryString">The query string for the current request. Can be populated with HttpContext.Request.Url.Query.
		/// Must start with a question mark (?).</param>
		/// <returns>Returns true if all relevant variables were assigned from the query string; returns false if there was a problem.</returns>
		private bool ExtractQueryStringParms(string queryString)
		{
			if (String.IsNullOrEmpty(queryString)) return false;

			queryString = queryString.Remove(0, 1); // Strip off the ?

			bool filepathIsEncrypted = AppSetting.Instance.EncryptMediaObjectUrlOnClient;
			if (filepathIsEncrypted)
			{
				// Decode, then decrypt the query string. Note that we must replace spaces with a '+'. This is required when the the URL is
				// used in javascript to create the Silverlight media player. Apparently, Silverlight or the media player javascript decodes
				// the query string when it requests the URL, so that means any instances of '%2b' are decoded into '+' before it gets here.
				// Ideally, we wouldn't even call UrlDecode in this case, but we don't have a way of knowing that it has already been decoded.
				// So we decode anyway, which doesn't cause any harm *except* it converts '+' to a space, so we need to convert them back.
				queryString = HelperFunctions.Decrypt(HttpUtility.UrlDecode(queryString).Replace(" ", "+"));
			}

			//moid={0}&dt={1}g={2}
			foreach (string nameValuePair in queryString.Split(new char[] { '&' }))
			{
				string[] nameOrValue = nameValuePair.Split(new char[] { '=' });
				switch (nameOrValue[0])
				{
					case "g":
						{
							int gid;
							if (Int32.TryParse(nameOrValue[1], out gid))
								_galleryIdInQueryString = gid;
							else
								return false;
							break;
						}
					case "moid":
						{
							int moid;
							if (Int32.TryParse(nameOrValue[1], out moid))
								_mediaObjectId = moid;
							else
								return false;
							break;
						}
					case "dt":
						{
							int dtInt;
							if (Int32.TryParse(nameOrValue[1], out dtInt))
							{
								if (DisplayObjectTypeEnumHelper.IsValidDisplayObjectType((DisplayObjectType)dtInt))
								{
									_displayType = (DisplayObjectType)dtInt; break;
								}
								else
									return false;
							}
							else
								return false;
						}
					default: return false; // Unexpected query string parm. Return false so execution is aborted.
				}
			}

			ValidateDisplayType();

			return true;
		}

		/// <summary>
		/// If an optimized version is being requested, make sure a file name is specified for it. If not, switch to the original version.
		/// This switch will be necessary for most non-image media objects, since the client usually requests optimized versions for everything.
		/// </summary>
		/// <remarks>This function became necessary when switching to the ID-based request in 2.4 (rather than the file-based request). It was 
		/// considered to change the requesting logic to ensure the correct display type is specified, and while that seems preferable from an
		/// architectural perspective, it was more complex to implement and potentially more fragile than this simple function.</remarks>
		private void ValidateDisplayType()
		{
			if ((_displayType == DisplayObjectType.Optimized) && (String.IsNullOrEmpty(MediaObjectFilePath)))
			{
				_displayType = DisplayObjectType.Original;
				_mediaObjectFilePath = null;

				// Comment out the exception, as it generates unnecessary errors when bots request deleted items
				//if (String.IsNullOrEmpty(MediaObjectFilePath))
				//{
				//  throw new InvalidMediaObjectException(String.Format("A request was made to the Gallery Server Pro HTTP handler to serve the optimized image for media object ID {0}, but either the media object does not exist or neither the optimized nor the original has a filename stored in the database, and therefore cannot be served.", _mediaObjectId));
				//}
			}
		}

		private bool IsUserAuthorized()
		{
			// If no media object is specified, then return true (this happens for empty album thumbnails).
			if (MediaObject.Id == int.MinValue)
			{
				return true;
			}

			SecurityActions requestedPermission = SecurityActions.ViewAlbumOrMediaObject;

			if ((this._displayType == DisplayObjectType.Original) && (MediaObject.MimeType.TypeCategory == MimeTypeCategory.Image))
			{
				requestedPermission = SecurityActions.ViewOriginalImage;
			}

			return Util.IsUserAuthorized(requestedPermission, RoleController.GetGalleryServerRolesForUser(), MediaObject.Parent.Id, GalleryId, MediaObject.IsPrivate);
		}

		/// <summary>
		/// Determines whether the current request is for a media object. Returns <c>true</c> when the moid query string parameter
		/// has a value greater than <see cref="Int32.MinValue" />. Empty albums using this handler to generate a default image
		/// pass Int32.MinValue for the media object ID, so in these cases this function returns <c>false</c>.
		/// </summary>
		/// <returns>
		/// 	<c>true</c> if the current request is for a media object; otherwise, <c>false</c>.
		/// </returns>
		private bool IsMediaObjectRequest()
		{
			return (_mediaObjectId > int.MinValue);
		}

		/// <summary>
		/// Returns a value indicating whether the requested media object currently exists in the gallery.
		/// </summary>
		/// <returns>
		/// 	<c>true</c> if the requested media object exists; otherwise, <c>false</c>.
		/// </returns>
		private bool DoesMediaObjectExist()
		{
			return (MediaObject.Id > int.MinValue);
		}

		private void ShowMediaObject()
		{
			if (MediaObject.Id == int.MinValue)
			{
				// A filename matching the DefaultFilename constant is our signal to generate the
				// default album thumbnail and send to client.
				ProcessDefaultThumbnail();
			}
			else
			{
				if (!MimeTypeEnumHelper.IsValidMimeTypeCategory(MediaObject.MimeType.TypeCategory))
				{
					throw new UnexpectedQueryStringException();
				}

				if ((MediaObject.MimeType.TypeCategory != MimeTypeCategory.Image) && (this._mediaObjectId > int.MinValue))
				{
					// We never apply the watermark to non-image media objects.
					ProcessMediaObject();
				}
				else
				{
					// Apply watermark to thumbnails only when the config setting applyWatermarkToThumbnails = true.
					// Apply watermark to optimized and original images only when applyWatermark = true.
					bool applyWatermark = GallerySettings.ApplyWatermark;
					bool applyWatermarkToThumbnails = GallerySettings.ApplyWatermarkToThumbnails;
					bool isThumbnail = (_displayType == DisplayObjectType.Thumbnail);

					if (AppSetting.Instance.License.IsInReducedFunctionalityMode && !isThumbnail)
					{
						ProcessMediaObjectWithWatermark();
					}
					else if ((applyWatermark && !isThumbnail) || (applyWatermark && applyWatermarkToThumbnails && isThumbnail))
					{
						// If the user belongs to a role with watermarks set to visible, then show it; otherwise don't show the watermark.
						if (Util.IsUserAuthorized(SecurityActions.HideWatermark, RoleController.GetGalleryServerRolesForUser(), MediaObject.Parent.Id, GalleryId, MediaObject.IsPrivate))
						{
							// Show the image without the watermark.
							ProcessMediaObject();
						}
						else
						{
							// Overlay watermark on image before sending it to client.
							ProcessMediaObjectWithWatermark();
						}
					}
					else
					{
						ProcessMediaObject();
					}
				}
			}
		}

		private void ProcessDefaultThumbnail()
		{
			// Generate the default album thumbnail and send to client.
			Bitmap bmp = null;
			try
			{
				this._context.Response.ContentType = "image/jpeg";

				HttpCachePolicy cachePolicy = this._context.Response.Cache;
				cachePolicy.SetExpires(DateTime.Now.AddSeconds(2592000)); // 30 days
				cachePolicy.SetCacheability(HttpCacheability.Public);
				cachePolicy.SetValidUntilExpires(true);

				bmp = GetDefaultThumbnailBitmap();
				bmp.Save(_context.Response.OutputStream, ImageFormat.Jpeg);
			}
			finally
			{
				if (bmp != null)
					bmp.Dispose();
			}
		}

		private Bitmap GetDefaultThumbnailBitmap()
		{
			//Return a bitmap of a default album image.  This will be used when no actual
			//image is available to serve as the pictorial view of the album.

			float ratio = GallerySettings.EmptyAlbumThumbnailWidthToHeightRatio;
			int maxLength = GallerySettings.MaxThumbnailLength;
			string imageText = GallerySettings.EmptyAlbumThumbnailText;
			string fontName = GallerySettings.EmptyAlbumThumbnailFontName;
			int fontSize = GallerySettings.EmptyAlbumThumbnailFontSize;
			Color bgColor = HelperFunctions.GetColor(GallerySettings.EmptyAlbumThumbnailBackgroundColor);
			Color fontColor = HelperFunctions.GetColor(GallerySettings.EmptyAlbumThumbnailFontColor);

			int rctWidth, rctHeight; //Image width and height
			int x; //Starting point from left for the text
			int y; //Start point from top for the text

			if (ratio > 1)
			{
				rctWidth = maxLength;
				rctHeight = Convert.ToInt32((float)maxLength / ratio);
			}
			else
			{
				rctHeight = maxLength;
				rctWidth = Convert.ToInt32((float)maxLength * ratio);
			}

			Bitmap bmp = null;
			Graphics g = null;
			try
			{
				// If the font name does not match an installed font, .NET will substitute Microsoft Sans Serif.
				Font fnt = new Font(fontName, fontSize);
				Rectangle rct = new Rectangle(0, 0, rctWidth, rctHeight);
				bmp = new Bitmap(rct.Width, rct.Height);
				g = Graphics.FromImage(bmp);

				// Calculate x and y offset for text
				Size textSize = g.MeasureString(imageText, fnt).ToSize();

				x = (rctWidth - textSize.Width) / 2;
				y = (rctHeight - textSize.Height) / 2;

				if (x < 0) x = 0;
				if (y < 0) y = 0;

				// Generate image
				g.FillRectangle(new SolidBrush(bgColor), rct);
				g.DrawString(imageText, fnt, new SolidBrush(fontColor), x, y);
			}
			finally
			{
				if (g != null)
					g.Dispose();
			}

			return bmp;
		}

		private void ProcessMediaObject()
		{
			// Send the specified file to the client.
			try
			{
				this._context.Response.Clear();
				this._context.Response.ContentType = MediaObject.MimeType.FullType;
				this._context.Response.Buffer = false;

				HttpCachePolicy cachePolicy = this._context.Response.Cache;
				cachePolicy.SetExpires(DateTime.Now.AddSeconds(2592000)); // 30 days
				cachePolicy.SetCacheability(HttpCacheability.Public);
				cachePolicy.SetValidUntilExpires(true);

				FileStream fileStream = null;
				try
				{
					byte[] buffer = new byte[_bufferSize];

					try
					{
						fileStream = File.OpenRead(MediaObjectFilePath);
					}
					catch (ArgumentException) { return; }			 // If the file or directory isn't found, just return. This helps avoid clogging the error log 
					catch (FileNotFoundException) { return; }	 // with entries caused by search engine retrieving media objects that have been moved or deleted.		 
					catch (DirectoryNotFoundException) { return; }

					// Required for Silverlight to properly work
					this._context.Response.AddHeader("Content-Length", fileStream.Length.ToString());

					int byteCount;
					while ((byteCount = fileStream.Read(buffer, 0, buffer.Length)) > 0)
					{
						if (this._context.Response.IsClientConnected)
						{
							this._context.Response.OutputStream.Write(buffer, 0, byteCount);
							this._context.Response.Flush();
						}
						else
						{
							return;
						}
					}
				}
				finally
				{
					if (fileStream != null)
						fileStream.Close();
				}
			}
			catch (Exception ex)
			{
				AppErrorController.LogError(ex);
			}
		}

		private void ProcessMediaObjectWithWatermark()
		{
			// Send the specified file to the client with the watermark overlayed on top.
			this._context.Response.Clear();
			this._context.Response.ContentType = MediaObject.MimeType.FullType;

			Image watermarkedImage = null;
			try
			{
				try
				{
					watermarkedImage = ImageHelper.AddWatermark(MediaObjectFilePath, MediaObject.GalleryId);
				}
				catch (Exception ex)
				{
					// Can't apply watermark to image. Substitute an error image and send that to the user.
					if (!(ex is FileNotFoundException))
					{
						// Don't log FileNotFoundException exceptions. This helps avoid clogging the error log 
						// with entries caused by search engine retrieving media objects that have been moved or deleted.
						AppErrorController.LogError(ex);
					}
					watermarkedImage = Image.FromFile(this._context.Request.MapPath(String.Concat(Util.GalleryRoot, "/images/error_48x48.png")));
				}

				watermarkedImage.Save(this._context.Response.OutputStream, ImageFormat.Jpeg);
			}
			finally
			{
				if (watermarkedImage != null)
					watermarkedImage.Dispose();
			}
		}

		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Software Developer (Senior) Tech Info Systems
United States United States
I have nearly 20 years of industry experience in software development, architecture, and Microsoft Office products. My company Tech Info Systems provides custom software development services for corporations, governments, and other organizations. Tech Info Systems is a registered member of the Microsoft Partner Program and I am a Microsoft Certified Professional Developer (MCPD).

I am the creator and lead developer of Gallery Server Pro, a free, open source ASP.NET gallery for sharing photos, video, audio, documents, and other files over the web. It has been developed over several years and has involved thousands of hours. The end result is a robust, configurable, and professional grade gallery that can be integrated into your web site, whether you are a large corporation, small business, professional photographer, or a local church.

Comments and Discussions