Last updated September 1, 2014. Created on October 30, 2003.
Edited by killua99, keven1894, asiabrazil, ghoti. Log in to edit this page.

Drupal can display brief, "clean" URLs like those at drupal.org. For Apache sites, mod_rewrite powers this feature. On IIS you'll use either a third-party module or (on IIS7) Microsoft's URL Rewrite module to add this functionality. Refer to your specific IIS version below for details.

Some Third Party ISAPI Rewrite Modules

IIS7

The best method is Microsoft's URL Rewrite Module for IIS7, available via the Web Platform Installer on Windows Server 2008 and Vista. Download and documentation are also available on Microsoft's IIS.Net site. You can also use third party rewrite modules (see list above).

Note: Service Pack 2 for Windows Server 2008 & Vista contained important IIS7 bug fixes (KB954946) that affect how REQUEST_URI works. You can download the patch individually or SP2 from the MS website. IIS7 URL Rewrite Home contains useful explanations and links, including a walkthrough video.

You will also need to enable (if you haven't already) FastCGI.

After setting up the rewrite module and enabling FastCGI you will need to edit your site's web.config file. On IIS7, the web.config file replicates and replaces the functionality of .htaccess (which is included in your Drupal distribution). Here is the web.config file provided with the Acquia Drupal distribution for your reference (if you used the Acquia Drupal Web Platform Installer this is already included):

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
  <!-- Don't show directory listings for URLs which map to a directory. -->
    <directoryBrowse enabled="false" />

    <!--
       Caching configuration was not delegated by default. Some hosters may not delegate the caching
       configuration to site owners by default and that may cause errors when users install. Uncomment
       this if you want to and are allowed to enable caching
     -->
    <!--
    <caching>
      <profiles>
        <add extension=".php" policy="DisableCache" kernelCachePolicy="DisableCache" />
        <add extension=".html" policy="CacheForTimePeriod" kernelCachePolicy="CacheForTimePeriod" duration="14:00:00" />
      </profiles>
    </caching>
     -->

    <rewrite>
      <rules>
        <!-- rule name="postinst-redirect" stopProcessing="true">
          <match url="." />
          <action type="Rewrite" url="postinst.php"/>
        </rule -->

        <rule name="Protect files and directories from prying eyes" stopProcessing="true">
          <match url=".(engine|inc|info|install|module|profile|test|po|sh|.sql|postinst.1|theme|tpl(.php)?|xtmpl|svn-base)$|^(code-style.pl|Entries.|Repository|Root|Tag|Template|all-wcprops|entries|format)$" />
          <action type="CustomResponse" statusCode="403" subStatusCode="0" statusReason="Forbidden" statusDescription="Access is forbidden." />
        </rule>
        <rule name="Force simple error message for requests for non-existent favicon.ico" stopProcessing="true">
          <match url="favicon.ico" />
          <action type="CustomResponse" statusCode="404" subStatusCode="1" statusReason="File Not Found" statusDescription="The requested file favicon.ico was not found" />
        </rule>
                <!-- To redirect all users to access the site WITH the 'www.' prefix,
                http://example.com/... will be redirected to http://www.example.com/...)
                adapt and uncomment the following:   -->
                <!--
        <rule name="Redirect to add www" stopProcessing="true">
          <match url="^(.)$" ignoreCase="false" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^example.com$" />
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="http://www.example.com/{R:1}" />
        </rule>
                -->
                <!-- To redirect all users to access the site WITHOUT the 'www.' prefix,
                http://www.example.com/... will be redirected to http://example.com/...)
                adapt and uncomment the following:   -->
                <!--
        <rule name="Redirect to remove www" stopProcessing="true">
          <match url="^(.)$" ignoreCase="false" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^www.example.com$" />
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="http://example.com/{R:1}" />
        </rule>
                -->
        <!-- Rewrite URLs of the form 'x' to the form 'index.php?q=x'. -->
        <rule name="Short URLS" stopProcessing="true">
          <match url="^(.*)$" ignoreCase="false" />
          <conditions>
            <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
            <add input="{URL}" pattern="^/favicon.ico$" ignoreCase="false" negate="true" />
          </conditions>
          <action type="Rewrite" url="index.php?q={R:1}" appendQueryString="true" />
        </rule>
      </rules>
    </rewrite>

    <!-- httpErrors>
      <remove statusCode="404" subStatusCode="-1" />
      <error statusCode="404" prefixLanguageFilePath="" path="/index.php" responseMode="ExecuteURL" />
    </httpErrors -->

    <defaultDocument>
     <!-- Set the default document -->
      <files>
        <remove value="index.php" />
        <add value="index.php" />
      </files>
    </defaultDocument>
  </system.webServer>
</configuration>
For multi-site configuration, make sure convert each sub-directory into application and assign sufficient user (prefer the same as the one using in master site) to the sub-directory. If application configuration set up inappropriate, an "internal server error" will be posted.

IIS6

On IIS6, you can use third party modules (see above) to add mod_rewrite-like functionality to IIS. You will also want to download and set up the FastCGI module.

IIS5

On IIS5, you can use third party modules (see above) to add mod_rewrite-like functionality to IIS. Due to improved application pool security implemented in IIS6+, deployment on IIS6+ is recommended.

IIS5 Alternative: Creating a Custom Error Handler
Note: This method seems to work for IIS5 but not IIS6+.
You probably want to disable logging in IIS, since every page view is considered an error using this technique.

  • make sure your Drupal is working well without clean urls enabled.
  • open your Internet Services Manager or MMC and browse to the root directory of the web site where you installed Drupal. You cannot just browse to a subdirectory if you happenned to install to a subdirectory.
  • right click and select properties -> custom errors tab
  • set the HTTP Error 404 and 405 lines to MessageType=URL, URL=/index.php. If you are using Drupal in a subdirectory, prepend your subdir before /index.php
  • paste the following code into the bottom of settings.php file, which is usually located under sites/default/. the first two lines should be edited. If you aren't using a subdirectory, set $sub_directory to "". then set $active=1 and enjoy!
    // CONFIGURATION
    $sub_dir = "/41/"; // enter a subdirectory, if any. otherwise, use ""
    $active = 0; // set to 1 if using clean URLS with IIS
    
    // CODE
    if ($active && strstr($_SERVER["QUERY_STRING"], ";")) {
      $qs = explode(";", $_SERVER["QUERY_STRING"]);
      $url = array_pop($qs);
      $parts = parse_url($url);
      unset($_GET, $_SERVER['QUERY_STRING']); // remove cruft added by IIS
      if ($sub_dir) {
        $parts["path"] = substr($parts["path"], strlen($sub_dir));
      }
      $_GET["q"] = trim($parts["path"], "/");
      $_SERVER["REQUEST_URI"] = $parts["path"];
      if( array_key_exists( "query", $parts ) && $parts["query"] ) {
        $_SERVER["REQUEST_URI"] .= '?'. $parts["query"]; 
        $_SERVER["QUERY_STRING"] = $parts["query"];
        $_SERVER["ARGV"] = array($parts["query"]);
        parse_str($parts['query'], $arr);
        $_GET = array_merge($_GET, $arr);
        $_REQUEST = array_merge($_REQUEST, $arr);
      }
    }
    
  • at this point, you should be able to request clean url pages and receive a proper page in response. for example, request the page /node/1 and hopefully you will see your first node shown. you should not use the q= syntax; use the clean url syntax. if you get an IIS error, you have a problem. please fix redo the above and then retest.
  • browse to index.php?q=admin/system, enable clean URLS, and press Submit.
  • you may get a php error if your php error reporting in your php.ini file is set to high. Try this setting in your php.ini file
    error_reporting = E_ALL & ~E_NOTICE

Looking for support? Visit the Drupal.org forums, or join #drupal-support in IRC.

Comments

dman’s picture

We installed an IIS mod_rewrite.dll
http://www.iismods.com/url-rewrite/documentation.htm
and got it working OK, once I'd tuned the config file a bit.

This mod DOES NOT first check if the file exists, so it must be set to exclude any directories/files you DON'T want rewritten before rewriting everything else.
Like so

Debug 0
Reload 5000
#Browse LOT
#RewriteRule ^/(.*) /index.php
RewriteRule ^/index.php\?q\=(.*)$ /index.php?q=$1 [l]
RewriteRule ^/themes/(.*)$ /themes/$1 [l]
RewriteRule ^/misc/(.*)$ /misc/$1 [l]
RewriteRule ^/css/(.*)$ /css/$1 [l]
RewriteRule ^/files/(.*)$ /files/$1 [l]
RewriteRule ^/images/(.*)$ /images/$1 [l]

# for modules that provide their own js (tinymce,img assist etc)
RewriteRule ^(.*\.js)$ $1 [l]
RewriteRule ^(.*\.gif)$ $1 [l]
RewriteRule ^(.*\.png)$ $1 [l]
RewriteRule ^/modules/tinymce/(.*)$ /modules/tinymce/$1 [l]

# stand-alone
RewriteRule ^/cron.php$ /cron.php [l]

# Handle query strings on the end
RewriteRule ^/(.*)\?(.*)$ /index.php?q=$1&$2 [l]

# now pass through to the generic handler.
RewriteRule ^/(.*)$ /index.php?q=$1 [l]

... this is still under testing, so may need tweaking as I encounter new problems.

.dan.
How to troubleshoot Drupal | http://www.coders.co.nz/

brashquido’s picture

The above handbook entry is well out of date now, and there are several URL manipulation tools now available. See here for an updated rules configuration for using Helicon's ISAPI Rewrite on Drupal 5;

http://drupal.org/node/61367

And here for a complete walkthrough if you not that familiar with Drupal, IIS or ISAPI Rewrite;

http://www.iis-aid.com/articles/how_to_guides/using_drupal_clean_urls_wi...

----------------
Dominic Ryan
www.iis-aid.com

josh7weaver’s picture

The above handbook and guidelines worked great for me, and I am running Drupal 6.8, hardly outdated. Thanks a lot Drupal!

(By the way, I believe your second link is dead...)

---------------
Josh Weaver
TechMuscle-Inc.com

brashquido’s picture

daniel.s’s picture

There's also the open-source Ionics ISAPI Rewrite Filter on CodePlex

It requires you to register the ISAPI extension in IIS, and uses one .ini file for all the rewrite rules. It doesn't support .htaccess files.

It also comes bundled with some sample rulesets. From memory there was one for Drupal already.

benobo’s picture

Basically rewriting URLs in IIS is engaged by using the Redirect utility. One must include both the request URL and the destination URL in the "Redirect to:" text box separated by a semi-colon, and, one should also check the box next to "The exact URL entered above".

For example, if one desires to allow the user to enter http://myWeb/shortpath/file.htm but actually map to the file that is located at http://myWeb/long/longer/longest/file.htm one enters

/shortpath/file.htm;/long/longer/longest/file.htm

or, more generally

/shortpath/*;/long/longer/longest/$0

in the "Redirect to:" text box on the folder that is being used as the base for rewriting. In this case the folder named 'shortpath' is being used as the base for rewriting. The shorter alias is referred to as a clean or perhaps friendly URL.

This works in IIS 5.1 and above.

Here's the reference URL: http://support.microsoft.com/kb/324000

dman’s picture

I think I know what you are saying, but have no actual idea :-} (I'm not doing any IIS this year)
Would you be able to supply a screenshot(s) (if you are talking about some UI dialog) and we can post it into the handbook page? I think that would be helpful to everyone.

.dan.
How to troubleshoot Drupal | http://www.coders.co.nz/

ranaonline’s picture

This is the easiest working way to create friendly URL on the website. I had to spend quite some time looking for this.
My Config:

  • shared windows hosting server
  • PHP 4 & 5 (please use php 5, as register_global is off by default. I didn't have access to php.ini and .htaccess was not allowed to override.)
  • IIS 6
  • No ISAPI (or any way to install)
  • No mod_rewrite
  • Drupal 6.6
  • no experience

I did exactly as it was mentioned here and it worked smoothly! www.thefreeantispyware.com/site/
I tried to install everything. I am not a coder and I do not have any knowledge of code. Infact when I copied the settings, i didnt remove the <?php and got an error in the settings.php file :(

Makes Drupal my favorite CMS now (i've tried joomla, modx, mambo, snews, CS, wordpress, xoops and many more). I was then told that the Drupal community welcomes you with open hands :) I really want to thank you for tihis.

newbuntu’s picture

I can't create/edit content after enabling "clean url" on IIS. See http://drupal.org/node/339697#comment-1136710

Sahin’s picture

jbeall’s picture

Hi,

I had some trouble with this method, but I eventually got it working. I had to do several things.

First off, for context, I'm running Windows 2003/IIS6, with Zend Server (which means I'm using FastCGI, which is the preferred, although not the only, way to run PHP in IIS). It's possible some of the things I did will only work in the FastCGI environment.

The first thing I noted is that it is attempting to recreate $_SERVER['REQUEST_URI'] for some reason, and not doing it accurately at times. But $_SERVER['REQUEST_URI'] was already working just fine for me (IIS or Zend Server populated it for me perfectly), so there was no reason to try and recreate it. So I got rid of the lines that make assignments to $_SERVER['REQUEST_URI'].

The next thing I noted is that $_POST was completely empty, and that this was causing problems--while I could get to any page via GET, none of the forms worked! Big problem. Turns out this is a "feature" in IIS that has to do with it's child execution policy. In a nutshell, when your 404 handler is called, a second request is initiated, and that request is always GET (regardless of what the first request method was--the one that the browser actually made), and it doesn't pass along the POST parameters. More reading about this is available here:

http://www.developmentnow.com/g/59_2006_4_0_0_746187/IIS-6-Form-Post-Dat...
http://blogs.msdn.com/david.wang/archive/2005/11/29/Child_URL_Execution_...

But, I made an interesting discovery. Even though $_POST was empty, and the request method was always GET, I could still read from the stream php://input and get the request body that the browser sent along with it's original request. That's a raw, url-encoded string of key-value pairs. Using PHP's parse_str() function, I was able to parse that raw string into an array, and then assign it to $_POST--and voila, we got our $_POST array back.

I also updated the code that checks to see if we're inside a 404 handler--the code above just looked for the existence of a semicolon, which could result in some false positives. But if we're inside an IIS custom 404 handler, the query string will always start with "404;" and look something like "404;http://mydomain:80/cms/node/1234", so checking to see if $_SERVER['QUERY_STRING'] starts with "404;" seems like a much better check.

So here's my final, working copy of the code above:

// CONFIGURATION
$sub_dir = "/cms/"; // enter a subdirectory, if any. otherwise, use ""
$active = true; // set to true if using clean URLS with IIS

// CODE
// If we're inside a custom 404 handler, the query string will always start with 404;
// and look something like:
// 404;http://mydomain:80/cms/node/1234
if ($active && substr($_SERVER["QUERY_STRING"],0,4) === "404;") {
	// First we need to get the $_POST populated, because IIS will not
	// give us that information if we're inside a custom 404 handler.
	// The interesting thing is that we can still get to the request body (the raw POST data) through the php://input
	// stream, and thus we can use that to recreate the $_POST array.
	$recreatedPost = array();
	parse_str(file_get_contents("php://input"),$recreatedPost);
	$_POST = $recreatedPost;
	
	$qs = explode(";", $_SERVER["QUERY_STRING"]);
	$url = array_pop($qs);
	$parts = parse_url($url);
	unset($_GET, $_SERVER['QUERY_STRING']); // remove cruft added by IIS
	if ($sub_dir) {
	  	$parts["path"] = substr($parts["path"], strlen($sub_dir));
	}
	$_GET["q"] = trim($parts["path"], "/");
	if( array_key_exists( "query", $parts ) && $parts["query"] ) {
		$_SERVER["REQUEST_URI"] .= '?'. $parts["query"];
		$_SERVER["argv"] = array($parts["query"]);
		parse_str($parts['query'], $arr);
		$_GET = array_merge($_GET, $arr);
		$_REQUEST = array_merge($_REQUEST, $arr);
	}
}

This is great, because it doesn't require any additional modules and is working completely inside of a base IIS installation.

edsko’s picture

Hey,

I seem to be running into the same problem (Drupal 6.12, IIS 6). With the 404 redirect as described initially, my site appears to work, but information posted through forms (such as the user login form) seems to be lost. For instance, when you try to login, you simply get the user login form back.

However, when I use your code instead and try again, the requests seems to hang indefinitely (the page never gets rendered). Perhaps the file_get_contents is waiting for an end-of-file which isn't coming? (Just a guess). Any ideas/suggestions? (Tried on Linux/Firefox and Mac/Safari).

Thanks!

jbeall’s picture

Hi Edsko,

Not sure what exactly your setup is--the code I posted is for getting clean URLs to work on IIS, which is a Windows-only webserver. But you said you tried on Linux and Mac, so you must not be using IIS. What web server are you using?

-Josh

killerkent’s picture

This worked for me except in my case the $_SERVER['REQUEST_URI'] *wasn't* already being set....

which meant that although the $_POST array was set ok - Drupal wasn't doing anything with it. (as far as I could see)

so I added that line back in and hey presto - it worked.

Hope this helps someone

jbeall’s picture

Hi Killerkent,

What's your system setup--are you using Zend Server, or any other form of FastCGI? I assume you're using IIS for the webserver, since this thread is about getting clean URLs working on IIS...?

-Josh

killerkent’s picture

I just installed the IIS fastcgi extension which I got from here....

http://www.iis.net/extensions/FastCGI (if I remember right)

however I spoke too soon as it *still* doesn't work for me on multipart forms :( - although other simpler forms do work.

Irritating as I only wanted to run ubercart which seems to need imagecache for images which in turn seems to require clean urls.

jbeall’s picture

Hey Kent,

Don't know if it would fix your problem, but you could try Zend Server. The community edition is free.

http://www.zend.com/server

It's what I'm using, and it's working great for me...

-Josh

jbeall’s picture

It's just that I wasn't using many of them, so I didn't notice.

I've identified the problem, I can think of the solution, but I don't have time to implement it right now. Here's the deal: in my fix, I'm expecting the result of file_get_contents("php://input") to be a url-style set of key=value pairs, like firstname=josh&mail=josh@drupal.org

But in the case of a multipart form, file_get_contents("php://input") is going to look like this:

-----------------------------1184049667376
Content-Disposition: form-data; name="name"

Josh
-----------------------------1184049667376
Content-Disposition: form-data; name="mail"

josh@drupal.org
-----------------------------1184049667376--

This is a format that parse_str is not able to handle. You could write some string parsing in order to extract the data out of this, and I can't imagine it would be that hard, but it would take some time. The first line should always give you your delimiter.

The other thing you'll have to account for, I realized, is that you will need to handle file uploads manually as well. So if a file upload comes in, you're going to have to parse it out of file_get_contents("php://input"), and then populate the $_FILES array appropriately. At least, I imagine you'll have to do that--I haven't tested it.

For now I'm going back to "unclean" URLs, and perhaps at some point in the future I'll have a chance to look into this further.

-Josh

jbeall’s picture

It's pretty hairy, but it works for me.

To my great distress, it looks like there is essentially know way to get move_uploaded_file to work, even though you *can* get the data for the file that was uploaded. I went through all the trouble of repopulating the $_FILES array and saving the uploaded file data to the temp folder, but move_uploaded_file() will still fail, because is_uploaded_file() will return false for the temp files that you create from the php://input stream.

So, this will work for form text input, but not file uploads, assuming your app depends on either move_uploaded_file or is_uploaded_file working. For me, that's "good enough" because don't use any of Drupal's file upload features. Any images or other files we need to upload are stored elsewhere and copied into place via a fileshare.

// This is for Clean URLs on IIS -- requires you to configure index.php as the 404 and 405 handler.

// CONFIGURATION
$sub_dir = "/cms/"; // enter a subdirectory, if any. otherwise, use ""
$active = true; // set to true if using clean URLS with IIS

// CODE
// If we're inside a custom 404 handler, the query string will always start with 404;
// and look something like:
// 404;http://mydomain:80/cms/node/1234
if ($active && substr($_SERVER["QUERY_STRING"],0,4) === "404;") {
	
	// First we need to get the $_POST populated, because IIS will not
	// give us that information if we're inside a custom 404 handler.
	repopulate_post_array();
	
	// Now get the path information out of the querystring
	$qs = explode(";", $_SERVER["QUERY_STRING"]);
	$url = array_pop($qs);
	$parts = parse_url($url);
	unset($_GET, $_SERVER['QUERY_STRING']); // remove cruft added by IIS
	if ($sub_dir) {
	  	$parts["path"] = substr($parts["path"], strlen($sub_dir));
	}
	$_GET["q"] = trim($parts["path"], "/");
	if( array_key_exists( "query", $parts ) && $parts["query"] ) {
		$_SERVER["REQUEST_URI"] .= '?'. $parts["query"];
		$_SERVER["argv"] = array($parts["query"]);
		$arr = array();
		parse_str($parts['query'], $arr);
		$_GET = array_merge($_GET, $arr);
		$_REQUEST = array_merge($_REQUEST, $arr);
	}
}

function repopulate_post_array()
{
	// More info about the reason this is necessary here :
	// http://www.developmentnow.com/g/59_2006_4_0_0_746187/IIS-6-Form-Post-Data-Missing-in-404405-Custom-Error-Handler.htm
	// http://bugs.php.net/bug.php?id=38094
	// http://blogs.msdn.com/david.wang/archive/2005/11/29/Child_URL_Execution_and_SSI_exec.aspx
	//  ^--- (talks about child execution and how it affects SSI, but it's the same issue that's affecting us with $_POST being empty)
	// The interesting thing is that we can still get to the request body (the raw POST data) through the php://input
	// stream, and thus we can use that to recreate the $_POST array.
	$rawInput = file_get_contents("php://input");
	
	
	// first we inspect the content type header to see if there is a boundary
	// set for a multipart form.
	$ctHeader = $_SERVER['CONTENT_TYPE'];
	$ctHeader = str_replace("\r",'',$ctHeader); // Get rid of any newline characters that might be present
	$ctHeader = str_replace("\n",'',$ctHeader);
	
	$ctMatches = array();
	$pattern = 'multipart\/form-data; +boundary=(.*)';
	// The "i" after the pattern indicates a case-insensitive search
	$ctMatchCount = preg_match("/{$pattern}/i",$ctHeader,$ctMatches);
	// If we got a match count of 0, then we probably have the much simpler form enctype of
	// application/x-www-form-urlencoded which can be parsed with PHP's parse_str function
	$recreatedPost = array();
	if($ctMatchCount === 0)
	{
		parse_str($rawInput,$recreatedPost);
	}
	// Otherwise, it gets really ugly.  We have to post the form data manually, and it's complex.
	// It's going to look something like this. Note the binary data for the file upload--I have
	// manually base64 encoded it, but it would NOT be base64 encoded in reality.  It would be
	// a raw binary representation of whatever file was uploaded.
	//-----------------------------34303110730191
	//Content-Disposition: form-data; name="Name"
	//
	//Josh
	//-----------------------------34303110730191
	//Content-Disposition: form-data; name="Good"
	//
	//on
	//-----------------------------34303110730191
	//Content-Disposition: form-data; name="State"
	//
	//Nevada
	//-----------------------------34303110730191
	//Content-Disposition: form-data; name="FileForUpload"; filename="blackpix.gif"
	//Content-Type: image/gif
	//
	//R0lGODlhASYjNjU1MzM7ASYjNjU1MzM7JiM2NTUzMzsmIzY1NTMzOyYjNjU1MzM7JiM2NTUzMzsmIzY1NTMzOyYjNjU1MzM7////LCYjNjU1MzM7JiM2NTUzMzsmIzY1NTMzOyYjNjU1MzM7ASYjNjU1MzM7ASYjNjU1MzM7JiM2NTUzMzsCAkQBJiM2NTUzMzs7
	//-----------------------------34303110730191--
	//	
	else
	{
		$boundary = trim($ctMatches[1],'"'); // the boundary might look like this:
											 // 	Content-Type: multipart/mixed; boundary="gc0p4Jq0M:2Yt08jU534c0p"
											 // If there are quotes, we want to discard them.
		$openDelimiter = "--{$boundary}\r\n";	//The specified boundary will have an additional -- prepended.
		$delimiter = "\r\n--{$boundary}\r\n";    // All but the first delimiter will also be prepended with \r\n
		$closeDelimiter = "\r\n--{$boundary}--\r\n";	// The close delimiter will also have -- and \r\n appended.  See
											    // http://www.faqs.org/rfcs/rfc1521.html and scroll down to appendix D,
											    // and search for close-delimiter and delimiter.
		
		if(isset($_SERVER['TMP']))
			$tempDir = $_SERVER['TMP'];
		else 
			$tempDir = $_SERVER['TEMP'];
			
		// Make sure the tempDir string ends with exactly one slash
		$tempDir = trim($tempDir,'\/');
		$tempDir .= DIRECTORY_SEPARATOR;
			
		// Now parse the values out of the input stream
		
		
		// First, remove the closeDelimiter--we don't need it.
		$rawData = $rawInput;
		$rawData = str_replace($closeDelimiter,'',$rawData);
		// Now split out the data
		$rawData = explode($delimiter,$rawData);
		// Now get rid of the open delimiter, which will be the first part of
		// the first bit of data.
		$rawData[0] = str_replace($openDelimiter,'',$rawData[0]);
		
		// Now we walk the array and split it out into constituent parts
		$formData = array();
		foreach($rawData as $index => $value)
		{
			$rawValues = explode("\r\n\r\n",$value);
			// In some case, we'll have trailing \r\n characters, which we don't want.
			if(substr($rawValues[1],-2) === "\r\n" && false)
				$rawValues[1] = substr_replace($rawValues[1],'',-2);
			$formValue = array(
							'headers'=>explode("\r\n",$rawValues[0]),
							'data'=>$rawValues[1]
						);
			$formData[$index] = $formValue;
		}
		
		// We've now got a neat array that will look something like this:
		
		/*
		formData: Array
		(
		    [0] => Array
		        (
		            [headers] => Array
		                (
		                    [0] => Content-Disposition: form-data; name="Name"
		                )
		
		            [data] => Joshua
		        )
		
		    [1] => Array
		        (
		            [headers] => Array
		                (
		                    [0] => Content-Disposition: form-data; name="Email"
		                )
		
		            [data] => josh@example.com
		        )
		
		    [2] => Array
		        (
		            [headers] => Array
		                (
		                    [0] => Content-Disposition: form-data; name="Phone"
		                )
		
		            [data] => 123 456 7890
		        )
		
		    [3] => Array
		        (
		            [headers] => Array
		                (
		                    [0] => Content-Disposition: form-data; name="FileForUpload"; filename="sample.gif"
		                    [1] => Content-Type: image/gif
		                )
		
		            [data] => {binary data}
		        )
		
		 * 
		 */
		
		foreach($formData as $formPart){
		
			$name = '';
			$filename = '';
			$contentType = '';
			foreach($formPart['headers'] as $header)
			{
				$contentDispositionPrefix = "Content-Disposition";
				// This if statement will see if "header" begins with the string "Content-Disposition".
				// This is a case insensitive check (as indicated by the last parameter to substr_compare)
				if(substr_compare($header,$contentDispositionPrefix,0,strlen($contentDispositionPrefix),true) === 0){
					// First get the name, which should be attached to the Content-Disposition header
					// I've had too much trouble trying to use regular expressions to parse the
					// key value pairs out of a string like this:
					//		Content-Disposition: form-data; name="FileForUpload"; filename="deleteme.vbs"
					// Seems trivial, but I kept running up against cases where it would grab extra
					// characters.  Simplest solution I've been able to come up with is explode
					// into smaller chunks and then operate on those chunks. Semicolon seemed
					// the most appropriate delimiter.
					$nameMatch = array();
					$filenameMatch = array();
					$headerChunks = explode(';',$header);
					foreach($headerChunks as $headerChunk)
					{
						$headerChunk = trim($headerChunk);		
						// Also look for a filename
						if(preg_match('/\Afilename="?(.*)["\Z]/i',$headerChunk,$filenameMatch) > 0){
							$filename = $filenameMatch[1];
						}
						if(preg_match('/\Aname="?(.*)["\Z]/i',$headerChunk,$nameMatch) > 0){
							$name = $nameMatch[1];
						}
					}
				}
			}
			
			// Now, if we got a filename, it must be a file upload that we need to handle.
			if(strlen($filename) > 0)
			{
				// First, find the content type that was sent, if any
				foreach($formPart['headers'] as $header)
				{
					// Find the content type
					$contentTypePrefix = "Content-Type";
					if(substr_compare($header,$contentTypePrefix,0,strlen($contentTypePrefix),true) === 0){
						$contentTypeMatch = array();
					
						$headerChunks = explode(';',$header);
						foreach($headerChunks as $headerChunk)
						{
							if(preg_match('/Content-Type:(.*)/i',$headerChunk,$contentTypeMatch) > 0){
								$contentType = trim($contentTypeMatch[1]);
								break;
							}
						}
					}
				}
				
				// Generate a random filename for the temporary file, and write out the file to that location.
				$tmpFilename = $tempDir . md5(uniqid());
				$filePutResult = file_put_contents($tmpFilename,$formPart['data']);
				// Now populate the $_FILES array accordingly.
				$fileEntry = array(
									'name' => $filename,
									'type' => $contentType,
									'tmp_name' => $tmpFilename,
									'error' => 0,
									'size' => strlen($formPart['data'])
								);
				$_FILES[$name] = $fileEntry;
			}
			else // If there is no filename set, then it must be a regular form field that we need to store in $_POST
			{
				$urlEncodedPostString .= $name . '=' . urlencode($formPart['data']) . '&';
			}
		}
		$urlEncodedPostString = trim($urlEncodedPostString,'&');
		$recreatedPost = array();
		// We have to parse a string in order to properly handle multidimensional POST arrays.
		// parase_str will handle things like name[first]=josh properly.
		parse_str($urlEncodedPostString,$recreatedPost);
		
		function delete_temporary_upload_files(){
			foreach($_FILES as $file){
				unlink($file['tmp_name']);
			}
		}
		
		
		register_shutdown_function('delete_temporary_upload_files');
		
		// The parsing, moving, and setting of the uploaded files appears to work fine.
		// But alas, it is to no avail, because move_uploaded_file does not recognize
		// the file in question as being a file that was uploaded.  The file is still
		// there, and you can get at it with other file IO functions, but this won't
		// help with third party applications which (naturally) assume that
		// move_uploaded_file is going to work for moving uploaded files.
		// The reason this fails is because of a security check built in to 
		// move_uploaded_file -- it is supposed to know about files that PHP parsed
		// and handled itself.  Because we're doing the parsing manually, it thinks
		// some trickery is up.  Read more here:
		// http://php.net/move_uploaded_file
		// http://php.net/is_uploaded_file
				
	}
	
	$_POST = $recreatedPost;
}
dman’s picture

OMG. Why why why why WHY are you doing this oddity with php://input ?

I'm pretty damn sure that whatever you are working around can be solved by using the correct 3 lines of normal, sane, PHP functions or 3 lines of Drupal API upload handling funcs.
I know it can be really hard to find the correct API function names sometimes, (it is hard) but the rule of thumb is if you find something to be more difficult than it should be ... there's probably an easier way. When you find the true path you will be relieved.
If you find what you are doing is so insanely complex as this php://input example ... it's probably been solved before. Better.

No human coder messes with php://input when PHP has been built from the ground up to make HTTP transactions easy. That stream concept is only relevant to really low-level transactions. Form submissions are a solved problem for the last decade. You do not need to remake the wheel here.

Because of the abstract way you've tried to solve your issue, I can't even tell what the question is any more. :-(
But I do imagine that a little looking at EXISTING solutions that do file uploads within Drupal are the ones to learn from. Not some random PHP tutorial or prior art.

jbeall’s picture

If you don't understand why parsing php://input manually is necessary, you apparently haven't understood the problem.

IIS's 404 handler is preventing PHP's "normal" operation in which it handles the form submissions--making you do the work yourself if you want a 404 handler to be able to process a POST request. This is a known issue that has to do with IIS's child execution policy.

Having said that, I can't say that this problem hasn't been solved before--I just haven't been able to find it solved before, so I did it myself. You are certainly welcome to present a better solution. Indeed, I'd love to have a simpler/easier/better solution!

rkin005’s picture

I initially tried the above web.config and it didn't work for me. Replacing the rule titled

<!-- Rewrite URLs of the form 'x' to the form 'index.php?q=x'. -->

to the following worked. Someone else can probably explain why..

<rewrite>
  <rules>
    <rule name="Drupal clean URLs" enabled="true">
      <match url="^(.*)$" ignoreCase="false" />
      <conditions logicalGrouping="MatchAll">
        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />    
        <add input="{REQUEST_URI}" negate="true" pattern="/favicon.ico$" />
      </conditions>
      <action type="Rewrite" url="index.php?q={R:1}" appendQueryString="true" />
    </rule>
  </rules>
</rewrite>
porcupine73’s picture

I just tried this on Windows 2008 Web Server R2 with IIS 7.5 and Microsoft's rewrite add-in, and it wouldn't work.

I copied and pasted the XML at the top of the page into the web.config file.

I think the XML at the top of the page is missing the asterisk *

The code at top of the page showed

        <rule name="Short URLS" stopProcessing="true">
          <match url="^(.)$" ignoreCase="false" />

Which didn't work for me, but it did start working after putting in the asterisk:

        <rule name="Short URLS" stopProcessing="true">
          <match url="^(.*)$" ignoreCase="false" />
robinm’s picture

Many thanks. This solved the problem for me on Windows Server 2008 with IIS 7.0.

paulvantuyl’s picture

I was searching and searching and could not find why this wasn't working. Drupal 6 running on GoDaddy Shared Windows Hosting with clean urls now...

Also, the www redirects don't work unless you have this. Originally they are:

 <!-- To redirect all users to access the site WITH the 'www.' prefix,
                http://example.com/... will be redirected to http://www.example.com/...)
                adapt and uncomment the following:   -->
                <!--
        <rule name="Redirect to add www" stopProcessing="true">
          <match url="^(.)$" ignoreCase="false" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^example.com$" />
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="http://www.example.com/{R:1}" />
        </rule>
                -->
                <!-- To redirect all users to access the site WITHOUT the 'www.' prefix,
                http://www.example.com/... will be redirected to http://example.com/...)
                adapt and uncomment the following:   -->
                <!--
        <rule name="Redirect to remove www" stopProcessing="true">
          <match url="^(.)$" ignoreCase="false" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^www.example.com$" />
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="http://example.com/{R:1}" />
        </rule>
                -->

And what is working for me is

 <!-- To redirect all users to access the site WITH the 'www.' prefix,
                http://example.com/... will be redirected to http://www.example.com/...)
                adapt and uncomment the following:   -->
                <!--
        <rule name="Redirect to add www" stopProcessing="true">
          <match url="^(.*)$" ignoreCase="false" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^example.com$" />
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="http://www.example.com/{R:1}" />
        </rule>
                -->
                <!-- To redirect all users to access the site WITHOUT the 'www.' prefix,
                http://www.example.com/... will be redirected to http://example.com/...)
                adapt and uncomment the following:   -->
                <!--
        <rule name="Redirect to remove www" stopProcessing="true">
          <match url="^(.*)$" ignoreCase="false" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^www.example.com$" />
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="http://example.com/{R:1}" />
        </rule>
                -->
holydrupal’s picture

I am using win 2008 R2 and PHP runs as CGI, but after reading and trying a lot of documents regarding clean-urls I could not make it happen.
Every web.config I use I get the error page.

my code is as below:

<configuration>
<system.webServer>
<rewrite>
<!-- To redirect all users to access the site WITH the 'www.' prefix,
                http://example.com/... will be redirected to http://www.example.com/...)
                adapt and uncomment the following:   -->
                
        <rule name="Redirect to add www" stopProcessing="true">
          <match url="^(.*)$" ignoreCase="false" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^example.com$" />
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="http://www.example.com/{R:1}" />
        </rule>
                
                <!-- To redirect all users to access the site WITHOUT the 'www.' prefix,
                http://www.example.com/... will be redirected to http://example.com/...)
                adapt and uncomment the following:   -->
                
        <rule name="Redirect to remove www" stopProcessing="true">
          <match url="^(.*)$" ignoreCase="false" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^www.example.com$" />
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="http://example.com/{R:1}" />
        </rule>
                 

</system.webServer>
</configuration>

Is there anything wrong with this?

tmax’s picture

this rule is simply not working when i put my website name to re direct the url in place of example.com

     <rule name="Redirect to add www" stopProcessing="true">
          <match url="^(.)$" ignoreCase="false" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^example.com$" />
          </conditions>
          <action type="Redirect" redirectType="Permanent" url="http://www.example.com/{R:1}" />
        </rule>

m running iis7 hosting
rewrite module already installed on server side. what could be the error
am i missing anything here

paulvantuyl’s picture

Canadaka’s picture

using IIS7 which do you think is faster, ISAPI Rewrite 3 or Microsoft's URL Rewrite Module? I am currently using the full version of ISAPI Rewrite but I'm wondering if I should switch to the Microsoft one since its written by microsoft for their webserver it might be more efficient?

tomwrobel’s picture

Hi All

Following the comments above, I've got Clean URLs to work on URL rewrite/fastcgi/IIS 7, but some urls don't work.

urls in the form /node/foo are fine, but /search/foo fails (403 error page)

index.php?q=none/foo and index.php?q=search/foo are both fine, so I don't think it's a drupal permissions issue. Can anyone tell me what the problem might be? This is with a new, and patched, Windows 2008 server

EDIT: *facepalm*, the problem is using the word "test" as a search string with the 403 redirect conditions in the web.config file above.

Tom

lkujala’s picture

I know everyone loves to use command line scripts... some of us prefer to use the GUI tools instead of mucking around with the actual files.

After a lot of knashing of teeth, I think I finally have the URL Rewrite rules working.... so I am posting my configuration summary for IIS7:
1 - download and install the MS rewrite module: rewrite_2.0_rtw_x64.msi
2 - download and install the MS IIS admin pack: AdminPack_x64.msi
(need this for ReWrite Module GUI!)
3 - Open IIS Administration console
4 - Drill down to the site (mysite.com) to the Features view
5 - Open Directory Browsing and Disable (if not already)
6 - Open URL Rewrite and Add Rule
7 - Select Inbound - Blank Rule
8 - Fill in the form
Name: Drupal
Requested URL: matches pattern Using: Regular expressions
Pattern: ^(.*)$
Ignore Case: false
Action: rewrite
Rewrite URL: index.php?q={R:1}
Append Query String: True
Conditions Logical grouping: match all
Condition 1: {REQUEST_FILENAME} "is not a file"
Condition 2: {REQUEST_FILENAME} "is not a directory"
Condition 3: {URL} "does not match the pattern" Pattern: ^/favicon.ico$ Ignore case: false
9 - Apply to save changes
10 - Create another inbound rule to secure site details
Requested url: matches the pattern Using: regular expression
Pattern is:
.(engine|inc|info|install|module|profile|test|po|sh|.sql|postinst.1|theme|tpl(.php)?|xtmpl|svn-base)$|^(code-style.pl|Entries.|Repository|Root|Tag|Template|all-wcprops|entries|format)$
Ignore Case: true
Action: custom response
Status Code: 403 substatus code: 0 Reason: access denied
11 - Apply to save changes

This "should" get CleanURLs working properly... good luck.

ladybug_3777’s picture

****
edited to add my system info:
Using:
Windows 7
Drupal 7
IIS 7
****

I couldn't get ANY of the web configuration files to work right for me, but following your steps through the GUI did it!!

Thank you SO much!!

A few things I noticed along the way:

For some strange reason for the conditions #1 and #2, I didn't have "is not a file" and "is not a directory" listed in my drop down for {REQUEST_FILENAME}. I cancelled rule creation and reopened it and suddenly it started working correctly.

And the last part
"Action: custom response
Status Code: 403 substatus code: 0 Reason: access denied"

I had to also provide an "Error Description" or else I couldn't click the Apply button because it was disabled.

Other than that, it worked like a charm. THANK YOU SO MUCH!

jakedaynes’s picture

Hey guys, I just enabled Clean URLs on my Drupal 7 installation by using Ionic's ISAPI rewrite tool.

I have to say, it was easy once I realised what to do - not a lot of straightforward documentation out there, so here it goes:

After running the .msi (I assume you can do this if you are using IIS and Drupal ;) ), you need to create an iirf.ini file and put it in your website's root folder.

The .ini should contain only the following:

# Accept a url with the following extensions and pass them through unchanged.
RewriteRule (.*.gif|.*.png|.*.jpg|.*.pdf|.*.js|.*.css) $1 [I,L]

# Make URLs sane
RewriteRule /index.php.* $0 [I,L]
RewriteRule /(.*)\?(.*) /index.php\?q=$1&$2 [I,L]
RewriteRule /(.*) /index.php\?q=$1 [I,L]

That is it. Run your Drupal Clean URL test and voila.

You can contact me if you have any other questions.

amirsalmani’s picture

well written article.

openthinktech’s picture

I am running Windows Server 2003 IIS6. I installed the IIRF .msi file and added your above mentioned code and this allowed me to enable Clean URLs finally (THank you!)

However, since I am running Open Atrium 1.x on Drupal 6, this caused a secondary issue with the user pictures. User pictures stopped loading. The code below fixed the user pictures issue.

# Do not pass to drupal if the file or directory exists

RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]

# Make URLs sane
RewriteRule ^/cron\.php$ /cron.php [I,L]
RewriteRule ^/index\.php\?q\=(.*)$ /index.php?q=$1 [I,L]
RewriteRule ^/update\.php\?op\=(.*)$ /update.php?op=$1 [I,L]
RewriteRule ^/update\.php /update.php [I,L]
RewriteRule ^/xmlrpc\.php /xmlrpc.php [I,L]

RewriteRule ^/robots\.txt /robots.txt [I,L]

# Handle query strings on the end
RewriteRule /(.*)\?(.*)$ /index.php\?q=$1&$2 [I,L]

# now pass through to the generic handler
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^/(.*)$ /index.php?q=$1 [I,L]

Thank Marco for this code ( http://iirf.codeplex.com/discussions/349803 )

Thomas Newman
Web Developer
https://open-think.com

henrikdelphin’s picture

After some trial and error I just got this working on Aquia Drupal, IIS 7, Windows Server 2008. I want to add some comments on problems I experienced, that doesn't seem to be covered here previously.

- The web.config file in the original post above (and that is supplied with Acquia Drupal) works. I add this because I thought it looked like it would rewrite URLs to the format ?q=path and not from it, based on the comment above one of the lines in the file.

- I installed Aquia drupal through the Web Platform Installer, and even though this told me that the URL Rewrite module was installed, it did not show up in the Server Manager console. For this to happen, I had to manually install the module (downloaded from Microsoft's website, link in original post above) and then restart the server manager. The clean URLs-test failed until after the server manager was restarted.

- Problems with enabling clean URLs may also have been due to placing my Drupal install in a sub-folder. However, I moved Drupal to the root of the website at the same time as I installed the rewrite module properly, so I am unsure whether both of these or only the missing rewrite module was the cause of the problems.

While none of this is solid advice to anyone struggling with this, I hope it may at least give some pointers for possible sources of the issues.