From 97e99d6fe37a3dcaa4725c7dcf25a3d72768a107 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 18 Jan 2023 21:59:01 +0000 Subject: [PATCH 01/27] GUACAMOLE-926: Create directory infrastructure for batch creation. --- .../net/auth/AtomicDirectoryOperation.java | 57 ++++++++ .../apache/guacamole/net/auth/Directory.java | 85 ++++++++++- .../DirectoryOperationException.java | 12 ++ .../rest/directory/DirectoryResource.java | 134 +++++++++++++++--- 4 files changed, 265 insertions(+), 23 deletions(-) create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java new file mode 100644 index 000000000..1d5e196eb --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.guacamole.net.auth; + +import org.apache.guacamole.GuacamoleException; + +/** + * An operation that should be attempted atomically when passed to + * {@link Directory#tryAtomically()}, if atomic operations are supported by + * the Directory. + */ +public interface AtomicDirectoryOperation { + + /** + * Attempt the operation atomically. If the Directory does not support + * atomic operations, the atomic flag will be set to false. If the atomic + * flag is set to true, the provided directory is guaranteed to perform + * the operations within this function atomically. Atomicity of the + * provided directory outside this function, or of the directory invoking + * this function are not guaranteed. + * + * NOTE: If atomicity is required for this operation, a GuacamoleException + * may be thrown by this function before any changes are made, ensuring the + * operation will only ever be performed atomically. + * + * @param atomic + * True if the provided directory is guaranteed to peform the operation + * atomically within the context of this function. + * + * @param directory + * A directory that will perform the operation atomically if the atomic + * flag is set to true. If the flag is false, the directory may still + * be used, though atomicity is not guaranteed. + * + * @throws GuacamoleException + * If an issue occurs during the operation. + */ + void executeOperation(boolean atomic, Directory directory) + throws GuacamoleException; +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java index ba1e1522d..ac76fae9d 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java @@ -20,6 +20,7 @@ package org.apache.guacamole.net.auth; import java.util.Collection; +import java.util.Iterator; import java.util.Set; import org.apache.guacamole.GuacamoleException; @@ -198,6 +199,29 @@ public interface Directory { void add(ObjectType object) throws GuacamoleException; + /** + * Adds the given objects to the overall set. If new identifiers are + * created for any of the the added objects, the identifiers will be + * automatically assigned via setIdentifier(). + * + * @param objects + * The objects to add. + * + * @throws GuacamoleException + * If an error occurs while adding any of the objects, or if adding + * the objects is not allowed. + */ + default void add(Collection objects) + throws GuacamoleException { + + // Add each object individually by default + Iterator iterator = objects.iterator(); + while (iterator.hasNext()) { + add(iterator.next()); + } + + } + /** * Updates the stored object with the data contained in the given object. * @@ -209,14 +233,73 @@ public interface Directory { void update(ObjectType object) throws GuacamoleException; + /** + * Updates the stored objects with the data contained in the given objects. + * + * @param objects The objects which will supply the data for the update. + * + * @throws GuacamoleException If an error occurs while updating the objects, + * or if updating an object is not allowed. + */ + default void update(Collection objects) + throws GuacamoleException { + + // Update each object individually by default + Iterator iterator = objects.iterator(); + while (iterator.hasNext()) { + update(iterator.next()); + } + + } + /** * Removes the object with the given identifier from the overall set. * * @param identifier The identifier of the object to remove. * * @throws GuacamoleException If an error occurs while removing the object, - * or if removing object is not allowed. + * or if removing the object is not allowed. */ void remove(String identifier) throws GuacamoleException; + /** + * Removes all object with any of the given identifier from the overall set. + * + * @param identifiers The identifiers of the objects to remove. + * + * @throws GuacamoleException If an error occurs while removing an object, + * or if removing an object is not allowed. + */ + default void remove(Collection identifiers) + throws GuacamoleException { + + // Remove each object individually by default + Iterator iterator = identifiers.iterator(); + while (iterator.hasNext()) { + remove(iterator.next()); + } + + } + + /** + * Attempt to perform the provided operation atomically if possible. If the + * operation can be performed atomically, the atomic flag will be set to + * true, and the directory passed to the provided operation callback will + * peform directory operations atomically within the operation callback. + * + * @param operation + * The directory operation that should be performed atomically. + * + * @throws GuacamoleException + * If an error occurs during execution of the provided operation. + */ + default void tryAtomically(AtomicDirectoryOperation operation) + throws GuacamoleException { + + // By default, perform the operation non-atomically. If atomic operation + // is supported by an implementation, it must be implemented there. + operation.executeOperation(false, this); + + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java new file mode 100644 index 000000000..3a88d4270 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java @@ -0,0 +1,12 @@ +package org.apache.guacamole.rest.directory; + +import org.apache.guacamole.GuacamoleException; + +public class DirectoryOperationException extends GuacamoleException { + + public DirectoryOperationException(String message) { + super(message); + } + + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index 50d428528..8aa464bd0 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -19,8 +19,10 @@ package org.apache.guacamole.rest.directory; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import javax.inject.Inject; @@ -37,6 +39,7 @@ import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleUnsupportedException; +import org.apache.guacamole.net.auth.AtomicDirectoryOperation; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Directory; @@ -341,6 +344,17 @@ public abstract class DirectoryResource> patches) + public void patchObjects(List> patches) throws GuacamoleException { - // Apply each operation specified within the patch - for (APIPatch patch : patches) { + // Objects will be add, updated, and removed atomically + Collection objectsToAdd = new ArrayList<>(); + Collection objectsToUpdate = new ArrayList<>(); + Collection identifiersToRemove = new ArrayList<>(); - // Only remove is supported - if (patch.getOp() != APIPatch.Operation.remove) - throw new GuacamoleUnsupportedException("Only the \"remove\" " - + "operation is supported."); + // Apply each operation specified within the patch + for (APIPatch patch : patches) { // Retrieve and validate path String path = patch.getPath(); if (!path.startsWith("/")) throw new GuacamoleClientException("Patch paths must start with \"/\"."); - // Remove specified object - String identifier = path.substring(1); - try { - directory.remove(identifier); - fireDirectorySuccessEvent(DirectoryEvent.Operation.REMOVE, identifier, null); + // Append each provided object to the list, to be added atomically + if(patch.getOp() == APIPatch.Operation.add) { + + // Filter/sanitize object contents + InternalType internal = filterAndTranslate(patch.getValue()); + + // Add to the list of objects to create + objectsToAdd.add(internal); } - catch (GuacamoleException | RuntimeException | Error e) { - fireDirectoryFailureEvent(DirectoryEvent.Operation.REMOVE, identifier, null, e); - throw e; + + // Append each provided object to the list, to be updated atomically + else if (patch.getOp() == APIPatch.Operation.replace) { + + // Filter/sanitize object contents + InternalType internal = filterAndTranslate(patch.getValue()); + + // Add to the list of objects to update + objectsToUpdate.add(internal); } + // Append each identifier to the list, to be removed atomically + else if (patch.getOp() == APIPatch.Operation.remove) { + + String identifier = path.substring(1); + identifiersToRemove.add(identifier); + + } + + } + + // Perform all requested operations atomically + directory.tryAtomically(new AtomicDirectoryOperation() { + + @Override + public void executeOperation(boolean atomic, Directory directory) + throws GuacamoleException { + + // If the underlying directory implentation does not support + // atomic operations, abort the patch operation. This REST + // endpoint requires that operations be performed atomically. + if (!atomic) + throw new GuacamoleUnsupportedException( + "Atomic operations are not supported. " + + "The patch cannot be executed."); + + // First, create every object from the patch + directory.add(objectsToAdd); + + // Next, update every object from the patch + directory.update(objectsToUpdate); + + // Finally, remove every object from the patch + directory.remove(identifiersToRemove); + + } + + }); + + // Fire directory success events for each created object + Iterator addedIterator = objectsToAdd.iterator(); + while (addedIterator.hasNext()) { + + InternalType internal = addedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.ADD, internal.getIdentifier(), internal); + + } + + // Fire directory success events for each updated object + Iterator updatedIterator = objectsToUpdate.iterator(); + while (updatedIterator.hasNext()) { + + InternalType internal = updatedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.UPDATE, internal.getIdentifier(), internal); + + } + + // Fire directory success events for each removed object + Iterator removedIterator = identifiersToRemove.iterator(); + while (removedIterator.hasNext()) { + + String identifier = removedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.UPDATE, identifier, null); + } } @@ -453,8 +544,7 @@ public abstract class DirectoryResource Date: Wed, 18 Jan 2023 23:57:02 +0000 Subject: [PATCH 02/27] GUACAMOLE-926: Set JDBC to batch mode and just do operations one at a time. --- .../JDBCAuthenticationProviderModule.java | 6 + .../ActiveConnectionDirectory.java | 6 +- .../auth/jdbc/base/JDBCDirectory.java | 44 ++++ .../jdbc/connection/ConnectionDirectory.java | 6 +- .../ConnectionGroupDirectory.java | 6 +- .../SharingProfileDirectory.java | 6 +- .../auth/jdbc/user/UserDirectory.java | 6 +- .../jdbc/usergroup/UserGroupDirectory.java | 6 +- .../apache/guacamole/net/auth/Directory.java | 62 ------ .../DirectoryOperationException.java | 12 -- .../rest/directory/DirectoryResource.java | 204 +++++++++++------- 11 files changed, 188 insertions(+), 176 deletions(-) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/JDBCDirectory.java delete mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java index 5ae0ea53f..7b735bdf4 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java @@ -45,6 +45,7 @@ import org.apache.guacamole.auth.jdbc.security.SaltService; import org.apache.guacamole.auth.jdbc.security.SecureRandomSaltService; import org.apache.guacamole.auth.jdbc.permission.SystemPermissionService; import org.apache.guacamole.auth.jdbc.user.UserService; +import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.apache.guacamole.auth.jdbc.permission.ConnectionGroupPermissionMapper; import org.apache.guacamole.auth.jdbc.permission.ConnectionGroupPermissionService; @@ -126,6 +127,11 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule { // Transaction factory bindTransactionFactoryType(JdbcTransactionFactory.class); + // Set the JDBC Auth provider to use batch execution when possible + bindConfigurationSetting(configuration -> { + configuration.setDefaultExecutorType(ExecutorType.BATCH); + }); + // Add MyBatis mappers addMapperClass(ConnectionMapper.class); addMapperClass(ConnectionGroupMapper.class); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java index b0d6324aa..28510b738 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java @@ -25,16 +25,14 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.ActiveConnection; -import org.apache.guacamole.net.auth.Directory; /** * Implementation of a Directory which contains all currently-active * connections. */ -public class ActiveConnectionDirectory extends RestrictedObject - implements Directory { +public class ActiveConnectionDirectory extends JDBCDirectory { /** * Service for retrieving and manipulating active connections. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/JDBCDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/JDBCDirectory.java new file mode 100644 index 000000000..fcba2f678 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/JDBCDirectory.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.guacamole.auth.jdbc.base; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AtomicDirectoryOperation; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.Identifiable; +import org.mybatis.guice.transactional.Transactional; + +/** + * An implementation of Directory that uses database transactions to guarantee + * atomicity for any operations supplied to tryAtomically(). + */ +public abstract class JDBCDirectory + extends RestrictedObject implements Directory { + + @Transactional + public void tryAtomically(AtomicDirectoryOperation operation) + throws GuacamoleException { + + // Execute the operation atomically - the @Transactional annotation + // specifies that the entire operation will be performed in a transaction + operation.executeOperation(true, this); + + } +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index 52a127df4..3e364f509 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -25,17 +25,15 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.Connection; -import org.apache.guacamole.net.auth.Directory; import org.mybatis.guice.transactional.Transactional; /** * Implementation of the Connection Directory which is driven by an underlying, * arbitrary database. */ -public class ConnectionDirectory extends RestrictedObject - implements Directory { +public class ConnectionDirectory extends JDBCDirectory { /** * Service for managing connection objects. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java index 9f3930597..2e21dc21d 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java @@ -25,17 +25,15 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.ConnectionGroup; -import org.apache.guacamole.net.auth.Directory; import org.mybatis.guice.transactional.Transactional; /** * Implementation of the ConnectionGroup Directory which is driven by an * underlying, arbitrary database. */ -public class ConnectionGroupDirectory extends RestrictedObject - implements Directory { +public class ConnectionGroupDirectory extends JDBCDirectory { /** * Service for managing connection group objects. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java index 632557052..4035ff03f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java @@ -24,8 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; -import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.SharingProfile; import org.mybatis.guice.transactional.Transactional; @@ -33,8 +32,7 @@ import org.mybatis.guice.transactional.Transactional; * Implementation of the SharingProfile Directory which is driven by an * underlying, arbitrary database. */ -public class SharingProfileDirectory extends RestrictedObject - implements Directory { +public class SharingProfileDirectory extends JDBCDirectory { /** * Service for managing sharing profile objects. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java index dffd8e2ec..72d5a4f30 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java @@ -25,8 +25,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; -import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.User; import org.mybatis.guice.transactional.Transactional; @@ -34,8 +33,7 @@ import org.mybatis.guice.transactional.Transactional; * Implementation of the User Directory which is driven by an underlying, * arbitrary database. */ -public class UserDirectory extends RestrictedObject - implements Directory { +public class UserDirectory extends JDBCDirectory { /** * Service for managing user objects. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java index 911b8521f..c6bb89572 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java @@ -24,8 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; -import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.UserGroup; import org.mybatis.guice.transactional.Transactional; @@ -33,8 +32,7 @@ import org.mybatis.guice.transactional.Transactional; * Implementation of the UserGroup Directory which is driven by an underlying, * arbitrary database. */ -public class UserGroupDirectory extends RestrictedObject - implements Directory { +public class UserGroupDirectory extends JDBCDirectory { /** * Service for managing user group objects. diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java index ac76fae9d..bc5c52afa 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java @@ -20,7 +20,6 @@ package org.apache.guacamole.net.auth; import java.util.Collection; -import java.util.Iterator; import java.util.Set; import org.apache.guacamole.GuacamoleException; @@ -199,29 +198,6 @@ public interface Directory { void add(ObjectType object) throws GuacamoleException; - /** - * Adds the given objects to the overall set. If new identifiers are - * created for any of the the added objects, the identifiers will be - * automatically assigned via setIdentifier(). - * - * @param objects - * The objects to add. - * - * @throws GuacamoleException - * If an error occurs while adding any of the objects, or if adding - * the objects is not allowed. - */ - default void add(Collection objects) - throws GuacamoleException { - - // Add each object individually by default - Iterator iterator = objects.iterator(); - while (iterator.hasNext()) { - add(iterator.next()); - } - - } - /** * Updates the stored object with the data contained in the given object. * @@ -233,25 +209,6 @@ public interface Directory { void update(ObjectType object) throws GuacamoleException; - /** - * Updates the stored objects with the data contained in the given objects. - * - * @param objects The objects which will supply the data for the update. - * - * @throws GuacamoleException If an error occurs while updating the objects, - * or if updating an object is not allowed. - */ - default void update(Collection objects) - throws GuacamoleException { - - // Update each object individually by default - Iterator iterator = objects.iterator(); - while (iterator.hasNext()) { - update(iterator.next()); - } - - } - /** * Removes the object with the given identifier from the overall set. * @@ -262,25 +219,6 @@ public interface Directory { */ void remove(String identifier) throws GuacamoleException; - /** - * Removes all object with any of the given identifier from the overall set. - * - * @param identifiers The identifiers of the objects to remove. - * - * @throws GuacamoleException If an error occurs while removing an object, - * or if removing an object is not allowed. - */ - default void remove(Collection identifiers) - throws GuacamoleException { - - // Remove each object individually by default - Iterator iterator = identifiers.iterator(); - while (iterator.hasNext()) { - remove(iterator.next()); - } - - } - /** * Attempt to perform the provided operation atomically if possible. If the * operation can be performed atomically, the atomic flag will be set to diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java deleted file mode 100644 index 3a88d4270..000000000 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.apache.guacamole.rest.directory; - -import org.apache.guacamole.GuacamoleException; - -public class DirectoryOperationException extends GuacamoleException { - - public DirectoryOperationException(String message) { - super(message); - } - - -} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index 8aa464bd0..412068a96 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -417,49 +417,6 @@ public abstract class DirectoryResource> patches) throws GuacamoleException { - // Objects will be add, updated, and removed atomically - Collection objectsToAdd = new ArrayList<>(); - Collection objectsToUpdate = new ArrayList<>(); - Collection identifiersToRemove = new ArrayList<>(); - - // Apply each operation specified within the patch - for (APIPatch patch : patches) { - - // Retrieve and validate path - String path = patch.getPath(); - if (!path.startsWith("/")) - throw new GuacamoleClientException("Patch paths must start with \"/\"."); - - // Append each provided object to the list, to be added atomically - if(patch.getOp() == APIPatch.Operation.add) { - - // Filter/sanitize object contents - InternalType internal = filterAndTranslate(patch.getValue()); - - // Add to the list of objects to create - objectsToAdd.add(internal); - } - - // Append each provided object to the list, to be updated atomically - else if (patch.getOp() == APIPatch.Operation.replace) { - - // Filter/sanitize object contents - InternalType internal = filterAndTranslate(patch.getValue()); - - // Add to the list of objects to update - objectsToUpdate.add(internal); - } - - // Append each identifier to the list, to be removed atomically - else if (patch.getOp() == APIPatch.Operation.remove) { - - String identifier = path.substring(1); - identifiersToRemove.add(identifier); - - } - - } - // Perform all requested operations atomically directory.tryAtomically(new AtomicDirectoryOperation() { @@ -475,48 +432,139 @@ public abstract class DirectoryResource addedObjects = new ArrayList<>(); + Collection updatedObjects = new ArrayList<>(); + Collection removedIdentifiers = new ArrayList<>(); - // Finally, remove every object from the patch - directory.remove(identifiersToRemove); + // True if any operation in the patch failed. Any failure will + // fail the request, though won't result in immediate stoppage + // since more errors may yet be uncovered. + boolean failed = false; + + // Apply each operation specified within the patch + for (APIPatch patch : patches) { + + // Retrieve and validate path + String path = patch.getPath(); + if (!path.startsWith("/")) + throw new GuacamoleClientException("Patch paths must start with \"/\"."); + + if(patch.getOp() == APIPatch.Operation.add) { + + // Filter/sanitize object contents + InternalType internal = filterAndTranslate(patch.getValue()); + + try { + + // Attempt to add the new object + directory.add(internal); + + // Add the object to the list if addition was successful + addedObjects.add(internal); + + } + catch (GuacamoleException | RuntimeException | Error e) { + fireDirectoryFailureEvent( + DirectoryEvent.Operation.ADD, + internal.getIdentifier(), internal, e); + + // TODO: Save the error for later inclusion in a big JSON error response + failed = true; + } + + } + + else if (patch.getOp() == APIPatch.Operation.replace) { + + // Filter/sanitize object contents + InternalType internal = filterAndTranslate(patch.getValue()); + + try { + + // Attempt to update the object + directory.update(internal); + + // Add the object to the list if the update was successful + updatedObjects.add(internal); + } + catch (GuacamoleException | RuntimeException | Error e) { + fireDirectoryFailureEvent( + DirectoryEvent.Operation.UPDATE, + internal.getIdentifier(), internal, e); + + // TODO: Save the error for later inclusion in a big JSON error response + failed = true; + } + } + + // Append each identifier to the list, to be removed atomically + else if (patch.getOp() == APIPatch.Operation.remove) { + + String identifier = path.substring(1); + + try { + + // Attempt to remove the object + directory.remove(identifier); + + // Add the object to the list if the removal was successful + removedIdentifiers.add(identifier); + } + catch (GuacamoleException | RuntimeException | Error e) { + fireDirectoryFailureEvent( + DirectoryEvent.Operation.UPDATE, identifier, null, e); + + // TODO: Save the error for later inclusion in a big JSON error response + failed = true; + } + } + + } + + // If any operation failed, fail now + if (failed) { + throw new GuacamoleClientException( + "oh noes the patch batch failed"); + } + + // Fire directory success events for each created object + Iterator addedIterator = addedObjects.iterator(); + while (addedIterator.hasNext()) { + + InternalType internal = addedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.ADD, internal.getIdentifier(), internal); + + } + + // Fire directory success events for each updated object + Iterator updatedIterator = updatedObjects.iterator(); + while (updatedIterator.hasNext()) { + + InternalType internal = updatedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.UPDATE, internal.getIdentifier(), internal); + + } + + // Fire directory success events for each removed object + Iterator removedIterator = removedIdentifiers.iterator(); + while (removedIterator.hasNext()) { + + String identifier = removedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.UPDATE, identifier, null); + + } } }); - // Fire directory success events for each created object - Iterator addedIterator = objectsToAdd.iterator(); - while (addedIterator.hasNext()) { - - InternalType internal = addedIterator.next(); - fireDirectorySuccessEvent( - DirectoryEvent.Operation.ADD, internal.getIdentifier(), internal); - - } - - // Fire directory success events for each updated object - Iterator updatedIterator = objectsToUpdate.iterator(); - while (updatedIterator.hasNext()) { - - InternalType internal = updatedIterator.next(); - fireDirectorySuccessEvent( - DirectoryEvent.Operation.UPDATE, internal.getIdentifier(), internal); - - } - - // Fire directory success events for each removed object - Iterator removedIterator = identifiersToRemove.iterator(); - while (removedIterator.hasNext()) { - - String identifier = removedIterator.next(); - fireDirectorySuccessEvent( - DirectoryEvent.Operation.UPDATE, identifier, null); - - } + // TODO: JSON response with failures or success } From 7d1d5cdf1338f86043b54dd1e00012495923a238 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 19 Jan 2023 00:45:38 +0000 Subject: [PATCH 03/27] GUACAMOLE-926: Success / Error handling in REST API. --- .../src/main/java/org/apache/guacamole/rest/APIError.java | 2 ++ .../apache/guacamole/rest/directory/DirectoryResource.java | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java index 846e2930e..0d925d04f 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java @@ -202,6 +202,8 @@ public class APIError { this.translatableMessage = translatable.getTranslatableMessage(); } + // TODO: Handle patch exceptions, need a bunch of JSON saying which things failed + // Use generic translation string if message is not translated else this.translatableMessage = new TranslatableMessage(UNTRANSLATED_MESSAGE_KEY, diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index 412068a96..9ca71dba0 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -564,7 +564,9 @@ public abstract class DirectoryResource Date: Mon, 23 Jan 2023 23:40:48 +0000 Subject: [PATCH 04/27] GUACAMOLE-926: Improve response plumbing through to user. --- .../jdbc/connection/ConnectionDirectory.java | 15 +- .../net/auth/DelegatingDirectory.java | 6 + .../org/apache/guacamole/rest/APIError.java | 27 +++- .../rest/directory/DirectoryResource.java | 142 +++++++++++++++--- .../identifier/RelatedObjectSetResource.java | 2 +- .../rest/{ => jsonpatch}/APIPatch.java | 6 +- .../rest/jsonpatch/APIPatchError.java | 71 +++++++++ .../jsonpatch/APIPatchFailureException.java | 66 ++++++++ .../rest/jsonpatch/APIPatchOutcome.java | 110 ++++++++++++++ .../rest/jsonpatch/APIPatchResponse.java | 56 +++++++ .../rest/jsonpatch/package-info.java | 24 +++ .../permission/PermissionSetResource.java | 2 +- 12 files changed, 497 insertions(+), 30 deletions(-) rename guacamole/src/main/java/org/apache/guacamole/rest/{ => jsonpatch}/APIPatch.java (93%) create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index 3e364f509..775628889 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -68,8 +68,19 @@ public class ConnectionDirectory extends JDBCDirectory { @Override @Transactional public void update(Connection object) throws GuacamoleException { - ModeledConnection connection = (ModeledConnection) object; - connectionService.updateObject(getCurrentUser(), connection); + + // If the provided connection is already an internal type, update + // using the internal method + if (object instanceof ModeledConnection) { + ModeledConnection connection = (ModeledConnection) object; + connectionService.updateObject(getCurrentUser(), connection); + } + + // If the type is not already the expected internal type, use the + // external update method + else { + connectionService.updateExternalObject(getCurrentUser(), object); + } } @Override diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java index 0a9046c01..cc19847c1 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java @@ -90,4 +90,10 @@ public class DelegatingDirectory directory.remove(identifier); } + @Override + public void tryAtomically(AtomicDirectoryOperation operation) + throws GuacamoleException { + directory.tryAtomically(operation); + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java index 0d925d04f..63b55289b 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java @@ -21,6 +21,8 @@ package org.apache.guacamole.rest; import java.util.Collection; import java.util.Collections; +import java.util.List; + import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; @@ -31,6 +33,8 @@ import org.apache.guacamole.language.TranslatableMessage; import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; +import org.apache.guacamole.rest.jsonpatch.APIPatchFailureException; +import org.apache.guacamole.rest.jsonpatch.APIPatchOutcome; import org.apache.guacamole.tunnel.GuacamoleStreamException; /** @@ -71,6 +75,12 @@ public class APIError { */ private final Collection expected; + /** + * The outcome of each patch in the associated request, if this was a + * JSON Patch request. Otherwise null. + */ + private List patches = null; + /** * The type of error that occurred. */ @@ -202,13 +212,14 @@ public class APIError { this.translatableMessage = translatable.getTranslatableMessage(); } - // TODO: Handle patch exceptions, need a bunch of JSON saying which things failed - // Use generic translation string if message is not translated else this.translatableMessage = new TranslatableMessage(UNTRANSLATED_MESSAGE_KEY, Collections.singletonMap(UNTRANSLATED_MESSAGE_VARIABLE_NAME, this.message)); + if (exception instanceof APIPatchFailureException) + this.patches = ((APIPatchFailureException) exception).getPatches(); + } /** @@ -245,6 +256,18 @@ public class APIError { return expected; } + /** + * Return the outcome for every patch in the request, if the request was + * a JSON patch request. Otherwise, null. + * + * @return + * The outcome for every patch if responding to a JSON Patch request, + * otherwise null. + */ + public List getPatches() { + return patches; + } + /** * Returns a human-readable error message describing the error that * occurred. diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index 9ca71dba0..0b4e476bd 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -53,8 +53,12 @@ import org.apache.guacamole.net.auth.permission.SystemPermissionSet; import org.apache.guacamole.net.event.DirectoryEvent; import org.apache.guacamole.net.event.DirectoryFailureEvent; import org.apache.guacamole.net.event.DirectorySuccessEvent; -import org.apache.guacamole.rest.APIPatch; import org.apache.guacamole.rest.event.ListenerService; +import org.apache.guacamole.rest.jsonpatch.APIPatch; +import org.apache.guacamole.rest.jsonpatch.APIPatchError; +import org.apache.guacamole.rest.jsonpatch.APIPatchFailureException; +import org.apache.guacamole.rest.jsonpatch.APIPatchOutcome; +import org.apache.guacamole.rest.jsonpatch.APIPatchResponse; /** * A REST resource which abstracts the operations available on all Guacamole @@ -344,7 +348,20 @@ public abstract class DirectoryResource> patches) + public APIPatchResponse patchObjects(List> patches) throws GuacamoleException { + // An outcome for each patch included in the request. This list + // may include both success and failure responses, though the + // presense of any failure would indicated that the entire + // request has failed and no changes have been made. + List patchOutcomes = new ArrayList<>(); + // Perform all requested operations atomically directory.tryAtomically(new AtomicDirectoryOperation() { @@ -432,13 +460,16 @@ public abstract class DirectoryResource addedObjects = new ArrayList<>(); Collection updatedObjects = new ArrayList<>(); Collection removedIdentifiers = new ArrayList<>(); + // A list of all responses associated with the successful + // creation of new objects + List creationSuccesses = new ArrayList<>(); + // True if any operation in the patch failed. Any failure will // fail the request, though won't result in immediate stoppage // since more errors may yet be uncovered. @@ -465,14 +496,33 @@ public abstract class DirectoryResource response.clearIdentifier()); + + // Return an error response, including any failures that + // caused the failure of any patch in the request + throw new APIPatchFailureException( + "The provided patches failed to apply.", patchOutcomes); + } // Fire directory success events for each created object @@ -536,7 +634,8 @@ public abstract class DirectoryResource * The type of object being patched. diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java new file mode 100644 index 000000000..85d28a714 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.guacamole.rest.jsonpatch; + +import org.apache.guacamole.rest.jsonpatch.APIPatch.Operation; + +/** + * A failure outcome associated with a particular patch within a JSON Patch + * request. This status indicates that a particular patch failed to apply, + * and includes the error describing the failure, along with the operation and + * path from the original patch, and the identifier of the object + * referenced by the original patch. + */ +public class APIPatchError extends APIPatchOutcome { + + /** + * The error associated with the submitted patch. + */ + private final String error; + + /** + * Create a failure status associated with a submitted patch from a JSON + * patch API request. + * + * @param op + * The operation requested by the failed patch. + * + * @param identifier + * The identifier of the object associated with the failed patch. If + * the patch failed to create a new object, this will be null. + * + * @param path + * The patch from the failed patch. + * + * @param error + * The error message associated with the failure that prevented the + * patch from applying. + */ + public APIPatchError( + Operation op, String identifier, String path, String error) { + super(op, identifier, path); + this.error = error; + } + + /** + * Return the error associated with the patch failure. + * + * @return + * The error associated with the patch failure. + */ + public String getError() { + return error; + } +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java new file mode 100644 index 000000000..fa40bffc6 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.guacamole.rest.jsonpatch; + +import java.util.List; + +import org.apache.guacamole.GuacamoleClientException; + +/** + * An exception describing a failure to apply the patches from a JSON Patch + * request. A list of outcomes is included, one for each patch in the request. + */ +public class APIPatchFailureException extends GuacamoleClientException { + + /** + * A list of outcomes, each one corresponding to a patch in the request + * corresponding to this response. This may include a mix of successes and + * failures. Any failure will result in a failure of the entire request + * since JSON Patch requests are handled atomically. + */ + public final List patches; + + /** + * Create a new patch request failure with the provided list of outcomes + * for individual patches. + * + * @param message + * A human-readable message describing the overall request failure. + * + * @param patches + * A list of patch outcomes, one for each patch in the request + * associated with this response. + */ + public APIPatchFailureException( + String message, List patches) { + + super(message ); + this.patches = patches; + } + + /** + * Return the outcome for each patch in the request corresponding to this + * response. + */ + public List getPatches() { + return patches; + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java new file mode 100644 index 000000000..245664cbc --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.guacamole.rest.jsonpatch; + +import org.apache.guacamole.rest.jsonpatch.APIPatch.Operation; + +/** + * A successful outcome associated with a particular patch within a JSON Patch + * request. The outcome contains the operation requested by the original patch, + * the path from the original patch, and the identifier of the object corresponding + * to the value from the original patch. + * + * The purpose of this class is to present a relatively lightweight outcome for + * the user who submitted the Patch request. Rather than including the full + * contents of the value, only the identifier is included, allowing the user to + * determine the identifier of any newly-created objects as part of the request. + * + */ +public class APIPatchOutcome { + + /** + * The requested operation for the patch corresponding to this outcome. + */ + private final Operation op; + + /** + * The identifier for the value in patch corresponding to this outcome. + * If the value in the patch was null, this identifier should also be null. + */ + private String identifier; + + /** + * The path for the patch corresponding to this outcome. + */ + private final String path; + + /** + * Create an outcome associated with a submitted patch, as part of a JSON + * patch API request. + * + * @param op + * @param identifier + * @param path + */ + public APIPatchOutcome(Operation op, String identifier, String path) { + this.op = op; + this.identifier = identifier; + this.path = path; + } + + /** + * Clear the identifier associated with this patch outcome. This must + * be done when an identifier in a outcome refers to a temporary object + * that was rolled back during processing of a request. + */ + public void clearIdentifier() { + this.identifier = null; + } + + /** + * Returns the requested operation for the patch corresponding to this + * outcome. + * + * @return + * The requested operation for the patch corresponding to this outcome. + */ + public Operation getOp() { + return op; + } + + /** + * Returns the path for the patch corresponding to this outcome. + * + * @return + * The path for the patch corresponding to this outcome. + */ + public String getPath() { + return path; + } + + /** + * Returns the identifier for the value in patch corresponding to this + * outcome, or null if the value in the patch was null. + * + * @return + * The identifier for the value in patch corresponding to this + * outcome, or null if the value was null. + */ + public String getIdentifier() { + return identifier; + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java new file mode 100644 index 000000000..21096a2bc --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.guacamole.rest.jsonpatch; + +import java.util.List; + +/** + * A REST response describing the successful application of a JSON PATCH + * request to a directory. This consists of a list of outcomes, one for each + * patch within the request, in the same order. + */ +public class APIPatchResponse { + + /** + * A list of outcomes, each one corresponding to a patch in the request + * corresponding to this response. + */ + public final List patches; + + /** + * Create a new patch response with the provided list of outcomes for + * individual patches. + * + * @param patches + * A list of patch outcomes, one for each patch in the request + * associated with this response. + */ + public APIPatchResponse(List patches) { + this.patches = patches; + } + + /** + * Return the outcome for each patch in the request corresponding to this + * response. + */ + public List getPatches() { + return patches; + } +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java new file mode 100644 index 000000000..b5f824f63 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Classes related to JSON Patch HTTP requests or responses. + * See https://www.rfc-editor.org/rfc/rfc6902. + */ +package org.apache.guacamole.rest.jsonpatch; diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java index 38b337e0e..62f4d0ed9 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java @@ -31,7 +31,7 @@ import org.apache.guacamole.net.auth.Permissions; import org.apache.guacamole.net.auth.permission.ObjectPermission; import org.apache.guacamole.net.auth.permission.Permission; import org.apache.guacamole.net.auth.permission.SystemPermission; -import org.apache.guacamole.rest.APIPatch; +import org.apache.guacamole.rest.jsonpatch.APIPatch; /** * A REST resource which abstracts the operations available on the permissions From e6bd12ee4c80b895ec00af81738f005ed254b3f5 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 24 Jan 2023 01:23:55 +0000 Subject: [PATCH 05/27] GUACAMOLE-926: Allow JDBC extensions to accept identifiers from user for update. --- .../ActiveConnectionService.java | 8 +++++ .../jdbc/base/DirectoryObjectService.java | 18 ++++++++++++ .../base/ModeledDirectoryObjectService.java | 14 +++++++++ .../guacamole/auth/jdbc/base/ObjectModel.java | 29 ++++++++++++++++++- .../jdbc/connection/ConnectionDirectory.java | 11 ++++--- .../auth/jdbc/connection/ConnectionModel.java | 5 ---- .../jdbc/connection/ConnectionService.java | 7 ++++- .../connectiongroup/ConnectionGroupModel.java | 6 ---- .../ConnectionGroupService.java | 7 ++++- .../sharingprofile/SharingProfileModel.java | 6 ---- .../sharingprofile/SharingProfileService.java | 7 ++++- .../guacamole/auth/jdbc/user/UserService.java | 4 +++ .../auth/jdbc/usergroup/UserGroupService.java | 4 +++ .../rest/directory/DirectoryResource.java | 16 ++++++++++ 14 files changed, 115 insertions(+), 27 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java index 046cee1e2..80c75dbbb 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java @@ -166,6 +166,14 @@ public class ActiveConnectionService } + @Override + public void updateExternalObject(ModeledAuthenticatedUser user, ActiveConnection object) throws GuacamoleException { + + // Updating active connections is not implemented + throw new GuacamoleSecurityException("Permission denied."); + + } + /** * Retrieve the permission set for the specified user that relates * to access to active connections. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java index 590e01e9f..29d40cc49 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java @@ -116,6 +116,24 @@ public interface DirectoryObjectService { void deleteObject(ModeledAuthenticatedUser user, String identifier) throws GuacamoleException; + /** + * Updates the object corresponding to the given external representation, + * applying any changes that have been made. If no such object exists, + * this function has no effect. + * + * @param user + * The user updating the object. + * + * @param object + * The external object to apply updates from. + * + * @throws GuacamoleException + * If the user lacks permission to update the object, or an error + * occurs while updating the object. + */ + void updateExternalObject(ModeledAuthenticatedUser user, ExternalType object) + throws GuacamoleException; + /** * Updates the given object, applying any changes that have been made. If * no such object exists, this function has no effect. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java index 80bc7eb5c..75c25926b 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java @@ -511,6 +511,20 @@ public abstract class ModeledDirectoryObjectService getIdentifiers(ModeledAuthenticatedUser user) throws GuacamoleException { diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java index c3052b1b4..69eb1fd36 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java @@ -21,6 +21,8 @@ package org.apache.guacamole.auth.jdbc.base; import java.util.Collection; +import org.apache.guacamole.GuacamoleException; + /** * Object representation of a Guacamole object, such as a user or connection, * as represented in the database. @@ -75,7 +77,7 @@ public abstract class ObjectModel { * * @return * The ID of this object in the database, or null if this object was - * not retrieved from the database. + * not retrieved from or intended to update the database. */ public Integer getObjectID() { return objectID; @@ -91,6 +93,31 @@ public abstract class ObjectModel { this.objectID = objectID; } + /** + * Given a text identifier, attempt to convert to an integer database ID. + * If the identifier is valid, the database ID will be set to this value. + * Otherwise, a GuacamoleException will be thrown. + * + * @param identifier + * The identifier to convert to an integer and set on the database + * model, if valid. + * + * @throws GuacamoleException + * If the provided identifier is not a valid integer. + */ + public void setObjectID(String identifier) throws GuacamoleException { + + // Try to convert the provided identifier to an integer ID + try { + setObjectID(Integer.parseInt(identifier)); + } + + catch (NumberFormatException e) { + throw new GuacamoleException( + "Database identifiers must be integers."); + } + } + /** * Returns a map of attribute name/value pairs for all attributes associated * with this model which do not have explicit mappings to actual model diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index 775628889..b2f3e2bea 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -71,16 +71,15 @@ public class ConnectionDirectory extends JDBCDirectory { // If the provided connection is already an internal type, update // using the internal method - if (object instanceof ModeledConnection) { - ModeledConnection connection = (ModeledConnection) object; - connectionService.updateObject(getCurrentUser(), connection); - } + if (object instanceof ModeledConnection) + connectionService.updateObject( + getCurrentUser(), (ModeledConnection) object); // If the type is not already the expected internal type, use the // external update method - else { + else connectionService.updateExternalObject(getCurrentUser(), object); - } + } @Override diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java index da454025d..03413650b 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java @@ -387,9 +387,4 @@ public class ConnectionModel extends ChildObjectModel { } - @Override - public void setIdentifier(String identifier) { - throw new UnsupportedOperationException("Connection identifiers are derived from IDs. They cannot be set."); - } - } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java index b3ed89ce8..cdf2afb76 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java @@ -110,14 +110,19 @@ public class ConnectionService extends ModeledChildDirectoryObjectService Date: Tue, 24 Jan 2023 19:20:03 +0000 Subject: [PATCH 06/27] GUACAMOLE-926: Parse YAML, CSV, JSON on frontend and submit to API. --- doc/licenses/base64-js-1.5.1/LICENSE | 22 + doc/licenses/base64-js-1.5.1/README | 8 + .../base64-js-1.5.1/dep-coordinates.txt | 1 + doc/licenses/buffer-4.9.2/LICENSE | 21 + doc/licenses/buffer-4.9.2/README | 7 + doc/licenses/buffer-4.9.2/dep-coordinates.txt | 1 + doc/licenses/core-util-is-1.0.3/LICENSE | 19 + doc/licenses/core-util-is-1.0.3/README | 7 + .../core-util-is-1.0.3/dep-coordinates.txt | 1 + doc/licenses/csv-6.2.5/LICENSE | 21 + doc/licenses/csv-6.2.5/README | 7 + doc/licenses/csv-6.2.5/dep-coordinates.txt | 2 + doc/licenses/events-3.3.0/LICENSE | 22 + doc/licenses/events-3.3.0/README | 7 + doc/licenses/events-3.3.0/dep-coordinates.txt | 1 + doc/licenses/ieee754-1.2.1/LICENSE | 11 + doc/licenses/ieee754-1.2.1/README | 7 + .../ieee754-1.2.1/dep-coordinates.txt | 1 + doc/licenses/inherits-2.0.4/LICENSE | 15 + doc/licenses/inherits-2.0.4/README | 8 + .../inherits-2.0.4/dep-coordinates.txt | 1 + doc/licenses/isarray-1.0.0/LICENSE | 21 + doc/licenses/isarray-1.0.0/README | 8 + .../isarray-1.0.0/dep-coordinates.txt | 1 + .../process-nextick-args-2.0.1/README | 8 + .../dep-coordinates.txt | 1 + .../process-nextick-args-2.0.1/license.md | 19 + doc/licenses/readable-stream-2.3.7/LICENSE | 47 + doc/licenses/readable-stream-2.3.7/README | 8 + .../readable-stream-2.3.7/dep-coordinates.txt | 1 + doc/licenses/safe-buffer-5.1.2/LICENSE | 21 + doc/licenses/safe-buffer-5.1.2/README | 8 + .../safe-buffer-5.1.2/dep-coordinates.txt | 1 + doc/licenses/setimmediate-1.0.5/LICENSE.txt | 20 + doc/licenses/setimmediate-1.0.5/README | 8 + .../setimmediate-1.0.5/dep-coordinates.txt | 1 + doc/licenses/stream-browserify-2.0.2/LICENSE | 20 + doc/licenses/stream-browserify-2.0.2/README | 8 + .../dep-coordinates.txt | 1 + doc/licenses/string_decoder-1.1.1/LICENSE | 47 + doc/licenses/string_decoder-1.1.1/README | 8 + .../string_decoder-1.1.1/dep-coordinates.txt | 1 + .../timers-browserify-2.0.12/LICENSE.md | 23 + doc/licenses/timers-browserify-2.0.12/README | 8 + .../dep-coordinates.txt | 1 + doc/licenses/util-deprecate-1.0.2/LICENSE | 24 + doc/licenses/util-deprecate-1.0.2/README | 8 + .../util-deprecate-1.0.2/dep-coordinates.txt | 1 + guacamole/src/main/frontend/package-lock.json | 3907 ++++++++++++++++- guacamole/src/main/frontend/package.json | 7 +- .../src/app/index/config/indexRouteConfig.js | 9 + .../app/rest/services/connectionService.js | 40 + .../src/app/rest/types/RelatedObjectPatch.js | 2 +- .../connectionHistoryPlayerController.js | 2 +- .../importConnectionsController.js | 78 + .../controllers/settingsController.js | 2 +- .../services/connectionImportParseService.js | 355 ++ .../src/app/settings/styles/buttons.css | 10 +- .../templates/settingsConnections.html | 4 + .../settings/templates/settingsImport.html | 11 + .../src/app/settings/types/ParseError.js | 59 + .../action-icons/guac-monitor-add-many.svg | 77 + .../main/frontend/src/translations/en.json | 10 +- guacamole/src/main/frontend/webpack.config.js | 16 + 64 files changed, 5093 insertions(+), 9 deletions(-) create mode 100644 doc/licenses/base64-js-1.5.1/LICENSE create mode 100644 doc/licenses/base64-js-1.5.1/README create mode 100644 doc/licenses/base64-js-1.5.1/dep-coordinates.txt create mode 100644 doc/licenses/buffer-4.9.2/LICENSE create mode 100644 doc/licenses/buffer-4.9.2/README create mode 100644 doc/licenses/buffer-4.9.2/dep-coordinates.txt create mode 100644 doc/licenses/core-util-is-1.0.3/LICENSE create mode 100644 doc/licenses/core-util-is-1.0.3/README create mode 100644 doc/licenses/core-util-is-1.0.3/dep-coordinates.txt create mode 100644 doc/licenses/csv-6.2.5/LICENSE create mode 100644 doc/licenses/csv-6.2.5/README create mode 100644 doc/licenses/csv-6.2.5/dep-coordinates.txt create mode 100644 doc/licenses/events-3.3.0/LICENSE create mode 100644 doc/licenses/events-3.3.0/README create mode 100644 doc/licenses/events-3.3.0/dep-coordinates.txt create mode 100644 doc/licenses/ieee754-1.2.1/LICENSE create mode 100644 doc/licenses/ieee754-1.2.1/README create mode 100644 doc/licenses/ieee754-1.2.1/dep-coordinates.txt create mode 100644 doc/licenses/inherits-2.0.4/LICENSE create mode 100644 doc/licenses/inherits-2.0.4/README create mode 100644 doc/licenses/inherits-2.0.4/dep-coordinates.txt create mode 100644 doc/licenses/isarray-1.0.0/LICENSE create mode 100644 doc/licenses/isarray-1.0.0/README create mode 100644 doc/licenses/isarray-1.0.0/dep-coordinates.txt create mode 100644 doc/licenses/process-nextick-args-2.0.1/README create mode 100644 doc/licenses/process-nextick-args-2.0.1/dep-coordinates.txt create mode 100644 doc/licenses/process-nextick-args-2.0.1/license.md create mode 100644 doc/licenses/readable-stream-2.3.7/LICENSE create mode 100644 doc/licenses/readable-stream-2.3.7/README create mode 100644 doc/licenses/readable-stream-2.3.7/dep-coordinates.txt create mode 100644 doc/licenses/safe-buffer-5.1.2/LICENSE create mode 100644 doc/licenses/safe-buffer-5.1.2/README create mode 100644 doc/licenses/safe-buffer-5.1.2/dep-coordinates.txt create mode 100644 doc/licenses/setimmediate-1.0.5/LICENSE.txt create mode 100644 doc/licenses/setimmediate-1.0.5/README create mode 100644 doc/licenses/setimmediate-1.0.5/dep-coordinates.txt create mode 100644 doc/licenses/stream-browserify-2.0.2/LICENSE create mode 100644 doc/licenses/stream-browserify-2.0.2/README create mode 100644 doc/licenses/stream-browserify-2.0.2/dep-coordinates.txt create mode 100644 doc/licenses/string_decoder-1.1.1/LICENSE create mode 100644 doc/licenses/string_decoder-1.1.1/README create mode 100644 doc/licenses/string_decoder-1.1.1/dep-coordinates.txt create mode 100644 doc/licenses/timers-browserify-2.0.12/LICENSE.md create mode 100644 doc/licenses/timers-browserify-2.0.12/README create mode 100644 doc/licenses/timers-browserify-2.0.12/dep-coordinates.txt create mode 100644 doc/licenses/util-deprecate-1.0.2/LICENSE create mode 100644 doc/licenses/util-deprecate-1.0.2/README create mode 100644 doc/licenses/util-deprecate-1.0.2/dep-coordinates.txt create mode 100644 guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js create mode 100644 guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js create mode 100644 guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html create mode 100644 guacamole/src/main/frontend/src/app/settings/types/ParseError.js create mode 100644 guacamole/src/main/frontend/src/images/action-icons/guac-monitor-add-many.svg diff --git a/doc/licenses/base64-js-1.5.1/LICENSE b/doc/licenses/base64-js-1.5.1/LICENSE new file mode 100644 index 000000000..9143d4cd6 --- /dev/null +++ b/doc/licenses/base64-js-1.5.1/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Jameson Little + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +Footer diff --git a/doc/licenses/base64-js-1.5.1/README b/doc/licenses/base64-js-1.5.1/README new file mode 100644 index 000000000..6cacd8fc3 --- /dev/null +++ b/doc/licenses/base64-js-1.5.1/README @@ -0,0 +1,8 @@ +base64-js (https://github.com/beatgammit/base64-js) +--------------------------------------------- + + Version: 1.5.1 + From: 'Jameson Little' (https://github.com/beatgammit/) + License(s): + MIT (bundled/base640-js-1.5.1/LICENSE) + diff --git a/doc/licenses/base64-js-1.5.1/dep-coordinates.txt b/doc/licenses/base64-js-1.5.1/dep-coordinates.txt new file mode 100644 index 000000000..1d66ee817 --- /dev/null +++ b/doc/licenses/base64-js-1.5.1/dep-coordinates.txt @@ -0,0 +1 @@ +base64-js:1.5.1 diff --git a/doc/licenses/buffer-4.9.2/LICENSE b/doc/licenses/buffer-4.9.2/LICENSE new file mode 100644 index 000000000..d6bf75dcf --- /dev/null +++ b/doc/licenses/buffer-4.9.2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh, and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/doc/licenses/buffer-4.9.2/README b/doc/licenses/buffer-4.9.2/README new file mode 100644 index 000000000..7caad56c9 --- /dev/null +++ b/doc/licenses/buffer-4.9.2/README @@ -0,0 +1,7 @@ +buffer (https://github.com/feross/buffer) +--------------------------------------------- + + Version: 4.9.2 + From: 'Feross Aboukhadijeh' (https://github.com/feross) + License(s): + MIT (bundled/buffer-4.9.2/LICENSE) diff --git a/doc/licenses/buffer-4.9.2/dep-coordinates.txt b/doc/licenses/buffer-4.9.2/dep-coordinates.txt new file mode 100644 index 000000000..30f193d0a --- /dev/null +++ b/doc/licenses/buffer-4.9.2/dep-coordinates.txt @@ -0,0 +1 @@ +buffer:4.9.2 diff --git a/doc/licenses/core-util-is-1.0.3/LICENSE b/doc/licenses/core-util-is-1.0.3/LICENSE new file mode 100644 index 000000000..d8d7f9437 --- /dev/null +++ b/doc/licenses/core-util-is-1.0.3/LICENSE @@ -0,0 +1,19 @@ +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/doc/licenses/core-util-is-1.0.3/README b/doc/licenses/core-util-is-1.0.3/README new file mode 100644 index 000000000..411c1f1e9 --- /dev/null +++ b/doc/licenses/core-util-is-1.0.3/README @@ -0,0 +1,7 @@ +core-util-is (https://github.com/isaacs/core-util-is) +--------------------------------------------- + + Version: 1.0.3 + From: 'isaacs' (https://github.com/isaacs) + License(s): + MIT (bundled/core-util-is-1.0.3/LICENSE) diff --git a/doc/licenses/core-util-is-1.0.3/dep-coordinates.txt b/doc/licenses/core-util-is-1.0.3/dep-coordinates.txt new file mode 100644 index 000000000..f4b1c7a8f --- /dev/null +++ b/doc/licenses/core-util-is-1.0.3/dep-coordinates.txt @@ -0,0 +1 @@ +core-util-is:1.0.3 diff --git a/doc/licenses/csv-6.2.5/LICENSE b/doc/licenses/csv-6.2.5/LICENSE new file mode 100644 index 000000000..918eaf05a --- /dev/null +++ b/doc/licenses/csv-6.2.5/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2010 Adaltas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/licenses/csv-6.2.5/README b/doc/licenses/csv-6.2.5/README new file mode 100644 index 000000000..5fb35fae4 --- /dev/null +++ b/doc/licenses/csv-6.2.5/README @@ -0,0 +1,7 @@ +node-csv (https://github.com/adaltas/node-csv) +--------------------------------------------- + + Version: 6.2.5 + From: 'adaltas' (https://github.com/adaltas) + License(s): + MIT (bundled/csv-6.2.5/LICENSE) diff --git a/doc/licenses/csv-6.2.5/dep-coordinates.txt b/doc/licenses/csv-6.2.5/dep-coordinates.txt new file mode 100644 index 000000000..dc0f22695 --- /dev/null +++ b/doc/licenses/csv-6.2.5/dep-coordinates.txt @@ -0,0 +1,2 @@ +csv:6.2.5 +csv-parse:5.3.3 diff --git a/doc/licenses/events-3.3.0/LICENSE b/doc/licenses/events-3.3.0/LICENSE new file mode 100644 index 000000000..52ed3b0a6 --- /dev/null +++ b/doc/licenses/events-3.3.0/LICENSE @@ -0,0 +1,22 @@ +MIT + +Copyright Joyent, Inc. and other Node contributors. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/licenses/events-3.3.0/README b/doc/licenses/events-3.3.0/README new file mode 100644 index 000000000..274b952ab --- /dev/null +++ b/doc/licenses/events-3.3.0/README @@ -0,0 +1,7 @@ +events (https://github.com/browserify/events) +--------------------------------------------- + + Version: 3.3.0 + From: 'browserify' (https://github.com/browserify) + License(s): + MIT (bundled/events-3.3.0/LICENSE) diff --git a/doc/licenses/events-3.3.0/dep-coordinates.txt b/doc/licenses/events-3.3.0/dep-coordinates.txt new file mode 100644 index 000000000..f644fcc1b --- /dev/null +++ b/doc/licenses/events-3.3.0/dep-coordinates.txt @@ -0,0 +1 @@ +events:3.3.0 diff --git a/doc/licenses/ieee754-1.2.1/LICENSE b/doc/licenses/ieee754-1.2.1/LICENSE new file mode 100644 index 000000000..5aac82c78 --- /dev/null +++ b/doc/licenses/ieee754-1.2.1/LICENSE @@ -0,0 +1,11 @@ +Copyright 2008 Fair Oaks Labs, Inc. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/doc/licenses/ieee754-1.2.1/README b/doc/licenses/ieee754-1.2.1/README new file mode 100644 index 000000000..06d856f16 --- /dev/null +++ b/doc/licenses/ieee754-1.2.1/README @@ -0,0 +1,7 @@ +ieee754 (https://github.com/feross/ieee754) +--------------------------------------------- + + Version: 1.2.1 + From: 'Feross Aboukhadijeh' (https://github.com/feross) + License(s): + MIT (bundled/ieee754-1.2.1/LICENSE) diff --git a/doc/licenses/ieee754-1.2.1/dep-coordinates.txt b/doc/licenses/ieee754-1.2.1/dep-coordinates.txt new file mode 100644 index 000000000..4fb7d7765 --- /dev/null +++ b/doc/licenses/ieee754-1.2.1/dep-coordinates.txt @@ -0,0 +1 @@ +ieee754:1.2.1 diff --git a/doc/licenses/inherits-2.0.4/LICENSE b/doc/licenses/inherits-2.0.4/LICENSE new file mode 100644 index 000000000..052085c43 --- /dev/null +++ b/doc/licenses/inherits-2.0.4/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/doc/licenses/inherits-2.0.4/README b/doc/licenses/inherits-2.0.4/README new file mode 100644 index 000000000..9d0b40126 --- /dev/null +++ b/doc/licenses/inherits-2.0.4/README @@ -0,0 +1,8 @@ +inherits (https://github.com/isaacs/inherits) +--------------------------------------------- + + Version: 2.0.4 + From: 'Isaac Z. Schlueter' (https://github.com/isaacs) + License(s): + ISC (bundled/inherits-2.0.4/LICENSE) + diff --git a/doc/licenses/inherits-2.0.4/dep-coordinates.txt b/doc/licenses/inherits-2.0.4/dep-coordinates.txt new file mode 100644 index 000000000..4a17c13e3 --- /dev/null +++ b/doc/licenses/inherits-2.0.4/dep-coordinates.txt @@ -0,0 +1 @@ +inherits:2.0.4 diff --git a/doc/licenses/isarray-1.0.0/LICENSE b/doc/licenses/isarray-1.0.0/LICENSE new file mode 100644 index 000000000..de3226673 --- /dev/null +++ b/doc/licenses/isarray-1.0.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/licenses/isarray-1.0.0/README b/doc/licenses/isarray-1.0.0/README new file mode 100644 index 000000000..3fde38c8c --- /dev/null +++ b/doc/licenses/isarray-1.0.0/README @@ -0,0 +1,8 @@ +isarray (https://github.com/juliangruber/isarray) +--------------------------------------------- + + Version: 1.0.0 + From: 'Julian Gruber' (https://github.com/juliangruber) + License(s): + MIT (bundled/isarray-1.0.0/LICENSE) + diff --git a/doc/licenses/isarray-1.0.0/dep-coordinates.txt b/doc/licenses/isarray-1.0.0/dep-coordinates.txt new file mode 100644 index 000000000..d444bdbd9 --- /dev/null +++ b/doc/licenses/isarray-1.0.0/dep-coordinates.txt @@ -0,0 +1 @@ +isarray:1.0.0 diff --git a/doc/licenses/process-nextick-args-2.0.1/README b/doc/licenses/process-nextick-args-2.0.1/README new file mode 100644 index 000000000..d7c0f6b3b --- /dev/null +++ b/doc/licenses/process-nextick-args-2.0.1/README @@ -0,0 +1,8 @@ +process-nextick-args (https://github.com/calvinmetcalf/process-nextick-args) +--------------------------------------------- + + Version: 2.0.1 + From: 'Calvin Metcalf' (https://github.com/calvinmetcalf) + License(s): + MIT (bundled/process-nextick-args-2.0.1/LICENSE) + diff --git a/doc/licenses/process-nextick-args-2.0.1/dep-coordinates.txt b/doc/licenses/process-nextick-args-2.0.1/dep-coordinates.txt new file mode 100644 index 000000000..7c68c4b48 --- /dev/null +++ b/doc/licenses/process-nextick-args-2.0.1/dep-coordinates.txt @@ -0,0 +1 @@ +process-nextick-args:2.0.1 diff --git a/doc/licenses/process-nextick-args-2.0.1/license.md b/doc/licenses/process-nextick-args-2.0.1/license.md new file mode 100644 index 000000000..c67e3532b --- /dev/null +++ b/doc/licenses/process-nextick-args-2.0.1/license.md @@ -0,0 +1,19 @@ +# Copyright (c) 2015 Calvin Metcalf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.** diff --git a/doc/licenses/readable-stream-2.3.7/LICENSE b/doc/licenses/readable-stream-2.3.7/LICENSE new file mode 100644 index 000000000..2873b3b2e --- /dev/null +++ b/doc/licenses/readable-stream-2.3.7/LICENSE @@ -0,0 +1,47 @@ +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" diff --git a/doc/licenses/readable-stream-2.3.7/README b/doc/licenses/readable-stream-2.3.7/README new file mode 100644 index 000000000..cb58f728b --- /dev/null +++ b/doc/licenses/readable-stream-2.3.7/README @@ -0,0 +1,8 @@ +readable-stream (https://github.com/nodejs/readable-stream) +--------------------------------------------- + + Version: 2.3.7 + From: 'Node.js' (https://github.com/nodejs) + License(s): + MIT (bundled/readable-stream-2.3.7/LICENSE) + diff --git a/doc/licenses/readable-stream-2.3.7/dep-coordinates.txt b/doc/licenses/readable-stream-2.3.7/dep-coordinates.txt new file mode 100644 index 000000000..f645a21bb --- /dev/null +++ b/doc/licenses/readable-stream-2.3.7/dep-coordinates.txt @@ -0,0 +1 @@ +readable-stream:2.3.7 diff --git a/doc/licenses/safe-buffer-5.1.2/LICENSE b/doc/licenses/safe-buffer-5.1.2/LICENSE new file mode 100644 index 000000000..0c068ceec --- /dev/null +++ b/doc/licenses/safe-buffer-5.1.2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/doc/licenses/safe-buffer-5.1.2/README b/doc/licenses/safe-buffer-5.1.2/README new file mode 100644 index 000000000..26cf5dc2f --- /dev/null +++ b/doc/licenses/safe-buffer-5.1.2/README @@ -0,0 +1,8 @@ +safe-buffer (https://github.com/feross/safe-buffer) +--------------------------------------------- + + Version: 5.1.2 + From: 'Feross Aboukhadijeh' (https://github.com/feross) + License(s): + MIT (bundled/safe-buffer-5.1.2/LICENSE) + diff --git a/doc/licenses/safe-buffer-5.1.2/dep-coordinates.txt b/doc/licenses/safe-buffer-5.1.2/dep-coordinates.txt new file mode 100644 index 000000000..594ab08c8 --- /dev/null +++ b/doc/licenses/safe-buffer-5.1.2/dep-coordinates.txt @@ -0,0 +1 @@ +safe-buffer:5.1.2 diff --git a/doc/licenses/setimmediate-1.0.5/LICENSE.txt b/doc/licenses/setimmediate-1.0.5/LICENSE.txt new file mode 100644 index 000000000..32b20de6a --- /dev/null +++ b/doc/licenses/setimmediate-1.0.5/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/licenses/setimmediate-1.0.5/README b/doc/licenses/setimmediate-1.0.5/README new file mode 100644 index 000000000..0fe5638ee --- /dev/null +++ b/doc/licenses/setimmediate-1.0.5/README @@ -0,0 +1,8 @@ +setImmediate.js (https://github.com/YuzuJS/setImmediate) +--------------------------------------------- + + Version: 1.0.5 + From: 'Yuzu (by Barnes & Noble Education)' (https://github.com/YuzuJS) + License(s): + MIT (bundled/setimmediate-1.0.5/LICENSE) + diff --git a/doc/licenses/setimmediate-1.0.5/dep-coordinates.txt b/doc/licenses/setimmediate-1.0.5/dep-coordinates.txt new file mode 100644 index 000000000..d2111b4e0 --- /dev/null +++ b/doc/licenses/setimmediate-1.0.5/dep-coordinates.txt @@ -0,0 +1 @@ +setimmediate:1.0.5 diff --git a/doc/licenses/stream-browserify-2.0.2/LICENSE b/doc/licenses/stream-browserify-2.0.2/LICENSE new file mode 100644 index 000000000..3e7d0c01e --- /dev/null +++ b/doc/licenses/stream-browserify-2.0.2/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/licenses/stream-browserify-2.0.2/README b/doc/licenses/stream-browserify-2.0.2/README new file mode 100644 index 000000000..8ab34a47f --- /dev/null +++ b/doc/licenses/stream-browserify-2.0.2/README @@ -0,0 +1,8 @@ +stream-browserify (https://github.com/browserify/stream-browserify) +--------------------------------------------- + + Version: 2.0.2 + From: 'browserify' (https://github.com/browserify) + License(s): + MIT (bundled/stream-browserify-2.0.2/LICENSE) + diff --git a/doc/licenses/stream-browserify-2.0.2/dep-coordinates.txt b/doc/licenses/stream-browserify-2.0.2/dep-coordinates.txt new file mode 100644 index 000000000..7bc88e94d --- /dev/null +++ b/doc/licenses/stream-browserify-2.0.2/dep-coordinates.txt @@ -0,0 +1 @@ +stream-browserify:2.0.2 diff --git a/doc/licenses/string_decoder-1.1.1/LICENSE b/doc/licenses/string_decoder-1.1.1/LICENSE new file mode 100644 index 000000000..2873b3b2e --- /dev/null +++ b/doc/licenses/string_decoder-1.1.1/LICENSE @@ -0,0 +1,47 @@ +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" diff --git a/doc/licenses/string_decoder-1.1.1/README b/doc/licenses/string_decoder-1.1.1/README new file mode 100644 index 000000000..a64b3299f --- /dev/null +++ b/doc/licenses/string_decoder-1.1.1/README @@ -0,0 +1,8 @@ +string_decoder (https://github.com/nodejs/string_decoder) +--------------------------------------------- + + Version: 1.1.1 + From: 'Node.js' (https://github.com/nodejs) + License(s): + MIT (bundled/string_decoder-1.1.1/LICENSE) + diff --git a/doc/licenses/string_decoder-1.1.1/dep-coordinates.txt b/doc/licenses/string_decoder-1.1.1/dep-coordinates.txt new file mode 100644 index 000000000..bbdc75db5 --- /dev/null +++ b/doc/licenses/string_decoder-1.1.1/dep-coordinates.txt @@ -0,0 +1 @@ +string_decoder:1.1.1 diff --git a/doc/licenses/timers-browserify-2.0.12/LICENSE.md b/doc/licenses/timers-browserify-2.0.12/LICENSE.md new file mode 100644 index 000000000..f2bdf520a --- /dev/null +++ b/doc/licenses/timers-browserify-2.0.12/LICENSE.md @@ -0,0 +1,23 @@ +# timers-browserify + +This project uses the [MIT](http://jryans.mit-license.org/) license: + + Copyright © 2012 J. Ryan Stinnett + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the “Software”), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. diff --git a/doc/licenses/timers-browserify-2.0.12/README b/doc/licenses/timers-browserify-2.0.12/README new file mode 100644 index 000000000..0e1eb4e8a --- /dev/null +++ b/doc/licenses/timers-browserify-2.0.12/README @@ -0,0 +1,8 @@ +timers-browserify (https://github.com/browserify/timers-browserify) +--------------------------------------------- + + Version: 2.0.12 + From: 'browserify' (https://github.com/browserify) + License(s): + MIT (bundled/timers-browserify-2.0.12/LICENSE) + diff --git a/doc/licenses/timers-browserify-2.0.12/dep-coordinates.txt b/doc/licenses/timers-browserify-2.0.12/dep-coordinates.txt new file mode 100644 index 000000000..b1dfbee66 --- /dev/null +++ b/doc/licenses/timers-browserify-2.0.12/dep-coordinates.txt @@ -0,0 +1 @@ +timers-browserify:2.0.12 diff --git a/doc/licenses/util-deprecate-1.0.2/LICENSE b/doc/licenses/util-deprecate-1.0.2/LICENSE new file mode 100644 index 000000000..6a60e8c22 --- /dev/null +++ b/doc/licenses/util-deprecate-1.0.2/LICENSE @@ -0,0 +1,24 @@ +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/licenses/util-deprecate-1.0.2/README b/doc/licenses/util-deprecate-1.0.2/README new file mode 100644 index 000000000..93844707b --- /dev/null +++ b/doc/licenses/util-deprecate-1.0.2/README @@ -0,0 +1,8 @@ +util-deprecate (https://github.com/TooTallNate/util-deprecate) +--------------------------------------------- + + Version: 1.0.2 + From: 'Nathan Rajlich' (https://github.com/TooTallNate) + License(s): + MIT (bundled/util-deprecate-1.0.2/LICENSE) + diff --git a/doc/licenses/util-deprecate-1.0.2/dep-coordinates.txt b/doc/licenses/util-deprecate-1.0.2/dep-coordinates.txt new file mode 100644 index 000000000..f8c30242a --- /dev/null +++ b/doc/licenses/util-deprecate-1.0.2/dep-coordinates.txt @@ -0,0 +1 @@ +util-deprecate:1.0.2 diff --git a/guacamole/src/main/frontend/package-lock.json b/guacamole/src/main/frontend/package-lock.json index a9f041274..32b9bee38 100644 --- a/guacamole/src/main/frontend/package-lock.json +++ b/guacamole/src/main/frontend/package-lock.json @@ -12,14 +12,19 @@ "angular-translate": "^2.19.0", "angular-translate-interpolation-messageformat": "^2.19.0", "angular-translate-loader-static-files": "^2.19.0", - "blob-polyfill": "^7.0.20220408", + "blob-polyfill": ">=7.0.20220408", + "csv": "^6.2.5", "datalist-polyfill": "^1.25.1", "file-saver": "^2.0.5", "jquery": "^3.6.4", "jstz": "^2.1.1", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "yaml": "^2.2.1" }, "devDependencies": { + "@babel/core": "^7.20.12", + "@babel/preset-env": "^7.20.2", + "babel-loader": "^8.3.0", "clean-webpack-plugin": "^4.0.0", "closure-webpack-plugin": "^2.6.1", "copy-webpack-plugin": "^5.1.2", @@ -34,6 +39,1797 @@ "webpack-cli": "^4.10.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.20.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz", + "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", + "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/generator/node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dev": true, + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", + "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz", + "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz", + "integrity": "sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.2.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz", + "integrity": "sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", + "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.10", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", + "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz", + "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.13", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz", + "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", + "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.20.7.tgz", + "integrity": "sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz", + "integrity": "sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz", + "integrity": "sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", + "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.11.tgz", + "integrity": "sha512-tA4N427a7fjf1P0/2I4ScsHGc5jcHPbb30xMbaTke2gxDuWpUfXDuX1FEymJwKk4tuGUvGcejAR6HdZVqmmPyw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz", + "integrity": "sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", + "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/template": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz", + "integrity": "sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", + "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.20.11.tgz", + "integrity": "sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-simple-access": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", + "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-identifier": "^7.19.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz", + "integrity": "sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", + "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "regenerator-transform": "^0.15.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", + "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", + "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.20.13", + "@babel/types": "^7.20.7", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", + "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -49,6 +1845,53 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -766,6 +2609,216 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/babel-loader/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/babel-loader/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader/node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/babel-loader/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1703,6 +3756,12 @@ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -1786,6 +3845,19 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-js-compat": { + "version": "3.27.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.27.2.tgz", + "integrity": "sha512-welaYuF7ZtbYKGrIy7y3eb40d37rG1FvzEOfe7hSLd2iD6duMDqUhRfSvCGyC46HhR6Y8JXXdZ2lnRUMkPBpvg==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2544,6 +4616,35 @@ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "dev": true }, + "node_modules/csv": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.5.tgz", + "integrity": "sha512-T+K0H7MIrlrnP6KxYKo3lK+uLl6OC2Gmwdd81TG/VdkhKvpatl35sR7tyRSpDLGl22y2T+q9KvNHnVtn4OAscQ==", + "dependencies": { + "csv-generate": "^4.2.1", + "csv-parse": "^5.3.3", + "csv-stringify": "^6.2.3", + "stream-transform": "^3.2.1" + }, + "engines": { + "node": ">= 0.1.90" + } + }, + "node_modules/csv-generate": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.1.tgz", + "integrity": "sha512-w6GFHjvApv6bcJ2xdi9JGsH6ZvUBfC+vUdfefnEzurXG6hMRwzkBLnhztU2H7v7+zfCk1I/knnQ+tGbgpxWrBw==" + }, + "node_modules/csv-parse": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.3.tgz", + "integrity": "sha512-kEWkAPleNEdhFNkHQpFHu9RYPogsFj3dx6bCxL847fsiLgidzWg0z/O0B1kVWMJUc5ky64zGp18LX2T3DQrOfw==" + }, + "node_modules/csv-stringify": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.2.3.tgz", + "integrity": "sha512-4qGjUMwnlaRc00gc2jrIYh2w/h1fo25B0mTuY9K8fBiIgtmCX3LcgUbrEGViL98Ci4Se/F5LFEtu8k+dItJVZQ==" + }, "node_modules/cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -3045,6 +5146,15 @@ "node": ">=4.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -3522,6 +5632,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -3590,6 +5709,15 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", @@ -4646,6 +6774,12 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz", "integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -4750,6 +6884,12 @@ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7202,6 +9342,39 @@ "node": ">= 0.10" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, "node_modules/regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -7231,6 +9404,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpu-core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", + "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsgen": "^0.7.1", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", + "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -7975,6 +10192,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "node_modules/stream-transform": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.1.tgz", + "integrity": "sha512-ApK+WTJ5bCOf0A2tlec1qhvr8bGEBM/sgXXB7mysdCYgZJO5DZeaV3h3G+g0HnAQ372P5IhiGqnW29zoLOfTzQ==" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -8399,6 +10621,15 @@ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==" }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -8492,6 +10723,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -9147,6 +11418,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "node_modules/yaml": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", + "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==", + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -9161,6 +11440,1261 @@ } }, "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.20.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz", + "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==", + "dev": true + }, + "@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", + "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", + "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz", + "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz", + "integrity": "sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.2.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dev": true, + "requires": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz", + "integrity": "sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", + "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.10", + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-replace-supers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", + "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "requires": { + "@babel/types": "^7.20.2" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + } + }, + "@babel/helpers": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz", + "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==", + "dev": true, + "requires": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.13", + "@babel/types": "^7.20.7" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz", + "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==", + "dev": true + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", + "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.7" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.20.7.tgz", + "integrity": "sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz", + "integrity": "sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz", + "integrity": "sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", + "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.11.tgz", + "integrity": "sha512-tA4N427a7fjf1P0/2I4ScsHGc5jcHPbb30xMbaTke2gxDuWpUfXDuX1FEymJwKk4tuGUvGcejAR6HdZVqmmPyw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz", + "integrity": "sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", + "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/template": "^7.20.7" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz", + "integrity": "sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", + "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.20.11.tgz", + "integrity": "sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-simple-access": "^7.20.2" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", + "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-identifier": "^7.19.1" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz", + "integrity": "sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", + "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "regenerator-transform": "^0.15.1" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", + "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/traverse": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", + "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.20.13", + "@babel/types": "^7.20.7", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", + "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -9173,6 +12707,44 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, "@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -9775,6 +13347,155 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, + "babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -10518,6 +14239,12 @@ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -10584,6 +14311,15 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.0.tgz", "integrity": "sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg==" }, + "core-js-compat": { + "version": "3.27.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.27.2.tgz", + "integrity": "sha512-welaYuF7ZtbYKGrIy7y3eb40d37rG1FvzEOfe7hSLd2iD6duMDqUhRfSvCGyC46HhR6Y8JXXdZ2lnRUMkPBpvg==", + "dev": true, + "requires": { + "browserslist": "^4.21.4" + } + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -11160,6 +14896,32 @@ } } }, + "csv": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.5.tgz", + "integrity": "sha512-T+K0H7MIrlrnP6KxYKo3lK+uLl6OC2Gmwdd81TG/VdkhKvpatl35sR7tyRSpDLGl22y2T+q9KvNHnVtn4OAscQ==", + "requires": { + "csv-generate": "^4.2.1", + "csv-parse": "^5.3.3", + "csv-stringify": "^6.2.3", + "stream-transform": "^3.2.1" + } + }, + "csv-generate": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.1.tgz", + "integrity": "sha512-w6GFHjvApv6bcJ2xdi9JGsH6ZvUBfC+vUdfefnEzurXG6hMRwzkBLnhztU2H7v7+zfCk1I/knnQ+tGbgpxWrBw==" + }, + "csv-parse": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.3.tgz", + "integrity": "sha512-kEWkAPleNEdhFNkHQpFHu9RYPogsFj3dx6bCxL847fsiLgidzWg0z/O0B1kVWMJUc5ky64zGp18LX2T3DQrOfw==" + }, + "csv-stringify": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.2.3.tgz", + "integrity": "sha512-4qGjUMwnlaRc00gc2jrIYh2w/h1fo25B0mTuY9K8fBiIgtmCX3LcgUbrEGViL98Ci4Se/F5LFEtu8k+dItJVZQ==" + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -11563,6 +15325,12 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -11935,6 +15703,12 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -11982,6 +15756,12 @@ "is-glob": "^4.0.1" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", @@ -12735,6 +16515,12 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz", "integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==" }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -12812,6 +16598,12 @@ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==" }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -14783,6 +18575,36 @@ "resolve": "^1.9.0" } }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -14803,6 +18625,43 @@ "functions-have-names": "^1.2.2" } }, + "regexpu-core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", + "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", + "dev": true, + "requires": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsgen": "^0.7.1", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsgen": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", + "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==", + "dev": true + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + } + } + }, "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -15414,6 +19273,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "stream-transform": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.1.tgz", + "integrity": "sha512-ApK+WTJ5bCOf0A2tlec1qhvr8bGEBM/sgXXB7mysdCYgZJO5DZeaV3h3G+g0HnAQ372P5IhiGqnW29zoLOfTzQ==" + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -15753,6 +19617,12 @@ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==" }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -15830,6 +19700,34 @@ "which-boxed-primitive": "^1.0.2" } }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -16342,6 +20240,11 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "yaml": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", + "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==" + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/guacamole/src/main/frontend/package.json b/guacamole/src/main/frontend/package.json index cb4982da0..617cf308b 100644 --- a/guacamole/src/main/frontend/package.json +++ b/guacamole/src/main/frontend/package.json @@ -12,13 +12,18 @@ "angular-translate-interpolation-messageformat": "^2.19.0", "angular-translate-loader-static-files": "^2.19.0", "blob-polyfill": ">=7.0.20220408", + "csv": "^6.2.5", "datalist-polyfill": "^1.25.1", "file-saver": "^2.0.5", "jquery": "^3.6.4", "jstz": "^2.1.1", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "yaml": "^2.2.1" }, "devDependencies": { + "@babel/core": "^7.20.12", + "@babel/preset-env": "^7.20.2", + "babel-loader": "^8.3.0", "clean-webpack-plugin": "^4.0.0", "closure-webpack-plugin": "^2.6.1", "copy-webpack-plugin": "^5.1.2", diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js index d1e78754c..51c423932 100644 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js @@ -126,6 +126,15 @@ angular.module('index').config(['$routeProvider', '$locationProvider', resolve : { routeToUserHomePage: routeToUserHomePage } }) + // Connection import page + .when('/settings/:dataSource/import', { + title : 'APP.NAME', + bodyClassName : 'settings', + templateUrl : 'app/settings/templates/settingsImport.html', + controller : 'importConnectionsController', + resolve : { updateCurrentToken: updateCurrentToken } + }) + // Management screen .when('/settings/:dataSource?/:tab', { title : 'APP.NAME', diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js index 3531177ea..a91c8d05d 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js @@ -154,6 +154,46 @@ angular.module('rest').factory('connectionService', ['$injector', } }; + + /** + * Makes a request to the REST API to create multiple connections, returning a + * a promise that can be used for processing the results of the call. This + * operation is atomic - if any errors are encountered during the connection + * creation process, the entire request will fail, and no connections will be + * created. + * + * @param {Connection[]} connections The connections to create. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * create operation is successful. + */ + service.createConnections = function createConnections(dataSource, connections) { + + // An object containing a PATCH operation to create each connection + const patchBody = connections.map(connection => ({ + op: "add", + path: "/", + value: connection + })); + + // Make a PATCH request to create the connections + return authenticationService.request({ + method : 'PATCH', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections', + data : patchBody + }) + + // Clear the cache + .then(function connectionUpdated(){ + cacheService.connections.removeAll(); + + // Clear users cache to force reload of permissions for this + // newly updated connection + cacheService.users.removeAll(); + }); + + } /** * Makes a request to the REST API to delete a connection, diff --git a/guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js b/guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js index bb82def73..c06493816 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js +++ b/guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js @@ -82,4 +82,4 @@ angular.module('rest').factory('RelatedObjectPatch', [function defineRelatedObje return RelatedObjectPatch; -}]); \ No newline at end of file +}]); diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js b/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js index b40f5f93a..12c368be3 100644 --- a/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js +++ b/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js @@ -20,7 +20,7 @@ /** * The controller for the session recording player page. */ -angular.module('manage').controller('connectionHistoryPlayerController', ['$scope', '$injector', +angular.module('settings').controller('connectionHistoryPlayerController', ['$scope', '$injector', function connectionHistoryPlayerController($scope, $injector) { // Required services diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js new file mode 100644 index 000000000..33b8914ad --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * The controller for the connection import page. + */ +angular.module('settings').controller('importConnectionsController', ['$scope', '$injector', + function importConnectionsController($scope, $injector) { + + // Required services + const connectionImportParseService = $injector.get('connectionImportParseService'); + const connectionService = $injector.get('connectionService'); + + function processData(type, data) { + + let requestBody; + + // Parse the data based on the provided mimetype + switch(type) { + + case "application/json": + case "text/json": + requestBody = connectionImportParseService.parseJSON(data); + break; + + case "text/csv": + requestBody = connectionImportParseService.parseCSV(data); + break; + + case "application/yaml": + case "application/x-yaml": + case "text/yaml": + case "text/x-yaml": + requestBody = connectionImportParseService.parseYAML(data); + break; + + } + + console.log(requestBody); + } + + $scope.upload = function() { + + const files = angular.element('#file')[0].files; + + if (files.length <= 0) { + console.error("TODO: This should be a proper error tho"); + return; + } + + // The file that the user uploaded + const file = files[0]; + + // Call processData when the data is ready + const reader = new FileReader(); + reader.onloadend = (e => processData(file.type, e.target.result)); + + // Read all the data into memory and call processData when done + reader.readAsBinaryString(file); + } + +}]); diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js b/guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js index a462d8715..9961d292f 100644 --- a/guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js +++ b/guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js @@ -20,7 +20,7 @@ /** * The controller for the general settings page. */ -angular.module('manage').controller('settingsController', ['$scope', '$injector', +angular.module('settings').controller('settingsController', ['$scope', '$injector', function settingsController($scope, $injector) { // Required services diff --git a/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js b/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js new file mode 100644 index 000000000..f2735acd6 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js @@ -0,0 +1,355 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* global _ */ + +import { parse as parseCSVData } from 'csv-parse/lib/sync' +import { parse as parseYAMLData } from 'yaml' + +/** + * A service for parsing user-provided JSON, YAML, or JSON connection data into + * an appropriate format for bulk uploading using the PATCH REST endpoint. + */ +angular.module('settings').factory('connectionImportParseService', + ['$injector', function connectionImportParseService($injector) { + + // Required types + const Connection = $injector.get('Connection'); + const ParseError = $injector.get('ParseError'); + const TranslatableMessage = $injector.get('TranslatableMessage'); + + // Required services + const $q = $injector.get('$q'); + const $routeParams = $injector.get('$routeParams'); + const schemaService = $injector.get('schemaService'); + const connectionGroupService = $injector.get('connectionGroupService'); + + const service = {}; + + /** + * Perform basic checks, common to all file types - namely that the parsed + * data is an array, and contains at least one connection entry. + * + * @throws {ParseError} + * An error describing the parsing failure, if one of the basic checks + * fails. + */ + function performBasicChecks(parsedData) { + + // Make sure that the file data parses to an array (connection list) + if (!(parsedData instanceof Array)) + throw new ParseError({ + message: 'Import data must be a list of connections', + translatableMessage: new TranslatableMessage({ + key: 'SETTINGS_CONNECTION_IMPORT.ERROR_ARRAY_REQUIRED' + }) + }); + + // Make sure that the connection list is not empty - contains at least + // one connection + if (!parsedData.length) + throw new ParseError({ + message: 'The provided CSV file is empty', + translatableMessage: new TranslatableMessage({ + key: 'SETTINGS_CONNECTION_IMPORT.ERROR_EMPTY_FILE' + }) + }); + } + + /** + * Convert a provided YAML representation of a connection list into a JSON + * string to be submitted to the PATCH REST endpoint. The returned JSON + * string will contain a PATCH operation to create each connection in the + * provided list. + * + * @param {String} yamlData + * The YAML-encoded connection list to convert to a PATCH request body. + * + * @return {Promise.} + * A promise resolving to an array of Connection objects, one for each + * connection in the provided CSV. + */ + service.parseYAML = function parseYAML(yamlData) { + + // Parse from YAML into a javascript array + const parsedData = parseYAMLData(yamlData); + + // Check that the data is the correct format, and not empty + performBasicChecks(parsedData); + + // Convert to an array of Connection objects and return + const deferredConnections = $q.defer(); + deferredConnections.resolve( + parsedData.map(connection => new Connection(connection))); + return deferredConnections.promise; + + }; + + /** + * Returns a promise that resolves to an object detailing the connection + * attributes for the current data source, as well as the connection + * paremeters for every protocol, for the current data source. + * + * The object that the promise will contain an "attributes" key that maps to + * a set of attribute names, and a "protocolParameters" key that maps to an + * object mapping protocol names to sets of parameter names for that protocol. + * + * The intended use case for this object is to determine if there is a + * connection parameter or attribute with a given name, by e.g. checking the + * path `.protocolParameters[protocolName]` to see if a protocol exists, + * checking the path `.protocolParameters[protocolName][fieldName]` to see + * if a parameter exists for a given protocol, or checking the path + * `.attributes[fieldName]` to check if a connection attribute exists. + * + * @returns {Promise.} + */ + function getFieldLookups() { + + // The current data source - the one that the connections will be + // imported into + const dataSource = $routeParams.dataSource; + + // Fetch connection attributes and protocols for the current data source + return $q.all({ + attributes : schemaService.getConnectionAttributes(dataSource), + protocols : schemaService.getProtocols(dataSource) + }) + .then(function connectionStructureRetrieved({attributes, protocols}) { + + return { + + // Translate the forms and fields into a flat map of attribute + // name to `true` boolean value + attributes: attributes.reduce( + (attributeMap, form) => { + form.fields.forEach( + field => attributeMap[field.name] = true); + return attributeMap + }, {}), + + // Translate the protocol definitions into a map of protocol + // name to map of field name to `true` boolean value + protocolParameters: _.mapValues( + protocols, protocol => protocol.connectionForms.reduce( + (protocolFieldMap, form) => { + form.fields.forEach( + field => protocolFieldMap[field.name] = true); + return protocolFieldMap; + }, {})) + }; + }); + + } + + + /** + * Returns a promise that resolves to an object mapping potential groups + * that might be encountered in an imported connection to group identifiers. + * + * The idea is that a user-provided import file might directly specify a + * parentIdentifier, or it might specify a named group path like "ROOT", + * "ROOT/parent", or "ROOT/parent/child". This object resolved by the + * promise returned from this function will map all of the above to the + * identifier of the appropriate group, if defined. + * + * @returns {Promise.} + */ + function getGroupLookups() { + + // The current data source - defines all the groups that the connections + // might be imported into + const dataSource = $routeParams.dataSource; + + const deferredGroupLookups = $q.defer(); + + connectionGroupService.getConnectionGroupTree(dataSource).then( + rootGroup => { + + const groupLookup = {}; + + // Add the specified group to the lookup, appending all specified + // prefixes, and then recursively call saveLookups for all children + // of the group, appending to the prefix for each level + function saveLookups(prefix, group) { + + // To get the path for the current group, add the name + const currentPath = prefix + group.name; + + // Add the current path to the lookup + groupLookup[currentPath] = group.identifier; + + // Add each child group to the lookup + const nextPrefix = currentPath + "/"; + _.forEach(group.childConnectionGroups, + childGroup => saveLookups(nextPrefix, childGroup)); + } + + // Start at the root group + saveLookups("", rootGroup); + + // Resolve with the now fully-populated lookups + deferredGroupLookups.resolve(groupLookup); + + }); + + return deferredGroupLookups.promise; + } + +/* +// Example Connection JSON +{ + "attributes": { + "failover-only": "true", + "guacd-encryption": "none", + "guacd-hostname": "potato", + "guacd-port": "1234", + "ksm-user-config-enabled": "true", + "max-connections": "1", + "max-connections-per-user": "1", + "weight": "1" + }, + "name": "Bloatato", + "parameters": { + "audio-servername": "heyoooooooo", + "clipboard-encoding": "", + "color-depth": "", + "create-recording-path": "", + "cursor": "remote", + "dest-host": "pooootato", + "dest-port": "4444", + "disable-copy": "", + "disable-paste": "true", + "enable-audio": "true", + "enable-sftp": "true", + "force-lossless": "true", + "hostname": "potato", + "password": "taste", + "port": "4321", + "read-only": "", + "recording-exclude-mouse": "", + "recording-exclude-output": "", + "recording-include-keys": "", + "recording-name": "heyoooooo", + "recording-path": "/path/to/goo", + "sftp-disable-download": "", + "sftp-disable-upload": "", + "sftp-hostname": "what what good sir", + "sftp-port": "", + "sftp-private-key": "lol i'll never tell", + "sftp-server-alive-interval": "", + "swap-red-blue": "true", + "username": "test", + "wol-send-packet": "", + "wol-udp-port": "", + "wol-wait-time": "" + }, + + // or a numeric identifier - we will probably want to offer a way to allow + // them to specify a path like "ROOT/parent/child" or just "/parent/child" or + // something like that + // TODO: Call the + "parentIdentifier": "ROOT", + "protocol": "vnc" + +} +*/ + + /** + * Convert a provided JSON representation of a connection list into a JSON + * string to be submitted to the PATCH REST endpoint. The returned JSON + * string will contain a PATCH operation to create each connection in the + * provided list. + * + * TODO: Describe disambiguation suffixes, e.g. hostname (parameter), and + * that we will accept without the suffix if it's unambigous. (or not? how about not?) + * + * @param {String} csvData + * The JSON-encoded connection list to convert to a PATCH request body. + * + * @return {Promise.} + * A promise resolving to an array of Connection objects, one for each + * connection in the provided CSV. + */ + service.parseCSV = function parseCSV(csvData) { + + const deferredConnections = $q.defer(); + + return $q.all({ + fieldLookups : getFieldLookups(), + groupLookups : getGroupLookups() + }) + .then(function lookupsReady({fieldLookups, groupLookups}) { + + const {attributes, protocolParameters} = fieldLookups; + + console.log({attributes, protocolParameters}, groupLookups); + + // Convert to an array of arrays, one per CSV row (including the header) + const parsedData = parseCSVData(csvData); + + // Slice off the header row to get the data rows + const connectionData = parsedData.slice(1); + + // Check that the provided CSV is not empty (the parser always + // returns an array) + performBasicChecks(connectionData); + + // The header row - an array of string header values + const header = parsedData[0]; + + // TODO: Connectionify this + deferredConnections.resolve(connectionData); + }); + + return deferredConnections.promise; + + }; + + /** + * Convert a provided JSON representation of a connection list into a JSON + * string to be submitted to the PATCH REST endpoint. The returned JSON + * string will contain a PATCH operation to create each connection in the + * provided list. + * + * @param {String} jsonData + * The JSON-encoded connection list to convert to a PATCH request body. + * + * @return {Promise.} + * A promise resolving to an array of Connection objects, one for each + * connection in the provided CSV. + */ + service.parseJSON = function parseJSON(jsonData) { + + // Parse from JSON into a javascript array + const parsedData = JSON.parse(yamlData); + + // Check that the data is the correct format, and not empty + performBasicChecks(parsedData); + + // Convert to an array of Connection objects and return + const deferredConnections = $q.defer(); + deferredConnections.resolve( + parsedData.map(connection => new Connection(connection))); + return deferredConnections.promise; + + }; + + return service; + +}]); diff --git a/guacamole/src/main/frontend/src/app/settings/styles/buttons.css b/guacamole/src/main/frontend/src/app/settings/styles/buttons.css index f925c5121..167e46a06 100644 --- a/guacamole/src/main/frontend/src/app/settings/styles/buttons.css +++ b/guacamole/src/main/frontend/src/app/settings/styles/buttons.css @@ -20,7 +20,8 @@ a.button.add-user, a.button.add-user-group, a.button.add-connection, -a.button.add-connection-group { +a.button.add-connection-group, +a.button.import-connections { font-size: 0.8em; padding-left: 1.8em; position: relative; @@ -29,7 +30,8 @@ a.button.add-connection-group { a.button.add-user::before, a.button.add-user-group::before, a.button.add-connection::before, -a.button.add-connection-group::before { +a.button.add-connection-group::before, +a.button.import-connections::before { content: ' '; position: absolute; @@ -59,3 +61,7 @@ a.button.add-connection::before { a.button.add-connection-group::before { background-image: url('images/action-icons/guac-group-add.svg'); } + +a.button.import-connections::before { + background-image: url('images/action-icons/guac-monitor-add-many.svg'); +} diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html index ca7bc30df..76ebeebec 100644 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html @@ -9,6 +9,10 @@
+ {{'SETTINGS_CONNECTIONS.ACTION_IMPORT_CONNECTIONS' | translate}} + {{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION' | translate}} diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html new file mode 100644 index 000000000..c9c54e10e --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html @@ -0,0 +1,11 @@ +
+ +
+

{{'SETTINGS_CONNECTION_IMPORT.HEADER' | translate}}

+ +
+ + + + +
diff --git a/guacamole/src/main/frontend/src/app/settings/types/ParseError.js b/guacamole/src/main/frontend/src/app/settings/types/ParseError.js new file mode 100644 index 000000000..b3d3da03a --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/types/ParseError.js @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Service which defines the ParseError class. + */ +angular.module('settings').factory('ParseError', [function defineParseError() { + + /** + * An error representing a parsing failure when attempting to convert + * user-provided data into a list of Connection objects. + * + * @constructor + * @param {ParseError|Object} [template={}] + * The object whose properties should be copied within the new + * ParseError. + */ + var ParseError = function ParseError(template) { + + // Use empty object by default + template = template || {}; + + /** + * A human-readable message describing the error that occurred. + * + * @type String + */ + this.message = template.message; + + /** + * A message which can be translated using the translation service, + * consisting of a translation key and optional set of substitution + * variables. + * + * @type TranslatableMessage + */ + this.translatableMessage = template.translatableMessage; + + }; + + return ParseError; + +}]); diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-monitor-add-many.svg b/guacamole/src/main/frontend/src/images/action-icons/guac-monitor-add-many.svg new file mode 100644 index 000000000..95ab14b76 --- /dev/null +++ b/guacamole/src/main/frontend/src/images/action-icons/guac-monitor-add-many.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 450a455bb..21812d376 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -5,7 +5,7 @@ "APP" : { "NAME" : "Apache Guacamole", - "VERSION" : "${project.version}", + "VERSION" : "1.5.0", "ACTION_ACKNOWLEDGE" : "OK", "ACTION_CANCEL" : "Cancel", @@ -906,6 +906,7 @@ "SETTINGS_CONNECTIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_IMPORT_CONNECTIONS" : "Import", "ACTION_NEW_CONNECTION" : "New Connection", "ACTION_NEW_CONNECTION_GROUP" : "New Group", "ACTION_NEW_SHARING_PROFILE" : "New Sharing Profile", @@ -922,6 +923,13 @@ }, + "SETTINGS_CONNECTION_IMPORT": { + + "HEADER": "Connection Import", + + "ERROR_ARRAY_REQUIRED": "The provided file must contain a list of connections" + }, + "SETTINGS_PREFERENCES" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", diff --git a/guacamole/src/main/frontend/webpack.config.js b/guacamole/src/main/frontend/webpack.config.js index 29bb8ddd5..dc6ad08cf 100644 --- a/guacamole/src/main/frontend/webpack.config.js +++ b/guacamole/src/main/frontend/webpack.config.js @@ -47,6 +47,22 @@ module.exports = { module: { rules: [ + // NOTE: This is required in order to parse ES2020 language features, + // like the optional chaining and nullish coalescing operators. It + // specifically needs to operate on the node-modules directory since + // Webpack 4 cannot handle such language features. + { + test: /\.js$/i, + use: { + loader: 'babel-loader', + options: { + presets: [ + ['@babel/preset-env'] + ] + } + } + }, + // Automatically extract imported CSS for later reference within separate CSS file { test: /\.css$/i, From fac76ef0cb1a3ff52bd5c03ba4aa632630175c02 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 1 Feb 2023 23:25:12 +0000 Subject: [PATCH 07/27] GUACAMOLE-926: Migrate import code to a dedicated module. --- .../importConnectionsController.js | 10 +- .../frontend/src/app/import/importModule.js | 24 ++++ .../import/services/connectionCSVService.js | 103 ++++++++++++++++++ .../services/connectionParseService.js} | 62 +++++------ .../templates/connectionImport.html} | 0 .../{settings => import}/types/ParseError.js | 2 +- .../src/app/index/config/indexRouteConfig.js | 4 +- .../app/rest/services/connectionService.js | 37 +++---- .../src/app/rest/types/DirectoryPatch.js | 95 ++++++++++++++++ .../app/rest/types/DirectoryPatchOutcome.js | 83 ++++++++++++++ 10 files changed, 361 insertions(+), 59 deletions(-) rename guacamole/src/main/frontend/src/app/{settings => import}/controllers/importConnectionsController.js (83%) create mode 100644 guacamole/src/main/frontend/src/app/import/importModule.js create mode 100644 guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js rename guacamole/src/main/frontend/src/app/{settings/services/connectionImportParseService.js => import/services/connectionParseService.js} (98%) rename guacamole/src/main/frontend/src/app/{settings/templates/settingsImport.html => import/templates/connectionImport.html} (100%) rename guacamole/src/main/frontend/src/app/{settings => import}/types/ParseError.js (95%) create mode 100644 guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js create mode 100644 guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js similarity index 83% rename from guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js rename to guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 33b8914ad..178d6c69d 100644 --- a/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -20,11 +20,11 @@ /** * The controller for the connection import page. */ -angular.module('settings').controller('importConnectionsController', ['$scope', '$injector', +angular.module('import').controller('importConnectionsController', ['$scope', '$injector', function importConnectionsController($scope, $injector) { // Required services - const connectionImportParseService = $injector.get('connectionImportParseService'); + const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); function processData(type, data) { @@ -36,18 +36,18 @@ angular.module('settings').controller('importConnectionsController', ['$scope', case "application/json": case "text/json": - requestBody = connectionImportParseService.parseJSON(data); + requestBody = connectionParseService.parseJSON(data); break; case "text/csv": - requestBody = connectionImportParseService.parseCSV(data); + requestBody = connectionParseService.parseCSV(data); break; case "application/yaml": case "application/x-yaml": case "text/yaml": case "text/x-yaml": - requestBody = connectionImportParseService.parseYAML(data); + requestBody = connectionParseService.parseYAML(data); break; } diff --git a/guacamole/src/main/frontend/src/app/import/importModule.js b/guacamole/src/main/frontend/src/app/import/importModule.js new file mode 100644 index 000000000..6480d62fc --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/importModule.js @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * The module for code supporting importing user-supplied files. Currently, only + * connection import is supported. + */ +angular.module('import', ['rest']); diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js new file mode 100644 index 000000000..361a15283 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* global _ */ + +/** + * A service for parsing user-provided CSV connection data for bulk import. + */ +angular.module('import').factory('connectionCSVService', + ['$injector', function connectionCSVService($injector) { + + // Required services + const $q = $injector.get('$q'); + const $routeParams = $injector.get('$routeParams'); + const schemaService = $injector.get('schemaService'); + + const service = {}; + + /** + * Returns a promise that resolves to an object detailing the connection + * attributes for the current data source, as well as the connection + * paremeters for every protocol, for the current data source. + * + * The object that the promise will contain an "attributes" key that maps to + * a set of attribute names, and a "protocolParameters" key that maps to an + * object mapping protocol names to sets of parameter names for that protocol. + * + * The intended use case for this object is to determine if there is a + * connection parameter or attribute with a given name, by e.g. checking the + * path `.protocolParameters[protocolName]` to see if a protocol exists, + * checking the path `.protocolParameters[protocolName][fieldName]` to see + * if a parameter exists for a given protocol, or checking the path + * `.attributes[fieldName]` to check if a connection attribute exists. + * + * @returns {Promise.} + */ + function getFieldLookups() { + + // The current data source - the one that the connections will be + // imported into + const dataSource = $routeParams.dataSource; + + // Fetch connection attributes and protocols for the current data source + return $q.all({ + attributes : schemaService.getConnectionAttributes(dataSource), + protocols : schemaService.getProtocols(dataSource) + }) + .then(function connectionStructureRetrieved({attributes, protocols}) { + + return { + + // Translate the forms and fields into a flat map of attribute + // name to `true` boolean value + attributes: attributes.reduce( + (attributeMap, form) => { + form.fields.forEach( + field => attributeMap[field.name] = true); + return attributeMap + }, {}), + + // Translate the protocol definitions into a map of protocol + // name to map of field name to `true` boolean value + protocolParameters: _.mapValues( + protocols, protocol => protocol.connectionForms.reduce( + (protocolFieldMap, form) => { + form.fields.forEach( + field => protocolFieldMap[field.name] = true); + return protocolFieldMap; + }, {})) + }; + }); + } + + /** + * + * + * @returns {Promise.>} + * A promise that will resolve to a function that translates a CSV data + * row (array of strings) to a connection object. + */ + service.getCSVTransformer = function getCSVTransformer(headerRow) { + + }; + + return service; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js similarity index 98% rename from guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js rename to guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index f2735acd6..1051b800b 100644 --- a/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -26,8 +26,8 @@ import { parse as parseYAMLData } from 'yaml' * A service for parsing user-provided JSON, YAML, or JSON connection data into * an appropriate format for bulk uploading using the PATCH REST endpoint. */ -angular.module('settings').factory('connectionImportParseService', - ['$injector', function connectionImportParseService($injector) { +angular.module('import').factory('connectionParseService', + ['$injector', function connectionParseService($injector) { // Required types const Connection = $injector.get('Connection'); @@ -71,35 +71,6 @@ angular.module('settings').factory('connectionImportParseService', }) }); } - - /** - * Convert a provided YAML representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. - * - * @param {String} yamlData - * The YAML-encoded connection list to convert to a PATCH request body. - * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided CSV. - */ - service.parseYAML = function parseYAML(yamlData) { - - // Parse from YAML into a javascript array - const parsedData = parseYAMLData(yamlData); - - // Check that the data is the correct format, and not empty - performBasicChecks(parsedData); - - // Convert to an array of Connection objects and return - const deferredConnections = $q.defer(); - deferredConnections.resolve( - parsedData.map(connection => new Connection(connection))); - return deferredConnections.promise; - - }; /** * Returns a promise that resolves to an object detailing the connection @@ -321,6 +292,35 @@ angular.module('settings').factory('connectionImportParseService', }; + /** + * Convert a provided YAML representation of a connection list into a JSON + * string to be submitted to the PATCH REST endpoint. The returned JSON + * string will contain a PATCH operation to create each connection in the + * provided list. + * + * @param {String} yamlData + * The YAML-encoded connection list to convert to a PATCH request body. + * + * @return {Promise.} + * A promise resolving to an array of Connection objects, one for each + * connection in the provided CSV. + */ + service.parseYAML = function parseYAML(yamlData) { + + // Parse from YAML into a javascript array + const parsedData = parseYAMLData(yamlData); + + // Check that the data is the correct format, and not empty + performBasicChecks(parsedData); + + // Convert to an array of Connection objects and return + const deferredConnections = $q.defer(); + deferredConnections.resolve( + parsedData.map(connection => new Connection(connection))); + return deferredConnections.promise; + + }; + /** * Convert a provided JSON representation of a connection list into a JSON * string to be submitted to the PATCH REST endpoint. The returned JSON diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html similarity index 100% rename from guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html rename to guacamole/src/main/frontend/src/app/import/templates/connectionImport.html diff --git a/guacamole/src/main/frontend/src/app/settings/types/ParseError.js b/guacamole/src/main/frontend/src/app/import/types/ParseError.js similarity index 95% rename from guacamole/src/main/frontend/src/app/settings/types/ParseError.js rename to guacamole/src/main/frontend/src/app/import/types/ParseError.js index b3d3da03a..21cd545d7 100644 --- a/guacamole/src/main/frontend/src/app/settings/types/ParseError.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseError.js @@ -20,7 +20,7 @@ /** * Service which defines the ParseError class. */ -angular.module('settings').factory('ParseError', [function defineParseError() { +angular.module('import').factory('ParseError', [function defineParseError() { /** * An error representing a parsing failure when attempting to convert diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js index 51c423932..dd9cc0637 100644 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js @@ -127,10 +127,10 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Connection import page - .when('/settings/:dataSource/import', { + .when('/import/:dataSource/connection', { title : 'APP.NAME', bodyClassName : 'settings', - templateUrl : 'app/settings/templates/settingsImport.html', + templateUrl : 'app/import/templates/connectionImport.html', controller : 'importConnectionsController', resolve : { updateCurrentToken: updateCurrentToken } }) diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js index a91c8d05d..053559530 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js @@ -156,41 +156,38 @@ angular.module('rest').factory('connectionService', ['$injector', }; /** - * Makes a request to the REST API to create multiple connections, returning a - * a promise that can be used for processing the results of the call. This - * operation is atomic - if any errors are encountered during the connection - * creation process, the entire request will fail, and no connections will be - * created. + * Makes a request to the REST API to apply a supplied list of connection + * patches, returning a promise that can be used for processing the results + * of the call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. * - * @param {Connection[]} connections The connections to create. + * @param {DirectoryPatch.[]} patches + * An array of patches to apply. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the - * create operation is successful. + * patch operation is successful. */ - service.createConnections = function createConnections(dataSource, connections) { + service.patchConnections = function patchConnections(dataSource, patches) { - // An object containing a PATCH operation to create each connection - const patchBody = connections.map(connection => ({ - op: "add", - path: "/", - value: connection - })); - - // Make a PATCH request to create the connections + // Make the PATCH request return authenticationService.request({ method : 'PATCH', url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections', - data : patchBody + data : patches }) // Clear the cache - .then(function connectionUpdated(){ + .then(function connectionsPatched(){ cacheService.connections.removeAll(); - // Clear users cache to force reload of permissions for this - // newly updated connection + // Clear users cache to force reload of permissions for any + // newly created or replaced connections cacheService.users.removeAll(); + }); } diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js new file mode 100644 index 000000000..96a2cb093 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Service which defines the DirectoryPatch class. + */ +angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch() { + + /** + * The object consumed by REST API calls when representing changes to an + * arbitrary set of directory-based objects. + * @constructor + * + * @template DirectoryObject + * The directory-based object type that this DirectoryPatch will + * operate on. + * + * @param {DirectoryObject|Object} [template={}] + * The object whose properties should be copied within the new + * DirectoryPatch. + */ + var DirectoryPatch = function DirectoryPatch(template) { + + // Use empty object by default + template = template || {}; + + /** + * The operation to apply to the objects indicated by the path. Valid + * operation values are defined within DirectoryPatch.Operation. + * + * @type {String} + */ + this.op = template.op; + + /** + * The path of the objects to modify. For creation of new objects, this + * should be "/". Otherwise, it should be "/{identifier}", specifying + * the identifier of the existing object being modified. + * + * @type {String} + * @default '/' + */ + this.path = template.path || '/'; + + /** + * The object being added or replaced, or the identifier of the object + * being removed. + * + * @type {DirectoryObject|String} + */ + this.value = template.value; + + }; + + /** + * All valid patch operations for directory-based objects. + */ + DirectoryPatch.Operation = { + + /** + * Adds the specified object to the relation. + */ + ADD : "add", + + /** + * Removes the specified object from the relation. + */ + REPLACE : "replace", + + /** + * Removes the specified object from the relation. + */ + REMOVE : "remove" + + }; + + return DirectoryPatch; + +}]); diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js new file mode 100644 index 000000000..4534dae13 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Service which defines the DirectoryPatchOutcome class. + */ +angular.module('rest').factory('DirectoryPatchOutcome', [ + function defineDirectoryPatchOutcome() { + + /** + * An object returned by a PATCH request to a directory REST API, + * representing the outcome associated with a particular patch in the + * request. This object can indicate either a successful or unsuccessful + * response. The error field is only meaningful for unsuccessful patches. + * @constructor + * + * @template DirectoryObject + * The directory-based object type that this DirectoryPatchOutcome + * represents a patch outcome for. + * + * @param {DirectoryObject|Object} [template={}] + * The object whose properties should be copied within the new + * DirectoryPatchOutcome. + */ + var DirectoryPatchOutcome = function DirectoryPatchOutcome(template) { + + // Use empty object by default + template = template || {}; + + /** + * The operation to apply to the objects indicated by the path. Valid + * operation values are defined within DirectoryPatch.Operation. + * + * @type {String} + */ + this.op = template.op; + + /** + * The path of the object operated on by the corresponding patch in the + * request. + * + * @type {String} + */ + this.path = template.path; + + /** + * The identifier of the object operated on by the corresponding patch + * in the request. If the object was newly created and the PATCH request + * did not fail, this will be the identifier of the newly created object. + * + * @type {String} + */ + this.identifier = template.identifier; + + /** + * The error message associated with the failure, if the patch failed to + * apply. + * + * @type {String} + */ + this.error = template.error; + + }; + + return DirectoryPatch; + +}]); From a6af634d868b11bff7da7c82dccf4ff5cece35ef Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 2 Feb 2023 01:56:33 +0000 Subject: [PATCH 08/27] GUACAMOLE-926: Implement logic for translating CSV rows to connection objects. --- .../importConnectionsController.js | 57 +++- .../import/services/connectionCSVService.js | 258 +++++++++++++++++- .../import/services/connectionParseService.js | 211 ++++---------- .../frontend/src/app/import/styles/import.css | 22 ++ .../import/templates/connectionImport.html | 14 +- .../src/app/import/types/ParseError.js | 19 +- .../frontend/src/app/index/indexModule.js | 1 + .../templates/settingsConnections.html | 2 +- .../main/frontend/src/translations/en.json | 32 ++- 9 files changed, 431 insertions(+), 185 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/import/styles/import.css diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 178d6c69d..73164a08c 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -25,42 +25,81 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // Required services const connectionParseService = $injector.get('connectionParseService'); - const connectionService = $injector.get('connectionService'); + const connectionService = $injector.get('connectionService'); + + // Required types + const ParseError = $injector.get('ParseError'); + const TranslatableMessage = $injector.get('TranslatableMessage'); + + function handleSuccess(data) { + console.log("OMG SUCCESS: ", data) + } + + // Set any caught error message to the scope for display + const handleError = error => { + console.error(error); + $scope.error = error; + } + + // Clear the current error + const clearError = () => delete $scope.error; function processData(type, data) { - - let requestBody; + + // The function that will process all the raw data and return a list of + // patches to be submitted to the API + let processDataCallback; // Parse the data based on the provided mimetype switch(type) { case "application/json": case "text/json": - requestBody = connectionParseService.parseJSON(data); + processDataCallback = connectionParseService.parseJSON; break; case "text/csv": - requestBody = connectionParseService.parseCSV(data); + processDataCallback = connectionParseService.parseCSV; break; case "application/yaml": case "application/x-yaml": case "text/yaml": case "text/x-yaml": - requestBody = connectionParseService.parseYAML(data); + processDataCallback = connectionParseService.parseYAML; break; - + + default: + handleError(new ParseError({ + message: 'Invalid file type: ' + type, + key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', + variables: { TYPE: type } + })); + return; } + + // Make the call to process the data into a series of patches + processDataCallback(data) - console.log(requestBody); + // Send the data off to be imported if parsing is successful + .then(handleSuccess) + + // Display any error found while parsing the file + .catch(handleError); } $scope.upload = function() { + + // Clear any error message from the previous upload attempt + clearError(); const files = angular.element('#file')[0].files; if (files.length <= 0) { - console.error("TODO: This should be a proper error tho"); + handleError(new ParseError({ + message: 'No file supplied', + key: 'CONNECTION_IMPORT.ERROR_NO_FILE_SUPPLIED' + })); return; } diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js index 361a15283..ee14f809b 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -19,16 +19,26 @@ /* global _ */ +// A suffix that indicates that a particular header refers to a parameter +const PARAMETER_SUFFIX = ' (parameter)'; + +// A suffix that indicates that a particular header refers to an attribute +const ATTRIBUTE_SUFFIX = ' (attribute)'; + /** * A service for parsing user-provided CSV connection data for bulk import. */ angular.module('import').factory('connectionCSVService', ['$injector', function connectionCSVService($injector) { + + // Required types + const ParseError = $injector.get('ParseError'); + const TranslatableMessage = $injector.get('TranslatableMessage'); // Required services - const $q = $injector.get('$q'); - const $routeParams = $injector.get('$routeParams'); - const schemaService = $injector.get('schemaService'); + const $q = $injector.get('$q'); + const $routeParams = $injector.get('$routeParams'); + const schemaService = $injector.get('schemaService'); const service = {}; @@ -88,14 +98,254 @@ angular.module('import').factory('connectionCSVService', } /** + * Given a CSV header row, create and return a promise that will resolve to + * a function that can take a CSV data row and return a connection object. + * If an error occurs while parsing a particular row, the resolved function + * will throw a ParseError describing the failure. * + * The provided CSV must contain columns for name and protocol. Optionally, + * the parentIdentifier of the target parent connection group, or a connection + * name path e.g. "ROOT/parent/child" may be included. Additionallty, + * connection parameters or attributes can be included. + * + * The names of connection attributes and parameters are not guaranteed to + * be mutually exclusive, so the CSV import format supports a distinguishing + * suffix. A column may be explicitly declared to be a parameter using a + * " (parameter)" suffix, or an attribute using an " (attribute)" suffix. + * No suffix is required if the name is unique across connections and + * attributes. + * + * If a parameter or attribute name conflicts with the standard + * "name", "protocol", "group", or "parentIdentifier" fields, the suffix is + * required. + * + * This returned object will be very similar to the Connection type, with + * the exception that a human-readable "group" field may be present. + * + * If a failure occurs while attempting to create the transformer function, + * the promise will be rejected with a ParseError describing the failure. * * @returns {Promise.>} * A promise that will resolve to a function that translates a CSV data - * row (array of strings) to a connection object. + * row (array of strings) to a connection object. */ service.getCSVTransformer = function getCSVTransformer(headerRow) { + // A promise that will be resolved with the transformer or rejected if + // an error occurs + const deferred = $q.defer(); + + getFieldLookups().then(({attributes, protocolParameters}) => { + + // All configuration required to generate a function that can + // transform a row of CSV into a connection object. + // NOTE: This is a single object instead of a collection of variables + // to ensure that no stale references are used - e.g. when one getter + // invokes another getter + const transformConfig = { + + // Callbacks for required fields + nameGetter: undefined, + protocolGetter: undefined, + + // Callbacks for a parent group ID or group path + groupGetter: _.noop, + parentIdentifierGetter: _.noop, + + // Callbacks that will generate either connection attributes or + // parameters. These callbacks will return a {type, name, value} + // object containing the type ("parameter" or "attribute"), + // the name of the attribute or parameter, and the corresponding + // value. + parameterOrAttributeGetters: [] + + }; + + // A set of all headers that have been seen so far. If any of these + // are duplicated, the CSV is invalid. + const headerSet = {}; + + // Iterate through the headers one by one + headerRow.forEach((rawHeader, index) => { + + // Trim to normalize all headers + const header = rawHeader.trim(); + + // Check if the header is duplicated + if (headerSet[header]) { + deferred.reject(new ParseError({ + message: 'Duplicate CSV Header: ' + header, + translatableMessage: new TranslatableMessage({ + key: 'CONNECTION_IMPORT.ERROR_DUPLICATE_CSV_HEADER', + variables: { HEADER: header } + }) + })); + return; + } + + // Mark that this particular header has already been seen + headerSet[header] = true; + + // A callback that returns the field at the current index + const fetchFieldAtIndex = row => row[index]; + + // Set up the name callback + if (header == 'name') + transformConfig.nameGetter = fetchFieldAtIndex; + + // Set up the protocol callback + else if (header == 'protocol') + transformConfig.protocolGetter = fetchFieldAtIndex; + + // Set up the group callback + else if (header == 'group') + transformConfig.groupGetter = fetchFieldAtIndex; + + // Set up the group parent ID callback + else if (header == 'parentIdentifier') + transformConfig.parentIdentifierGetter = fetchFieldAtIndex; + + // At this point, any other header might refer to a connection + // parameter or to an attribute + + // A field may be explicitly specified as a parameter + else if (header.endsWith(PARAMETER_SUFFIX)) { + + // Push as an explicit parameter getter + const parameterName = header.replace(PARAMETER_SUFFIX); + transformConfig.parameterOrAttributeGetters.push( + row => ({ + type: 'parameter', + name: parameterName, + value: fetchFieldAtIndex(row) + }) + ); + } + + // A field may be explicitly specified as a parameter + else if (header.endsWith(ATTRIBUTE_SUFFIX)) { + + // Push as an explicit attribute getter + const attributeName = header.replace(ATTRIBUTE_SUFFIX); + transformConfig.parameterOrAttributeGetters.push( + row => ({ + type: 'attribute', + name: parameterName, + value: fetchFieldAtIndex(row) + }) + ); + } + + // The field is ambiguous, either an attribute or parameter, + // so the getter will have to determine this for every row + else + transformConfig.parameterOrAttributeGetters.push(row => { + + // The name is just the value of the current header + const name = header; + + // The value is at the index that matches the position + // of the header + const value = fetchFieldAtIndex(row); + + // The protocol may determine whether a field is + // a parameter or an attribute (or both) + const protocol = transformConfig.protocolGetter(row); + + // Determine if the field refers to an attribute or a + // parameter (or both, which is an error) + const isAttribute = !!attributes[name]; + const isParameter = !!_.get( + protocolParameters, [protocol, name]); + + // If there is both an attribute and a protocol-specific + // parameter with the provided name, it's impossible to + // figure out which this should be + if (isAttribute && isParameter) + throw new ParseError({ + message: 'Ambiguous CSV Header: ' + header, + key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', + variables: { HEADER: header } + }); + + // It's neither an attribute or a parameter + else if (!isAttribute && !isParameter) + throw new ParseError({ + message: 'Invalid CSV Header: ' + header, + key: 'CONNECTION_IMPORT.ERROR_INVALID_CSV_HEADER', + variables: { HEADER: header } + }); + + // Choose the appropriate type + const type = isAttribute ? 'attributes' : 'parameters'; + + return { type, name, value }; + }); + }); + + // Fail if the name wasn't provided + if (!transformConfig.nameGetter) + return deferred.reject(new ParseError({ + message: 'The connection name must be provided', + key: 'CONNECTION_IMPORT.ERROR_REQUIRED_NAME' + })); + + // Fail if the protocol wasn't provided + if (!transformConfig.protocolGetter) + return deferred.reject(new ParseError({ + message: 'The connection protocol must be provided', + key: 'CONNECTION_IMPORT.ERROR_REQUIRED_PROTOCOL' + })); + + // The function to transform a CSV row into a connection object + deferred.resolve(function transformCSVRow(row) { + + const { + nameGetter, protocolGetter, + parentIdentifierGetter, groupGetter, + parameterOrAttributeGetters + } = transformConfig; + + // Set name and protocol + const name = nameGetter(row); + const protocol = protocolGetter(row); + + // Set the parent group ID and/or group path + const group = groupGetter && groupGetter(row); + const parentIdentifier = ( + parentIdentifierGetter && parentIdentifierGetter(row)); + + return { + + // Simple fields that are not protocol-specific + ...{ + name, + protocol, + parentIdentifier, + group + }, + + // Fields that might potentially be either attributes or + // parameters, depending on the protocol + ...parameterOrAttributeGetters.reduce((values, getter) => { + + // Determine the type, name, and value + const { type, name, value } = getter(row); + + // Set the value and continue on to the next attribute + // or parameter + values[type][name] = value; + return values; + + }, {parameters: {}, attributes: {}}) + + } + + }); + + }); + + return deferred.promise; }; return service; diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 1051b800b..82b7e0c6b 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -38,15 +38,17 @@ angular.module('import').factory('connectionParseService', const $q = $injector.get('$q'); const $routeParams = $injector.get('$routeParams'); const schemaService = $injector.get('schemaService'); + const connectionCSVService = $injector.get('connectionCSVService'); const connectionGroupService = $injector.get('connectionGroupService'); const service = {}; /** * Perform basic checks, common to all file types - namely that the parsed - * data is an array, and contains at least one connection entry. + * data is an array, and contains at least one connection entry. Returns an + * error if any of these basic checks fails. * - * @throws {ParseError} + * returns {ParseError} * An error describing the parsing failure, if one of the basic checks * fails. */ @@ -54,80 +56,20 @@ angular.module('import').factory('connectionParseService', // Make sure that the file data parses to an array (connection list) if (!(parsedData instanceof Array)) - throw new ParseError({ + return new ParseError({ message: 'Import data must be a list of connections', - translatableMessage: new TranslatableMessage({ - key: 'SETTINGS_CONNECTION_IMPORT.ERROR_ARRAY_REQUIRED' - }) + key: 'CONNECTION_IMPORT.ERROR_ARRAY_REQUIRED' }); // Make sure that the connection list is not empty - contains at least // one connection if (!parsedData.length) - throw new ParseError({ - message: 'The provided CSV file is empty', - translatableMessage: new TranslatableMessage({ - key: 'SETTINGS_CONNECTION_IMPORT.ERROR_EMPTY_FILE' - }) + return new ParseError({ + message: 'The provided file is empty', + key: 'CONNECTION_IMPORT.ERROR_EMPTY_FILE' }); } - /** - * Returns a promise that resolves to an object detailing the connection - * attributes for the current data source, as well as the connection - * paremeters for every protocol, for the current data source. - * - * The object that the promise will contain an "attributes" key that maps to - * a set of attribute names, and a "protocolParameters" key that maps to an - * object mapping protocol names to sets of parameter names for that protocol. - * - * The intended use case for this object is to determine if there is a - * connection parameter or attribute with a given name, by e.g. checking the - * path `.protocolParameters[protocolName]` to see if a protocol exists, - * checking the path `.protocolParameters[protocolName][fieldName]` to see - * if a parameter exists for a given protocol, or checking the path - * `.attributes[fieldName]` to check if a connection attribute exists. - * - * @returns {Promise.} - */ - function getFieldLookups() { - - // The current data source - the one that the connections will be - // imported into - const dataSource = $routeParams.dataSource; - - // Fetch connection attributes and protocols for the current data source - return $q.all({ - attributes : schemaService.getConnectionAttributes(dataSource), - protocols : schemaService.getProtocols(dataSource) - }) - .then(function connectionStructureRetrieved({attributes, protocols}) { - - return { - - // Translate the forms and fields into a flat map of attribute - // name to `true` boolean value - attributes: attributes.reduce( - (attributeMap, form) => { - form.fields.forEach( - field => attributeMap[field.name] = true); - return attributeMap - }, {}), - - // Translate the protocol definitions into a map of protocol - // name to map of field name to `true` boolean value - protocolParameters: _.mapValues( - protocols, protocol => protocol.connectionForms.reduce( - (protocolFieldMap, form) => { - form.fields.forEach( - field => protocolFieldMap[field.name] = true); - return protocolFieldMap; - }, {})) - }; - }); - - } - /** * Returns a promise that resolves to an object mapping potential groups @@ -181,68 +123,9 @@ angular.module('import').factory('connectionParseService', return deferredGroupLookups.promise; } - -/* -// Example Connection JSON -{ - "attributes": { - "failover-only": "true", - "guacd-encryption": "none", - "guacd-hostname": "potato", - "guacd-port": "1234", - "ksm-user-config-enabled": "true", - "max-connections": "1", - "max-connections-per-user": "1", - "weight": "1" - }, - "name": "Bloatato", - "parameters": { - "audio-servername": "heyoooooooo", - "clipboard-encoding": "", - "color-depth": "", - "create-recording-path": "", - "cursor": "remote", - "dest-host": "pooootato", - "dest-port": "4444", - "disable-copy": "", - "disable-paste": "true", - "enable-audio": "true", - "enable-sftp": "true", - "force-lossless": "true", - "hostname": "potato", - "password": "taste", - "port": "4321", - "read-only": "", - "recording-exclude-mouse": "", - "recording-exclude-output": "", - "recording-include-keys": "", - "recording-name": "heyoooooo", - "recording-path": "/path/to/goo", - "sftp-disable-download": "", - "sftp-disable-upload": "", - "sftp-hostname": "what what good sir", - "sftp-port": "", - "sftp-private-key": "lol i'll never tell", - "sftp-server-alive-interval": "", - "swap-red-blue": "true", - "username": "test", - "wol-send-packet": "", - "wol-udp-port": "", - "wol-wait-time": "" - }, - - // or a numeric identifier - we will probably want to offer a way to allow - // them to specify a path like "ROOT/parent/child" or just "/parent/child" or - // something like that - // TODO: Call the - "parentIdentifier": "ROOT", - "protocol": "vnc" - -} -*/ /** - * Convert a provided JSON representation of a connection list into a JSON + * Convert a provided CSV representation of a connection list into a JSON * string to be submitted to the PATCH REST endpoint. The returned JSON * string will contain a PATCH operation to create each connection in the * provided list. @@ -259,36 +142,44 @@ angular.module('import').factory('connectionParseService', */ service.parseCSV = function parseCSV(csvData) { - const deferredConnections = $q.defer(); + // Convert to an array of arrays, one per CSV row (including the header) + // NOTE: skip_empty_lines is required, or a trailing newline will error + let parsedData; + try { + parsedData = parseCSVData(csvData, {skip_empty_lines: true}); + } - return $q.all({ - fieldLookups : getFieldLookups(), - groupLookups : getGroupLookups() - }) - .then(function lookupsReady({fieldLookups, groupLookups}) { + // If the CSV parser throws an error, reject with that error. No + // translation key will be available here. + catch(error) { + console.error(error); + const deferred = $q.defer(); + deferred.reject(error); + return deferred.promise; + } + + // Slice off the header row to get the data rows + const connectionData = parsedData.slice(1); + + // Check that the provided CSV is not empty (the parser always + // returns an array) + const checkError = performBasicChecks(connectionData); + if (checkError) { + const deferred = $q.defer(); + deferred.reject(checkError); + return deferred.promise; + } - const {attributes, protocolParameters} = fieldLookups; - - console.log({attributes, protocolParameters}, groupLookups); - - // Convert to an array of arrays, one per CSV row (including the header) - const parsedData = parseCSVData(csvData); - - // Slice off the header row to get the data rows - const connectionData = parsedData.slice(1); - - // Check that the provided CSV is not empty (the parser always - // returns an array) - performBasicChecks(connectionData); - - // The header row - an array of string header values - const header = parsedData[0]; - - // TODO: Connectionify this - deferredConnections.resolve(connectionData); - }); + // The header row - an array of string header values + const header = parsedData[0]; - return deferredConnections.promise; + return connectionCSVService.getCSVTransformer(header).then( + + // If the transformer was successfully generated, apply it to the + // data rows + // TODO: Also apply the group -> parentIdentifier transform + csvTransformer => connectionData.map(csvTransformer) + ); }; @@ -303,7 +194,7 @@ angular.module('import').factory('connectionParseService', * * @return {Promise.} * A promise resolving to an array of Connection objects, one for each - * connection in the provided CSV. + * connection in the provided YAML. */ service.parseYAML = function parseYAML(yamlData) { @@ -311,7 +202,9 @@ angular.module('import').factory('connectionParseService', const parsedData = parseYAMLData(yamlData); // Check that the data is the correct format, and not empty - performBasicChecks(parsedData); + const checkError = performBasicChecks(connectionData); + if (checkError) + return $q.defer().reject(checkError); // Convert to an array of Connection objects and return const deferredConnections = $q.defer(); @@ -332,7 +225,7 @@ angular.module('import').factory('connectionParseService', * * @return {Promise.} * A promise resolving to an array of Connection objects, one for each - * connection in the provided CSV. + * connection in the provided JSON. */ service.parseJSON = function parseJSON(jsonData) { @@ -340,7 +233,9 @@ angular.module('import').factory('connectionParseService', const parsedData = JSON.parse(yamlData); // Check that the data is the correct format, and not empty - performBasicChecks(parsedData); + const checkError = performBasicChecks(connectionData); + if (checkError) + return $q.defer().reject(checkError); // Convert to an array of Connection objects and return const deferredConnections = $q.defer(); diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css new file mode 100644 index 000000000..f5551bb42 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +.import .parseError { + color: red; +} \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index c9c54e10e..39b12fece 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -1,11 +1,23 @@
-

{{'SETTINGS_CONNECTION_IMPORT.HEADER' | translate}}

+

{{'CONNECTION_IMPORT.HEADER' | translate}}

+ + +

+ + +

+ {{error.message}} +

+
diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseError.js b/guacamole/src/main/frontend/src/app/import/types/ParseError.js index 21cd545d7..1e4ce478e 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseError.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseError.js @@ -44,13 +44,22 @@ angular.module('import').factory('ParseError', [function defineParseError() { this.message = template.message; /** - * A message which can be translated using the translation service, - * consisting of a translation key and optional set of substitution - * variables. + * The key associated with the translation string that used when + * displaying this message. * - * @type TranslatableMessage + * @type String */ - this.translatableMessage = template.translatableMessage; + this.key = template.key; + + /** + * The object which should be passed through to the translation service + * for the sake of variable substitution. Each property of the provided + * object will be substituted for the variable of the same name within + * the translation string. + * + * @type Object + */ + this.variables = template.variables; }; diff --git a/guacamole/src/main/frontend/src/app/index/indexModule.js b/guacamole/src/main/frontend/src/app/index/indexModule.js index e0281f439..2bcc94527 100644 --- a/guacamole/src/main/frontend/src/app/index/indexModule.js +++ b/guacamole/src/main/frontend/src/app/index/indexModule.js @@ -35,6 +35,7 @@ angular.module('index', [ 'client', 'clipboard', 'home', + 'import', 'login', 'manage', 'navigation', diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html index 76ebeebec..0718307a1 100644 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html @@ -11,7 +11,7 @@ {{'SETTINGS_CONNECTIONS.ACTION_IMPORT_CONNECTIONS' | translate}} + href="#/import/{{dataSource | escape}}/connection/">{{'SETTINGS_CONNECTIONS.ACTION_IMPORT_CONNECTIONS' | translate}} Date: Sat, 4 Feb 2023 02:05:17 +0000 Subject: [PATCH 09/27] GUACAMOLE-926: Translate to API patches in patch service. --- .../import/services/connectionParseService.js | 146 +++++++++++++++--- .../main/frontend/src/translations/en.json | 5 +- 2 files changed, 124 insertions(+), 27 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 82b7e0c6b..322e22c15 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -31,6 +31,7 @@ angular.module('import').factory('connectionParseService', // Required types const Connection = $injector.get('Connection'); + const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); const TranslatableMessage = $injector.get('TranslatableMessage'); @@ -124,6 +125,62 @@ angular.module('import').factory('connectionParseService', return deferredGroupLookups.promise; } + /** + * Returns a promise that will resolve to a transformer function that will + * take an object that may a "group" field, replacing it if present with a + * "parentIdentifier". If both a "group" and "parentIdentifier" field are + * present on the provided object, or if no group exists at the specified + * path, the function will throw a ParseError describing the failure. + * + * @returns {Promise.>} + * A promise that will resolve to a function that will transform a + * "group" field into a "parentIdentifier" field if possible. + */ + function getGroupTransformer() { + return getGroupLookups().then(lookups => connection => { + + // If there's no group to translate, do nothing + if (!connection.group) + return; + + // If both are specified, the parent group is ambigious + if (connection.parentIdentifier) + throw new ParseError({ + message: 'Only one of group or parentIdentifier can be set', + key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + }); + + // Look up the parent identifier for the specified group path + const identifier = lookups[connection.group]; + + // If the group doesn't match anything in the tree + if (!identifier) + throw new ParseError({ + message: 'No group found named: ' + connection.group, + key: 'CONNECTION_IMPORT.ERROR_INVALID_GROUP', + variables: { GROUP: connection.group } + }); + + // Set the parent identifier now that it's known + return { + ...connection, + parentIdentifier: identifier + }; + + }); + } + + // Translate a given javascript object to a full-fledged Connection + const connectionTransformer = connection => new Connection(connection); + + // Translate a Connection object to a patch requesting the creation of said + // Connection + const patchTransformer = connection => new DirectoryPatch({ + op: 'add', + path: '/', + value: connection + }); + /** * Convert a provided CSV representation of a connection list into a JSON * string to be submitted to the PATCH REST endpoint. The returned JSON @@ -172,14 +229,29 @@ angular.module('import').factory('connectionParseService', // The header row - an array of string header values const header = parsedData[0]; - - return connectionCSVService.getCSVTransformer(header).then( - - // If the transformer was successfully generated, apply it to the - // data rows - // TODO: Also apply the group -> parentIdentifier transform - csvTransformer => connectionData.map(csvTransformer) - ); + + return $q.all({ + csvTransformer : connectionCSVService.getCSVTransformer(header), + groupTransformer : getGroupTransformer() + }) + + // Transform the rows from the CSV file to an array of API patches + .then(({csvTransformer, groupTransformer}) => connectionData.map( + dataRow => { + + // Translate the raw CSV data to a javascript object + let connectionObject = csvTransformer(dataRow); + + // Translate the group on the object to a parentIdentifier + connectionObject = groupTransformer(connectionObject); + + // Translate to a full-fledged Connection + const connection = connectionTransformer(connectionObject); + + // Finally, translate to a patch for creating the connection + return patchTransformer(connection); + + })); }; @@ -203,14 +275,26 @@ angular.module('import').factory('connectionParseService', // Check that the data is the correct format, and not empty const checkError = performBasicChecks(connectionData); - if (checkError) - return $q.defer().reject(checkError); - - // Convert to an array of Connection objects and return - const deferredConnections = $q.defer(); - deferredConnections.resolve( - parsedData.map(connection => new Connection(connection))); - return deferredConnections.promise; + if (checkError) { + const deferred = $q.defer(); + deferred.reject(checkError); + return deferred.promise; + } + + // Transform the data from the YAML file to an array of API patches + return getGroupTransformer().then( + groupTransformer => parsedData.map(connectionObject => { + + // Translate the group on the object to a parentIdentifier + connectionObject = groupTransformer(connectionObject); + + // Translate to a full-fledged Connection + const connection = connectionTransformer(connectionObject); + + // Finally, translate to a patch for creating the connection + return patchTransformer(connection); + + })); }; @@ -231,17 +315,29 @@ angular.module('import').factory('connectionParseService', // Parse from JSON into a javascript array const parsedData = JSON.parse(yamlData); - + // Check that the data is the correct format, and not empty const checkError = performBasicChecks(connectionData); - if (checkError) - return $q.defer().reject(checkError); - - // Convert to an array of Connection objects and return - const deferredConnections = $q.defer(); - deferredConnections.resolve( - parsedData.map(connection => new Connection(connection))); - return deferredConnections.promise; + if (checkError) { + const deferred = $q.defer(); + deferred.reject(checkError); + return deferred.promise; + } + + // Transform the data from the YAML file to an array of API patches + return getGroupTransformer().then( + groupTransformer => parsedData.map(connectionObject => { + + // Translate the group on the object to a parentIdentifier + connectionObject = groupTransformer(connectionObject); + + // Translate to a full-fledged Connection + const connection = connectionTransformer(connectionObject); + + // Finally, translate to a patch for creating the connection + return patchTransformer(connection); + + })); }; diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 89647a491..98b983b9f 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -197,11 +197,12 @@ "ERROR_EMPTY_FILE": "The provided file is empty", "ERROR_INVALID_CSV_HEADER": "Invalid CSV Header \"{HEADER}\" is neither an attribute or parameter", + "ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found", "ERROR_INVALID_FILE_TYPE": "Invalid import file type \"{TYPE}\"", "ERROR_NO_FILE_SUPPLIED": "Please select a file to import", - "ERROR_REQUIRED_GROUP": - "Either group or parentIdentifier must be specified, but not both", + "ERROR_AMBIGUOUS_PARENT_GROUP": + "Both group and parentIdentifier may be not specified at the same time", "ERROR_REQUIRED_PROTOCOL": "No connection protocol found in the provided file", "ERROR_REQUIRED_NAME": From 314adf6c235f59feaa117132424118fabe82e0f1 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 7 Feb 2023 00:31:06 +0000 Subject: [PATCH 10/27] GUACAMOLE-926: Remove patch update functionality. It's needed for batch import, and it's a can of worms. --- .../ActiveConnectionService.java | 8 -- .../jdbc/base/DirectoryObjectService.java | 18 ---- .../base/ModeledDirectoryObjectService.java | 14 --- .../guacamole/auth/jdbc/base/ObjectModel.java | 29 +----- .../jdbc/connection/ConnectionDirectory.java | 14 +-- .../auth/jdbc/connection/ConnectionModel.java | 5 + .../jdbc/connection/ConnectionService.java | 7 +- .../connectiongroup/ConnectionGroupModel.java | 6 ++ .../ConnectionGroupService.java | 7 +- .../sharingprofile/SharingProfileModel.java | 6 ++ .../sharingprofile/SharingProfileService.java | 7 +- .../guacamole/auth/jdbc/user/UserService.java | 4 - .../auth/jdbc/usergroup/UserGroupService.java | 4 - .../rest/directory/DirectoryResource.java | 94 ++++--------------- 14 files changed, 39 insertions(+), 184 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java index 80c75dbbb..046cee1e2 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java @@ -166,14 +166,6 @@ public class ActiveConnectionService } - @Override - public void updateExternalObject(ModeledAuthenticatedUser user, ActiveConnection object) throws GuacamoleException { - - // Updating active connections is not implemented - throw new GuacamoleSecurityException("Permission denied."); - - } - /** * Retrieve the permission set for the specified user that relates * to access to active connections. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java index 29d40cc49..590e01e9f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java @@ -116,24 +116,6 @@ public interface DirectoryObjectService { void deleteObject(ModeledAuthenticatedUser user, String identifier) throws GuacamoleException; - /** - * Updates the object corresponding to the given external representation, - * applying any changes that have been made. If no such object exists, - * this function has no effect. - * - * @param user - * The user updating the object. - * - * @param object - * The external object to apply updates from. - * - * @throws GuacamoleException - * If the user lacks permission to update the object, or an error - * occurs while updating the object. - */ - void updateExternalObject(ModeledAuthenticatedUser user, ExternalType object) - throws GuacamoleException; - /** * Updates the given object, applying any changes that have been made. If * no such object exists, this function has no effect. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java index 75c25926b..80bc7eb5c 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java @@ -511,20 +511,6 @@ public abstract class ModeledDirectoryObjectService getIdentifiers(ModeledAuthenticatedUser user) throws GuacamoleException { diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java index 69eb1fd36..c3052b1b4 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java @@ -21,8 +21,6 @@ package org.apache.guacamole.auth.jdbc.base; import java.util.Collection; -import org.apache.guacamole.GuacamoleException; - /** * Object representation of a Guacamole object, such as a user or connection, * as represented in the database. @@ -77,7 +75,7 @@ public abstract class ObjectModel { * * @return * The ID of this object in the database, or null if this object was - * not retrieved from or intended to update the database. + * not retrieved from the database. */ public Integer getObjectID() { return objectID; @@ -93,31 +91,6 @@ public abstract class ObjectModel { this.objectID = objectID; } - /** - * Given a text identifier, attempt to convert to an integer database ID. - * If the identifier is valid, the database ID will be set to this value. - * Otherwise, a GuacamoleException will be thrown. - * - * @param identifier - * The identifier to convert to an integer and set on the database - * model, if valid. - * - * @throws GuacamoleException - * If the provided identifier is not a valid integer. - */ - public void setObjectID(String identifier) throws GuacamoleException { - - // Try to convert the provided identifier to an integer ID - try { - setObjectID(Integer.parseInt(identifier)); - } - - catch (NumberFormatException e) { - throw new GuacamoleException( - "Database identifiers must be integers."); - } - } - /** * Returns a map of attribute name/value pairs for all attributes associated * with this model which do not have explicit mappings to actual model diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index b2f3e2bea..3e364f509 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -68,18 +68,8 @@ public class ConnectionDirectory extends JDBCDirectory { @Override @Transactional public void update(Connection object) throws GuacamoleException { - - // If the provided connection is already an internal type, update - // using the internal method - if (object instanceof ModeledConnection) - connectionService.updateObject( - getCurrentUser(), (ModeledConnection) object); - - // If the type is not already the expected internal type, use the - // external update method - else - connectionService.updateExternalObject(getCurrentUser(), object); - + ModeledConnection connection = (ModeledConnection) object; + connectionService.updateObject(getCurrentUser(), connection); } @Override diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java index 03413650b..da454025d 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java @@ -387,4 +387,9 @@ public class ConnectionModel extends ChildObjectModel { } + @Override + public void setIdentifier(String identifier) { + throw new UnsupportedOperationException("Connection identifiers are derived from IDs. They cannot be set."); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java index cdf2afb76..b3ed89ce8 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java @@ -110,19 +110,14 @@ public class ConnectionService extends ModeledChildDirectoryObjectService addedObjects = new ArrayList<>(); - Collection updatedObjects = new ArrayList<>(); Collection removedIdentifiers = new ArrayList<>(); // A list of all responses associated with the successful @@ -527,64 +526,6 @@ public abstract class DirectoryResource updatedIterator = updatedObjects.iterator(); - while (updatedIterator.hasNext()) { - - InternalType internal = updatedIterator.next(); - fireDirectorySuccessEvent( - DirectoryEvent.Operation.UPDATE, - internal.getIdentifier(), internal); - - } - // Fire directory success events for each removed object Iterator removedIterator = removedIdentifiers.iterator(); while (removedIterator.hasNext()) { String identifier = removedIterator.next(); fireDirectorySuccessEvent( - DirectoryEvent.Operation.UPDATE, + DirectoryEvent.Operation.REMOVE, identifier, null); } From 761438e02d4a86a4b12269f2494a431860cfa794 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 9 Feb 2023 01:51:14 +0000 Subject: [PATCH 11/27] GUACAMOLE-926: Also accept user and group identifiers to post in a seperate request. --- .../import/services/connectionCSVService.js | 138 ++++++-- .../import/services/connectionParseService.js | 322 +++++++++++------- .../src/app/import/types/ParseError.js | 2 +- .../src/app/import/types/ParseResult.js | 87 +++++ .../src/app/import/types/ProtoConnection.js | 111 ++++++ .../main/frontend/src/translations/en.json | 4 + 6 files changed, 522 insertions(+), 142 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/import/types/ParseResult.js create mode 100644 guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js index ee14f809b..a104cdd46 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -33,6 +33,7 @@ angular.module('import').factory('connectionCSVService', // Required types const ParseError = $injector.get('ParseError'); + const ProtoConnection = $injector.get('ProtoConnection'); const TranslatableMessage = $injector.get('TranslatableMessage'); // Required services @@ -43,7 +44,7 @@ angular.module('import').factory('connectionCSVService', const service = {}; /** - * Returns a promise that resolves to an object detailing the connection + * Returns a promise that resolves to a object detailing the connection * attributes for the current data source, as well as the connection * paremeters for every protocol, for the current data source. * @@ -96,12 +97,66 @@ angular.module('import').factory('connectionCSVService', }; }); } + + /** + * Split a raw user-provided, semicolon-seperated list of identifiers into + * an array of identifiers. If identifiers contain semicolons, they can be + * escaped with backslashes, and backslashes can also be escaped using other + * backslashes. + * + * @param {type} rawIdentifiers + * The raw string value as fetched from the CSV. + * + * @returns {Array.} + * An array of identifier values. + */ + function splitIdentifiers(rawIdentifiers) { + + // Keep track of whether a backslash was seen + let escaped = false; + + return _.reduce(rawIdentifiers, (identifiers, ch) => { + + // The current identifier will be the last one in the final list + let identifier = identifiers[identifiers.length - 1]; + + // If a semicolon is seen, set the "escaped" flag and continue + // to the next character + if (!escaped && ch == '\\') { + escaped = true; + return identifiers; + } + + // End the current identifier and start a new one if there's an + // unescaped semicolon + else if (!escaped && ch == ';') { + identifiers.push(''); + return identifiers; + } + + // In all other cases, just append to the identifier + else { + identifier += ch; + escaped = false; + } + + // Save the updated identifier to the list + identifiers[identifiers.length - 1] = identifier; + + return identifiers; + + }, ['']) + + // Filter out any 0-length (empty) identifiers + .filter(identifier => identifier.length); + + } /** * Given a CSV header row, create and return a promise that will resolve to - * a function that can take a CSV data row and return a connection object. - * If an error occurs while parsing a particular row, the resolved function - * will throw a ParseError describing the failure. + * a function that can take a CSV data row and return a ProtoConnection + * object. If an error occurs while parsing a particular row, the resolved + * function will throw a ParseError describing the failure. * * The provided CSV must contain columns for name and protocol. Optionally, * the parentIdentifier of the target parent connection group, or a connection @@ -120,14 +175,17 @@ angular.module('import').factory('connectionCSVService', * required. * * This returned object will be very similar to the Connection type, with - * the exception that a human-readable "group" field may be present. + * the exception that a human-readable "group" field may be present, in + * addition to "user" and "userGroup" fields containing arrays of user and + * user group identifiers for whom read access should be granted to this + * connection. * * If a failure occurs while attempting to create the transformer function, * the promise will be rejected with a ParseError describing the failure. * * @returns {Promise.>} * A promise that will resolve to a function that translates a CSV data - * row (array of strings) to a connection object. + * row (array of strings) to a ProtoConnection object. */ service.getCSVTransformer = function getCSVTransformer(headerRow) { @@ -149,8 +207,12 @@ angular.module('import').factory('connectionCSVService', protocolGetter: undefined, // Callbacks for a parent group ID or group path - groupGetter: _.noop, - parentIdentifierGetter: _.noop, + groupGetter: undefined, + parentIdentifierGetter: undefined, + + // Callbacks for user and user group identifiers + usersGetter: () => [], + userGroupsGetter: () => [], // Callbacks that will generate either connection attributes or // parameters. These callbacks will return a {type, name, value} @@ -188,6 +250,11 @@ angular.module('import').factory('connectionCSVService', // A callback that returns the field at the current index const fetchFieldAtIndex = row => row[index]; + + // A callback that splits identifier lists by semicolon + // characters into a javascript list of identifiers + const identifierListCallback = row => + splitIdentifiers(fetchFieldAtIndex(row)); // Set up the name callback if (header == 'name') @@ -203,7 +270,18 @@ angular.module('import').factory('connectionCSVService', // Set up the group parent ID callback else if (header == 'parentIdentifier') - transformConfig.parentIdentifierGetter = fetchFieldAtIndex; + transformConfig.parentIdentifierGetter = ( + identifierListCallback); + + // Set the user identifiers callback + else if (header == 'users') + transformConfig.usersGetter = ( + identifierListCallback); + + // Set the user group identifiers callback + else if (header == 'groups') + transformConfig.userGroupsGetter = ( + identifierListCallback); // At this point, any other header might refer to a connection // parameter or to an attribute @@ -282,47 +360,61 @@ angular.module('import').factory('connectionCSVService', return { type, name, value }; }); }); + + const { + nameGetter, protocolGetter, + parentIdentifierGetter, groupGetter, + usersGetter, userGroupsGetter, + parameterOrAttributeGetters + } = transformConfig; // Fail if the name wasn't provided - if (!transformConfig.nameGetter) + if (!nameGetter) return deferred.reject(new ParseError({ message: 'The connection name must be provided', key: 'CONNECTION_IMPORT.ERROR_REQUIRED_NAME' })); // Fail if the protocol wasn't provided - if (!transformConfig.protocolGetter) + if (!protocolGetter) return deferred.reject(new ParseError({ message: 'The connection protocol must be provided', key: 'CONNECTION_IMPORT.ERROR_REQUIRED_PROTOCOL' })); + + // If both are specified, the parent group is ambigious + if (parentIdentifierGetter && groupGetter) + throw new ParseError({ + message: 'Only one of group or parentIdentifier can be set', + key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + }); // The function to transform a CSV row into a connection object deferred.resolve(function transformCSVRow(row) { - - const { - nameGetter, protocolGetter, - parentIdentifierGetter, groupGetter, - parameterOrAttributeGetters - } = transformConfig; - // Set name and protocol + // Get name and protocol const name = nameGetter(row); const protocol = protocolGetter(row); + + // Get any users or user groups who should be granted access + const users = usersGetter(row); + const groups = userGroupsGetter(row); - // Set the parent group ID and/or group path + // Get the parent group ID and/or group path const group = groupGetter && groupGetter(row); const parentIdentifier = ( parentIdentifierGetter && parentIdentifierGetter(row)); - return { + return new ProtoConnection({ - // Simple fields that are not protocol-specific + // Fields that are not protocol-specific ...{ name, protocol, parentIdentifier, - group + group, + users, + groups }, // Fields that might potentially be either attributes or @@ -339,7 +431,7 @@ angular.module('import').factory('connectionCSVService', }, {parameters: {}, attributes: {}}) - } + }); }); diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 322e22c15..f2f6c1daa 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -33,6 +33,7 @@ angular.module('import').factory('connectionParseService', const Connection = $injector.get('Connection'); const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); + const ParseResult = $injector.get('ParseResult'); const TranslatableMessage = $injector.get('TranslatableMessage'); // Required services @@ -41,8 +42,81 @@ angular.module('import').factory('connectionParseService', const schemaService = $injector.get('schemaService'); const connectionCSVService = $injector.get('connectionCSVService'); const connectionGroupService = $injector.get('connectionGroupService'); + const userService = $injector.get('userService'); + const userGroupService = $injector.get('userGroupService'); const service = {}; + + /** + * Resolves to an object whose keys are all valid identifiers in the current + * data source. The provided `requestFunction` should resolve to such an + * object when provided with the current data source. + * + * @param {Function.>} requestFunction + * A function that, given a data source, will return a promise resolving + * to an object with keys that are unique identifiers for entities in + * that data source. + * + * @returns {Promise.>} + * A promise that will resolve to an function that, given an array of + * identifiers, will return all identifiers that do not exist as keys in + * the object returned by `requestFunction`, i.e. all identifiers that + * do not exist in the current data source. + */ + function getIdentifierMap(requestFunction) { + + // The current data source to which all the identifiers will belong + const dataSource = $routeParams.dataSource; + + // Make the request and return the response, which should be an object + // whose keys are all valid identifiers for the current data source + return requestFunction(dataSource); + } + + /** + * Return a promise that resolves to a function that takes an array of + * identifiers, returning a ParseError describing the invalid identifiers + * from the list. The provided `requestFunction` should resolve to such an + * object whose keys are all valid identifiers when provided with the + * current data source. + * + * @param {Function.>} requestFunction + * A function that, given a data source, will return a promise resolving + * to an object with keys that are unique identifiers for entities in + * that data source. + * + * @returns {Promise.>} + * A promise that will resolve to a function to check the validity of + * each identifier in a provided array, returning a ParseError + * describing the problem if any are not valid. + */ + function getInvalidIdentifierErrorChecker(requestFunction) { + + // Fetch all the valid user identifiers in the system, and + return getIdentifierMap(requestFunction).then(validIdentifiers => + + // The resolved function that takes a list of user group identifiers + allIdentifiers => { + + // Filter to only include invalid identifiers + const invalidIdentifiers = _.filter(allIdentifiers, + identifier => !validIdentifiers[identifier]); + + if (invalidIdentifiers.length) { + + // Quote and comma-seperate for display + const identifierList = invalidIdentifiers.map( + identifier => '"' + identifier + '"').join(', '); + + return new ParseError({ + message: 'Invalid User Group Identifiers: ' + identifierList, + key: 'CONNECTION_IMPORT.ERROR_INVALID_USER_GROUP_IDENTIFIERS', + variables: { IDENTIFIER_LIST: identifierList } + }); + } + + }); + } /** * Perform basic checks, common to all file types - namely that the parsed @@ -127,9 +201,9 @@ angular.module('import').factory('connectionParseService', /** * Returns a promise that will resolve to a transformer function that will - * take an object that may a "group" field, replacing it if present with a - * "parentIdentifier". If both a "group" and "parentIdentifier" field are - * present on the provided object, or if no group exists at the specified + * take an object that may contain a "group" field, replacing it if present + * with a "parentIdentifier". If both a "group" and "parentIdentifier" field + * are present on the provided object, or if no group exists at the specified * path, the function will throw a ParseError describing the failure. * * @returns {Promise.>} @@ -170,32 +244,109 @@ angular.module('import').factory('connectionParseService', }); } - // Translate a given javascript object to a full-fledged Connection - const connectionTransformer = connection => new Connection(connection); + /** + * Convert a provided ProtoConnection array into a ParseResult. Any provided + * transform functions will be run on each entry in `connectionData` before + * any other processing is done. + * + * @param {*[]} connectionData + * An arbitrary array of data. This must evaluate to a ProtoConnection + * object after being run through all functions in `transformFunctions`. + * + * @param {Function[]} transformFunctions + * An array of transformation functions to run on each entry in + * `connection` data. + * + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. + */ + function parseConnectionData(connectionData, transformFunctions) { + + // Check that the provided connection data array is not empty + const checkError = performBasicChecks(connectionData); + if (checkError) { + const deferred = $q.defer(); + deferred.reject(checkError); + return deferred.promise; + } - // Translate a Connection object to a patch requesting the creation of said - // Connection - const patchTransformer = connection => new DirectoryPatch({ - op: 'add', - path: '/', - value: connection - }); + return $q.all({ + groupTransformer : getGroupTransformer(), + invalidUserIdErrorDetector : getInvalidIdentifierErrorChecker( + userService.getUsers), + invalidGroupIDErrorDetector : getInvalidIdentifierErrorChecker( + userGroupService.getUserGroups), + }) + + // Transform the rows from the CSV file to an array of API patches + // and lists of user and group identifiers + .then(({groupTransformer, + invalidUserIdErrorDetector, invalidGroupIDErrorDetector}) => + connectionData.reduce((parseResult, data) => { + + const { patches, identifiers, users, groups } = parseResult; + + // Run the array data through each provided transform + let connectionObject = data; + _.forEach(transformFunctions, transform => { + connectionObject = transform(connectionObject); + }); + + // All errors found while parsing this connection + const connectionErrors = []; + parseResult.errors.push(connectionErrors); + + // Translate the group on the object to a parentIdentifier + try { + connectionObject = groupTransformer(connectionObject); + } + + // If there was a problem with the group or parentIdentifier + catch (error) { + connectionErrors.push(error); + } + + // Push any errors for invalid user or user group identifiers + const pushError = error => error && connectionErrors.push(error); + pushError(invalidUserIdErrorDetector(connectionObject.users)); + pushError(invalidGroupIDErrorDetector(connectionObject.userGroups)); + + // Add the user and group identifiers for this connection + users.push(connectionObject.users); + groups.push(connectionObject.groups); + + // Translate to a full-fledged Connection + const connection = new Connection(connectionObject); + + // Finally, add a patch for creating the connection + patches.push(new DirectoryPatch({ + op: 'add', + path: '/', + value: connection + })); + + // If there are any errors for this connection fail the whole batch + if (connectionErrors.length) + parseResult.hasErrors = true; + + return parseResult; + + }, new ParseResult())); + } /** * Convert a provided CSV representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. - * - * TODO: Describe disambiguation suffixes, e.g. hostname (parameter), and - * that we will accept without the suffix if it's unambigous. (or not? how about not?) + * object to be submitted to the PATCH REST endpoint, as well as a list of + * objects containing lists of user and user group identifiers to be granted + * to each connection. * * @param {String} csvData - * The JSON-encoded connection list to convert to a PATCH request body. + * The CSV-encoded connection list to process. * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided CSV. + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. */ service.parseCSV = function parseCSV(csvData) { @@ -211,134 +362,69 @@ angular.module('import').factory('connectionParseService', catch(error) { console.error(error); const deferred = $q.defer(); - deferred.reject(error); + deferred.reject(new ParseError({ message: error.message })); return deferred.promise; } + // The header row - an array of string header values + const header = parsedData.length ? parsedData[0] : []; + // Slice off the header row to get the data rows const connectionData = parsedData.slice(1); - // Check that the provided CSV is not empty (the parser always - // returns an array) - const checkError = performBasicChecks(connectionData); - if (checkError) { - const deferred = $q.defer(); - deferred.reject(checkError); - return deferred.promise; - } - - // The header row - an array of string header values - const header = parsedData[0]; + // Generate the CSV transform function, and apply it to every row + // before applying all the rest of the standard transforms + return connectionCSVService.getCSVTransformer(header).then( + csvTransformer => - return $q.all({ - csvTransformer : connectionCSVService.getCSVTransformer(header), - groupTransformer : getGroupTransformer() - }) - - // Transform the rows from the CSV file to an array of API patches - .then(({csvTransformer, groupTransformer}) => connectionData.map( - dataRow => { - - // Translate the raw CSV data to a javascript object - let connectionObject = csvTransformer(dataRow); - - // Translate the group on the object to a parentIdentifier - connectionObject = groupTransformer(connectionObject); - - // Translate to a full-fledged Connection - const connection = connectionTransformer(connectionObject); - - // Finally, translate to a patch for creating the connection - return patchTransformer(connection); - - })); + // Apply the CSV transform to every row + parseConnectionData(connectionData, [csvTransformer])); }; /** * Convert a provided YAML representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. + * object to be submitted to the PATCH REST endpoint, as well as a list of + * objects containing lists of user and user group identifiers to be granted + * to each connection. * * @param {String} yamlData - * The YAML-encoded connection list to convert to a PATCH request body. + * The YAML-encoded connection list to process. * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided YAML. + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. */ service.parseYAML = function parseYAML(yamlData) { // Parse from YAML into a javascript array - const parsedData = parseYAMLData(yamlData); + const connectionData = parseYAMLData(yamlData); - // Check that the data is the correct format, and not empty - const checkError = performBasicChecks(connectionData); - if (checkError) { - const deferred = $q.defer(); - deferred.reject(checkError); - return deferred.promise; - } - - // Transform the data from the YAML file to an array of API patches - return getGroupTransformer().then( - groupTransformer => parsedData.map(connectionObject => { - - // Translate the group on the object to a parentIdentifier - connectionObject = groupTransformer(connectionObject); - - // Translate to a full-fledged Connection - const connection = connectionTransformer(connectionObject); - - // Finally, translate to a patch for creating the connection - return patchTransformer(connection); - - })); - + // Produce a ParseResult + return parseConnectionData(connectionData); }; /** - * Convert a provided JSON representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. + * Convert a provided JSON-encoded representation of a connection list into + * an array of patches to be submitted to the PATCH REST endpoint, as well + * as a list of objects containing lists of user and user group identifiers + * to be granted to each connection. * * @param {String} jsonData - * The JSON-encoded connection list to convert to a PATCH request body. + * The JSON-encoded connection list to process. * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided JSON. + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. */ service.parseJSON = function parseJSON(jsonData) { // Parse from JSON into a javascript array - const parsedData = JSON.parse(yamlData); + const connectionData = JSON.parse(jsonData); - // Check that the data is the correct format, and not empty - const checkError = performBasicChecks(connectionData); - if (checkError) { - const deferred = $q.defer(); - deferred.reject(checkError); - return deferred.promise; - } + // Produce a ParseResult + return parseConnectionData(connectionData); - // Transform the data from the YAML file to an array of API patches - return getGroupTransformer().then( - groupTransformer => parsedData.map(connectionObject => { - - // Translate the group on the object to a parentIdentifier - connectionObject = groupTransformer(connectionObject); - - // Translate to a full-fledged Connection - const connection = connectionTransformer(connectionObject); - - // Finally, translate to a patch for creating the connection - return patchTransformer(connection); - - })); - }; return service; diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseError.js b/guacamole/src/main/frontend/src/app/import/types/ParseError.js index 1e4ce478e..33fd07765 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseError.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseError.js @@ -31,7 +31,7 @@ angular.module('import').factory('ParseError', [function defineParseError() { * The object whose properties should be copied within the new * ParseError. */ - var ParseError = function ParseError(template) { + const ParseError = function ParseError(template) { // Use empty object by default template = template || {}; diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js new file mode 100644 index 000000000..e6aa59229 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Service which defines the ParseResult class. + */ +angular.module('import').factory('ParseResult', [function defineParseResult() { + + /** + * The result of parsing a connection import file - containing a list of + * API patches ready to be submitted to the PATCH REST API for batch + * connection creation, a set of users and user groups to grant access to + * each connection, and any errors that may have occured while parsing + * each connection. + * + * @constructor + * @param {ParseResult|Object} [template={}] + * The object whose properties should be copied within the new + * ParseResult. + */ + const ParseResult = function ParseResult(template) { + + // Use empty object by default + template = template || {}; + + /** + * An array of patches, ready to be submitted to the PATCH REST API for + * batch connection creation. + * + * @type {DirectoryPatch[]} + */ + this.patches = template.patches || []; + + /** + * A list of user identifiers that should be granted read access to the + * the corresponding connection (at the same array index). + * + * @type {String[]} + */ + this.users = template.users || []; + + /** + * A list of user group identifiers that should be granted read access + * to the corresponding connection (at the same array index). + * + * @type {String[]} + */ + this.groups = template.groups || []; + + /** + * An array of errors encountered while parsing the corresponding + * connection (at the same array index). Each connection should have a + * an array of errors. If empty, no errors occured for this connection. + * + * @type {ParseError[][]} + */ + this.errors = template.errors || []; + + /** + * True if any errors were encountered while parsing the connections + * represented by this ParseResult. This should always be true if there + * are a non-zero number of elements in the errors list for any + * connection, or false otherwise. + */ + this.hasErrors = template.hasErrors || false; + + }; + + return ParseResult; + +}]); diff --git a/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js b/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js new file mode 100644 index 000000000..c853934d0 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Service which defines the Connection class. + */ +angular.module('import').factory('ProtoConnection', [ + function defineProtoConnection() { + + /** + * A representation of a connection to be imported, as parsed from an + * user-supplied import file. + * + * @constructor + * @param {Connection|Object} [template={}] + * The object whose properties should be copied within the new + * Connection. + */ + var ProtoConnection = function ProtoConnection(template) { + + // Use empty object by default + template = template || {}; + + /** + * The unique identifier of the connection group that contains this + * connection. + * + * @type String + */ + this.parentIdentifier = template.parentIdentifier; + + /** + * The path to the connection group that contains this connection, + * written as e.g. "ROOT/parent/child/group". + * + * @type String + */ + this.group = template.group; + + /** + * The human-readable name of this connection, which is not necessarily + * unique. + * + * @type String + */ + this.name = template.name; + + /** + * The name of the protocol associated with this connection, such as + * "vnc" or "rdp". + * + * @type String + */ + this.protocol = template.protocol; + + /** + * Connection configuration parameters, as dictated by the protocol in + * use, arranged as name/value pairs. This information may not be + * available until directly queried. If this information is + * unavailable, this property will be null or undefined. + * + * @type Object. + */ + this.parameters = template.parameters || {}; + + /** + * Arbitrary name/value pairs which further describe this connection. + * The semantics and validity of these attributes are dictated by the + * extension which defines them. + * + * @type Object. + */ + this.attributes = template.attributes || {}; + + /** + * The identifiers of all users who should be granted read access to + * this connection. + * + * @type String[] + */ + this.users = template.users || []; + + /** + * The identifiers of all user groups who should be granted read access + * to this connection. + * + * @type String[] + */ + this.groups = template.groups || []; + + }; + + return ProtoConnection; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 98b983b9f..978ffc0e2 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -200,6 +200,10 @@ "ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found", "ERROR_INVALID_FILE_TYPE": "Invalid import file type \"{TYPE}\"", + "ERROR_INVALID_USER_IDENTIFIERS": + "Users not found: {IDENTIFIER_LIST}", + "ERROR_INVALID_USER_GROUP_IDENTIFIERS": + "User Groups not found: {IDENTIFIER_LIST}", "ERROR_NO_FILE_SUPPLIED": "Please select a file to import", "ERROR_AMBIGUOUS_PARENT_GROUP": "Both group and parentIdentifier may be not specified at the same time", From bfb7f3b78a7699183d8255b6bf3415d9fc32c2b1 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Mon, 13 Feb 2023 19:02:26 +0000 Subject: [PATCH 12/27] GUACAMOLE-926: Create users and groups; don't require them to exist beforehand. --- .../importConnectionsController.js | 79 +++++++++- .../import/services/connectionCSVService.js | 8 +- .../import/services/connectionParseService.js | 135 +++++------------- ...ProtoConnection.js => ImportConnection.js} | 12 +- .../src/app/import/types/ParseResult.js | 17 +++ .../app/rest/services/connectionService.js | 6 +- .../src/app/rest/types/DirectoryPatch.js | 10 +- .../app/rest/types/DirectoryPatchOutcome.js | 10 +- .../app/rest/types/DirectoryPatchResponse.js | 50 +++++++ 9 files changed, 195 insertions(+), 132 deletions(-) rename guacamole/src/main/frontend/src/app/import/types/{ProtoConnection.js => ImportConnection.js} (91%) create mode 100644 guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 73164a08c..2d7cdab22 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -24,19 +24,73 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function importConnectionsController($scope, $injector) { // Required services + const $routeParams = $injector.get('$routeParams'); const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); // Required types + const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); const TranslatableMessage = $injector.get('TranslatableMessage'); + + /** + * Given a successful response to an import PATCH request, make another + * request to delete every created connection in the provided request, i.e. + * clean up every connection that was created. + * + * @param {DirectoryPatchResponse} creationResponse + */ + function cleanUpConnections(creationResponse) { + + // The patches to delete - one delete per initial creation + const deletionPatches = creationResponse.patches.map(patch => + new DirectoryPatch({ + op: 'remove', + path: '/' + patch.identifier + })); + + console.log("Deletion Patches", deletionPatches); + + connectionService.patchConnections( + $routeParams.dataSource, deletionPatches) - function handleSuccess(data) { - console.log("OMG SUCCESS: ", data) + .then(deletionResponse => + console.log("Deletion response", deletionResponse)) + .catch(handleParseError); + + } + + /** + * Process a successfully parsed import file, creating any specified + * connections, creating and granting permissions to any specified users + * and user groups. + * + * TODO: + * - Do batch import of connections + * - Create all users/groups not already present + * - Grant permissions to all users/groups as defined in the import file + * - On failure: Roll back everything (maybe ask the user first): + * - Attempt to delete all created connections + * - Attempt to delete any created users / groups + * + * @param {ParseResult} parseResult + * The result of parsing the user-supplied import file. + * + */ + function handleParseSuccess(parseResult) { + connectionService.patchConnections( + $routeParams.dataSource, parseResult.patches) + + .then(response => { + console.log("Creation Response", response); + + // TODON'T: Delete connections so we can test over and over + cleanUpConnections(response); + }); } // Set any caught error message to the scope for display - const handleError = error => { + const handleParseError = error => { console.error(error); $scope.error = error; } @@ -44,14 +98,25 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // Clear the current error const clearError = () => delete $scope.error; - function processData(type, data) { + /** + * Process the uploaded import file, importing the connections, granting + * connection permissions, or displaying errors to the user if there are + * problems with the provided file. + * + * @param {String} mimeType + * The MIME type of the uploaded data file. + * + * @param {String} data + * The raw string contents of the import file. + */ + function processData(mimeType, data) { // The function that will process all the raw data and return a list of // patches to be submitted to the API let processDataCallback; // Parse the data based on the provided mimetype - switch(type) { + switch(mimeType) { case "application/json": case "text/json": @@ -82,10 +147,10 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ processDataCallback(data) // Send the data off to be imported if parsing is successful - .then(handleSuccess) + .then(handleParseSuccess) // Display any error found while parsing the file - .catch(handleError); + .catch(handleParseError); } $scope.upload = function() { diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js index a104cdd46..492b9388b 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -33,7 +33,7 @@ angular.module('import').factory('connectionCSVService', // Required types const ParseError = $injector.get('ParseError'); - const ProtoConnection = $injector.get('ProtoConnection'); + const ImportConnection = $injector.get('ImportConnection'); const TranslatableMessage = $injector.get('TranslatableMessage'); // Required services @@ -154,7 +154,7 @@ angular.module('import').factory('connectionCSVService', /** * Given a CSV header row, create and return a promise that will resolve to - * a function that can take a CSV data row and return a ProtoConnection + * a function that can take a CSV data row and return a ImportConnection * object. If an error occurs while parsing a particular row, the resolved * function will throw a ParseError describing the failure. * @@ -185,7 +185,7 @@ angular.module('import').factory('connectionCSVService', * * @returns {Promise.>} * A promise that will resolve to a function that translates a CSV data - * row (array of strings) to a ProtoConnection object. + * row (array of strings) to a ImportConnection object. */ service.getCSVTransformer = function getCSVTransformer(headerRow) { @@ -405,7 +405,7 @@ angular.module('import').factory('connectionCSVService', const parentIdentifier = ( parentIdentifierGetter && parentIdentifierGetter(row)); - return new ProtoConnection({ + return new ImportConnection({ // Fields that are not protocol-specific ...{ diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index f2f6c1daa..7c4f6c754 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -42,81 +42,8 @@ angular.module('import').factory('connectionParseService', const schemaService = $injector.get('schemaService'); const connectionCSVService = $injector.get('connectionCSVService'); const connectionGroupService = $injector.get('connectionGroupService'); - const userService = $injector.get('userService'); - const userGroupService = $injector.get('userGroupService'); const service = {}; - - /** - * Resolves to an object whose keys are all valid identifiers in the current - * data source. The provided `requestFunction` should resolve to such an - * object when provided with the current data source. - * - * @param {Function.>} requestFunction - * A function that, given a data source, will return a promise resolving - * to an object with keys that are unique identifiers for entities in - * that data source. - * - * @returns {Promise.>} - * A promise that will resolve to an function that, given an array of - * identifiers, will return all identifiers that do not exist as keys in - * the object returned by `requestFunction`, i.e. all identifiers that - * do not exist in the current data source. - */ - function getIdentifierMap(requestFunction) { - - // The current data source to which all the identifiers will belong - const dataSource = $routeParams.dataSource; - - // Make the request and return the response, which should be an object - // whose keys are all valid identifiers for the current data source - return requestFunction(dataSource); - } - - /** - * Return a promise that resolves to a function that takes an array of - * identifiers, returning a ParseError describing the invalid identifiers - * from the list. The provided `requestFunction` should resolve to such an - * object whose keys are all valid identifiers when provided with the - * current data source. - * - * @param {Function.>} requestFunction - * A function that, given a data source, will return a promise resolving - * to an object with keys that are unique identifiers for entities in - * that data source. - * - * @returns {Promise.>} - * A promise that will resolve to a function to check the validity of - * each identifier in a provided array, returning a ParseError - * describing the problem if any are not valid. - */ - function getInvalidIdentifierErrorChecker(requestFunction) { - - // Fetch all the valid user identifiers in the system, and - return getIdentifierMap(requestFunction).then(validIdentifiers => - - // The resolved function that takes a list of user group identifiers - allIdentifiers => { - - // Filter to only include invalid identifiers - const invalidIdentifiers = _.filter(allIdentifiers, - identifier => !validIdentifiers[identifier]); - - if (invalidIdentifiers.length) { - - // Quote and comma-seperate for display - const identifierList = invalidIdentifiers.map( - identifier => '"' + identifier + '"').join(', '); - - return new ParseError({ - message: 'Invalid User Group Identifiers: ' + identifierList, - key: 'CONNECTION_IMPORT.ERROR_INVALID_USER_GROUP_IDENTIFIERS', - variables: { IDENTIFIER_LIST: identifierList } - }); - } - - }); - } /** * Perform basic checks, common to all file types - namely that the parsed @@ -245,12 +172,12 @@ angular.module('import').factory('connectionParseService', } /** - * Convert a provided ProtoConnection array into a ParseResult. Any provided + * Convert a provided ImportConnection array into a ParseResult. Any provided * transform functions will be run on each entry in `connectionData` before * any other processing is done. * * @param {*[]} connectionData - * An arbitrary array of data. This must evaluate to a ProtoConnection + * An arbitrary array of data. This must evaluate to a ImportConnection * object after being run through all functions in `transformFunctions`. * * @param {Function[]} transformFunctions @@ -271,21 +198,10 @@ angular.module('import').factory('connectionParseService', return deferred.promise; } - return $q.all({ - groupTransformer : getGroupTransformer(), - invalidUserIdErrorDetector : getInvalidIdentifierErrorChecker( - userService.getUsers), - invalidGroupIDErrorDetector : getInvalidIdentifierErrorChecker( - userGroupService.getUserGroups), - }) + return getGroupTransformer().then(groupTransformer => + connectionData.reduce((parseResult, data) => { - // Transform the rows from the CSV file to an array of API patches - // and lists of user and group identifiers - .then(({groupTransformer, - invalidUserIdErrorDetector, invalidGroupIDErrorDetector}) => - connectionData.reduce((parseResult, data) => { - - const { patches, identifiers, users, groups } = parseResult; + const { patches, users, groups, allUsers, allGroups } = parseResult; // Run the array data through each provided transform let connectionObject = data; @@ -307,14 +223,15 @@ angular.module('import').factory('connectionParseService', connectionErrors.push(error); } - // Push any errors for invalid user or user group identifiers - const pushError = error => error && connectionErrors.push(error); - pushError(invalidUserIdErrorDetector(connectionObject.users)); - pushError(invalidGroupIDErrorDetector(connectionObject.userGroups)); - // Add the user and group identifiers for this connection - users.push(connectionObject.users); - groups.push(connectionObject.groups); + const connectionUsers = connectionObject.users || []; + const connectionGroups = connectionObject.groups || []; + users.push(connectionUsers); + groups.push(connectionGroups); + + // Add all user and user group identifiers to the overall sets + connectionUsers.forEach(identifier => allUsers[identifier] = true); + connectionGroups.forEach(identifier => allGroups[identifier] = true); // Translate to a full-fledged Connection const connection = new Connection(connectionObject); @@ -398,7 +315,18 @@ angular.module('import').factory('connectionParseService', service.parseYAML = function parseYAML(yamlData) { // Parse from YAML into a javascript array - const connectionData = parseYAMLData(yamlData); + try { + const connectionData = parseYAMLData(yamlData); + } + + // If the YAML parser throws an error, reject with that error. No + // translation key will be available here. + catch(error) { + console.error(error); + const deferred = $q.defer(); + deferred.reject(new ParseError({ message: error.message })); + return deferred.promise; + } // Produce a ParseResult return parseConnectionData(connectionData); @@ -420,7 +348,18 @@ angular.module('import').factory('connectionParseService', service.parseJSON = function parseJSON(jsonData) { // Parse from JSON into a javascript array - const connectionData = JSON.parse(jsonData); + try { + const connectionData = JSON.parse(jsonData); + } + + // If the JSON parse attempt throws an error, reject with that error. + // No translation key will be available here. + catch(error) { + console.error(error); + const deferred = $q.defer(); + deferred.reject(new ParseError({ message: error.message })); + return deferred.promise; + } // Produce a ParseResult return parseConnectionData(connectionData); diff --git a/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js similarity index 91% rename from guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js rename to guacamole/src/main/frontend/src/app/import/types/ImportConnection.js index c853934d0..9f336ef77 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js +++ b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js @@ -18,21 +18,21 @@ */ /** - * Service which defines the Connection class. + * Service which defines the ImportConnection class. */ -angular.module('import').factory('ProtoConnection', [ - function defineProtoConnection() { +angular.module('import').factory('ImportConnection', [ + function defineImportConnection() { /** * A representation of a connection to be imported, as parsed from an * user-supplied import file. * * @constructor - * @param {Connection|Object} [template={}] + * @param {ImportConnection|Object} [template={}] * The object whose properties should be copied within the new * Connection. */ - var ProtoConnection = function ProtoConnection(template) { + var ImportConnection = function ImportConnection(template) { // Use empty object by default template = template || {}; @@ -106,6 +106,6 @@ angular.module('import').factory('ProtoConnection', [ }; - return ProtoConnection; + return ImportConnection; }]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js index e6aa59229..77c49eb31 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js @@ -63,6 +63,23 @@ angular.module('import').factory('ParseResult', [function defineParseResult() { */ this.groups = template.groups || []; + /** + * An object whose keys are the user identifiers of every user specified + * in the batch import. i.e. a set of all user identifiers. + * + * @type {Object.} + */ + this.allUsers = template.allUsers || {}; + + /** + * An object whose keys are the user group identifiers of every user + * group specified in the batch import. i.e. a set of all user group + * identifiers. + * + * @type {Object.} + */ + this.allGroups = template.allGroups || {}; + /** * An array of errors encountered while parsing the corresponding * connection (at the same array index). Each connection should have a diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js index 053559530..5c1451ec2 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js @@ -158,7 +158,7 @@ angular.module('rest').factory('connectionService', ['$injector', /** * Makes a request to the REST API to apply a supplied list of connection * patches, returning a promise that can be used for processing the results - * of the call. + * of the call. * * This operation is atomic - if any errors are encountered during the * connection patching process, the entire request will fail, and no @@ -181,12 +181,14 @@ angular.module('rest').factory('connectionService', ['$injector', }) // Clear the cache - .then(function connectionsPatched(){ + .then(function connectionsPatched(patchResponse){ cacheService.connections.removeAll(); // Clear users cache to force reload of permissions for any // newly created or replaced connections cacheService.users.removeAll(); + + return patchResponse; }); diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js index 96a2cb093..05874d4fc 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js @@ -59,10 +59,9 @@ angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch( this.path = template.path || '/'; /** - * The object being added or replaced, or the identifier of the object - * being removed. + * The object being added, or undefined if deleting. * - * @type {DirectoryObject|String} + * @type {DirectoryObject} */ this.value = template.value; @@ -78,11 +77,6 @@ angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch( */ ADD : "add", - /** - * Removes the specified object from the relation. - */ - REPLACE : "replace", - /** * Removes the specified object from the relation. */ diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js index 4534dae13..e6710c2ca 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js @@ -30,15 +30,11 @@ angular.module('rest').factory('DirectoryPatchOutcome', [ * response. The error field is only meaningful for unsuccessful patches. * @constructor * - * @template DirectoryObject - * The directory-based object type that this DirectoryPatchOutcome - * represents a patch outcome for. - * - * @param {DirectoryObject|Object} [template={}] + * @param {DirectoryPatchOutcome|Object} [template={}] * The object whose properties should be copied within the new * DirectoryPatchOutcome. */ - var DirectoryPatchOutcome = function DirectoryPatchOutcome(template) { + const DirectoryPatchOutcome = function DirectoryPatchOutcome(template) { // Use empty object by default template = template || {}; @@ -78,6 +74,6 @@ angular.module('rest').factory('DirectoryPatchOutcome', [ }; - return DirectoryPatch; + return DirectoryPatchOutcome; }]); diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js new file mode 100644 index 000000000..9538c077e --- /dev/null +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Service which defines the DirectoryPatchResponse class. + */ +angular.module('rest').factory('DirectoryPatchResponse', [ + function defineDirectoryPatchResponse() { + + /** + * An object returned by a PATCH request to a directory REST API, + * representing the successful response to a patch request. + * + * @param {DirectoryPatchResponse|Object} [template={}] + * The object whose properties should be copied within the new + * DirectoryPatchResponse. + */ + const DirectoryPatchResponse = function DirectoryPatchResponse(template) { + + // Use empty object by default + template = template || {}; + + /** + * An outcome for each patch in the corresponding patch request. + * + * @type {DirectoryPatchOutcome[]} + */ + this.patches = template.patches; + + }; + + return DirectoryPatchResponse; + +}]); From 51e0fb8c661373afadb7e09e8965076db8057bf8 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 14 Feb 2023 02:12:20 +0000 Subject: [PATCH 13/27] GUACAMOLE-926: Grant permissions to all users / groups. --- .../importConnectionsController.js | 165 ++++++++++++++++-- .../import/services/connectionParseService.js | 34 +++- .../src/app/import/types/ParseResult.js | 27 +-- .../src/app/rest/services/userGroupService.js | 33 ++++ .../src/app/rest/services/userService.js | 33 ++++ 5 files changed, 253 insertions(+), 39 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 2d7cdab22..8cd235e6e 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -17,6 +17,8 @@ * under the License. */ +/* global _ */ + /** * The controller for the connection import page. */ @@ -24,14 +26,21 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function importConnectionsController($scope, $injector) { // Required services + const $q = $injector.get('$q'); const $routeParams = $injector.get('$routeParams'); const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); + const permissionService = $injector.get('permissionService'); + const userService = $injector.get('userService'); + const userGroupService = $injector.get('userGroupService'); // Required types const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); + const PermissionSet = $injector.get('PermissionSet'); const TranslatableMessage = $injector.get('TranslatableMessage'); + const User = $injector.get('User'); + const UserGroup = $injector.get('UserGroup'); /** * Given a successful response to an import PATCH request, make another @@ -56,10 +65,133 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .then(deletionResponse => console.log("Deletion response", deletionResponse)) - .catch(handleParseError); + .catch(handleError); } + /** + * Create all users and user groups mentioned in the import file that don't + * already exist in the current data source. + * + * @param {ParseResult} parseResult + * The result of parsing the user-supplied import file. + * + * @return {Object} + * An object containing the results of the calls to create the users + * and groups. + */ + function createUsersAndGroups(parseResult) { + + const dataSource = $routeParams.dataSource; + + return $q.all({ + existingUsers : userService.getUsers(dataSource), + existingGroups : userGroupService.getUserGroups(dataSource) + }).then(({existingUsers, existingGroups}) => { + + const userPatches = Object.keys(parseResult.users) + + // Filter out any existing users + .filter(identifier => !existingUsers[identifier]) + + // A patch to create each new user + .map(username => new DirectoryPatch({ + op: 'add', + path: '/', + value: new User({ username }) + })); + + const groupPatches = Object.keys(parseResult.groups) + + // Filter out any existing groups + .filter(identifier => !existingGroups[identifier]) + + // A patch to create each new user group + .map(identifier => new DirectoryPatch({ + op: 'add', + path: '/', + value: new UserGroup({ identifier }) + })); + + return $q.all({ + createdUsers: userService.patchUsers(dataSource, userPatches), + createdGroups: userGroupService.patchUserGroups(dataSource, groupPatches) + }); + + }); + + } + + /** + * Grant read permissions for each user and group in the supplied parse + * result to each connection in their connection list. Note that there will + * be a seperate request for each user and group. + * + * @param {ParseResult} parseResult + * The result of successfully parsing a user-supplied import file. + * + * @param {Object} response + * The response from the PATCH API request. + * + * @returns {Promise.} + * A promise that will resolve with the result of every permission + * granting request. + */ + function grantConnectionPermissions(parseResult, response) { + + const dataSource = $routeParams.dataSource; + + // All connection grant requests, one per user/group + const userRequests = {}; + const groupRequests = {}; + + // Create a PermissionSet granting access to all connections at + // the provided indices within the provided parse result + const createPermissionSet = indices => + new PermissionSet({ connectionPermissions: indices.reduce( + (permissions, index) => { + const connectionId = response.patches[index].identifier; + permissions[connectionId] = [ + PermissionSet.ObjectPermissionType.READ]; + return permissions; + }, {}) }); + + // Now that we've created all the users, grant access to each + _.forEach(parseResult.users, (connectionIndices, identifier) => + + // Grant the permissions - note the group flag is `false` + userRequests[identifier] = permissionService.patchPermissions( + dataSource, identifier, + + // Create the permissions to these connections for this user + createPermissionSet(connectionIndices), + + // Do not remove any permissions + new PermissionSet(), + + // This call is not for a group + false)); + + // Now that we've created all the groups, grant access to each + _.forEach(parseResult.groups, (connectionIndices, identifier) => + + // Grant the permissions - note the group flag is `true` + groupRequests[identifier] = permissionService.patchPermissions( + dataSource, identifier, + + // Create the permissions to these connections for this user + createPermissionSet(connectionIndices), + + // Do not remove any permissions + new PermissionSet(), + + // This call is for a group + true)); + + // Return the result from all the permission granting calls + return $q.all({ ...userRequests, ...groupRequests }); + } + /** * Process a successfully parsed import file, creating any specified * connections, creating and granting permissions to any specified users @@ -78,19 +210,32 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ * */ function handleParseSuccess(parseResult) { - connectionService.patchConnections( - $routeParams.dataSource, parseResult.patches) - - .then(response => { - console.log("Creation Response", response); - // TODON'T: Delete connections so we can test over and over - cleanUpConnections(response); + const dataSource = $routeParams.dataSource; + + console.log("parseResult", parseResult); + + // First, attempt to create the connections + connectionService.patchConnections(dataSource, parseResult.patches) + .then(response => { + + // If connection creation is successful, create users and groups + createUsersAndGroups(parseResult).then(() => { + + grantConnectionPermissions(parseResult, response).then(results => { + console.log("permission requests", results); + + // TODON'T: Delete connections so we can test over and over + cleanUpConnections(response); + }) + + + }); }); } // Set any caught error message to the scope for display - const handleParseError = error => { + const handleError = error => { console.error(error); $scope.error = error; } @@ -150,7 +295,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .then(handleParseSuccess) // Display any error found while parsing the file - .catch(handleParseError); + .catch(handleError); } $scope.upload = function() { diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 7c4f6c754..4b9f50c2d 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -199,9 +199,9 @@ angular.module('import').factory('connectionParseService', } return getGroupTransformer().then(groupTransformer => - connectionData.reduce((parseResult, data) => { + connectionData.reduce((parseResult, data, index) => { - const { patches, users, groups, allUsers, allGroups } = parseResult; + const { patches, users, groups } = parseResult; // Run the array data through each provided transform let connectionObject = data; @@ -223,15 +223,33 @@ angular.module('import').factory('connectionParseService', connectionErrors.push(error); } - // Add the user and group identifiers for this connection + // The users and user groups that should be granted access const connectionUsers = connectionObject.users || []; const connectionGroups = connectionObject.groups || []; - users.push(connectionUsers); - groups.push(connectionGroups); - // Add all user and user group identifiers to the overall sets - connectionUsers.forEach(identifier => allUsers[identifier] = true); - connectionGroups.forEach(identifier => allGroups[identifier] = true); + // Add this connection index to the list for each user + connectionUsers.forEach(identifier => { + + // If there's an existing list, add the index to that + if (users[identifier]) + users[identifier].push(index); + + // Otherwise, create a new list with just this index + else + users[identifier] = [index]; + }); + + // Add this connection index to the list for each group + connectionGroups.forEach(identifier => { + + // If there's an existing list, add the index to that + if (groups[identifier]) + groups[identifier].push(index); + + // Otherwise, create a new list with just this index + else + groups[identifier] = [index]; + }); // Translate to a full-fledged Connection const connection = new Connection(connectionObject); diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js index 77c49eb31..1a5abd14d 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js @@ -48,28 +48,13 @@ angular.module('import').factory('ParseResult', [function defineParseResult() { this.patches = template.patches || []; /** - * A list of user identifiers that should be granted read access to the - * the corresponding connection (at the same array index). + * An object whose keys are the user identifiers of users specified + * in the batch import. and the keys are an array of indices of + * connections to which those users should be granted access. * - * @type {String[]} + * @type {Object.} */ - this.users = template.users || []; - - /** - * A list of user group identifiers that should be granted read access - * to the corresponding connection (at the same array index). - * - * @type {String[]} - */ - this.groups = template.groups || []; - - /** - * An object whose keys are the user identifiers of every user specified - * in the batch import. i.e. a set of all user identifiers. - * - * @type {Object.} - */ - this.allUsers = template.allUsers || {}; + this.users = template.users || {}; /** * An object whose keys are the user group identifiers of every user @@ -78,7 +63,7 @@ angular.module('import').factory('ParseResult', [function defineParseResult() { * * @type {Object.} */ - this.allGroups = template.allGroups || {}; + this.groups = template.users || {}; /** * An array of errors encountered while parsing the corresponding diff --git a/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js b/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js index 090efa944..987f8383a 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js @@ -190,6 +190,39 @@ angular.module('rest').factory('userGroupService', ['$injector', }; + /** + * Makes a request to the REST API to apply a supplied list of user group + * patches, returning a promise that can be used for processing the results + * of the call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. + * + * @param {DirectoryPatch.[]} patches + * An array of patches to apply. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + service.patchUserGroups = function patchUserGroups(dataSource, patches) { + + // Make the PATCH request + return authenticationService.request({ + method : 'PATCH', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', + data : patches + }) + + // Clear the cache + .then(function userGroupsPatched(patchResponse){ + cacheService.users.removeAll(); + return patchResponse; + }); + + } + return service; }]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/userService.js b/guacamole/src/main/frontend/src/app/rest/services/userService.js index faa26b40a..252aead3a 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/userService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/userService.js @@ -235,6 +235,39 @@ angular.module('rest').factory('userService', ['$injector', }); }; + + /** + * Makes a request to the REST API to apply a supplied list of user patches, + * returning a promise that can be used for processing the results of the + * call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. + * + * @param {DirectoryPatch.[]} patches + * An array of patches to apply. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + service.patchUsers = function patchUsers(dataSource, patches) { + + // Make the PATCH request + return authenticationService.request({ + method : 'PATCH', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users', + data : patches + }) + + // Clear the cache + .then(function usersPatched(patchResponse){ + cacheService.users.removeAll(); + return patchResponse; + }); + + } return service; From a1c1a5888612a0b05b7741dc27c7ec8d8b30dd2d Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 16 Feb 2023 01:30:55 +0000 Subject: [PATCH 14/27] GUACAMOLE-926: Create new file upload UI. --- .../importConnectionsController.js | 240 ++++++++++++++--- .../directives/connectionImportFileUpload.js | 253 ++++++++++++++++++ .../src/app/import/styles/file-upload.css | 119 ++++++++ .../frontend/src/app/import/styles/import.css | 9 + .../import/templates/connectionImport.html | 16 +- .../templates/connectionImportFileUpload.html | 30 +++ .../main/frontend/src/translations/en.json | 14 +- guacamole/src/main/frontend/webpack.config.js | 16 +- 8 files changed, 641 insertions(+), 56 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js create mode 100644 guacamole/src/main/frontend/src/app/import/styles/file-upload.css create mode 100644 guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 8cd235e6e..31a49adf8 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -42,6 +42,70 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ const User = $injector.get('User'); const UserGroup = $injector.get('UserGroup'); + /** + * Any error that may have occured during import file parsing. + * + * @type {ParseError} + */ + $scope.error = null; + + /** + * True if the file is fully uploaded and ready to be processed, or false + * otherwise. + * + * @type {Boolean} + */ + $scope.dataReady = false; + + /** + * True if the file upload has been aborted mid-upload, or false otherwise. + */ + $scope.aborted = false; + + /** + * True if fully-uploaded data is being processed, or false otherwise. + */ + $scope.processing = false; + + /** + * The MIME type of the uploaded file, if any. + * + * @type {String} + */ + $scope.mimeType = null; + + /** + * The raw string contents of the uploaded file, if any. + * + * @type {String} + */ + $scope.fileData = null; + + /** + * The file reader currently being used to upload the file, if any. If + * null, no file upload is currently in progress. + * + * @type {FileReader} + */ + $scope.fileReader = null; + + /** + * Clear all file upload state. + */ + function resetUploadState() { + + $scope.aborted = false; + $scope.dataReady = false; + $scope.processing = false; + $scope.fileData = null; + $scope.mimeType = null; + $scope.fileReader = null; + + // Broadcast an event to clear the file upload UI + $scope.$broadcast('clearFile'); + + } + /** * Given a successful response to an import PATCH request, make another * request to delete every created connection in the provided request, i.e. @@ -227,20 +291,35 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // TODON'T: Delete connections so we can test over and over cleanUpConnections(response); - }) + resetUploadState(); + }) }); }); } - // Set any caught error message to the scope for display + /** + * Set any caught error message to the scope for display. + * + * @argument {ParseError} error + * The error to display. + */ const handleError = error => { + + // Any error indicates that processing of the file has failed, so clear + // all upload state to allow for a fresh retry + resetUploadState(); + + // Set the error for display console.error(error); $scope.error = error; + } - // Clear the current error + /** + * Clear the current displayed error. + */ const clearError = () => delete $scope.error; /** @@ -255,39 +334,35 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ * The raw string contents of the import file. */ function processData(mimeType, data) { + + // Data processing has begun + $scope.processing = true; // The function that will process all the raw data and return a list of // patches to be submitted to the API let processDataCallback; - // Parse the data based on the provided mimetype - switch(mimeType) { + // Choose the appropriate parse function based on the mimetype + if (mimeType.endsWith("json")) + processDataCallback = connectionParseService.parseJSON; - case "application/json": - case "text/json": - processDataCallback = connectionParseService.parseJSON; - break; + else if (mimeType.endsWith("csv")) + processDataCallback = connectionParseService.parseCSV; - case "text/csv": - processDataCallback = connectionParseService.parseCSV; - break; + else if (mimeType.endsWith("yaml")) + processDataCallback = connectionParseService.parseYAML; - case "application/yaml": - case "application/x-yaml": - case "text/yaml": - case "text/x-yaml": - processDataCallback = connectionParseService.parseYAML; - break; - - default: - handleError(new ParseError({ - message: 'Invalid file type: ' + type, - key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', - variables: { TYPE: type } - })); - return; + // We don't expect this to happen - the file upload directive should + // have already have filtered out any invalid file types + else { + handleError(new ParseError({ + message: 'Invalid file type: ' + type, + key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', + variables: { TYPE: type } + })); + return; } - + // Make the call to process the data into a series of patches processDataCallback(data) @@ -298,30 +373,109 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .catch(handleError); } - $scope.upload = function() { + /** + * Process the uploaded import data. Only usuable if the upload is fully + * complete. + */ + $scope.import = () => processData($scope.mimeType, $scope.fileData); + + /** + * @return {Boolean} + * True if import should be disabled, or false if cancellation + * should be allowed. + */ + $scope.importDisabled = () => + + // Disable import if no data is ready + !$scope.dataReady || + + // Disable import if the file is currently being processed + $scope.processing; + + /** + * Cancel any in-progress upload, or clear any uploaded-but + */ + $scope.cancel = function() { + + // Clear any error message + clearError(); + + // If the upload is in progress, stop it now; the FileReader will + // reset the upload state when it stops + if ($scope.fileReader) { + $scope.aborted = true; + $scope.fileReader.abort(); + } + + // Clear any upload state - there's no FileReader handler to do it + else + resetUploadState(); + + }; + + /** + * @return {Boolean} + * True if cancellation should be disabled, or false if cancellation + * should be allowed. + */ + $scope.cancelDisabled = () => + + // Disable cancellation if the import has already been cancelled + $scope.aborted || + + // Disable cancellation if the file is currently being processed + $scope.processing || + + // Disable cancellation if no data is ready or being uploaded + !($scope.fileReader || $scope.dataReady); + + /** + * Handle a provided File upload, reading all data onto the scope for + * import processing, should the user request an import. + * + * @argument {File} file + * The file to upload onto the scope for further processing. + */ + $scope.handleFile = function(file) { // Clear any error message from the previous upload attempt clearError(); - const files = angular.element('#file')[0].files; + // Initialize upload state + $scope.aborted = false; + $scope.dataReady = false; + $scope.processing = false; + $scope.uploadStarted = true; - if (files.length <= 0) { - handleError(new ParseError({ - message: 'No file supplied', - key: 'CONNECTION_IMPORT.ERROR_NO_FILE_SUPPLIED' - })); - return; - } + // Save the MIME type to the scope + $scope.mimeType = file.type; - // The file that the user uploaded - const file = files[0]; + // Save the file to the scope when ready + $scope.fileReader = new FileReader(); + $scope.fileReader.onloadend = (e => { - // Call processData when the data is ready - const reader = new FileReader(); - reader.onloadend = (e => processData(file.type, e.target.result)); + // If the upload was explicitly aborted, clear any upload state and + // do not process the data + if ($scope.aborted) + resetUploadState(); - // Read all the data into memory and call processData when done - reader.readAsBinaryString(file); + else { + + // Save the uploaded data + $scope.fileData = e.target.result; + + // Mark the data as ready + $scope.dataReady = true; + + // Clear the file reader from the scope now that this file is + // fully uploaded + $scope.fileReader = null; + + } + }); + + // Read all the data into memory + $scope.fileReader.readAsBinaryString(file); } }]); diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js new file mode 100644 index 000000000..d988e7c7d --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* global _ */ + +/** + * A directive that allows for file upload, either through drag-and-drop or + * a file browser. + */ + +/** + * All legal import file types. Any file not belonging to one of these types + * must be rejected. + */ +const LEGAL_FILE_TYPES = ["csv", "json", "yaml"]; + +angular.module('import').directive('connectionImportFileUpload', [ + function connectionImportFileUpload() { + + const directive = { + restrict: 'E', + replace: true, + templateUrl: 'app/import/templates/connectionImportFileUpload.html', + scope: { + + /** + * The function to invoke when a file is provided to the file upload + * UI, either by dragging and dropping, or by navigating using the + * file browser. The function will be called with 2 arguments - the + * mime type, and the raw string contents of the file. + * + * @type function + */ + onFile : '&', + } + }; + + directive.controller = ['$scope', '$injector', '$element', + function fileUploadController($scope, $injector, $element) { + + // Required services + const $timeout = $injector.get('$timeout'); + + /** + * Whether a drag/drop operation is currently in progress (the user has + * dragged a file over the Guacamole connection but has not yet + * dropped it). + * + * @type boolean + */ + $scope.dropPending = false; + + /** + * The error associated with the file upload, if any. An object of the + * form { key, variables }, or null if no error has occured. + */ + $scope.error = null; + + /** + * The name of the file that's currently being uploaded, or has yet to + * be imported, if any. + */ + $scope.fileName = null; + + // Clear the file if instructed to do so by the parent + $scope.$on('clearFile', () => delete $scope.fileName); + + /** + * Clear any displayed error message. + */ + const clearError = () => $scope.error = null; + + /** + * Set an error for display using the provided translation key and + * translation variables. + * + * @param {String} key + * The translation key. + * + * @param {Object.} variables + * The variables to subsitute into the message, if any. + */ + const setError = (key, variables) => $scope.error = { key, variables }; + + /** + * The location where files can be dragged-and-dropped to. + * + * @type Element + */ + const dropTarget = $element.find('.drop-target')[0]; + + /** + * Displays a visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param {Event} e + * The event related to the in-progress drag/drop operation. + */ + const notifyDragStart = function notifyDragStart(e) { + + e.preventDefault(); + e.stopPropagation(); + + $scope.$apply(() => { + $scope.dropPending = true; + }); + + }; + + /** + * Removes the visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param {Event} e + * The event related to the end of the former drag/drop operation. + */ + const notifyDragEnd = function notifyDragEnd(e) { + + e.preventDefault(); + e.stopPropagation(); + + $scope.$apply(() => { + $scope.dropPending = false; + }); + + }; + + // Add listeners to the drop target to ensure that the visual state + // stays up to date + dropTarget.addEventListener('dragenter', notifyDragStart, false); + dropTarget.addEventListener('dragover', notifyDragStart, false); + dropTarget.addEventListener('dragleave', notifyDragEnd, false); + + /** + * Given a user-supplied file, validate that the file type is correct, + * and invoke the onFile callback provided to this directive if so. + * + * @param {File} file + * The user-supplied file. + */ + function handleFile(file) { + + // Clear any error from a previous attempted file upload + clearError(); + + // The MIME type of the provided file + const mimeType = file.type; + + // Check if the mimetype ends with one of the supported types, + // e.g. "application/json" or "text/csv" + if (_.every(LEGAL_FILE_TYPES.map( + type => !mimeType.endsWith(type)))) { + + // If the provided file is not one of the supported types, + // display an error and abort processing + setError('CONNECTION_IMPORT.ERROR_INVALID_FILE_TYPE', + { TYPE: mimeType }); + return; + } + + $scope.fileName = file.name; + + // Invoke the provided file callback using the file + $scope.onFile({ file }); + } + + /** + * Drop target event listener that will be invoked if the user drops + * anything onto the drop target. If a valid file is provided, the + * onFile callback provided to this directive will be called; otherwise + * an error will be displayed, if appropriate. + * + * @param {Event} e + * The drop event that triggered this handler. + */ + dropTarget.addEventListener('drop', function(e) { + + notifyDragEnd(e); + + const files = e.dataTransfer.files; + + // Ignore any non-files that are dragged into the drop area + if (files.length < 1) + return; + + if (files.length > 2) { + + // If more than one file was provided, print an error explaining + // that only a single file is allowed and abort processing + setError('CONNECTION_IMPORT.ERROR_FILE_SINGLE_ONLY'); + return; + } + + handleFile(files[0]); + + }, false); + + /** + * The hidden file input used to create a file browser. + * + * @type Element + */ + const fileUploadInput = $element.find('.file-upload-input')[0]; + + /** + * A function that will click on the hidden file input to open a file + * browser to allow the user to select a file for upload. + */ + $scope.openFileBrowser = () => + $timeout(() => fileUploadInput.click(), 0, false); + + /** + * A handler that will be invoked when a user selectes a file in the + * file browser. After some error checking, the file will be passed to + * the onFile callback provided to this directive. + * + * @param {Event} e + * The event that was triggered when the user selected a file in + * their file browser. + */ + fileUploadInput.onchange = e => { + + // Process the uploaded file + handleFile(e.target.files[0]); + + // Clear the value to ensure that the change event will be fired + // if the user selects the same file again + fileUploadInput.value = null; + + }; + + }]; + return directive; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/styles/file-upload.css b/guacamole/src/main/frontend/src/app/import/styles/file-upload.css new file mode 100644 index 000000000..afe0fcf97 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/styles/file-upload.css @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +.file-upload-container { + + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 24px 24px; + + width: fit-content; + + border: 1px solid rgba(0,0,0,.25); + box-shadow: 1px 1px 2px rgb(0 0 0 / 25%); + + margin-left: auto; + margin-right: auto; + +} + +.file-upload-container .upload-header { + + display: flex; + flex-direction: row; + width: 500px; + margin-bottom: 5px; + justify-content: space-between; + +} + +.file-upload-container .file-error { + + color: red; + +} + +.file-upload-container .file-options { + + font-weight: bold; + +} + +.file-upload-container .file-upload-input { + + display: none; + +} + +.file-upload-container .drop-target { + + display: flex; + flex-direction: column; + + align-items: center; + justify-content: space-evenly; + + width: 500px; + height: 200px; + + background: rgba(0,0,0,.04); + border: 1px solid black; + +} + +.file-upload-container .drop-target.file-present { + + background: rgba(0,0,0,.15); + +} + + +.file-upload-container .drop-target .file-name { + + font-weight: bold; + font-size: 1.5em; + +} + +.file-upload-container .drop-target.drop-pending { + + background: #3161a9; + +} + +.file-upload-container .drop-target.drop-pending > * { + + opacity: 0.5; + +} + +.file-upload-container .drop-target .title { + + font-weight: bold; + font-size: 1.25em; + +} + +.file-upload-container .drop-target .browse-link { + + text-decoration: underline; + cursor: pointer; + +} diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index f5551bb42..5addb1933 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -19,4 +19,13 @@ .import .parseError { color: red; +} + +.import .import-buttons { + + margin-top: 10px; + display: flex; + gap: 10px; + justify-content: center; + } \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index 39b12fece..881cb2429 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -5,9 +5,19 @@ - - - + + +
+ + +
+

+ +

+ {{'CONNECTION_IMPORT.UPLOAD_FILE_TYPES' | translate}} + {{'CONNECTION_IMPORT.UPLOAD_HELP_LINK' | translate}} + +
+ +
+ +
{{'CONNECTION_IMPORT.UPLOAD_DROP_TITLE' | translate}}
+ + + + {{'CONNECTION_IMPORT.UPLOAD_BROWSE_LINK' | translate}} + + +
{{fileName}}
+ +
+ + +

+ + diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 978ffc0e2..73afddb3c 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -186,6 +186,9 @@ "CONNECTION_IMPORT": { + "BUTTON_CANCEL": "Cancel", + "BUTTON_IMPORT": "Import Connections", + "HEADER": "Connection Import", "ERROR_AMBIGUOUS_CSV_HEADER": @@ -199,7 +202,7 @@ "Invalid CSV Header \"{HEADER}\" is neither an attribute or parameter", "ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found", "ERROR_INVALID_FILE_TYPE": - "Invalid import file type \"{TYPE}\"", + "Unsupported file type: \"{TYPE}\"", "ERROR_INVALID_USER_IDENTIFIERS": "Users not found: {IDENTIFIER_LIST}", "ERROR_INVALID_USER_GROUP_IDENTIFIERS": @@ -210,7 +213,14 @@ "ERROR_REQUIRED_PROTOCOL": "No connection protocol found in the provided file", "ERROR_REQUIRED_NAME": - "No connection name found in the provided file" + "No connection name found in the provided file", + + "UPLOAD_FILE_TYPES": "CSV, JSON, or YAML", + "UPLOAD_HELP_LINK": "View Format Tips", + "UPLOAD_DROP_TITLE": "Drop a File Here", + "UPLOAD_BROWSE_LINK": "Browse for File", + + "ERROR_FILE_SINGLE_ONLY": "Please upload only a single file at a time" }, diff --git a/guacamole/src/main/frontend/webpack.config.js b/guacamole/src/main/frontend/webpack.config.js index dc6ad08cf..7f8f9da81 100644 --- a/guacamole/src/main/frontend/webpack.config.js +++ b/guacamole/src/main/frontend/webpack.config.js @@ -95,14 +95,14 @@ module.exports = { optimization: { minimizer: [ - // Minify using Google Closure Compiler - new ClosureWebpackPlugin({ mode: 'STANDARD' }, { - languageIn: 'ECMASCRIPT_2020', - languageOut: 'ECMASCRIPT5', - compilationLevel: 'SIMPLE' - }), - - new CssMinimizerPlugin() +// // Minify using Google Closure Compiler +// new ClosureWebpackPlugin({ mode: 'STANDARD' }, { +// languageIn: 'ECMASCRIPT_2020', +// languageOut: 'ECMASCRIPT5', +// compilationLevel: 'SIMPLE' +// }), +// +// new CssMinimizerPlugin() ], splitChunks: { From 690a8a34cabfb899d35e52201d6573657ab832c7 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 21 Feb 2023 22:14:02 +0000 Subject: [PATCH 15/27] GUACAMOLE-926: Add file format help page. --- .../frontend/src/app/import/styles/help.css | 60 ++++++++++++ .../templates/connectionImportFileHelp.html | 93 +++++++++++++++++++ .../templates/connectionImportFileUpload.html | 2 +- .../src/app/index/config/indexRouteConfig.js | 8 ++ .../main/frontend/src/translations/en.json | 18 ++++ 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 guacamole/src/main/frontend/src/app/import/styles/help.css create mode 100644 guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html diff --git a/guacamole/src/main/frontend/src/app/import/styles/help.css b/guacamole/src/main/frontend/src/app/import/styles/help.css new file mode 100644 index 000000000..e78acea4a --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/styles/help.css @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +.import.help { + + text-transform: none; + +} + +.import.help p { + + max-width: 70em; + +} + +.import.help h2 { + + padding-top: 0px; + padding-bottom: 0px; + +} + +.import.help p, .import.help pre { + + margin-left: 1em; + +} + +.import.help pre { + + background-color: rgba(0,0,0,0.15); + padding: 10px; + width: fit-content; + +} + +.import.help .footnotes { + + border-top: 1px solid gray; + padding-top: 1em; + width: fit-content; + margin-left: 1em; + +} \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html new file mode 100644 index 000000000..bd7f21845 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html @@ -0,0 +1,93 @@ +
+ +
+

{{'CONNECTION_IMPORT.HELP_HEADER' | translate}}

+ +
+ +

{{'CONNECTION_IMPORT.HELP_FILE_TYPE_HEADER' | translate}}

+

{{'CONNECTION_IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}

+ +

{{'CONNECTION_IMPORT.HELP_CSV_HEADER' | translate}}

+

{{'CONNECTION_IMPORT.HELP_CSV_DESCRIPTION' | translate}}

+

{{'CONNECTION_IMPORT.HELP_CSV_MORE_DETAILS' | translate}}

+
name,protocol,hostname,group,users,groups,guacd-encryption (attribute)
+conn1,vnc,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none
+conn2,rdp,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl
+conn3,ssh,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,
+conn4,kubernetes,,,,,
+ +

{{'CONNECTION_IMPORT.HELP_JSON_HEADER' | translate}}

+

{{'CONNECTION_IMPORT.HELP_JSON_DESCRIPTION' | translate}}

+

{{'CONNECTION_IMPORT.HELP_JSON_MORE_DETAILS' | translate}}

+
[
+  {
+    "name": "conn1",
+    "protocol": "vnc",
+    "parameters": { "hostname": "conn1.web.com" },
+    "parentIdentifier": "ROOT",
+    "users": [ "guac user 1", "guac user 2" ],
+    "groups": [ "Connection 1 Users" ],
+    "attributes": { "guacd-encryption": "none" }
+  },
+  {
+    "name": "conn2",
+    "protocol": "rdp",
+    "parameters": { "hostname": "conn2.web.com" },
+    "group": "ROOT/Parent Group",
+    "users": [ "guac user 1" ],
+    "attributes": { "guacd-encryption": "none" }
+  },
+  {
+    "name": "conn3",
+    "protocol": "ssh",
+    "parameters": { "hostname": "conn3.web.com" },
+    "group": "ROOT/Parent Group/Child Group",
+    "users": [ "guac user 2", "guac user 3" ],
+  },
+  {
+    "name": "conn4",
+    "protocol": "kubernetes",
+  }
+]
+ +

{{'CONNECTION_IMPORT.HELP_YAML_HEADER' | translate}}

+

{{'CONNECTION_IMPORT.HELP_YAML_DESCRIPTION' | translate}}

+
---
+  - name: conn1
+    protocol: vnc
+    parameters:
+      hostname: conn1.web.com
+    group: ROOT
+    users:
+    - guac user 1
+    - guac user 2
+    groups:
+    - AWS EC2 Administrators
+    attributes:
+      guacd-encryption: none
+  - name: conn2
+    protocol: rdp
+    parameters:
+      hostname: conn2.web.com
+    group: ROOT/Parent Group
+    users:
+    - guac user 1
+    attributes:
+      guacd-encryption: none
+  - name: conn3
+    protocol: ssh
+    parameters:
+      hostname: conn3.web.com
+    group: ROOT/Parent Group/Child Group
+    users:
+    - guac user 2
+    - guac user 3
+  - name: conn4
+    protocol: kubernetes
+ +
    +
  1. {{'CONNECTION_IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}
  2. +
+ +
diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html index 0e9741ede..08dbaf511 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html @@ -3,7 +3,7 @@
{{'CONNECTION_IMPORT.UPLOAD_FILE_TYPES' | translate}} {{'CONNECTION_IMPORT.UPLOAD_HELP_LINK' | translate}}
diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js index dd9cc0637..7caeee526 100644 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js @@ -135,6 +135,14 @@ angular.module('index').config(['$routeProvider', '$locationProvider', resolve : { updateCurrentToken: updateCurrentToken } }) + // Connection import page + .when('/import/connection/file-format-help', { + title : 'APP.NAME', + bodyClassName : 'settings', + templateUrl : 'app/import/templates/connectionImportFileHelp.html', + resolve : { updateCurrentToken: updateCurrentToken } + }) + // Management screen .when('/settings/:dataSource?/:tab', { title : 'APP.NAME', diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 73afddb3c..06e3a4d9d 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -191,6 +191,24 @@ "HEADER": "Connection Import", + "HELP_HEADER": "Connection Import File Format", + + "HELP_FILE_TYPE_HEADER": "File Types", + "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified.", + + "HELP_CSV_HEADER": "CSV Format", + "HELP_CSV_DESCRIPTION": "A connection import CSV file has one connection record per row. Each column will specify a connection field. At minimum the connection name and protocol must be specified.", + "HELP_CSV_MORE_DETAILS": "The CSV header for each row specifies the connection field. The connection group ID that the connection should be imported into may be directly specified with \"parentIdentifier\", or the path to the parent group may be specified using \"group\" as shown below. In most cases, there should be no conflict between fields, but if needed, an \" (attribute)\" or \" (parameter)\" suffix may be added to disambiguate. Lists of user or user group identifiers must be semicolon-seperated.¹", + + "HELP_JSON_HEADER": "JSON Format", + "HELP_JSON_DESCRIPTION": "A connection import JSON file is a list of connection objects. At minimum the connection name and protocol must be specified in each connection object.", + "HELP_JSON_MORE_DETAILS": "The connection group ID that the connection should be imported into may be directly specified with a \"parentIdentifier\" field, or the path to the parent group may be specified using a \"group\" field as shown below. An array of user and user group identifiers to grant access to may be specified per connection.", + + "HELP_YAML_HEADER": "YAML Format", + "HELP_YAML_DESCRIPTION": "A connection import YAML file is a list of connection objects with exactly the same structure as the JSON format.", + + "HELP_SEMICOLON_FOOTNOTE": "If needed, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", + "ERROR_AMBIGUOUS_CSV_HEADER": "Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter", "ERROR_ARRAY_REQUIRED": From 5a2a4a79a7d45a4cc10c96e09bdd419d3938efa4 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 21 Feb 2023 23:58:15 +0000 Subject: [PATCH 16/27] GUACAMOLE-926: Roll back created users and user groups as well. --- .../importConnectionsController.js | 184 +++++++++++++----- .../directives/connectionImportFileUpload.js | 9 +- .../import/services/connectionParseService.js | 8 +- .../frontend/src/app/import/styles/help.css | 1 - .../templates/connectionImportFileHelp.html | 6 +- .../main/frontend/src/translations/en.json | 5 +- 6 files changed, 146 insertions(+), 67 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 31a49adf8..73acb1cc3 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -106,33 +106,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } - /** - * Given a successful response to an import PATCH request, make another - * request to delete every created connection in the provided request, i.e. - * clean up every connection that was created. - * - * @param {DirectoryPatchResponse} creationResponse - */ - function cleanUpConnections(creationResponse) { - - // The patches to delete - one delete per initial creation - const deletionPatches = creationResponse.patches.map(patch => - new DirectoryPatch({ - op: 'remove', - path: '/' + patch.identifier - })); - - console.log("Deletion Patches", deletionPatches); - - connectionService.patchConnections( - $routeParams.dataSource, deletionPatches) - - .then(deletionResponse => - console.log("Deletion response", deletionResponse)) - .catch(handleError); - - } - /** * Create all users and user groups mentioned in the import file that don't * already exist in the current data source. @@ -178,8 +151,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ })); return $q.all({ - createdUsers: userService.patchUsers(dataSource, userPatches), - createdGroups: userGroupService.patchUserGroups(dataSource, groupPatches) + userResponse: userService.patchUsers(dataSource, userPatches), + groupResponse: userGroupService.patchUserGroups(dataSource, groupPatches) }); }); @@ -256,22 +229,126 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ return $q.all({ ...userRequests, ...groupRequests }); } + // Given a PATCH API response, create an array of patches to delete every + // entity created in the original request that generated this response + const createDeletionPatches = creationResponse => + creationResponse.patches.map(patch => + + // Add one deletion patch per original creation patch + new DirectoryPatch({ + op: 'remove', + path: '/' + patch.identifier + })); + + /** + * Given a successful response to a connection PATCH request, make another + * request to delete every created connection in the provided request, i.e. + * clean up every connection that was created. + * + * @param {DirectoryPatchResponse} creationResponse + * The response to the connection PATCH request. + * + * @returns {DirectoryPatchResponse} + * The response to the PATCH deletion request. + */ + function cleanUpConnections(creationResponse) { + + return connectionService.patchConnections( + $routeParams.dataSource, createDeletionPatches(creationResponse)) + + // TODO: Better error handling? Make additional cleanup requests? + .catch(handleError); + + } + + /** + * Given a successful response to a user PATCH request, make another + * request to delete every created user in the provided request. + * + * @param {DirectoryPatchResponse} creationResponse + * The response to the user PATCH request. + * + * @returns {DirectoryPatchResponse} + * The response to the PATCH deletion request. + */ + function cleanUpUsers(creationResponse) { + + return userService.patchUsers( + $routeParams.dataSource, createDeletionPatches(creationResponse)) + + // TODO: Better error handling? Make additional cleanup requests? + .catch(handleError); + + } + + /** + * Given a successful response to a user group PATCH request, make another + * request to delete every created user group in the provided request. + * + * @param {DirectoryPatchResponse} creationResponse + * The response to the user group PATCH creation request. + * + * @returns {DirectoryPatchResponse} + * The response to the PATCH deletion request. + */ + function cleanUpUserGroups(creationResponse) { + + return userGroupService.patchUserGroups( + $routeParams.dataSource, createDeletionPatches(creationResponse)) + + // TODO: Better error handling? Make additional cleanup requests? + .catch(handleError); + + } + + /** + * Make requests to delete all connections, users, and/or groups from any + * provided PATCH API responses. If any responses are not provided, no + * cleanup will be attempted. + * + * @param {DirectoryPatchResponse} connectionResponse + * The response to the connection PATCH creation request. + * + * @param {DirectoryPatchResponse} userResponse + * The response to the user PATCH creation request. + * + * @param {DirectoryPatchResponse} userGroupResponse + * The response to the user group PATCH creation request. + * + * @returns {Object} + * An object containing PATCH deletion responses corresponding to any + * provided connection, user, and/or user group creation responses. + */ + function cleanUpAll(connectionResponse, userResponse, userGroupResponse) { + + // All cleanup requests that need to be made + const requests = {}; + + // If the connection response was provided, clean up connections + if (connectionResponse) + requests.connectionCleanup = cleanUpConnections(connectionResponse); + + // If the user response was provided, clean up users + if (userResponse) + requests.userCleanup = cleanUpUsers(userResponse); + + // If the user group response was provided, clean up user groups + if (connectionResponse) + requests.userGroupCleanup = cleanUpUserGroups(userGroupResponse); + + // Return when all cleanup is complete + return $q.all(requests); + } + /** * Process a successfully parsed import file, creating any specified * connections, creating and granting permissions to any specified users - * and user groups. - * - * TODO: - * - Do batch import of connections - * - Create all users/groups not already present - * - Grant permissions to all users/groups as defined in the import file - * - On failure: Roll back everything (maybe ask the user first): - * - Attempt to delete all created connections - * - Attempt to delete any created users / groups + * and user groups. If successful, the user will be shown a success message. + * If not, any errors will be displayed, and the user will be given ???an + * option??? to roll back any already-created entities. * * @param {ParseResult} parseResult * The result of parsing the user-supplied import file. - * */ function handleParseSuccess(parseResult) { @@ -281,21 +358,20 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // First, attempt to create the connections connectionService.patchConnections(dataSource, parseResult.patches) - .then(response => { + .then(connectionResponse => { // If connection creation is successful, create users and groups - createUsersAndGroups(parseResult).then(() => { + createUsersAndGroups(parseResult).then( + ({userResponse, groupResponse}) => - grantConnectionPermissions(parseResult, response).then(results => { - console.log("permission requests", results); + grantConnectionPermissions(parseResult, connectionResponse) + .then(() => - // TODON'T: Delete connections so we can test over and over - cleanUpConnections(response); + // TODON'T: Delete the stuff so we can test over and over + cleanUpAll(connectionResponse, userResponse, groupResponse) + .then(resetUploadState) - resetUploadState(); - }) - - }); + )); }); } @@ -315,7 +391,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ console.error(error); $scope.error = error; - } + }; /** * Clear the current displayed error. @@ -356,14 +432,16 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // have already have filtered out any invalid file types else { handleError(new ParseError({ - message: 'Invalid file type: ' + type, + message: 'Invalid file type: ' + mimeType, key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', - variables: { TYPE: type } + variables: { TYPE: mimeType } })); return; } // Make the call to process the data into a series of patches + // TODO: Check if there's errors, and if so, display those rather than + // just YOLOing a create call processDataCallback(data) // Send the data off to be imported if parsing is successful @@ -476,6 +554,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // Read all the data into memory $scope.fileReader.readAsBinaryString(file); - } + }; }]); diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js index d988e7c7d..4e2c76b24 100644 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js @@ -19,17 +19,16 @@ /* global _ */ -/** - * A directive that allows for file upload, either through drag-and-drop or - * a file browser. - */ - /** * All legal import file types. Any file not belonging to one of these types * must be rejected. */ const LEGAL_FILE_TYPES = ["csv", "json", "yaml"]; +/** + * A directive that allows for file upload, either through drag-and-drop or + * a file browser. + */ angular.module('import').directive('connectionImportFileUpload', [ function connectionImportFileUpload() { diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 4b9f50c2d..cf15a374e 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -142,7 +142,7 @@ angular.module('import').factory('connectionParseService', // If there's no group to translate, do nothing if (!connection.group) - return; + return connection; // If both are specified, the parent group is ambigious if (connection.parentIdentifier) @@ -333,8 +333,9 @@ angular.module('import').factory('connectionParseService', service.parseYAML = function parseYAML(yamlData) { // Parse from YAML into a javascript array + let connectionData; try { - const connectionData = parseYAMLData(yamlData); + connectionData = parseYAMLData(yamlData); } // If the YAML parser throws an error, reject with that error. No @@ -366,8 +367,9 @@ angular.module('import').factory('connectionParseService', service.parseJSON = function parseJSON(jsonData) { // Parse from JSON into a javascript array + let connectionData; try { - const connectionData = JSON.parse(jsonData); + connectionData = JSON.parse(jsonData); } // If the JSON parse attempt throws an error, reject with that error. diff --git a/guacamole/src/main/frontend/src/app/import/styles/help.css b/guacamole/src/main/frontend/src/app/import/styles/help.css index e78acea4a..a73b43afa 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/help.css +++ b/guacamole/src/main/frontend/src/app/import/styles/help.css @@ -31,7 +31,6 @@ .import.help h2 { - padding-top: 0px; padding-bottom: 0px; } diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html index bd7f21845..e6063e89f 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html @@ -43,11 +43,11 @@ conn4,kubernetes,,,,, "protocol": "ssh", "parameters": { "hostname": "conn3.web.com" }, "group": "ROOT/Parent Group/Child Group", - "users": [ "guac user 2", "guac user 3" ], + "users": [ "guac user 2", "guac user 3" ] }, { "name": "conn4", - "protocol": "kubernetes", + "protocol": "kubernetes" } ] @@ -63,7 +63,7 @@ conn4,kubernetes,,,,, - guac user 1 - guac user 2 groups: - - AWS EC2 Administrators + - Connection 1 Users attributes: guacd-encryption: none - name: conn2 diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 06e3a4d9d..0233f8bb6 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -194,7 +194,8 @@ "HELP_HEADER": "Connection Import File Format", "HELP_FILE_TYPE_HEADER": "File Types", - "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified.", + "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified. Any users or user groups that do not exist in the current data source will be automatically created.", + "HELP_CSV_HEADER": "CSV Format", "HELP_CSV_DESCRIPTION": "A connection import CSV file has one connection record per row. Each column will specify a connection field. At minimum the connection name and protocol must be specified.", @@ -207,7 +208,7 @@ "HELP_YAML_HEADER": "YAML Format", "HELP_YAML_DESCRIPTION": "A connection import YAML file is a list of connection objects with exactly the same structure as the JSON format.", - "HELP_SEMICOLON_FOOTNOTE": "If needed, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", + "HELP_SEMICOLON_FOOTNOTE": "If present, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", "ERROR_AMBIGUOUS_CSV_HEADER": "Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter", From 322adbc294c2189019e84a278017fdf85d309658 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 23 Feb 2023 19:44:46 +0000 Subject: [PATCH 17/27] GUACAMOLE-926: Display connection-specific errors to the user. --- .../importConnectionsController.js | 57 ++-- .../directives/connectionImportErrors.js | 248 ++++++++++++++++++ .../directives/connectionImportFileUpload.js | 4 +- .../frontend/src/app/import/importModule.js | 2 +- .../import/services/connectionCSVService.js | 12 +- .../import/services/connectionParseService.js | 8 +- .../frontend/src/app/import/styles/import.css | 17 +- .../import/templates/connectionErrors.html | 45 ++++ .../import/templates/connectionImport.html | 9 +- .../templates/connectionImportFileHelp.html | 24 +- .../templates/connectionImportFileUpload.html | 8 +- .../src/app/import/types/DisplayErrorList.js | 83 ++++++ .../src/app/import/types/ImportConnection.js | 2 +- .../app/import/types/ImportConnectionError.js | 108 ++++++++ .../app/rest/services/connectionService.js | 4 +- .../main/frontend/src/app/rest/types/Error.js | 9 + .../main/frontend/src/translations/en.json | 16 +- 17 files changed, 598 insertions(+), 58 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js create mode 100644 guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html create mode 100644 guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js create mode 100644 guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 73acb1cc3..ec1227e95 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -49,6 +49,21 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ */ $scope.error = null; + /** + * The result of parsing the current upload, if successful. + * + * @type {ParseResult} + */ + $scope.parseResult = null; + + /** + * The failure associated with the current attempt to create connections + * through the API, if any. + * + * @type {Error} + */ + $scope.patchFailure = null;; + /** * True if the file is fully uploaded and ready to be processed, or false * otherwise. @@ -100,6 +115,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.fileData = null; $scope.mimeType = null; $scope.fileReader = null; + $scope.parseResult = null; + $scope.patchFailure = null; // Broadcast an event to clear the file upload UI $scope.$broadcast('clearFile'); @@ -254,10 +271,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function cleanUpConnections(creationResponse) { return connectionService.patchConnections( - $routeParams.dataSource, createDeletionPatches(creationResponse)) - - // TODO: Better error handling? Make additional cleanup requests? - .catch(handleError); + $routeParams.dataSource, createDeletionPatches(creationResponse)); } @@ -274,10 +288,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function cleanUpUsers(creationResponse) { return userService.patchUsers( - $routeParams.dataSource, createDeletionPatches(creationResponse)) - - // TODO: Better error handling? Make additional cleanup requests? - .catch(handleError); + $routeParams.dataSource, createDeletionPatches(creationResponse)); } @@ -294,10 +305,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function cleanUpUserGroups(creationResponse) { return userGroupService.patchUserGroups( - $routeParams.dataSource, createDeletionPatches(creationResponse)) - - // TODO: Better error handling? Make additional cleanup requests? - .catch(handleError); + $routeParams.dataSource, createDeletionPatches(creationResponse)); } @@ -352,6 +360,15 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ */ function handleParseSuccess(parseResult) { + $scope.processing = false; + $scope.parseResult = parseResult; + + // If errors were encounted during file parsing, abort further + // processing - the user will have a chance to fix the errors and try + // again + if (parseResult.hasErrors) + return; + const dataSource = $routeParams.dataSource; console.log("parseResult", parseResult); @@ -372,7 +389,12 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .then(resetUploadState) )); - }); + }) + + // If an error occured when the call to create the connections was made, + // skip any further processing - the user will have a chance to fix the + // problems and try again + .catch(patchFailure => { $scope.patchFailure = patchFailure; }); } /** @@ -433,7 +455,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ else { handleError(new ParseError({ message: 'Invalid file type: ' + mimeType, - key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', + key: 'IMPORT.INVALID_FILE_TYPE', variables: { TYPE: mimeType } })); return; @@ -459,8 +481,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ /** * @return {Boolean} - * True if import should be disabled, or false if cancellation - * should be allowed. + * True if import should be disabled, or false if import should be + * allowed. */ $scope.importDisabled = () => @@ -471,7 +493,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.processing; /** - * Cancel any in-progress upload, or clear any uploaded-but + * Cancel any in-progress upload, or clear any uploaded-but-errored-out + * batch. */ $scope.cancel = function() { diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js new file mode 100644 index 000000000..dcf42695a --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* global _ */ + +/** + * A directive that displays errors that occured during parsing of a connection + * import file, or errors that were returned from the API during the connection + * batch creation attempt. + */ +angular.module('import').directive('connectionImportErrors', [ + function connectionImportErrors() { + + const directive = { + restrict: 'E', + replace: true, + templateUrl: 'app/import/templates/connectionErrors.html', + scope: { + + /** + * The result of parsing the import file. Any errors in this file + * will be displayed to the user. + * + * @type ParseResult + */ + parseResult : '=', + + /** + * The error associated with an attempt to batch create the + * connections represented by the ParseResult, if the ParseResult + * had no errors. If the provided ParseResult has errors, no request + * should have been made, and any provided patch error will be + * ignored. + * + * @type Error + */ + patchFailure : '=', + + } + }; + + directive.controller = ['$scope', '$injector', + function connectionImportErrorsController($scope, $injector) { + + // Required types + const DisplayErrorList = $injector.get('DisplayErrorList'); + const ImportConnectionError = $injector.get('ImportConnectionError'); + const ParseError = $injector.get('ParseError'); + const SortOrder = $injector.get('SortOrder'); + + // Required services + const $q = $injector.get('$q'); + const $translate = $injector.get('$translate'); + + // There are errors to display if the parse result generated errors, or + // if the patch request failed + $scope.hasErrors = () => + !!_.get($scope, 'parseResult.hasErrors') || !!$scope.patchFailure; + + /** + * All connections with their associated errors for display. These may + * be either parsing failures, or errors returned from the API. Both + * error types will be adapted to a common display format, though the + * error types will never be mixed, because no REST request should ever + * be made if there are client-side parse errors. + * + * @type {ImportConnectionError[]} + */ + $scope.connectionErrors = []; + + /** + * SortOrder instance which maintains the sort order of the visible + * connection errors. + * + * @type SortOrder + */ + $scope.errorOrder = new SortOrder([ + 'rowNumber', + 'name', + 'protocol', + 'errors', + ]); + + /** + * Array of all connection error properties that are filterable. + * + * @type String[] + */ + $scope.filteredErrorProperties = [ + 'rowNumber', + 'name', + 'protocol', + 'errors', + ]; + + /** + * Generate a ImportConnectionError representing any errors associated + * with the row at the given index within the given parse result. + * + * @param {ParseResult} parseResult + * The result of parsing the connection import file. + * + * @param {Integer} index + * The current row within the import file, 0-indexed. + * + * @returns {ImportConnectionError} + * The connection error object associated with the given row in the + * given parse result. + */ + const generateConnectionError = (parseResult, index) => { + + // Get the patch associated with the current row + const patch = parseResult.patches[index]; + + // The value of a patch is just the Connection object + const connection = patch.value; + + return new ImportConnectionError({ + + // Add 1 to the index to get the position in the file + rowNumber: index + 1, + + // Basic connection information - name and protocol. + name: connection.name, + protocol: connection.protocol, + + // The group and parent identifiers, if any are set. Include + // both since these could be a potential source of conflict. + // TODO: Should we _really_ have both of these here? + group: connection.group, + parentIdentifier: connection.parentIdentifier, + + // Get the list of user and group identifiers from the parse + // result. There should one entry in each of these lists for + // each patch. + users: parseResult.users[index], + groups: parseResult.groups[index], + + // The human-readable error messages + errors: new DisplayErrorList( + [ ...(parseResult.errors[index] || []) ]) + }); + }; + + // If a new connection patch failure is seen, update the display list + $scope.$watch('patchFailure', async function patchFailureChanged(patchFailure) { + + const { parseResult } = $scope; + + // Do not attempt to process anything before the data has loaded + if (!patchFailure || !parseResult) + return; + + // Set up the list of connection errors based on the existing parse + // result, with error messages fetched from the patch failure + $scope.connectionErrors = parseResult.patches.map( + (patch, index) => { + + // Generate a connection error for display + const connectionError = generateConnectionError(parseResult, index); + + // Set the error from the PATCH request, if there is one + // TODO: These generally aren't translated from the backend - + // should we even bother trying to translate them? + const error = _.get(patchFailure, ['patches', index, 'error']); + if (error) + connectionError.errors = new DisplayErrorList([error]); + + return connectionError; + }); + }); + + // If a new parse result with errors is seen, update the display list + $scope.$watch('parseResult', async function parseResultChanged(parseResult) { + + // Do not process if there are no errors in the provided result + if (!parseResult || !parseResult.hasErrors) + return; + + // All promises from all translation requests. The scope will not be + // updated until all translations are ready. + const translationPromises = []; + + // The parse result should only be updated on a fresh file import; + // therefore it should be safe to skip checking the patch errors + // entirely - if set, they will be from the previous file and no + // longer relevant. + + // Set up the list of connection errors based on the updated parse + // result + const connectionErrors = parseResult.patches.map( + (patch, index) => { + + // Generate a connection error for display + const connectionError = generateConnectionError(parseResult, index); + + // Go through the errors and check if any are translateable + connectionError.errors.getArray().forEach( + (error, errorIndex) => { + + // If this error is a ParseError, it can be translated. + // NOTE: Generally one would translate error messages in the + // template, but in this case, the connection errors need to + // be raw strings in order to enable sorting and filtering. + if (error instanceof ParseError) + + // Fetch the translation and update it when it's ready + translationPromises.push($translate( + error.key, error.variables) + .then(translatedError => { + connectionError.errors.getArray()[errorIndex] = translatedError; + })); + + }); + + return connectionError; + + }); + + // Once all the translations have been completed, update the + // connectionErrors all in one go, to ensure no excessive reloading + $q.all(translationPromises).then(() => { + $scope.connectionErrors = connectionErrors; + }); + + }); + + }]; + + return directive; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js index 4e2c76b24..196e8f48f 100644 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js @@ -170,7 +170,7 @@ angular.module('import').directive('connectionImportFileUpload', [ // If the provided file is not one of the supported types, // display an error and abort processing - setError('CONNECTION_IMPORT.ERROR_INVALID_FILE_TYPE', + setError('IMPORT.ERROR_INVALID_FILE_TYPE', { TYPE: mimeType }); return; } @@ -204,7 +204,7 @@ angular.module('import').directive('connectionImportFileUpload', [ // If more than one file was provided, print an error explaining // that only a single file is allowed and abort processing - setError('CONNECTION_IMPORT.ERROR_FILE_SINGLE_ONLY'); + setError('IMPORT.ERROR_FILE_SINGLE_ONLY'); return; } diff --git a/guacamole/src/main/frontend/src/app/import/importModule.js b/guacamole/src/main/frontend/src/app/import/importModule.js index 6480d62fc..46e5fa157 100644 --- a/guacamole/src/main/frontend/src/app/import/importModule.js +++ b/guacamole/src/main/frontend/src/app/import/importModule.js @@ -21,4 +21,4 @@ * The module for code supporting importing user-supplied files. Currently, only * connection import is supported. */ -angular.module('import', ['rest']); +angular.module('import', ['rest', 'list']); diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js index 492b9388b..389749555 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -238,7 +238,7 @@ angular.module('import').factory('connectionCSVService', deferred.reject(new ParseError({ message: 'Duplicate CSV Header: ' + header, translatableMessage: new TranslatableMessage({ - key: 'CONNECTION_IMPORT.ERROR_DUPLICATE_CSV_HEADER', + key: 'IMPORT.ERROR_DUPLICATE_CSV_HEADER', variables: { HEADER: header } }) })); @@ -342,7 +342,7 @@ angular.module('import').factory('connectionCSVService', if (isAttribute && isParameter) throw new ParseError({ message: 'Ambiguous CSV Header: ' + header, - key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', + key: 'IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', variables: { HEADER: header } }); @@ -350,7 +350,7 @@ angular.module('import').factory('connectionCSVService', else if (!isAttribute && !isParameter) throw new ParseError({ message: 'Invalid CSV Header: ' + header, - key: 'CONNECTION_IMPORT.ERROR_INVALID_CSV_HEADER', + key: 'IMPORT.ERROR_INVALID_CSV_HEADER', variables: { HEADER: header } }); @@ -372,21 +372,21 @@ angular.module('import').factory('connectionCSVService', if (!nameGetter) return deferred.reject(new ParseError({ message: 'The connection name must be provided', - key: 'CONNECTION_IMPORT.ERROR_REQUIRED_NAME' + key: 'IMPORT.ERROR_REQUIRED_NAME' })); // Fail if the protocol wasn't provided if (!protocolGetter) return deferred.reject(new ParseError({ message: 'The connection protocol must be provided', - key: 'CONNECTION_IMPORT.ERROR_REQUIRED_PROTOCOL' + key: 'IMPORT.ERROR_REQUIRED_PROTOCOL' })); // If both are specified, the parent group is ambigious if (parentIdentifierGetter && groupGetter) throw new ParseError({ message: 'Only one of group or parentIdentifier can be set', - key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + key: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' }); // The function to transform a CSV row into a connection object diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index cf15a374e..dd9f49593 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -60,7 +60,7 @@ angular.module('import').factory('connectionParseService', if (!(parsedData instanceof Array)) return new ParseError({ message: 'Import data must be a list of connections', - key: 'CONNECTION_IMPORT.ERROR_ARRAY_REQUIRED' + key: 'IMPORT.ERROR_ARRAY_REQUIRED' }); // Make sure that the connection list is not empty - contains at least @@ -68,7 +68,7 @@ angular.module('import').factory('connectionParseService', if (!parsedData.length) return new ParseError({ message: 'The provided file is empty', - key: 'CONNECTION_IMPORT.ERROR_EMPTY_FILE' + key: 'IMPORT.ERROR_EMPTY_FILE' }); } @@ -148,7 +148,7 @@ angular.module('import').factory('connectionParseService', if (connection.parentIdentifier) throw new ParseError({ message: 'Only one of group or parentIdentifier can be set', - key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + key: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' }); // Look up the parent identifier for the specified group path @@ -158,7 +158,7 @@ angular.module('import').factory('connectionParseService', if (!identifier) throw new ParseError({ message: 'No group found named: ' + connection.group, - key: 'CONNECTION_IMPORT.ERROR_INVALID_GROUP', + key: 'IMPORT.ERROR_INVALID_GROUP', variables: { GROUP: connection.group } }); diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index 5addb1933..8f038e4d1 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -27,5 +27,18 @@ display: flex; gap: 10px; justify-content: center; - -} \ No newline at end of file + +} + + +.import .errors table { + width: 100%; +} + +.import .errors .error-message { + color: red; +} + +.import .errors .error-message ul { + margin: 0px; +} diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html b/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html new file mode 100644 index 000000000..eee23c260 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html @@ -0,0 +1,45 @@ +
+ + + + + + + + + + + + + + + + + + + + + + +
+ {{'IMPORT.TABLE_HEADER_ROW_NUMBER' | translate}} + + {{'IMPORT.TABLE_HEADER_NAME' | translate}} + + {{'IMPORT.TABLE_HEADER_PROTOCOL' | translate}} + + {{'IMPORT.TABLE_HEADER_ERRORS' | translate}} +
{{error.rowNumber}}{{error.name}}{{error.protocol}} +
    +
  • + {{ message }} +
  • +
+
+ + + +
diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index 881cb2429..d0f9fd537 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -1,7 +1,7 @@
-

{{'CONNECTION_IMPORT.HEADER' | translate}}

+

{{'IMPORT.HEADER' | translate}}

@@ -10,11 +10,11 @@
@@ -29,5 +29,8 @@ {{error.message}}

+ + +
diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html index e6063e89f..8e88d235a 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html @@ -1,25 +1,25 @@
-

{{'CONNECTION_IMPORT.HELP_HEADER' | translate}}

+

{{'IMPORT.HELP_HEADER' | translate}}

-

{{'CONNECTION_IMPORT.HELP_FILE_TYPE_HEADER' | translate}}

-

{{'CONNECTION_IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}

+

{{'IMPORT.HELP_FILE_TYPE_HEADER' | translate}}

+

{{'IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}

-

{{'CONNECTION_IMPORT.HELP_CSV_HEADER' | translate}}

-

{{'CONNECTION_IMPORT.HELP_CSV_DESCRIPTION' | translate}}

-

{{'CONNECTION_IMPORT.HELP_CSV_MORE_DETAILS' | translate}}

+

{{'IMPORT.HELP_CSV_HEADER' | translate}}

+

{{'IMPORT.HELP_CSV_DESCRIPTION' | translate}}

+

{{'IMPORT.HELP_CSV_MORE_DETAILS' | translate}}

name,protocol,hostname,group,users,groups,guacd-encryption (attribute)
 conn1,vnc,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none
 conn2,rdp,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl
 conn3,ssh,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,
 conn4,kubernetes,,,,,
-

{{'CONNECTION_IMPORT.HELP_JSON_HEADER' | translate}}

-

{{'CONNECTION_IMPORT.HELP_JSON_DESCRIPTION' | translate}}

-

{{'CONNECTION_IMPORT.HELP_JSON_MORE_DETAILS' | translate}}

+

{{'IMPORT.HELP_JSON_HEADER' | translate}}

+

{{'IMPORT.HELP_JSON_DESCRIPTION' | translate}}

+

{{'IMPORT.HELP_JSON_MORE_DETAILS' | translate}}

[
   {
     "name": "conn1",
@@ -51,8 +51,8 @@ conn4,kubernetes,,,,,
} ] -

{{'CONNECTION_IMPORT.HELP_YAML_HEADER' | translate}}

-

{{'CONNECTION_IMPORT.HELP_YAML_DESCRIPTION' | translate}}

+

{{'IMPORT.HELP_YAML_HEADER' | translate}}

+

{{'IMPORT.HELP_YAML_DESCRIPTION' | translate}}

---
   - name: conn1
     protocol: vnc
@@ -87,7 +87,7 @@ conn4,kubernetes,,,,,
protocol: kubernetes
    -
  1. {{'CONNECTION_IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}
  2. +
  3. {{'IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}
diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html index 08dbaf511..188567c00 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html @@ -1,20 +1,20 @@
- {{'CONNECTION_IMPORT.UPLOAD_FILE_TYPES' | translate}} + {{'IMPORT.UPLOAD_FILE_TYPES' | translate}} {{'CONNECTION_IMPORT.UPLOAD_HELP_LINK' | translate}} + class="file-help-link">{{'IMPORT.UPLOAD_HELP_LINK' | translate}}
-
{{'CONNECTION_IMPORT.UPLOAD_DROP_TITLE' | translate}}
+
{{'IMPORT.UPLOAD_DROP_TITLE' | translate}}
- {{'CONNECTION_IMPORT.UPLOAD_BROWSE_LINK' | translate}} + {{'IMPORT.UPLOAD_BROWSE_LINK' | translate}}
{{fileName}}
diff --git a/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js b/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js new file mode 100644 index 000000000..d775c5734 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Service which defines the DisplayErrorList class. + */ +angular.module('import').factory('DisplayErrorList', [ + function defineDisplayErrorList() { + + /** + * A list of human-readable error messages, intended to be usable in a + * sortable / filterable table. + * + * @constructor + * @param {String[]} messages + * The error messages that should be prepared for display. + */ + const DisplayErrorList = function DisplayErrorList(messages) { + + // Use empty message list by default + this.messages = messages || []; + + // The single String message composed of all messages concatenated + // together. This will be used for filtering / sorting, and should only + // be calculated once. + this.cachedMessage = null; + + }; + + /** + * Return a sortable / filterable representation of all the error messages + * wrapped by this DisplayErrorList. + * + * NOTE: Once this method is called, any changes to the underlying array + * will have no effect. This is to ensure that repeated calls to toString() + * by sorting / filtering UI code will not regenerate the concatenated + * message every time. + * + * @returns {String} + * A sortable / filterable representation of the error messages wrapped + * by this DisplayErrorList + */ + DisplayErrorList.prototype.toString = function messageListToString() { + + // Generate the concatenated message if not already generated + if (!this.concatenatedMessage) + this.concatenatedMessage = this.messages.join(' '); + + return this.concatenatedMessage; + + } + + /** + * Return the underlying array containing the raw error messages, wrapped + * by this DisplayErrorList. + * + * @returns {String[]} + * The underlying array containing the raw error messages, wrapped by + * this DisplayErrorList + */ + DisplayErrorList.prototype.getArray = function getUnderlyingArray() { + return this.messages; + } + + return DisplayErrorList; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js index 9f336ef77..5639ba1c6 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js +++ b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js @@ -32,7 +32,7 @@ angular.module('import').factory('ImportConnection', [ * The object whose properties should be copied within the new * Connection. */ - var ImportConnection = function ImportConnection(template) { + const ImportConnection = function ImportConnection(template) { // Use empty object by default template = template || {}; diff --git a/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js new file mode 100644 index 000000000..4d4fd3661 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Service which defines the ImportConnectionError class. + */ +angular.module('import').factory('ImportConnectionError', ['$injector', + function defineImportConnectionError($injector) { + + // Required types + const DisplayErrorList = $injector.get('DisplayErrorList'); + + /** + * A representation of a connection to be imported, as parsed from an + * user-supplied import file. + * + * @constructor + * @param {ImportConnection|Object} [template={}] + * The object whose properties should be copied within the new + * Connection. + */ + const ImportConnectionError = function ImportConnectionError(template) { + + // Use empty object by default + template = template || {}; + + /** + * The row number within the original connection import file for this + * connection. This should be 1-indexed. + */ + this.rowNumber = template.rowNumber; + + /** + * The unique identifier of the connection group that contains this + * connection. + * + * @type String + */ + this.parentIdentifier = template.parentIdentifier; + + /** + * The path to the connection group that contains this connection, + * written as e.g. "ROOT/parent/child/group". + * + * @type String + */ + this.group = template.group; + + /** + * The human-readable name of this connection, which is not necessarily + * unique. + * + * @type String + */ + this.name = template.name; + + /** + * The name of the protocol associated with this connection, such as + * "vnc" or "rdp". + * + * @type String + */ + this.protocol = template.protocol; + + /** + * The identifiers of all users who should be granted read access to + * this connection. + * + * @type String[] + */ + this.users = template.users || []; + + /** + * The identifiers of all user groups who should be granted read access + * to this connection. + * + * @type String[] + */ + this.groups = template.groups || []; + + /** + * The error messages associated with this particular connection, if any. + * + * @type ImportConnectionError + */ + this.errors = template.errors || new DisplayErrorList(); + + }; + + return ImportConnectionError; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js index 5c1451ec2..c46874ea4 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js @@ -24,9 +24,11 @@ angular.module('rest').factory('connectionService', ['$injector', function connectionService($injector) { // Required services - var requestService = $injector.get('requestService'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); + + // Required types + const Error = $injector.get('Error'); var service = {}; diff --git a/guacamole/src/main/frontend/src/app/rest/types/Error.js b/guacamole/src/main/frontend/src/app/rest/types/Error.js index 47f9cf770..74b28ebae 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/Error.js +++ b/guacamole/src/main/frontend/src/app/rest/types/Error.js @@ -78,6 +78,15 @@ angular.module('rest').factory('Error', [function defineError() { */ this.expected = template.expected; + /** + * The outcome for each patch that was submitted as part of the request + * that generated this error, if the request was a directory PATCH + * request. In all other cases, this will be null. + * + * @type DirectoryPatchOutcome[] + */ + this.patches = template.patches || null; + }; /** diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 0233f8bb6..c5292b80f 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -184,11 +184,13 @@ }, - "CONNECTION_IMPORT": { + "IMPORT": { "BUTTON_CANCEL": "Cancel", "BUTTON_IMPORT": "Import Connections", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "HEADER": "Connection Import", "HELP_HEADER": "Connection Import File Format", @@ -196,7 +198,6 @@ "HELP_FILE_TYPE_HEADER": "File Types", "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified. Any users or user groups that do not exist in the current data source will be automatically created.", - "HELP_CSV_HEADER": "CSV Format", "HELP_CSV_DESCRIPTION": "A connection import CSV file has one connection record per row. Each column will specify a connection field. At minimum the connection name and protocol must be specified.", "HELP_CSV_MORE_DETAILS": "The CSV header for each row specifies the connection field. The connection group ID that the connection should be imported into may be directly specified with \"parentIdentifier\", or the path to the parent group may be specified using \"group\" as shown below. In most cases, there should be no conflict between fields, but if needed, an \" (attribute)\" or \" (parameter)\" suffix may be added to disambiguate. Lists of user or user group identifiers must be semicolon-seperated.¹", @@ -234,12 +235,17 @@ "ERROR_REQUIRED_NAME": "No connection name found in the provided file", + "ERROR_FILE_SINGLE_ONLY": "Please upload only a single file at a time", + + "TABLE_HEADER_NAME" : "Name", + "TABLE_HEADER_PROTOCOL" : "Protocol", + "TABLE_HEADER_ERRORS" : "Errors", + "TABLE_HEADER_ROW_NUMBER": "Row Number", + "UPLOAD_FILE_TYPES": "CSV, JSON, or YAML", "UPLOAD_HELP_LINK": "View Format Tips", "UPLOAD_DROP_TITLE": "Drop a File Here", - "UPLOAD_BROWSE_LINK": "Browse for File", - - "ERROR_FILE_SINGLE_ONLY": "Please upload only a single file at a time" + "UPLOAD_BROWSE_LINK": "Browse for File" }, From 2ce7876f26845502a2d5b73f616ab7b8b546f867 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 3 Mar 2023 02:38:47 +0000 Subject: [PATCH 18/27] GUACAMOLE-926: Migrate upload directive into import page controller since they're so tangled up together to make it not worth seperating them. --- .../importConnectionsController.js | 178 ++++++++++++- .../directives/connectionImportFileUpload.js | 252 ------------------ .../src/app/import/styles/file-upload.css | 119 --------- .../frontend/src/app/import/styles/import.css | 101 +++++++ .../import/templates/connectionImport.html | 26 +- .../templates/connectionImportFileUpload.html | 30 --- 6 files changed, 300 insertions(+), 406 deletions(-) delete mode 100644 guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js delete mode 100644 guacamole/src/main/frontend/src/app/import/styles/file-upload.css delete mode 100644 guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index ec1227e95..14da850f9 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -25,9 +25,14 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$injector', function importConnectionsController($scope, $injector) { + // The file types supported for connection import + const LEGAL_FILE_TYPES = ['csv', 'json', 'yaml']; + // Required services + const $document = $injector.get('$document'); const $q = $injector.get('$q'); const $routeParams = $injector.get('$routeParams'); + const $timeout = $injector.get('$timeout'); const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); const permissionService = $injector.get('permissionService'); @@ -112,11 +117,13 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.aborted = false; $scope.dataReady = false; $scope.processing = false; + $scope.error = null; $scope.fileData = null; $scope.mimeType = null; $scope.fileReader = null; $scope.parseResult = null; $scope.patchFailure = null; + $scope.fileName = null; // Broadcast an event to clear the file upload UI $scope.$broadcast('clearFile'); @@ -410,7 +417,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ resetUploadState(); // Set the error for display - console.error(error); $scope.error = error; }; @@ -462,8 +468,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } // Make the call to process the data into a series of patches - // TODO: Check if there's errors, and if so, display those rather than - // just YOLOing a create call processDataCallback(data) // Send the data off to be imported if parsing is successful @@ -537,7 +541,30 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ * @argument {File} file * The file to upload onto the scope for further processing. */ - $scope.handleFile = function(file) { + const handleFile = file => { + + // Clear any error from a previous attempted file upload + clearError(); + + // The MIME type of the provided file + const mimeType = file.type; + + // Check if the mimetype ends with one of the supported types, + // e.g. "application/json" or "text/csv" + if (_.every(LEGAL_FILE_TYPES.map( + type => !mimeType.endsWith(type)))) { + + // If the provided file is not one of the supported types, + // display an error and abort processing + handleError(new ParseError({ + message: "Invalid file type: " + type, + key: 'IMPORT.ERROR_INVALID_FILE_TYPE', + variables: { TYPE: mimeType } + })); + return; + } + + $scope.fileName = file.name; // Clear any error message from the previous upload attempt clearError(); @@ -578,5 +605,148 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // Read all the data into memory $scope.fileReader.readAsBinaryString(file); }; + + /** + * Whether a drag/drop operation is currently in progress (the user has + * dragged a file over the Guacamole connection but has not yet + * dropped it). + * + * @type boolean + */ + $scope.dropPending = false; + + /** + * The name of the file that's currently being uploaded, or has yet to + * be imported, if any. + */ + $scope.fileName = null; + + /** + * The container for the file upload UI. + * + * @type Element + * + */ + const uploadContainer = angular.element( + $document.find('.file-upload-container')); + + /** + * The location where files can be dragged-and-dropped to. + * + * @type Element + */ + const dropTarget = uploadContainer.find('.drop-target'); + + /** + * Displays a visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param {Event} e + * The event related to the in-progress drag/drop operation. + */ + const notifyDragStart = function notifyDragStart(e) { + + e.preventDefault(); + e.stopPropagation(); + + $scope.$apply(() => { + $scope.dropPending = true; + }); + + }; + + /** + * Removes the visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param {Event} e + * The event related to the end of the former drag/drop operation. + */ + const notifyDragEnd = function notifyDragEnd(e) { + + e.preventDefault(); + e.stopPropagation(); + + $scope.$apply(() => { + $scope.dropPending = false; + }); + + }; + + // Add listeners to the drop target to ensure that the visual state + // stays up to date + dropTarget.on('dragenter', notifyDragStart); + dropTarget.on('dragover', notifyDragStart); + dropTarget.on('dragleave', notifyDragEnd); + + /** + * Drop target event listener that will be invoked if the user drops + * anything onto the drop target. If a valid file is provided, the + * onFile callback provided to this directive will be called; otherwise + * an error will be displayed, if appropriate. + * + * @param {Event} e + * The drop event that triggered this handler. + */ + dropTarget.on('drop', e => { + + notifyDragEnd(e); + + const files = e.originalEvent.dataTransfer.files; + + // Ignore any non-files that are dragged into the drop area + if (files.length < 1) + return; + + if (files.length >= 2) { + + // If more than one file was provided, print an error explaining + // that only a single file is allowed and abort processing + handleError(new ParseError({ + message: 'Only a single file may be imported at once', + key: 'IMPORT.ERROR_FILE_SINGLE_ONLY' + })); + return; + } + + handleFile(files[0]); + + }); + + /** + * The hidden file input used to create a file browser. + * + * @type Element + */ + const fileUploadInput = uploadContainer.find('.file-upload-input'); + + /** + * A function that will click on the hidden file input to open a file + * browser to allow the user to select a file for upload. + */ + $scope.openFileBrowser = () => + $timeout(() => fileUploadInput.click(), 0, false); + + /** + * A handler that will be invoked when a user selectes a file in the + * file browser. After some error checking, the file will be passed to + * the onFile callback provided to this directive. + * + * @param {Event} e + * The event that was triggered when the user selected a file in + * their file browser. + */ + fileUploadInput.on('change', e => { + + // Process the uploaded file + handleFile(e.target.files[0]); + + // Clear the value to ensure that the change event will be fired + // if the user selects the same file again + fileUploadInput.value = null; + + }); }]); diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js deleted file mode 100644 index 196e8f48f..000000000 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/* global _ */ - -/** - * All legal import file types. Any file not belonging to one of these types - * must be rejected. - */ -const LEGAL_FILE_TYPES = ["csv", "json", "yaml"]; - -/** - * A directive that allows for file upload, either through drag-and-drop or - * a file browser. - */ -angular.module('import').directive('connectionImportFileUpload', [ - function connectionImportFileUpload() { - - const directive = { - restrict: 'E', - replace: true, - templateUrl: 'app/import/templates/connectionImportFileUpload.html', - scope: { - - /** - * The function to invoke when a file is provided to the file upload - * UI, either by dragging and dropping, or by navigating using the - * file browser. The function will be called with 2 arguments - the - * mime type, and the raw string contents of the file. - * - * @type function - */ - onFile : '&', - } - }; - - directive.controller = ['$scope', '$injector', '$element', - function fileUploadController($scope, $injector, $element) { - - // Required services - const $timeout = $injector.get('$timeout'); - - /** - * Whether a drag/drop operation is currently in progress (the user has - * dragged a file over the Guacamole connection but has not yet - * dropped it). - * - * @type boolean - */ - $scope.dropPending = false; - - /** - * The error associated with the file upload, if any. An object of the - * form { key, variables }, or null if no error has occured. - */ - $scope.error = null; - - /** - * The name of the file that's currently being uploaded, or has yet to - * be imported, if any. - */ - $scope.fileName = null; - - // Clear the file if instructed to do so by the parent - $scope.$on('clearFile', () => delete $scope.fileName); - - /** - * Clear any displayed error message. - */ - const clearError = () => $scope.error = null; - - /** - * Set an error for display using the provided translation key and - * translation variables. - * - * @param {String} key - * The translation key. - * - * @param {Object.} variables - * The variables to subsitute into the message, if any. - */ - const setError = (key, variables) => $scope.error = { key, variables }; - - /** - * The location where files can be dragged-and-dropped to. - * - * @type Element - */ - const dropTarget = $element.find('.drop-target')[0]; - - /** - * Displays a visual indication that dropping the file currently - * being dragged is possible. Further propagation and default behavior - * of the given event is automatically prevented. - * - * @param {Event} e - * The event related to the in-progress drag/drop operation. - */ - const notifyDragStart = function notifyDragStart(e) { - - e.preventDefault(); - e.stopPropagation(); - - $scope.$apply(() => { - $scope.dropPending = true; - }); - - }; - - /** - * Removes the visual indication that dropping the file currently - * being dragged is possible. Further propagation and default behavior - * of the given event is automatically prevented. - * - * @param {Event} e - * The event related to the end of the former drag/drop operation. - */ - const notifyDragEnd = function notifyDragEnd(e) { - - e.preventDefault(); - e.stopPropagation(); - - $scope.$apply(() => { - $scope.dropPending = false; - }); - - }; - - // Add listeners to the drop target to ensure that the visual state - // stays up to date - dropTarget.addEventListener('dragenter', notifyDragStart, false); - dropTarget.addEventListener('dragover', notifyDragStart, false); - dropTarget.addEventListener('dragleave', notifyDragEnd, false); - - /** - * Given a user-supplied file, validate that the file type is correct, - * and invoke the onFile callback provided to this directive if so. - * - * @param {File} file - * The user-supplied file. - */ - function handleFile(file) { - - // Clear any error from a previous attempted file upload - clearError(); - - // The MIME type of the provided file - const mimeType = file.type; - - // Check if the mimetype ends with one of the supported types, - // e.g. "application/json" or "text/csv" - if (_.every(LEGAL_FILE_TYPES.map( - type => !mimeType.endsWith(type)))) { - - // If the provided file is not one of the supported types, - // display an error and abort processing - setError('IMPORT.ERROR_INVALID_FILE_TYPE', - { TYPE: mimeType }); - return; - } - - $scope.fileName = file.name; - - // Invoke the provided file callback using the file - $scope.onFile({ file }); - } - - /** - * Drop target event listener that will be invoked if the user drops - * anything onto the drop target. If a valid file is provided, the - * onFile callback provided to this directive will be called; otherwise - * an error will be displayed, if appropriate. - * - * @param {Event} e - * The drop event that triggered this handler. - */ - dropTarget.addEventListener('drop', function(e) { - - notifyDragEnd(e); - - const files = e.dataTransfer.files; - - // Ignore any non-files that are dragged into the drop area - if (files.length < 1) - return; - - if (files.length > 2) { - - // If more than one file was provided, print an error explaining - // that only a single file is allowed and abort processing - setError('IMPORT.ERROR_FILE_SINGLE_ONLY'); - return; - } - - handleFile(files[0]); - - }, false); - - /** - * The hidden file input used to create a file browser. - * - * @type Element - */ - const fileUploadInput = $element.find('.file-upload-input')[0]; - - /** - * A function that will click on the hidden file input to open a file - * browser to allow the user to select a file for upload. - */ - $scope.openFileBrowser = () => - $timeout(() => fileUploadInput.click(), 0, false); - - /** - * A handler that will be invoked when a user selectes a file in the - * file browser. After some error checking, the file will be passed to - * the onFile callback provided to this directive. - * - * @param {Event} e - * The event that was triggered when the user selected a file in - * their file browser. - */ - fileUploadInput.onchange = e => { - - // Process the uploaded file - handleFile(e.target.files[0]); - - // Clear the value to ensure that the change event will be fired - // if the user selects the same file again - fileUploadInput.value = null; - - }; - - }]; - return directive; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/styles/file-upload.css b/guacamole/src/main/frontend/src/app/import/styles/file-upload.css deleted file mode 100644 index afe0fcf97..000000000 --- a/guacamole/src/main/frontend/src/app/import/styles/file-upload.css +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -.file-upload-container { - - display: flex; - flex-direction: column; - align-items: center; - padding: 24px 24px 24px; - - width: fit-content; - - border: 1px solid rgba(0,0,0,.25); - box-shadow: 1px 1px 2px rgb(0 0 0 / 25%); - - margin-left: auto; - margin-right: auto; - -} - -.file-upload-container .upload-header { - - display: flex; - flex-direction: row; - width: 500px; - margin-bottom: 5px; - justify-content: space-between; - -} - -.file-upload-container .file-error { - - color: red; - -} - -.file-upload-container .file-options { - - font-weight: bold; - -} - -.file-upload-container .file-upload-input { - - display: none; - -} - -.file-upload-container .drop-target { - - display: flex; - flex-direction: column; - - align-items: center; - justify-content: space-evenly; - - width: 500px; - height: 200px; - - background: rgba(0,0,0,.04); - border: 1px solid black; - -} - -.file-upload-container .drop-target.file-present { - - background: rgba(0,0,0,.15); - -} - - -.file-upload-container .drop-target .file-name { - - font-weight: bold; - font-size: 1.5em; - -} - -.file-upload-container .drop-target.drop-pending { - - background: #3161a9; - -} - -.file-upload-container .drop-target.drop-pending > * { - - opacity: 0.5; - -} - -.file-upload-container .drop-target .title { - - font-weight: bold; - font-size: 1.25em; - -} - -.file-upload-container .drop-target .browse-link { - - text-decoration: underline; - cursor: pointer; - -} diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index 8f038e4d1..3fbdbe077 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -42,3 +42,104 @@ .import .errors .error-message ul { margin: 0px; } + +.file-upload-container { + + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 24px 24px; + + width: fit-content; + + border: 1px solid rgba(0,0,0,.25); + box-shadow: 1px 1px 2px rgb(0 0 0 / 25%); + + margin-left: auto; + margin-right: auto; + +} + +.file-upload-container .upload-header { + + display: flex; + flex-direction: row; + width: 500px; + margin-bottom: 5px; + justify-content: space-between; + +} + +.file-upload-container .file-error { + + color: red; + +} + +.file-upload-container .file-options { + + font-weight: bold; + +} + +.file-upload-container .file-upload-input { + + display: none; + +} + +.file-upload-container .drop-target { + + display: flex; + flex-direction: column; + + align-items: center; + justify-content: space-evenly; + + width: 500px; + height: 200px; + + background: rgba(0,0,0,.04); + border: 1px solid black; + +} + +.file-upload-container .drop-target.file-present { + + background: rgba(0,0,0,.15); + +} + + +.file-upload-container .drop-target .file-name { + + font-weight: bold; + font-size: 1.5em; + +} + +.file-upload-container .drop-target.drop-pending { + + background: #3161a9; + +} + +.file-upload-container .drop-target.drop-pending > * { + + opacity: 0.5; + +} + +.file-upload-container .drop-target .title { + + font-weight: bold; + font-size: 1.25em; + +} + +.file-upload-container .drop-target .browse-link { + + text-decoration: underline; + cursor: pointer; + +} \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index d0f9fd537..c9e37be46 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -4,8 +4,32 @@

{{'IMPORT.HEADER' | translate}}

+ +
+ +
+ {{'IMPORT.UPLOAD_FILE_TYPES' | translate}} + {{'IMPORT.UPLOAD_HELP_LINK' | translate}} + +
+ +
+ +
{{'IMPORT.UPLOAD_DROP_TITLE' | translate}}
+ + + + {{'IMPORT.UPLOAD_BROWSE_LINK' | translate}} + + +
{{fileName}}
+ +
+ +
-
From eadc1779a0d2b008afd70fa7d9855fec39efe722 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 3 Mar 2023 19:38:46 +0000 Subject: [PATCH 19/27] GUACAMOLE-926: Import/upload UI improvements. --- .../importConnectionsController.js | 9 ++-- .../frontend/src/app/import/importModule.js | 2 +- .../frontend/src/app/import/styles/import.css | 26 ++++++++++- .../import/templates/connectionImport.html | 44 ++++++++++++------- .../main/frontend/src/translations/en.json | 6 ++- 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 14da850f9..124b69555 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -43,7 +43,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); const PermissionSet = $injector.get('PermissionSet'); - const TranslatableMessage = $injector.get('TranslatableMessage'); const User = $injector.get('User'); const UserGroup = $injector.get('UserGroup'); @@ -424,7 +423,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ /** * Clear the current displayed error. */ - const clearError = () => delete $scope.error; + $scope.clearError = () => delete $scope.error; /** * Process the uploaded import file, importing the connections, granting @@ -503,7 +502,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.cancel = function() { // Clear any error message - clearError(); + $scope.clearError(); // If the upload is in progress, stop it now; the FileReader will // reset the upload state when it stops @@ -544,7 +543,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ const handleFile = file => { // Clear any error from a previous attempted file upload - clearError(); + $scope.clearError(); // The MIME type of the provided file const mimeType = file.type; @@ -567,7 +566,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.fileName = file.name; // Clear any error message from the previous upload attempt - clearError(); + $scope.clearError(); // Initialize upload state $scope.aborted = false; diff --git a/guacamole/src/main/frontend/src/app/import/importModule.js b/guacamole/src/main/frontend/src/app/import/importModule.js index 46e5fa157..164ec2b16 100644 --- a/guacamole/src/main/frontend/src/app/import/importModule.js +++ b/guacamole/src/main/frontend/src/app/import/importModule.js @@ -21,4 +21,4 @@ * The module for code supporting importing user-supplied files. Currently, only * connection import is supported. */ -angular.module('import', ['rest', 'list']); +angular.module('import', ['rest', 'list', 'notification']); diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index 3fbdbe077..0b813083d 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -60,6 +60,17 @@ } +.file-upload-container.file-selected { + display: flex; + flex-direction: row; + gap: 100px; +} + +.file-upload-container .clear { + margin: 0; + padding: +} + .file-upload-container .upload-header { display: flex; @@ -142,4 +153,17 @@ text-decoration: underline; cursor: pointer; -} \ No newline at end of file +} + +.import .parseError { + display: flex; + justify-content: center; + padding: 0.75em; + margin-bottom: 10px; + gap: 3em; +} + +.import .parseError .clear { + cursor: pointer; +} + diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index c9e37be46..488b38160 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -4,8 +4,32 @@

{{'IMPORT.HEADER' | translate}}

+ +
+ + + + + + + {{error.message}} + + + {{'IMPORT.BUTTON_CLEAR_ERROR' | translate}} + +
+ +
+
{{fileName}}
+ +
-
+
{{'IMPORT.UPLOAD_FILE_TYPES' | translate}} @@ -17,20 +41,19 @@
-
{{'IMPORT.UPLOAD_DROP_TITLE' | translate}}
+
{{'IMPORT.UPLOAD_DROP_TITLE' | translate}}
- + {{'IMPORT.UPLOAD_BROWSE_LINK' | translate}} -
{{fileName}}
+
{{fileName}}
-
- -

- - -

- {{error.message}} -

- diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index c5292b80f..f80e91a5a 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -187,8 +187,12 @@ "IMPORT": { "BUTTON_CANCEL": "Cancel", + "BUTTON_CLEAR" : "Clear", + "BUTTON_CLEAR_ERROR": "✖", "BUTTON_IMPORT": "Import Connections", + "ERROR_HEADER": "Error", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HEADER": "Connection Import", @@ -240,7 +244,7 @@ "TABLE_HEADER_NAME" : "Name", "TABLE_HEADER_PROTOCOL" : "Protocol", "TABLE_HEADER_ERRORS" : "Errors", - "TABLE_HEADER_ROW_NUMBER": "Row Number", + "TABLE_HEADER_ROW_NUMBER": "Row #", "UPLOAD_FILE_TYPES": "CSV, JSON, or YAML", "UPLOAD_HELP_LINK": "View Format Tips", From 7bf192fd73018bb8b61a9626d34d786b648255f0 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 9 Mar 2023 19:14:13 +0000 Subject: [PATCH 20/27] GUACAMOLE-926: Switch to standard notification style. --- .../importConnectionsController.js | 67 ++++++++----------- .../frontend/src/app/import/styles/import.css | 19 ------ .../import/templates/connectionImport.html | 17 ----- .../main/frontend/src/translations/en.json | 3 +- 4 files changed, 30 insertions(+), 76 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 124b69555..ea06979e7 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -35,6 +35,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ const $timeout = $injector.get('$timeout'); const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); + const guacNotification = $injector.get('guacNotification'); const permissionService = $injector.get('permissionService'); const userService = $injector.get('userService'); const userGroupService = $injector.get('userGroupService'); @@ -45,14 +46,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ const PermissionSet = $injector.get('PermissionSet'); const User = $injector.get('User'); const UserGroup = $injector.get('UserGroup'); - - /** - * Any error that may have occured during import file parsing. - * - * @type {ParseError} - */ - $scope.error = null; - + /** * The result of parsing the current upload, if successful. * @@ -116,7 +110,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.aborted = false; $scope.dataReady = false; $scope.processing = false; - $scope.error = null; $scope.fileData = null; $scope.mimeType = null; $scope.fileReader = null; @@ -404,7 +397,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } /** - * Set any caught error message to the scope for display. + * Display the provided error to the user in a dismissable dialog. * * @argument {ParseError} error * The error to display. @@ -415,15 +408,24 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // all upload state to allow for a fresh retry resetUploadState(); - // Set the error for display - $scope.error = error; - + guacNotification.showStatus({ + className : 'error', + title : 'IMPORT.DIALOG_HEADER_ERROR', + + // Use the translation key if available + text : { + key: error.key || error.message, + variables: error.variables + }, + + // Add a button to hide the error + actions : [{ + name : 'IMPORT.BUTTON_CLEAR', + callback : () => guacNotification.showStatus(false) + }] + }) + }; - - /** - * Clear the current displayed error. - */ - $scope.clearError = () => delete $scope.error; /** * Process the uploaded import file, importing the connections, granting @@ -455,16 +457,14 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ else if (mimeType.endsWith("yaml")) processDataCallback = connectionParseService.parseYAML; - // We don't expect this to happen - the file upload directive should - // have already have filtered out any invalid file types - else { - handleError(new ParseError({ - message: 'Invalid file type: ' + mimeType, - key: 'IMPORT.INVALID_FILE_TYPE', - variables: { TYPE: mimeType } - })); - return; - } + // The file type was validated before being uploaded - this should + // never happen + else + processDataCallback = () => { + throw new ParseError({ + message: "Unexpected invalid file type: " + mimeType + }); + }; // Make the call to process the data into a series of patches processDataCallback(data) @@ -501,9 +501,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ */ $scope.cancel = function() { - // Clear any error message - $scope.clearError(); - // If the upload is in progress, stop it now; the FileReader will // reset the upload state when it stops if ($scope.fileReader) { @@ -542,9 +539,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ */ const handleFile = file => { - // Clear any error from a previous attempted file upload - $scope.clearError(); - // The MIME type of the provided file const mimeType = file.type; @@ -556,7 +550,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // If the provided file is not one of the supported types, // display an error and abort processing handleError(new ParseError({ - message: "Invalid file type: " + type, + message: "Invalid file type: " + mimeType, key: 'IMPORT.ERROR_INVALID_FILE_TYPE', variables: { TYPE: mimeType } })); @@ -564,9 +558,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } $scope.fileName = file.name; - - // Clear any error message from the previous upload attempt - $scope.clearError(); // Initialize upload state $scope.aborted = false; diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index 0b813083d..656571c63 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -17,10 +17,6 @@ * under the License. */ -.import .parseError { - color: red; -} - .import .import-buttons { margin-top: 10px; @@ -30,7 +26,6 @@ } - .import .errors table { width: 100%; } @@ -68,7 +63,6 @@ .file-upload-container .clear { margin: 0; - padding: } .file-upload-container .upload-header { @@ -154,16 +148,3 @@ cursor: pointer; } - -.import .parseError { - display: flex; - justify-content: center; - padding: 0.75em; - margin-bottom: 10px; - gap: 3em; -} - -.import .parseError .clear { - cursor: pointer; -} - diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index 488b38160..e33959e66 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -5,23 +5,6 @@
-
- - - - - - - {{error.message}} - - - {{'IMPORT.BUTTON_CLEAR_ERROR' | translate}} - -
-
{{fileName}}