From 43b761d04501ed12a2dacb562c666a5a21893481 Mon Sep 17 00:00:00 2001
From: Yuriy Gerasimov <yuri.gerasimov@gmail.com>
Date: Wed, 6 Apr 2011 20:43:25 +0300
Subject: [PATCH] Issue #1010544 by miraclegr,ygerasimov: Files resource tests. Rename file if file exists during creation.

---
 resources/file_resource.inc                     |  174 ++++++++++-----------
 tests/functional/ServicesResourceFileTests.test |  194 +++++++++++++++++++++++
 2 files changed, 278 insertions(+), 90 deletions(-)
 create mode 100644 tests/functional/ServicesResourceFileTests.test

diff --git a/resources/file_resource.inc b/resources/file_resource.inc
index f188f10..551247d 100644
--- a/resources/file_resource.inc
+++ b/resources/file_resource.inc
@@ -25,7 +25,7 @@ function _file_resource_definition() {
           array(
             'name' => 'file',
             'type' => 'array',
-            'description'    => t('An array representing a file.'),
+            'description' => t('An array representing a file.'),
             'source' => 'data',
             'optional' => FALSE,
           ),
@@ -108,27 +108,27 @@ function _file_resource_definition() {
  * Adds a new file and returns the fid.
  *
  * @param $file
- *   An object as representing the file.
+ *   An array as representing the file.
  * @return
  *   Unique identifier for the file (fid) or errors if there was a problem.
  */
 function _file_resource_create($file) {
   global $user;
-  $file = (object) $file;
 
-  // If the file data is empty then bail
-  if (!isset($file->file)) {
-    return FALSE;
+  // If the file data is empty then bail.
+  if (!isset($file['file'])) {
+    return services_error('No file data received.');
   }
 
-  // If the submitted file is an update, then set the update parameter for
-  // drupal_write_record(), indicating such. Otherwise we can just pass the
-  // object in and it will be treated as an insert.
-  $update = array();
-  if (!empty($file->fid)) {
-    $update = 'fid';
+  // Check whether we have filename or filepath for the file.
+  if ((!isset($file['filename']) || empty($file['filename']))
+   && (!isset($file['filepath']) || empty($file['filepath']))) {
+    return services_error('No filename and no filepath specified or they are both empty.');
   }
 
+  // Ensure we create new file.
+  $file['fid'] = NULL;
+
   // Build the list of non-munged extensions.
   // @todo: this should not be here. we need to figure out the right place.
   // @todo: also isn't that repeated variable get a waste? I mean, I guess it
@@ -139,56 +139,54 @@ function _file_resource_create($file) {
     variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps odt ods odp'));
   }
 
+  // If filename has not been specified extract it from filepath.
+  if (!isset($file['filename']) || empty($file['filename'])) {
+    $file['filename'] = trim(basename($file['filepath']), '.');
+  }
+
+  // If no filepath specified use standard directory.
+  if (!isset($file['filepath']) || empty($file['filepath'])) {
+    $file['filepath'] = file_directory_path() . '/' . $file['filename'];
+  }
+
   // Get the directory name for the location of the file:
-  $dir = dirname($file->filepath);
+  $dir = dirname($file['filepath']);
   // Build the destination folder tree if it doesn't already exists.
   if (!file_check_directory($dir, FILE_CREATE_DIRECTORY)) {
-    return services_error("Could not create destination directory for file.");
+    return services_error("Could not create destination directory for file. " . $dir);
   }
-
-  // Update file object as necessary
-  $file->filepath = file_destination(file_create_path($file->filepath), FILE_EXISTS_RENAME);
-  $file->filename = file_munge_filename(trim(basename($file->filepath), '.'), $extensions, TRUE);
-  $file->filemime = file_get_mimetype($file->filename);
+  // Update file data as necessary
+  $file['filepath'] = file_destination(file_create_path($file['filepath']), FILE_EXISTS_RENAME);
+  $file['filename'] = file_munge_filename(trim(basename($file['filepath']), '.'), $extensions, TRUE);
+  $file['filemime'] = file_get_mimetype($file['filename']);
+  $destination = file_destination(file_create_path($file['filepath']), FILE_EXISTS_RENAME);
 
   // Rename potentially executable files, to help prevent exploits.
-  if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file->filename) && (drupal_substr($file->filename, -4) != '.txt')) {
-    $file->filemime = 'text/plain';
-    $file->filepath .= '.txt';
-    $file->filename .= '.txt';
+  if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file['filename']) && (drupal_substr($file['filename'], -4) != '.txt')) {
+    $file['filemime'] = 'text/plain';
+    $file['filepath'] .= '.txt';
+    $file['filename'] .= '.txt';
+    // As the file may be named example.php.txt, we need to munge again to
+    // convert to example.php_.txt, then create the correct destination.
+    $file['filename'] = file_munge_filename($file['filename'], $extensions, TRUE);
+    $destination = file_destination(file_create_path($file['filepath']), FILE_EXISTS_RENAME);
   }
 
-  // If the destination is not provided, or is not writable, error our
-  if (empty($file->filepath) || file_check_path($file->filepath) === FALSE) {
-    return services_error("Destintion directory does not exist or is not writeable.");
+  if (!$filename = file_save_data(base64_decode($file['file']), $destination)) {
+    return services_error("Could not write file to destination");
   }
-
-  //The filepath that ends up in the node must contain the filename
-  $file->filepath .= '/'. $file->filename;
+  $file['filepath'] = $destination;
 
   // Update the timestamp to the current time, otherwise the file could
   // get deleted on the next cron run if its status is set to 0.
-  $file->timestamp = time();
-
-  // Write the file
-  if (!file_save_data(base64_decode($file->file), $file->filepath)) {
-    return services_error("Could not write file to destination");
-  }
+  $file['timestamp'] = time();
 
   // If we made it this far it's safe to record this file in the database.
   drupal_write_record('files', $file, $update);
 
-  // hook_file_insert() requires an object
-  if (empty($update)) {
-    foreach (module_implements('file_insert') as $module) {
-      $function =  $module .'_file_insert';
-      $function($file);
-    }
-  }
-
-  return (object)array(
-    'fid' => $file->fid,
-    'uri' => services_resource_uri(array('file', $fid)),
+  return array(
+    'fid' => $file['fid'],
+    'uri' => services_resource_uri(array('file', $file['fid'])),
   );
 }
 
@@ -200,65 +198,59 @@ function _file_resource_create($file) {
  * @return
  *   The file
  */
-function _file_resource_retrieve($fid, $file_include_contents =  TRUE) {
+function _file_resource_retrieve($fid, $file_contents =  TRUE) {
   if ($file = db_fetch_array(db_query('SELECT * FROM {files} WHERE fid = %d', $fid))) {
-    if ($file_include_contents) {
-      $filepath = file_create_path($file['filepath']);
-      $binaryfile = fopen($filepath, 'rb');
-      $file['file'] = base64_encode(fread($binaryfile, filesize($filepath)));
+    if ($file_contents) {
+      $binaryfile = fopen($file['filepath'], 'rb');
+      if ($binaryfile === FALSE) {
+        services_error(t('Cannot open file with ID %fid.', array('%fid' => $fid)));
+      }
+      $file['file'] = base64_encode(fread($binaryfile, filesize($file['filepath'])));
       fclose($binaryfile);
     }
     return $file;
   }
-  else {
-    return services_error(t('There is no file with the given ID.'));
-  }
+  return services_error(t('There is no file with the given ID.'));
 }
 
 /**
- * Generates an array of base64 encoded files attached to a node
+ * Generates an array of base64 encoded files attached to a node.
  *
  * @param $nid
  *   Number. Node ID
  * @return
  *   Array. A list of all files from the given node
  */
-function _file_resource_load_node_files($nid, $file_include_contents = TRUE) {
+function _file_resource_load_node_files($nid, $file_include_contents) {
   $node = node_load($nid);
-  if (isset($node->files)) {
-    $files = array();
-    foreach ($node->files as $file) {
-      // Rebuild the files array so it only contains files we know we're allowed to list
-      if ($file->list) {
-        $files[] = $file;
-      }
+  if (!isset($node->files)) {
+    return services_error(t('There are no files on given node.'));
+  }
+
+  $return = array();
+  foreach ($node->files as $file) {
+    // Do not return files that are not listed.
+    if (!$file->list) {
+      continue;
     }
-    if (count($files) > 0) {
-      $send = array();
-      foreach ($files as $file) {
-        $file = array_shift($files);
-        if ($file_include_contents) {
-          $filepath = file_create_path($file->filepath);
-          $binaryfile = fopen($filepath, 'rb');
-          $file = base64_encode(fread($binaryfile, filesize($filepath)));
-          fclose($binaryfile);
-        }
-        $send[$file->fid] = array(
-          'file'      => $file,
-          'filename'  => $file->filename,
-          'uid'       => $file->uid,
-          'filemime'  => $file->filemime,
-          'filesize'  => $file->filesize,
-          'status'    => $file->status,
-          'timestamp' => $file->timestamp
-        );
-      }
+    $return[$file->fid] = array(
+      'filename'  => $file->filename,
+      'uid'       => $file->uid,
+      'filemime'  => $file->filemime,
+      'filesize'  => $file->filesize,
+      'status'    => $file->status,
+      'timestamp' => $file->timestamp
+    );
+    // If to add content of the file.
+    if ($file_include_contents) {
+      $filepath = file_create_path($file->filepath);
+      $binaryfile = fopen($filepath, 'rb');
+      $return[$file->fid]['file'] = base64_encode(fread($binaryfile, filesize($filepath)));
+      fclose($binaryfile);
     }
-    return $send;
-  }
-  else {
-    return services_error(t('There are no files on given node.'));
+
   }
+  return $return;
 }
 
 /**
@@ -270,8 +262,11 @@ function _file_resource_load_node_files($nid, $file_include_contents = TRUE) {
  *   Whether or not the delete was successful.
  */
 function _file_resource_delete($fid) {
+  // Fetch details of the file.
   $file = db_fetch_array(db_query('SELECT * FROM {files} WHERE fid = %d', $fid));
-
+  // Remove data from {files} table.
+  db_query('DELETE FROM {files} WHERE fid = %d', $fid);
+  // Remove file physically.
   return file_delete($file['filepath']);
 }
 
@@ -284,7 +279,7 @@ function _file_resource_access($op = 'view', $args = array()) {
     return TRUE;
   }
 
-  if ($op=='create') {
+  if ($op == 'create') {
     $file = (object)$args[0];
   }
   else {
@@ -317,4 +312,3 @@ function _file_resource_node_access($op = 'view', $args = array()) {
   }
   return FALSE;
 }
-
diff --git a/tests/functional/ServicesResourceFileTests.test b/tests/functional/ServicesResourceFileTests.test
new file mode 100644
index 0000000..5275554
--- /dev/null
+++ b/tests/functional/ServicesResourceFileTests.test
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * @file
+ * Test for file resource.
+ *
+ */
+
+require_once('ServicesWebTestCase.php');
+
+/**
+ * Test class.
+ */
+class ServicesResourceFileTests extends ServicesWebTestCase {
+  // Class variables
+  protected $privileged_user = NULL ;
+  // Endpoint details.
+  protected $endpoint = NULL;
+  // Test file.
+  protected $testfiles;
+
+  /**
+   * Implements setUp().
+   */
+  public function setUp() {
+    parent::setUp(
+      'upload',
+      'autoload',
+      'ctools',
+      'services',
+      'rest_server',
+      'services_sessauth',
+      'inputstream'
+    );
+    // Set up endpoint.
+    $this->endpoint =  $this->saveNewEndpoint();
+    // Create and log in our privileged user.
+    $this->privileged_user = $this->drupalCreateUser(array(
+      'get own binary files',
+      'save file information',
+      'administer services',
+      'administer site configuration',
+      'upload files',
+    ));
+    $this->drupalLogin($this->privileged_user);
+    // Get a test file.
+    $this->testfiles = $this->drupalGetTestFiles('text');
+    
+    // Create directory structure in sandbox. This should be done because 
+    // file_check_directory() will not do that for us as it should. 
+    // @see http://drupal.org/node/180970
+    $directory = file_directory_path() . '/sites/default/files/simpletest';
+    @mkdir($directory, 0775, TRUE);
+  }
+
+  /**
+   * Implements getInfo().
+   */
+  public static function getInfo() {
+    return array(
+      'name' => t('Resource File'),
+      'description' => t('Test the resource File methods.'),
+      'group' => t('Services'),
+    );
+  }
+
+  /**
+   * Test create method.
+   */
+  public function testResourceFileCreate() {
+    // Create file argument with data.
+    $file = array(
+      'filesize' => filesize($this->testfiles[0]->filename),
+      'filename' => $this->testfiles[0]->filename,
+      'file' => base64_encode(file_get_contents($this->testfiles[0]->filename)),
+      'uid' => $this->privileged_user->uid,
+    );
+
+    // Create file with call.
+    $result = $this->servicesPost($this->endpoint->path . '/file', array('file' => $file));
+    $this->assertEqual($result['code'], 200, t('File created.'), 'FileResource: Create');
+
+    // Load file and assert that it exists.
+    //$file_load = file_load($result['body']['fid']);
+    $dbresult = db_query('SELECT * FROM {files} WHERE fid = %d', $result['body']['fid']);
+    $file_load = db_fetch_object($dbresult);
+    $this->assertTrue(is_file($file_load->filepath), t('New file saved to disk.'), 'FileResource: Create');
+    
+    // Lets create another file with the same details 
+    // and ensure name and path are changed automatically.
+    $result = $this->servicesPost($this->endpoint->path . '/file', array('file' => $file));
+    $dbresult = db_query('SELECT * FROM {files} WHERE fid = %d', $result['body']['fid']);
+    $file1 = db_fetch_object($dbresult);
+    $file2 = db_fetch_object($dbresult);
+    $this->assertTrue($file1->filename != $file2->filename && $file1->filepath != $file2->filepath, 
+      t('Two identical files created end up with different names.'), 'FileResource: Create');
+  }
+
+  /**
+   * Test retrieve method.
+   */
+  public function testResourceFileRetrieve() {
+    // Emulate uploaded file.
+    $testfile = $this->uploadTestFile();
+    
+    // Retrieve file.
+    $result = $this->servicesGet($this->endpoint->path . '/file/' . $testfile['fid']);
+    $file = $result['body'];
+
+    // Assert that fid, filesize and uri are the same.
+    $this->assertTrue($file['fid'] == $testfile['fid']
+                   && $file['filesize'] == $testfile['filesize'],
+      t('File retrieved.'), 'FileResource: Retrieve');
+  }
+
+  /**
+   * Test delete method.
+   */
+  public function testResourceFileDelete() {
+    // Emulate uploaded file.
+    $testfile = $this->uploadTestFile();
+
+    // Delete file via call.
+    $result = $this->servicesDelete($this->endpoint->path . '/file/' . $testfile['fid']);
+
+    // Check if record in files table still exists.
+    $fid = db_result(db_query('SELECT fid FROM {files} WHERE fid = %d', $testfile['fid']));
+    $this->assertTrue(empty($fid), t('File deleted.'), 'FileResource: Delete');
+  }
+  
+  /**
+   * Test nodeFiles action.
+   */
+  public function testNodeFiles() {
+    $testfile1 = $this->uploadTestFile();
+    $testfile2 = $this->uploadTestFile();
+    $testfile3 = $this->uploadTestFile();
+
+    // Needed for upload module to create new records in {upload} table.  
+    // @see upload_save()
+    $testfile1['new'] = TRUE;
+    $testfile2['new'] = TRUE;
+    $testfile3['new'] = TRUE;
+    // First two files are listed. Third is not listed.
+    $testfile1['list'] = 1;
+    $testfile2['list'] = 1;
+    $testfile3['list'] = 0;
+    
+    // Create node with three files.
+    $settings = array('files' => array(
+      $testfile1['fid'] => (object)$testfile1, 
+      $testfile2['fid'] => (object)$testfile2, 
+      $testfile3['fid'] => (object)$testfile3, 
+    ));
+    $node = $this->drupalCreateNode($settings);
+
+    $result = $this->servicesPost($this->endpoint->path . '/file/nodeFiles', array('nid' => $node->nid));
+    $this->assertTrue(isset($result['body'][$testfile1['fid']]) && isset($result['body'][$testfile2['fid']]) && !isset($result['body'][$testfile3['fid']]), 
+      t('Attached files listed.'), 'FileResource: nodeFiles');
+  }
+
+  /**
+   * Emulate uploaded file.
+   * 
+   * Copy file from simpletest file samples and create record in files table.
+   * 
+   * @return array
+   *  File data. 
+   */
+  public function uploadTestFile($file = NULL) {
+    if (empty($file)) {
+      $file = next($this->testfiles);
+    }
+    $testfile =  array(
+      'fid' => NULL,
+      'uid' => $this->privileged_user->uid,
+      'filename' => trim(basename($file->filename), '.'),
+      'filepath' => $file->filename,
+      'filemime' => file_get_mimetype($file->filename),
+      'filesize' => filesize($file->filename),
+      'status' => FILE_STATUS_PERMANENT,
+      'timestamp' => time(),
+    );
+    $source = $testfile['filepath'];
+    $destination = file_directory_path() . '/' . $testfile['filepath'];
+    $dirname = dirname($destination);
+    file_check_directory($dirname, FILE_CREATE_DIRECTORY);
+    file_copy($source, $destination);
+
+    drupal_write_record('files', $testfile);
+    
+    return $testfile;
+  }
+}
-- 
1.7.0.4

