diff --git a/app/controllers/admin.js b/app/controllers/admin.js index 119e201..315957f 100644 --- a/app/controllers/admin.js +++ b/app/controllers/admin.js @@ -1,6 +1,6 @@ // admin.js // Copyright (C) 2021 Digital Telepresence, LLC -// All Rights Reserved +// License: Apache-2.0 'use strict'; diff --git a/app/controllers/admin/post.js b/app/controllers/admin/post.js index 7980a38..143e282 100644 --- a/app/controllers/admin/post.js +++ b/app/controllers/admin/post.js @@ -33,6 +33,8 @@ class PostController extends SiteController { router.get('/', this.getIndex.bind(this)); + router.delete('/:postId', this.deletePost.bind(this)); + return router; } diff --git a/app/controllers/post.js b/app/controllers/post.js index a961f6c..bc85bb3 100644 --- a/app/controllers/post.js +++ b/app/controllers/post.js @@ -42,32 +42,6 @@ class PostController extends SiteController { ); } - async populatePostYear (req, res, next, postYear) { - try { - res.locals.postYear = parseInt(postYear, 10); - if (!res.locals.postYear || isNaN(res.locals.postYear)) { - throw new Error('Invalid post year'); - } - return next(); - } catch (error) { - this.log.error('failed to populate post year', { postYear, error }); - return next(error); - } - } - - async populatePostMonth (req, res, next, postMonth) { - try { - res.locals.postMonth = parseInt(postMonth, 10); - if (!res.locals.postMonth || isNaN(res.locals.postMonth) || (postMonth < 1) || (postMonth > 12)) { - throw new Error('Invalid post month'); - } - return next(); - } catch (error) { - this.log.error('failed to populate post month', { postMonth, error }); - return next(error); - } - } - async populatePostSlug (req, res, next, postSlug) { const { post: postService } = this.dtp.services; try { @@ -82,8 +56,15 @@ class PostController extends SiteController { } } - async getView (req, res) { - res.render('post/view'); + async getView (req, res, next) { + const { resource: resourceService } = this.dtp.services; + try { + await resourceService.recordView(req, 'Post', res.locals.post._id); + res.render('post/view'); + } catch (error) { + this.log.error('failed to service post view', { postId: res.locals.post._id, error }); + return next(error); + } } async getIndex (req, res, next) { diff --git a/app/controllers/welcome.js b/app/controllers/welcome.js index 19dedc1..5a9964e 100644 --- a/app/controllers/welcome.js +++ b/app/controllers/welcome.js @@ -6,7 +6,10 @@ const DTP_COMPONENT_NAME = 'welcome'; +const path = require('path'); + const express = require('express'); +const captcha = require('svg-captcha'); const { SiteController/*, SiteError */ } = require('../../lib/site-lib'); @@ -20,9 +23,12 @@ class WelcomeController extends SiteController { const { limiter: limiterService } = this.dtp.services; const welcomeLimiter = limiterService.create(limiterService.config.welcome); + captcha.loadFont(path.join(this.dtp.config.root, 'client', 'fonts', 'Dirty Sweb.ttf')); + const router = express.Router(); this.dtp.app.use('/welcome', welcomeLimiter, router); + router.get('/signup/captcha', this.getSignupCaptcha.bind(this)); router.get('/signup', this.getSignupView.bind(this)); router.get('/login', this.getLoginView.bind(this)); router.get('/', this.getHomeView.bind(this)); @@ -30,7 +36,21 @@ class WelcomeController extends SiteController { return router; } + async getSignupCaptcha (req, res) { + const signupCaptcha = captcha(req.session.captcha.signup, { + color: false, + noise: 3, + width: 300, + height: 80, + }); + res.set('Content-Type', 'image/svg+xml'); + res.set('Content-Length', signupCaptcha.length); + res.status(200).send(signupCaptcha); + } + async getSignupView (req, res) { + req.session.captcha = req.session.captcha || { }; + req.session.captcha.signup = captcha.randomText(4 + Math.floor(Math.random()*4)); res.render('welcome/signup'); } diff --git a/app/models/comment.js b/app/models/comment.js new file mode 100644 index 0000000..8917984 --- /dev/null +++ b/app/models/comment.js @@ -0,0 +1,24 @@ +// comment.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const path = require('path'); + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const { CommentStats, CommentStatsDefaults } = require(path.join(__dirname, 'lib', 'resource-stats.js')); + +const CommentSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: 1 }, + resourceType: { type: String, enum: ['Post', 'Page', 'Newsletter'], required: true }, + resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' }, + replyTo: { type: Schema.ObjectId, index: 1, ref: 'Comment' }, + author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, + content: { type: String, required: true, maxlength: 3000 }, + stats: { type: CommentStats, default: CommentStatsDefaults, required: true }, +}); + +module.exports = mongoose.model('Comment', CommentSchema); \ No newline at end of file diff --git a/app/models/lib/resource-stats.js b/app/models/lib/resource-stats.js new file mode 100644 index 0000000..10ad788 --- /dev/null +++ b/app/models/lib/resource-stats.js @@ -0,0 +1,35 @@ +// lib/resource-stats.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +module.exports.ResourceStats = new Schema({ + totalViewCount: { type: Number, default: 0, required: true }, + upvoteCount: { type: Number, default: 0, required: true }, + downvoteCount: { type: Number, default: 0, required: true }, + commentCount: { type: Number, default: 0, required: true }, +}); + +module.exports.ResourceStatsDefaults = { + totalViewCount: 0, + upvoteCount: 0, + downvoteCount: 0, + commentCount: 0, +}; + +module.exports.CommentStats = new Schema({ + upvoteCount: { type: Number, default: 0, required: true }, + downvoteCount: { type: Number, default: 0, required: true }, + replyCount: { type: Number, default: 0, required: true }, +}); + +module.exports.CommentStatsDefaults = { + upvoteCount: 0, + downvoteCount: 0, + replyCount: 0, +}; \ No newline at end of file diff --git a/app/models/post.js b/app/models/post.js index 4881602..9d45be9 100644 --- a/app/models/post.js +++ b/app/models/post.js @@ -4,10 +4,13 @@ 'use strict'; +const path = require('path'); const mongoose = require('mongoose'); const Schema = mongoose.Schema; +const { ResourceStats, ResourceStatsDefaults } = require(path.join(__dirname, 'lib', 'resource-stats.js')); + const POST_STATUS_LIST = ['draft','published','archived']; const PostSchema = new Schema({ @@ -20,7 +23,9 @@ const PostSchema = new Schema({ summary: { type: String, required: true }, content: { type: String, required: true, select: false }, status: { type: String, enum: POST_STATUS_LIST, default: 'draft', index: true }, + stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true }, flags: { + enableComments: { type: Boolean, default: true, index: true }, isFeatured: { type: Boolean, default: false, index: true }, }, }); diff --git a/app/models/resource-view.js b/app/models/resource-view.js new file mode 100644 index 0000000..05c4fa5 --- /dev/null +++ b/app/models/resource-view.js @@ -0,0 +1,30 @@ +// resource-view.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const RESOURCE_TYPE_LIST = ['Page', 'Post', 'Newsletter']; + +const ResourceViewSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: 1, expires: '30d' }, + resourceType: { type: String, enum: RESOURCE_TYPE_LIST, required: true }, + resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' }, + uniqueKey: { type: String, required: true, index: 1 }, + viewCount: { type: Number, default: 0, required: true }, +}); + +ResourceViewSchema.index({ + created: 1, + resourceType: 1, + resource: 1, + uniqueKey: 1, +}, { + name: 'res_view_daily_unique', +}); + +module.exports = mongoose.model('ResourceView', ResourceViewSchema); \ No newline at end of file diff --git a/app/services/post.js b/app/services/post.js index 5d3accc..eb8fc09 100644 --- a/app/services/post.js +++ b/app/services/post.js @@ -36,7 +36,11 @@ class PostService extends SiteService { post.slug = this.createPostSlug(post._id, post.title); post.summary = striptags(postDefinition.summary.trim()); post.content = postDefinition.content.trim(); - post.status = 'draft'; + post.status = postDefinition.status || 'draft'; + post.flags = { + enableComments: postDefinition.enableComments === 'on', + isFeatured: postDefinition.isFeatured === 'on', + }; await post.save(); @@ -65,9 +69,8 @@ class PostService extends SiteService { updateOp.$set.status = striptags(postDefinition.status.trim()); } - if (Object.keys(updateOp.$set).length === 0) { - return; // no update to perform - } + updateOp.$set['flags.enableComments'] = postDefinition.enableComments === 'on'; + updateOp.$set['flags.isFeatured'] = postDefinition.isFeatured === 'on'; await Post.updateOne( { _id: post._id }, diff --git a/app/services/resource.js b/app/services/resource.js new file mode 100644 index 0000000..d07536d --- /dev/null +++ b/app/services/resource.js @@ -0,0 +1,72 @@ +// resource.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const { SiteService } = require('../../lib/site-lib'); + +const mongoose = require('mongoose'); + +const ResourceView = mongoose.model('ResourceView'); + +class ResourceService extends SiteService { + + constructor (dtp) { + super(dtp, module.exports); + this.populateResourceView = [ + { + path: 'resource', + }, + ]; + } + + /** + * Records 24-hour unique view counts for a given resource happening on a + * given ExpressJS Request. Views are uniqued by stripping time from the + * current Date, and upserting a tracking object in MongoDB. + * + * @param {Request} req + * @param {String} resourceType 'Post', 'Page', or 'Newsletter' + * @param {mongoose.Types.ObjectId} resourceId The _id of the object for which + * a view is being tracked. + */ + async recordView (req, resourceType, resourceId) { + const CURRENT_DAY = new Date(); + CURRENT_DAY.setHours(0, 0, 0, 0); + + let uniqueKey = req.ip.toString().trim().toLowerCase(); + if (req.user) { + uniqueKey += `:user:${req.user._id.toString()}`; + } + + const response = await ResourceView.updateOne( + { + created: CURRENT_DAY, + resourceType, + resource: resourceId, + uniqueKey, + }, + { + $inc: { viewCount: 1 }, + }, + { upsert: true }, + ); + this.log.debug('resource view', { response }); + if (response.upsertedCount > 0) { + const Model = mongoose.model(resourceType); + await Model.updateOne( + { _id: resourceId }, + { + $inc: { 'stats.totalViewCount': 1 }, + }, + ); + } + } +} + +module.exports = { + slug: 'resource', + name: 'resource', + create: (dtp) => { return new ResourceService(dtp); }, +}; \ No newline at end of file diff --git a/app/views/admin/post/editor.pug b/app/views/admin/post/editor.pug index cad0ee5..216fa57 100644 --- a/app/views/admin/post/editor.pug +++ b/app/views/admin/post/editor.pug @@ -5,12 +5,12 @@ block content form(method="POST", action= actionUrl).uk-form div(uk-grid).uk-grid-small - .uk-width-2-3 + div(class="uk-width-1-1 uk-width-2-3@m") .uk-margin label(for="content").uk-form-label Post body textarea(id="content", name="content", rows="4").uk-textarea= post ? post.content : undefined - .uk-width-1-3 + div(class="uk-width-1-1 uk-width-1-3@m") .uk-margin label(for="title").uk-form-label Post title input(id="title", name="title", type="text", placeholder= "Enter post title", value= post ? post.title : undefined).uk-input @@ -20,14 +20,22 @@ block content div(uk-grid) .uk-width-auto button(type="submit").uk-button.dtp-button-primary= post ? 'Update post' : 'Create post' - .uk-width-auto - a(href="/admin/post").uk-button.dtp-button-default Cancel .uk-margin label(for="status").uk-form-label Status select(id="status", name="status").uk-select option(value="draft", selected= post ? post.status === 'draft' : true) Draft option(value="published", selected= post ? post.status === 'published' : false) Published - option(value="archived", selected= post ? post.status === 'archived' : true) Archived + option(value="archived", selected= post ? post.status === 'archived' : false) Archived + .uk-margin + div(uk-grid).uk-grid-small + .uk-width-auto + label + input(id="enable-comments", name="enableComments", type="checkbox", checked= post ? post.flags.enableComments : true).uk-checkbox + | Enable comments + .uk-width-auto + label + input(id="is-featured", name="isFeatured", type="checkbox", checked= post ? post.flags.isFeatured : true).uk-checkbox + | Featured block viewjs script(src="/tinymce/tinymce.min.js") @@ -61,7 +69,6 @@ block viewjs { title: 'Body Image', value: 'dtp-image-body' }, { title: 'Title Image', value: 'dtp-image-title' }, ], - document_base_url: '/post/#{post.slug}', convert_urls: false, }); diff --git a/app/views/components/back-button.pug b/app/views/components/back-button.pug index e3f50e9..1b9a522 100644 --- a/app/views/components/back-button.pug +++ b/app/views/components/back-button.pug @@ -1,4 +1,4 @@ -button(type="button", onclick= "return window.history.back();").uk-button.dtp-button-primary +button(type="button", onclick= "return dtp.app.goBack();").uk-button.dtp-button-primary span i.fas.fa-chevron-left span.uk-margin-small-left Back diff --git a/app/views/components/library.pug b/app/views/components/library.pug index 3b83fa6..93557d3 100644 --- a/app/views/components/library.pug +++ b/app/views/components/library.pug @@ -11,4 +11,9 @@ include button-icon mixin renderCell (label, value, className) div(style="padding: 10px 20px;", title=`${label}: ${numeral(value).format('0,0')}`).uk-card.uk-card-default.uk-card-body.no-select .uk-text-muted= label - div(class=className)= value \ No newline at end of file + div(class=className)= value + +- + function displayIntegerValue (value) { + return numeral(value).format(value > 1000 ? '0,0.0a' : '0,0'); + } \ No newline at end of file diff --git a/app/views/index.pug b/app/views/index.pug index c42dbb5..af09314 100644 --- a/app/views/index.pug +++ b/app/views/index.pug @@ -34,8 +34,8 @@ block content img(src="/img/default-poster.jpg").responsive .uk-width-2-3 article.uk-article - h4.uk-article-title= post.title - .uk-margin.uk-margin-small.uk-text-truncate= post.summary + h4(style="line-height: 1;").uk-article-title.uk-margin-remove= post.title + .uk-text-truncate= post.summary .uk-article-meta div(uk-grid).uk-grid-small .uk-width-auto published: #{moment(post.created).format("MMM DD YYYY HH:MM a")} diff --git a/app/views/layouts/main.pug b/app/views/layouts/main.pug index 105045e..e9776e0 100644 --- a/app/views/layouts/main.pug +++ b/app/views/layouts/main.pug @@ -92,6 +92,7 @@ html(lang='en') if user script. window.dtp.user = !{JSON.stringify(safeUser, null, 2)} + window.dtp.domain = !{JSON.stringify(site.domain)} if channel script. diff --git a/app/views/post/view.pug b/app/views/post/view.pug index 28cbe76..058eac0 100644 --- a/app/views/post/view.pug +++ b/app/views/post/view.pug @@ -3,7 +3,7 @@ block content include ../components/page-sidebar - section.uk-section.uk-section-default + section.uk-section.uk-section-default.uk-section-small .uk-container div(uk-grid) .uk-width-2-3 @@ -17,9 +17,38 @@ block content a(href=`/admin/post/${post._id}`).uk-button.dtp-button-text EDIT .uk-text-lead= post.summary .uk-margin - .uk-article-meta= moment(post.created).format('MMM DD, YYYY [at] hh:mm a') + .uk-article-meta + div(uk-grid).uk-grid-small.uk-flex-top + .uk-width-expand + div published: #{moment(post.created).format('MMM DD, YYYY - hh:mm a').toUpperCase()} + .uk-width-auto + +renderButtonIcon('fa-eye', displayIntegerValue(post.stats.totalViewCount)) + .uk-width-auto + +renderButtonIcon('fa-chevron-up', displayIntegerValue(post.stats.upvoteCount)) + .uk-width-auto + +renderButtonIcon('fa-chevron-down', displayIntegerValue(post.stats.downvoteCount)) + .uk-width-auto + +renderButtonIcon('fa-comment', displayIntegerValue(post.stats.commentCount)) .uk-margin != post.content + if post.updated + .uk-margin + .uk-article-meta This post was updated on #{moment(post.updated).format('MMMM DD, YYYY, [at] hh:mm a')}. + + if post.flags.enableComments + .dtp-border-bottom + h3.uk-heading-bullet Comments + + .uk-margin + form(method="POST", action=`/post/${post._id}/comment`, onsubmit="return dtp.app.submitForm(event);").uk-form + .uk-margin + textarea(id="content", name="content", rows="4", placeholder="Enter comment").uk-textarea + .uk-text-small + div(uk-grid).uk-flex-between + .uk-width-auto You are commenting as: #{user.username} + .uk-width-auto 0 of 3000 + button(type="submit").uk-button.dtp-button-primary Post comment + .uk-width-1-3 +renderPageSidebar() \ No newline at end of file diff --git a/app/views/welcome/signup.pug b/app/views/welcome/signup.pug index 92a74a7..dda7f25 100644 --- a/app/views/welcome/signup.pug +++ b/app/views/welcome/signup.pug @@ -39,6 +39,10 @@ block content .uk-container.uk-container-small .uk-margin-large .uk-text-center + .uk-margin + .uk-width-medium.uk-margin-auto + div(style="background: white;") + img(src='/welcome/signup/captcha', style="padding: 8px 0;").uk-display-block.uk-margin-auto + input(id="captcha", name="captcha", type="text", placeholder="Enter captcha text").uk-input.uk-text-center .uk-margin-small - button(type="submit").uk-button.uk-button-primary Create Account - .uk-text-center.uk-text-small.uk-text-muted You aren't going to be asked for money and I'm not going to bug you. This is only a technology demo. \ No newline at end of file + button(type="submit").uk-button.dtp-button-primary Create Account \ No newline at end of file diff --git a/app/workers/newsletter.js b/app/workers/newsletter.js index 7e42dfe..6fc3ee6 100644 --- a/app/workers/newsletter.js +++ b/app/workers/newsletter.js @@ -1,6 +1,6 @@ // newsletter.js // Copyright (C) 2021 Digital Telepresence, LLC -// All Rights Reserved +// License: Apache-2.0 'use strict'; diff --git a/client/fonts/CodeBars.ttf b/client/fonts/CodeBars.ttf new file mode 100644 index 0000000..53d06c4 Binary files /dev/null and b/client/fonts/CodeBars.ttf differ diff --git a/client/fonts/Dirty Sweb.ttf b/client/fonts/Dirty Sweb.ttf new file mode 100644 index 0000000..35f4ab3 Binary files /dev/null and b/client/fonts/Dirty Sweb.ttf differ diff --git a/client/fonts/Ink Studio.ttf b/client/fonts/Ink Studio.ttf new file mode 100644 index 0000000..b02ddfa Binary files /dev/null and b/client/fonts/Ink Studio.ttf differ diff --git a/client/fonts/inkswipe.ttf b/client/fonts/inkswipe.ttf new file mode 100644 index 0000000..a68750f Binary files /dev/null and b/client/fonts/inkswipe.ttf differ diff --git a/client/js/site-admin-host-stats-app.js b/client/js/site-admin-host-stats-app.js index 9d6fe7a..633f119 100644 --- a/client/js/site-admin-host-stats-app.js +++ b/client/js/site-admin-host-stats-app.js @@ -225,6 +225,30 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp { UIkit.modal.alert(`Failed to delete newsletter: ${error.message}`); } } + + async deletePost (event) { + const postId = event.currentTarget.getAttribute('data-post-id'); + const postTitle = event.currentTarget.getAttribute('data-post-title'); + console.log(postId, postTitle); + try { + await UIkit.modal.confirm(`Are you sure you want to delete "${postTitle}"`); + } catch (error) { + this.log.info('deletePost', 'aborted'); + return; + } + try { + const response = await fetch(`/admin/post/${postId}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to delete post'); + } + await this.processResponse(response); + } catch (error) { + this.log.error('deletePost', 'failed to delete post', { postId, postTitle, error }); + UIkit.modal.alert(`Failed to delete post: ${error.message}`); + } + } } dtp.DtpSiteAdminHostStatsApp = DtpSiteAdminHostStatsApp; \ No newline at end of file diff --git a/client/js/site-app.js b/client/js/site-app.js index 22403ec..a195f66 100644 --- a/client/js/site-app.js +++ b/client/js/site-app.js @@ -137,6 +137,15 @@ export default class DtpSiteApp extends DtpApp { this.chat.messageList.scrollTop = this.chat.messageList.scrollHeight; } + async goBack ( ) { + if (document.referrer && (document.referrer.indexOf(`://${window.dtp.domain}`) >= 0)) { + window.history.back(); + } else { + window.location.href= '/'; + } + return false; + } + async submitForm (event, userAction) { event.preventDefault(); event.stopPropagation(); diff --git a/package.json b/package.json index 952d093..28f3dee 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "socket.io": "^4.4.0", "socket.io-emitter": "^3.2.0", "striptags": "^3.2.0", + "svg-captcha": "^1.4.0", "systeminformation": "^5.9.16", "tinymce": "^5.10.2", "uikit": "^3.9.4", diff --git a/sites-start-local b/sites-start-local index e7691da..2af3553 100755 --- a/sites-start-local +++ b/sites-start-local @@ -4,7 +4,7 @@ MINIO_ROOT_USER="sites" MINIO_ROOT_PASSWORD="1f1c7c9b-b833-4462-ae41-d56f52faa49c" export MINIO_ROOT_USER MINIO_ROOT_PASSWORD -forever start app/workers/host-services.js +forever start --killSignal=SIGINT app/workers/host-services.js minio server ./data/minio --console-address ":9001" diff --git a/supervisord/dtp-sites.conf b/supervisord/dtp-sites.conf new file mode 100644 index 0000000..2df7a0b --- /dev/null +++ b/supervisord/dtp-sites.conf @@ -0,0 +1,13 @@ +[program:dtp-sites] +numprocs=1 +process_name=%(program_name)s_%(process_num)02d +command=/home/dtp/.nvm/versions/node/v16.13.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 dtp-sites.js +directory=/home/dtp/live/dtp-sites +autostart=true +autorestart=true +startretries=3 +stopsignal=INT +stderr_logfile=/var/log/dtp-sites/dtp-sites.err.log +stdout_logfile=/var/log/dtp-sites/dtp-sites.out.log +user=dtp +environment=HOME='/home/dtp/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=dtp-sites \ No newline at end of file diff --git a/supervisord/host-services.conf b/supervisord/host-services.conf new file mode 100644 index 0000000..ef11c57 --- /dev/null +++ b/supervisord/host-services.conf @@ -0,0 +1,13 @@ +[program:host-services] +numprocs=1 +process_name=%(program_name)s_%(process_num)02d +command=/home/dtp/.nvm/versions/node/v16.13.0/bin/node --optimize_for_size --max_old_space_size=1024 --gc_interval=100 app/workers/host-services.js +directory=/home/dtp/live/dtp-sites +autostart=true +autorestart=true +startretries=3 +stopsignal=INT +stderr_logfile=/var/log/dtp-sites/host-services.err.log +stdout_logfile=/var/log/dtp-sites/host-services.out.log +user=dtp +environment=HOME='/home/dtp/live/dtp-sites',HTTP_BIND_PORT=30%(process_num)02d,NODE_ENV=production,LOGNAME=host-services \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b76f5f8..5a41567 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5787,6 +5787,13 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +opentype.js@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-0.7.3.tgz#40fb8ce18bfd60e74448efdfe442834098397aab" + integrity sha1-QPuM4Yv9YOdESO/f5EKDQJg5eqs= + dependencies: + tiny-inflate "^1.0.2" + openurl@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/openurl/-/openurl-1.1.1.tgz#3875b4b0ef7a52c156f0db41d4609dbb0f94b387" @@ -7523,6 +7530,13 @@ sver-compat@^1.5.0: es6-iterator "^2.0.1" es6-symbol "^3.1.1" +svg-captcha@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/svg-captcha/-/svg-captcha-1.4.0.tgz#32ead3c6463936c218bb3bc9ed04fea4eeffe492" + integrity sha512-/fkkhavXPE57zRRCjNqAP3txRCSncpMx3NnNZL7iEoyAtYwUjPhJxW6FQTQPG5UPEmCrbFoXS10C3YdJlW7PDg== + dependencies: + opentype.js "^0.7.3" + symbol-observable@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" @@ -7653,6 +7667,11 @@ time-stamp@^1.0.0: resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= +tiny-inflate@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" + integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== + tinymce@^5.10.2: version "5.10.2" resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.10.2.tgz#cf1ff01025909be26c64348509e6de8e70d58e1d"