mirror of https://gitee.com/bigwinds/arangodb
415 lines
14 KiB
C++
415 lines
14 KiB
C++
////////////////////////////////////////////////////////////////////////////////
|
|
/// DISCLAIMER
|
|
///
|
|
/// Copyright 2017 ArangoDB GmbH, Cologne, Germany
|
|
///
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
/// you may not use this file except in compliance with the License.
|
|
/// You may obtain a copy of the License at
|
|
///
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
///
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
/// See the License for the specific language governing permissions and
|
|
/// limitations under the License.
|
|
///
|
|
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
|
|
///
|
|
/// @author Wilfried Goesgens
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "AqlUserFunctions.h"
|
|
|
|
#include "Aql/Query.h"
|
|
#include "Aql/QueryRegistry.h"
|
|
#include "Aql/QueryString.h"
|
|
#include "Basics/StringUtils.h"
|
|
#include "Basics/VelocyPackHelper.h"
|
|
#include "RestServer/QueryRegistryFeature.h"
|
|
#include "Transaction/Helpers.h"
|
|
#include "Transaction/Methods.h"
|
|
#include "Transaction/StandaloneContext.h"
|
|
#include "Transaction/V8Context.h"
|
|
#include "Utils/OperationOptions.h"
|
|
#include "Utils/SingleCollectionTransaction.h"
|
|
#include "V8/JavaScriptSecurityContext.h"
|
|
#include "V8/v8-globals.h"
|
|
#include "V8/v8-utils.h"
|
|
#include "V8Server/V8DealerFeature.h"
|
|
|
|
#include <v8.h>
|
|
#include <velocypack/Builder.h>
|
|
#include <velocypack/Iterator.h>
|
|
#include <velocypack/StringRef.h>
|
|
#include <velocypack/velocypack-aliases.h>
|
|
#include <regex>
|
|
|
|
using namespace arangodb;
|
|
|
|
namespace {
|
|
std::string const collectionName("_aqlfunctions");
|
|
|
|
// Must not start with `_`, may contain alphanumerical characters, should have
|
|
// at least one set of double colons followed by more alphanumerical characters.
|
|
std::regex const funcRegEx("[a-zA-Z0-9][a-zA-Z0-9_]*(::[a-zA-Z0-9_]+)+", std::regex::ECMAScript);
|
|
|
|
// we may filter less restrictive:
|
|
std::regex const funcFilterRegEx("[a-zA-Z0-9_]+(::[a-zA-Z0-9_]*)*", std::regex::ECMAScript);
|
|
|
|
bool isValidFunctionName(std::string const& testName) {
|
|
return std::regex_match(testName, funcRegEx);
|
|
}
|
|
|
|
bool isValidFunctionNameFilter(std::string const& testName) {
|
|
return std::regex_match(testName, funcFilterRegEx);
|
|
}
|
|
|
|
void reloadAqlUserFunctions() {
|
|
std::string const def("reloadAql");
|
|
V8DealerFeature::DEALER->addGlobalContextMethod(def);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Result arangodb::unregisterUserFunction(TRI_vocbase_t& vocbase, std::string const& functionName) {
|
|
if (functionName.empty() || !isValidFunctionNameFilter(functionName)) {
|
|
return Result(TRI_ERROR_QUERY_FUNCTION_INVALID_NAME,
|
|
std::string("error deleting AQL user function: '") +
|
|
functionName + "' contains invalid characters");
|
|
}
|
|
|
|
std::string aql(
|
|
"FOR fn IN @@col FILTER fn._key == @fnName REMOVE { _key: fn._key } in "
|
|
"@@col RETURN 1");
|
|
std::string UCFN = basics::StringUtils::toupper(functionName);
|
|
|
|
auto binds = std::make_shared<VPackBuilder>();
|
|
binds->openObject();
|
|
binds->add("fnName", VPackValue(UCFN));
|
|
binds->add("@col", VPackValue(collectionName));
|
|
binds->close(); // obj
|
|
|
|
{
|
|
bool const contextOwnedByExterior = (v8::Isolate::GetCurrent() != nullptr);
|
|
arangodb::aql::Query query(contextOwnedByExterior, vocbase,
|
|
arangodb::aql::QueryString(aql), binds, nullptr,
|
|
arangodb::aql::PART_MAIN);
|
|
auto queryRegistry = QueryRegistryFeature::registry();
|
|
aql::QueryResult queryResult = query.executeSync(queryRegistry);
|
|
|
|
if (queryResult.result.fail()) {
|
|
if (queryResult.result.is(TRI_ERROR_REQUEST_CANCELED) ||
|
|
(queryResult.result.is(TRI_ERROR_QUERY_KILLED))) {
|
|
return Result(TRI_ERROR_REQUEST_CANCELED);
|
|
}
|
|
return queryResult.result;
|
|
}
|
|
|
|
VPackSlice countSlice = queryResult.data->slice();
|
|
if (!countSlice.isArray()) {
|
|
return Result(TRI_ERROR_INTERNAL,
|
|
"bad query result for deleting AQL user functions");
|
|
}
|
|
|
|
if (countSlice.length() != 1) {
|
|
return Result(TRI_ERROR_QUERY_FUNCTION_NOT_FOUND,
|
|
std::string("no AQL user function with name '") +
|
|
functionName + "' found");
|
|
}
|
|
}
|
|
|
|
reloadAqlUserFunctions();
|
|
return Result();
|
|
}
|
|
|
|
Result arangodb::unregisterUserFunctionsGroup(TRI_vocbase_t& vocbase,
|
|
std::string const& functionFilterPrefix,
|
|
int& deleteCount) {
|
|
deleteCount = 0;
|
|
|
|
if (functionFilterPrefix.empty()) {
|
|
return Result(TRI_ERROR_BAD_PARAMETER);
|
|
}
|
|
|
|
if (!isValidFunctionNameFilter(functionFilterPrefix)) {
|
|
return Result(TRI_ERROR_QUERY_FUNCTION_INVALID_NAME,
|
|
std::string("error deleting AQL user function: '") +
|
|
functionFilterPrefix + "' contains invalid characters");
|
|
}
|
|
|
|
std::string uc(functionFilterPrefix);
|
|
basics::StringUtils::toupperInPlace(&uc);
|
|
|
|
if ((uc.length() < 2) || (uc[uc.length() - 1] != ':') || (uc[uc.length() - 2] != ':')) {
|
|
uc += "::";
|
|
}
|
|
|
|
auto binds = std::make_shared<VPackBuilder>();
|
|
binds->openObject();
|
|
binds->add("fnLength", VPackValue(uc.length()));
|
|
binds->add("ucName", VPackValue(uc));
|
|
binds->add("@col", VPackValue(collectionName));
|
|
binds->close();
|
|
|
|
std::string aql(
|
|
"FOR fn IN @@col FILTER UPPER(LEFT(fn.name, @fnLength)) == @ucName "
|
|
"REMOVE { _key: fn._key} in @@col RETURN 1");
|
|
|
|
{
|
|
bool const contextOwnedByExterior = (v8::Isolate::GetCurrent() != nullptr);
|
|
arangodb::aql::Query query(contextOwnedByExterior, vocbase,
|
|
arangodb::aql::QueryString(aql), binds, nullptr,
|
|
arangodb::aql::PART_MAIN);
|
|
auto queryRegistry = QueryRegistryFeature::registry();
|
|
aql::QueryResult queryResult = query.executeSync(queryRegistry);
|
|
|
|
if (queryResult.result.fail()) {
|
|
if (queryResult.result.is(TRI_ERROR_REQUEST_CANCELED) ||
|
|
(queryResult.result.is(TRI_ERROR_QUERY_KILLED))) {
|
|
return Result(TRI_ERROR_REQUEST_CANCELED);
|
|
}
|
|
return queryResult.result;
|
|
}
|
|
|
|
VPackSlice countSlice = queryResult.data->slice();
|
|
if (!countSlice.isArray()) {
|
|
return Result(TRI_ERROR_INTERNAL,
|
|
"bad query result for deleting AQL user functions");
|
|
}
|
|
|
|
deleteCount = static_cast<int>(countSlice.length());
|
|
}
|
|
|
|
reloadAqlUserFunctions();
|
|
return Result();
|
|
}
|
|
|
|
Result arangodb::registerUserFunction(TRI_vocbase_t& vocbase, velocypack::Slice userFunction,
|
|
bool& replacedExisting) {
|
|
replacedExisting = false;
|
|
|
|
Result res;
|
|
std::string name;
|
|
|
|
try {
|
|
auto vname = userFunction.get("name");
|
|
if (!vname.isString()) {
|
|
return Result(TRI_ERROR_QUERY_FUNCTION_INVALID_NAME,
|
|
"function name has to be provided as a string");
|
|
}
|
|
name = vname.copyString();
|
|
if (name.empty()) {
|
|
return Result(TRI_ERROR_QUERY_FUNCTION_INVALID_NAME,
|
|
"function name has to be provided and must not be empty");
|
|
}
|
|
} catch (...) {
|
|
return Result(TRI_ERROR_QUERY_FUNCTION_INVALID_NAME,
|
|
"missing mandatory attribute 'name'");
|
|
}
|
|
|
|
if (!isValidFunctionName(name)) {
|
|
return Result(TRI_ERROR_QUERY_FUNCTION_INVALID_NAME,
|
|
std::string("error creating AQL user function: '") + name +
|
|
"' is not a valid name");
|
|
}
|
|
|
|
auto cvString = userFunction.get("code");
|
|
if (!cvString.isString() || cvString.getStringLength() == 0) {
|
|
return Result(TRI_ERROR_QUERY_FUNCTION_INVALID_CODE,
|
|
"expecting string with function definition");
|
|
}
|
|
|
|
std::string tmp = cvString.copyString();
|
|
std::string code = std::string("(") + tmp + "\n)";
|
|
|
|
bool isDeterministic = false;
|
|
VPackSlice isDeterministicSlice = userFunction.get("isDeterministic");
|
|
if (isDeterministicSlice.isBoolean()) {
|
|
isDeterministic = isDeterministicSlice.getBoolean();
|
|
}
|
|
|
|
{
|
|
ISOLATE;
|
|
bool throwV8Exception = (isolate != nullptr);
|
|
|
|
JavaScriptSecurityContext securityContext = JavaScriptSecurityContext::createRestrictedContext();
|
|
V8ConditionalContextGuard contextGuard(res, isolate, &vocbase, securityContext);
|
|
|
|
if (res.fail()) {
|
|
return res;
|
|
}
|
|
|
|
std::string testCode("(function() { var callback = ");
|
|
testCode += tmp + "; return callback; })()";
|
|
v8::HandleScope scope(isolate);
|
|
|
|
v8::Handle<v8::Value> result;
|
|
{
|
|
v8::TryCatch tryCatch(isolate);
|
|
|
|
result = TRI_ExecuteJavaScriptString(isolate, isolate->GetCurrentContext(),
|
|
TRI_V8_STD_STRING(isolate, testCode),
|
|
TRI_V8_ASCII_STRING(isolate,
|
|
"userFunction"),
|
|
false);
|
|
|
|
if (result.IsEmpty() || !result->IsFunction() || tryCatch.HasCaught()) {
|
|
if (tryCatch.HasCaught()) {
|
|
res.reset(TRI_ERROR_QUERY_FUNCTION_INVALID_CODE,
|
|
std::string(TRI_errno_string(TRI_ERROR_QUERY_FUNCTION_INVALID_CODE)) +
|
|
": " + TRI_StringifyV8Exception(isolate, &tryCatch));
|
|
|
|
if (!tryCatch.CanContinue()) {
|
|
if (throwV8Exception) {
|
|
tryCatch.ReThrow();
|
|
}
|
|
TRI_GET_GLOBALS();
|
|
v8g->_canceled = true;
|
|
}
|
|
} else {
|
|
res.reset(TRI_ERROR_QUERY_FUNCTION_INVALID_CODE,
|
|
std::string(TRI_errno_string(TRI_ERROR_QUERY_FUNCTION_INVALID_CODE)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!res.ok()) {
|
|
return res;
|
|
}
|
|
std::string _key(name);
|
|
basics::StringUtils::toupperInPlace(&_key);
|
|
|
|
VPackBuilder oneFunctionDocument;
|
|
oneFunctionDocument.openObject();
|
|
oneFunctionDocument.add("_key", VPackValue(_key));
|
|
oneFunctionDocument.add("name", VPackValue(name));
|
|
oneFunctionDocument.add("code", VPackValue(code));
|
|
oneFunctionDocument.add("isDeterministic", VPackValue(isDeterministic));
|
|
oneFunctionDocument.close();
|
|
|
|
{
|
|
arangodb::OperationOptions opOptions;
|
|
opOptions.waitForSync = true;
|
|
|
|
// find and load collection given by name or identifier
|
|
auto ctx = transaction::V8Context::CreateWhenRequired(vocbase, true);
|
|
SingleCollectionTransaction trx(ctx, collectionName, AccessMode::Type::WRITE);
|
|
|
|
res = trx.begin();
|
|
if (res.fail()) {
|
|
return res;
|
|
}
|
|
|
|
arangodb::OperationResult result;
|
|
result = trx.insert(collectionName, oneFunctionDocument.slice(), opOptions);
|
|
|
|
if (result.result.is(TRI_ERROR_ARANGO_UNIQUE_CONSTRAINT_VIOLATED)) {
|
|
replacedExisting = true;
|
|
result = trx.replace(collectionName, oneFunctionDocument.slice(), opOptions);
|
|
}
|
|
// Will commit if no error occured.
|
|
// or abort if an error occured.
|
|
// result stays valid!
|
|
res = trx.finish(result.result);
|
|
}
|
|
|
|
if (res.ok()) {
|
|
reloadAqlUserFunctions();
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
Result arangodb::toArrayUserFunctions(TRI_vocbase_t& vocbase,
|
|
std::string const& functionFilterPrefix,
|
|
velocypack::Builder& result) {
|
|
std::string aql;
|
|
auto binds = std::make_shared<VPackBuilder>();
|
|
|
|
binds->openObject();
|
|
|
|
if (!functionFilterPrefix.empty()) {
|
|
aql =
|
|
"FOR function IN @@col FILTER LEFT(function._key, @fnLength) == "
|
|
"@ucName RETURN function";
|
|
|
|
std::string uc(functionFilterPrefix);
|
|
basics::StringUtils::toupperInPlace(&uc);
|
|
|
|
if ((uc.length() < 2) || (uc[uc.length() - 1] != ':') || (uc[uc.length() - 2] != ':')) {
|
|
uc += "::";
|
|
}
|
|
|
|
binds->add("fnLength", VPackValue(uc.length()));
|
|
binds->add("ucName", VPackValue(uc));
|
|
} else {
|
|
aql = "FOR function IN @@col RETURN function";
|
|
}
|
|
|
|
binds->add("@col", VPackValue(collectionName));
|
|
binds->close();
|
|
|
|
bool const contextOwnedByExterior = (v8::Isolate::GetCurrent() != nullptr);
|
|
arangodb::aql::Query query(contextOwnedByExterior, vocbase,
|
|
arangodb::aql::QueryString(aql), binds, nullptr,
|
|
arangodb::aql::PART_MAIN);
|
|
auto queryRegistry = QueryRegistryFeature::registry();
|
|
aql::QueryResult queryResult = query.executeSync(queryRegistry);
|
|
|
|
if (queryResult.result.fail()) {
|
|
if (queryResult.result.is(TRI_ERROR_REQUEST_CANCELED) ||
|
|
(queryResult.result.is(TRI_ERROR_QUERY_KILLED))) {
|
|
return Result(TRI_ERROR_REQUEST_CANCELED);
|
|
}
|
|
return queryResult.result;
|
|
}
|
|
|
|
auto usersFunctionsSlice = queryResult.data->slice();
|
|
|
|
if (!usersFunctionsSlice.isArray()) {
|
|
return Result(TRI_ERROR_INTERNAL,
|
|
"bad query result for AQL user functions");
|
|
}
|
|
|
|
result.openArray();
|
|
std::string tmp;
|
|
for (auto const& it : VPackArrayIterator(usersFunctionsSlice)) {
|
|
VPackSlice resolved;
|
|
resolved = it.resolveExternal();
|
|
|
|
if (!resolved.isObject()) {
|
|
return Result(TRI_ERROR_INTERNAL,
|
|
"element that stores AQL user function is not an object");
|
|
}
|
|
|
|
VPackSlice name, fn, dtm;
|
|
bool isDeterministic = false;
|
|
name = resolved.get("name");
|
|
fn = resolved.get("code");
|
|
dtm = resolved.get("isDeterministic");
|
|
if (dtm.isBoolean()) {
|
|
isDeterministic = dtm.getBool();
|
|
}
|
|
// We simply ignore invalid entries in the _functions collection:
|
|
if (name.isString() && fn.isString() && (fn.getStringLength() > 2)) {
|
|
auto ref = arangodb::velocypack::StringRef(fn);
|
|
|
|
ref = ref.substr(1, ref.length() - 2);
|
|
tmp = basics::StringUtils::trim(ref.toString());
|
|
|
|
VPackBuilder oneFunction;
|
|
oneFunction.openObject();
|
|
oneFunction.add("name", name);
|
|
oneFunction.add("code", VPackValue(tmp));
|
|
oneFunction.add("isDeterministic", VPackValue(isDeterministic));
|
|
oneFunction.close();
|
|
result.add(oneFunction.slice());
|
|
}
|
|
}
|
|
result.close(); // close Array
|
|
|
|
return Result();
|
|
}
|