diff --git a/assets/src/bundles/admin/index.js b/assets/src/bundles/admin/index.js --- a/assets/src/bundles/admin/index.js +++ b/assets/src/bundles/admin/index.js @@ -1,9 +1,10 @@ /** - * Copyright (C) 2018 The Software Heritage developers + * Copyright (C) 2018-2022 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ export * from './deposit'; +export * from './mailmap'; export * from './origin-save'; diff --git a/assets/src/bundles/admin/mailmap-form.ejs b/assets/src/bundles/admin/mailmap-form.ejs new file mode 100644 --- /dev/null +++ b/assets/src/bundles/admin/mailmap-form.ejs @@ -0,0 +1,29 @@ +<%# + Copyright (C) 2022 The Software Heritage developers + See the AUTHORS file at the top-level directory of this distribution + License: GNU Affero General Public License version 3, or any later version + See top-level LICENSE file for more information +%> + +
+
+ + readonly <% } %> required> +
+
+ + +
+
+ checked <% } %>> + +
+
+ +
+
\ No newline at end of file diff --git a/assets/src/bundles/admin/mailmap.js b/assets/src/bundles/admin/mailmap.js new file mode 100644 --- /dev/null +++ b/assets/src/bundles/admin/mailmap.js @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2022 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU Affero General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +import {csrfPost, handleFetchError} from 'utils/functions'; + +import mailmapFormTemplate from './mailmap-form.ejs'; + +let mailmapsTable; + +export function mailmapForm(buttonText, email = '', displayName = '', + displayNameActivated = false, update = false) { + return mailmapFormTemplate({ + buttonText: buttonText, + email: email, + displayName: displayName, + displayNameActivated: displayNameActivated, + updateForm: update + }); +} + +function getMailmapDataFromForm() { + return { + 'from_email': $('#swh-mailmap-from-email').val(), + 'display_name': $('#swh-mailmap-display-name').val(), + 'display_name_activated': $('#swh-mailmap-display-name-activated').prop('checked') + }; +} + +function processMailmapForm(formTitle, formHtml, formApiUrl) { + swh.webapp.showModalHtml(formTitle, formHtml); + $(`#swh-mailmap-form`).on('submit', async event => { + event.preventDefault(); + event.stopPropagation(); + const postData = getMailmapDataFromForm(); + try { + const response = await csrfPost( + formApiUrl, {'Content-Type': 'application/json'}, JSON.stringify(postData) + ); + $('#swh-web-modal-html').modal('hide'); + handleFetchError(response); + mailmapsTable.draw(); + } catch (response) { + const error = await response.text(); + swh.webapp.showModalMessage('Error', error); + } + }); +} + +export function addNewMailmap() { + const mailmapFormHtml = mailmapForm('Add mailmap'); + processMailmapForm('Add new mailmap', mailmapFormHtml, Urls.profile_mailmap_add()); +} + +export function updateMailmap(mailmapId) { + let mailmapData; + const rows = mailmapsTable.rows().data(); + for (let i = 0; i < rows.length; ++i) { + const row = rows[i]; + if (row.id === mailmapId) { + mailmapData = row; + break; + } + } + const mailmapFormHtml = mailmapForm('Update mailmap', mailmapData.from_email, + mailmapData.display_name, + mailmapData.display_name_activated, true); + processMailmapForm('Update existing mailmap', mailmapFormHtml, Urls.profile_mailmap_update()); +} + +const mdiCheckBold = ''; +const mdiCloseThick = ''; + +export function initMailmapUI() { + $(document).ready(() => { + mailmapsTable = $('#swh-mailmaps-table') + .on('error.dt', (e, settings, techNote, message) => { + $('#swh-mailmaps-list-error').text( + 'An error occurred while retrieving the mailmaps list'); + console.log(message); + }) + .DataTable({ + serverSide: true, + ajax: Urls.profile_mailmap_list_datatables(), + columns: [ + { + data: 'from_email', + name: 'from_email', + render: $.fn.dataTable.render.text() + }, + { + data: 'from_email_verified', + name: 'from_email_verified', + render: (data, type, row) => { + return data ? mdiCheckBold : mdiCloseThick; + }, + className: 'dt-center' + }, + { + data: 'display_name', + name: 'display_name', + render: $.fn.dataTable.render.text() + }, + { + data: 'display_name_activated', + name: 'display_name_activated', + render: (data, type, row) => { + return data ? mdiCheckBold : mdiCloseThick; + }, + className: 'dt-center' + }, + { + data: 'last_update_date', + name: 'last_update_date', + render: (data, type, row) => { + if (type === 'display') { + const date = new Date(data); + return date.toLocaleString(); + } + return data; + } + }, + { + render: (data, type, row) => { + const lastUpdateDate = new Date(row.last_update_date); + const lastProcessingDate = new Date(row.mailmap_last_processing_date); + if (!lastProcessingDate || lastProcessingDate < lastUpdateDate) { + return mdiCloseThick; + } else { + return mdiCheckBold; + } + }, + className: 'dt-center', + orderable: false + }, + { + render: (data, type, row) => { + const html = + ``; + return html; + }, + orderable: false + } + + ], + ordering: true, + searching: true, + searchDelay: 1000, + scrollY: '50vh', + scrollCollapse: true + }); + }); +} diff --git a/cypress/integration/mailmap.spec.js b/cypress/integration/mailmap.spec.js new file mode 100644 --- /dev/null +++ b/cypress/integration/mailmap.spec.js @@ -0,0 +1,226 @@ +/** + * Copyright (C) 2022 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU Affero General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +const $ = Cypress.$; + +function fillFormAndSubmitMailmap(fromEmail, displayName, activated) { + if (fromEmail) { + cy.get('#swh-mailmap-from-email') + .clear() + .type(fromEmail, {delay: 0, force: true}); + } + + if (displayName) { + cy.get('#swh-mailmap-display-name') + .clear() + .type(displayName, {delay: 0, force: true}); + } + + if (activated) { + cy.get('#swh-mailmap-display-name-activated') + .check({force: true}); + } else { + cy.get('#swh-mailmap-display-name-activated') + .uncheck({force: true}); + } + + cy.get('#swh-mailmap-form-submit') + .click(); +} + +function addNewMailmap(fromEmail, displayName, activated) { + cy.get('#swh-add-new-mailmap') + .click(); + + fillFormAndSubmitMailmap(fromEmail, displayName, activated); +} + +function updateMailmap(fromEmail, displayName, activated) { + cy.contains('Edit') + .click(); + + fillFormAndSubmitMailmap(fromEmail, displayName, activated); +} + +function checkMailmapRow(fromEmail, displayName, activated, + processed = false, row = 1, nbRows = 1) { + cy.get('tbody tr').then(rows => { + assert.equal(rows.length, 1); + const cells = rows[0].cells; + assert.equal($(cells[0]).text(), fromEmail); + assert.include($(cells[1]).html(), 'mdi-check-bold'); + assert.equal($(cells[2]).text(), displayName); + assert.include($(cells[3]).html(), activated ? 'mdi-check-bold' : 'mdi-close-thick'); + assert.notEqual($(cells[4]).text(), ''); + assert.include($(cells[5]).html(), processed ? 'mdi-check-bold' : 'mdi-close-thick'); + }); +} + +describe('Test mailmap administration', function() { + + before(function() { + this.url = this.Urls.admin_mailmap(); + }); + + beforeEach(function() { + cy.task('db:user_mailmap:delete'); + cy.intercept('POST', this.Urls.profile_mailmap_add()) + .as('mailmapAdd'); + cy.intercept('POST', this.Urls.profile_mailmap_update()) + .as('mailmapUpdate'); + cy.intercept(`${this.Urls.profile_mailmap_list_datatables()}**`) + .as('mailmapList'); + }); + + it('should not display mailmap admin link in sidebar when anonymous', function() { + cy.visit(this.url); + cy.get('.swh-mailmap-admin-item') + .should('not.exist'); + }); + + it('should not display mailmap admin link when connected as unprivileged user', function() { + cy.userLogin(); + cy.visit(this.url); + + cy.get('.swh-mailmap-admin-item') + .should('not.exist'); + + }); + + it('should display mailmap admin link in sidebar when connected as privileged user', function() { + cy.mailmapAdminLogin(); + cy.visit(this.url); + + cy.get('.swh-mailmap-admin-item') + .should('exist'); + }); + + it('should not create a new mailmap when input data are empty', function() { + cy.mailmapAdminLogin(); + cy.visit(this.url); + + addNewMailmap('', '', true); + + cy.get('#swh-mailmap-form :invalid').should('exist'); + + cy.get('#swh-mailmap-form') + .should('be.visible'); + + }); + + it('should not create a new mailmap when from email is invalid', function() { + cy.mailmapAdminLogin(); + cy.visit(this.url); + + addNewMailmap('invalid_email', 'display name', true); + + cy.get('#swh-mailmap-form :invalid').should('exist'); + + cy.get('#swh-mailmap-form') + .should('be.visible'); + }); + + it('should create a new mailmap when input data are valid', function() { + cy.mailmapAdminLogin(); + cy.visit(this.url); + + const fromEmail = 'user@example.org'; + const displayName = 'New user display name'; + addNewMailmap(fromEmail, displayName, true); + cy.wait('@mailmapAdd'); + + cy.get('#swh-mailmap-form :invalid').should('not.exist'); + + // ensure table redraw before next check + cy.contains(fromEmail); + + cy.get('#swh-mailmap-form') + .should('not.be.visible'); + + checkMailmapRow(fromEmail, displayName, true); + + }); + + it('should not create a new mailmap for an email already mapped', function() { + cy.mailmapAdminLogin(); + cy.visit(this.url); + + const fromEmail = 'user@example.org'; + const displayName = 'New user display name'; + addNewMailmap(fromEmail, displayName, true); + cy.wait('@mailmapAdd'); + + addNewMailmap(fromEmail, displayName, true); + cy.wait('@mailmapAdd'); + + cy.get('#swh-mailmap-form') + .should('not.be.visible'); + + cy.contains('Error') + .should('be.visible'); + + checkMailmapRow(fromEmail, displayName, true); + + }); + + it('should update a mailmap', function() { + cy.mailmapAdminLogin(); + cy.visit(this.url); + + const fromEmail = 'user@example.org'; + const displayName = 'New display name'; + addNewMailmap(fromEmail, displayName, false); + cy.wait('@mailmapAdd'); + + cy.get('#swh-mailmap-form :invalid').should('not.exist'); + + // ensure table redraw before next check + cy.contains(fromEmail); + + cy.get('#swh-mailmap-form') + .should('not.be.visible'); + + checkMailmapRow(fromEmail, displayName, false); + + const newDisplayName = 'Updated display name'; + updateMailmap('', newDisplayName, true); + cy.wait('@mailmapUpdate'); + + cy.get('#swh-mailmap-form :invalid').should('not.exist'); + + // ensure table redraw before next check + cy.contains(fromEmail); + + cy.get('#swh-mailmap-form') + .should('not.be.visible'); + + checkMailmapRow(fromEmail, newDisplayName, true); + + }); + + it('should indicate when a mailmap has been processed', function() { + cy.mailmapAdminLogin(); + cy.visit(this.url); + + const fromEmail = 'user@example.org'; + const displayName = 'New user display name'; + addNewMailmap(fromEmail, displayName, true); + cy.wait('@mailmapAdd'); + + // ensure table redraw before next check + cy.contains(fromEmail); + + checkMailmapRow(fromEmail, displayName, true, false); + + cy.task('db:user_mailmap:mark_processed'); + + cy.visit(this.url); + checkMailmapRow(fromEmail, displayName, true, true); + + }); + +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -7,6 +7,7 @@ const axios = require('axios'); const fs = require('fs'); +const sqlite3 = require('sqlite3').verbose(); async function httpGet(url) { const response = await axios.get(url); @@ -32,6 +33,10 @@ }; }; +function getDatabase() { + return new sqlite3.Database('./swh-web-test.sqlite3'); +} + module.exports = (on, config) => { require('@cypress/code-coverage/task')(on, config); // produce JSON files prior launching browser in order to dynamically generate tests @@ -124,6 +129,23 @@ global.swhTestsData = swhTestsData; } return global.swhTestsData; + }, + 'db:user_mailmap:delete': () => { + const db = getDatabase(); + db.serialize(function() { + db.run('DELETE FROM user_mailmap'); + db.run('DELETE FROM user_mailmap_event'); + }); + db.close(); + return true; + }, + 'db:user_mailmap:mark_processed': () => { + const db = getDatabase(); + db.serialize(function() { + db.run('UPDATE user_mailmap SET mailmap_last_processing_date=datetime("now", "+1 hour")'); + }); + db.close(); + return true; } }); return config; diff --git a/cypress/support/index.js b/cypress/support/index.js --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -84,6 +84,9 @@ body: '' }).as('swhCoverageWidget'); } +Cypress.Commands.add('mailmapAdminLogin', () => { + return loginUser('mailmap-admin', 'mailmap-admin'); +}); before(function() { diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ "schema-utils": "^4.0.0", "script-loader": "^0.7.2", "spdx-expression-parse": "^3.0.1", + "sqlite3": "^5.0.2", "style-loader": "^3.3.1", "stylelint": "^14.5.3", "stylelint-config-standard": "^25.0.0", diff --git a/swh/web/admin/mailmap.py b/swh/web/admin/mailmap.py new file mode 100644 --- /dev/null +++ b/swh/web/admin/mailmap.py @@ -0,0 +1,16 @@ +# Copyright (C) 2022 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU Affero General Public License version 3, or any later version +# See top-level LICENSE file for more information + +from django.contrib.auth.decorators import permission_required +from django.shortcuts import render + +from swh.web.admin.adminurls import admin_route +from swh.web.auth.utils import MAILMAP_ADMIN_PERMISSION + + +@admin_route(r"mailmap/", view_name="admin-mailmap") +@permission_required(MAILMAP_ADMIN_PERMISSION) +def _admin_mailmap(request): + return render(request, "admin/mailmap.html") diff --git a/swh/web/admin/urls.py b/swh/web/admin/urls.py --- a/swh/web/admin/urls.py +++ b/swh/web/admin/urls.py @@ -9,6 +9,7 @@ from swh.web.admin.adminurls import AdminUrls import swh.web.admin.deposit # noqa +import swh.web.admin.mailmap # noqa import swh.web.admin.origin_save # noqa from swh.web.config import is_feature_enabled diff --git a/swh/web/auth/mailmap.py b/swh/web/auth/mailmap.py --- a/swh/web/auth/mailmap.py +++ b/swh/web/auth/mailmap.py @@ -4,15 +4,18 @@ # See top-level LICENSE file for more information import json +from typing import Any, Dict from django.conf.urls import url +from django.core.paginator import Paginator from django.db import IntegrityError from django.db.models import Q +from django.http.request import HttpRequest from django.http.response import ( HttpResponse, HttpResponseBadRequest, - HttpResponseForbidden, HttpResponseNotFound, + JsonResponse, ) from rest_framework import serializers from rest_framework.decorators import api_view @@ -20,7 +23,11 @@ from rest_framework.response import Response from swh.web.auth.models import UserMailmap, UserMailmapEvent -from swh.web.auth.utils import MAILMAP_PERMISSION +from swh.web.auth.utils import ( + MAILMAP_ADMIN_PERMISSION, + MAILMAP_PERMISSION, + any_permission_required, +) class UserMailmapSerializer(serializers.ModelSerializer): @@ -30,18 +37,20 @@ @api_view(["GET"]) +@any_permission_required(MAILMAP_PERMISSION, MAILMAP_ADMIN_PERMISSION) def profile_list_mailmap(request: Request) -> HttpResponse: - if not request.user.has_perm(MAILMAP_PERMISSION): - return HttpResponseForbidden() + mailmap_admin = request.user.has_perm(MAILMAP_ADMIN_PERMISSION) - mms = UserMailmap.objects.filter(user_id=str(request.user.id),).all() + mms = UserMailmap.objects.filter( + user_id=None if mailmap_admin else str(request.user.id) + ).all() return Response(UserMailmapSerializer(mms, many=True).data) @api_view(["POST"]) +@any_permission_required(MAILMAP_PERMISSION, MAILMAP_ADMIN_PERMISSION) def profile_add_mailmap(request: Request) -> HttpResponse: - if not request.user.has_perm(MAILMAP_PERMISSION): - return HttpResponseForbidden() + mailmap_admin = request.user.has_perm(MAILMAP_ADMIN_PERMISSION) event = UserMailmapEvent.objects.create( user_id=str(request.user.id), @@ -51,29 +60,46 @@ from_email = request.data.pop("from_email", None) if not from_email: - return HttpResponseBadRequest("'from_email' must be provided and non-empty.") + return HttpResponseBadRequest( + "'from_email' must be provided and non-empty.", content_type="text/plain" + ) + + user_id = None if mailmap_admin else str(request.user.id) + + from_email_verified = request.data.pop("from_email_verified", False) + if mailmap_admin: + # consider email verified when mailmap is added by admin + from_email_verified = True try: UserMailmap.objects.create( - user_id=str(request.user.id), from_email=from_email, **request.data + user_id=user_id, + from_email=from_email, + from_email_verified=from_email_verified, + **request.data, ) except IntegrityError as e: - if "user_mailmap_from_email_key" in e.args[0]: - return HttpResponseBadRequest("This 'from_email' already exists.") + if ( + "user_mailmap_from_email_key" in e.args[0] + or "user_mailmap.from_email" in e.args[0] + ): + return HttpResponseBadRequest( + "This 'from_email' already exists.", content_type="text/plain" + ) else: raise event.successful = True event.save() - mm = UserMailmap.objects.get(user_id=str(request.user.id), from_email=from_email) + mm = UserMailmap.objects.get(user_id=user_id, from_email=from_email) return Response(UserMailmapSerializer(mm).data) @api_view(["POST"]) +@any_permission_required(MAILMAP_PERMISSION, MAILMAP_ADMIN_PERMISSION) def profile_update_mailmap(request: Request) -> HttpResponse: - if not request.user.has_perm(MAILMAP_PERMISSION): - return HttpResponseForbidden() + mailmap_admin = request.user.has_perm(MAILMAP_ADMIN_PERMISSION) event = UserMailmapEvent.objects.create( user_id=str(request.user.id), @@ -83,18 +109,20 @@ from_email = request.data.pop("from_email", None) if not from_email: - return HttpResponseBadRequest("'from_email' must be provided and non-empty.") + return HttpResponseBadRequest( + "'from_email' must be provided and non-empty.", content_type="text/plain" + ) - user_id = str(request.user.id) + user_id = None if mailmap_admin else str(request.user.id) try: to_update = ( - UserMailmap.objects.filter(Q(user_id__isnull=True) | Q(user_id=user_id)) + UserMailmap.objects.filter(user_id=user_id) .filter(from_email=from_email) .get() ) except UserMailmap.DoesNotExist: - return HttpResponseNotFound() + return HttpResponseNotFound("'from_email' cannot be found in mailmaps.") for attr, value in request.data.items(): setattr(to_update, attr, value) @@ -108,6 +136,50 @@ return Response(UserMailmapSerializer(mm).data) +@any_permission_required(MAILMAP_PERMISSION, MAILMAP_ADMIN_PERMISSION) +def profile_list_mailmap_datatables(request: HttpRequest) -> HttpResponse: + mailmap_admin = request.user.has_perm(MAILMAP_ADMIN_PERMISSION) + + mailmaps = UserMailmap.objects.filter( + user_id=None if mailmap_admin else str(request.user.id) + ) + + search_value = request.GET.get("search[value]", "") + + column_order = request.GET.get("order[0][column]") + field_order = request.GET.get(f"columns[{column_order}][name]", "from_email") + order_dir = request.GET.get("order[0][dir]", "asc") + if order_dir == "desc": + field_order = "-" + field_order + + mailmaps = mailmaps.order_by(field_order) + + table_data: Dict[str, Any] = {} + table_data["draw"] = int(request.GET.get("draw", 1)) + table_data["recordsTotal"] = mailmaps.count() + + length = int(request.GET.get("length", 10)) + page = int(request.GET.get("start", 0)) / length + 1 + + if search_value: + mailmaps = mailmaps.filter( + Q(from_email__icontains=search_value) + | Q(display_name__icontains=search_value) + ) + + table_data["recordsFiltered"] = mailmaps.count() + + paginator = Paginator(mailmaps, length) + + mailmaps_data = [ + UserMailmapSerializer(mm).data for mm in paginator.page(int(page)).object_list + ] + + table_data["data"] = mailmaps_data + + return JsonResponse(table_data) + + urlpatterns = [ url(r"^profile/mailmap/list/$", profile_list_mailmap, name="profile-mailmap-list",), url(r"^profile/mailmap/add/$", profile_add_mailmap, name="profile-mailmap-add",), @@ -116,4 +188,9 @@ profile_update_mailmap, name="profile-mailmap-update", ), + url( + r"^profile/mailmap/list/datatables/$", + profile_list_mailmap_datatables, + name="profile-mailmap-list-datatables", + ), ] diff --git a/swh/web/auth/migrations/0006_fix_mailmap_admin_user_id.py b/swh/web/auth/migrations/0006_fix_mailmap_admin_user_id.py new file mode 100644 --- /dev/null +++ b/swh/web/auth/migrations/0006_fix_mailmap_admin_user_id.py @@ -0,0 +1,41 @@ +# Copyright (C) 2022 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU Affero General Public License version 3, or any later version +# See top-level LICENSE file for more information + +import datetime + +from django.db import migrations + + +def _set_first_mailmaps_as_edited_by_admin(apps, schema_editor): + """First mailmaps in production database have been created by a user + with "swh.web.mailmap" permission because no "swh.web.admin.mailmap" + permission existed at the time. + + So change user_id to None to indicate these mailmaps have been created + by a mailmap administrator. + """ + UserMailmap = apps.get_model("swh_web_auth", "UserMailmap") + + for mailmap in UserMailmap.objects.filter( + last_update_date__lte=datetime.datetime( + 2022, 2, 12 + ) # first mailmaps added on 2022/2/11 in production + ): + if mailmap.user_id is not None: + mailmap.user_id = None + mailmap.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("swh_web_auth", "0005_usermailmapevent"), + ] + + operations = [ + migrations.RunPython( + _set_first_mailmaps_as_edited_by_admin, migrations.RunPython.noop + ), + ] diff --git a/swh/web/auth/utils.py b/swh/web/auth/utils.py --- a/swh/web/auth/utils.py +++ b/swh/web/auth/utils.py @@ -11,8 +11,11 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from django.contrib.auth.decorators import user_passes_test from django.http.request import HttpRequest +from swh.web.common.exc import ForbiddenExc + OIDC_SWH_WEB_CLIENT_ID = "swh-web" SWH_AMBASSADOR_PERMISSION = "swh.ambassador" @@ -20,6 +23,7 @@ ADMIN_LIST_DEPOSIT_PERMISSION = "swh.web.admin.list_deposits" MAILMAP_PERMISSION = "swh.web.mailmap" ADD_FORGE_MODERATOR_PERMISSION = "swh.web.add_forge_now.moderator" +MAILMAP_ADMIN_PERMISSION = "swh.web.admin.mailmap" def _get_fernet(password: bytes, salt: bytes) -> Fernet: @@ -96,3 +100,16 @@ return user.is_authenticated and ( user.is_staff or any([user.has_perm(perm) for perm in permissions]) ) + + +def any_permission_required(*perms): + """View decorator granting access to it if user has at least one + permission among those passed as parameters. + """ + + def check_perms(user): + if any(user.has_perm(perm) for perm in perms): + return True + raise ForbiddenExc + + return user_passes_test(check_perms) diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py --- a/swh/web/common/utils.py +++ b/swh/web/common/utils.py @@ -30,6 +30,7 @@ from swh.web.auth.utils import ( ADD_FORGE_MODERATOR_PERMISSION, ADMIN_LIST_DEPOSIT_PERMISSION, + MAILMAP_ADMIN_PERMISSION, ) from swh.web.common.exc import BadInputExc from swh.web.common.typing import QueryParameters @@ -316,6 +317,7 @@ "ADMIN_LIST_DEPOSIT_PERMISSION": ADMIN_LIST_DEPOSIT_PERMISSION, "ADD_FORGE_MODERATOR_PERMISSION": ADD_FORGE_MODERATOR_PERMISSION, "FEATURES": get_config()["features"], + "MAILMAP_ADMIN_PERMISSION": MAILMAP_ADMIN_PERMISSION, } diff --git a/swh/web/templates/admin/mailmap.html b/swh/web/templates/admin/mailmap.html new file mode 100644 --- /dev/null +++ b/swh/web/templates/admin/mailmap.html @@ -0,0 +1,53 @@ +{% extends "layout.html" %} + +{% comment %} +Copyright (C) 2022 The Software Heritage developers +See the AUTHORS file at the top-level directory of this distribution +License: GNU Affero General Public License version 3, or any later version +See top-level LICENSE file for more information +{% endcomment %} + +{% load swh_templatetags %} +{% load render_bundle from webpack_loader %} + +{% block header %} +{{ block.super }} +{% render_bundle 'admin' %} +{% endblock %} + +{% block title %} Mailmap administration – Software Heritage archive {% endblock %} + +{% block navbar-content %} +

Mailmap administration

+{% endblock %} + +{% block content %} +

+ This interface enables to manage author display names in the archive based + on their emails. +

+
+ +
+
+ + + + + + + + + + + + +
EmailVerifiedDisplay nameActivatedLast updateEffective
+

+
+ +{% endblock content %} diff --git a/swh/web/templates/layout.html b/swh/web/templates/layout.html --- a/swh/web/templates/layout.html +++ b/swh/web/templates/layout.html @@ -244,12 +244,20 @@ {% endif %} {% endif %} {% if user.is_staff or ADMIN_LIST_DEPOSIT_PERMISSION in user.get_all_permissions %} - + + {% endif %} + {% if MAILMAP_ADMIN_PERMISSION in user.get_all_permissions %} + {% endif %} {% endif %} diff --git a/swh/web/tests/auth/test_mailmap.py b/swh/web/tests/auth/test_mailmap.py --- a/swh/web/tests/auth/test_mailmap.py +++ b/swh/web/tests/auth/test_mailmap.py @@ -16,21 +16,14 @@ from swh.model.model import Person from swh.web.auth.models import UserMailmap, UserMailmapEvent -from swh.web.auth.utils import MAILMAP_PERMISSION from swh.web.common.utils import reverse from swh.web.tests.utils import ( check_api_post_response, check_http_get_response, - create_django_permission, + check_http_post_response, ) -@pytest.fixture -def mailmap_user(regular_user): - regular_user.user_permissions.add(create_django_permission(MAILMAP_PERMISSION)) - return regular_user - - @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("view_name", ["profile-mailmap-add", "profile-mailmap-update"]) def test_mailmap_endpoints_anonymous_user(api_client, view_name): @@ -39,172 +32,332 @@ @pytest.mark.django_db(transaction=True) -def test_mailmap_endpoints_user_with_permission(api_client, mailmap_user): - api_client.force_login(mailmap_user) +def test_mailmap_endpoints_user_with_permission( + api_client, mailmap_user, mailmap_admin +): - request_data = {"from_email": "bar@example.org", "display_name": "bar"} + for user, name in ((mailmap_user, "bar"), (mailmap_admin, "baz")): - for view_name in ("profile-mailmap-add", "profile-mailmap-update"): - url = reverse(view_name) - check_api_post_response( - api_client, url, data=request_data, status_code=200, - ) + UserMailmapEvent.objects.all().delete() - # FIXME: use check_api_get_responses; currently this crashes without - # content_type="application/json" - resp = check_http_get_response( - api_client, - reverse("profile-mailmap-list"), - status_code=200, - content_type="application/json", - ).data - assert len(resp) == 1 - assert resp[0]["from_email"] == "bar@example.org" - assert resp[0]["display_name"] == "bar" - - events = UserMailmapEvent.objects.order_by("timestamp").all() - assert len(events) == 2 - assert events[0].request_type == "add" - assert json.loads(events[0].request) == request_data - assert events[1].request_type == "update" - assert json.loads(events[1].request) == request_data + api_client.force_login(user) + + request_data = {"from_email": f"{name}@example.org", "display_name": name} + + for view_name in ("profile-mailmap-add", "profile-mailmap-update"): + url = reverse(view_name) + check_api_post_response( + api_client, url, data=request_data, status_code=200, + ) + + # FIXME: use check_api_get_responses; currently this crashes without + # content_type="application/json" + resp = check_http_get_response( + api_client, + reverse("profile-mailmap-list"), + status_code=200, + content_type="application/json", + ).data + assert len(resp) == 1 + assert resp[0]["from_email"] == f"{name}@example.org" + assert resp[0]["display_name"] == name + + events = UserMailmapEvent.objects.order_by("timestamp").all() + assert len(events) == 2 + assert events[0].request_type == "add" + assert json.loads(events[0].request) == request_data + assert events[1].request_type == "update" + assert json.loads(events[1].request) == request_data @pytest.mark.django_db(transaction=True) -def test_mailmap_add_duplicate(api_client, mailmap_user): - api_client.force_login(mailmap_user) +def test_mailmap_add_duplicate(api_client, mailmap_user, mailmap_admin): - check_api_post_response( - api_client, - reverse("profile-mailmap-add"), - data={"from_email": "foo@example.org", "display_name": "bar"}, - status_code=200, - ) - check_api_post_response( - api_client, - reverse("profile-mailmap-add"), - data={"from_email": "foo@example.org", "display_name": "baz"}, - status_code=400, - ) + for user, name in ((mailmap_user, "foo"), (mailmap_admin, "bar")): + + api_client.force_login(user) + + check_api_post_response( + api_client, + reverse("profile-mailmap-add"), + data={"from_email": f"{name}@example.org", "display_name": name}, + status_code=200, + ) + check_api_post_response( + api_client, + reverse("profile-mailmap-add"), + data={"from_email": f"{name}@example.org", "display_name": name}, + status_code=400, + ) @pytest.mark.django_db(transaction=True) -def test_mailmap_add_full(api_client, mailmap_user): - api_client.force_login(mailmap_user) - - request_data = { - "from_email": "foo@example.org", - "from_email_verified": True, - "from_email_verification_request_date": "2021-02-07T14:04:15Z", - "display_name": "bar", - "display_name_activated": True, - "to_email": "bar@example.org", - "to_email_verified": True, - "to_email_verification_request_date": "2021-02-07T15:54:59Z", - } +def test_mailmap_add_full(api_client, mailmap_user, mailmap_admin): - check_api_post_response( - api_client, reverse("profile-mailmap-add"), data=request_data, status_code=200, - ) + for user, name in ((mailmap_user, "foo"), (mailmap_admin, "bar")): - resp = check_http_get_response( - api_client, - reverse("profile-mailmap-list"), - status_code=200, - content_type="application/json", - ).data - assert len(resp) == 1 - assert resp[0].items() >= request_data.items() + api_client.force_login(user) + + UserMailmapEvent.objects.all().delete() + + request_data = { + "from_email": f"{name}@example.org", + "from_email_verified": True, + "from_email_verification_request_date": "2021-02-07T14:04:15Z", + "display_name": name, + "display_name_activated": True, + "to_email": "baz@example.org", + "to_email_verified": True, + "to_email_verification_request_date": "2021-02-07T15:54:59Z", + } + + check_api_post_response( + api_client, + reverse("profile-mailmap-add"), + data=request_data, + status_code=200, + ) + + resp = check_http_get_response( + api_client, + reverse("profile-mailmap-list"), + status_code=200, + content_type="application/json", + ).data + assert len(resp) == 1 + assert resp[0].items() >= request_data.items() - events = UserMailmapEvent.objects.all() - assert len(events) == 1 - assert events[0].request_type == "add" - assert json.loads(events[0].request) == request_data - assert events[0].successful + events = UserMailmapEvent.objects.all() + assert len(events) == 1 + assert events[0].request_type == "add" + assert json.loads(events[0].request) == request_data + assert events[0].successful @pytest.mark.django_db(transaction=True) -def test_mailmap_endpoints_error_response(api_client, mailmap_user): - api_client.force_login(mailmap_user) +def test_mailmap_endpoints_error_response(api_client, mailmap_user, mailmap_admin): + + for user in (mailmap_user, mailmap_admin): + + api_client.force_login(user) - url = reverse("profile-mailmap-add") - resp = check_api_post_response(api_client, url, status_code=400) - assert b"from_email" in resp.content + UserMailmapEvent.objects.all().delete() - url = reverse("profile-mailmap-update") - resp = check_api_post_response(api_client, url, status_code=400) - assert b"from_email" in resp.content + url = reverse("profile-mailmap-add") + resp = check_api_post_response(api_client, url, status_code=400) + assert b"from_email" in resp.content - events = UserMailmapEvent.objects.order_by("timestamp").all() - assert len(events) == 2 + url = reverse("profile-mailmap-update") + resp = check_api_post_response(api_client, url, status_code=400) + assert b"from_email" in resp.content - assert events[0].request_type == "add" - assert json.loads(events[0].request) == {} - assert not events[0].successful + events = UserMailmapEvent.objects.order_by("timestamp").all() + assert len(events) == 2 - assert events[1].request_type == "update" - assert json.loads(events[1].request) == {} - assert not events[1].successful + assert events[0].request_type == "add" + assert json.loads(events[0].request) == {} + assert not events[0].successful + + assert events[1].request_type == "update" + assert json.loads(events[1].request) == {} + assert not events[1].successful @pytest.mark.django_db(transaction=True) -def test_mailmap_update(api_client, mailmap_user): - api_client.force_login(mailmap_user) +def test_mailmap_update(api_client, mailmap_user, mailmap_admin): - before_add = datetime.datetime.now(tz=datetime.timezone.utc) + for user, name in ((mailmap_user, "foo"), (mailmap_admin, "bar")): - check_api_post_response( - api_client, - reverse("profile-mailmap-add"), - data={"from_email": "orig1@example.org", "display_name": "Display Name 1"}, - status_code=200, - ) - check_api_post_response( - api_client, - reverse("profile-mailmap-add"), - data={"from_email": "orig2@example.org", "display_name": "Display Name 2"}, - status_code=200, - ) - after_add = datetime.datetime.now(tz=datetime.timezone.utc) + api_client.force_login(user) + + UserMailmapEvent.objects.all().delete() + + before_add = datetime.datetime.now(tz=datetime.timezone.utc) + + check_api_post_response( + api_client, + reverse("profile-mailmap-add"), + data={ + "from_email": f"{name}1@example.org", + "display_name": "Display Name 1", + }, + status_code=200, + ) + check_api_post_response( + api_client, + reverse("profile-mailmap-add"), + data={ + "from_email": f"{name}2@example.org", + "display_name": "Display Name 2", + }, + status_code=200, + ) + after_add = datetime.datetime.now(tz=datetime.timezone.utc) + + user_id = None if user == mailmap_admin else str(user.id) + + mailmaps = list( + UserMailmap.objects.filter(user_id=user_id).order_by("from_email").all() + ) + assert len(mailmaps) == 2, mailmaps + + assert mailmaps[0].from_email == f"{name}1@example.org", mailmaps + assert mailmaps[0].display_name == "Display Name 1", mailmaps + assert before_add <= mailmaps[0].last_update_date <= after_add + + assert mailmaps[1].from_email == f"{name}2@example.org", mailmaps + assert mailmaps[1].display_name == "Display Name 2", mailmaps + assert before_add <= mailmaps[0].last_update_date <= after_add + + before_update = datetime.datetime.now(tz=datetime.timezone.utc) + + check_api_post_response( + api_client, + reverse("profile-mailmap-update"), + data={ + "from_email": f"{name}1@example.org", + "display_name": "Display Name 1b", + }, + status_code=200, + ) + + after_update = datetime.datetime.now(tz=datetime.timezone.utc) - mailmaps = list(UserMailmap.objects.order_by("from_email").all()) - assert len(mailmaps) == 2, mailmaps + mailmaps = list( + UserMailmap.objects.filter(user_id=user_id).order_by("from_email").all() + ) + assert len(mailmaps) == 2, mailmaps - assert mailmaps[0].from_email == "orig1@example.org", mailmaps - assert mailmaps[0].display_name == "Display Name 1", mailmaps - assert before_add <= mailmaps[0].last_update_date <= after_add + assert mailmaps[0].from_email == f"{name}1@example.org", mailmaps + assert mailmaps[0].display_name == "Display Name 1b", mailmaps + assert before_update <= mailmaps[0].last_update_date <= after_update - assert mailmaps[1].from_email == "orig2@example.org", mailmaps - assert mailmaps[1].display_name == "Display Name 2", mailmaps - assert before_add <= mailmaps[0].last_update_date <= after_add + assert mailmaps[1].from_email == f"{name}2@example.org", mailmaps + assert mailmaps[1].display_name == "Display Name 2", mailmaps + assert before_add <= mailmaps[1].last_update_date <= after_add - before_update = datetime.datetime.now(tz=datetime.timezone.utc) + events = UserMailmapEvent.objects.order_by("timestamp").all() + assert len(events) == 3 + assert events[0].request_type == "add" + assert events[1].request_type == "add" + assert events[2].request_type == "update" + +@pytest.mark.django_db(transaction=True) +def test_mailmap_update_from_email_not_found(api_client, mailmap_admin): + api_client.force_login(mailmap_admin) check_api_post_response( api_client, reverse("profile-mailmap-update"), - data={"from_email": "orig1@example.org", "display_name": "Display Name 1b"}, - status_code=200, + data={"from_email": "invalid@example.org", "display_name": "Display Name",}, + status_code=404, ) - after_update = datetime.datetime.now(tz=datetime.timezone.utc) - mailmaps = list(UserMailmap.objects.order_by("from_email").all()) - assert len(mailmaps) == 2, mailmaps +NB_MAILMAPS = 20 +MM_PER_PAGE = 10 + + +def _create_mailmaps(client): + mailmaps = [] + for i in range(NB_MAILMAPS): + resp = check_http_post_response( + client, + reverse("profile-mailmap-add"), + data={ + "from_email": f"user{i:02d}@example.org", + "display_name": f"User {i:02d}", + }, + status_code=200, + ) + mailmaps.append(json.loads(resp.content)) + return mailmaps + + +@pytest.mark.django_db(transaction=True, reset_sequences=True) +def test_mailmap_list_datatables_no_parameters(client, mailmap_admin): + client.force_login(mailmap_admin) + mailmaps = _create_mailmaps(client) + + url = reverse("profile-mailmap-list-datatables") + + resp = check_http_get_response(client, url, status_code=200) + mailmap_data = json.loads(resp.content) + + assert mailmap_data["recordsTotal"] == NB_MAILMAPS + assert mailmap_data["recordsFiltered"] == NB_MAILMAPS + + # mailmaps sorted by ascending from_email by default + for i in range(10): + assert mailmap_data["data"][i]["from_email"] == mailmaps[i]["from_email"] + + +@pytest.mark.django_db(transaction=True, reset_sequences=True) +@pytest.mark.parametrize("sort_direction", ["asc", "desc"]) +def test_mailmap_list_datatables_ordering(client, mailmap_admin, sort_direction): + client.force_login(mailmap_admin) + mailmaps = _create_mailmaps(client) + mailmaps_sorted = list(sorted(mailmaps, key=lambda d: d["display_name"])) + all_display_names = [mm["display_name"] for mm in mailmaps_sorted] + if sort_direction == "desc": + all_display_names = list(reversed(all_display_names)) + + for i in range(NB_MAILMAPS // MM_PER_PAGE): + url = reverse( + "profile-mailmap-list-datatables", + query_params={ + "draw": i, + "length": MM_PER_PAGE, + "start": i * MM_PER_PAGE, + "order[0][column]": 2, + "order[0][dir]": sort_direction, + "columns[2][name]": "display_name", + }, + ) + + resp = check_http_get_response(client, url, status_code=200) + data = json.loads(resp.content) + + assert data["draw"] == i + assert data["recordsFiltered"] == NB_MAILMAPS + assert data["recordsTotal"] == NB_MAILMAPS + assert len(data["data"]) == MM_PER_PAGE + + display_names = [mm["display_name"] for mm in data["data"]] + + expected_display_names = all_display_names[ + i * MM_PER_PAGE : (i + 1) * MM_PER_PAGE + ] + assert display_names == expected_display_names + + +@pytest.mark.django_db(transaction=True, reset_sequences=True) +def test_mailmap_list_datatables_search(client, mailmap_admin): + client.force_login(mailmap_admin) + _create_mailmaps(client) + + search_value = "user1" + + url = reverse( + "profile-mailmap-list-datatables", + query_params={ + "draw": 1, + "length": MM_PER_PAGE, + "start": 0, + "search[value]": search_value, + }, + ) - assert mailmaps[0].from_email == "orig1@example.org", mailmaps - assert mailmaps[0].display_name == "Display Name 1b", mailmaps - assert before_update <= mailmaps[0].last_update_date <= after_update + resp = check_http_get_response(client, url, status_code=200) + data = json.loads(resp.content) - assert mailmaps[1].from_email == "orig2@example.org", mailmaps - assert mailmaps[1].display_name == "Display Name 2", mailmaps - assert before_add <= mailmaps[1].last_update_date <= after_add + assert data["draw"] == 1 + assert data["recordsFiltered"] == MM_PER_PAGE + assert data["recordsTotal"] == NB_MAILMAPS + assert len(data["data"]) == MM_PER_PAGE - events = UserMailmapEvent.objects.order_by("timestamp").all() - assert len(events) == 3 - assert events[0].request_type == "add" - assert events[1].request_type == "add" - assert events[2].request_type == "update" + for mailmap in data["data"]: + assert search_value in mailmap["from_email"] def populate_mailmap(): diff --git a/swh/web/tests/auth/test_migrations.py b/swh/web/tests/auth/test_migrations.py new file mode 100644 --- /dev/null +++ b/swh/web/tests/auth/test_migrations.py @@ -0,0 +1,38 @@ +# Copyright (C) 2022 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +from datetime import datetime + +APP_NAME = "swh_web_auth" + +MIGRATION_0005 = "0005_usermailmapevent" +MIGRATION_0006 = "0006_fix_mailmap_admin_user_id" + + +def test_fix_mailmap_admin_user_id(migrator): + state = migrator.apply_tested_migration((APP_NAME, MIGRATION_0005)) + UserMailmap = state.apps.get_model(APP_NAME, "UserMailmap") + + user_id = "45" + + UserMailmap.objects.create( + user_id=user_id, + from_email="user@example.org", + from_email_verified=True, + display_name="New display name", + ) + + UserMailmap.objects.filter(user_id=user_id).update( + last_update_date=datetime(2022, 2, 11, 14, 16, 13, 614000) + ) + + assert UserMailmap.objects.filter(user_id=user_id).count() == 1 + assert UserMailmap.objects.filter(user_id=None).count() == 0 + + state = migrator.apply_tested_migration((APP_NAME, MIGRATION_0006)) + UserMailmap = state.apps.get_model(APP_NAME, "UserMailmap") + + assert UserMailmap.objects.filter(user_id=user_id).count() == 0 + assert UserMailmap.objects.filter(user_id=None).count() == 1 diff --git a/swh/web/tests/conftest.py b/swh/web/tests/conftest.py --- a/swh/web/tests/conftest.py +++ b/swh/web/tests/conftest.py @@ -36,7 +36,12 @@ from swh.storage.algos.origin import origin_get_latest_visit_status from swh.storage.algos.revisions_walker import get_revisions_walker from swh.storage.algos.snapshot import snapshot_get_all_branches, snapshot_get_latest -from swh.web.auth.utils import ADD_FORGE_MODERATOR_PERMISSION, OIDC_SWH_WEB_CLIENT_ID +from swh.web.auth.utils import ( + ADD_FORGE_MODERATOR_PERMISSION, + MAILMAP_ADMIN_PERMISSION, + MAILMAP_PERMISSION, + OIDC_SWH_WEB_CLIENT_ID, +) from swh.web.common import converters from swh.web.common.origin_save import get_scheduler_load_task_types from swh.web.common.typing import OriginVisitInfo @@ -1224,3 +1229,19 @@ create_django_permission(ADD_FORGE_MODERATOR_PERMISSION) ) return moderator + + +@pytest.fixture +def mailmap_admin(): + mailmap_admin = User.objects.create_user(username="mailmap-admin", password="") + mailmap_admin.user_permissions.add( + create_django_permission(MAILMAP_ADMIN_PERMISSION) + ) + return mailmap_admin + + +@pytest.fixture +def mailmap_user(): + mailmap_user = User.objects.create_user(username="mailmap-user", password="") + mailmap_user.user_permissions.add(create_django_permission(MAILMAP_PERMISSION)) + return mailmap_user diff --git a/swh/web/tests/create_test_users.py b/swh/web/tests/create_test_users.py --- a/swh/web/tests/create_test_users.py +++ b/swh/web/tests/create_test_users.py @@ -11,6 +11,7 @@ ADD_FORGE_MODERATOR_PERMISSION, ADMIN_LIST_DEPOSIT_PERMISSION, SWH_AMBASSADOR_PERMISSION, + MAILMAP_ADMIN_PERMISSION, ) from swh.web.tests.utils import create_django_permission @@ -24,12 +25,17 @@ "ambassador@example.org", [SWH_AMBASSADOR_PERMISSION], ), - "deposit": ("deposit", "deposit@example.org", [ADMIN_LIST_DEPOSIT_PERMISSION],), + "deposit": ("deposit", "deposit@example.org", [ADMIN_LIST_DEPOSIT_PERMISSION]), "add-forge-moderator": ( "add-forge-moderator", "moderator@example.org", [ADD_FORGE_MODERATOR_PERMISSION], ), + "mailmap-admin": ( + "mailmap-admin", + "mailmap-admin@example.org", + [MAILMAP_ADMIN_PERMISSION], + ), } diff --git a/swh/web/tests/utils.py b/swh/web/tests/utils.py --- a/swh/web/tests/utils.py +++ b/swh/web/tests/utils.py @@ -228,7 +228,9 @@ app_label = ".".join(perm_splitted[:-1]) perm_name = perm_splitted[-1] content_type = ContentType.objects.create( - id=1000 + ContentType.objects.count(), app_label=app_label, model="dummy" + id=1000 + ContentType.objects.count(), + app_label=app_label, + model=perm_splitted[-1], ) return Permission.objects.create( diff --git a/yarn.lock b/yarn.lock --- a/yarn.lock +++ b/yarn.lock @@ -1885,6 +1885,11 @@ resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" @@ -1940,6 +1945,11 @@ dependencies: default-require-extensions "^3.0.0" +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + "aproba@^1.0.3 || ^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" @@ -1963,6 +1973,14 @@ delegates "^1.0.0" readable-stream "^3.6.0" +are-we-there-yet@~1.1.2: + version "1.1.7" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2261,6 +2279,13 @@ resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= + dependencies: + inherits "~2.0.0" + bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -2889,6 +2914,11 @@ resolved "https://registry.yarnpkg.com/chosen-js/-/chosen-js-1.8.7.tgz#9bfa5597f5081d602ff4ae904af9aef33265bb1d" integrity sha512-eVdrZJ2U5ISdObkgsi0od5vIJdLwq1P1Xa/Vj/mgxkMZf14DlgobfB6nrlFi3kW4kkvKLsKk4NDqZj1MU1DCpw== +chownr@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" @@ -3022,6 +3052,11 @@ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + codemirror@^5.65.1: version "5.65.1" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.1.tgz#5988a812c974c467f964bcc1a00c944e373de502" @@ -3209,7 +3244,7 @@ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== -console-control-strings@^1.0.0, console-control-strings@^1.1.0: +console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= @@ -4122,7 +4157,7 @@ dependencies: ms "2.1.2" -debug@^3.1.0, debug@^3.1.1, debug@^3.2.7: +debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -4294,6 +4329,11 @@ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + detect-node@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" @@ -5437,6 +5477,13 @@ jsonfile "^6.0.1" universalify "^2.0.0" +fs-minipass@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== + dependencies: + minipass "^2.6.0" + fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -5459,6 +5506,16 @@ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fstream@^1.0.0, fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + fsu@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fsu/-/fsu-1.1.1.tgz#bd36d3579907c59d85b257a75b836aa9e0c31834" @@ -5509,6 +5566,20 @@ strip-ansi "^6.0.1" wide-align "^1.1.2" +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + gaze@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" @@ -5831,7 +5902,7 @@ dependencies: has-symbols "^1.0.2" -has-unicode@^2.0.1: +has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= @@ -6302,7 +6373,7 @@ resolved "https://registry.yarnpkg.com/icheck-bootstrap/-/icheck-bootstrap-3.0.1.tgz#60c9c9a71524e1d9dd5bd05167a62fef05cc3a1b" integrity sha512-Rj3SybdcMcayhsP4IJ+hmCNgCKclaFcs/5zwCuLXH1WMo468NegjhZVxbSNKhEjJjnwc4gKETogUmPYSQ9lEZQ== -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -6336,6 +6407,13 @@ resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= +ignore-walk@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" + integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== + dependencies: + minimatch "^3.0.4" + ignore@^5.1.1, ignore@^5.1.4, ignore@^5.1.8, ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -6415,7 +6493,7 @@ once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -6614,6 +6692,13 @@ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" @@ -7786,6 +7871,14 @@ dependencies: minipass "^3.0.0" +minipass@^2.6.0, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: version "3.1.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" @@ -7793,6 +7886,13 @@ dependencies: yallist "^4.0.0" +minizlib@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + minizlib@^2.0.0, minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -7806,7 +7906,7 @@ resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^0.5.5, mkdirp@~0.5.1: +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -7973,7 +8073,7 @@ resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== -nanoid@3.3.1, nanoid@^3.2.0, nanoid@^3.3.1: +nanoid@3.3.1, nanoid@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== @@ -7988,6 +8088,15 @@ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +needle@^2.2.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.9.1.tgz#22d1dffbe3490c2b83e301f7709b6736cd8f2684" + integrity sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + negotiator@0.6.2, negotiator@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -8010,11 +8119,34 @@ dependencies: lower-case "^1.1.1" +node-addon-api@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + node-forge@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== +node-gyp@3.x: + version "3.8.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" + integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3 || 4" + osenv "0" + request "^2.87.0" + rimraf "2" + semver "~5.3.0" + tar "^2.0.0" + which "1" + node-gyp@^8.4.1: version "8.4.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" @@ -8031,6 +8163,22 @@ tar "^6.1.2" which "^2.0.2" +node-pre-gyp@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" + integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + node-preload@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301" @@ -8080,6 +8228,21 @@ undefsafe "^2.0.5" update-notifier "^5.1.0" +"nopt@2 || 3": + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= + dependencies: + abbrev "1" + +nopt@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" + integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== + dependencies: + abbrev "1" + osenv "^0.1.4" + nopt@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" @@ -8144,6 +8307,27 @@ jsdom "^16.6.0" marked "^4.0.10" +npm-bundled@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" + integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== + dependencies: + npm-normalize-package-bin "^1.0.1" + +npm-normalize-package-bin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" + integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== + +npm-packlist@^1.1.6: + version "1.4.8" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" + integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + npm-normalize-package-bin "^1.0.1" + npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -8151,6 +8335,16 @@ dependencies: path-key "^3.0.0" +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + npmlog@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" @@ -8178,6 +8372,11 @@ dependencies: boolbase "^1.0.0" +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" @@ -8221,7 +8420,7 @@ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.0.1, object-assign@^4.1.1: +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -8356,6 +8555,24 @@ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@0, osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + ospath@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" @@ -9215,7 +9432,7 @@ resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= -rc@^1.2.8: +rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -9265,7 +9482,7 @@ string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@~2.3.3, readable-stream@~2.3.6: +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.3, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -9404,7 +9621,7 @@ dependencies: throttleit "^1.0.0" -request@^2.88.0: +request@^2.87.0, request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -9538,7 +9755,7 @@ resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rimraf@^2.6.3: +rimraf@2, rimraf@^2.6.1, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -9595,7 +9812,7 @@ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -9628,7 +9845,7 @@ klona "^2.0.4" neo-async "^2.6.2" -sax@^1.2.1: +sax@^1.2.1, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -9725,7 +9942,7 @@ dependencies: semver "^6.3.0" -"semver@2 || 3 || 4 || 5", semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -9752,6 +9969,11 @@ dependencies: lru-cache "^6.0.0" +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= + send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -9801,7 +10023,7 @@ parseurl "~1.3.3" send "0.17.1" -set-blocking@^2.0.0: +set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -10091,6 +10313,16 @@ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +sqlite3@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.2.tgz#00924adcc001c17686e0a6643b6cbbc2d3965083" + integrity sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA== + dependencies: + node-addon-api "^3.0.0" + node-pre-gyp "^0.11.0" + optionalDependencies: + node-gyp "3.x" + sshpk@^1.14.1, sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -10199,6 +10431,15 @@ inherits "^2.0.1" readable-stream "^2.0.2" +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + "string-width@^1.0.1 || ^2.0.0": version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -10255,6 +10496,13 @@ dependencies: safe-buffer "~5.1.0" +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + "strip-ansi@^3.0.1 || ^4.0.0", strip-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" @@ -10489,6 +10737,28 @@ resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== +tar@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" + integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== + dependencies: + block-stream "*" + fstream "^1.0.12" + inherits "2" + +tar@^4: + version "4.4.19" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" + integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== + dependencies: + chownr "^1.1.4" + fs-minipass "^1.2.7" + minipass "^2.9.0" + minizlib "^1.3.3" + mkdirp "^0.5.5" + safe-buffer "^5.2.1" + yallist "^3.1.1" + tar@^6.0.2, tar@^6.1.2: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" @@ -11365,6 +11635,13 @@ has-tostringtag "^1.0.0" is-typed-array "^1.1.7" +which@1, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + which@2.0.2, which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -11372,14 +11649,7 @@ dependencies: isexe "^2.0.0" -which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.2: +wide-align@^1.1.0, wide-align@^1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== @@ -11501,6 +11771,11 @@ resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== +yallist@^3.0.0, yallist@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"