// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include #include #include #include #include #include #include static bool executeWebOpenCommand(const char* input, std::string& output) { PyObject* scope = PyDict_New(); PyObject* request = PyUnicode_FromString(input); if (!scope || !request || PyDict_SetItemString(scope, "__builtins__", PyEval_GetBuiltins()) < 0 || PyDict_SetItemString(scope, "request_text", request) < 0) { Py_XDECREF(request); Py_XDECREF(scope); return false; } Py_DECREF(request); PyObject* executed = PyRun_String(R"PY( import FreeCAD as App, json, os, base64, hashlib, importlib record = json.loads(request_text)['records'][0] args = record.get('args', {}) reply = {'apiId': record['apiId'], 'name': 'Std_Open'} try: if args.get('activeTask'): raise ValueError('Finish the active task before opening files') # Reuse the Part registrations from Mod/Part/Init.py. STEP with colors # belongs to Import, not a geometry-only substitute in another module. import Part try: import IfcOpenShellNative if not App.getImportType('ifc'): App.addImportType('Industry Foundation Classes (*.ifc *.IFC)', 'IfcOpenShellNative') except ImportError: pass try: import Mesh if not App.getImportType('obj'): App.addImportType('Wavefront OBJ (*.obj *.OBJ)', 'Mesh') except ImportError: pass for extension, description in (('brep', 'BREP (*.brep *.BREP *.brp *.BRP)'), ('iges', 'IGES (*.iges *.IGES *.igs *.IGS)')): if not App.getImportType(extension): App.addImportType(description, 'Part') formats = [{'extension': 'FCStd', 'modules': ['FreeCAD']}] for extension, modules in App.getImportType().items(): if extension.lower() == 'fcstd': continue modules = list(dict.fromkeys([modules] if isinstance(modules, str) else list(modules or []))) if modules: formats.append({'extension': extension, 'modules': modules}) operation = args.get('operation', 'open') if operation not in ('list', 'open'): raise ValueError('Open operation must be list or open') files = args.get('files', []) if not isinstance(files, list) or operation == 'open' and not files: raise ValueError('Select one or more files') opened, errors = [], [] if operation == 'open': for entry in files: before = set(App.listDocuments()) previous = App.activeDocument() previous_name = previous.Name if previous else '' filename = entry.get('filename', '') if isinstance(entry, dict) else '' try: if not isinstance(filename, str) or not filename or filename in ('.', '..') or '/' in filename or chr(92) in filename: raise ValueError('Filename must not contain directory components') data = base64.b64decode(entry['fileData'], validate=True) if not data: raise ValueError('The selected file is empty') digest = hashlib.sha256(data).hexdigest() directory = os.path.join(App.getUserAppDataDir(), 'WebOpenFiles', digest) path = os.path.join(directory, filename) existing = next((doc for doc in App.listDocuments().values() if doc.FileName == path), None) if existing: App.setActiveDocument(existing.Name) documents = [existing] else: os.makedirs(directory, exist_ok=True) with open(path, 'wb') as stream: stream.write(data) extension = os.path.splitext(filename)[1][1:] if extension.lower() == 'fcstd': document = App.openDocument(path) if document is None: raise RuntimeError('Native document restore returned no document') App.setActiveDocument(document.Name) else: modules = App.getImportType(extension) or App.getImportType(extension.lower()) modules = list(dict.fromkeys([modules] if isinstance(modules, str) else list(modules or []))) module_name = entry.get('module') if not modules: raise ValueError('No native import handler is registered for .' + extension) if not module_name: if len(modules) != 1: raise ValueError('Choose an import handler: ' + ', '.join(modules)) module_name = modules[0] if module_name not in modules: raise ValueError('Import handler is not registered for this extension') module = importlib.import_module(module_name) if getattr(module, '_freecad_import_stub', False) or not callable(getattr(module, 'open', None)): raise RuntimeError('Native import handler is not available in this runtime: ' + module_name) module.open(path) documents = [doc for name, doc in App.listDocuments().items() if name not in before] if not documents: raise RuntimeError('Import handler did not open a native document') opened.append({'filename': filename, 'sha256': digest, 'reused': existing is not None, 'documents': [{'name': doc.Name, 'label': doc.Label, 'filename': doc.FileName, 'objectCount': len(doc.Objects)} for doc in documents]}) except Exception as error: # Roll back only documents created by this failed open, never pre-existing work. for name in set(App.listDocuments()) - before: App.closeDocument(name) if previous_name in App.listDocuments(): App.setActiveDocument(previous_name) errors.append({'filename': str(filename), 'message': str(error)}) active = App.activeDocument() value = {'command': 'Std_Open', 'documentId': args.get('documentId', ''), 'formats': formats, 'opened': opened, 'errors': errors, 'activeDocument': active.Name if active else None, 'operation': operation} reply.update(status='success', native=True, result=value) except Exception as error: reply.update(status='error', code='open_command_failed', message=str(error)) response_text = json.dumps({'ok': reply['status'] == 'success', 'records': [reply]}) )PY", Py_file_input, scope, scope); if (!executed) { Py_DECREF(scope); return false; } Py_DECREF(executed); PyObject* response = PyDict_GetItemString(scope, "response_text"); const char* text = response ? PyUnicode_AsUTF8(response) : nullptr; if (text) output = text; const bool ok = text != nullptr; Py_DECREF(scope); if (ok) { auto envelope = QJsonDocument::fromJson(QByteArray::fromStdString(output)).object(); auto rows = envelope.value("records").toArray(); if (rows.size() == 1 && rows.first().toObject().value("status").toString() == "success") { auto row = rows.first().toObject(); auto value = row.value("result").toObject(); auto files = value.value("opened").toArray(); for (int i = 0; i < files.size(); ++i) { auto file = files[i].toObject(); auto documents = file.value("documents").toArray(); for (int j = 0; j < documents.size(); ++j) { auto item = documents[j].toObject(); auto* doc = App::GetApplication().getDocument(item.value("name").toString().toUtf8().constData()); if (doc) { if (!file.value("reused").toBool()) doc->setUndoMode(1); item.insert("partialRestore", doc->testStatus(App::Document::PartialRestore)); item.insert("restoreError", doc->testStatus(App::Document::RestoreError)); } documents[j] = item; } file.insert("documents", documents); files[i] = file; } value.insert("opened", files); row.insert("result", value); rows[0] = row; envelope.insert("records", rows); output = QJsonDocument(envelope).toJson(QJsonDocument::Compact).toStdString(); } } return ok; }