Subversion Repositories cheapmusic

Rev

Rev 103 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
103 - 1
<?php
2
/*
3
miniProxy - A simple PHP web proxy. <https://github.com/joshdick/miniProxy>
4
Written and maintained by Joshua Dick <http://joshdick.net>.
5
miniProxy is licensed under the GNU GPL v3 <http://www.gnu.org/licenses/gpl.html>.
6
*/
7
 
8
/****************************** START CONFIGURATION ******************************/
9
 
10
//To allow proxying any URL, set $whitelistPatterns to an empty array (the default).
11
//To only allow proxying of specific URLs (whitelist), add corresponding regular expressions
12
//to the $whitelistPatterns array. Enter the most specific patterns possible, to prevent possible abuse.
13
//You can optionally use the "getHostnamePattern()" helper function to build a regular expression that
14
//matches all URLs for a given hostname.
15
$whitelistPatterns = array(
16
  //Usage example: To support any URL at example.net, including sub-domains, uncomment the
17
  //line below (which is equivalent to [ @^https?://([a-z0-9-]+\.)*example\.net@i ]):
18
  //getHostnamePattern("example.net")
19
);
20
 
21
//To enable CORS (cross-origin resource sharing) for proxied sites, set $forceCORS to true.
22
$forceCORS = false;
23
 
24
//Set to false to report the client machine's IP address to proxied sites via the HTTP `x-forwarded-for` header.
25
//Setting to false may improve compatibility with some sites, but also exposes more information about end users to proxied sites.
26
# SP CHANGE: the value set from plugin
27
#$anonymize = true;
28
 
29
//Start/default URL that that will be proxied when miniProxy is first loaded in a browser/accessed directly with no URL to proxy.
30
//If empty, miniProxy will show its own landing page.
31
$startURL = "";
32
 
33
//When no $startURL is configured above, miniProxy will show its own landing page with a URL form field
34
//and the configured example URL. The example URL appears in the instructional text on the miniProxy landing page,
35
//and is proxied when pressing the 'Proxy It!' button on the landing page if its URL form is left blank.
36
$landingExampleURL = "https://example.net";
37
 
38
/****************************** END CONFIGURATION ******************************/
39
 
40
ob_start("ob_gzhandler");
41
 
42
if (version_compare(PHP_VERSION, "5.4.7", "<")) {
43
  die("miniProxy requires PHP version 5.4.7 or later.");
44
}
45
 
155 - 46
$requiredExtensions = ["curl", "mbstring", "xml"];
103 - 47
foreach($requiredExtensions as $requiredExtension) {
48
  if (!extension_loaded($requiredExtension)) {
49
    die("miniProxy requires PHP's \"" . $requiredExtension . "\" extension. Please install/enable it on your server and try again.");
50
  }
51
}
52
 
53
//Helper function for use inside $whitelistPatterns.
54
//Returns a regex that matches all HTTP[S] URLs for a given hostname.
55
function getHostnamePattern($hostname) {
56
  $escapedHostname = str_replace(".", "\.", $hostname);
57
  return "@^https?://([a-z0-9-]+\.)*" . $escapedHostname . "@i";
58
}
59
 
60
//Helper function used to removes/unset keys from an associative array using case insensitive matching
61
function removeKeys(&$assoc, $keys2remove) {
62
  $keys = array_keys($assoc);
63
  $map = array();
64
  $removedKeys = array();
65
  foreach ($keys as $key) {
66
    $map[strtolower($key)] = $key;
67
  }
68
  foreach ($keys2remove as $key) {
69
    $key = strtolower($key);
70
    if (isset($map[$key])) {
71
      unset($assoc[$map[$key]]);
72
      $removedKeys[] = $map[$key];
73
    }
74
  }
75
  return $removedKeys;
76
}
77
 
78
if (!function_exists("getallheaders")) {
79
  //Adapted from http://www.php.net/manual/en/function.getallheaders.php#99814
80
  function getallheaders() {
81
    $result = array();
82
    foreach($_SERVER as $key => $value) {
83
      if (substr($key, 0, 5) == "HTTP_") {
84
        $key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5)))));
85
        $result[$key] = $value;
86
      }
87
    }
88
    return $result;
89
  }
90
}
91
 
92
# SP CHANGE: if PROXY_PREFIX is not already defined
93
if (!defined("PROXY_PREFIX")) {
94
	$usingDefaultPort =  (!isset($_SERVER["HTTPS"]) && $_SERVER["SERVER_PORT"] === 80) || (isset($_SERVER["HTTPS"]) && $_SERVER["SERVER_PORT"] === 443);
95
	$prefixPort = $usingDefaultPort ? "" : ":" . $_SERVER["SERVER_PORT"];
96
	//Use HTTP_HOST to support client-configured DNS (instead of SERVER_NAME), but remove the port if one is present
97
	$prefixHost = $_SERVER["HTTP_HOST"];
98
	$prefixHost = strpos($prefixHost, ":") ? implode(":", explode(":", $_SERVER["HTTP_HOST"], -1)) : $prefixHost;
99
 
100
	define("PROXY_PREFIX", "http" . (isset($_SERVER["HTTPS"]) ? "s" : "") . "://" . $prefixHost . $prefixPort . $_SERVER["SCRIPT_NAME"] . "?");
101
}
102
 
103
# SP CHANGE: to assign custom values
104
function assignSPCurlCustomValues($ch) {
105
	global $sourceId;
106
 
107
	# SP CHANGE: to fix the ssl related issues
108
	curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
109
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
110
 
111
	// to use proxy if proxy enabled
112
	if ($sourceId > 0) {
113
		$proxyCtrler = New ProxyController();
114
		$proxyInfo = $proxyCtrler->__getProxyInfo($sourceId);
115
		curl_setopt($ch, CURLOPT_PROXY, $proxyInfo['proxy'].":".$proxyInfo['port']);
116
 
117
		if (CURLOPT_HTTPPROXYTUNNEL_VAL) {
118
			curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, CURLOPT_HTTPPROXYTUNNEL_VAL);
119
		}
120
 
121
		if (!empty($proxyInfo['proxy_auth'])) {
122
			curl_setopt ($ch, CURLOPT_PROXYUSERPWD, $proxyInfo['proxy_username'].":".$proxyInfo['proxy_password']);
123
		}
124
 
125
	}
126
 
127
	return $ch;
128
 
129
}
130
 
131
//Makes an HTTP request via cURL, using request data that was passed directly to this script.
132
function makeRequest($url) {
133
 
134
  global $anonymize;
135
 
136
  //Tell cURL to make the request using the brower's user-agent if there is one, or a fallback user-agent otherwise.
137
  $user_agent = $_SERVER["HTTP_USER_AGENT"];
138
  if (empty($user_agent)) {
139
    $user_agent = "Mozilla/5.0 (compatible; miniProxy)";
140
  }
141
  $ch = curl_init();
142
  curl_setopt($ch, CURLOPT_USERAGENT, $user_agent);
143
 
144
  //Get ready to proxy the browser's request headers...
145
  $browserRequestHeaders = getallheaders();
146
 
147
  //...but let cURL set some headers on its own.
148
  $removedHeaders = removeKeys($browserRequestHeaders, array(
149
    "Accept-Encoding", //Throw away the browser's Accept-Encoding header if any and let cURL make the request using gzip if possible.
150
    "Content-Length",
151
    "Host",
152
    "Origin"
153
  ));
154
 
155 - 155
  $removedHeaders = array_map("strtolower", $removedHeaders);
103 - 156
 
157
  curl_setopt($ch, CURLOPT_ENCODING, "");
158
  //Transform the associative array from getallheaders() into an
159
  //indexed array of header strings to be passed to cURL.
160
  $curlRequestHeaders = array();
161
  foreach ($browserRequestHeaders as $name => $value) {
162
    $curlRequestHeaders[] = $name . ": " . $value;
163
  }
164
  if (!$anonymize) {
165
    $curlRequestHeaders[] = "X-Forwarded-For: " . $_SERVER["REMOTE_ADDR"];
166
  }
167
  //Any `origin` header sent by the browser will refer to the proxy itself.
168
  //If an `origin` header is present in the request, rewrite it to point to the correct origin.
155 - 169
  if (in_array("origin", $removedHeaders)) {
103 - 170
    $urlParts = parse_url($url);
171
    $port = $urlParts['port'];
172
    $curlRequestHeaders[] = "Origin: " . $urlParts['scheme'] . "://" . $urlParts['host'] . (empty($port) ? "" : ":" . $port);
173
  };
174
  curl_setopt($ch, CURLOPT_HTTPHEADER, $curlRequestHeaders);
175
 
176
  //Proxy any received GET/POST/PUT data.
177
  switch ($_SERVER["REQUEST_METHOD"]) {
178
    case "POST":
179
      curl_setopt($ch, CURLOPT_POST, true);
180
      //For some reason, $HTTP_RAW_POST_DATA isn't working as documented at
181
      //http://php.net/manual/en/reserved.variables.httprawpostdata.php
182
      //but the php://input method works. This is likely to be flaky
183
      //across different server environments.
184
      //More info here: http://stackoverflow.com/questions/8899239/http-raw-post-data-not-being-populated-after-upgrade-to-php-5-3
185
      //If the miniProxyFormAction field appears in the POST data, remove it so the destination server doesn't receive it.
186
      $postData = Array();
187
      parse_str(file_get_contents("php://input"), $postData);
188
      if (isset($postData["miniProxyFormAction"])) {
189
        unset($postData["miniProxyFormAction"]);
190
      }
191
      curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
192
    break;
193
    case "PUT":
194
      curl_setopt($ch, CURLOPT_PUT, true);
195
      curl_setopt($ch, CURLOPT_INFILE, fopen("php://input", "r"));
196
    break;
197
  }
198
 
199
  //Other cURL options.
200
  curl_setopt($ch, CURLOPT_HEADER, true);
201
  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
202
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
203
 
204
  # SP CHANGE: assign curl custom values for SP
205
  $ch = assignSPCurlCustomValues($ch);
206
 
207
  //Set the request URL.
208
  curl_setopt($ch, CURLOPT_URL, $url);
209
 
210
  //Make the request.
211
  $response = curl_exec($ch);
212
  $responseInfo = curl_getinfo($ch);
213
  $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
214
 
215
  # SP CHANGE: to find the errors if occured
216
  $errorNo = curl_errno($ch );
217
  $errMsg = curl_error($ch);
218
 
219
  curl_close($ch);
220
 
221
  //Setting CURLOPT_HEADER to true above forces the response headers and body
222
  //to be output together--separate them.
223
  $responseHeaders = substr($response, 0, $headerSize);
224
  $responseBody = substr($response, $headerSize);
225
 
226
  # SP CHANGE: to set the errors if occured
227
  # return array("headers" => $responseHeaders, "body" => $responseBody, "responseInfo" => $responseInfo);
228
  return array("headers" => $responseHeaders, "body" => $responseBody, "responseInfo" => $responseInfo, 'error' => $errorNo, 'errmsg' => $errMsg);
229
 
230
}
231
 
232
//Converts relative URLs to absolute ones, given a base URL.
233
//Modified version of code found at http://nashruddin.com/PHP_Script_for_Converting_Relative_to_Absolute_URL
234
function rel2abs($rel, $base) {
235
  if (empty($rel)) $rel = ".";
236
  if (parse_url($rel, PHP_URL_SCHEME) != "" || strpos($rel, "//") === 0) return $rel; //Return if already an absolute URL
237
  if ($rel[0] == "#" || $rel[0] == "?") return $base.$rel; //Queries and anchors
238
  extract(parse_url($base)); //Parse base URL and convert to local variables: $scheme, $host, $path
239
  $path = isset($path) ? preg_replace("#/[^/]*$#", "", $path) : "/"; //Remove non-directory element from path
240
  if ($rel[0] == "/") $path = ""; //Destroy path if relative url points to root
241
  $port = isset($port) && $port != 80 ? ":" . $port : "";
242
  $auth = "";
243
  if (isset($user)) {
244
    $auth = $user;
245
    if (isset($pass)) {
246
      $auth .= ":" . $pass;
247
    }
248
    $auth .= "@";
249
  }
250
  $abs = "$auth$host$port$path/$rel"; //Dirty absolute URL
251
  for ($n = 1; $n > 0; $abs = preg_replace(array("#(/\.?/)#", "#/(?!\.\.)[^/]+/\.\./#"), "/", $abs, -1, $n)) {} //Replace '//' or '/./' or '/foo/../' with '/'
252
  return $scheme . "://" . $abs; //Absolute URL is ready.
253
}
254
 
255
//Proxify contents of url() references in blocks of CSS text.
256
function proxifyCSS($css, $baseURL) {
257
  // Add a "url()" wrapper to any CSS @import rules that only specify a URL without the wrapper,
258
  // so that they're proxified when searching for "url()" wrappers below.
259
  $sourceLines = explode("\n", $css);
260
  $normalizedLines = [];
261
  foreach ($sourceLines as $line) {
262
    if (preg_match("/@import\s+url/i", $line)) {
263
      $normalizedLines[] = $line;
264
    } else {
265
      $normalizedLines[] = preg_replace_callback(
266
        "/(@import\s+)([^;\s]+)([\s;])/i",
267
        function($matches) use ($baseURL) {
268
          return $matches[1] . "url(" . $matches[2] . ")" . $matches[3];
269
        },
270
        $line);
271
    }
272
  }
273
  $normalizedCSS = implode("\n", $normalizedLines);
274
  return preg_replace_callback(
275
    "/url\((.*?)\)/i",
276
    function($matches) use ($baseURL) {
277
        $url = $matches[1];
278
        //Remove any surrounding single or double quotes from the URL so it can be passed to rel2abs - the quotes are optional in CSS
279
        //Assume that if there is a leading quote then there should be a trailing quote, so just use trim() to remove them
280
        if (strpos($url, "'") === 0) {
281
          $url = trim($url, "'");
282
        }
283
        if (strpos($url, "\"") === 0) {
284
          $url = trim($url, "\"");
285
        }
286
        if (stripos($url, "data:") === 0) return "url(" . $url . ")"; //The URL isn't an HTTP URL but is actual binary data. Don't proxify it.
287
        return "url(" . PROXY_PREFIX . rel2abs($url, $baseURL) . ")";
288
    },
289
    $normalizedCSS);
290
}
291
 
292
//Proxify "srcset" attributes (normally associated with <img> tags.)
293
function proxifySrcset($srcset, $baseURL) {
294
  $sources = array_map("trim", explode(",", $srcset)); //Split all contents by comma and trim each value
295
  $proxifiedSources = array_map(function($source) use ($baseURL) {
296
    $components = array_map("trim", str_split($source, strrpos($source, " "))); //Split by last space and trim
297
    $components[0] = PROXY_PREFIX . rel2abs(ltrim($components[0], "/"), $baseURL); //First component of the split source string should be an image URL; proxify it
298
    return implode($components, " "); //Recombine the components into a single source
299
  }, $sources);
300
  $proxifiedSrcset = implode(", ", $proxifiedSources); //Recombine the sources into a single "srcset"
301
  return $proxifiedSrcset;
302
}
303
 
304
//Extract and sanitize the requested URL, handling cases where forms have been rewritten to point to the proxy.
305
if (isset($_POST["miniProxyFormAction"])) {
306
  $url = $_POST["miniProxyFormAction"];
307
  unset($_POST["miniProxyFormAction"]);
308
} else {
309
  $queryParams = Array();
310
  parse_str($_SERVER["QUERY_STRING"], $queryParams);
311
  //If the miniProxyFormAction field appears in the query string, make $url start with its value, and rebuild the the query string without it.
312
  if (isset($queryParams["miniProxyFormAction"])) {
313
    $formAction = $queryParams["miniProxyFormAction"];
314
    unset($queryParams["miniProxyFormAction"]);
315
    $url = $formAction . "?" . http_build_query($queryParams);
316
  } else {
317
    $url = substr($_SERVER["REQUEST_URI"], strlen($_SERVER["SCRIPT_NAME"]) + 1);
318
 
319
    # SP CHANGE: if url passed in query parameters
320
    if (!empty($queryParams['url'])) {
321
    	$url = $queryParams['url'];
322
    }
323
 
324
  }
325
}
326
 
327
if (empty($url)) {
328
    if (empty($startURL)) {
329
      die("<html><head><title>miniProxy</title></head><body><h1>Welcome to miniProxy!</h1>miniProxy can be directly invoked like this: <a href=\"" . PROXY_PREFIX . $landingExampleURL . "\">" . PROXY_PREFIX . $landingExampleURL . "</a><br /><br />Or, you can simply enter a URL below:<br /><br /><form onsubmit=\"if (document.getElementById('site').value) { window.location.href='" . PROXY_PREFIX . "' + document.getElementById('site').value; return false; } else { window.location.href='" . PROXY_PREFIX . $landingExampleURL . "'; return false; }\" autocomplete=\"off\"><input id=\"site\" type=\"text\" size=\"50\" /><input type=\"submit\" value=\"Proxy It!\" /></form></body></html>");
330
    } else {
331
      $url = $startURL;
332
    }
333
} else if (strpos($url, ":/") !== strpos($url, "://")) {
334
    //Work around the fact that some web servers (e.g. IIS 8.5) change double slashes appearing in the URL to a single slash.
335
    //See https://github.com/joshdick/miniProxy/pull/14
336
    $pos = strpos($url, ":/");
337
    $url = substr_replace($url, "://", $pos, strlen(":/"));
338
}
339
$scheme = parse_url($url, PHP_URL_SCHEME);
340
if (empty($scheme)) {
341
  //Assume that any supplied URLs starting with // are HTTP URLs.
342
  if (strpos($url, "//") === 0) {
343
    $url = "http:" . $url;
344
  }
345
} else if (!preg_match("/^https?$/i", $scheme)) {
346
    die('Error: Detected a "' . $scheme . '" URL. miniProxy exclusively supports http[s] URLs.');
347
}
348
 
349
//Validate the requested URL against the whitelist.
350
$urlIsValid = count($whitelistPatterns) === 0;
351
foreach ($whitelistPatterns as $pattern) {
352
  if (preg_match($pattern, $url)) {
353
    $urlIsValid = true;
354
    break;
355
  }
356
}
357
if (!$urlIsValid) {
358
  die("Error: The requested URL was disallowed by the server administrator.");
359
}
360
 
361
$response = makeRequest($url);
362
$rawResponseHeaders = $response["headers"];
363
$responseBody = $response["body"];
364
$responseInfo = $response["responseInfo"];
365
 
366
//If CURLOPT_FOLLOWLOCATION landed the proxy at a diferent URL than
367
//what was requested, explicitly redirect the proxy there.
368
$responseURL = $responseInfo["url"];
369
if ($responseURL !== $url) {
370
 
371
	# SP CHANGE: if url redirected to some other url
372
	#header("Location: " . PROXY_PREFIX . $responseURL, true);
373
  	header("Location: " . PROXY_PREFIX . $responseURL . "&base_url=1", true);
374
  	exit(0);
375
 
376
}
377
 
378
//A regex that indicates which server response headers should be stripped out of the proxified response.
379
$header_blacklist_pattern = "/^Content-Length|^Transfer-Encoding|^Content-Encoding.*gzip/i";
380
 
381
//cURL can make multiple requests internally (for example, if CURLOPT_FOLLOWLOCATION is enabled), and reports
382
//headers for every request it makes. Only proxy the last set of received response headers,
383
//corresponding to the final request made by cURL for any given call to makeRequest().
384
$responseHeaderBlocks = array_filter(explode("\r\n\r\n", $rawResponseHeaders));
385
$lastHeaderBlock = end($responseHeaderBlocks);
386
$headerLines = explode("\r\n", $lastHeaderBlock);
387
foreach ($headerLines as $header) {
388
  $header = trim($header);
389
  if (!preg_match($header_blacklist_pattern, $header)) {
390
    header($header, false);
391
  }
392
}
393
//Prevent robots from indexing proxified pages
394
header("X-Robots-Tag: noindex, nofollow", true);
395
 
396
if ($forceCORS) {
397
  //This logic is based on code found at: http://stackoverflow.com/a/9866124/278810
398
  //CORS headers sent below may conflict with CORS headers from the original response,
399
  //so these headers are sent after the original response headers to ensure their values
400
  //are the ones that actually end up getting sent to the browser.
401
  //Explicit [ $replace = true ] is used for these headers even though this is PHP's default behavior.
402
 
403
  //Allow access from any origin.
404
  header("Access-Control-Allow-Origin: *", true);
405
  header("Access-Control-Allow-Credentials: true", true);
406
 
407
  //Handle CORS headers received during OPTIONS requests.
408
  if ($_SERVER["REQUEST_METHOD"] == "OPTIONS") {
409
    if (isset($_SERVER["HTTP_ACCESS_CONTROL_REQUEST_METHOD"])) {
410
      header("Access-Control-Allow-Methods: GET, POST, OPTIONS", true);
411
    }
412
    if (isset($_SERVER["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"])) {
413
      header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}", true);
414
    }
415
    //No further action is needed for OPTIONS requests.
416
    exit(0);
417
  }
418
 
419
}
420
 
421
$contentType = "";
422
if (isset($responseInfo["content_type"])) $contentType = $responseInfo["content_type"];
423
 
424
//This is presumably a web page, so attempt to proxify the DOM.
425
if (stripos($contentType, "text/html") !== false) {
426
 
427
  //Attempt to normalize character encoding.
428
  $detectedEncoding = mb_detect_encoding($responseBody, "UTF-8, ISO-8859-1");
429
  if ($detectedEncoding) {
430
    $responseBody = mb_convert_encoding($responseBody, "HTML-ENTITIES", $detectedEncoding);
431
  }
432
 
433
  //Parse the DOM.
434
  $doc = new DomDocument();
435
  @$doc->loadHTML($responseBody);
436
  $xpath = new DOMXPath($doc);
437
 
438
  //Rewrite forms so that their actions point back to the proxy.
439
  foreach($xpath->query("//form") as $form) {
440
    $method = $form->getAttribute("method");
441
    $action = $form->getAttribute("action");
442
    //If the form doesn't have an action, the action is the page itself.
443
    //Otherwise, change an existing action to an absolute version.
444
    $action = empty($action) ? $url : rel2abs($action, $url);
445
    //Rewrite the form action to point back at the proxy.
446
    $form->setAttribute("action", rtrim(PROXY_PREFIX, "?"));
447
    //Add a hidden form field that the proxy can later use to retreive the original form action.
448
    $actionInput = $doc->createDocumentFragment();
449
    $actionInput->appendXML('<input type="hidden" name="miniProxyFormAction" value="' . htmlspecialchars($action) . '" />');
450
    $form->appendChild($actionInput);
451
  }
452
  //Proxify <meta> tags with an 'http-equiv="refresh"' attribute.
453
  foreach ($xpath->query("//meta[@http-equiv]") as $element) {
454
    if (strcasecmp($element->getAttribute("http-equiv"), "refresh") === 0) {
455
      $content = $element->getAttribute("content");
456
      if (!empty($content)) {
457
        $splitContent = preg_split("/=/", $content);
458
        if (isset($splitContent[1])) {
459
          $element->setAttribute("content", $splitContent[0] . "=" . PROXY_PREFIX . rel2abs($splitContent[1], $url));
460
        }
461
      }
462
    }
463
  }
464
  //Profixy <style> tags.
465
  foreach($xpath->query("//style") as $style) {
466
    $style->nodeValue = proxifyCSS($style->nodeValue, $url);
467
  }
468
  //Proxify tags with a "style" attribute.
469
  foreach ($xpath->query("//*[@style]") as $element) {
470
    $element->setAttribute("style", proxifyCSS($element->getAttribute("style"), $url));
471
  }
472
  //Proxify "srcset" attributes in <img> tags.
473
  foreach ($xpath->query("//img[@srcset]") as $element) {
474
    $element->setAttribute("srcset", proxifySrcset($element->getAttribute("srcset"), $url));
475
  }
476
  //Proxify any of these attributes appearing in any tag.
477
  $proxifyAttributes = array("href", "src");
478
  foreach($proxifyAttributes as $attrName) {
479
    foreach($xpath->query("//*[@" . $attrName . "]") as $element) { //For every element with the given attribute...
480
      $attrContent = $element->getAttribute($attrName);
155 - 481
      if ($attrName == "href" && preg_match("/^(about|javascript|magnet|mailto):|#/i", $attrContent)) continue;
482
      if ($attrName == "src" && preg_match("/^(data):/i", $attrContent)) continue;
103 - 483
      $attrContent = rel2abs($attrContent, $url);
484
      $attrContent = PROXY_PREFIX . $attrContent;
485
      $element->setAttribute($attrName, $attrContent);
486
    }
487
  }
488
 
489
  //Attempt to force AJAX requests to be made through the proxy by
490
  //wrapping window.XMLHttpRequest.prototype.open in order to make
491
  //all request URLs absolute and point back to the proxy.
492
  //The rel2abs() JavaScript function serves the same purpose as the server-side one in this file,
493
  //but is used in the browser to ensure all AJAX request URLs are absolute and not relative.
494
  //Uses code from these sources:
495
  //http://stackoverflow.com/questions/7775767/javascript-overriding-xmlhttprequest-open
496
  //https://gist.github.com/1088850
497
  //TODO: This is obviously only useful for browsers that use XMLHttpRequest but
498
  //it's better than nothing.
499
 
500
  $head = $xpath->query("//head")->item(0);
501
  $body = $xpath->query("//body")->item(0);
155 - 502
  $prependElem = $head != null ? $head : $body;
103 - 503
 
504
  //Only bother trying to apply this hack if the DOM has a <head> or <body> element;
505
  //insert some JavaScript at the top of whichever is available first.
506
  //Protects against cases where the server sends a Content-Type of "text/html" when
507
  //what's coming back is most likely not actually HTML.
508
  //TODO: Do this check before attempting to do any sort of DOM parsing?
155 - 509
  if ($prependElem != null) {
103 - 510
 
511
    $scriptElem = $doc->createElement("script",
512
      '(function() {
513
 
514
        if (window.XMLHttpRequest) {
515
 
516
          function parseURI(url) {
517
            var m = String(url).replace(/^\s+|\s+$/g, "").match(/^([^:\/?#]+:)?(\/\/(?:[^:@]*(?::[^:@]*)?@)?(([^:\/?#]*)(?::(\d*))?))?([^?#]*)(\?[^#]*)?(#[\s\S]*)?/);
518
            // authority = "//" + user + ":" + pass "@" + hostname + ":" port
519
            return (m ? {
520
              href : m[0] || "",
521
              protocol : m[1] || "",
522
              authority: m[2] || "",
523
              host : m[3] || "",
524
              hostname : m[4] || "",
525
              port : m[5] || "",
526
              pathname : m[6] || "",
527
              search : m[7] || "",
528
              hash : m[8] || ""
529
            } : null);
530
          }
531
 
532
          function rel2abs(base, href) { // RFC 3986
533
 
534
            function removeDotSegments(input) {
535
              var output = [];
536
              input.replace(/^(\.\.?(\/|$))+/, "")
537
                .replace(/\/(\.(\/|$))+/g, "/")
538
                .replace(/\/\.\.$/, "/../")
539
                .replace(/\/?[^\/]*/g, function (p) {
540
                  if (p === "/..") {
541
                    output.pop();
542
                  } else {
543
                    output.push(p);
544
                  }
545
                });
546
              return output.join("").replace(/^\//, input.charAt(0) === "/" ? "/" : "");
547
            }
548
 
549
            href = parseURI(href || "");
550
            base = parseURI(base || "");
551
 
552
            return !href || !base ? null : (href.protocol || base.protocol) +
553
            (href.protocol || href.authority ? href.authority : base.authority) +
554
            removeDotSegments(href.protocol || href.authority || href.pathname.charAt(0) === "/" ? href.pathname : (href.pathname ? ((base.authority && !base.pathname ? "/" : "") + base.pathname.slice(0, base.pathname.lastIndexOf("/") + 1) + href.pathname) : base.pathname)) +
555
            (href.protocol || href.authority || href.pathname ? href.search : (href.search || base.search)) +
556
            href.hash;
557
 
558
          }
559
 
560
          var proxied = window.XMLHttpRequest.prototype.open;
561
          window.XMLHttpRequest.prototype.open = function() {
562
              if (arguments[1] !== null && arguments[1] !== undefined) {
563
                var url = arguments[1];
564
                url = rel2abs("' . $url . '", url);
155 - 565
                if (url.indexOf("' . PROXY_PREFIX . '") == -1) {
566
                   url = "' . PROXY_PREFIX . '" + url;
567
                }
103 - 568
                arguments[1] = url;
569
              }
570
              return proxied.apply(this, [].slice.call(arguments));
571
          };
572
 
573
        }
574
 
575
      })();'
576
    );
577
    $scriptElem->setAttribute("type", "text/javascript");
578
 
579
    $prependElem->insertBefore($scriptElem, $prependElem->firstChild);
580
 
581
  }
582
 
583
  echo "<!-- Proxified page constructed by miniProxy -->\n" . $doc->saveHTML();
584
} else if (stripos($contentType, "text/css") !== false) { //This is CSS, so proxify url() references.
585
  echo proxifyCSS($responseBody, $url);
586
} else { //This isn't a web page or CSS, so serve unmodified through the proxy with the correct headers (images, JavaScript, etc.)
587
  header("Content-Length: " . strlen($responseBody), true);
588
  echo $responseBody;
589
}