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;
|
_wsActiveDragPath=item.path;
|
||||||
_wsActiveDragType=item.type;
|
_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){
|
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('Files')) return false;
|
||||||
if(e.dataTransfer&&e.dataTransfer.types&&e.dataTransfer.types.includes('application/ws-path')) return true;
|
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'));
|
return !!(_wsActiveDragPath&&e.dataTransfer&&e.dataTransfer.types&&e.dataTransfer.types.includes('text/plain'));
|
||||||
}
|
}
|
||||||
function _wsDragSrcPath(e){
|
function _wsDragSrcPath(e){
|
||||||
const custom=e.dataTransfer.getData('application/ws-path');
|
const custom=e.dataTransfer.getData('application/ws-path');
|
||||||
if(custom) return custom;
|
if(custom) return custom;
|
||||||
if(_wsActiveDragPath) return _wsActiveDragPath;
|
// Stripped-MIME fallback: only trust the active flag when the drop's own
|
||||||
return e.dataTransfer.getData('text/plain')||'';
|
// 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){
|
function _wsDragSrcType(e){
|
||||||
const custom=e.dataTransfer.getData('application/ws-type');
|
const custom=e.dataTransfer.getData('application/ws-type');
|
||||||
@@ -14872,10 +14895,14 @@ function _bindWorkspaceMoveDropTarget(el,destDir){
|
|||||||
if(!_isWorkspaceTreeMoveDrag(e))return;
|
if(!_isWorkspaceTreeMoveDrag(e))return;
|
||||||
e.preventDefault();e.stopPropagation();
|
e.preventDefault();e.stopPropagation();
|
||||||
el.classList.remove('drag-over');
|
el.classList.remove('drag-over');
|
||||||
const srcPath=_wsDragSrcPath(e);
|
try{
|
||||||
if(!srcPath)return;
|
const srcPath=_wsDragSrcPath(e);
|
||||||
const srcType=_wsDragSrcType(e);
|
if(!srcPath)return;
|
||||||
await _performWorkspaceMove(srcPath,destDir,srcType==='dir');
|
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);
|
e.preventDefault();e.stopPropagation();_showFileContextMenu(e,item);
|
||||||
};
|
};
|
||||||
el.ondragstart=(e)=>{_setWsDragData(e,item);e.dataTransfer.effectAllowed='copy';el.classList.add('dragging');};
|
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 isLk = item.type === 'symlink';
|
||||||
const isExternalLink = isLk && item.target_outside_workspace;
|
const isExternalLink = isLk && item.target_outside_workspace;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ def _extract_drag_functions():
|
|||||||
extract_function(name)
|
extract_function(name)
|
||||||
for name in (
|
for name in (
|
||||||
'_setWsDragData',
|
'_setWsDragData',
|
||||||
|
'_clearWsDragData',
|
||||||
'_isWorkspaceTreeMoveDrag',
|
'_isWorkspaceTreeMoveDrag',
|
||||||
'_wsDragSrcPath',
|
'_wsDragSrcPath',
|
||||||
'_wsDragSrcType',
|
'_wsDragSrcType',
|
||||||
@@ -131,6 +132,42 @@ process.stdout.write(String(_isWorkspaceTreeMoveDrag({dataTransfer: dropDt})));
|
|||||||
assert _run_node(js) == 'false', \
|
assert _run_node(js) == 'false', \
|
||||||
'foreign text drag must be rejected when no active-drag flag is set'
|
'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):
|
def test_files_drag_rejected_even_with_active_flag(self):
|
||||||
"""Files drop must be rejected regardless of active-drag state."""
|
"""Files drop must be rejected regardless of active-drag state."""
|
||||||
js = """\
|
js = """\
|
||||||
|
|||||||
Reference in new issue
Block a user