diff --git a/lib/SimpleSAML/Module.php b/lib/SimpleSAML/Module.php
index 5ac8c46bba1b795d0c523ed58b04fa3b57c979d2..18a332b51415af4f96bd89c4f4c26b6dc892a19c 100644
--- a/lib/SimpleSAML/Module.php
+++ b/lib/SimpleSAML/Module.php
@@ -129,6 +129,23 @@ class SimpleSAML_Module {
 		return $className;
 	}
 
+
+	/**
+	 * Get absolute URL to a specified module resource.
+	 *
+	 * This function creates an absolute URL to a resource stored under ".../modules/<module>/www/".
+	 *
+	 * @param string $resource  Resource path, on the form "<module name>/<resource>"
+	 * @return string  The absolute URL to the given resource.
+	 */
+	public static function getModuleURL($resource) {
+		assert('is_string($resource)');
+		assert('$resource[0] !== "/"');
+
+		$config = SimpleSAML_Configuration::getInstance();
+		return SimpleSAML_Utilities::selfURLhost() . '/' . $config->getBaseURL() . 'module.php/' . $resource;
+	}
+
 }
 
 ?>
\ No newline at end of file
diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php
index e32086e2c6feac5d6df6384fa185b735ee54bc22..d87c62a720102b91b5d03a84b2d00b9a7b348aed 100644
--- a/lib/SimpleSAML/XHTML/Template.php
+++ b/lib/SimpleSAML/XHTML/Template.php
@@ -190,21 +190,8 @@ class SimpleSAML_XHTML_Template {
 	 */	
 	private function includeAtTemplateBase($file) {
 		$data = $this->data;
-		$filename = $this->configuration->getPathValue('templatedir') . $this->configuration->getValue('theme.use') . '/' . $file;
 
-		if (!file_exists($filename)) {
-		
-			SimpleSAML_Logger::error($_SERVER['PHP_SELF'].' - Template: Could not find template file [' . $file . 
-				'] at [' . $filename . '] - Now trying at base');
-			
-			$filename = $this->configuration->getPathValue('templatedir') . $this->configuration->getValue('theme.base') . '/' . $file;
-			if (!file_exists($filename)) {
-				SimpleSAML_Logger::error($_SERVER['PHP_SELF'].' - Template: Could not find template file [' . $file . 
-					'] at [' . $filename . ']');
-				throw new Exception('Could not load template file [' . $file . ']');
-			}
-		
-		} 
+		$filename = $this->findTemplatePath($file);
 		
 		include($filename);
 	}
@@ -223,8 +210,16 @@ class SimpleSAML_XHTML_Template {
 		assert('is_string($name)');
 
 		if(!array_key_exists($name, $this->dictionaries)) {
-			$dictDir = $this->configuration->getPathValue('dictionarydir');
-			$this->dictionaries[$name] = $this->readDictionaryFile($dictDir . $name . '.php');
+			$sepPos = strpos($name, ':');
+			if($sepPos !== FALSE) {
+				$module = substr($name, 0, $sepPos);
+				$fileName = substr($name, $sepPos + 1);
+				$dictDir = SimpleSAML_Module::getModuleDir($module) . '/dictionaries/';
+			} else {
+				$dictDir = $this->configuration->getPathValue('dictionarydir');
+				$fileName = $name;
+			}
+			$this->dictionaries[$name] = $this->readDictionaryFile($dictDir . $fileName . '.php');
 		}
 
 		return $this->dictionaries[$name];
@@ -249,7 +244,7 @@ class SimpleSAML_XHTML_Template {
 		}
 
 		/* Check whether we should use the default dictionary or a dictionary specified in the tag. */
-		if(substr($tag, 0, 1) === '{' && preg_match('/^{(\w+?):(.*)}$/', $tag, $matches)) {
+		if(substr($tag, 0, 1) === '{' && preg_match('/^{((?:\w+:)?\w+?):(.*)}$/', $tag, $matches)) {
 			$dictionary = $matches[1];
 			$tag = $matches[2];
 		} else {
@@ -460,31 +455,99 @@ class SimpleSAML_XHTML_Template {
 	 * Show the template to the user.
 	 */
 	public function show() {
-	
-		$filename  = $this->configuration->getPathValue('templatedir') . 
-			$this->configuration->getValue('theme.use') . '/' . $this->template;
-		
 
-		if (!file_exists($filename)) {
-			SimpleSAML_Logger::warning($_SERVER['PHP_SELF'].' - Template: Could not find template file [' . $this->template . '] at [' . $filename . '] - now trying the base template');
-			
-			
-			$filename = $this->configuration->getPathValue('templatedir') . 
-				$this->configuration->getValue('theme.base') . '/' . $this->template;
-			
-
-			if (!file_exists($filename)) {
-				SimpleSAML_Logger::critical($_SERVER['PHP_SELF'].' - Template: Could not find template file [' . $this->template . '] at [' . $filename . ']');
-			
-				echo 'Fatal error: Could not find template file [' . $this->template . '] at [' . $filename . ']';
-				exit(0);
-			}
-		}
-		
+		$filename = $this->findTemplatePath($this->template);
 		require_once($filename);
 	}
-	
-	
+
+
+	/**
+	 * Find template path.
+	 *
+	 * This function locates the given template based on the template name.
+	 * It will first search for the template in the current theme directory, and
+	 * then the default theme.
+	 *
+	 * The template name may be on the form <module name>:<template path>, in which case
+	 * it will search for the template file in the given module.
+	 *
+	 * An error will be thrown if the template file couldn't be found.
+	 *
+	 * @param string $template  The relative path from the theme directory to the template file.
+	 * @return string  The absolute path to the template file.
+	 */
+	private function findTemplatePath($template) {
+		assert('is_string($template)');
+
+		$tmp = explode(':', $template, 2);
+		if (count($tmp) === 2) {
+			$templateModule = $tmp[0];
+			$templateName = $tmp[1];
+		} else {
+			$templateModule = 'default';
+			$templateName = $tmp[0];
+		}
+
+		$tmp = explode(':', $this->configuration->getValue('theme.use'), 2);
+		if (count($tmp) === 2) {
+			$themeModule = $tmp[0];
+			$themeName = $tmp[1];
+		} else {
+			$themeModule = NULL;
+			$themeName = $tmp[0];
+		}
+
+
+		/* First check the current theme. */
+		if ($themeModule !== NULL) {
+			/* .../module/<themeModule>/themes/<themeName>/<templateModule>/<templateName> */
+
+			$filename = SimpleSAML_Module::getModuleDir($themeModule) . '/themes/' . $themeName . '/' .
+				$templateModule . '/' . $templateName;
+		} elseif ($templateModule !== 'default') {
+			/* .../module/<templateModule>/templates/<themeName>/<templateName> */
+			$filename = SimpleSAML_Module::getModuleDir($templateModule) . '/templates/' .
+				$themeName . '/' . $templateName;
+		} else {
+			/* .../templates/<theme>/<templateName> */
+			$filename = $this->configuration->getPathValue('templatedir') . $themeName . '/' .
+				$templateName;
+		}
+
+		if (file_exists($filename)) {
+			return $filename;
+		}
+
+
+		/* Not found in current theme. */
+		SimpleSAML_Logger::info($_SERVER['PHP_SELF'].' - Template: Could not find template file [' .
+			$template . '] at [' . $filename . '] - now trying the base template');
+
+
+		/* Try default theme. */
+		$baseTheme = $this->configuration->getValue('theme.base');
+		if ($templateModule !== 'default') {
+			/* .../module/<templateModule>/templates/<baseTheme>/<templateName> */
+			$filename = SimpleSAML_Module::getModuleDir($templateModule) . '/templates/' .
+				$baseTheme . '/' . $templateName;
+		} else {
+			/* .../templates/<baseTheme>/<templateName> */
+			$filename = $this->configuration->getPathValue('templatedir') . $baseTheme . '/' .
+				$templateName;
+		}
+
+		if (file_exists($filename)) {
+			return $filename;
+		}
+
+
+		/* Not found in default template - log error and throw exception. */
+		$error = 'Template: Could not find template file [' . $template . '] at [' . $filename . ']';
+		SimpleSAML_Logger::critical($_SERVER['PHP_SELF'] . ' - ' . $error);
+
+		throw new Exception($error);
+	}
+
 }
 
 ?>
\ No newline at end of file
diff --git a/www/module.php b/www/module.php
new file mode 100644
index 0000000000000000000000000000000000000000..73c36b21d37c69462aceeb589f87cbe045492f11
--- /dev/null
+++ b/www/module.php
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Handler for module requests.
+ *
+ * This web page receives requests for web-pages hosted by modules, and directs them to
+ * the RequestHandler in the module.
+ *
+ * @author Olav Morken, UNINETT AS.
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+
+require_once('_include.php');
+
+SimpleSAML_Error_Assertion::installHandler();
+
+/* Index pages - filenames to attempt when accessing directories. */
+$indexFiles = array('index.php', 'index.html', 'index.htm', 'index.txt');
+
+/* MIME types - key is file extension, value is MIME type. */
+$mimeTypes = array(
+	'bml' => 'image/x-ms-bmp',
+	'css' => 'text/css',
+	'gif' => 'image/gif',
+	'htm' => 'text/html',
+	'html' => 'text/html',
+	'shtml' => 'text/html',
+	'jpe' => 'image/jpeg',
+	'jpeg' => 'image/jpeg',
+	'jpg' => 'image/jpeg',
+	'js' => 'text/javascript',
+	'pdf' => 'application/pdf',
+	'png' => 'image/png',
+	'svg' => 'image/svg+xml',
+	'svgz' => 'image/svg+xml',
+	'swf' => 'application/x-shockwave-flash',
+	'swfl' => 'application/x-shockwave-flash',
+	'txt' => 'text/plain',
+	'xht' => 'application/xhtml+xml',
+	'xhtml' => 'application/xhtml+xml',
+	);
+
+try {
+
+	if (empty($_SERVER['PATH_INFO'])) {
+		throw new SimpleSAML_Error_NotFound('No PATH_INFO to module.php');
+	}
+
+	$url = $_SERVER['PATH_INFO'];
+	assert('substr($url, 0, 1) === "/"');
+
+	$modEnd = strpos($url, '/', 1);
+	if ($modEnd === FALSE) {
+		/* The path must always be on the form /module/. */
+		throw new SimpleSAML_Error_NotFound('The URL must at least contain a module name followed by a slash.');
+	}
+
+	$module = substr($url, 1, $modEnd - 1);
+	$url = substr($url, $modEnd + 1);
+	if ($url === FALSE) {
+		$url = '';
+	}
+
+	if (!SimpleSAML_Module::isModuleEnabled($module)) {
+		throw new SimpleSAML_Error_NotFound('The module \'' . $module .
+			'\' was either not found, or wasn\'t enabled.');
+	}
+
+	/* Make sure that the request isn't suspicious (contains references to current
+	 * directory or parent directory or anything like that. Searching for './' in the
+	 * URL will detect both '../' and './'. Searching for '\' will detect attempts to
+	 * use Windows-style paths.
+	 */
+	if (strpos($url, '\\')) {
+		throw new SimpleSAML_Error_BadRequest('Requested URL contained a backslash.');
+	} elseif (strpos($url, './')) {
+		throw new SimpleSAML_Error_BadRequest('Requested URL contained \'./\'.');
+	}
+
+	$path = SimpleSAML_Module::getModuleDir($module) . '/www/' . $url;
+
+	if ($path[strlen($path)-1] === '/') {
+		/* Path ends with a slash - directory reference. Attempt to find index file
+		 * in directory.
+		 */
+		foreach ($indexFiles as $if) {
+			if (file_exists($path . $if)) {
+				$path .= $if;
+				break;
+			}
+		}
+	}
+
+	if (is_dir($path)) {
+		/* Path is a directory - maybe no index file was found in the previous step, or
+		 * maybe the path didn't end with a slash. Either way, we don't do directory
+		 * listings.
+		 */
+		throw new SimpleSAML_Error_NotFound('Directory listing not available.');
+	}
+
+	if (!file_exists($path)) {
+		/* File not found. */
+		SimpleSAML_Logger::info('Could not find file \'' . $path . '\'.');
+		throw new SimpleSAML_Error_NotFound('The URL wasn\'t found in the module.');
+	}
+
+	if (preg_match('#\.php$#', $path)) {
+		/* PHP file - attempt to run it. */
+		require($path);
+		exit();
+	}
+
+	/* Some other file type - attempt to serve it. */
+
+	/* Find MIME type for file, based on extension. */
+	if (preg_match('#\.([^/]+)$#', $path, $type)) {
+		$type = strtolower($type[1]);
+		if (array_key_exists($type, $mimeTypes)) {
+			$contentType = $mimeTypes[$type];
+		} else {
+			$contentType = mime_content_type($path);
+		}
+	} else {
+		$contentType = mime_content_type($path);
+	}
+
+	$contentLength = sprintf('%u', filesize($path)); /* Force filesize to an unsigned number. */
+
+	header('Content-Type: ' . $contentType);
+	header('Content-Length: ' . $contentLength);
+	readfile($path);
+	exit();
+
+} catch(SimpleSAML_Error_Error $e) {
+
+	$e->show();
+
+} catch(Exception $e) {
+
+	$e = new SimpleSAML_Error_Error('UNHANDLEDEXCEPTION', $e);
+	$e->show();
+
+}
+
+?>
\ No newline at end of file