Clean URLs on IIS
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
- ISAPI Rewrite by Helicon software. There is a free "lite" and a paid version. IIS Aid has an article on configuring the Helicon module. There is also an important note about .htaccess files with ISAPI_Rewrite 3 here.
Check this thread for some approaches to setting up ISAPI_Rewrite.
- Ionic's ISAPI Rewrite Filter
- Micronovae's IIS Mod-Rewrite (the "standard" version works w/IIS5) (documentation).
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>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.phpfile, which is usually located undersites/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!<?php
// 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/1and 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

another mod_rewrite
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/
Out of date?
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
Not for me...
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
Updated for ISAPI Rewrite 3.x Lite
Using Drupal Clean URLs with IIS and ISAPI Rewrite Version 3
----------------
Dominic Ryan
www.iis-aid.com
Ionics ISAPI Rewrite Filter
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.
How to Rewrite Long URLs In IIS
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
Would a picture make this clear?
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/
shared windows hosting & friendly url on drupal
This is the easiest working way to create friendly URL on the website. I had to spend quite some time looking for this.
- 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
My Config:
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.
I can't create/edit content
I can't create/edit content after enabling "clean url" on IIS. See http://drupal.org/node/339697#comment-1136710
Got 404 method working on IIS6
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:
<?php
// 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.
Requests hang indefinitely?
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!
http://www.self-defence.ie
Not sure what you did -- IIS is Windows only
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
Thanks
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
Are you using FastCGI?
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
Yes IIS 6, and....
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.
I don't know if it would fix it, but you could try Zend Server
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
Ok, it turns out multipart forms aren't working for me either
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
Well, I've come up with the following solution...
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.
<?php
// 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 :
// <a href="http://www.developmentnow.com/g/59_2006_4_0_0_746187/IIS-6-Form-Post-Data-Missing-in-404405-Custom-Error-Handler.htm
" title="http://www.developmentnow.com/g/59_2006_4_0_0_746187/IIS-6-Form-Post-Data-Missing-in-404405-Custom-Error-Handler.htm
" rel="nofollow">http://www.developmentnow.com/g/59_2006_4_0_0_746187/IIS-6-Form-Post-Dat...</a> // <a href="http://bugs.php.net/bug.php?id=38094
" title="http://bugs.php.net/bug.php?id=38094
" rel="nofollow">http://bugs.php.net/bug.php?id=38094
</a> // <a href="http://blogs.msdn.com/david.wang/archive/2005/11/29/Child_URL_Execution_and_SSI_exec.aspx
" title="http://blogs.msdn.com/david.wang/archive/2005/11/29/Child_URL_Execution_and_SSI_exec.aspx
" rel="nofollow">http://blogs.msdn.com/david.wang/archive/2005/11/29/Child_URL_Execution_...</a> // ^--- (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
// <a href="http://www.faqs.org/rfcs/rfc1521.html" title="http://www.faqs.org/rfcs/rfc1521.html" rel="nofollow">http://www.faqs.org/rfcs/rfc1521.html</a> 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:
// <a href="http://php.net/move_uploaded_file
" title="http://php.net/move_uploaded_file
" rel="nofollow">http://php.net/move_uploaded_file
</a> // <a href="http://php.net/is_uploaded_file
" title="http://php.net/is_uploaded_file
" rel="nofollow">http://php.net/is_uploaded_file
</a>
}
$_POST = $recreatedPost;
}
?>
WHY?
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://inputexample ... it's probably been solved before. Better.No human coder messes with
php://inputwhen 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.
.dan.
You need to go back and re-read the thread
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!
IIS7 - different web.config for clean urls
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>