harden(#4757): gate stripped-MIME drop on text/plain===tracked path + robust drag-flag cleanup (gate finding)
Codex gate found a SILENT hazard: if a workspace drag is abandoned without ondragend firing (cancel/drop-outside/blur), _wsActiveDragPath lingered and a later FOREIGN text/plain drag could be misread as a workspace move. Hardening: - _wsDragSrcPath now only trusts the active flag when the drop's own text/plain EQUALS _wsActiveDragPath (a foreign/empty text drag resolves to '' -> no move). - Shared _clearWsDragData() called from ondragend, a drop finally, and window-level dragend/drop/pagehide/blur fallbacks (lost-dragend cleanup). - 2 regression tests (stale-flag foreign-text -> empty; _clearWsDragData resets), proven non-vacuous. Co-authored-by: Rod Boev <rod.boev@gmail.com>
This commit is contained in:
1 parent
30c9f5e52a
commit
63e3f91654
2 files changed
+71
-7
No files matched your search
+34
-7
@@ -14772,16 +14772,39 @@ function _setWsDragData(e,item){
|
||||
_wsActiveDragPath=item.path;
|
||||
_wsActiveDragType=item.type;
|
||||
}
|
||||
function _clearWsDragData(){
|
||||
_wsActiveDragPath=null;
|
||||
_wsActiveDragType=null;
|
||||
}
|
||||
// Window-level fallback cleanup: if a workspace drag is abandoned without the
|
||||
// row's ondragend firing (drag cancelled, dropped outside any target, tab
|
||||
// blurred/hidden mid-drag), the active-drag flag must not survive — otherwise a
|
||||
// later FOREIGN text/plain drag could be misread as a workspace move.
|
||||
if(typeof window!=='undefined'&&!window._wsDragCleanupBound){
|
||||
window._wsDragCleanupBound=true;
|
||||
window.addEventListener('dragend',_clearWsDragData,true);
|
||||
window.addEventListener('drop',_clearWsDragData,true);
|
||||
window.addEventListener('pagehide',_clearWsDragData);
|
||||
window.addEventListener('blur',_clearWsDragData);
|
||||
}
|
||||
function _isWorkspaceTreeMoveDrag(e){
|
||||
if(e.dataTransfer&&e.dataTransfer.types&&e.dataTransfer.types.includes('Files')) return false;
|
||||
if(e.dataTransfer&&e.dataTransfer.types&&e.dataTransfer.types.includes('application/ws-path')) return true;
|
||||
// Stripped-MIME (macOS WebKit) fallback: accept text/plain ONLY while a
|
||||
// workspace drag is genuinely in flight. dragover/drop events can't read the
|
||||
// payload, so gate on the active flag alone here; the drop handler additionally
|
||||
// proves text/plain === _wsActiveDragPath before performing the move.
|
||||
return !!(_wsActiveDragPath&&e.dataTransfer&&e.dataTransfer.types&&e.dataTransfer.types.includes('text/plain'));
|
||||
}
|
||||
function _wsDragSrcPath(e){
|
||||
const custom=e.dataTransfer.getData('application/ws-path');
|
||||
if(custom) return custom;
|
||||
if(_wsActiveDragPath) return _wsActiveDragPath;
|
||||
return e.dataTransfer.getData('text/plain')||'';
|
||||
// Stripped-MIME fallback: only trust the active flag when the drop's own
|
||||
// text/plain matches it. A foreign text/plain drag (different/empty content)
|
||||
// must NOT resolve to our tracked workspace path even if the flag lingered.
|
||||
const plain=e.dataTransfer.getData('text/plain')||'';
|
||||
if(_wsActiveDragPath&&plain===_wsActiveDragPath) return _wsActiveDragPath;
|
||||
return '';
|
||||
}
|
||||
function _wsDragSrcType(e){
|
||||
const custom=e.dataTransfer.getData('application/ws-type');
|
||||
@@ -14872,10 +14895,14 @@ function _bindWorkspaceMoveDropTarget(el,destDir){
|
||||
if(!_isWorkspaceTreeMoveDrag(e))return;
|
||||
e.preventDefault();e.stopPropagation();
|
||||
el.classList.remove('drag-over');
|
||||
const srcPath=_wsDragSrcPath(e);
|
||||
if(!srcPath)return;
|
||||
const srcType=_wsDragSrcType(e);
|
||||
await _performWorkspaceMove(srcPath,destDir,srcType==='dir');
|
||||
try{
|
||||
const srcPath=_wsDragSrcPath(e);
|
||||
if(!srcPath)return;
|
||||
const srcType=_wsDragSrcType(e);
|
||||
await _performWorkspaceMove(srcPath,destDir,srcType==='dir');
|
||||
}finally{
|
||||
_clearWsDragData();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14898,7 +14925,7 @@ function _renderTreeItems(container, entries, depth){
|
||||
e.preventDefault();e.stopPropagation();_showFileContextMenu(e,item);
|
||||
};
|
||||
el.ondragstart=(e)=>{_setWsDragData(e,item);e.dataTransfer.effectAllowed='copy';el.classList.add('dragging');};
|
||||
el.ondragend=()=>{el.classList.remove('dragging');_clearWorkspaceMoveDragOver();_wsActiveDragPath=null;_wsActiveDragType=null;};
|
||||
el.ondragend=()=>{el.classList.remove('dragging');_clearWorkspaceMoveDragOver();_clearWsDragData();};
|
||||
|
||||
const isLk = item.type === 'symlink';
|
||||
const isExternalLink = isLk && item.target_outside_workspace;
|
||||
|
||||
@@ -51,6 +51,7 @@ def _extract_drag_functions():
|
||||
extract_function(name)
|
||||
for name in (
|
||||
'_setWsDragData',
|
||||
'_clearWsDragData',
|
||||
'_isWorkspaceTreeMoveDrag',
|
||||
'_wsDragSrcPath',
|
||||
'_wsDragSrcType',
|
||||
@@ -131,6 +132,42 @@ process.stdout.write(String(_isWorkspaceTreeMoveDrag({dataTransfer: dropDt})));
|
||||
assert _run_node(js) == 'false', \
|
||||
'foreign text drag must be rejected when no active-drag flag is set'
|
||||
|
||||
def test_stale_flag_foreign_text_resolves_empty(self):
|
||||
"""If the active-drag flag lingers (lost dragend) and a FOREIGN text/plain
|
||||
drag arrives whose content differs from the tracked path, _wsDragSrcPath
|
||||
must return '' (not the stale path) so no spurious workspace move runs.
|
||||
|
||||
Regression guard for the gate-found SILENT hazard: the stripped-MIME
|
||||
fallback now requires text/plain === _wsActiveDragPath."""
|
||||
js = """\
|
||||
const item = {path: 'docs/readme.md', type: 'file'};
|
||||
const startDt = new FakeDataTransfer([], {});
|
||||
startDt.setData = function(mime, val){ this._map[mime]=val; if(!this.types.includes(mime)) this.types.push(mime); };
|
||||
_setWsDragData({dataTransfer: startDt}, item);
|
||||
// Flag still set (dragend never fired). A foreign text drag arrives with
|
||||
// DIFFERENT content and no custom MIME.
|
||||
const dropDt = new FakeDataTransfer(['text/plain'], {'text/plain': 'some pasted text'});
|
||||
dropDt.getData = function(mime){ if(mime==='application/ws-path') return ''; return this._map[mime]||''; };
|
||||
process.stdout.write('[' + _wsDragSrcPath({dataTransfer: dropDt}) + ']');
|
||||
"""
|
||||
assert _run_node(js) == '[]', \
|
||||
'stale flag + foreign text (content != tracked path) must resolve to empty'
|
||||
|
||||
def test_clear_ws_drag_data_resets_flags(self):
|
||||
"""_clearWsDragData nulls both flags so a later text/plain-only drag is rejected."""
|
||||
js = """\
|
||||
const item = {path: 'tmp.txt', type: 'file'};
|
||||
const startDt = new FakeDataTransfer([], {});
|
||||
startDt.setData = function(mime, val){ this._map[mime]=val; if(!this.types.includes(mime)) this.types.push(mime); };
|
||||
_setWsDragData({dataTransfer: startDt}, item);
|
||||
_clearWsDragData();
|
||||
const dropDt = new FakeDataTransfer(['text/plain'], {'text/plain': item.path});
|
||||
process.stdout.write(String(_isWorkspaceTreeMoveDrag({dataTransfer: dropDt})));
|
||||
"""
|
||||
assert _run_node(js) == 'false', \
|
||||
'_clearWsDragData must reset flags so a stripped-MIME drag is no longer accepted'
|
||||
|
||||
|
||||
def test_files_drag_rejected_even_with_active_flag(self):
|
||||
"""Files drop must be rejected regardless of active-drag state."""
|
||||
js = """\
|
||||
|
||||
Reference in new issue
Block a user