lostvita/blog

File chooser dialog can only be shown with a user activation

lostvita opened this issue · 2 comments

日前,做需求过程碰到了这么一个问题:File chooser dialog can only be shown with a user activation,从描述看是在没有用户激活页面的情况下尝试去调起文件选择器,被浏览器拒绝了。
image

需求背景:页面加载时,通过代码触发file inputclick事件,达到调起文件选择器的目的。大致如下:

const autoClick = () => {
    const input = document.createElement('input')
    input.type = 'file'
    input.accept = '.gif,.jpg,.jpeg,.png,.bmp'
    input.onchange = (e) => {
        console.log('>>', e.target.files)
    }
    input.click()
}

setTimeout(autoClick, 0)

image

猜想这是浏览器考虑安全因素做的限制,确实也是。试想一下用户打开一个未知网页,不断调起文件选择器(或者新建窗口操作),用户的设备将直接被卡死。

但还是想探究验证一下浏览器内核是如何做限制的。chromium源码位置

void FileInputType::HandleDOMActivateEvent(Event& event) {
  // ...

  if (!LocalFrame::HasTransientUserActivation(document.GetFrame())) {
    String message =
        "File chooser dialog can only be shown with a user activation.";
    document.AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
        mojom::ConsoleMessageSource::kJavaScript,
        mojom::ConsoleMessageLevel::kWarning, message));
    return;
  }
 // ...
}

可以看出,浏览器在响应dom事件时会先判断是否有短暂的用户激活行为。但,短暂是多久?

bool HasTransientUserActivation() const {
    return user_activation_state_.IsActive();
}

浏览器用user_activation_state_维护了用户激活的状态,其中IsActive最终是通过IsActiveInternal实现的,里面维护了一个transient_state_expiry_time_变量,这就是浏览器用来标识短暂的用户激活的变量。

bool UserActivationState::IsActiveInternal() const {
  return base::TimeTicks::Now() <= transient_state_expiry_time_;
}

void UserActivationState::ActivateTransientState() {
  transient_state_expiry_time_ = base::TimeTicks::Now() + kActivationLifespan;
}

constexpr base::TimeDelta kActivationLifespan = base::Seconds(5);

浏览器每收到用户的交互行为时(单纯地移动鼠标可不算,浏览器应该维护了一套属于用户激活行为的事件白名单)ActivateTransientState,这里会更新激活态的过期时间,在kActivationLifespan时间后就会过期,也就是5s

搞清楚了原理,我们用demo来验证一下!

const autoClick = () => {
    console.log('>> autoClick')
    const input = document.createElement('input')
    input.type = 'file'
    input.accept = '.gif,.jpg,.jpeg,.png,.bmp'
    input.onchange = (e) => {
        console.log('>>', e.target)
    }
    input.click()
}

// 脚本加载时,3s后自动触发`input click`
setTimeout(autoClick, 3000)
  1. 纯加载网页,无用户激活行为,期望不调起:
    file-choose-1

  2. 加载后,用户在3s内点击网页,期望调起:
    file-choose-2

  3. 加载并且用户点击网页,5s后执行autoClick,期望不调起:
    file-choose-3

总结一下:在用户激活页面(点击,选中等)的5s内,自动执行的脚本有响应,否则浏览器无响应。

Is there any way to bypass user activation verification?

Is there any way to bypass user activation verification?