Merge branch 'master' into ruby-3

This commit is contained in:
Earlopain 2022-05-28 15:45:25 +02:00
commit 57f711bb4d
No known key found for this signature in database
GPG Key ID: 6CFB948E15246897
88 changed files with 1086 additions and 13105 deletions

View File

@ -42,9 +42,6 @@ gem 'net-smtp'
gem 'net-pop'
gem 'net-imap'
# needed for looser jpeg header compat
gem 'ruby-imagespec', :require => "image_spec", :git => "https://github.com/r888888888/ruby-imagespec.git", :branch => "exif-fixes"
group :production, :staging do
gem 'unicorn', :platforms => :ruby
end
@ -55,7 +52,6 @@ group :production do
end
group :development, :test do
gem 'awesome_print'
gem 'pry-byebug'
gem 'listen'
gem 'puma'
@ -67,9 +63,7 @@ group :test do
gem "factory_bot"
gem "mocha", :require => "mocha/minitest"
gem "ffaker"
gem "simplecov", :require => false
gem "timecop"
gem "webmock"
gem "minitest-ci"
gem "mock_redis"
end

View File

@ -1,10 +1,3 @@
GIT
remote: https://github.com/r888888888/ruby-imagespec.git
revision: 2dab9811f4abb4fbaeea66feb42e388ba545b2d8
branch: exif-fixes
specs:
ruby-imagespec (0.3.1)
GIT
remote: https://github.com/zwagoth/dtext_rb.git
revision: efc37c7795c53425935245cde57cef2121b4f402
@ -86,7 +79,6 @@ GEM
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
awesome_print (1.9.2)
bcrypt (3.1.16)
bootsnap (1.9.3)
msgpack (~> 1.0)
@ -108,7 +100,6 @@ GEM
dalli (3.1.5)
diff-lcs (1.5.0)
digest (3.1.0)
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
@ -200,8 +191,6 @@ GEM
mini_mime (1.1.2)
mini_portile2 (2.8.0)
minitest (5.15.0)
minitest-ci (3.4.0)
minitest (>= 5.0.6)
mocha (1.13.0)
mock_redis (0.29.0)
ruby2_keywords
@ -243,7 +232,7 @@ GEM
puma (5.6.4)
nio4r (~> 2.0)
racc (1.6.0)
rack (2.2.3)
rack (2.2.3.1)
rack-proxy (0.7.0)
rack
rack-test (1.1.0)
@ -323,12 +312,6 @@ GEM
simple_form (5.1.0)
actionpack (>= 5.2)
activemodel (>= 5.2)
simplecov (0.21.2)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.3)
sprockets (4.0.3)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -377,7 +360,6 @@ PLATFORMS
DEPENDENCIES
active_model_serializers (~> 0.10.0)
addressable
awesome_print
bcrypt
bootsnap
cityhash
@ -396,7 +378,6 @@ DEPENDENCIES
mailgun-ruby
marcel
memoist
minitest-ci
mocha
mock_redis
net-imap
@ -413,7 +394,6 @@ DEPENDENCIES
resolv
responders
retriable
ruby-imagespec!
ruby-vips
sanitize
shoulda-context
@ -421,7 +401,6 @@ DEPENDENCIES
sidekiq
sidekiq-unique-jobs
simple_form
simplecov
streamio-ffmpeg
timecop
unicorn

View File

@ -5,15 +5,13 @@ module Admin
def index
end
def enable_uploads
DangerZone.enable_uploads
StaffAuditLog.log(:uploads_enable, CurrentUser.user)
redirect_to admin_danger_zone_index_path
end
def disable_uploads
DangerZone.disable_uploads
StaffAuditLog.log(:uploads_disable, CurrentUser.user)
def uploading_limits
new_level = params[:uploading_limits][:min_level].to_i
raise ArgumentError, "#{new_level} is not valid" unless User.level_hash.values.include? new_level
if new_level != DangerZone.min_upload_level
DangerZone.min_upload_level = new_level
StaffAuditLog.log(:min_upload_level, CurrentUser.user, { level: new_level })
end
redirect_to admin_danger_zone_index_path
end
end

View File

@ -6,6 +6,7 @@ class PostReplacementsController < ApplicationController
content_security_policy only: [:new] do |p|
p.img_src :self, :data, :blob, "*"
p.media_src :self, :data, :blob, "*"
end
def new

View File

@ -3,7 +3,6 @@ class PostSetMaintainersController < ApplicationController
respond_to :js, except: [:index]
before_action :member_only
def index
@invites = PostSetMaintainer.where(user_id: CurrentUser.id).order(updated_at: :desc).includes(:post_set)
end
@ -94,9 +93,8 @@ class PostSetMaintainersController < ApplicationController
end
def check_edit_access(set)
unless set.is_owner?(CurrentUser) || CurrentUser.is_admin?
unless set.can_edit_settings?(CurrentUser)
raise User::PrivilegeError
end
end
end

View File

@ -41,24 +41,23 @@ class PostSetsController < ApplicationController
def edit
@post_set = PostSet.find(params[:id])
check_edit_access(@post_set)
@can_edit = @post_set.is_owner?(CurrentUser) || CurrentUser.is_admin?
check_post_edit_access(@post_set)
respond_with(@post_set)
end
def update
@post_set = PostSet.find(params[:id])
check_edit_access(@post_set)
check_settings_edit_access(@post_set)
@post_set.update(set_params)
flash[:notice] = @post_set.valid? ? 'Set updated' : @post_set.errors.full_messages.join('; ')
if CurrentUser.is_admin? && !@post_set.is_owner?(CurrentUser.user)
unless @post_set.is_owner?(CurrentUser.user)
if @post_set.saved_change_to_is_public?
ModAction.log(:set_mark_private, {set_id: @post_set.id, user_id: @post_set.creator_id})
ModAction.log(:set_change_visibility, { set_id: @post_set.id, user_id: @post_set.creator_id, is_public: @post_set.is_public })
end
if @post_set.saved_change_to_watched_attribute?
Modaction.log(:set_update, {set_id: @post_set.id, user_id: @post_set.creator_id})
if @post_set.saved_change_to_watched_attributes?
ModAction.log(:set_update, { set_id: @post_set.id, user_id: @post_set.creator_id })
end
end
@ -71,13 +70,13 @@ class PostSetsController < ApplicationController
def post_list
@post_set = PostSet.find(params[:id])
check_edit_access(@post_set)
check_post_edit_access(@post_set)
respond_with(@post_set)
end
def update_posts
@post_set = PostSet.find(params[:id])
check_edit_access(@post_set)
check_post_edit_access(@post_set)
@post_set.update(update_posts_params)
flash[:notice] = @post_set.valid? ? 'Set posts updated.' : @post_set.errors.full_messages.join('; ')
@ -86,9 +85,7 @@ class PostSetsController < ApplicationController
def destroy
@post_set = PostSet.find(params[:id])
unless @post_set.is_owner?(CurrentUser.user) || CurrentUser.is_admin?
raise User::PrivilegeError
end
check_settings_edit_access(@post_set)
if CurrentUser.is_admin?
ModAction.log(:set_delete, {set_id: @post_set.id, user_id: @post_set.creator_id})
end
@ -110,7 +107,7 @@ class PostSetsController < ApplicationController
def add_posts
@post_set = PostSet.find(params[:id])
check_edit_access(@post_set)
check_post_edit_access(@post_set)
@post_set.add(params[:post_ids].map(&:to_i))
@post_set.save
respond_with(@post_set)
@ -118,7 +115,7 @@ class PostSetsController < ApplicationController
def remove_posts
@post_set = PostSet.find(params[:id])
check_edit_access(@post_set)
check_post_edit_access(@post_set)
@post_set.remove(params[:post_ids].map(&:to_i))
@post_set.save
respond_with(@post_set)
@ -126,17 +123,20 @@ class PostSetsController < ApplicationController
private
def check_edit_access(set)
unless set.is_owner?(CurrentUser.user) || set.is_maintainer?(CurrentUser.user)
def check_settings_edit_access(set)
unless set.can_edit_settings?(CurrentUser.user)
raise User::PrivilegeError
end
if !set.is_public && !set.is_owner?(CurrentUser.user)
end
def check_post_edit_access(set)
unless set.can_edit_posts?(CurrentUser.user)
raise User::PrivilegeError
end
end
def check_view_access(set)
unless set.is_public || set.is_owner?(CurrentUser.user) || CurrentUser.is_admin?
unless set.can_view?(CurrentUser.user)
raise User::PrivilegeError
end
end

View File

@ -13,7 +13,7 @@ class TagAliasRequestsController < ApplicationController
elsif @tag_alias_request.forum_topic
redirect_to forum_topic_path(@tag_alias_request.forum_topic)
else
redirect_to tag_alias_path(@tag_alias_request.tag_alias)
redirect_to tag_alias_path(@tag_alias_request.tag_relationship)
end
end

View File

@ -13,7 +13,7 @@ class TagImplicationRequestsController < ApplicationController
elsif @tag_implication_request.forum_topic
redirect_to forum_topic_path(@tag_implication_request.forum_topic)
else
redirect_to tag_implication_path(@tag_implication_request.tag_implication)
redirect_to tag_implication_path(@tag_implication_request.tag_relationship)
end
end

View File

@ -1,6 +1,7 @@
class UploadsController < ApplicationController
before_action :member_only
before_action :janitor_only, only: [:index, :show]
before_action :ensure_uploads_enabled, only: [:new, :create]
respond_to :html, :json
content_security_policy only: [:new] do |p|
p.img_src :self, :data, :blob, "*"
@ -32,10 +33,6 @@ class UploadsController < ApplicationController
end
def create
if DangerZone.uploads_disabled?
return access_denied("Uploads are disabled.")
end
Post.transaction do
@service = UploadService.new(upload_params)
@upload = @service.start!
@ -81,4 +78,10 @@ class UploadsController < ApplicationController
params.require(:upload).permit(permitted_params).merge(uploader_id: CurrentUser.id, uploader_ip_addr: CurrentUser.ip_addr)
end
def ensure_uploads_enabled
if DangerZone.uploads_disabled?(CurrentUser.user)
access_denied "Uploads are disabled"
end
end
end

View File

@ -100,8 +100,8 @@ class ModActionDecorator < ApplicationDecorator
### Set ###
when "set_mark_private"
"Made set ##{vals['set_id']} by #{user} private"
when "set_change_visibility"
"Made set ##{vals['set_id']} by #{user} #{vals['is_public'] ? 'public' : 'private'}"
when "set_update"
"Edited set ##{vals['set_id']} by #{user}"
when "set_delete"

View File

@ -40,6 +40,7 @@ export { default as PostModeMenu } from '../src/javascripts/post_mode_menu.js';
export { default as PostReplacement } from '../src/javascripts/post_replacement.js';
export { default as PostVersions } from '../src/javascripts/post_versions.js';
export { default as RelatedTag } from '../src/javascripts/related_tag.js';
export { default as Replacer } from '../src/javascripts/replacer.js';
export { default as Shortcuts } from '../src/javascripts/shortcuts.js';
export { default as Utility } from '../src/javascripts/utility.js';
export { default as TagRelationships } from '../src/javascripts/tag_relationships.js';

View File

@ -19,25 +19,16 @@ RelatedTag.initialize_all = function() {
}
import TagEditor from './tag_editor.vue';
import Vue from 'vue';
import { createApp } from 'vue';
RelatedTag.tag_editor_setup = false;
RelatedTag.init_post_show_editor = function() {
if (RelatedTag.tag_editor_setup)
return;
RelatedTag.tag_editor_setup = true;
const app = new Vue({
render: (h) => h(TagEditor)
});
app.$mount('#tag-string-editor');
setTimeout(function() {
// Work around that browsers seem to take a few frames to acknowledge that the element is there before it can be focused.
const el = app.$children[0].$refs['otherTags'];
el.style.height = el.scrollHeight + "px";
el.focus();
el.scrollIntoView();
}, 20);
const app = createApp(TagEditor);
app.mount('#tag-string-editor');
}
RelatedTag.on_click_related_tags_button = function (event) {

View File

@ -1,142 +0,0 @@
const Replacer = {};
const thumbURLs = [
"/images/notfound-preview.png",
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
];
const thumbs = {
notfound: "/images/notfound-preview.png",
none: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
};
Replacer.old_domain = "";
Replacer.update_preview_file = function (file) {
const objectUrl = URL.createObjectURL(file);
Replacer.set_preview_url(objectUrl);
};
Replacer.update_preview_url = function () {
const url = $("#post_replacement_replacement_url").val();
if (!url) {
Replacer.upload_allow_clear();
return;
}
const sampleURL = Replacer.isSampleURL(url);
if (sampleURL !== false) {
$('#bad_upload_url_reason').text(sampleURL);
$('#bad_upload_url').show();
return;
} else {
$('#bad_upload_url').hide();
}
const domain = $("<a>").prop("href", url).prop("hostname");
if (domain && domain !== Replacer.old_domain) {
$.getJSON("/upload_whitelists/is_allowed.json", {url: url}, function(data) {
if(data.domain) {
Replacer.upload_allow_set(data.is_allowed, data.domain, data.reason);
if(!data.is_allowed)
Replacer.set_preview_url(thumbs.none);
}
});
} else if (!domain) {
Replacer.upload_allow_clear();
}
Replacer.old_domain = domain;
Replacer.set_preview_url(url);
};
Replacer.update_preview_dims = function () {
const img = $('#replacement_preview_img')[0];
if (thumbURLs.filter(function (x) {
return img.src.indexOf(x) !== -1;
}).length !== 0)
return;
Replacer.set_preview_dims(img.naturalHeight, img.naturalWidth);
};
Replacer.preview_error = function (e) {
const img = e.target;
Replacer.set_preview_dims(-1, -1);
if (thumbURLs.filter(function (x) {
return img.src.indexOf(x) !== -1;
}).length !== 0)
return;
Replacer.set_preview_url(thumbs.notfound);
};
Replacer.set_preview_dims = function (height, width) {
if (height <= 0 && width <= 0) {
$('#replacement_preview_dims').text('');
} else {
$('#replacement_preview_dims').text(`${width}x${height}`);
}
};
Replacer.set_preview_url = function (url) {
$('#replacement_preview_img').attr('src', url);
};
Replacer.update_preview = function () {
const $file = $("#post_replacement_replacement_file")[0];
if ($file && $file.files[0])
Replacer.update_preview_file($file.files[0]);
else
Replacer.update_preview_url();
}
Replacer.update_preview_paste = function () {
setTimeout(Replacer.update_preview, 150);
}
Replacer.isSampleURL = function (url) {
const patterns = [
{reason: 'Thumbnail URL', test: /[at]\.facdn\.net/gi},
{reason: 'Sample URL', test: /pximg\.net.*\/img-master\//gi},
{reason: 'Sample URL', test: /d3gz42uwgl1r1y\.cloudfront\.net\/.*\/\d+x\d+\./gi},
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/[\w\-_]+\.(jpg|png)(:large)?$/gi},
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/[\w\-_]+\?format=(jpg|png)(?!&name=orig)/gi},
{reason: 'Sample URL', test: /derpicdn\.net\/.*\/large\./gi},
{reason: 'Sample URL', test: /metapix\.net\/files\/(preview|screen)\//gi},
{reason: 'Sample URL', test: /sofurryfiles\.com\/std\/preview/gi}];
for (let i = 0; i < patterns.length; ++i) {
const pattern = patterns[i];
if (pattern.test.test(url))
return pattern.reason;
}
return false;
}
Replacer.upload_allow_clear = function() {
$('#whitelist-warning').hide();
}
Replacer.upload_allow_set = function(allowed, domain, reason) {
const classes = ['whitelist-warning-disallowed', 'whitelist-warning-allowed'];
$('#whitelist-warning').removeClass().addClass(classes[allowed ? 1 : 0]);
$('#whitelist-warning-domain').text(domain);
if(allowed)
$('#whitelist-warning-not').hide();
else
$('#whitelist-warning-not').show();
$('#whitelist-warning').show();
}
Replacer.init_uploader = function () {
$('#replacement_preview_img').on('load', Replacer.update_preview_dims);
$('#replacement_preview_img').on('error', Replacer.preview_error);
$('#post_replacement_replacement_url').on('keyup', Replacer.update_preview);
$('#post_replacement_replacement_file').on('change', Replacer.update_preview);
$('#post_replacement_replacement_url').on('paste', Replacer.update_preview_paste);
};
$(function () {
if ($("#c-post-replacements > #a-new").length) {
Replacer.init_uploader();
}
});
export default Replacer;

View File

@ -0,0 +1,28 @@
<template>
<file-input @previewChanged="previewData = $event"
:fileInputName="'post_replacement[replacement_file]'"
:urlInputName="'post_replacement[replacement_url]'"></file-input>
<Teleport to="#replacement-preview">
<file-preview :data="previewData"></file-preview>
</Teleport>
</template>
<script>
import filePreview from "./uploader/file_preview.vue";
import fileInput from "./uploader/file_input.vue";
export default {
components: {
"file-preview": filePreview,
"file-input": fileInput,
},
data() {
return {
previewData: {
url: "",
isVideo: false,
},
};
},
};
</script>

View File

@ -0,0 +1,9 @@
import Replacer from "./replacement_uploader.vue";
import { createApp } from "vue";
export default {
init() {
const app = createApp(Replacer);
app.mount("#replacement-uploader");
}
}

View File

@ -2,7 +2,7 @@
<div>
<div v-show="!preview.show">
<textarea class="tag-textarea" id="post_tag_string" v-model="tags" rows="5" data-autocomplete="tag-edit"
ref="otherTags" name="post[tag_string]" spellcheck="false" @keyup="updateTagCount"></textarea>
ref="otherTags" name="post[tag_string]" :spellcheck="false" @keyup="updateTagCount"></textarea>
</div>
<div v-show="preview.show">
<tag-preview :tags="preview.tags" :loading="preview.loading" @close="previewFinalTags"></tag-preview>
@ -27,9 +27,9 @@
</template>
<script>
import Vue from 'vue';
import relatedTags from './uploader_related.vue';
import tagPreview from './uploader_tag_preview.vue';
import { nextTick } from 'vue';
import relatedTags from './uploader/related.vue';
import tagPreview from './uploader/tag_preview.vue';
import Post from './posts';
import Autocomplete from "./autocomplete.js.erb";
import Utility from "./utility.js";
@ -57,6 +57,13 @@
};
},
mounted() {
setTimeout(() => {
// Work around that browsers seem to take a few frames to acknowledge that the element is there before it can be focused.
const el = this.$refs.otherTags;
el.style.height = el.scrollHeight + "px";
el.focus();
el.scrollIntoView();
}, 20);
if(Utility.meta("enable-auto-complete") !== "true")
return;
Autocomplete.initialize_tag_autocomplete();
@ -105,7 +112,7 @@
}
this.tags = groups.join('\n') + ' ';
}
Vue.nextTick(function() {
nextTick(function() {
Post.update_tag_count({target: $("#post_tag_string")});
})
},
@ -152,9 +159,7 @@
return sortedRelated;
};
const getSelectedTags = function () {
const field = self.$refs['otherTags'];
console.log(field.hasOwnProperty('selectionStart'));
console.log(field.selectionStart);
const field = self.$refs.otherTags;
if (typeof field['selectionStart'] === 'undefined')
return null;
const length = field.selectionEnd - field.selectionStart;

View File

@ -1,12 +1,9 @@
import Uploader from './uploader.vue';
import Vue from 'vue';
import Uploader from './uploader/uploader.vue';
import { createApp } from 'vue';
export default {
init() {
const app = new Vue({
render: (h) => h(Uploader)
});
app.$mount('#uploader');
const app = createApp(Uploader);
app.mount('#uploader');
}
}

View File

@ -0,0 +1,168 @@
<template>
<span>
<div v-if="!disableFileUpload">
<div class="box-section sect_red" v-if="fileTooLarge">
The file you are trying to upload is too large. Maximum allowed is {{this.maxFileSize / (1024*1024) }} MiB.<br>
Check out <a href="/help/supported_filetypes">the Supported Formats</a> for more information.
</div>
<label>File:
<input type="file" :name="fileInputName" ref="post_file" @change="updatePreviewFile"
accept="image/png,image/apng,image/jpeg,image/gif,video/webm,.png,.apng,.jpg,.jpeg,.gif,.webm"
:disabled="disableFileUpload"/>
</label>
<button @click.prevent="clearFileUpload" v-show="disableURLUpload">Clear</button>
</div>
<div v-if="!disableURLUpload">
<div class="box-section sect_red" v-if="badDirectURL">
The direct URL entered has the following problem: {{ directURLProblem }}<br>
You should review <a href="/wiki_pages/howto:sites_and_sources">the sourcing guide</a>.
</div>
<label>{{!disableFileUpload ? "(or) " : "" }}URL:
<input type="text" :name="urlInputName" size="50" v-model="uploadURL" :disabled="disableURLUpload"/>
</label>
<div id="whitelist-warning" v-show="whitelist.visible"
:class="{'whitelist-warning-allowed': whitelist.allowed, 'whitelist-warning-disallowed': !whitelist.allowed}">
<span v-if="whitelist.allowed">Uploads from <b>{{whitelist.domain}}</b> are permitted.</span>
<span v-if="!whitelist.allowed">Uploads from <b>{{whitelist.domain}}</b> are not permitted.
<span v-if="whitelist.reason">Reason given: {{whitelist.reason}}</span>
(<a href="/upload_whitelists">View whitelisted domains</a>)</span>
</div>
</div>
</span>
</template>
<script>
export default {
data() {
return {
whitelist: {
visible: false,
allowed: false,
reason: "",
domain: "",
oldDomain: "",
},
uploadURL: new URLSearchParams(window.location.search).get("upload_url") || "",
fileTooLarge: false,
maxFileSize: window.uploaderSettings.maxFileSize,
disableFileUpload: false,
disableURLUpload: false,
}
},
props: ["fileInputName", "urlInputName"],
computed: {
directURLProblem: function () {
return this.directURLCheck(this.uploadURL);
},
badDirectURL: function () {
return !!this.directURLProblem;
},
invalidUploadValue: function() {
return this.badDirectURL || this.fileTooLarge;
}
},
watch: {
uploadURL: {
immediate: true,
handler() {
this.fileTooLarge = false;
this.uploadValueChanged(this.uploadURL);
this.updatePreviewURL();
}
},
invalidUploadValue() {
this.$emit("invalidUploadValueChanged", this.invalidUploadValue);
}
},
methods: {
whitelistWarning(allowed, domain, reason) {
this.whitelist.allowed = allowed;
this.whitelist.domain = domain;
this.whitelist.reason = reason;
this.whitelist.visible = true;
},
clearWhitelistWarning() {
this.whitelist.visible = false;
this.whitelist.domain = "";
},
directURLCheck(url) {
const patterns = [
{ reason: "Thumbnail URL", test: /[at]\.(facdn|furaffinity)\.net/gi },
{ reason: "Sample URL", test: /pximg\.net.*\/img-master\//gi },
{ reason: "Sample URL", test: /d3gz42uwgl1r1y\.cloudfront\.net\/.*\/\d+x\d+\./gi },
{ reason: "Sample URL", test: /pbs\.twimg\.com\/media\/[\w\-_]+\.(jpg|png)(:large)?$/gi },
{ reason: "Sample URL", test: /pbs\.twimg\.com\/media\/[\w\-_]+\?format=(jpg|png)(?!&name=orig)/gi },
{ reason: "Sample URL", test: /derpicdn\.net\/.*\/large\./gi },
{ reason: "Sample URL", test: /metapix\.net\/files\/(preview|screen)\//gi },
{ reason: "Sample URL", test: /sofurryfiles\.com\/std\/preview/gi }
];
for (const pattern of patterns) {
if (pattern.test.test(url)) {
return pattern.reason;
}
}
return "";
},
clearFileUpload() {
if (!this.$refs["post_file"]?.files?.[0]) {
return;
}
this.$refs["post_file"].value = null;
this.disableURLUpload = false;
this.disableFileUpload = false;
this.fileTooLarge = false;
this.setEmptyThumb();
this.uploadValueChanged("");
},
updatePreviewURL() {
if (this.uploadURL.length === 0 || this.$refs["post_file"]?.files?.[0]) {
this.disableFileUpload = false;
this.whitelist.oldDomain = "";
this.clearWhitelistWarning();
return;
}
this.disableFileUpload = true;
const domain = $("<a>").prop("href", this.uploadURL).prop("hostname");
if (domain && domain !== this.whitelist.oldDomain) {
$.getJSON("/upload_whitelists/is_allowed.json", {url: this.uploadURL}, data => {
if (data.domain) {
this.whitelistWarning(data.is_allowed, data.domain, data.reason);
if (!data.is_allowed) {
this.setEmptyThumb();
}
}
});
} else if (!domain) {
this.clearWhitelistWarning();
this.setEmptyThumb();
}
this.whitelist.oldDomain = domain;
if(/^(https?\:\/\/|www).*?$/.test(this.uploadURL)) {
const isVideo = /^(https?\:\/\/|www).*?\.(webm)$/.test(this.uploadURL);
this.previewChanged(this.uploadURL, isVideo);
} else {
this.setEmptyThumb();
}
},
updatePreviewFile() {
const file = this.$refs["post_file"].files[0];
this.fileTooLarge = file.size > this.maxFileSize;
const objectUrl = URL.createObjectURL(file);
this.disableURLUpload = true;
this.uploadValueChanged(file);
this.previewChanged(objectUrl, file.type === "video/webm");
},
uploadValueChanged(value) {
this.$emit("uploadValueChanged", value);
},
setEmptyThumb() {
this.previewChanged("", false);
},
previewChanged(url, isVideo) {
this.$emit("previewChanged", { url: url, isVideo: isVideo });
},
}
}
</script>

View File

@ -0,0 +1,74 @@
<template>
<div class="upload_preview_container" :class="classes">
<div class="box-section sect_red" v-show="overDims">
One of the image dimensions is above the maximum allowed of 15,000px and will fail to upload.
</div>
<div v-if="!failed">
<div class="upload_preview_dims">{{ previewDimensions }}</div>
<video v-if="data.isVideo" class="upload_preview_img" controls :src="finalPreviewUrl"
v-on:loadeddata="updateDimensions($event)" v-on:error="previewFailed()">
</video>
<img v-else class="upload_preview_img" :src="finalPreviewUrl"
referrerpolicy="no-referrer"
v-on:load="updateDimensions($event)" v-on:error="previewFailed()"/>
</div>
<div v-else class="preview-fail box-section sect_yellow">
<p>The preview for this file failed to load. Please, double check that the URL you provided is correct.</p>
Note that some sites intentionally prevent images they host from being displayed on other sites. The file can still be uploaded despite that.
</div>
</div>
</template>
<script>
const thumbNone = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
export default {
props: {
classes: String,
data: {
validator: function(obj) {
return typeof obj.isVideo === "boolean" && typeof obj.url === "string";
}
},
},
data() {
return {
heigth: 0,
width: 0,
overDims: false,
failed: false,
}
},
computed: {
previewDimensions() {
if (this.width > 1 && this.height > 1)
return this.width + "×" + this.height;
return "";
},
finalPreviewUrl() {
return this.data.url === "" ? thumbNone : this.data.url;
},
},
watch: {
data: function() {
this.resetFilePreview();
}
},
methods: {
updateDimensions(e) {
const target = e.target;
this.height = target.naturalHeight || target.videoHeight;
this.width = target.naturalWidth || target.videoWidth;
this.overDims = (this.height > 15000 || this.width > 15000);
},
resetFilePreview() {
this.overDims = false;
this.width = 0;
this.height = 0;
this.failed = false;
},
previewFailed() {
this.failed = true;
},
}
};
</script>

View File

@ -8,10 +8,10 @@
<script>
export default {
props: ['value', 'index', 'last'],
props: ['modelValue', 'index', 'last'],
data() {
return {
backendValue: this.value
backendValue: this.modelValue
};
},
computed: {
@ -21,7 +21,7 @@
},
set: function (v) {
this.backendValue = v;
this.$emit('input', v);
this.$emit('update:modelValue', v);
}
}
},
@ -34,7 +34,7 @@
}
},
watch: {
value(v) {
modelValue(v) {
this.backendValue = v;
}
}

View File

@ -0,0 +1,11 @@
<template>
<a :class="'tag-type-' + tagType" :href="'/wiki_pages/show_or_new?title=' + name" target="_blank">
{{ name }}
</a>
</template>
<script>
export default {
props: ["tagType", "name"],
}
</script>

View File

@ -0,0 +1,45 @@
<template>
<div>
<div v-show="loading">Fetching tags...</div>
<div class="related-tags flex-wrap">
<div class="related-items" v-for="sTags, i in splitTags" :key="i">
<tag-preview-tag v-for="tag, $idx in sTags" :key="$idx" :tag="tag"></tag-preview-tag>
</div>
</div>
<div>
<a href="#" @click.prevent="close">Close Preview</a>
</div>
</div>
</template>
<script>
import tagPreviewTag from './tag_preview_tag.vue';
export default {
props: ['tags', 'loading'],
components: {
'tag-preview-tag': tagPreviewTag
},
methods: {
close: function () {
this.$emit('close');
}
},
computed: {
splitTags: function () {
var newTags = this.tags.concat([]);
newTags.sort(function (a, b) {
return a.a === b.a ? 0 : (a.a < b.a ? -1 : 1);
});
var chunkArray = function (arr, size) {
var chunks = [];
for (var i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
};
return chunkArray(newTags, 15);
}
}
}
</script>

View File

@ -0,0 +1,24 @@
<template>
<span v-if="tag.type === 'alias'" class="tag-preview tag-preview-alias">
<del><tag-link :name="tag.a" :tagType="tag.tagTypeA"></tag-link></del>
<tag-link :name="tag.b" :tagType="tag.tagTypeB"></tag-link>
</span>
<span v-else-if="tag.type === 'implication'" class="tag-preview tag-preview-implication">
<tag-link :name="tag.a" :tagType="tag.tagTypeA"></tag-link>
<tag-link :name="tag.b" :tagType="tag.tagTypeB"></tag-link>
</span>
<span v-else class="tag-preview">
<tag-link :name="tag.a" :tagType="tag.tagTypeA"></tag-link>
</span>
</template>
<script>
import tagLink from "./tag_link.vue";
export default {
components: {
"tag-link": tagLink,
},
props: ["tag"],
}
</script>

View File

@ -1,47 +1,18 @@
<template>
<div class="flex-grid-outer">
<div class="col box-section" style="flex: 2 0 0;">
<div class="box-section sect_red" v-show="filePreview.overDims">
One of the image dimensions is above the maximum allowed of 15,000px and will fail to upload.
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_file">File</label>
<div class="hint"><a href="/help/supported_filetypes">Supported Formats</a></div>
</div>
<div class="col2">
<div v-if="!disableFileUpload">
<div class="box-section sect_red" v-if="fileTooLarge">
The file you are trying to upload is too large. Maximum allowed is {{this.maxFileSize / (1024*1024) }} MiB.<br>
Check out <a href="/help/supported_filetypes">the Supported Formats</a> for more information.
</div>
<label>File:
<input type="file" ref="post_file" @change="updateFilePreview"
accept="image/png,image/apng,image/jpeg,image/gif,video/webm,.png,.apng,.jpg,.jpeg,.gif,.webm"
:disabled="disableFileUpload"/>
</label>
<button @click="clearFile" v-show="disableURLUpload">Clear</button>
</div>
<div v-if="!disableURLUpload">
<div class="box-section sect_red" v-if="badDirectURL">
The direct URL entered has the following problem: {{ directURLProblem }}<br>
You should review <a href="/wiki_pages/howto:sites_and_sources">the sourcing guide</a>.
</div>
<label>{{!disableFileUpload ? '(or) ' : '' }}URL:
<input type="text" size="50" v-model="uploadURL"
:disabled="disableURLUpload"/>
</label>
<div id="whitelist-warning" v-show="whitelist.visible"
:class="{'whitelist-warning-allowed': whitelist.allowed, 'whitelist-warning-disallowed': !whitelist.allowed}">
<span v-if="whitelist.allowed">Uploads from <b>{{whitelist.domain}}</b> are permitted.</span>
<span v-if="!whitelist.allowed">Uploads from <b>{{whitelist.domain}}</b> are not permitted.
<span v-if="whitelist.reason">Reason given: {{whitelist.reason}}</span>
(<a href='/upload_whitelists'>View whitelisted domains</a>)</span>
</div>
</div>
<file-input @uploadValueChanged="uploadValue = $event"
@previewChanged="previewData = $event"
@invalidUploadValueChanged="invalidUploadValue = $event"></file-input>
</div>
</div>
<file-preview classes="box-section in-editor below-upload" :preview="filePreview"></file-preview>
<file-preview classes="box-section in-editor below-upload" :data="previewData"></file-preview>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_sources">Sources</label>
@ -184,7 +155,7 @@
</div>
</div>
<div class="col2">
<file-preview classes="box-section in-editor" :preview="filePreview"></file-preview>
<file-preview classes="box-section in-editor" :data="previewData"></file-preview>
<div class="box-section sect_red" v-show="showErrors && notEnoughTags">
You must provide at least <b>{{4 - tagCount}}</b> more tags. Tags in other sections count
towards this total.
@ -278,25 +249,18 @@
</div>
</div>
<div id="preview-sidebar" class="col box-section" style="margin-left: 10px; padding: 10px;">
<file-preview classes="in-sidebar" :preview="filePreview" @load="updateFilePreviewDims" @error="filePreviewError"></file-preview>
<file-preview classes="in-sidebar" :data="previewData"></file-preview>
</div>
</div>
</template>
<script>
import Vue from 'vue';
import source from './uploader_source.vue';
import checkbox from './uploader_checkbox.vue';
import relatedTags from './uploader_related.vue';
import tagPreview from './uploader_tag_preview.vue';
import filePreview from './uploader_file_preview.vue';
const thumbURLs = [
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
];
const thumbs = {
none: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
};
import source from './source.vue';
import checkbox from './checkbox.vue';
import relatedTags from './related.vue';
import tagPreview from './tag_preview.vue';
import filePreview from './file_preview.vue';
import fileInput from './file_input.vue';
const sex_checks = [
{name: 'Male'},
@ -330,133 +294,15 @@
{name: 'Human'},
{name: 'Taur'}];
function updateFilePreviewDims(e) {
const target = e.target;
if (thumbURLs.filter(function (x) {
return target.src.indexOf(x) !== -1;
}).length !== 0)
return;
this.filePreview.height = target.naturalHeight || target.videoHeight;
this.filePreview.width = target.naturalWidth || target.videoWidth;
this.filePreview.overDims = (this.filePreview.height > 15000 || this.filePreview.width > 15000);
}
function filePreviewError() {
this.filePreview.failed = true;
}
function updatePreviewFile() {
const file = this.$refs['post_file'].files[0];
this.fileTooLarge = file.size > this.maxFileSize;
const objectUrl = URL.createObjectURL(file);
if (file.type.match('video/webm'))
this.setPreviewVideo(objectUrl);
else
this.setPreviewImage(objectUrl);
this.disableURLUpload = true;
}
function updatePreviewURL() {
const self = this;
if (this.uploadURL.length === 0 || (this.$refs['post_file'] && this.$refs['post_file'].files.length > 0)) {
this.disableFileUpload = false;
this.oldDomain = '';
self.clearWhitelistWarning();
return;
}
this.disableFileUpload = true;
const domain = $("<a>").prop("href", this.uploadURL).prop("hostname");
if (domain && domain !== this.oldDomain) {
$.getJSON("/upload_whitelists/is_allowed.json", {url: this.uploadURL}, function (data) {
if (data.domain) {
self.whitelistWarning(data.is_allowed, data.domain, data.reason);
if (!data.is_allowed)
self.setPreviewImage(thumbs.none);
}
});
} else if (!domain) {
self.clearWhitelistWarning();
}
this.oldDomain = domain;
if (this.uploadURL.match(/^(https?\:\/\/|www).*?\.(webm)$/))
this.setPreviewVideo(this.uploadURL);
else if (this.uploadURL.match(/^(https?\:\/\/|www).*?$/))
this.setPreviewImage(this.uploadURL);
else
this.setPreviewImage(thumbs.none);
}
function updateFilePreview() {
this.resetFilePreview();
if (this.$refs['post_file'] && this.$refs['post_file'].files[0])
updatePreviewFile.call(this);
else
updatePreviewURL.call(this);
}
function setPreviewImage(url) {
this.filePreview.isVideo = false;
this.filePreview.url = url;
}
function setPreviewVideo(url) {
this.filePreview.isVideo = true;
this.filePreview.url = url;
}
function resetFilePreview() {
// This might not be an objectURL, but revoking in those cases doesn't hurt
URL.revokeObjectURL(this.filePreview.url);
this.filePreview.isVideo = false;
this.filePreview.url = thumbs.none;
this.filePreview.overDims = false;
this.filePreview.width = 0;
this.filePreview.height = 0;
this.filePreview.failed = false;
this.fileTooLarge = false;
}
function directURLCheck(url) {
var patterns = [
{reason: 'Thumbnail URL', test: /[at]\.facdn\.net/gi},
{reason: 'Sample URL', test: /pximg\.net.*\/img-master\//gi},
{reason: 'Sample URL', test: /d3gz42uwgl1r1y\.cloudfront\.net\/.*\/\d+x\d+\./gi},
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/[\w\-_]+\.(jpg|png)(:large)?$/gi},
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/[\w\-_]+\?format=(jpg|png)(?!&name=orig)/gi},
{reason: 'Sample URL', test: /derpicdn\.net\/.*\/large\./gi},
{reason: 'Sample URL', test: /metapix\.net\/files\/(preview|screen)\//gi},
{reason: 'Sample URL', test: /sofurryfiles\.com\/std\/preview/gi}];
for (var i = 0; i < patterns.length; ++i) {
var pattern = patterns[i];
if (pattern.test.test(url))
return pattern.reason;
}
return '';
}
function clearFileUpload() {
if (!(this.$refs['post_file'] && this.$refs['post_file'].files[0]))
return;
this.$refs['post_file'].value = null;
this.disableURLUpload = this.disableFileUpload = false;
this.resetFilePreview();
this.updateFilePreview();
}
function tagSorter(a, b) {
return a[0] > b[0] ? 1 : -1;
}
function unloadWarning() {
if (this.allowNavigate)
if (this.allowNavigate || this.uploadValue === "") {
return;
const post_file = this.$refs['post_file'];
if ((post_file && post_file.files && post_file.files.length) || this.uploadURL) {
return true;
}
return true;
}
export default {
@ -465,7 +311,8 @@
'image-checkbox': checkbox,
'related-tags': relatedTags,
'tag-preview': tagPreview,
'file-preview': filePreview
'file-preview': filePreview,
'file-input': fileInput,
},
data() {
const allChecks = {};
@ -485,27 +332,15 @@
return {
safe: window.uploaderSettings.safeSite,
showErrors: false,
whitelist: {
visible: false,
allowed: false,
domain: ''
},
allowNavigate: false,
submitting: false,
disableFileUpload: false,
disableURLUpload: false,
filePreview: {
heigth: 0,
width: 0,
overDims: false,
url: thumbs.none,
previewData: {
url: '',
isVideo: false,
failed: false,
},
uploadURL: '',
oldDomain: '',
uploadValue: '',
invalidUploadValue: false,
noSource: false,
sources: [''],
@ -550,9 +385,6 @@
duplicateId: 0,
descrLimit: window.uploaderSettings.descrLimit,
maxFileSize: window.uploaderSettings.maxFileSize,
fileTooLarge: false,
};
},
mounted() {
@ -580,9 +412,6 @@
self.pushTag(trimTag, true);
}
};
fillField('uploadURL', 'upload_url');
if(params.has('upload_url'))
this.updateFilePreview();
fillField('parentID', 'parent');
fillField('description', 'description');
fillTags();
@ -596,29 +425,7 @@
if(this.allowUploadAsPending)
fillFieldBool("uploadAsPending", "upload_as_pending")
},
watch: {
uploadURL: function() {
this.updateFilePreview();
}
},
methods: {
updateFilePreview,
updateFilePreviewDims,
setPreviewImage,
setPreviewVideo,
resetFilePreview,
filePreviewError,
clearFile: clearFileUpload,
whitelistWarning(allowed, domain, reason) {
this.whitelist.allowed = allowed;
this.whitelist.domain = domain;
this.whitelist.reason = reason;
this.whitelist.visible = true;
},
clearWhitelistWarning() {
this.whitelist.visible = false;
this.whitelist.domain = '';
},
removeSource(i) {
this.sources.splice(i, 1);
},
@ -627,7 +434,7 @@
this.sources.push('');
},
setCheck(tag, value) {
Vue.set(this.checkboxes.selected, tag, value);
this.checkboxes.selected[tag] = value;
},
submit() {
this.showErrors = true;
@ -637,11 +444,10 @@
const self = this;
this.submitting = true;
const data = new FormData();
const post_file = this.$refs['post_file'];
if (post_file && post_file.files && post_file.files.length) {
data.append('upload[file]', this.$refs['post_file'].files[0]);
if (typeof this.uploadValue === "string") {
data.append('upload[direct_url]', this.uploadValue);
} else {
data.append('upload[direct_url]', this.uploadURL);
data.append('upload[file]', this.uploadValue);
}
data.append('upload[tag_string]', this.tags);
data.append('upload[rating]', this.rating);
@ -792,12 +598,6 @@
tagsArray() {
return this.tags.toLowerCase().split(' ');
},
directURLProblem: function () {
return directURLCheck(this.uploadURL);
},
badDirectURL: function () {
return !!this.directURLProblem;
},
sourceWarning: function () {
const validSourceCount = this.sources.filter(function (i) {
return i.length > 0;
@ -816,8 +616,8 @@
return !this.rating;
},
preventUpload: function () {
return this.sourceWarning || this.badDirectURL || this.notEnoughTags
|| this.invalidRating || this.fileTooLarge;
return this.sourceWarning || this.notEnoughTags
|| this.invalidRating || this.invalidUploadValue;
},
duplicatePath: function () {
return `/posts/${this.duplicateId}`;

View File

@ -1,34 +0,0 @@
<template>
<div class="upload_preview_container" :class="classes">
<div v-if="!preview.failed">
<div class="upload_preview_dims">{{ previewDimensions }}</div>
<img class="upload_preview_img" :src="preview.url"
referrerpolicy="no-referrer"
v-if="!preview.isVideo"
v-on:load="$emit('load', $event)" v-on:error="$emit('error')"/>
<video class="upload_preview_img" controls :src="preview.url"
v-on:loadeddata="$emit('load', $event)" v-on:error="$emit('error')"
v-if="preview.isVideo"></video>
</div>
<div class="preview-fail box-section sect_yellow" v-if="preview.failed">
<p>The preview for this file failed to load. Please, double check that the URL you provided is correct.</p>
Note that some sites intentionally prevent images they host from being displayed on other sites. The file can still be uploaded despite that.
</div>
</div>
</template>
<script>
export default {
props: {
classes: String,
preview: Object
},
computed: {
previewDimensions() {
if (this.preview.width && this.preview.height)
return this.preview.width + '×' + this.preview.height;
return '';
},
}
};
</script>

View File

@ -1,74 +0,0 @@
<template>
<div>
<div v-show="loading">Fetching tags...</div>
<div class="related-tags flex-wrap">
<div class="related-items" v-for="sTags, i in splitTags" :key="i">
<tag-preview v-for="tag, $idx in sTags" :key="$idx" :tag="tag"></tag-preview>
</div>
</div>
<div>
<a href="#" @click.prevent="close">Close Preview</a>
</div>
</div>
</template>
<script>
import Vue from 'vue';
const tagPreviewTag = Vue.extend({
functional: true,
props: ['tag'],
render: function (h, ctx) {
function create_tag_link(name, tagType) {
return h('a', {
staticClass: 'tag-type-' + tagType,
attrs: { href: "/wiki_pages/show_or_new?title=" + name, target: "_blank" }
}, name);
}
var tag = ctx.props.tag;
switch (tag.type) {
default:
case 'tag':
return h('span', {staticClass: 'tag-preview'}, [create_tag_link(tag.a, tag.tagTypeA)]);
case 'alias':
return h('span', {staticClass: 'tag-preview tag-preview-alias'}, [
h('del', undefined, [
create_tag_link(tag.a, tag.tagTypeA)
]), ' → ', create_tag_link(tag.b, tag.tagTypeB)
]);
case 'implication':
return h('span', {staticClass: 'tag-preview tag-preview-implication'}, [
create_tag_link(tag.a, tag.tagTypeA), ' ⇐ ', create_tag_link(tag.b, tag.tagTypeB)
]);
}
}
});
export default {
props: ['tags', 'loading'],
components: {
'tag-preview': tagPreviewTag
},
methods: {
close: function () {
this.$emit('close');
}
},
computed: {
splitTags: function () {
var newTags = this.tags.concat([]);
newTags.sort(function (a, b) {
return a.a === b.a ? 0 : (a.a < b.a ? -1 : 1);
});
var chunkArray = function (arr, size) {
var chunks = [];
for (var i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
};
return chunkArray(newTags, 15);
}
}
}
</script>

View File

@ -65,6 +65,7 @@
@import "specific/sessions.scss";
@import "specific/site_map.scss";
@import "specific/staff_notes.scss";
@import "specific/stats.scss";
@import "specific/tags.scss";
@import "specific/takedowns.scss";
@import "specific/terms_of_service.scss";

View File

@ -23,4 +23,8 @@ div#c-post-replacements {
color: themed("color-text-muted");
}
}
.upload_preview_img {
max-width: 100%;
}
}

View File

@ -42,7 +42,9 @@ div.related-tags {
}
.tag-active {
background: rgb(0, 111, 250);
@include themable {
background: themed("color-active-tag");
}
color: white;
}

View File

@ -0,0 +1,65 @@
.stats-column {
width:400px;
display:inline-block;
vertical-align:top;
}
.stats-chart {
display:inline-block;
width:300px;
height:300px;
}
.stats-chart-container {
white-space:nowrap;
}
.stats-pct {
text-align:right;
}
#stats-column-1 table {
margin-bottom:60px;
}
#stats-column-2 table {
margin-bottom:19px;
}
#stats-column-3 table {
margin-bottom:5px;
}
.stats-column table:last-child {
margin-bottom:10px !important;
}
.stats-column table {
margin-bottom:2em;
padding:3px;
border-spacing:0;
}
.stats-rounded {
@include themable {
background-color: themed("color-section");
box-shadow:2px 2px 5px darken( themed("color-section"), 15%);
}
border-radius:8px;
overflow: hidden;
}
.stats-rounded tr:nth-child(even) {
@include themable {
background-color: lighten( themed("color-section"), 3%);
}
}
.stats-rounded tr:nth-child(even) td {
@include themable {
border-top:1px solid lighten( themed("color-section"), 10%);
border-bottom:1px solid lighten( themed("color-section"), 10%);
}
}
.stats-rounded tr:last-child td{ border-bottom:none !important; }
.stats-rounded td { padding:2px 10px; vertical-align:top; }
.stats-rounded th { font-weight:bold; text-align:left; vertical-align:top; padding:0.2em 0.5em; white-space:nowrap; }

View File

@ -33,6 +33,8 @@ $theme_bloodlust: (
"color-score-positive": #3e9e49,
"color-score-negative": #e45f5f,
"color-active-tag": #222222,
// Detail Colors
"color-detail-quote": #67717b,
"color-detail-code": #ffe380,

View File

@ -41,6 +41,8 @@ $theme_hexagon: (
"color-score-positive": #3e9e49,
"color-score-negative": #e45f5f,
"color-active-tag": #006ffa,
// Detail Colors
"color-detail-quote": #b4c7d9,
"color-detail-code": #ffe380,

View File

@ -21,6 +21,8 @@ $theme_hotdog: (
"color-score-positive": #3e9e49,
"color-score-negative": #e45f5f,
"color-active-tag": #ff0000,
// Detail Colors
"color-detail-quote": #67717b,
"color-detail-code": #ffe380,

View File

@ -22,6 +22,8 @@ $theme_pony: (
"color-score-positive": #3e9e49,
"color-score-negative": #e45f5f,
"color-active-tag": #6f36da,
// Detail Colors
"color-detail-quote": #67717b,
"color-detail-code": #ffe380,

View File

@ -22,6 +22,8 @@ $theme_serpent: (
"color-score-positive": #3e9e49,
"color-score-negative": #e45f5f,
"color-active-tag": #44a544,
// Detail Colors
"color-detail-quote": #67717b,
"color-detail-code": #ffe380,

View File

@ -3,106 +3,36 @@ module DanbooruImageResizer
# Taken from ArgyllCMS 2.0.0 (see also: https://ninedegreesbelow.com/photography/srgb-profile-comparison.html)
SRGB_PROFILE = "#{Rails.root}/config/sRGB.icm"
# http://jcupitt.github.io/libvips/API/current/libvips-resample.html#vips-thumbnail
# https://www.libvips.org/API/current/libvips-resample.html#vips-thumbnail
THUMBNAIL_OPTIONS = { size: :down, linear: false, no_rotate: true, export_profile: SRGB_PROFILE, import_profile: SRGB_PROFILE }
THUMBNAIL_OPTIONS_NO_ICC = { size: :down, linear: false, no_rotate: true, export_profile: SRGB_PROFILE }
# http://jcupitt.github.io/libvips/API/current/VipsForeignSave.html#vips-jpegsave
# https://www.libvips.org/API/current/VipsForeignSave.html#vips-jpegsave
JPEG_OPTIONS = { background: 0, strip: true, interlace: true, optimize_coding: true }
CROP_OPTIONS = { linear: false, no_rotate: true, export_profile: SRGB_PROFILE, import_profile: SRGB_PROFILE, crop: :attention }
CROP_OPTIONS_NO_ICC = { linear: false, no_rotate: true, export_profile: SRGB_PROFILE, crop: :attention }
# XXX libvips-8.4 on Debian doesn't support the `Vips::Image.thumbnail` method.
# On 8.4 we have to shell out to vipsthumbnail instead. Remove when Debian supports 8.5.
def resize(file, width, height, quality = 90)
if Vips.at_least_libvips?(8, 5)
resize_ruby(file, width, height, quality)
else
resize_shell(file, width, height, quality)
end
end
def crop(file, width, height, quality = 90)
if Vips.at_least_libvips?(8, 5)
crop_ruby(file, width, height, quality)
else
crop_shell(file, width, height, quality)
end
end
# https://github.com/jcupitt/libvips/wiki/HOWTO----Image-shrinking
# http://jcupitt.github.io/libvips/API/current/Using-vipsthumbnail.md.html
def resize_ruby(file, width, height, resize_quality)
def resize(file, width, height, resize_quality = 90)
output_file = Tempfile.new
begin
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **THUMBNAIL_OPTIONS)
rescue Vips::Error => e
raise e unless e.message =~ /icc_transform/i
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **THUMBNAIL_OPTIONS_NO_ICC)
end
resized_image = thumbnail(file, width, height, THUMBNAIL_OPTIONS)
resized_image.jpegsave(output_file.path, Q: resize_quality, **JPEG_OPTIONS)
output_file
end
def crop_ruby(file, width, height, resize_quality)
def crop(file, width, height, resize_quality = 90)
return nil unless Danbooru.config.enable_image_cropping?
output_file = Tempfile.new
begin
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **CROP_OPTIONS)
rescue Vips::Error => e
raise e unless e.message =~ /icc_transform/i
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **CROP_OPTIONS_NO_ICC)
end
resized_image = thumbnail(file, width, height, CROP_OPTIONS)
resized_image.jpegsave(output_file.path, Q: resize_quality, **JPEG_OPTIONS)
output_file
end
def resize_shell(file, width, height, quality)
output_file = Tempfile.new(["resize", ".jpg"])
# --size=WxH will upscale if the image is smaller than the target size.
# Fix the target size so that it's not bigger than the image.
image = Vips::Image.new_from_file(file.path)
target_width = [image.width, width].min
target_height = [image.height, height].min
arguments = [
file.path,
"--eprofile=#{SRGB_PROFILE}",
"--iprofile=#{SRGB_PROFILE}",
"--size=#{target_width}x#{target_height}",
"--format=#{output_file.path}[Q=#{quality},background=0,strip,interlace,optimize_coding]"
]
_, status = Open3.capture2e("vipsthumbnail", *arguments)
raise RuntimeError, "vipsthumbnail failed (exit status: #{status})" if status != 0
output_file
end
def crop_shell(file, width, height, quality)
return nil unless Danbooru.config.enable_image_cropping?
output_file = Tempfile.new(["crop", ".jpg"])
# --size=WxH will upscale if the image is smaller than the target size.
# Fix the target size so that it's not bigger than the image.
# image = Vips::Image.new_from_file(file.path)
arguments = [
file.path,
"--eprofile=#{SRGB_PROFILE}",
"--iprofile=#{SRGB_PROFILE}",
"--smartcrop=attention",
"--size=#{width}x#{height}",
"--format=#{output_file.path}[Q=#{quality},background=0,strip,interlace,optimize_coding]"
]
_, status = Open3.capture2e("vipsthumbnail", *arguments)
raise RuntimeError, "vipsthumbnail failed (exit status: #{status})" if status != 0
output_file
# https://github.com/libvips/libvips/wiki/HOWTO----Image-shrinking
# https://www.libvips.org/API/current/Using-vipsthumbnail.md.html
def thumbnail(file, width, height, options)
Vips::Image.thumbnail(file.path, width, height: height, **options)
rescue Vips::Error => e
raise e unless e.message =~ /icc_transform/i
Vips::Image.thumbnail(file.path, width, height: height, **options.except(:import_profile))
end
end

View File

@ -1,14 +1,14 @@
module DangerZone
def self.uploads_disabled?
redis_client.get("disable_uploads") == "y"
def self.uploads_disabled?(user)
user.level < min_upload_level
end
def self.disable_uploads
redis_client.set("disable_uploads", "y")
def self.min_upload_level
(redis_client.get("min_upload_level") || User::Levels::MEMBER).to_i
end
def self.enable_uploads
redis_client.set("disable_uploads", "n")
def self.min_upload_level=(min_upload_level)
redis_client.set("min_upload_level", min_upload_level)
end
def self.redis_client

View File

@ -69,8 +69,8 @@ module FileMethods
[video.width, video.height]
elsif is_image?
image_size = ImageSpec.new(file_path)
[image_size.width, image_size.height]
image = Vips::Image.new_from_file(file_path)
[image.width, image.height]
else
[0, 0]

View File

@ -1,11 +1,4 @@
class TagAliasRequest
include ActiveModel::Validations
attr_reader :antecedent_name, :consequent_name, :reason, :tag_alias, :forum_topic, :skip_forum
validate :validate_tag_alias
validate :validate_forum_topic
class TagAliasRequest < TagRelationshipRequest
def self.topic_title(antecedent_name, consequent_name)
"Tag alias: #{antecedent_name} -> #{consequent_name}"
end
@ -18,69 +11,7 @@ class TagAliasRequest
"create alias [[#{antecedent_name}]] -> [[#{consequent_name}]]"
end
def initialize(attributes)
@antecedent_name = attributes[:antecedent_name].strip.tr(" ", "_")
@consequent_name = attributes[:consequent_name].strip.tr(" ", "_")
@reason = attributes[:reason]
self.skip_forum = attributes[:skip_forum]
end
def create
return false if invalid?
TagAlias.transaction do
@tag_alias = build_tag_alias
@tag_alias.save
unless skip_forum
@forum_topic = build_forum_topic(@tag_alias.id)
@forum_topic.save
@tag_alias.forum_topic_id = @forum_topic.id
@tag_alias.forum_post_id = @forum_topic.posts.first.id
@tag_alias.save
end
end
end
def build_tag_alias
x = TagAlias.new(
:antecedent_name => antecedent_name,
:consequent_name => consequent_name
)
x.status = "pending"
x
end
def build_forum_topic(tag_alias_id)
ForumTopic.new(
:title => TagAliasRequest.topic_title(antecedent_name, consequent_name),
:original_post_attributes => {
:body => TagAliasRequest.command_string(antecedent_name, consequent_name, tag_alias_id) + "\n\nReason: #{reason}"
},
:category_id => Danbooru.config.alias_implication_forum_category
)
end
def validate_tag_alias
ta = @tag_alias || build_tag_alias
if ta.invalid?
self.errors.add(:base, ta.errors.full_messages.join("; "))
return false
end
end
def validate_forum_topic
return if skip_forum
ft = @forum_topic || build_forum_topic(nil)
if ft.invalid?
self.errors.add(:base, ft.errors.full_messages.join("; "))
return false
end
end
def skip_forum=(v)
@skip_forum = v.to_s.truthy?
def tag_relationship_class
TagAlias
end
end

View File

@ -1,11 +1,4 @@
class TagImplicationRequest
include ActiveModel::Validations
attr_reader :antecedent_name, :consequent_name, :reason, :tag_implication, :forum_topic, :skip_forum
validate :validate_tag_implication
validate :validate_forum_topic
class TagImplicationRequest < TagRelationshipRequest
def self.topic_title(antecedent_name, consequent_name)
"Tag implication: #{antecedent_name} -> #{consequent_name}"
end
@ -18,70 +11,7 @@ class TagImplicationRequest
"create implication [[#{antecedent_name}]] -> [[#{consequent_name}]]"
end
def initialize(attributes)
@antecedent_name = attributes[:antecedent_name].strip.tr(" ", "_")
@consequent_name = attributes[:consequent_name].strip.tr(" ", "_")
@reason = attributes[:reason]
self.skip_forum = attributes[:skip_forum]
end
def create
return false if invalid?
TagImplication.transaction do
@tag_implication = build_tag_implication
@tag_implication.save
unless skip_forum
@forum_topic = build_forum_topic(@tag_implication.id)
@forum_topic.save
@tag_implication.forum_topic_id = @forum_topic.id
@tag_implication.forum_post_id = @forum_topic.posts.first.id
@tag_implication.save
end
end
end
def build_tag_implication
x = TagImplication.new(
:antecedent_name => antecedent_name,
:consequent_name => consequent_name
)
x.status = "pending"
x
end
def build_forum_topic(tag_implication_id)
ForumTopic.new(
:title => TagImplicationRequest.topic_title(antecedent_name, consequent_name),
:original_post_attributes => {
:body => TagImplicationRequest.command_string(antecedent_name, consequent_name, tag_implication_id) + "\n\nReason: #{reason}"
},
:category_id => Danbooru.config.alias_implication_forum_category
)
end
def validate_tag_implication
ti = @tag_implication || build_tag_implication
if ti.invalid?
self.errors.add(:base, ti.errors.full_messages.join("; "))
return false
end
end
def validate_forum_topic
return if skip_forum
ft = @forum_topic || build_forum_topic(nil)
if ft.invalid?
self.errors.add(:base, ft.errors.full_messages.join("; "))
return false
end
end
def skip_forum=(v)
@skip_forum = v.to_s.truthy?
def tag_relationship_class
TagImplication
end
end

View File

@ -0,0 +1,73 @@
class TagRelationshipRequest
include ActiveModel::Validations
attr_reader :antecedent_name, :consequent_name, :tag_relationship, :reason, :forum_topic, :skip_forum
validate :validate_tag_relationship
validate :validate_forum_topic
validates :reason, length: { minimum: 5 }, unless: :skip_forum
def initialize(attributes)
@antecedent_name = attributes[:antecedent_name].strip.tr(" ", "_")
@consequent_name = attributes[:consequent_name].strip.tr(" ", "_")
@reason = attributes[:reason]
self.skip_forum = attributes[:skip_forum]
end
def create
return false if invalid?
tag_relationship_class.transaction do
@tag_relationship = build_tag_relationship
@tag_relationship.save
unless skip_forum
@forum_topic = build_forum_topic(@tag_relationship.id)
@forum_topic.save
@tag_relationship.forum_topic_id = @forum_topic.id
@tag_relationship.forum_post_id = @forum_topic.posts.first.id
@tag_relationship.save
end
end
end
def build_tag_relationship
x = tag_relationship_class.new(
antecedent_name: antecedent_name,
consequent_name: consequent_name
)
x.status = "pending"
x
end
def build_forum_topic(tag_relationship_id)
ForumTopic.new(
title: self.class.topic_title(antecedent_name, consequent_name),
original_post_attributes: {
body: self.class.command_string(antecedent_name, consequent_name, tag_relationship_id) + "\n\nReason: #{reason}"
},
category_id: Danbooru.config.alias_implication_forum_category
)
end
def validate_tag_relationship
tag_relationship = @tag_relationship || build_tag_relationship
if tag_relationship.invalid?
errors.add(:base, tag_relationship.errors.full_messages.join("; "))
end
end
def validate_forum_topic
return if skip_forum
ft = @forum_topic || build_forum_topic(nil)
if ft.invalid?
errors.add(:base, ft.errors.full_messages.join("; "))
end
end
def skip_forum=(v)
@skip_forum = v.to_s.truthy?
end
end

View File

@ -82,6 +82,7 @@ class UploadService
post.image_width = upload.image_width
post.image_height = upload.image_height
post.file_size = upload.file_size
post.duration = upload.video_duration(upload.file.path)
post.source = "#{replacement.source}\n" + post.source
post.tag_string = upload.tag_string
# Reset ownership information on post.

View File

@ -14,7 +14,8 @@ class BulkUpdateRequest < ApplicationRecord
validate :forum_topic_id_not_invalid
validate :validate_script, on: :create
validate :check_validate_script, on: :update
before_validation :initialize_attributes, :on => :create
validates :reason, length: { minimum: 5 }, on: :create, unless: :skip_forum
before_validation :initialize_attributes, on: :create
before_validation :normalize_text
after_create :create_forum_topic

View File

@ -1,8 +1,11 @@
class EmailBlacklist < ApplicationRecord
UNVERIFY_COUNT_TRESHOLD = 50
belongs_to_creator
validates :domain, uniqueness: { case_sensitive: false, message: 'already exists' }
after_save :invalidate_cache
after_create :invalidate_cache
after_create :unverify_accounts
after_destroy :invalidate_cache
def self.is_banned?(email)
@ -56,4 +59,12 @@ class EmailBlacklist < ApplicationRecord
def invalidate_cache
Cache.delete('banned_emails')
end
def unverify_accounts
# Only unverify exact domain matches
matching_users = User.search(email_matches: "*@#{domain}")
return if matching_users.count > UNVERIFY_COUNT_TRESHOLD
matching_users.each(&:mark_unverified!)
end
end

View File

@ -46,7 +46,7 @@ class ModAction < ApplicationRecord
:report_reason_update,
:set_update,
:set_delete,
:set_mark_private,
:set_change_visibility,
:tag_alias_create,
:tag_alias_update,
:tag_implication_create,

View File

@ -329,10 +329,12 @@ class Post < ApplicationRecord
def approve!(approver = CurrentUser.user, force: false)
raise ApprovalError.new("Post already approved.") if self.approver != nil && !force
PostEvent.add(id, CurrentUser.user, :approved)
approv = approvals.create(user: approver)
flags.each(&:resolve!)
if flags.unresolved.any?
unflag!
end
PostEvent.add(id, CurrentUser.user, :approved)
update(approver: approver, is_flagged: false, is_pending: false, is_deleted: false)
approv
end
@ -762,19 +764,19 @@ class Post < ApplicationRecord
when /^set:(\d+)$/i
set = PostSet.find_by_id($1.to_i)
set.add!(self) if set && set.can_edit?(CurrentUser.user)
set.add!(self) if set&.can_edit_posts?(CurrentUser.user)
when /^-set:(\d+)$/i
set = PostSet.find_by_id($1.to_i)
set.remove!(self) if set && set.can_edit?(CurrentUser.user)
set.remove!(self) if set&.can_edit_posts?(CurrentUser.user)
when /^set:(.+)$/i
set = PostSet.find_by_shortname($1)
set.add!(self) if set && set.can_edit?(CurrentUser.user)
set.add!(self) if set&.can_edit_posts?(CurrentUser.user)
when /^-set:(.+)$/i
set = PostSet.find_by_shortname($1)
set.remove!(self) if set && set.can_edit?(CurrentUser.user)
set.remove!(self) if set&.can_edit_posts?(CurrentUser.user)
when /^child:none$/i
children.each do |post|

View File

@ -66,7 +66,7 @@ class PostSet < ApplicationRecord
end
def saved_change_to_watched_attributes?
saved_change_to_name? || saved_change_to_shortname? || saved_change_to_description?
saved_change_to_name? || saved_change_to_shortname? || saved_change_to_description? || saved_change_to_transfer_on_delete?
end
module ValidationMethods
@ -146,11 +146,15 @@ class PostSet < ApplicationRecord
module AccessMethods
def can_view?(user)
is_public || creator_id == user.id || user.is_admin?
is_public || is_owner?(user) || user.is_admin?
end
def can_edit?(user)
is_owner?(user) || is_maintainer?(user)
def can_edit_settings?(user)
is_owner?(user) || user.is_admin?
end
def can_edit_posts?(user)
can_edit_settings?(user) || (is_maintainer?(user) && is_public)
end
def is_maintainer?(user)
@ -168,7 +172,7 @@ class PostSet < ApplicationRecord
def is_owner?(user)
return false if user.is_blocked?
creator_id == user.id || user.is_admin?
creator_id == user.id
end
end

View File

@ -203,7 +203,7 @@ class TagAlias < TagRelationship
end
rescue Exception => e
Rails.logger.error("[TA] #{e.message}\n#{e.backtrace}")
if tries < 5
if tries < 5 && !Rails.env.test?
tries += 1
sleep 2 ** tries
retry

View File

@ -124,7 +124,7 @@ class TagImplication < TagRelationship
forum_updater.update(approval_message(approver), "APPROVED") if update_topic
end
rescue Exception => e
if tries < 5
if tries < 5 && !Rails.env.test?
tries += 1
sleep 2 ** tries
retry

View File

@ -824,7 +824,7 @@ class User < ApplicationRecord
# q = q.attribute_matches(:note_update_count, params[:note_update_count])
# q = q.attribute_matches(:favorite_count, params[:favorite_count])
if params[:email_matches].present? && CurrentUser.is_admin?
if params[:email_matches].present?
q = q.where_ilike(:email, params[:email_matches])
end

View File

@ -2,12 +2,10 @@
<div id="a-index">
<h1>Danger Zone</h1>
<h2>Uploads</h2>
<% if DangerZone.uploads_disabled? %>
Uploads are currently <b>disabled</b>.
<%= link_to "Re-enable Uploads", enable_uploads_admin_danger_zone_index_path, method: :put, data: { confirm: "Are you sure?" } %>
<% else %>
Uploads are currently <b>enabled</b>.
<%= link_to "Prevent Uploads", disable_uploads_admin_danger_zone_index_path, method: :put, data: { confirm: "Are you sure?" } %>
Uploads are currently permitted for <b><%= User.level_string(DangerZone.min_upload_level) %></b> and up.
<%= simple_form_for(:uploading_limits, url: uploading_limits_admin_danger_zone_index_path, method: :put) do |f| %>
<%= f.input :min_level, collection: User.level_hash.select {|k,v| v >= User::Levels::MEMBER }.to_a, selected: DangerZone.min_upload_level %>
<%= f.button :submit, value: "Submit" %>
<% end %>
</div>
</div>

View File

@ -3,31 +3,26 @@
<%= format_text(WikiPage.titled(Danbooru.config.replacement_notice_wiki_page).first.try(&:body)) %>
</div>
<%= simple_form_for(post_replacement, url: post_replacements_path(post_id: post_replacement.post_id), method: :post) do |f| %>
<%= simple_form_for(post_replacement, url: post_replacements_path(post_id: post_replacement.post_id), html: { multipart: true }, method: :post) do |f| %>
<div>
<% if post.visible? %>
<%= PostPresenter.preview(post, tags: "status:any", no_blacklist: true) %>
<% end %>
<div><%= "#{post.image_width}x#{post.image_height} (#{post.file_size.to_s(:human_size, precision: 5)})" %></div>
</div>
<%= f.input :replacement_file, label: "File", as: :file %>
<%= f.input :replacement_url, label: "Replacement URL", hint: "The source URL to download the replacement from.", as: :string, input_html: {size: 40} %>
<div class="box-section sect_red" id="bad_upload_url" style="display: none;">
The direct URL entered has the following problem: <span id="bad_upload_url_reason"></span><br>
You should review <a href="/wiki/show/howto:sites_and_sources">the sourcing guide</a>.
</div>
<div id="whitelist-warning" style="display: none;"
:class="{'whitelist-warning-allowed': whitelist.allowed, 'whitelist-warning-disallowed': !whitelist.allowed}">
<span>Uploads from <b id="whitelist-warning-domain"></b> are <span id="whitelist-warning-not">not </span>permitted.</span>
</div>
<br>
<div id="replacement-uploader"></div>
<br>
<%= f.input :source, label: "Additional Source", hint: "(Optional) The submission page the replacement file came from.", input_html: {size: 40} %>
<%= f.input :reason, label: "Reason", hint: "Tell us why this file should replace the original.", as: :string, input_html: {size: 40} %>
<%= f.submit "Submit" %>
<% end %>
<div id="replacement-upload-preview">
<div id="replacement_preview_dims"></div>
<img id="replacement_preview_img" src="" style="max-width: 100%;"/>
</div>
<div id="replacement-preview"></div>
</div>
<%= javascript_tag nonce: true do -%>
var uploaderSettings = {
maxFileSize: <%= Danbooru.config.max_file_size %>,
};
Danbooru.Replacer.init();
<% end -%>

View File

@ -12,15 +12,15 @@
<%= subnav_link_to "Posts", posts_path(tags: "set:#{@post_set.shortname}") %>
<%= subnav_link_to "Maintainers", maintainers_post_set_path(@post_set) %>
<% if @post_set.is_owner?(CurrentUser.user) || CurrentUser.is_admin? %>
<% if @post_set.can_edit_settings?(CurrentUser.user) %>
<%= subnav_link_to "Edit", edit_post_set_path(@post_set) %>
<% end %>
<% if @post_set.is_owner?(CurrentUser.user) || @post_set.is_maintainer?(CurrentUser.user) %>
<% if @post_set.can_edit_posts?(CurrentUser.user) %>
<%= subnav_link_to "Edit Posts", post_list_post_set_path(@post_set) %>
<% end %>
<% if @post_set.is_owner?(CurrentUser.user) || CurrentUser.is_admin? %>
<% if @post_set.can_edit_settings?(CurrentUser.user) %>
<%= subnav_link_to "Delete", post_set_path(@post_set), method: 'delete', data: {confirm: 'Are you sure you want to delete this set?'} %>
<% end %>
<%= subnav_link_to "Report", new_ticket_path(type: 'set', disp_id: @post_set.id) %>

View File

@ -2,7 +2,7 @@
<div id="c-edit">
<h2>Editing <span class="set-name"><%= @post_set.name %></span></h2>
<% if @can_edit %>
<% if @post_set.can_edit_settings?(CurrentUser) %>
<div class='section'>
<%= simple_form_for(@post_set) do |f| %>
<%= f.input :name, as: :string %>
@ -21,13 +21,11 @@
<%= f.button :submit, "Update" %>
<% end %>
</div>
<% end %>
<% if @post_set.is_owner?(CurrentUser) %>
<%= link_to "&raquo; Click here to edit maintainers.".html_safe, maintainers_post_set_path(@post_set) %><br/>
<% end %>
<% if @post_set.is_owner?(CurrentUser) || @post_set.is_maintainer?(CurrentUser) %>
<% if @post_set.can_edit_posts?(CurrentUser) %>
<%= link_to "&raquo; Click here to add/remove posts.".html_safe, post_list_post_set_path(@post_set) %><br/>
<% end %>
</div>

View File

@ -5,7 +5,7 @@
<p>Maintainers are users who are assigned to a set and have the ability to add or remove posts from it. They cannot
edit any other details of the set.</p>
<% if @post_set.is_owner?(CurrentUser) && @post_set.is_public %>
<% if @post_set.can_edit_settings?(CurrentUser) && @post_set.is_public %>
<h3 style="margin-top:15px;">Add new Maintainer</h3>
<div class='section' style="width:380px;">
<%= form_tag(post_set_maintainers_path) do %>
@ -33,10 +33,10 @@
<td><%= m.status.capitalize %></td>
<td><%= compact_time m.updated_at %></td>
<td>
<% if @post_set.is_owner?(CurrentUser) && m.status == "approved" %>
<% if @post_set.can_edit_settings?(CurrentUser) && m.status == "approved" %>
<%= link_to "Remove", post_set_maintainer_path(m), method: :delete, data: {confirm: "Are you sure you want to remove this maintainer?"} %>
<% end %>
<% if @post_set.is_owner?(CurrentUser) && m.status == "pending" %>
<% if @post_set.can_edit_settings?(CurrentUser) && m.status == "pending" %>
<%= link_to "Remove", post_set_maintainer_path(m), method: :delete, data: {confirm: "Are you sure you want to remove this pending invite?"} %>
<% end %>
</td>

View File

@ -37,7 +37,7 @@
<div class="set-empty section">
<p>This set has no posts in it.</p>
<% if @post_set.is_owner?(CurrentUser.user) || @post_set.is_maintainer?(CurrentUser.user) %>
<% if @post_set.can_edit_posts?(CurrentUser.user) %>
To start adding posts to this set:
<ul>
<li>On a post's page, click <strong>Add to Set</strong> under <strong>Options</strong> in the sidebar,
@ -49,7 +49,7 @@
</ul>
<% end %>
</div>
<% elsif @post_set.is_owner?(CurrentUser.user) || @post_set.is_maintainer?(CurrentUser.user) %>
<% elsif @post_set.can_edit_posts?(CurrentUser.user) %>
<div class="section">
To add posts to this set:
<ul>

View File

@ -24,13 +24,11 @@
</span>
</div>
<div id="tag-string-editor">
<%= f.text_area :tag_string, :size => "60x5", :spellcheck => false, :"data-autocomplete" => "tag-edit", :"data-shortcut" => "e", :value => post.presenter.split_tag_list_text + " " %>
</div>
<div id="tag-string-editor"></div>
<div>
<%= f.label :locked_tags, "Locked Tags" %>
<%= f.text_area :locked_tags, :size => "60x2", :spellcheck => false, :"data-autocomplete" => "tag-edit", :"data-shortcut" => "e", :value => (post.locked_tags || ""), disabled: !CurrentUser.is_admin? %>
<%= f.text_area :locked_tags, :size => "60x2", :spellcheck => false, :"data-autocomplete" => "tag-edit", :value => (post.locked_tags || ""), disabled: !CurrentUser.is_admin? %>
</div>
</div>

View File

@ -1,69 +1,3 @@
<style>
.stats-column {
width:400px;
display:inline-block;
vertical-align:top;
}
.stats-chart {
display:inline-block;
width:300px;
height:300px;
}
.stats-chart-container {
white-space:nowrap;
}
.stats-pct {
text-align:right;
}
#stats-column-1 table {
margin-bottom:60px;
}
#stats-column-2 table {
margin-bottom:19px;
}
#stats-column-3 table {
margin-bottom:5px;
}
.stats-column table:last-child {
margin-bottom:10px !important;
}
table {
margin-bottom:2em;
padding:3px;
border-spacing:0;
}
td { padding:1px 4px; vertical-align:top; }
th { font-weight:bold; text-align:left; vertical-align:top; padding:0.2em 0.5em; white-space:nowrap; }
tr.selected { background-color:#288233 !important; /* green */ }
.rounded {
background-color:#203f6c;
border-radius:8px;
box-shadow:2px 2px 5px #07162D;
}
.rounded th { border-bottom:2px solid #6388c3; padding: 2px 10px; }
.rounded tr:nth-child(even) { background-color:#204274; }
.rounded tr:nth-child(even) td { border-top:1px solid #255193; border-bottom:1px solid #255193; }
.rounded tr:last-child td{ border-bottom:0; }
.rounded td { padding:2px 10px; }
.rounded-even { background-color:#204274; }
.rounded-even td { border-top:1px solid #255193; border-bottom:1px solid #255193; }
.rounded-odd { background-color:transparent; }
.rounded-odd td { border-top:none; border-bottom:none; }
</style>
<%= javascript_tag nonce: true do -%>
function pct(current,total) {
return Math.round((current/total)*100) + "%"
@ -185,7 +119,7 @@
<p>Refreshed once a day.</p>
<div class='stats-column' id='stats-column-1'>
<h2>Site</h2>
<table class='rounded'>
<table class='stats-rounded'>
<tr>
<td style='width:250px;'>Started</td>
<td style='width:105px;'><%= @stats['started'] %></td>
@ -213,7 +147,7 @@
</table>
<h2>Posts</h2>
<table class='rounded'>
<table class='stats-rounded'>
<tr>
<td style='width:250px;'>Total posts</td>
<td style='width:50px;'><%= del(@stats['total_posts']) %></td>
@ -283,7 +217,7 @@
<div class='stats-column' id='stats-column-2'>
<h2>Image files</h2>
<table class='rounded'>
<table class='stats-rounded'>
<tr>
<td style='width:250px;'>Total existing posts</td>
<td style='width:50px;'><%= del(@stats['active_posts']+@stats['deleted_posts']) %></td>
@ -320,7 +254,7 @@
</tr>
</table>
<h2>Users</h2>
<table class='rounded'>
<table class='stats-rounded'>
<tr>
<td style='width:250px;'>User count</td>
<td style='width:50px;'><%= del(@stats['total_users']) %></td>
@ -367,7 +301,7 @@
<div class='stats-column' id='stats-column-3'>
<h2>Comments</h2>
<table class='rounded'>
<table class='stats-rounded'>
<tr>
<td style='width:250px;'>Comment count</td>
<td style='width:50px;'><%= del(@stats['total_comments']) %></td>
@ -388,7 +322,7 @@
</table>
<h2>Forum Posts</h2>
<table class='rounded'>
<table class='stats-rounded'>
<tr>
<td style='width:250px;'>Thread count</td>
<td style='width:100px;'><%= del(@stats['total_forum_threads']) %></td>
@ -404,7 +338,7 @@
</table>
<h2>Blips</h2>
<table class='rounded'>
<table class='stats-rounded'>
<tr>
<td style='width:250px;'>Blip count</td>
<td style='width:50px;'><%= del(@stats['total_blips']) %></td>
@ -425,7 +359,7 @@
</table>
<h2>Tags</h2>
<table class='rounded'>
<table class='stats-rounded'>
<tr>
<td style='width:250px;'>Tag count</td>
<td style='width:50px;'><%= del(@stats['total_tags']) %></td>

View File

@ -24,8 +24,7 @@ Rails.application.routes.draw do
resources :staff_notes, only: [:index]
resources :danger_zone, only: [:index] do
collection do
put :enable_uploads
put :disable_uploads
put :uploading_limits
end
end
end
@ -176,7 +175,7 @@ Rails.application.routes.draw do
get :is_allowed
end
end
resources :email_blacklists
resources :email_blacklists, only: [:new, :create, :destroy, :index]
resource :iqdb_queries, :only => [:show] do
collection do
post :show

View File

@ -1,17 +1,29 @@
// config/webpack/rules/vue.js
const { VueLoaderPlugin } = require('vue-loader')
const { DefinePlugin } = require('webpack')
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
loader: 'vue-loader',
options: {
compilerOptions: {
whitespace: "preserve",
}
},
},
]
},
plugins: [new VueLoaderPlugin()],
plugins: [
new VueLoaderPlugin(),
new DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
})
],
resolve: {
extensions: ['.vue']
extensions: ['.vue'],
}
}

View File

@ -3,6 +3,7 @@
"private": true,
"dependencies": {
"@rails/webpacker": "^6.0.0-rc.5",
"@vue/compiler-sfc": "^3.1.0",
"babel-plugin-macros": "^3.1.0",
"core-js": "3.8",
"css-loader": "^6.3.0",
@ -16,9 +17,8 @@
"sass-loader": "^12.1.0",
"script-loader": "^0.7.2",
"style-loader": "^3.3.0",
"vue": "^2.6.10",
"vue-loader": "^15.7.0",
"vue-template-compiler": "^2.6.10",
"vue": "^3.1.0",
"vue-loader": "^16.0.0",
"webpack": "^5.51.1",
"webpack-cli": "^4.8.0",
"zingtouch": "^1.0.6"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,21 +1,49 @@
<!doctype html>
<html>
<html lang="en">
<head>
<title>Downbooru</title>
<style type="text/css">
<title>e621 - Maintenance</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
margin: 0;
padding: 10em 2em;
font-family: 'Palatino Linotype', serif;
color: white;
background-color: #012e56;
font-family: verdana,sans-serif;
font-size: large;
}
h1, p {
p {
text-align: center;
}
img, .mascot {
display: block;
margin-left: auto;
margin-right: auto;
}
.mascot {
/* Crop out some of the empty space to the left so it looks better on mobile */
background-image: url("https://static1.e621.net/data/mascot_bg/esix2.jpg");
background-position: -200px 0px;
width: 400px;
height: 615px;
}
a {
color: #b4c7d9;
}
a:hover {
color: #e9f2fa;
}
</style>
</head>
<body>
<h1>The site is down for maintenance.</h1>
<p><a class="twitter-timeline" data-width="500" data-tweet-limit="3" data-dnt="true" href="https://twitter.com/danboorubot">Check Twitter for updates.</a> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p>
<img src="http://static1.e621.net/data/mascot_bg/android-chrome-192x192.png">
<p>
We are performing maintenance on the site and its servers! Keep an eye on the <a href="https://twitter.com/e621dotnet">e621 Twitter</a> for more details.
</p>
<p>Thanks for your patience!</p>
<div class="mascot"></div>
</body>
</html>

11944
public/vendor/vue.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -2,5 +2,6 @@ FactoryBot.define do
factory(:bulk_update_request) do |f|
title { "xxx" }
script { "create alias aaa -> bbb" }
reason { "xxxxx" }
end
end

View File

@ -0,0 +1,27 @@
require 'test_helper'
class Admin::DangerZoneControllerTest < ActionDispatch::IntegrationTest
context "The danger zone controller" do
setup do
@admin = create(:admin_user)
end
teardown do
DangerZone.min_upload_level = User::Levels::MEMBER
end
context "index action" do
should "render" do
get_auth admin_danger_zone_index_path, @admin
assert_response :success
end
end
context "uploading limits action" do
should "work" do
put_auth uploading_limits_admin_danger_zone_index_path, @admin, params: { uploading_limits: { min_level: User::Levels::PRIVILEGED } }
assert_equal DangerZone.min_upload_level, User::Levels::PRIVILEGED
end
end
end
end

View File

@ -17,7 +17,7 @@ class BulkUpdateRequestsControllerTest < ActionDispatch::IntegrationTest
context "#create" do
should "succeed" do
assert_difference("BulkUpdateRequest.count", 1) do
post_auth bulk_update_requests_path, @user, params: {bulk_update_request: {script: "create alias aaa -> bbb", title: "xxx"}}
post_auth bulk_update_requests_path, @user, params: { bulk_update_request: { script: "create alias aaa -> bbb", title: "xxx", reason: "xxxxx" } }
end
end
end

View File

@ -13,6 +13,7 @@ class IqdbQueriesControllerTest < ActionDispatch::IntegrationTest
context "show action" do
context "with a url parameter" do
setup do
FactoryBot.create(:upload_whitelist, pattern: "*google.com")
@url = "https://google.com"
@params = { url: @url }
@mocked_response = [{
@ -41,7 +42,7 @@ class IqdbQueriesControllerTest < ActionDispatch::IntegrationTest
end
should "redirect to iqdbs" do
IqdbProxy.expects(:query).with(@posts[0].preview_file_url).returns(@mocked_response)
IqdbProxy.expects(:query_path).with(@posts[0].preview_file_path).returns(@mocked_response)
get_auth iqdb_queries_path, @user, params: @params
assert_select("#post_#{@posts[0].id}")
end
@ -49,14 +50,14 @@ class IqdbQueriesControllerTest < ActionDispatch::IntegrationTest
context "with matches" do
setup do
json = @posts.map {|x| {"post_id" => x.id, "score" => 1}}.to_json
json = @posts.map { |x| { "post_id" => x.id, "score" => 1 } }.to_json
@params = { matches: json }
end
should "render with matches" do
get_auth iqdb_queries_path, @user, params: @params
assert_response :success
end
end
end
end
end

View File

@ -4,7 +4,7 @@ class RelatedTagsControllerTest < ActionDispatch::IntegrationTest
context "The related tags controller" do
context "show action" do
should "work" do
get related_tag_path, params: { query: "touhou" }
get_auth related_tag_path, create(:user), params: { query: "touhou" }
assert_response :success
end
end

View File

@ -16,7 +16,7 @@ class TagAliasRequestsControllerTest < ActionDispatch::IntegrationTest
context "create action" do
should "render" do
assert_difference("ForumTopic.count", 1) do
post_auth tag_alias_request_path, @user, params: {:tag_alias_request => {:antecedent_name => "aaa", :consequent_name => "bbb", :reason => "ccc"}}
post_auth tag_alias_request_path, @user, params: { tag_alias_request: { antecedent_name: "aaa", consequent_name: "bbb", reason: "ccccc" } }
end
assert_redirected_to(forum_topic_path(ForumTopic.last))
end

View File

@ -18,7 +18,7 @@ class TagImplicationRequestsControllerTest < ActionDispatch::IntegrationTest
context "create action" do
should "create forum post" do
assert_difference("ForumTopic.count", 1) do
post_auth tag_implication_request_path, @user, params: {:tag_implication_request => {:antecedent_name => "aaa", :consequent_name => "bbb", :reason => "ccc"}}
post_auth tag_implication_request_path, @user, params: { tag_implication_request: { antecedent_name: "aaa", consequent_name: "bbb", reason: "ccccc" } }
end
assert_redirected_to(forum_topic_path(ForumTopic.last))
end

View File

@ -12,7 +12,6 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
context "The uploads controller" do
setup do
@user = create(:janitor_user)
mock_iqdb_service!
end
context "new action" do
@ -65,6 +64,26 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
end
end
end
context "when uploads are disabled" do
setup do
DangerZone.min_upload_level = User::Levels::PRIVILEGED
end
teardown do
DangerZone.min_upload_level = User::Levels::MEMBER
end
should "prevent uploads" do
get_auth new_upload_path, create(:user)
assert_response :forbidden
end
should "allow uploads for users of the same or higher level" do
get_auth new_upload_path, create(:privileged_user, created_at: 2.weeks.ago)
assert_response :success
end
end
end
context "index action" do
@ -75,7 +94,7 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
end
should "render" do
get uploads_path
get_auth uploads_path, @user
assert_response :success
end
@ -90,7 +109,7 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
status: @upload.status
}
get_auth uploads_path, params: { search: search_params }
get_auth uploads_path, @user, params: { search: search_params }
assert_response :success
end
end

View File

@ -90,25 +90,26 @@ class UploadServiceTest < ActiveSupport::TestCase
context ".generate_resizes" do
context "for a video" do
teardown do
@file.close
end
context "for a webm" do
setup do
@file = File.open("test/files/test-512x512.webm", "rb")
@upload = mock()
@upload.stubs(:is_video?).returns(true)
@upload = UploadService.new(FactoryBot.attributes_for(:upload).merge(file: @file, uploader: @user, uploader_ip_addr: '127.0.0.1')).start!
end
teardown do
@file.close
end
should "generate a video" do
preview, crop, sample = subject.generate_resizes(@file, @upload)
assert_operator(File.size(preview.path), :>, 0)
assert_operator(File.size(crop.path), :>, 0)
assert_equal(150, ImageSpec.new(preview.path).width)
assert_equal(150, ImageSpec.new(preview.path).height)
assert_equal(150, ImageSpec.new(crop.path).width)
assert_equal(150, ImageSpec.new(crop.path).height)
preview_image = Vips::Image.new_from_file(preview.path)
crop_image = Vips::Image.new_from_file(crop.path)
assert_equal(150, preview_image.width)
assert_equal(150, preview_image.height)
assert_equal(150, crop_image.width)
assert_equal(150, crop_image.height)
preview.close
preview.unlink
crop.close

View File

@ -1,13 +1,5 @@
ENV["RAILS_ENV"] = "test"
if ENV["SIMPLECOV"]
require 'simplecov'
SimpleCov.start 'rails' do
add_group "Libraries", ["app/logical", "lib"]
add_group "Presenters", "app/presenters"
end
end
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'cache'
@ -47,9 +39,7 @@ module TestHelpers
end
end
class ActiveSupport::TestCase
include IqdbTestHelper
include UploadTestHelper
include TestHelpers
@ -64,7 +54,6 @@ class ActiveSupport::TestCase
Danbooru.config.stubs(:storage_manager).returns(storage_manager)
Danbooru.config.stubs(:backup_storage_manager).returns(StorageManager::Null.new)
Danbooru.config.stubs(:enable_email_verification?).returns(false)
end
teardown do

View File

@ -1,30 +0,0 @@
module IqdbTestHelper
def mock_iqdb_service!
mock_sqs_service = Class.new do
def initialize
@commands = []
end
def commands
@commands
end
def send_message(msg)
@commands << msg.split(/\n/).first
end
end
service = mock_sqs_service.new
Post.stubs(:iqdb_sqs_service).returns(service)
Danbooru.config.stubs(:iqdbs_server).returns("http://localhost:3004")
end
def mock_iqdb_matches!(post_or_source, matches)
source = post_or_source.is_a?(Post) ? post_or_source.preview_file_url : post_or_source
url = "http://localhost:3004/similar?key=hunter2&url=#{CGI.escape source}&ref"
body = matches.map { |post| { post_id: post.id } }.to_json
stub_request(:get, url).to_return(body: body)
end
end

View File

@ -146,7 +146,7 @@ class BanTest < ActiveSupport::TestCase
context "when only expired bans exist" do
setup do
@ban = FactoryBot.create(:ban, :user => @user, :banner => @admin, :duration => -1)
@ban = FactoryBot.create(:ban, :user => @user, :banner => @admin, :duration => 1)
end
should "not return expired bans" do

View File

@ -71,7 +71,13 @@ class BulkUpdateRequestTest < ActiveSupport::TestCase
should "create a forum topic" do
assert_difference("ForumTopic.count", 1) do
BulkUpdateRequest.create(:title => "abc", :reason => "zzz", :script => "create alias aaa -> bbb")
create(:bulk_update_request)
end
end
should "not create a forum when skip_forum is true" do
assert_no_difference("ForumTopic.count") do
create(:bulk_update_request, skip_forum: true)
end
end

View File

@ -21,8 +21,32 @@ class EmailBlacklistTest < ActiveSupport::TestCase
should "detect email by mx" do
block = EmailBlacklist.create(creator: @user, domain: 'google.com', reason: 'test')
EmailBlacklist.stubs(:get_mx_records).returns(['google.com'])
assert(EmailBlacklist.is_banned?('spam@e621.net'))
EmailBlacklist.unstub(:get_mx_records)
assert_equal(false, EmailBlacklist.is_banned?('what@me.xynzs'))
end
should "keep accounts verified if there are too many matches" do
(EmailBlacklist::UNVERIFY_COUNT_TRESHOLD + 1).times do |i|
@domain_blocked_user = create(:user, email: "#{i}@domain.com")
end
EmailBlacklist.create(creator: @user, domain: "domain.com", reason: "test")
@domain_blocked_user.reload
assert @domain_blocked_user.is_verified?
end
should "unverify accounts if there are few matches" do
@domain_blocked_user = create(:user, email: "0@domain.com")
@other_user1 = create(:user, email: "0@prefix.domain.com")
@other_user2 = create(:user, email: "0@somethingelse.xynzs")
EmailBlacklist.create(creator: @user, domain: "domain.com", reason: "test")
@domain_blocked_user.reload
@other_user1.reload
@other_user2.reload
assert_not @domain_blocked_user.is_verified?
assert @other_user1.is_verified?
assert @other_user2.is_verified?
end
end

View File

@ -77,7 +77,8 @@ class NoteTest < ActiveSupport::TestCase
end
should "update the post's last_noted_at field" do
@note.update(x: 500)
assert_nil(@post.last_noted_at)
@note = create(:note, post: @post)
@post.reload
assert_equal(@post.last_noted_at, @note.updated_at)
end
@ -112,10 +113,12 @@ class NoteTest < ActiveSupport::TestCase
end
should "update the post's last_noted_at field" do
assert_nil(@post.last_noted_at)
@note.update(:x => 500)
@post.reload
assert_equal(@post.last_noted_at.to_i, @note.updated_at.to_i)
assert_equal(@post.last_noted_at, @note.updated_at)
Timecop.travel(1.day.from_now) do
@note.update(x: 500)
@post.reload
assert_equal(@post.last_noted_at, @note.updated_at)
end
end
should "create a version" do

View File

@ -95,6 +95,20 @@ class PostEventTest < ActiveSupport::TestCase
end
end
context "unflag on approve" do
setup do
as @janitor do
create(:post_flag, post: @post)
end
end
should "create both post events" do
assert_post_events_created(@janitor, [:flag_removed, :approved]) do
@post.approve!(@janitor)
end
end
end
context "replacements" do
setup do
@replacement = FactoryBot.create(:png_replacement, creator: @user, creator_ip_addr: '127.0.0.1', post: @post)

View File

@ -146,7 +146,6 @@ class PostReplacementTest < ActiveSupport::TestCase
end
end
should "update post with new image" do
old_md5 = @post.md5
@replacement.approve! penalize_current_uploader: true
@ -249,6 +248,18 @@ class PostReplacementTest < ActiveSupport::TestCase
@replacement.approve! penalize_current_uploader: false
assert_equal ["Status must be pending or original to approve"], @replacement.errors.full_messages
end
context "update the duration" do
setup do
@replacement = FactoryBot.create(:webm_replacement, creator: @user, creator_ip_addr: '127.0.0.1', post: @post)
end
should "when the replacement is a video" do
@replacement.approve! penalize_current_uploader: false
@post.reload
assert @post.duration
end
end
end
context "Toggle:" do

View File

@ -76,8 +76,7 @@ class PostTest < ActiveSupport::TestCase
end
should "remove the post from iqdb" do
mock_iqdb_service!
@post.expects(:remove_iqdb_async).once
@post.expunge!
end
@ -2255,8 +2254,8 @@ class PostTest < ActiveSupport::TestCase
assert_equal("https://#{Danbooru.config.hostname}/data/preview/deadbeef.jpg", @post.preview_file_url)
assert_equal("https://#{Socket.gethostname}/data/deadbeef.gif", @post.large_file_url)
assert_equal("https://#{Socket.gethostname}/data/deadbeef.gif", @post.file_url)
assert_equal("https://#{Danbooru.config.hostname}/data/deadbeef.gif", @post.large_file_url)
assert_equal("https://#{Danbooru.config.hostname}/data/deadbeef.gif", @post.file_url)
end
end

View File

@ -44,7 +44,20 @@ class TagAliasRequestTest < ActiveSupport::TestCase
should "save the forum post id" do
tar = TagAliasRequest.new(:antecedent_name => "aaa", :consequent_name => "bbb", :reason => "reason")
tar.create
assert_equal(tar.forum_topic.posts.first.id, tar.tag_alias.forum_post.id)
assert_equal(tar.forum_topic.posts.first.id, tar.tag_relationship.forum_post.id)
end
should "fail validation if the reason is too short" do
tar = TagAliasRequest.new(antecedent_name: "aaa", consequent_name: "bbb", reason: "")
tar.create
assert_match(/Reason is too short/, tar.errors.full_messages.join)
end
should "not create a forum post if skip_forum is true" do
assert_no_difference("ForumPost.count") do
tar = TagAliasRequest.new(antecedent_name: "aaa", consequent_name: "bbb", skip_forum: true)
tar.create
end
end
end
end

View File

@ -185,10 +185,10 @@ class TagAliasTest < ActiveSupport::TestCase
end
should "update the topic when failed" do
@alias.stubs(:sleep).returns(true)
@alias.stubs(:update_posts).raises(Exception, "oh no")
TagAlias.any_instance.stubs(:update_blacklists).raises(Exception, "oh no")
@alias.approve!(approver: @admin)
@topic.reload
@alias.reload
assert_equal("[FAILED] Tag alias: aaa -> bbb", @topic.title)
assert_match(/error: oh no/, @alias.status)

View File

@ -5,7 +5,6 @@ class UploadTest < ActiveSupport::TestCase
context "In all cases" do
setup do
mock_iqdb_service!
user = FactoryBot.create(:contributor_user)
CurrentUser.user = user
CurrentUser.ip_addr = "127.0.0.1"
@ -21,6 +20,7 @@ class UploadTest < ActiveSupport::TestCase
setup do
CurrentUser.user = FactoryBot.create(:user, :created_at => 1.year.ago)
User.any_instance.stubs(:upload_limit).returns(0)
Danbooru.config.stubs(:disable_throttles?).returns(false)
end
should "fail creation" do

294
yarn.lock
View File

@ -412,6 +412,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.7.tgz#0c3ed4a2eb07b165dfa85b3cc45c727334c4edae"
integrity sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g==
"@babel/parser@^7.16.4":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.0.tgz#10a8d4e656bc01128d299a787aa006ce1a91e112"
integrity sha512-AqDccGC+m5O/iUStSJy3DGRIUFu7WbY/CppZYwrEUB4N0tZlnI8CSTsgL7v5fHVFmUbRv2sd+yy27o8Ydt4MGg==
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.15.4":
version "7.15.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.15.4.tgz#dbdeabb1e80f622d9f0b583efb2999605e0a567e"
@ -1177,21 +1182,95 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065"
integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==
"@vue/component-compiler-utils@^3.1.0":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.2.2.tgz#2f7ed5feed82ff7f0284acc11d525ee7eff22460"
integrity sha512-rAYMLmgMuqJFWAOb3Awjqqv5X3Q3hVr4jH/kgrFJpiU0j3a90tnNBplqbj+snzrgZhC9W128z+dtgMifOiMfJg==
"@vue/compiler-core@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.35.tgz#f2fc01bd25d859a77b0d9ef5df28f657c1979070"
integrity sha512-1Mtmh8ceVUoUsn/PME5oM+Dus648rCeV/fBaZ4ERLFbTHBJXj6QmDPrSn9mfEyPDXE0RYIwyJNn884NdWK+Yiw==
dependencies:
consolidate "^0.15.1"
hash-sum "^1.0.2"
lru-cache "^4.1.2"
merge-source-map "^1.1.0"
postcss "^7.0.36"
postcss-selector-parser "^6.0.2"
source-map "~0.6.1"
vue-template-es2015-compiler "^1.9.0"
optionalDependencies:
prettier "^1.18.2"
"@babel/parser" "^7.16.4"
"@vue/shared" "3.2.35"
estree-walker "^2.0.2"
source-map "^0.6.1"
"@vue/compiler-dom@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.35.tgz#11bbcca0d49f9991d64dd8fbf8a0a4453caa571c"
integrity sha512-I4bXB9MkRSTJ3gVXRQ4iaYJgABZGew+K/CCBoAh9fdLaeY7A7uUlS5nWGOlICSVfOH0/xk4QlcXeGZYCJkEleA==
dependencies:
"@vue/compiler-core" "3.2.35"
"@vue/shared" "3.2.35"
"@vue/compiler-sfc@3.2.35", "@vue/compiler-sfc@^3.1.0":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.35.tgz#1de80f858b33548bc22d166126234435937ebe0c"
integrity sha512-2wKQtnuHfwBFc7uV2Cmtms3Cc7u/u6kKJI3F+i0A+9xnuahK39cCMNJKHzI9x93Xai+uft64fDc5JSh8zDQBQA==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.35"
"@vue/compiler-dom" "3.2.35"
"@vue/compiler-ssr" "3.2.35"
"@vue/reactivity-transform" "3.2.35"
"@vue/shared" "3.2.35"
estree-walker "^2.0.2"
magic-string "^0.25.7"
postcss "^8.1.10"
source-map "^0.6.1"
"@vue/compiler-ssr@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.35.tgz#8d7726f95444c02f3301083e2c6c814780fb7b00"
integrity sha512-dJyqB8fZbvVQEnWl5VGxkWHTqx0ERnZXXqInFzyOX8FpTEidmQbUSmDrXidea7bZTdeg6ly94kZFGPYXT29mgQ==
dependencies:
"@vue/compiler-dom" "3.2.35"
"@vue/shared" "3.2.35"
"@vue/reactivity-transform@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.35.tgz#29ba344b0ec9ec7ee75ca6c1a6f349595ca150d8"
integrity sha512-VjdQU4nIrgsh1iPqAdYZufWgFqdH9fIl6ttO2PCFlLsrQl7b8BcuawM6moSBLF8damBzSNcqvbvQDBhsI3fyVQ==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.35"
"@vue/shared" "3.2.35"
estree-walker "^2.0.2"
magic-string "^0.25.7"
"@vue/reactivity@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.35.tgz#c66af289f3beda6aba63c264db9c6acd607d1c73"
integrity sha512-6j9N9R1SwHVcJas4YqAzwdRS/cgmj3Z9aUert5Mv1jk5B9H9ivN/zot/fgMUbseWXigkkmX60OsfRbz49o8kCw==
dependencies:
"@vue/shared" "3.2.35"
"@vue/runtime-core@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.35.tgz#a87bd5214ff31f9dc6542f5c498d4f3543c6ea8f"
integrity sha512-P8AeGPRGyIiYdOdvLc/7KR8VSdbUGG8Jxdx6Xlj5okEjyV9IYxeHRIQIoye85K0lZXBH4zuh1syD1mX+oZ0KqQ==
dependencies:
"@vue/reactivity" "3.2.35"
"@vue/shared" "3.2.35"
"@vue/runtime-dom@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.35.tgz#004bf6109353e75cf22eb11baf247288c216ef7b"
integrity sha512-M5xrVJ/b0KqssjPQMdpwLp3KwzG1Tn2w/IrOptVqGY5c9fEBluIbm18AeO4Fr3YxfeyaPWm1rY8POrEso0UE3w==
dependencies:
"@vue/runtime-core" "3.2.35"
"@vue/shared" "3.2.35"
csstype "^2.6.8"
"@vue/server-renderer@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.35.tgz#79fb9e2315ec6196b5f2f385ef4ae31cf5599a2f"
integrity sha512-ZMF8V+bZ0EIjSB7yzPEmDlxRDOIXj04iqG4Rw/H5rIuBCf0b7rNTleiOldlX5haG++zUq6uiL2AVp/A9uyz+cw==
dependencies:
"@vue/compiler-ssr" "3.2.35"
"@vue/shared" "3.2.35"
"@vue/shared@3.2.35":
version "3.2.35"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.35.tgz#fb60530fa009dc21473386a7639eed833877cb0f"
integrity sha512-/sxDqMcy0MsfQ3LQixKYDxIinDYNy1dXTsF2Am0pv0toImWabymFQ8cFmPJnPt+gh5ElKwwn7KzQcDbLHar60A==
"@webassemblyjs/ast@1.11.1":
version "1.11.1"
@ -1516,11 +1595,6 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bluebird@^3.1.1:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@ -1644,7 +1718,7 @@ caniuse-lite@^1.0.30001248:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz#6853a606ec50893115db660f82c094d18f096d85"
integrity sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==
chalk@^2.0.0, chalk@^2.4.2:
chalk@^2.0.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@ -1653,7 +1727,7 @@ chalk@^2.0.0, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4.0:
chalk@^4.0, chalk@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@ -1787,13 +1861,6 @@ connect-history-api-fallback@^1.6.0:
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
consolidate@^0.15.1:
version "0.15.1"
resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==
dependencies:
bluebird "^3.1.1"
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
@ -1986,10 +2053,10 @@ csso@^4.2.0:
dependencies:
css-tree "^1.1.2"
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
csstype@^2.6.8:
version "2.6.20"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
debug@2.6.9:
version "2.6.9"
@ -2229,6 +2296,11 @@ estraverse@^5.2.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
@ -2528,15 +2600,10 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hash-sum@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=
he@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hash-sum@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a"
integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==
hpack.js@^2.1.6:
version "2.1.6"
@ -2948,7 +3015,7 @@ loader-runner@^4.2.0:
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.4.0:
loader-utils@^1.1.0, loader-utils@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
@ -2957,6 +3024,15 @@ loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.4.0:
emojis-list "^3.0.0"
json5 "^1.0.1"
loader-utils@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
json5 "^2.1.2"
locate-path@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
@ -3006,14 +3082,6 @@ lodash@^4.17.14:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lru-cache@^4.1.2:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@ -3021,6 +3089,13 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
magic-string@^0.25.7:
version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
dependencies:
sourcemap-codec "^1.4.8"
make-dir@^3.0.2, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@ -3050,13 +3125,6 @@ merge-descriptors@1.0.1:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
merge-source-map@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646"
integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==
dependencies:
source-map "^0.6.1"
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -3181,6 +3249,11 @@ nanoid@^3.1.25:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152"
integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==
nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@ -3410,6 +3483,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
@ -3687,14 +3765,14 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
postcss@^7.0.36:
version "7.0.36"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb"
integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==
postcss@^8.1.10:
version "8.4.14"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
supports-color "^6.1.0"
nanoid "^3.3.4"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.2.15, postcss@^8.3.5:
version "8.3.7"
@ -3705,11 +3783,6 @@ postcss@^8.2.15, postcss@^8.3.5:
nanoid "^3.1.25"
source-map-js "^0.6.2"
prettier@^1.18.2:
version "1.19.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -3723,11 +3796,6 @@ proxy-addr@~2.0.5:
forwarded "0.2.0"
ipaddr.js "1.9.1"
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
@ -4133,6 +4201,11 @@ source-map-js@^0.6.2:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-support@~0.5.20:
version "0.5.20"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
@ -4146,7 +4219,7 @@ source-map@^0.5.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
source-map@^0.6.0, source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@ -4156,6 +4229,11 @@ source-map@~0.7.2:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
spdy-transport@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
@ -4235,13 +4313,6 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"
supports-color@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
dependencies:
has-flag "^3.0.0"
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@ -4413,47 +4484,25 @@ vendors@^1.0.3:
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
vue-hot-reload-api@^2.3.0:
version "2.3.4"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
vue-loader@^15.7.0:
version "15.9.8"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.8.tgz#4b0f602afaf66a996be1e534fb9609dc4ab10e61"
integrity sha512-GwSkxPrihfLR69/dSV3+5CdMQ0D+jXg8Ma1S4nQXKJAznYFX14vHdc/NetQc34Dw+rBbIJyP7JOuVb9Fhprvog==
vue-loader@^16.0.0:
version "16.8.3"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.8.3.tgz#d43e675def5ba9345d6c7f05914c13d861997087"
integrity sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==
dependencies:
"@vue/component-compiler-utils" "^3.1.0"
hash-sum "^1.0.2"
loader-utils "^1.1.0"
vue-hot-reload-api "^2.3.0"
vue-style-loader "^4.1.0"
chalk "^4.1.0"
hash-sum "^2.0.0"
loader-utils "^2.0.0"
vue-style-loader@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35"
integrity sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==
vue@^3.1.0:
version "3.2.35"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.35.tgz#e0a63a8b2fcc0334a936d484d646e31b571e3f80"
integrity sha512-mc/15B0Wjd/4JMMGOcXUQAeXfjyg8MImA2EVZucNdyDPJe1nXhMNbYXOEVPEGfk/mCeyszCzl44dSAhHhQVH8g==
dependencies:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
vue-template-compiler@^2.6.10:
version "2.6.14"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz#a2f0e7d985670d42c9c9ee0d044fed7690f4f763"
integrity sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==
dependencies:
de-indent "^1.0.2"
he "^1.1.0"
vue-template-es2015-compiler@^1.9.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue@^2.6.10:
version "2.6.14"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==
"@vue/compiler-dom" "3.2.35"
"@vue/compiler-sfc" "3.2.35"
"@vue/runtime-dom" "3.2.35"
"@vue/server-renderer" "3.2.35"
"@vue/shared" "3.2.35"
watchpack@^2.2.0:
version "2.2.0"
@ -4623,11 +4672,6 @@ ws@^8.1.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.2.tgz#ca684330c6dd6076a737250ed81ac1606cb0a63e"
integrity sha512-Q6B6H2oc8QY3llc3cB8kVmQ6pnJWVQbP7Q5algTcIxx7YEpc0oU4NBVHlztA7Ekzfhw2r0rPducMUiCGWKQRzw==
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"