Search code examples
phpwrapperfopenphp-stream-wrappers

How to determine whether a string points to a valid file or wrapper in PHP?


Say I want to tell whether a string passed to fopen represents either a file path or a valid wrapper (e.g. "/home/user/example.txt" vs "php://input"). This is for the purpose of creating a tmpfile from what's in php://input to work around fseeking limitations for PHP wrappers.

As shown below, file_exists works for files, but not for wrapper URIs:

var_dump(file_exists("php://input"));
var_dump(file_exists("./exists.txt"));
var_dump(file_exists("./non_existent.txt"));
var_dump(file_exists("php://garbage"));

gives

bool(false)
bool(true)
bool(false)
bool(false)

The only one returning true is the file. I've found stream_get_wrappers() but I want to avoid complicating the check too much (such as using string comparison to try to detect a wrapper).

Using stream_get_meta_data does also seem to work, but it requires a call to fopen first, which would clog up error logs.

var_dump(stream_get_meta_data(fopen("php://input","r+")));
var_dump(stream_get_meta_data(fopen("./exists.txt","r+")));
var_dump(stream_get_meta_data(fopen("./non_existent.txt","r+")));
var_dump(stream_get_meta_data(fopen("php://garbage","r+")));

produces

array(9) {
  ["timed_out"]=>
  bool(false)
  ["blocked"]=>
  bool(true)
  ["eof"]=>
  bool(false)
  ["wrapper_type"]=>
  string(3) "PHP"
  ["stream_type"]=>
  string(5) "Input"
  ["mode"]=>
  string(2) "rb"
  ["unread_bytes"]=>
  int(0)
  ["seekable"]=>
  bool(true)
  ["uri"]=>
  string(11) "php://input"
}
array(9) {
  ["timed_out"]=>
  bool(false)
  ["blocked"]=>
  bool(true)
  ["eof"]=>
  bool(false)
  ["wrapper_type"]=>
  string(9) "plainfile"
  ["stream_type"]=>
  string(5) "STDIO"
  ["mode"]=>
  string(2) "r+"
  ["unread_bytes"]=>
  int(0)
  ["seekable"]=>
  bool(true)
  ["uri"]=>
  string(10) "./exists.txt"
}
NULL
NULL

I can use the wrapper_type from the array returned by stream_get_meta_data, but it still will spew garbage into logs if the file or wrapper URI doesn't exist, which I want to avoid.

What's the best way to detect whether my input string (to be passed to fopen) contains either a valid file path for an existing file or a valid PHP wrapper, or neither?

Update: I found a workaround that solves the problem, at the expense of an extra fopen call. I've put this in an answer below.


Solution

  • Update

    I was able to work around it like this:

    class example {
    
        var $file;
    
        function open($path) {
            $testHandle = fopen($path,"rb");
                    if(!$testHandle) {
                            error_log("Error parsing file: could not open $path");
                            return false;
                }
    
            $wrapperType = stream_get_meta_data($testHandle)["wrapper_type"];
            if ($wrapperType != "plainfile") {
                    $this->file = tmpfile();
                    fwrite($this->file,file_get_contents($path));
                    fclose($testHandle);
            } else {
                    $this->file = $testHandle;
            }
    
        }
    
    }
    

    If the passed $path (e.g. php://input) is not a directly-opened file, it will create a temporary file (with tmpfile()) and write the contents of the stream into that temporary file, closing $testHandle after. If, however, it is a file opened off the filesystem, (e.g. /path/to/file) it will simply set $this->file to $testHandle.

    This ensures that I am working with a file handle consistently; it should work out fine for me as none of the files I'm reading will be larger than a megabyte or so. However, I'd still like to be able to ditch the extra fopen call.