From 46e9ea8a7c7e867d32c412ce53efe5a0cccbe8a6 Mon Sep 17 00:00:00 2001 From: reef-actor Date: Mon, 29 Jan 2018 10:35:35 +0000 Subject: [PATCH 1/6] Move remove_js into dedicated plugin Script removal is not related to proxification. Class Html removed, no longer referenced. --- src/Html.php | 182 ----------------------------- src/Plugin/ProxifyPlugin.php | 11 -- src/Plugin/RemoveScriptsPlugin.php | 31 +++++ 3 files changed, 31 insertions(+), 193 deletions(-) delete mode 100644 src/Html.php create mode 100644 src/Plugin/RemoveScriptsPlugin.php diff --git a/src/Html.php b/src/Html.php deleted file mode 100644 index 08c4c40..0000000 --- a/src/Html.php +++ /dev/null @@ -1,182 +0,0 @@ -]*>(.*?)<\s*\/\s*script\s*>/is', '', $html); - return $html; - } - - public static function remove_styles($html){ - $html = preg_replace('/<\s*style[^>]*>(.*?)<\s*\/\s*style\s*>/is', '', $html); - return $html; - } - - public static function remove_comments($html){ - return preg_replace('//s', '', $html); - } - - private static function find($selector, $html, $start_from = 0){ - - $html = substr($html, $start_from); - - $inner_start = 0; - $inner_end = 0; - - $pattern = '//'; - - if(substr($selector, 0, 1) == '#'){ - $pattern = '/<(\w+)[^>]+id="'.substr($selector, 1).'"[^>]*>/is'; - } else if(substr($selector, 0, 1) == '.'){ - $pattern = '/<(\w+)[^>]+class="'.substr($selector, 1).'"[^>]*>/is'; - } else { - return false; - } - - if(preg_match($pattern, $html, $matches, PREG_OFFSET_CAPTURE)){ - - $outer_start = $matches[0][1]; - $inner_start = $matches[0][1] + strlen($matches[0][0]); - - // tag stuff - $tag_name = $matches[1][0]; - $tag_len = strlen($tag_name); - - $run_count = 300; - - // "open" 0){ - - $open_tag = strpos($html, "<{$tag_name}", $start); - $close_tag = strpos($html, " $outer_start + $start_from, - 'inner_start' => $inner_start + $start_from, - 'inner_end' => $inner_end + $start_from, - 'outer_end' => $outer_end + $start_from - ); - } - - return false; - } - - public static function extract_inner($selector, $html){ - return self::extract($selector, $html, true); - } - - public static function extract_outer($selector, $html){ - return self::extract($selector, $html, false); - } - - private static function extract($selector, $html, $inner = false){ - - $pos = 0; - $limit = 300; - - $result = array(); - $data = false; - - do { - - $data = self::find($selector, $html, $pos); - - if($data){ - - $code = substr($html, $inner ? $data['inner_start'] : $data['outer_start'], - $inner ? $data['inner_end'] - $data['inner_start'] : $data['outer_end'] - $data['outer_start']); - - $result[] = $code; - $pos = $data['outer_end']; - } - - } while ($data && --$limit > 0); - - return $result; - } - - public static function remove($selector, $html){ - return self::replace($selector, '', $html, false); - } - - public static function replace_outer($selector, $replace, $html, &$matches = NULL){ - return self::replace($selector, $replace, $html, false, $matches); - } - - public static function replace_inner($selector, $replace, $html, &$matches = NULL){ - return self::replace($selector, $replace, $html, true, $matches); - } - - private static function replace($selector, $replace, $html, $replace_inner = false, &$matches = NULL){ - - $start_from = 0; - $limit = 300; - - $data = false; - $replace = (array)$replace; - - do { - - $data = self::find($selector, $html, $start_from); - - if($data){ - - $r = array_shift($replace); - - // from where to where will we be replacing? - $replace_space = $replace_inner ? $data['inner_end'] - $data['inner_start'] : $data['outer_end'] - $data['outer_start']; - $replace_len = strlen($r); - - if($matches !== NULL){ - $matches[] = substr($html, $replace_inner ? $data['inner_start'] : $data['outer_start'], $replace_space); - } - - $html = substr_replace($html, $r, $replace_inner ? $data['inner_start'] : $data['outer_start'], $replace_space); - - // next time we resume search at position right at the end of this element - $start_from = $data['outer_end'] + ($replace_len - $replace_space); - } - - } while ($data && --$limit > 0); - - return $html; - } -} - -?> \ No newline at end of file diff --git a/src/Plugin/ProxifyPlugin.php b/src/Plugin/ProxifyPlugin.php index 3165428..6ecfb87 100644 --- a/src/Plugin/ProxifyPlugin.php +++ b/src/Plugin/ProxifyPlugin.php @@ -5,7 +5,6 @@ use Proxy\Plugin\AbstractPlugin; use Proxy\Event\ProxyEvent; use Proxy\Config; -use Proxy\Html; class ProxifyPlugin extends AbstractPlugin { @@ -153,16 +152,6 @@ public function onCompleted(ProxyEvent $event){ return; } - // remove JS from urls - $js_remove = (array)Config::get('js_remove'); - foreach($js_remove as $pattern){ - if(strpos($url_host, $pattern) !== false){ - $str = Html::remove_scripts($str); - } - } - - // add html.no-js - // let's remove all frames?? does not protect against the frames created dynamically via javascript $str = preg_replace('@]*>[^<]*<\\/iframe>@is', '', $str); diff --git a/src/Plugin/RemoveScriptsPlugin.php b/src/Plugin/RemoveScriptsPlugin.php new file mode 100644 index 0000000..553ab7f --- /dev/null +++ b/src/Plugin/RemoveScriptsPlugin.php @@ -0,0 +1,31 @@ +getUri(); + $response = $event['response']; + $content_type = $response->headers->get('content-type'); + if (strpos($content_type, 'text/html') === false) { + return; + } + + $url_host = parse_url($uri, PHP_URL_HOST); + + // remove JS from urls + $js_remove = (array)Config::get('js_remove'); + foreach ($js_remove as $pattern) { + if (strpos($url_host, $pattern) !== false) { + $content = $response->getContent(); + $content = preg_replace('/<\s*script[^>]*>(.*?)<\s*\/\s*script\s*>/is', '', $content); + $response->setContent($content); + } + } + } +} From 03eee03ac6b5f485f1a3ccf6cc4d96fc0b22d472 Mon Sep 17 00:00:00 2001 From: reef-actor Date: Mon, 29 Jan 2018 10:37:11 +0000 Subject: [PATCH 2/6] Move replace_title into dedicated plugin Title replacement is not related to proxification. --- src/Plugin/ProxifyPlugin.php | 7 ------- src/Plugin/ReplaceTitlePlugin.php | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 src/Plugin/ReplaceTitlePlugin.php diff --git a/src/Plugin/ProxifyPlugin.php b/src/Plugin/ProxifyPlugin.php index 6ecfb87..1fe5d1e 100644 --- a/src/Plugin/ProxifyPlugin.php +++ b/src/Plugin/ProxifyPlugin.php @@ -4,7 +4,6 @@ use Proxy\Plugin\AbstractPlugin; use Proxy\Event\ProxyEvent; -use Proxy\Config; class ProxifyPlugin extends AbstractPlugin { @@ -101,12 +100,6 @@ private function meta_refresh($matches){ // , <base>, <link>, <style>, <meta>, <script>, <noscript> private function proxify_head($str){ - // let's replace page titles with something custom - if(Config::get('replace_title')){ - $str = preg_replace('/<title[^>]*>(.*?)<\/title>/is', '<title>'.Config::get('replace_title').'', $str); - } - - // base - update base_url contained in href - remove tag entirely //$str = preg_replace_callback('/]*href= diff --git a/src/Plugin/ReplaceTitlePlugin.php b/src/Plugin/ReplaceTitlePlugin.php new file mode 100644 index 0000000..ee4a473 --- /dev/null +++ b/src/Plugin/ReplaceTitlePlugin.php @@ -0,0 +1,24 @@ +headers->get('content-type'); + if (strpos($content_type, 'text/html') === false) { + return; + } + + if (Config::get('replace_title')) { + $content = $response->getContent(); + $content = preg_replace('/]*>(.*?)<\/title>/is', '' . Config::get('replace_title') . '', $content); + $response->setContent($content); + } + } +} From 0affa914af94c1ac5ee1a0417331edf24f8f7cd1 Mon Sep 17 00:00:00 2001 From: reef-actor Date: Mon, 29 Jan 2018 10:38:13 +0000 Subject: [PATCH 3/6] Refactor ProxifyPlugin Use regex named capture groups to allow reuse of callbacks. Added 'image' & 'font' to content-type blacklist. --- src/Plugin/ProxifyPlugin.php | 244 ++++++++++------------------------- 1 file changed, 71 insertions(+), 173 deletions(-) diff --git a/src/Plugin/ProxifyPlugin.php b/src/Plugin/ProxifyPlugin.php index 1fe5d1e..e2b4631 100644 --- a/src/Plugin/ProxifyPlugin.php +++ b/src/Plugin/ProxifyPlugin.php @@ -1,184 +1,82 @@ \'|")\d+\s*;\s*url=(?.*?)\k@is' => 'self::proxifyUrlCallback', // content="X;url=" (meta-refresh) + '@\b(?:src|href)\s*=\s*(?\'|")(?.*?)\k@is' => 'self::proxifyUrlCallback', // src="" & href="" + '@[^a-z]{1}url\s*\((?\'|"|)(?[^\)]*)\k\)@im' => 'self::proxifyUrlCallback', // url() + '@\@import\s+(?\'|")(?.*?)\k@im' => 'self::proxifyUrlCallback', // @import '' + '@\b(?:srcset)\s*=\s*(?\'|")(?.*?)\k@im' => 'self::proxifySrcsetAttributeCallback', // srcset=" xxx, …" + '@<\s*form[^>]*action=(?\'|")(?.*?)\k[^>]*>@im' => 'self::proxifyFormCallback', //
+ ]; - private $base_url = ''; - - private function css_url($matches){ - - $url = trim($matches[1]); - if(starts_with($url, 'data:')){ - return $matches[0]; - } - - return str_replace($matches[1], proxify_url($matches[1], $this->base_url), $matches[0]); - } - - // this.params.logoImg&&(e="background-image: url("+this.params.logoImg+")") - private function css_import($matches){ - return str_replace($matches[2], proxify_url($matches[2], $this->base_url), $matches[0]); - } + private $base_url = ''; - // replace src= and href= - private function html_attr($matches){ - - // could be empty? - $url = trim($matches[2]); - - $schemes = array('data:', 'magnet:', 'about:', 'javascript:', 'mailto:', 'tel:', 'ios-app:', 'android-app:'); - if(starts_with($url, $schemes)){ - return $matches[0]; - } - - return str_replace($url, proxify_url($url, $this->base_url), $matches[0]); - } + public function onCompleted(ProxyEvent $event) + { + $response = $event['response']; + $content_type = $response->headers->get('content-type'); + if (starts_with($content_type, self::CONTENT_TYPE_BLACKLIST)) { + return; + } - private function form_action($matches){ - - // sometimes form action is empty - which means a postback to the current page - // $matches[1] holds single or double quote - whichever was used by webmaster - - // $matches[2] holds form submit URL - can be empty which in that case should be replaced with current URL - if(!$matches[2]){ - $matches[2] = $this->base_url; - } - - $new_action = proxify_url($matches[2], $this->base_url); - - // what is form method? - $form_post = preg_match('@method=(["\'])post\1@i', $matches[0]) == 1; - - // take entire form string - find real url and replace it with proxified url - $result = str_replace($matches[2], $new_action, $matches[0]); - - // must be converted to POST otherwise GET form would just start appending name=value pairs to your proxy url - if(!$form_post){ - - // may throw Duplicate Attribute warning but only first method matters - $result = str_replace("post->has('convertGET')){ - - // we don't need this parameter anymore - $request->post->remove('convertGET'); - - // replace all GET parameters with POST data - $request->get->replace($request->post->all()); - - // remove POST data - $request->post->clear(); - - // This is now a GET request - $request->setMethod('GET'); - - $request->prepare(); - } - } - - private function meta_refresh($matches){ - $url = $matches[2]; - return str_replace($url, proxify_url($url, $this->base_url), $matches[0]); - } - - // , <base>, <link>, <style>, <meta>, <script>, <noscript> - private function proxify_head($str){ - - // base - update base_url contained in href - remove <base> tag entirely - //$str = preg_replace_callback('/<base[^>]*href= - - // link - replace href with proxified - // link rel="shortcut icon" - replace or remove - - // meta - only interested in http-equiv - replace url refresh - // <meta http-equiv="refresh" content="5; url=http://example.com/"> - $str = preg_replace_callback('/content=(["\'])\d+\s*;\s*url=(.*?)\1/is', array($this, 'meta_refresh'), $str); - - return $str; - } - - // The <body> background attribute is not supported in HTML5. Use CSS instead. - private function proxify_css($str){ - - // The HTML5 standard does not require quotes around attribute values. - - // if {1} is not there then youtube breaks for some reason - $str = preg_replace_callback('@[^a-z]{1}url\s*\((?:\'|"|)(.*?)(?:\'|"|)\)@im', array($this, 'css_url'), $str); - - // https://developer.mozilla.org/en-US/docs/Web/CSS/@import - // TODO: what about @import directives that are outside <style>? - $str = preg_replace_callback('/@import (\'|")(.*?)\1/i', array($this, 'css_import'), $str); - - return $str; - } - - public function onCompleted(ProxyEvent $event){ - - // to be used when proxifying all the relative links - $this->base_url = $event['request']->getUri(); - $url_host = parse_url($this->base_url, PHP_URL_HOST); - - $response = $event['response']; - $content_type = $response->headers->get('content-type'); - - $str = $response->getContent(); - - // DO NOT do any proxification on .js files and text/plain content type - $no_proxify = array('text/javascript', 'application/javascript', 'application/x-javascript', 'text/plain'); - if(in_array($content_type, $no_proxify)){ - return; - } - - // let's remove all frames?? does not protect against the frames created dynamically via javascript - $str = preg_replace('@<iframe[^>]*>[^<]*<\\/iframe>@is', '', $str); - - $str = $this->proxify_head($str); - $str = $this->proxify_css($str); - - // src= and href= - $str = preg_replace_callback('@(?:src|href)\s*=\s*(["|\'])(.*?)\1@is', array($this, 'html_attr'), $str); - - // img srcset - $str = preg_replace_callback('/srcset=\"(.*?)\"/i', function($matches){ - $src = $matches[1]; - - // url_1 1x, url_2 4x, ... - $urls = preg_split('/\s*,\s*/', $src); - foreach($urls as $part){ - - // TODO: add str_until helper - $pos = strpos($part, ' '); - if($pos !== false){ - $url = substr($part, 0, $pos); - $src = str_replace($url, proxify_url($url, $this->base_url), $src); - } - } - - return 'srcset="'.$src.'"'; - }, $str); - - // form - $str = preg_replace_callback('@<form[^>]*action=(["\'])(.*?)\1[^>]*>@i', array($this, 'form_action'), $str); - - $response->setContent($str); - } + // to be used when proxifying all the relative links + $this->base_url = $event['request']->getUri(); + $proxified_content = preg_replace_callback_array(self::CONTENT_PARSERS, $response->getContent()); + $response->setContent($proxified_content); + } -} + public function onBeforeRequest(ProxyEvent $event) + { + $request = $event['request']; + $this->convertPostToGet($request); + } + + private function convertPostToGet($request) + { + if (!$request->post->has('convertGET')) { + return; + } + + $request->get->replace($request->post->all()); // Change POST data to GET data + $request->post->clear(); // Remove POST data + $request->setMethod('GET'); // This is now a GET request + $request->prepare(); + } + + private function proxifyFormCallback($matches) + { + $full_capture = $this->proxifyUrlCallback($matches); -?> + // If the form method is not post, inject method="post" and add a hidden input field called "convertGET" + $full_capture = preg_replace('@(<\s*form\s*)((?:(?!method=(\'|")post\3)[^>])*>)@i', '$1 method="post" $2<input type="hidden" name="convertGET" value="1">', $full_capture); + return $full_capture; + } + + private function proxifySrcsetAttributeCallback($matches) + { + $attribute = $matches[0]; + $value = $matches['value']; + $srcset_url_pattern = "@(?:\s*(?<url>[^\s,]*)(?:\s*(?:,|\S*)))@im"; + $proxified_value = preg_replace_callback($srcset_url_pattern, array($this, 'proxifyUrlCallback'), $value); + return str_replace($value, $proxified_value, $attribute); + } + + private function proxifyUrlCallback($matches) + { + $full_capture = $matches[0]; + if (!($url = $matches['url'] ?? null) || starts_with($url, self::LINK_TYPE_BLACKLIST)) { + return $full_capture; + } + + $proxified_url = proxify_url($url, $this->base_url); + return str_replace($url, $proxified_url, $full_capture); + } +} From fc6d87240378acb5c59ba0d596d3eb9f3cde295e Mon Sep 17 00:00:00 2001 From: reef-actor <reef-actor> Date: Mon, 29 Jan 2018 12:35:09 +0000 Subject: [PATCH 4/6] Simplify AbstractPlugin Reduced function call overhead. --- src/Plugin/AbstractPlugin.php | 110 +++++++++++----------------------- 1 file changed, 35 insertions(+), 75 deletions(-) diff --git a/src/Plugin/AbstractPlugin.php b/src/Plugin/AbstractPlugin.php index 605cee3..1ff4eca 100644 --- a/src/Plugin/AbstractPlugin.php +++ b/src/Plugin/AbstractPlugin.php @@ -1,81 +1,41 @@ <?php - namespace Proxy\Plugin; use Proxy\Event\ProxyEvent; -abstract class AbstractPlugin { - - // apply these methods only to those events whose request URL passes this filter - protected $url_pattern; - - public function onBeforeRequest(ProxyEvent $event){ - // fired right before a request is being sent to a proxy - } - - public function onHeadersReceived(ProxyEvent $event){ - // fired right after response headers have been fully received - last chance to modify before sending it back to the user - } - - public function onCurlWrite(ProxyEvent $event){ - // fired as the data is being written piece by piece - } - - public function onCompleted(ProxyEvent $event){ - // fired after the full response=headers+body has been read - will only be called on "non-streaming" responses - } - - final public function subscribe($dispatcher){ - - $dispatcher->addListener('request.before_send', function($event){ - $this->route('request.before_send', $event); - }); - - $dispatcher->addListener('request.sent', function($event){ - $this->route('request.sent', $event); - }); - - $dispatcher->addListener('curl.callback.write', function($event){ - $this->route('curl.callback.write', $event); - }); - - $dispatcher->addListener('request.complete', function($event){ - $this->route('request.complete', $event); - }); - } - - // dispatch based on filter - final private function route($event_name, ProxyEvent $event){ - $url = $event['request']->getUri(); - - // url filter provided and current request url does not match it - if($this->url_pattern){ - if(starts_with($this->url_pattern, '/') && preg_match($this->url_pattern, $url) !== 1){ - return; - } else if(stripos($url, $this->url_pattern) === false){ - return; - } - } - - switch($event_name){ - - case 'request.before_send': - $this->onBeforeRequest($event); - break; - - case 'request.sent': - $this->onHeadersReceived($event); - break; - - case 'curl.callback.write': - $this->onCurlWrite($event); - break; - - case 'request.complete': - $this->onCompleted($event); - break; - } - } -} +abstract class AbstractPlugin +{ + public function onBeforeRequest(ProxyEvent $event) + { + // fired right before a request is being sent to a proxy + } + + public function onHeadersReceived(ProxyEvent $event) + { + // fired right after response headers have been fully received - last chance to modify before sending it back to the user + } + + public function onCurlWrite(ProxyEvent $event) + { + // fired as the data is being written piece by piece + } -?> + public function onCompleted(ProxyEvent $event) + { + // fired after the full response=headers+body has been read - will only be called on "non-streaming" responses + } + + final public function subscribe($dispatcher) + { + $event_listeners = [ + 'request.before_send' => 'onBeforeRequest', + 'request.sent' => 'onHeadersReceived', + 'curl.callback.write' => 'onCurlWrite', + 'request.complete' => 'onCompleted', + ]; + + foreach ($event_listeners as $event => $listener) { + $dispatcher->addListener($event, [$this, $listener]); + } + } +} From 36aae6bc702eaa6e71ec4d35b286bdf78f4e6e52 Mon Sep 17 00:00:00 2001 From: reef-actor <reef-actor> Date: Thu, 1 Feb 2018 10:12:39 +0000 Subject: [PATCH 5/6] Return the url_pattern filter to AbstractPlugin --- src/Plugin/AbstractPlugin.php | 42 +++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Plugin/AbstractPlugin.php b/src/Plugin/AbstractPlugin.php index 1ff4eca..fd5c633 100644 --- a/src/Plugin/AbstractPlugin.php +++ b/src/Plugin/AbstractPlugin.php @@ -1,10 +1,21 @@ <?php + namespace Proxy\Plugin; use Proxy\Event\ProxyEvent; abstract class AbstractPlugin { + private const EVENT_LISTENERS = [ + 'request.before_send' => 'onBeforeRequest', + 'request.sent' => 'onHeadersReceived', + 'curl.callback.write' => 'onCurlWrite', + 'request.complete' => 'onCompleted', + ]; + + // apply these methods only to those events whose request URL passes this filter + protected $url_pattern; + public function onBeforeRequest(ProxyEvent $event) { // fired right before a request is being sent to a proxy @@ -27,15 +38,28 @@ public function onCompleted(ProxyEvent $event) final public function subscribe($dispatcher) { - $event_listeners = [ - 'request.before_send' => 'onBeforeRequest', - 'request.sent' => 'onHeadersReceived', - 'curl.callback.write' => 'onCurlWrite', - 'request.complete' => 'onCompleted', - ]; - - foreach ($event_listeners as $event => $listener) { - $dispatcher->addListener($event, [$this, $listener]); + foreach (self::EVENT_LISTENERS as $event_name => $listener) { + $dispatcher->addListener($event_name, function ($event) use ($event_name) { + $this->route($event_name, $event); + }); + } + } + + // dispatch based on filter + final private function route($event_name, ProxyEvent $event) + { + $url = $event['request']->getUri(); + + // url filter provided and current request url does not match it + if ($this->url_pattern) { + if (starts_with($this->url_pattern, '/') && preg_match($this->url_pattern, $url) !== 1) { + return; + } else if (stripos($url, $this->url_pattern) === false) { + return; + } } + + // Call the handler for this event + [$this, self::EVENT_LISTENERS[$event_name]]($event); } } From 4ce5868c33b455b467ebc0a6c998e4c27ea7aae9 Mon Sep 17 00:00:00 2001 From: reef-actor <reef-actor> Date: Mon, 5 Feb 2018 15:18:20 +0000 Subject: [PATCH 6/6] Remove const visibility modifiers froim AbstractPlugin & ProxifyPlugin --- src/Plugin/AbstractPlugin.php | 2 +- src/Plugin/ProxifyPlugin.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Plugin/AbstractPlugin.php b/src/Plugin/AbstractPlugin.php index fd5c633..69771b0 100644 --- a/src/Plugin/AbstractPlugin.php +++ b/src/Plugin/AbstractPlugin.php @@ -6,7 +6,7 @@ abstract class AbstractPlugin { - private const EVENT_LISTENERS = [ + const EVENT_LISTENERS = [ 'request.before_send' => 'onBeforeRequest', 'request.sent' => 'onHeadersReceived', 'curl.callback.write' => 'onCurlWrite', diff --git a/src/Plugin/ProxifyPlugin.php b/src/Plugin/ProxifyPlugin.php index e2b4631..11b4c65 100644 --- a/src/Plugin/ProxifyPlugin.php +++ b/src/Plugin/ProxifyPlugin.php @@ -6,9 +6,9 @@ class ProxifyPlugin extends AbstractPlugin { - private const CONTENT_TYPE_BLACKLIST = ['image', 'font', 'application/javascript', 'application/x-javascript', 'text/javascript', 'text/plain']; - private const LINK_TYPE_BLACKLIST = ['data:', 'magnet:', 'about:', 'javascript:', 'mailto:', 'tel:', 'ios-app:', 'android-app:']; - private const CONTENT_PARSERS = [ + const CONTENT_TYPE_BLACKLIST = ['image', 'font', 'application/javascript', 'application/x-javascript', 'text/javascript', 'text/plain']; + const LINK_TYPE_BLACKLIST = ['data:', 'magnet:', 'about:', 'javascript:', 'mailto:', 'tel:', 'ios-app:', 'android-app:']; + const CONTENT_PARSERS = [ '@\bcontent=(?<quote>\'|")\d+\s*;\s*url=(?<url>.*?)\k<quote>@is' => 'self::proxifyUrlCallback', // content="X;url=<url>" (meta-refresh) '@\b(?:src|href)\s*=\s*(?<quote>\'|")(?<url>.*?)\k<quote>@is' => 'self::proxifyUrlCallback', // src="<url>" & href="<url>" '@[^a-z]{1}url\s*\((?<delim>\'|"|)(?<url>[^\)]*)\k<delim>\)@im' => 'self::proxifyUrlCallback', // url(<url>)