384 lines · 19.2 KB · C++
RawDownload
////  EntityEditFilters.cpp//  libraries/entities/src////  Created by David Kelly on 2/7/2017.//  Copyright 2017 High Fidelity, Inc.//  Copyright 2023 Overte e.V.////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html//  SPDX-License-Identifier: Apache-2.0// #include "EntityEditFilters.h" #include <QUrl> #include <ResourceManager.h>#include <shared/ScriptInitializerMixin.h>#include <ScriptEngine.h>#include <ScriptManager.h>#include <ScriptProgram.h> QList<EntityItemID> EntityEditFilters::getZonesByPosition(glm::vec3& position) {    QList<EntityItemID> zones;    QList<EntityItemID> missingZones;    _lock.lockForRead();    auto zoneIDs = _filterDataMap.keys();    _lock.unlock();    for (auto id : zoneIDs) {        if (!id.isInvalidID()) {            // for now, look it up in the tree (soon we need to cache or similar?)            EntityItemPointer itemPtr = _tree->findEntityByEntityItemID(id);            auto zone = std::dynamic_pointer_cast<ZoneEntityItem>(itemPtr);            if (!zone) {                // TODO: maybe remove later?                removeFilter(id);            } else if (zone->contains(position)) {                zones.append(id);            }        } else {            // the null id is the global filter we put in the domain server's            // advanced entity server settings            zones.append(id);        }    }    return zones;} bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut,        bool& wasChanged, EntityTree::FilterType filterType, EntityItemID& itemID, const EntityItemPointer& existingEntity) {     // get the ids of all the zones (plus the global entity edit filter) that the position    // lies within    auto zoneIDs = getZonesByPosition(position);    for (auto id : zoneIDs) {        if (!itemID.isInvalidID() && id == itemID) {            continue;        }         // get the filter pair, etc...        _lock.lockForRead();        FilterData filterData = _filterDataMap.value(id);        _lock.unlock();         if (filterData.valid()) {            if (filterData.rejectAll) {                return false;            }             // check to see if this filter wants to filter this message type            if ((!filterData.wantsToFilterEdit && filterType == EntityTree::FilterType::Edit) ||                (!filterData.wantsToFilterPhysics && filterType == EntityTree::FilterType::Physics) ||                (!filterData.wantsToFilterDelete && filterType == EntityTree::FilterType::Delete) ||                (!filterData.wantsToFilterAdd && filterType == EntityTree::FilterType::Add)) {                 wasChanged = false;                return true; // accept the message            }             auto oldProperties = propertiesIn.getDesiredProperties();            auto specifiedProperties = propertiesIn.getChangedProperties();            propertiesIn.setDesiredProperties(specifiedProperties);            ScriptValue inputValues = propertiesIn.copyToScriptValue(filterData.engine.get(), false, true, true);            propertiesIn.setDesiredProperties(oldProperties);             auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter.             ScriptValueList args;            args << inputValues;            args << filterData.engine->newValue(filterType);             // get the current properties for then entity and include them for the filter call            if (existingEntity && filterData.wantsOriginalProperties) {                auto currentProperties = existingEntity->getProperties(filterData.includedOriginalProperties);                ScriptValue currentValues = currentProperties.copyToScriptValue(filterData.engine.get(), false, true, true);                args << currentValues;            }              // get the zone properties            if (filterData.wantsZoneProperties) {                auto zoneEntity = _tree->findEntityByEntityItemID(id);                if (zoneEntity) {                    auto zoneProperties = zoneEntity->getProperties(filterData.includedZoneProperties);                    ScriptValue zoneValues = zoneProperties.copyToScriptValue(filterData.engine.get(), false, true, true);                     if (filterData.wantsZoneBoundingBox) {                        bool success = true;                        AABox aaBox = zoneEntity->getAABox(success);                        if (success) {                            ScriptValue boundingBox = filterData.engine->newObject();                            ScriptValue bottomRightNear = vec3ToScriptValue(filterData.engine.get(), aaBox.getCorner());                            ScriptValue topFarLeft = vec3ToScriptValue(filterData.engine.get(), aaBox.calcTopFarLeft());                            ScriptValue center = vec3ToScriptValue(filterData.engine.get(), aaBox.calcCenter());                            ScriptValue boundingBoxDimensions = vec3ToScriptValue(filterData.engine.get(), aaBox.getDimensions());                            boundingBox.setProperty("brn", bottomRightNear);                            boundingBox.setProperty("tfl", topFarLeft);                            boundingBox.setProperty("center", center);                            boundingBox.setProperty("dimensions", boundingBoxDimensions);                            zoneValues.setProperty("boundingBox", boundingBox);                        }                    }                     // If this is an add or delete, or original properties weren't requested                    // there won't be original properties in the args, but zone properties need                    // to be the fourth parameter, so we need to pad the args accordingly                    int EXPECTED_ARGS = 3;                    if (args.length() < EXPECTED_ARGS) {                        args << ScriptValue();                    }                    assert(args.length() == EXPECTED_ARGS); // we MUST have 3 args by now!                    args << zoneValues;                }            }             ScriptValue result = filterData.filterFn.call(_nullObjectForFilter, args);             if (filterData.uncaughtExceptions()) {                return false;            }             if (result.isObject()) {                // make propertiesIn reflect the changes, for next filter...                propertiesIn.copyFromScriptValue(result, false);                 // and update propertiesOut too.  TODO: this could be more efficient...                propertiesOut.copyFromScriptValue(result, false);                // Javascript objects are == only if they are the same object. To compare arbitrary values, we need to use JSON.                auto out = QJsonValue::fromVariant(result.toVariant());                wasChanged |= (in != out);            } else if (result.isBool()) {                 // if the filter returned false, then it's authoritative                if (!result.toBool()) {                    return false;                }                 // otherwise, assume it wants to pass all properties                propertiesOut = propertiesIn;                wasChanged = false;             } else {                return false;            }        }    }    // if we made it here,    return true;} void EntityEditFilters::removeFilter(EntityItemID entityID) {    QWriteLocker writeLock(&_lock);    _filterDataMap.remove(entityID);} void EntityEditFilters::addFilter(EntityItemID entityID, QString filterURL) {     QUrl scriptURL(filterURL);     // setting it to an empty string is same as removing    if (filterURL.size() == 0) {        removeFilter(entityID);        return;    }     // The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp)    if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == HIFI_URL_SCHEME_FILE)) {        qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer.";        scriptRequestFinished(entityID);        return;    }     // first remove any existing info for this entity    removeFilter(entityID);     // reject all edits until we load the script    FilterData filterData;    filterData.rejectAll = true;     _lock.lockForWrite();    _filterDataMap.insert(entityID, filterData);    _lock.unlock();     auto scriptRequest = DependencyManager::get<ResourceManager>()->createResourceRequest(        this, scriptURL, true, -1, "EntityEditFilters::addFilter");    if (!scriptRequest) {        qWarning() << "Could not create ResourceRequest for Entity Edit filter.";        scriptRequestFinished(entityID);        return;    }    // Agent.cpp sets up a timeout here, but that is unnecessary, as ResourceRequest has its own.    connect(scriptRequest, &ResourceRequest::finished, this, [this, entityID]{ EntityEditFilters::scriptRequestFinished(entityID);} );    // FIXME: handle atp rquests setup here. See Agent::requestScript()    scriptRequest->send();    qDebug() << "script request sent for entity " << entityID;} // Copied from ScriptEngine.cpp. We should make this a class method for reuse.// Note: I've deliberately stopped short of using ScriptEngine instead of ScriptEngine, as that is out of project scope at this point.static bool hasCorrectSyntax(const ScriptProgramPointer& program) {    const auto syntaxCheck = program->checkSyntax();    if (syntaxCheck->state() != ScriptSyntaxCheckResult::Valid) {        const auto error = syntaxCheck->errorMessage();        const auto line = QString::number(syntaxCheck->errorLineNumber());        const auto column = QString::number(syntaxCheck->errorColumnNumber());        const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program->fileName(), line, column);        qCritical() << qPrintable(message);        return false;    }    return true;}static bool hadUncaughtExceptions(ScriptEngine& engine, const QString& fileName) {    if (engine.hasUncaughtException()) {        qCritical() << engine.uncaughtException();        engine.clearExceptions();        return true;    }    return false;} void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) {    qDebug() << "script request completed for entity " << entityID;    auto scriptRequest = qobject_cast<ResourceRequest*>(sender());    if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) {        const QString urlString = scriptRequest->getUrl().toString();        auto scriptContents = scriptRequest->getData();        qInfo() << "Downloaded script:" << scriptContents;        // create a ScriptEngine for this script        ScriptManagerPointer manager = newScriptManager(ScriptManager::ENTITY_SERVER_SCRIPT, "", urlString);        ScriptEnginePointer engine = manager->engine();        ScriptProgramPointer program = engine->newProgram(scriptContents, urlString);        if (hasCorrectSyntax(program)) {            engine->setObjectName("filter:" + entityID.toString());            engine->setProperty("type", "edit_filter");            engine->setProperty("fileName", urlString);            engine->setProperty("entityID", entityID);            engine->globalObject().setProperty("Script", engine->newQObject(manager.get()));            DependencyManager::get<ScriptInitializers>()->runScriptInitializers(engine.get());            engine->evaluate(scriptContents, urlString);            if (!hadUncaughtExceptions(*engine, urlString)) {                // put the engine in the engine map (so we don't leak them, etc...)                FilterData filterData;                filterData.engine = engine;                filterData.rejectAll = false;                 // define the uncaughtException function                ScriptEngine& engineRef = *engine;                filterData.uncaughtExceptions = [&engineRef, urlString]() { return hadUncaughtExceptions(engineRef, urlString); };                 // now get the filter function                auto global = engine->globalObject();                auto entitiesObject = engine->newObject();                entitiesObject.setProperty("ADD_FILTER_TYPE", EntityTree::FilterType::Add);                entitiesObject.setProperty("EDIT_FILTER_TYPE", EntityTree::FilterType::Edit);                entitiesObject.setProperty("PHYSICS_FILTER_TYPE", EntityTree::FilterType::Physics);                entitiesObject.setProperty("DELETE_FILTER_TYPE", EntityTree::FilterType::Delete);                global.setProperty("Entities", entitiesObject);                filterData.filterFn = global.property("filter");                if (!filterData.filterFn.isFunction()) {                    qDebug() << "Filter function specified but not found. Will reject all edits for those without lock rights.";                    engine.reset();                    filterData.rejectAll=true;                }                 // if the wantsToFilterEdit is a boolean evaluate as a boolean, otherwise assume true                ScriptValue wantsToFilterAddValue = filterData.filterFn.property("wantsToFilterAdd");                filterData.wantsToFilterAdd = wantsToFilterAddValue.isBool() ? wantsToFilterAddValue.toBool() : true;                 // if the wantsToFilterEdit is a boolean evaluate as a boolean, otherwise assume true                ScriptValue wantsToFilterEditValue = filterData.filterFn.property("wantsToFilterEdit");                filterData.wantsToFilterEdit = wantsToFilterEditValue.isBool() ? wantsToFilterEditValue.toBool() : true;                 // if the wantsToFilterPhysics is a boolean evaluate as a boolean, otherwise assume true                ScriptValue wantsToFilterPhysicsValue = filterData.filterFn.property("wantsToFilterPhysics");                filterData.wantsToFilterPhysics = wantsToFilterPhysicsValue.isBool() ? wantsToFilterPhysicsValue.toBool() : true;                 // if the wantsToFilterDelete is a boolean evaluate as a boolean, otherwise assume false                ScriptValue wantsToFilterDeleteValue = filterData.filterFn.property("wantsToFilterDelete");                filterData.wantsToFilterDelete = wantsToFilterDeleteValue.isBool() ? wantsToFilterDeleteValue.toBool() : false;                 // check to see if the filterFn has properties asking for Original props                ScriptValue wantsOriginalPropertiesValue = filterData.filterFn.property("wantsOriginalProperties");                // if the wantsOriginalProperties is a boolean, or a string, or list of strings, then evaluate as follows:                //   - boolean - true  - include all original properties                //               false - no properties at all                //   - string  - empty - no properties at all                //               any valid property - include just that property in the Original properties                //   - list of strings - include only those properties in the Original properties                if (wantsOriginalPropertiesValue.isBool()) {                    filterData.wantsOriginalProperties = wantsOriginalPropertiesValue.toBool();                } else if (wantsOriginalPropertiesValue.isString()) {                    auto stringValue = wantsOriginalPropertiesValue.toString();                    filterData.wantsOriginalProperties = !stringValue.isEmpty();                    if (filterData.wantsOriginalProperties) {                        EntityPropertyFlagsFromScriptValue(wantsOriginalPropertiesValue, filterData.includedOriginalProperties);                    }                } else if (wantsOriginalPropertiesValue.isArray()) {                    EntityPropertyFlagsFromScriptValue(wantsOriginalPropertiesValue, filterData.includedOriginalProperties);                    filterData.wantsOriginalProperties = !filterData.includedOriginalProperties.isEmpty();                }                 // check to see if the filterFn has properties asking for Zone props                ScriptValue wantsZonePropertiesValue = filterData.filterFn.property("wantsZoneProperties");                // if the wantsZoneProperties is a boolean, or a string, or list of strings, then evaluate as follows:                //   - boolean - true  - include all Zone properties                //               false - no properties at all                //   - string  - empty - no properties at all                //               any valid property - include just that property in the Zone properties                //   - list of strings - include only those properties in the Zone properties                if (wantsZonePropertiesValue.isBool()) {                    filterData.wantsZoneProperties = wantsZonePropertiesValue.toBool();                    filterData.wantsZoneBoundingBox = filterData.wantsZoneProperties; // include this too                } else if (wantsZonePropertiesValue.isString()) {                    auto stringValue = wantsZonePropertiesValue.toString();                    filterData.wantsZoneProperties = !stringValue.isEmpty();                    if (filterData.wantsZoneProperties) {                        if (stringValue == "boundingBox") {                            filterData.wantsZoneBoundingBox = true;                        } else {                            EntityPropertyFlagsFromScriptValue(wantsZonePropertiesValue, filterData.includedZoneProperties);                        }                    }                } else if (wantsZonePropertiesValue.isArray()) {                    auto length = wantsZonePropertiesValue.property("length").toInteger();                    for (int i = 0; i < length; i++) {                        auto stringValue = wantsZonePropertiesValue.property(i).toString();                        if (!stringValue.isEmpty()) {                            filterData.wantsZoneProperties = true;                             // boundingBox is a special case since it's not a true EntityPropertyFlag, so we                            // need to detect it here.                            if (stringValue == "boundingBox") {                                filterData.wantsZoneBoundingBox = true;                                break; // we can break here, since there are no other special cases                            }                         }                    }                    if (filterData.wantsZoneProperties) {                        EntityPropertyFlagsFromScriptValue(wantsZonePropertiesValue, filterData.includedZoneProperties);                    }                }                 _lock.lockForWrite();                _filterDataMap.insert(entityID, filterData);                _lock.unlock();                 qDebug() << "script request filter processed for entity id " << entityID;                 emit filterAdded(entityID, true);                return;            }        }    } else if (scriptRequest) {        const QString urlString = scriptRequest->getUrl().toString();        qCritical() << "Failed to download script";        // See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure.        qCritical() << "ResourceRequest error was" << scriptRequest->getResult();    } else {        qCritical() << "Failed to create script request.";    }    emit filterAdded(entityID, false);}