<?php

/**
 * @file
 * Implementation of plupload.module.
 */

/**
 * Implements hook_menu().
 */
function plupload_menu() {
  $items['plupload-handle-uploads'] = array(
    'title' => 'Handles uploads',
    'page callback' => 'plupload_handle_uploads',
    'type' => MENU_CALLBACK,
    'access callback' => 'plupload_upload_access',
    'access arguments' => array('access content'),
  );
  $items['plupload-test'] = array(
    'title' => 'Test Plupload',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('plupload_test'),
    // @todo: change this to something appropriate, not sure what.
    'access arguments' => array('Administer site configuration'),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Verifies the token for this request.
 */
function plupload_upload_access() {
  foreach (func_get_args() as $permission) {
    if (!user_access($permission)) {
      return FALSE;
    }
  }
  return !empty($_REQUEST['plupload_token']) && drupal_valid_token($_REQUEST['plupload_token'], 'plupload-handle-uploads');
}

/**
 * Form callback function for test page visible at URL "plupload-test".
 */
function plupload_test($form, &$form_state) {
  $form['pud'] = array(
    '#type' => 'plupload',
    '#title' => 'Plupload',
    // '#validators' => array(...);
  );

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => 'Submit',
  );

  return $form;
}

/**
 * Submit callback for plupload_test form.
 */
function plupload_test_submit($form, &$form_state) {
  $saved_files = array();
  $scheme = variable_get('file_default_scheme', 'public') . '://';
  // We can't use file_save_upload() because of
  // http://www.jacobsingh.name/content/tight-coupling-no-not
  // file_uri_to_object();
  foreach ($form_state['values']['pud'] as $uploaded_file) {
    if ($uploaded_file['status'] == 'done') {
      $source = $uploaded_file['tmppath'];
      $destination = file_stream_wrapper_uri_normalize($scheme . $uploaded_file['name']);
      // Rename it to its original name, and put it in its final home.
      // Note - not using file_move here because if we call file_get_mime
      // (in file_uri_to_object) while it has a .tmp extension, it horks.
      $destination = file_unmanaged_move($source, $destination, FILE_EXISTS_RENAME);
      $file = plupload_file_uri_to_object($destination);
      file_save($file);
      $saved_files[] = $file;
    }
    else {
      // @todo: move this to element validate or something and clean up t().
      form_set_error('pud', "Upload of {$uploaded_file['name']} failed");
    }
  }
}

/**
 * Implements hook_element_info().
 */
function plupload_element_info() {
  $types = array();
  $module_path = drupal_get_path('module', 'plupload');
  $types['plupload'] = array(
    '#input' => TRUE,
    '#attributes' => array('class' => array('plupload-element')),
    // @todo
    // '#element_validate' => array('file_managed_file_validate'),
    '#theme_wrappers' => array('form_element'),
    '#theme' => 'container',
    '#value_callback' => 'plupload_element_value',
    '#attached' => array(
      'library' => array(array('plupload', 'plupload')),
      'js' => array($module_path . '/plupload.js'),
      'css' => array($module_path . '/plupload.css'),
    ),
    '#process' => array('plupload_element_process'),
    '#element_validate' => array('plupload_element_validate'),
    '#pre_render' => array('plupload_element_pre_render'),
  );
  return $types;
}

/**
 * Validate callback for plupload form element.
 */
function plupload_element_value(&$element, $input = FALSE, $form_state = NULL) {
  $id = $element['#id'];
  // If a unique identifier added with '--', we need to exclude it
  if(preg_match('/(.*)(--[0-9]+)$/', $id, $reg)) {
    $id = $reg[1];
  }
  $files = array();
  foreach ($form_state['input'] as $key => $value) {
    if (preg_match('/' . $id . '_([0-9]+)_(.*)/', $key, $reg)) {
      $i = $reg[1];
      $key = $reg[2];

      // Only add the keys we expect.
      if (!in_array($key, array('tmpname', 'name', 'status'))) {
        continue;
      }

      // Munge the submitted file names for security.
      //
      // Similar munging is normally done by file_save_upload(), but submit
      // handlers for forms containing plupload elements can't use
      // file_save_upload(), for reasons discussed in plupload_test_submit().
      // So we have to do this for them.
      //
      // Note that we do the munging here in the value callback function
      // (rather than during form validation or elsewhere) because we want to
      // actually modify the submitted values rather than reject them outright;
      // file names that require munging can be innocent and do not necessarily
      // indicate an attempted exploit. Actual validation of the file names is
      // performed later, in plupload_element_validate().
      if (in_array($key, array('tmpname', 'name'))) {
        // Find the whitelist of extensions to use when munging. If there are
        // none, we'll be adding default ones in plupload_element_process(), so
        // use those here.
        if (isset($element['#upload_validators']['file_validate_extensions'][0])) {
          $extensions = $element['#upload_validators']['file_validate_extensions'][0];
        }
        else {
          $validators = _plupload_default_upload_validators();
          $extensions = $validators['file_validate_extensions'][0];
        }
        $value = file_munge_filename($value, $extensions, FALSE);
        // To prevent directory traversal issues, make sure the file name does
        // not contain any directory components in it. (This more properly
        // belongs in the form validation step, but it's simpler to do here so
        // that we don't have to deal with the temporary file names during form
        // validation and can just focus on the final file name.)
        //
        // This step is necessary since this module allows a large amount of
        // flexibility in where its files are placed (for example, they could
        // be intended for public://subdirectory rather than public://, and we
        // don't want an attacker to be able to get them back into the top
        // level of public:// in that case).
        $value = rtrim(drupal_basename($value), '.');


        // Based on the same feture from file_save_upload().
        if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $value) && (substr($value, -4) != '.txt')) {
          $value .= '.txt';

          // The .txt extension may not be in the allowed list of extensions.
          // We have to add it here or else the file upload will fail.
          if (!empty($extensions)) {
            $element['#upload_validators']['file_validate_extensions'][0] .= ' txt';
            drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $value)));
          }
        }
      }

      // The temporary file name has to be processed further so it matches what
      // was used when the file was written; see plupload_handle_uploads().
      if ($key == 'tmpname') {
        $value = _plupload_fix_temporary_filename($value);
        // We also define an extra key 'tmppath' which is useful so that submit
        // handlers do not need to know which directory plupload stored the
        // temporary files in before trying to copy them.
        $files[$i]['tmppath'] = variable_get('plupload_temporary_uri', 'temporary://') . $value;
      }
      elseif ($key == 'name') {
        if (module_exists('transliteration') && variable_get('transliteration_file_uploads', TRUE)) {
          $value = transliteration_clean_filename($value);
        }
      }

      // Store the final value in the array we will return.
      $files[$i][$key] = $value;
    }
  }
  return $files;
}

/**
 * Process callback (#process) for plupload form element.
 */
function plupload_element_process($element) {
  // Start session if not there yet. We need session if we want security
  // tokens to work properly.
  if (!drupal_session_started()) {
    drupal_session_start();
  }

  if (!isset($element['#upload_validators'])) {
    $element['#upload_validators'] = array();
  }
  $element['#upload_validators'] += _plupload_default_upload_validators();
  return $element;
}

/**
 * Element validation handler for a Plupload element.
 */
function plupload_element_validate($element, &$form_state) {
  foreach ($element['#value'] as $file_info) {
    // Here we create a $file object for a file that doesn't exist yet,
    // because saving the file to its destination is done in a submit handler.
    // Using tmp path will give validators access to the actual file on disk and
    // filesize information. We manually modify filename and mime to allow
    // extension checks.
    $file = plupload_file_uri_to_object($file_info['tmppath']);

    $destination = variable_get('file_default_scheme', 'public') . '://' . $file_info['name'];
    $destination = file_stream_wrapper_uri_normalize($destination);
    $file->filename = drupal_basename($destination);
    $file->filemime = file_get_mimetype($destination);

    foreach (file_validate($file, $element['#upload_validators']) as $error_message) {
      $message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename));
      form_error($element, $message . ' ' . $error_message);
    }
  }
}

/**
 * Pre render (#pre_render) callback to attach JS settings for the element.
 */
function plupload_element_pre_render($element) {
  $settings = isset($element['#plupload_settings']) ? $element['#plupload_settings'] : array();

  // The Plupload library supports client-side validation of file extension, so
  // pass along the information for it to do that. However, as with all client-
  // side validation, this is a UI enhancement only, and not a replacement for
  // server-side validation.
  if (empty($settings['filters']) && isset($element['#upload_validators']['file_validate_extensions'][0])) {
    $settings['filters'][] = array(
      // @todo Some runtimes (e.g., flash) require a non-empty title for each
      //   filter, but I don't know what this title is used for. Seems a shame
      //   to hard-code it, but what's a good way to avoid that?
      'title' => t('Allowed files'),
      'extensions' => str_replace(' ', ',', $element['#upload_validators']['file_validate_extensions'][0]),
    );
  }
  // Check for autoupload and autosubmit settings and add appropriate callback.
  if (!empty($element['#autoupload'])) {
    $settings['init']['FilesAdded'] = 'Drupal.plupload.filesAddedCallback';
    if (!empty($element['#autosubmit'])) {
      $settings['init']['UploadComplete'] = 'Drupal.plupload.uploadCompleteCallback';
    }
  }
  // Add a specific submit element that we want to click if one is specified.
  if (!empty($element['#submit_element'])) {
    $settings['submit_element'] = $element['#submit_element'];
  }
  // Check if there are event callbacks and append them to current ones, if any.
  if (!empty($element['#event_callbacks'])) {
    // array_merge() only accepts parameters of type array.
    if (!isset($settings['init'])) {
      $settings['init'] = array();
    }
    $settings['init'] = array_merge($settings['init'], $element['#event_callbacks']);
  }

  if (empty($element['#description'])) {
    $element['#description'] = '';
  }
  $element['#description'] = theme('file_upload_help', array('description' => $element['#description'], 'upload_validators' => $element['#upload_validators']));

  $element['#attached']['js'][] = array(
    'type' => 'setting',
    'data' => array('plupload' => array($element['#id'] => $settings)),
  );

  return $element;
}

/**
 * Returns the path to the plupload library.
 */
function _plupload_library_path() {
  return variable_get('plupload_library_path', module_exists('libraries') ? libraries_get_path('plupload') : 'sites/all/libraries/plupload');
}

/**
 * Implements hook_library().
 */
function plupload_library() {
  $library_path = _plupload_library_path();
  $libraries['plupload'] = array(
    'title' => 'Plupload',
    'website' => 'http://www.plupload.com',
    'version' => '1.5.1.1',
    'js' => array(
      // @todo - only add gears JS if gears is an enabled runtime.
      // $library_path . '/js/gears_init.js' => array(),
      $library_path . '/js/jquery.plupload.queue/jquery.plupload.queue.js' => array(),
      $library_path . '/js/plupload.full.js' => array(),
      0 => array(
        'type' => 'setting',
        'data' => array(
          'plupload' => array(
            // Element-specific settings get keyed by the element id (see
            // plupload_element_pre_render()), so put default settings in
            // '_default' (Drupal element ids do not have underscores, because
            // they have hyphens instead).
            '_default' => array(
              // @todo Provide a settings page for configuring these.
              'runtimes' => 'html5,flash,html4',
              'url' => url('plupload-handle-uploads', array('query' => array('plupload_token' => drupal_get_token('plupload-handle-uploads')))),
              'max_file_size' => file_upload_max_size() . 'b',
              'chunk_size' => parse_size(ini_get('post_max_size')) . 'b',
              'unique_names' => TRUE,
              'flash_swf_url' => file_create_url($library_path . '/js/plupload.flash.swf'),
              'silverlight_xap_url' => file_create_url($library_path . '/js/plupload.silverlight.xap'),
            ),
          ),
        ),
      ),
    ),
  );

  if (module_exists('locale')) {
    $module_path = drupal_get_path('module', 'plupload');
    $libraries['plupload']['js'][$module_path . '/js/i18n.js'] = array('scope' => 'footer');
  }

  return $libraries;
}

/**
 * Callback that handles and saves uploaded files.
 *
 * This will respond to the URL on which plupoad library will upload files.
 */
function plupload_handle_uploads() {
  // @todo: Implement file_validate_size();
  // Added a variable for this because in HA environments, temporary may need
  // to be a shared location for this to work.
  $temp_directory = variable_get('plupload_temporary_uri', 'temporary://');
  $writable = file_prepare_directory($temp_directory, FILE_CREATE_DIRECTORY);
  if (!$writable) {
    die('{"jsonrpc" : "2.0", "error" : {"code": 104, "message": "Failed to open temporary directory."}, "id" : "id"}');
  }
  // Try to make sure this is private via htaccess.
  file_create_htaccess($temp_directory, TRUE);

  // Chunk it?
  $chunk = isset($_REQUEST["chunk"]) ? $_REQUEST["chunk"] : 0;

  // Get and clean the filename.
  $file_name = isset($_REQUEST["name"]) ? $_REQUEST["name"] : '';
  $file_name = _plupload_fix_temporary_filename($file_name);

  // Check the file name for security reasons; it must contain letters, numbers
  // and underscores followed by a (single) ".tmp" extension. Since this check
  // is more stringent than the one performed in plupload_element_value(), we
  // do not need to run the checks performed in that function here. This is
  // fortunate, because it would be difficult for us to get the correct list of
  // allowed extensions to pass in to file_munge_filename() from this point in
  // the code (outside the form API).
  if (empty($file_name) || !preg_match('/^\w+\.tmp$/', $file_name)) {
    die('{"jsonrpc" : "2.0", "error" : {"code": 105, "message": "Invalid temporary file name."}, "id" : "id"}');
  }

  // Look for the content type header.
  if (isset($_SERVER["HTTP_CONTENT_TYPE"])) {
    $content_type = $_SERVER["HTTP_CONTENT_TYPE"];
  }
  if (isset($_SERVER["CONTENT_TYPE"])) {
    $content_type = $_SERVER["CONTENT_TYPE"];
  }

  // Is this a multipart upload?.
  if (strpos($content_type, "multipart") !== FALSE) {
    if (isset($_FILES['file']['tmp_name']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
      // Open temp file.
      $out = fopen($temp_directory . $file_name, $chunk == 0 ? "wb" : "ab");
      if ($out) {
        // Read binary input stream and append it to temp file.
        $in = fopen($_FILES['file']['tmp_name'], "rb");

        if ($in) {
          while ($buff = fread($in, 4096)) {
            fwrite($out, $buff);
          }
          fclose($in);
        }
        else {
          die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
        }

        fclose($out);
        drupal_unlink($_FILES['file']['tmp_name']);
      }
      else {
        die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
      }
    }
    else {
      die('{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}');
    }
  }
  else {
    // Open temp file.
    $out = fopen($temp_directory . $file_name, $chunk == 0 ? "wb" : "ab");
    if ($out) {
      // Read binary input stream and append it to temp file.
      $in = fopen("php://input", "rb");

      if ($in) {
        while ($buff = fread($in, 4096)) {
          fwrite($out, $buff);
        }
        fclose($in);
      }
      else {
        die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
      }

      fclose($out);
    }
    else {
      die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
    }
  }

  // Return JSON-RPC response.
  die('{"jsonrpc" : "2.0", "result" : null, "id" : "id"}');
}

/**
 * Returns a file object which can be passed to file_save().
 *
 * @param string $uri
 *   A string containing the URI, path, or filename.
 *
 * @return boolean
 *   A file object, or FALSE on error.
 *
 * @todo Replace with calls to this function with file_uri_to_object() when
 * http://drupal.org/node/685818 is fixed in core.
 */
function plupload_file_uri_to_object($uri) {
  global $user;
  $uri = file_stream_wrapper_uri_normalize($uri);
  $wrapper = file_stream_wrapper_get_instance_by_uri($uri);
  $file = new StdClass();
  $file->uid = $user->uid;
  $file->filename = drupal_basename($uri);
  $file->uri = $uri;
  $file->filemime = file_get_mimetype($uri);
  // This is gagged because some uris will not support it.
  $file->filesize = @filesize($uri);
  $file->timestamp = REQUEST_TIME;
  $file->status = FILE_STATUS_PERMANENT;
  return $file;
}

/**
 * Fix the temporary filename provided by the plupload library.
 *
 * Newer versions of the plupload JavaScript library upload temporary files
 * with names that contain the intended final prefix of the uploaded file
 * (e.g., ".jpg" or ".png"). Older versions of the plupload library always use
 * ".tmp" as the temporary file extension.
 *
 * We prefer the latter behavior, since although the plupload temporary
 * directory where these files live is always expected to be private (and we
 * protect it via .htaccess; see plupload_handle_uploads()), in case it ever
 * isn't we don't want people to be able to upload files with an arbitrary
 * extension into that directory.
 *
 * This function therefore fixes the plupload temporary filenames so that they
 * will always use a ".tmp" extension.
 *
 * @param string $filename
 *   The original temporary filename provided by the plupload library.
 *
 * @return string
 *   The corrected temporary filename, with a ".tmp" extension replacing the
 *   original one.
 */
function _plupload_fix_temporary_filename($filename) {
  $pos = strpos($filename, '.');
  if ($pos !== FALSE) {
    $filename = substr_replace($filename, '.tmp', $pos);
  }
  return $filename;
}

/**
 * Helper function to add defaults to $element['#upload_validators'].
 */
function _plupload_default_upload_validators() {
  return array(
    // See file_save_upload() for details.
    'file_validate_extensions' => array('jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'),
  );
}
