The short version: _upload_form() should be using a form #access property to set its requirement for upload files permission, and not hard-code it in the module logic. Because it does not do this, the only way to extend this form from a contributed module is to hack core.

The long version: I was asked by a client how to extend comment_upload.module so that it provided a permission "upload files to comments" that could be given out to certain low-level users, who don't have "upload files" permissions, since that's reserved for a more powerful role. Sure, you're thinking, throw in a hook_perm(), and modify the permission check in comment_upload.module. Bing, bang, done, right? WRONG. ;)

Why? Because comment_upload.module is calling _upload_form() to place the upload form in the comment. This is because it would be silly to duplicate all of that function's code in comment_upload.module, both because it's annoying, and also because if there were ever important security fixes to this part of code, comment_upload.module's version might fall out of date. Not good.

Unfortunately, _upload_form() has this dandy piece of code:

  if (user_access('upload files')) {
    // Make the form here.
  }

which results in absolutely nada being returned back to comment_upload.module's form.

If, however, we did $form['#access'] = user_access('upload files'), /then/ comment_upload.module could use hook_form_alter() to have it check "upload files to comments" instead of the more generic "upload files."

Whew!

Anyway, patch forthcoming.

Comments

webchick’s picture

Patch for 6.x and 7.x.

webchick’s picture

StatusFileSize
new1.78 KB

And here's one for 5.x.

webchick’s picture

Status: Active » Needs review

Oopsie.

keith.smith’s picture

Status: Needs review » Needs work

I know your just moving this line around, but what's with:

 ($limits['resolution'] ? t('Images are larger than %resolution will be resized. ', array('%resolution' => $limits['resolution'])) : '') . t('The maximum upload size is %filesize. Only files with the following extensions may be uploaded: %extensions. ', array('%extensions' => $limits['extensions'], '%filesize' => format_size($limits['file_size']))),

Doesn't "Images are larger than %resolution will be resized." have an extra "are" in it or something?

vladimir.dolgopolov’s picture

StatusFileSize
new1.45 KB

Here is the test for Simpletest module for the issue.
My first attempt was create a test module for this which called _upload_form() and put its own perm for uploads.
But it's better to isolate a test as far as possible.
There are 2 checks: for 'upload files' permission and for 'upload files to comments' (as an example).

gábor hojtsy’s picture

@webchick: patch looks good on short review.
@keith.smith: yeah, you are right
@vladimir: you'd sill need a custom form alter hook to alter the form and its #access check to the permission you'd like to check

vladimir.dolgopolov’s picture

@Gábor Hojtsy: It seems I have to generate a module in the test. Simpletest module can't do that itself, but I can.

webchick’s picture

Status: Needs work » Needs review
StatusFileSize
new3.55 KB

Here's a patch for 6.x/7.x that fixes the string bug found by Keith Smith. Thanks, and thanks a LOT for the test-writing, Vladimir!! :) You rock!

5.x doesn't have the sloppy string so old patch still applies.

vladimir.dolgopolov’s picture

Here is improved test for the issue.

The main idea: create simple module with hook_perm() and hook_form_alter() and alter a form according permissions.
After drupal_prepare_form() check for permissions the form has.

Test is too big because of a module creation stuff here.


class TestCase226853 extends DrupalTestCase {
  function get_info() {
    return array(
      'name' => t('[226853] upload.module hard-codes \'upload files\' permission check'),
      'desc' => t('The short version: _upload_form() should be using a form #access property to set its requirement for upload files permission, and not hard-code it in the module logic. Because it does not do this, the only way to extend this form from a contributed module is to hack core.'),
      'group' => t('Drupal 7 Tests'),
    );
  }
  
  function testIssue() {

    $this->drupalModuleEnable('upload');

    // A module makes new own permission and implements hook_form_alter()
    $module_name = $this->randomName();
    $module_text = <<< EOF
<?php
function {$module_name}_perm() {
  return array(
    'upload files to {$module_name}' => t('Attach files to content to something {$module_name} related.'),
  );
}

function {$module_name}_form_alter(&\$form, \$form_state, \$form_id) {
  if (\$form_id == '{$module_name}_form') {
    \$obj->files = array();
    \$form['attachments'] = array(
      '#type' => 'fieldset',
      '#title' => t('File attachments'),
      '#access' => user_access('upload files to {$module_name}') || user_access('upload files'),
      '#collapsible' => TRUE,
      '#collapsed' => empty(\$obj->files),
      '#description' => t('Changes made to the attachments are not permanent until you save this post. The first "listed" file will be included in RSS feeds.'),
      '#prefix' => '<div class="attachments">',
      '#suffix' => '</div>',
      '#weight' => 10,
    );
    // Wrapper for fieldset contents (used by ahah.js).
    \$form['attachments']['wrapper'] = array(
      '#prefix' => '<div id="attach-wrapper">',
      '#suffix' => '</div>',
    );
    \$form['attachments']['wrapper'] += _upload_form(\$obj);
    if (isset(\$form['attachments']['wrapper']['#access'])) {
      \$form['attachments']['wrapper']['#access'] |= user_access('upload files to {$module_name}');
    }
    \$form['#attributes']['enctype'] = 'multipart/form-data';
  }
}

EOF;

    $this->createModule($module_name, $module_text);

    // check for a user has only 'upload files' permission
    $this->checkPowerfulRole($module_name);

    // check for a user has only module's permission
    $this->checkModuleRole($module_name);

    // check for a user has other permission
    $this->checkRandomRole($module_name);
    
    $this->deleteModule($module_name);
  }

  function createModule($module_name, $module_text, $module_core = '7.x') {
    $module_dir = "sites/all/modules/$module_name";
    $module_file = "$module_dir/$module_name.module";
    $module_file_info = "$module_dir/$module_name.info";
    if (file_check_directory($module_dir, FILE_CREATE_DIRECTORY)) {
      $info = <<< EOF
name = "$module_name module"
description = "$module_name module for test. Delete it when you see it."
package = Temporary
core = $module_core

EOF;
      if ($fp = @fopen($module_file_info, 'w')) {
        fwrite($fp, $info);
        fclose($fp);
      }
      if ($fp = @fopen($module_file, 'w')) {
        fwrite($fp, $module_text);
        fclose($fp);
      }
      $this->drupalModuleEnable($module_name);
    }
  }

  function deleteModule($module_name) {
    $module_dir = "sites/all/modules/$module_name";
    $module_file = "$module_dir/$module_name.module";
    $module_file_info = "$module_dir/$module_name.info";
    @unlink($module_file);
    @unlink($module_file_info);
    @rmdir($module_dir);
  }

  // Check 'upload files' permission.
  // Should pass so 'upload files' is very general
  function checkPowerfulRole($module_name) {
    // function _upload_form() uses a global $user
    global $user; 
    $orig_user = $user;
    $form = array();
    // create a user for form's preparing
    $user = $this->drupalCreateUserRolePerm(array("upload files"));
    $this->drupalLoginUser($user);
    // prepare the form (executing any hook_form_alter)
    drupal_prepare_form("{$module_name}_form", $form, $form_state);
    $this->assertTrue($this->checkAccess($form), "check generic 'upload files'");
    $this->drupalGet('logout');
    $user = $orig_user; // restore the original user
  }

  // Check permission for upload by module's permission.
  // Should pass so module's permission checked in hook_form_alter()
  // It checks the issue: the user don't have 'upload files' permission,
  // but he have module's permission though. 
  function checkModuleRole($module_name) {
    global $user;
    $orig_user = $user;
    $form = array();
    $user = $this->drupalCreateUserRolePerm(array("upload files to {$module_name}"));
    $this->drupalLoginUser($user);
    drupal_prepare_form("{$module_name}_form", $form, $form_state);
    $this->assertTrue($this->checkAccess($form), "check module's 'upload files to {$module_name}'");
    $this->drupalGet('logout');
    $user = $orig_user;
  }

  // Check random permission
  // Should pass (assertFalse) so random permission does not allow file upload
  function checkRandomRole($module_name) {
    global $user;
    $orig_user = $user;
    $form = array();
    $user = $this->drupalCreateUserRolePerm(array($this->randomName()));
    $this->drupalLoginUser($user);
    drupal_prepare_form("{$module_name}_form", $form, $form_state);
    $this->assertFalse($this->checkAccess($form), "Check other roles");
    $this->drupalGet('logout');
    $user = $orig_user;
  }

  function checkAccess($form) {
    return !empty($form['attachments']['#access']) &&  // access to attachments
           isset($form['attachments']['wrapper']['new']); // is 'new' form exists?
           !empty($form['attachments']['wrapper']['#access']); // access to wrapper
  }

}
drewish’s picture

wanted to point to a similar issue #247095: Upload.module hard-codes 'view uploaded files' permission check, i'll try to test this out this afternoon.

drewish’s picture

Status: Needs review » Needs work
StatusFileSize
new8.95 KB

here's a re-roll with vladimir.dolgopolov's test included. i will say that this test freaks me the flip out. we now got the ability to have hidden modules so we don't need to be creating them dynamically but i'm a bit unsure where the module should live.

webchick’s picture

Version: 7.x-dev » 6.x-dev

I think I would rather fix this in 7.x with #563000: Provide upgrade path to file. So, assuming that patch makes it in, this is a 6.x-only bug.

Status: Needs work » Closed (outdated)

Automatically closed because Drupal 6 is no longer supported. If the issue verifiably applies to later versions, please reopen with details and update the version.