<?php
// $Id$

/**
 * @file
 *
 * File and directory handling functions, including outside the drupal "safety" zone.
 *
 * These are crucial for providing access as well as checking directory existance.
 * Some of these are customized functions of the drupal equivalents, given that some
 * of the file handling is outside the drupal install directory.
 */

/**
 * Gets a simple URI, without the addition query parameters or anchors.
 *
 * @param $uri
 *   The URI.
 * @return
 *   The resulting URI
 */
function mapi_file_uri($uri) {
  // strip off any query
  $hash = strpos($uri, '#');
  $query = strpos($uri, '?');
  if ($hash || $query) {
    $length = ($hash !== false && ($hash < $query) ? $hash : ($query ? $query : strlen($uri)));
    $uri = substr($uri, 0, $length);
  }

  return $uri;
}

/**
 * Checks to see if a URI exists.
 *
 * @param $uri
 *   The URI.
 * @param $refresh
 *   Refreshs the offsite URI cache.
 * @return
 *   TRUE if file, or existing URI.
 */
function mapi_file_exists($uri, $refresh = FALSE) {
  static $uris = array();

  // setup the caching
  if ($refresh) {
    $uris = array();
  }

  // automatic disqualification for non-existant entities
  if (!$uri) {
    return FALSE;
  }

  // checks the standard check of the filename
  if ((is_file($uri) || array_key_exists($uri, $uris))) {
    return TRUE;
  }
  elseif (is_string($uri) && !is_numeric($uri)) {
    // find the domain name to be checked.
    if (!preg_match('~^(?:(http|https|ftp):\/\/)?(?:(.*?):(.*?)@)?((?:[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(?:(?:\.[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])*))(?::([0-9]{1,5}))?((?:\/.*?)(?:\?(.*?))?(\#(?:.*))?)~', trim($uri), $details)) {
      return FALSE;
    }

    // checking for allowed external files
    // make sure Windows and MAC line returns function.
    $paths = str_replace(array("\r", "\r\n"), "\n", variable_get('mapi_allowed_external', ''));

    // perform checks
    $checks = explode("\n", $paths);
    foreach ($checks as $path) {
      $search = str_replace('\*', '.*', preg_quote($path));

      // checked for allowed website
      if (preg_match('|^'. $search .'$|', $details[4])) {
        // check for the files existance on the external machine
        // NEEDS WORK: use stream context to see if a 400-405 HTTP error occured.
        if ($file = @fopen($uri, 'rb')) {
          fclose($file);
          $uris[$uri] = true;
          return TRUE;
        }
        break;
      }
    }
  }

  return FALSE;
}

/**
 * Provides the basename for a filename, without query or anchors of a URI.
 *
 * @param $filename
 *   The filename.
 * @return
 *   The basename
 */
function mapi_file_basename($filename) {
  // basename not UTF-8 safe with filenames that start with UTF-8 characters such as �bergabe.
  $filename = mapi_file_uri($filename);
  if (strpos($filename, '://')) {
    return basename($filename);
  }
  return basename('/'. $filename);
}

/**
 * Provides the base file name without extension or path.
 * This includes
 *
 * @param $filename
 *   The filename.
 * @return
 *   Returns the filename minus the extension
 */
function mapi_file_name($filename) {
  $fname = mapi_file_basename($filename);

  if (!$fname || !($pos = strrpos($fname, '.'))) {
    return $filename;
  }
  return substr($fname, 0, $pos);
}

/**
 * Provides the extension for a filename.
 *
 * @param $filename
 *   The filename.
 * @return
 *   The extension
 */
function mapi_file_ext($filename) {
  $fname = mapi_file_basename($filename);

  if (!$fname || !($pos = strrpos($fname, '.'))) {
    return '';
  }
  return strtolower(substr($fname, $pos + 1));
}

/**
 *  The following provide manipulation of the file system.
 *
 */

/**
 *  Returns the current workspace paths as allowed by the system
 *
 *  @return
 *     Returns a list of the directories allowed to access various parts of the system.
 */
function mapi_directory_locations($workspace = null) {
  return mapi_invoke_workspaces('paths', $workspace);
}

/**
 * Checks for the existance of the directory, within the major directories.
 *
 * @param $directory
 *   The directory to be checked
 * @return
 *   Returns FALSE on failure, TRUE on success
 */
function mapi_directory_check_location($directory, $workspace = null) {
  $directory = realpath($directory);
  $paths = mapi_directory_locations($workspace);
  foreach ($paths as $type => $path) {
    if (strpos($directory, realpath($path)) !== 0) {
      return TRUE;
    }
  }
  return FALSE;
}

/**
 * Checks for the existance of the directory, that it exists within the appropriate public or
 * private directories, and provides the ability to create it if necessary.
 *
 * @param $directory
 *   The directory to be checked
 * @param $create
 *   Attempts to create directory if non-existant.
 * @param $recursive
 *   Allows a recursive call to mapi_directory_check, for multiple path creation.
 * @return
 *   Returns FALSE on failure, otherwise the real path name.
 */
function mapi_directory_check($directory, $create = FALSE, $recursive = FALSE, $workspace = null) {
  // NEEDS WORK: recursive section hasn't been tested fully.

  // setup the path, normalized to unix pathway.
  $path = str_replace('\\', '/', $directory);

  // check if located in the public, private, or temp directories
  if ($path && !mapi_directory_check_location($path, $workspace)) {
    return FALSE;
  }

  // check and create if necessary
  if (!is_dir($path) && $create) {
    if ($recursive) {
      // get all of the parts of the path
      $path_parts = explode('/', $path);

      // construct the path from ground up.
      $new_path = '';
      while (count($path_parts) && !mapi_directory_check_location($new_path, $workspace)) {
        $new_path .= '/'. array_shift($path_parts);
      }

      // now attempt to construct from bottom up.
      while (count($path_parts)) {
        if (!is_dir($new_path)) {
          if (!@mkdir($new_path)) {
            return FALSE;
          }
          @chmod($path, 0775);
        }
        $new_path .= '/'. array_shift($path_parts);
      }
    }

    // make directory
    if (!@mkdir($path)) {
      return FALSE;
    }

    // make directory useable, if possible
    @chmod($path, 0775);
  }

  // check for directory existance
  if (!is_dir($path)) {
    return FALSE;
  }

  return $path;
}

/**
 * Move a directory to the appropriate location.
 *
 * @param $source
 *   The source directory.
 * @param $dest
 *   The destination directory.
 * @return
 *   Returns FALSE on failure, TRUE on success.
 */
function mapi_directory_move($source, $dest, $workspace = null) {
  // safety check
  if (!mapi_directory_check_location($source, $workspace)) {
    return FALSE;
  }

  // safety check
  if (!mapi_directory_check_location($dest, $workspace)) {
    return FALSE;
  }

  // check that dest is not inside source
  if (strpos(realpath(rtrim($dest, '/')), realpath(rtrim($source, '/'))) === 0) {
    return FALSE;
  }

  // make sure that nothing exists at destination
  if (is_file($dest) || is_dir($dest)) {
    return FALSE;
  }

  return @rename($source, $dest);
}

/**
 * Delete a directory.
 *
 * @param $directory
 *   The directory to be deleted.
 * @param $recursive
 *   Determines whether sub-directories are deleted
 * @return
 *   Returns FALSE on failure, TRUE on success.
 */
function mapi_directory_delete($directory, $recursive = FALSE, $workspace = null) {
  // safety check
  if (!$directory = mapi_directory_check($directory, $workspace)) {
    return FALSE;
  }

  // make sure that something exists at destination
  if (!is_dir($directory)) {
    return FALSE;
  }

  $dirs = array();

  // get all the files
  if ($dh = opendir($directory)) {
    while (($file = readdir($dh)) !== false) {
      // ignore the listings of '.', and '..'
      if (in_array($file, array('.', '..'))) {
        continue;
      }

      $path = $directory .'/'. $file;
      // delete file
      if (is_file($path)) {
        @unlink($path);
      }
      // otherwise list the sub-directories for later removal
      elseif (is_dir($path)) {
        $dirs[] = $file;
      }
    }
    closedir($dh);
  }

  // do we have sub-directories to consider?
  if (count($dirs)) {
    if ($recursive) {
      foreach ($dirs as $dir) {
        mapi_directory_delete($directory .'/'. $dir, true, $workspace);
      }
    }
    // not recursive
    else {
      return FALSE;
    }
  }

  return @rmdir($directory);
}

/**
 * Creates a directory
 *
 * @param $directory
 *   The directory to be created.
 * @param $recursive
 *   Determines whether parent directories are created.
 * @return
 *   Returns FALSE on failure, TRUE on success.
 */
function mapi_directory_create($directory, $recursive = false, $workspace = null) {
  return mapi_directory_check($directory, true, $recursive, $workspace);
}

/**
 * Checks whether a file is located in the public, private or temp directories.
 *
 * @param $filename
 *   The filename to delete.
 * @return
 *   Returns FALSE on failure, returns the filename.
 */
function mapi_file_check_location($filename, $workspace = null) {
  // list of directories that a file can reside in.
  $dirs = mapi_directory_locations($workspace);

  // check all the directories
  foreach ($dirs as $dir) {
    if ($fname = file_check_location($filename, $dir)) {
      return $filename;
    }
  }

  // NEEDS WORK: check offsite locations

  // otherwise failed
  return FALSE;
}

/**
 * Copies a file from one place to another.
 *
 * @param $source
 *   The source filename.
 * @param $dest
 *   The destination filename.
 * @param $replace Replace behavior when the destination file already exists.
 *   - FILE_EXISTS_REPLACE - Replace the existing file
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is unique
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
 * @return
 *   Returns FALSE on failure, and the copied filename on success.
 */
function mapi_file_copy($source, $dest, $replace = FILE_EXIST_RENAME, $workspace = null) {
  // do check for source
  if (!$source = mapi_file_check_location($source, $workspace)) {
    return FALSE;
  }

  // do check for destination
  if (!$dest = mapi_file_check_location($dest, $workspace)) {
    return FALSE;
  }

  // only move the files if the are not the same
  if ($source != $dest) {
    if (is_file($dest)) {
      switch ($replace) {
        case FILE_EXIST_RENAME:
          $dest = mapi_file_freename($dest);
          break;
        case FILE_EXISTS_ERROR:
          return FALSE;
      }
    }

    // now simply rename file, this should preserve the file settings, etc.
    if (!@copy($source, $dest)) {
      return FALSE;
    }

    // Give everyone read access so that FTP'd users or
    // non-webserver users can see/read these files.
    @chmod($dest, 0664);
  }

  return $dest;
}

/**
 * Moves a given filename to another location.
 *
 * @param $source
 *   The source filename.
 * @param $dest
 *   The destination filename.
 * @param $replace Replace behavior when the destination file already exists.
 *   - FILE_EXISTS_REPLACE - Replace the existing file
 *   - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is unique
 *   - FILE_EXISTS_ERROR - Do nothing and return FALSE.
 * @return
 *   Returns FALSE on failure, and the moved filename on success.
 */
function mapi_file_move($source, $dest, $replace = FILE_EXIST_RENAME, $workspace = null) {
  $source = realpath($source);
  if ($dest = mapi_file_copy($source, $dest, $replace, $workspace)) {
    if ($source != $dest && file_delete($source)) {
      return $dest;
    }
  }

  return FALSE;
}

/**
 * Deletes only valid filenames.
 *
 * @param $filename
 *   The filename to delete.
 * @return
 *   Returns FALSE on failure, TRUE on success.
 */
function mapi_file_delete($filename, $workspace = null) {
  if ($path = mapi_file_check_location($filename, $workspace)) {
    return file_delete($path);
  }
}

/**
 * Transfer file using http to client. Pipes a file through Drupal to the
 * client.
 *
 * @param $source File to transfer.
 * @param $headers An array of http headers to send along with file.
 */
function mapi_file_transfer($source, $headers) {
  ob_end_clean();

  foreach ($headers as $header) {
    // To prevent HTTP header injection, we delete new lines that are
    // not followed by a space or a tab.
    // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
    $header = preg_replace('/\r?\n(?!\t| )/', '', $header);
    drupal_set_header($header);
  }

  $source = realpath($source);

  // Transfer file in 1024 byte chunks to save memory usage.
  if (is_file($source) && ($fd = fopen($source, 'rb'))) {
    while (!feof($fd)) {
      print fread($fd, 1024);
    }
    fclose($fd);
  }
  else {
    drupal_not_found();
  }
  exit();
}

/**
 * Returns the mime type for a filename, including URLs with http:// or https://
 * format. Will attempt to use the extension determination process first.
 *
 * @param $filename
 *   Name of file to check for mimetype
 * @return
 *   The mime type of a file, 'application/octet-stream' if unknown
 */
function mapi_file_mimetype($filename) {
  // attempt to find mime by extension first
  if ($ext = mapi_file_ext($filename)) {
    return mapi_extension_mime($ext);
  }

  // don't allow other schema apart from http, or https.
  if (strpos($filename, '://') === false || preg_match('#^http(s|)://#', $filename)) {
    // create stream context for testing the HTTP result, since we're only dealing with HTTP
    $options = array(
      'http' => array(
        'user_agent'    => 'spider',
        'max_redirects' => 10,
        'timeout'       => 120,
      )
    );

    // make sure the full URL is there
    if (strpos($filename, '://') === false) {
      $filename = url('/' . $filename, null, null, TRUE);
    }

    // open file and get the details for the looking at the mimetype
    $context = stream_context_create($options);
    if ($handle = @fopen($filename, 'r', false, $context)) {
      $results = array();
      if (!empty($http_response_header)) {
        $results = $http_response_header;
      }
      fclose($handle);

      foreach (array_reverse($results) as $line) {
        if (preg_match( '@Content-Type:\s+([\w\-\./+]+)(;\s+charset=(\S+))?@i', $line, $matches )) {
          // check mime type
          if (!isset($matches[1])) {
            continue;
          }
          return $matches[1];
        }
      }
    }
  }

  return 'application/octet-stream';
}

/**
 * Creates a filename which will not overwrite ones already existing.
 *
 * @param $filename
 *   Name of filename
 * @return
 *   A unique filename which preserves the extension and general details of the filename.
 */
function mapi_file_freename($filename) {
  $dir = dirname($filename);
  $name = mapi_file_name($filename);
  $ext = mapi_file_ext($filename);

  // check for the last _d on name (so we avoid _0_0_0 scenarios
  if ($pos = strrpos($name, '_')) {
    if (is_numeric(substr($name, $pos+1))) {
      $name = substr($name, 0, $pos);
    }
  }

  $count = 1;
  $filename = $dir .'/'. $name .'.'. $ext;
  while (is_file($filename)) {
    $filename = $dir .'/'. $name .'_'. $count++ .'.'. $ext;
  }

  return $filename;
}
