From 51edb19aa9b684ba1b285a937a14082685216a0c Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 11 Sep 2018 06:39:09 +0530 Subject: [PATCH 01/59] FIX: pop3 polling password and mailgun API key should be secret --- config/site_settings.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index 153ecac96f..d151832c73 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -783,7 +783,9 @@ email: pop3_polling_host: '' pop3_polling_port: 995 pop3_polling_username: '' - pop3_polling_password: '' + pop3_polling_password: + default: '' + secret: true pop3_polling_delete_from_server: true log_mail_processing_failures: false incoming_email_prefer_html: true @@ -837,6 +839,7 @@ email: mailgun_api_key: default: '' regex: '^(key-\h{32}|\h{32}-\h{8}-\h{8})$' + secret: true bounce_score_threshold: client: true default: 4 @@ -933,7 +936,7 @@ files: default: false client: true s3_use_iam_profile: false - s3_access_key_id: + s3_access_key_id: default: '' secret: true shadowed_by_global: true From 4ee8e7707350d1a7e8ad396940983dea7dc63f9f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Sep 2018 09:16:03 +0800 Subject: [PATCH 02/59] Apply prettier. --- test/smoke_test.js | 107 +++++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/test/smoke_test.js b/test/smoke_test.js index 3bc3e7559b..9767f872bc 100644 --- a/test/smoke_test.js +++ b/test/smoke_test.js @@ -9,8 +9,8 @@ const url = args[0]; console.log(`Starting Discourse Smoke Test for ${url}`); -const puppeteer = require('puppeteer'); -const path = require('path'); +const puppeteer = require("puppeteer"); +const path = require("path"); (async () => { const browser = await puppeteer.launch({ @@ -26,7 +26,8 @@ const path = require('path'); }); const takeFailureScreenshot = function() { - const screenshotPath = `${process.env.SMOKE_TEST_SCREENSHOT_PATH || 'tmp/smoke-test-screenshots'}/smoke-test-${Date.now()}.png`; + const screenshotPath = `${process.env.SMOKE_TEST_SCREENSHOT_PATH || + "tmp/smoke-test-screenshots"}/smoke-test-${Date.now()}.png`; console.log(`Screenshot of failure taken at ${screenshotPath}`); return page.screenshot({ path: screenshotPath, fullPage: true }); }; @@ -34,36 +35,43 @@ const path = require('path'); const exec = (description, fn, assertion) => { const start = +new Date(); - return fn.call().then(async output => { - if (assertion) { - if (assertion.call(this, output)) { - console.log(`PASSED: ${description} - ${(+new Date()) - start}ms`); + return fn + .call() + .then(async output => { + if (assertion) { + if (assertion.call(this, output)) { + console.log(`PASSED: ${description} - ${+new Date() - start}ms`); + } else { + console.log(`FAILED: ${description} - ${+new Date() - start}ms`); + await takeFailureScreenshot(); + console.log("SMOKE TEST FAILED"); + process.exit(1); + } } else { - console.log(`FAILED: ${description} - ${(+new Date()) - start}ms`); - await takeFailureScreenshot(); - console.log("SMOKE TEST FAILED"); - process.exit(1); + console.log(`PASSED: ${description} - ${+new Date() - start}ms`); } - } else { - console.log(`PASSED: ${description} - ${(+new Date()) - start}ms`); - } - }).catch(async error => { - console.log(`ERROR (${description}): ${error.message} - ${(+new Date()) - start}ms`); - await takeFailureScreenshot(); - console.log("SMOKE TEST FAILED"); - process.exit(1); - }); + }) + .catch(async error => { + console.log( + `ERROR (${description}): ${error.message} - ${+new Date() - start}ms` + ); + await takeFailureScreenshot(); + console.log("SMOKE TEST FAILED"); + process.exit(1); + }); }; const assert = (description, fn, assertion) => { return exec(description, fn, assertion); }; - page.on('console', msg => console.log(`PAGE LOG: ${msg.text()}`)); + page.on("console", msg => console.log(`PAGE LOG: ${msg.text()}`)); - page.on('response', resp => { + page.on("response", resp => { if (resp.status() !== 200) { - console.log("FAILED HTTP REQUEST TO " + resp.url() + " Status is: " + resp.status()); + console.log( + "FAILED HTTP REQUEST TO " + resp.url() + " Status is: " + resp.status() + ); } return resp; }); @@ -71,7 +79,9 @@ const path = require('path'); if (process.env.AUTH_USER && process.env.AUTH_PASSWORD) { await exec("basic authentication", () => { return page.setExtraHTTPHeaders({ - 'Authorization': `Basic ${new Buffer(`${process.env.AUTH_USER}:${process.env.AUTH_PASSWORD}`).toString('base64')}` + Authorization: `Basic ${new Buffer( + `${process.env.AUTH_USER}:${process.env.AUTH_PASSWORD}` + ).toString("base64")}` }); }); } @@ -85,7 +95,7 @@ const path = require('path'); }); await exec("go to latest page", () => { - return page.goto(path.join(url, 'latest')); + return page.goto(path.join(url, "latest")); }); await exec("at least one topic shows up", () => { @@ -93,7 +103,7 @@ const path = require('path'); }); await exec("go to categories page", () => { - return page.goto(path.join(url, 'categories')); + return page.goto(path.join(url, "categories")); }); await exec("can see categories on the page", () => { @@ -126,10 +136,16 @@ const path = require('path'); }); await exec("type in credentials & log in", () => { - let promise = page.type("#login-account-name", process.env.DISCOURSE_USERNAME || 'smoke_user'); + let promise = page.type( + "#login-account-name", + process.env.DISCOURSE_USERNAME || "smoke_user" + ); promise = promise.then(() => { - return page.type("#login-account-password", process.env.DISCOURSE_PASSWORD || 'P4ssw0rd'); + return page.type( + "#login-account-password", + process.env.DISCOURSE_PASSWORD || "P4ssw0rd" + ); }); promise = promise.then(() => { @@ -166,7 +182,7 @@ const path = require('path'); }); await exec("compose new topic", () => { - const date = `(${(+new Date())})`; + const date = `(${+new Date()})`; const title = `This is a new topic ${date}`; const post = `I can write a new topic inside the smoke test! ${date} \n\n`; @@ -210,16 +226,19 @@ const path = require('path'); }); await exec("composer is open", () => { - return page.waitForSelector("#reply-control .d-editor-input", { visible: true }); + return page.waitForSelector("#reply-control .d-editor-input", { + visible: true + }); }); await exec("compose reply", () => { - const post = `I can even write a reply inside the smoke test ;) (${(+new Date())})`; + const post = `I can even write a reply inside the smoke test ;) (${+new Date()})`; return page.type("#reply-control .d-editor-input", post); }); await exec("waiting for the preview", () => { - return page.waitForXPath("//div[contains(@class, 'd-editor-preview') and contains(.//p, 'I can even write a reply')]", + return page.waitForXPath( + "//div[contains(@class, 'd-editor-preview') and contains(.//p, 'I can even write a reply')]", { visible: true } ); }); @@ -228,19 +247,23 @@ const path = require('path'); return page.click("#reply-control .create"); }); - await assert("reply is created", () => { - let promise = page.waitForSelector(".topic-post"); + await assert( + "reply is created", + () => { + let promise = page.waitForSelector(".topic-post"); - promise = promise.then(() => { - return page.evaluate(() => { - return document.querySelectorAll(".topic-post").length; + promise = promise.then(() => { + return page.evaluate(() => { + return document.querySelectorAll(".topic-post").length; + }); }); - }); - return promise; - }, output => { - return output === 2; - }); + return promise; + }, + output => { + return output === 2; + } + ); } await exec("close browser", () => { From 244948497afefd1460755ddf67e6e8f37f33704f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Sep 2018 09:52:39 +0800 Subject: [PATCH 03/59] Lock puppeteer to 1.4.x. 1.8 doesn't seem to be able to click reliably. --- package.json | 2 +- yarn.lock | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index dbeab0e8e5..3a1322b0f5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "chrome-remote-interface": "^0.25", "eslint": "^4.19", "prettier": "^1.13", - "puppeteer": "^1.4", + "puppeteer": "1.4", "pretender": "^1.6" } } diff --git a/yarn.lock b/yarn.lock index 62f9d572c6..8c7d81dc03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -470,7 +470,7 @@ external-editor@^2.0.4: iconv-lite "^0.4.17" tmp "^0.0.33" -extract-zip@^1.6.6: +extract-zip@^1.6.5: version "1.6.7" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" dependencies: @@ -575,7 +575,7 @@ has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" -https-proxy-agent@^2.2.1: +https-proxy-agent@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" dependencies: @@ -851,18 +851,18 @@ pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" -puppeteer@^1.4: - version "1.8.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.8.0.tgz#9e8bbd2f5448cc19cac220efc0512837104877ad" +puppeteer@1.4: + version "1.4.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.4.0.tgz#437f0f3450d76e437185c0bf06f446e80f184692" dependencies: debug "^3.1.0" - extract-zip "^1.6.6" - https-proxy-agent "^2.2.1" + extract-zip "^1.6.5" + https-proxy-agent "^2.1.0" mime "^2.0.3" progress "^2.0.0" proxy-from-env "^1.0.0" rimraf "^2.6.1" - ws "^5.1.1" + ws "^3.0.0" readable-stream@^2.2.2: version "2.3.3" @@ -1078,11 +1078,13 @@ ws@3.3.x: safe-buffer "~5.1.0" ultron "~1.1.0" -ws@^5.1.1: - version "5.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" +ws@^3.0.0: + version "3.3.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" dependencies: async-limiter "~1.0.0" + safe-buffer "~5.1.0" + ultron "~1.1.0" yallist@^2.1.2: version "2.1.2" From 85620abb710741ed83c42565b1033c492a756aeb Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Sep 2018 10:15:06 +0800 Subject: [PATCH 04/59] DEV: Clear connections after multisite specs. --- spec/multisite/distributed_cache_spec.rb | 11 +---------- spec/multisite/jobs_spec.rb | 21 ++++----------------- spec/multisite/site_settings_spec.rb | 5 +---- spec/rails_helper.rb | 11 +++++++++++ 4 files changed, 17 insertions(+), 31 deletions(-) diff --git a/spec/multisite/distributed_cache_spec.rb b/spec/multisite/distributed_cache_spec.rb index a6080c7576..cdd25dbfaf 100644 --- a/spec/multisite/distributed_cache_spec.rb +++ b/spec/multisite/distributed_cache_spec.rb @@ -1,17 +1,8 @@ require 'rails_helper' -RSpec.describe 'Multisite SiteSettings' do +RSpec.describe 'Multisite SiteSettings', type: :multisite do let(:conn) { RailsMultisite::ConnectionManagement } - before do - conn.config_filename = "spec/fixtures/multisite/two_dbs.yml" - end - - after do - conn.clear_settings! - ActiveRecord::Base.establish_connection - end - def cache(name, namespace: true) DistributedCache.new(name, namespace: namespace) end diff --git a/spec/multisite/jobs_spec.rb b/spec/multisite/jobs_spec.rb index 0ffd9f47a5..943f33642c 100644 --- a/spec/multisite/jobs_spec.rb +++ b/spec/multisite/jobs_spec.rb @@ -1,24 +1,11 @@ require 'rails_helper' -RSpec.describe "Running Sidekiq Jobs in Multisite" do +RSpec.describe "Running Sidekiq Jobs in Multisite", type: :multisite do let(:conn) { RailsMultisite::ConnectionManagement } - before do - conn.config_filename = "spec/fixtures/multisite/two_dbs.yml" - end - - after do - conn.clear_settings! - ActiveRecord::Base.establish_connection - end - it 'should revert back to the default connection' do - expect(RailsMultisite::ConnectionManagement.current_db) - .to eq('default') - - Jobs::DestroyOldDeletionStubs.new.perform({}) - - expect(RailsMultisite::ConnectionManagement.current_db) - .to eq('default') + expect do + Jobs::DestroyOldDeletionStubs.new.perform({}) + end.to_not change { RailsMultisite::ConnectionManagement.current_db } end end diff --git a/spec/multisite/site_settings_spec.rb b/spec/multisite/site_settings_spec.rb index bfecad7fd3..5a0d7b4719 100644 --- a/spec/multisite/site_settings_spec.rb +++ b/spec/multisite/site_settings_spec.rb @@ -1,12 +1,11 @@ require 'rails_helper' -RSpec.describe 'Multisite SiteSettings' do +RSpec.describe 'Multisite SiteSettings', type: :multisite do let(:conn) { RailsMultisite::ConnectionManagement } before do @original_provider = SiteSetting.provider SiteSetting.provider = SiteSettings::DbProvider.new(SiteSetting) - conn.config_filename = "spec/fixtures/multisite/two_dbs.yml" end after do @@ -14,9 +13,7 @@ RSpec.describe 'Multisite SiteSettings' do conn.with_connection(db) { SiteSetting.where(name: 'default_locale').destroy_all } end - conn.clear_settings! SiteSetting.provider = @original_provider - ActiveRecord::Base.establish_connection end describe '#default_locale' do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index b267a16e0e..c1ac476077 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -175,6 +175,17 @@ RSpec.configure do |config| end end + config.before(:each, type: :multisite) do + RailsMultisite::ConnectionManagement.config_filename = + "spec/fixtures/multisite/two_dbs.yml" + end + + config.after(:each, type: :multisite) do + RailsMultisite::ConnectionManagement.clear_settings! + ActiveRecord::Base.clear_active_connections! + ActiveRecord::Base.establish_connection + end + class TestCurrentUserProvider < Auth::DefaultCurrentUserProvider def log_on_user(user, session, cookies) session[:current_user_id] = user.id From a033327b93429aae3b1de455736016bbee981f0f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Sep 2018 15:07:28 +0800 Subject: [PATCH 05/59] Manage qunit via yarn. --- .../javascripts/wizard/test/test_helper.js | 1 + app/views/qunit/index.html.erb | 2 - app/views/wizard/qunit.html.erb | 2 - package.json | 3 +- test/javascripts/test_helper.js | 1 + test/stylesheets/test_helper.css | 1 + vendor/assets/javascripts/qunit.js | 4926 ----------------- vendor/assets/stylesheets/qunit.css | 436 -- yarn.lock | 1010 +++- 9 files changed, 1006 insertions(+), 5376 deletions(-) delete mode 100644 vendor/assets/javascripts/qunit.js delete mode 100644 vendor/assets/stylesheets/qunit.css diff --git a/app/assets/javascripts/wizard/test/test_helper.js b/app/assets/javascripts/wizard/test/test_helper.js index 1d2ddbdb16..0b7e04d877 100644 --- a/app/assets/javascripts/wizard/test/test_helper.js +++ b/app/assets/javascripts/wizard/test/test_helper.js @@ -7,6 +7,7 @@ //= require handlebars //= require ember.debug //= require ember-template-compiler +//= require qunit/qunit/qunit //= require ember-qunit //= require ember-shim //= require wizard-application diff --git a/app/views/qunit/index.html.erb b/app/views/qunit/index.html.erb index 1ada1ae163..0515dfc0d6 100644 --- a/app/views/qunit/index.html.erb +++ b/app/views/qunit/index.html.erb @@ -2,9 +2,7 @@ QUnit Test Runner - <%= stylesheet_link_tag "qunit" %> <%= stylesheet_link_tag "test_helper" %> - <%= javascript_include_tag "qunit" %> <%= javascript_include_tag "test_helper" %> <%= csrf_meta_tags %> diff --git a/app/views/wizard/qunit.html.erb b/app/views/wizard/qunit.html.erb index 31755ff4d8..02c510ae8c 100644 --- a/app/views/wizard/qunit.html.erb +++ b/app/views/wizard/qunit.html.erb @@ -2,10 +2,8 @@ QUnit Test Runner - <%= stylesheet_link_tag "qunit" %> <%= stylesheet_link_tag "test_helper" %> <%= discourse_stylesheet_link_tag :wizard, theme_ids: nil %> - <%= javascript_include_tag "qunit" %> <%= javascript_include_tag "wizard/test/test_helper" %> <%= csrf_meta_tags %> diff --git a/package.json b/package.json index 3a1322b0f5..6f55039c66 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "eslint": "^4.19", "prettier": "^1.13", "puppeteer": "1.4", - "pretender": "^1.6" + "pretender": "^1.6", + "qunit": "2.6" } } diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 0e0f819935..42e119cd06 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -8,6 +8,7 @@ //= require ember.debug //= require ember-template-compiler //= require message-bus +//= require qunit/qunit/qunit //= require ember-qunit //= require fake_xml_http_request //= require route-recognizer diff --git a/test/stylesheets/test_helper.css b/test/stylesheets/test_helper.css index 4e8f83a6b9..4d4cef5863 100644 --- a/test/stylesheets/test_helper.css +++ b/test/stylesheets/test_helper.css @@ -1,4 +1,5 @@ @import '/stylesheets/desktop.css'; +@import 'qunit/qunit/qunit.css'; .modal-backdrop { display: none; diff --git a/vendor/assets/javascripts/qunit.js b/vendor/assets/javascripts/qunit.js deleted file mode 100644 index 3cda99631a..0000000000 --- a/vendor/assets/javascripts/qunit.js +++ /dev/null @@ -1,4926 +0,0 @@ -/*! - * QUnit 2.3.3 - * https://qunitjs.com/ - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2017-06-02T14:07Z - */ -(function (global$1) { - 'use strict'; - - global$1 = 'default' in global$1 ? global$1['default'] : global$1; - - var window = global$1.window; - var self$1 = global$1.self; - var console = global$1.console; - var setTimeout = global$1.setTimeout; - var clearTimeout = global$1.clearTimeout; - - var document = window && window.document; - var navigator = window && window.navigator; - - var localSessionStorage = function () { - var x = "qunit-test-string"; - try { - global$1.sessionStorage.setItem(x, x); - global$1.sessionStorage.removeItem(x); - return global$1.sessionStorage; - } catch (e) { - return undefined; - } - }(); - - var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { - return typeof obj; - } : function (obj) { - return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; - }; - - - - - - - - - - - - var classCallCheck = function (instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - }; - - var createClass = function () { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - return function (Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; - }(); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var toConsumableArray = function (arr) { - if (Array.isArray(arr)) { - for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; - - return arr2; - } else { - return Array.from(arr); - } - }; - - var toString = Object.prototype.toString; - var hasOwn = Object.prototype.hasOwnProperty; - var now = Date.now || function () { - return new Date().getTime(); - }; - - var defined = { - document: window && window.document !== undefined, - setTimeout: setTimeout !== undefined - }; - - // Returns a new Array with the elements that are in a but not in b - function diff(a, b) { - var i, - j, - result = a.slice(); - - for (i = 0; i < result.length; i++) { - for (j = 0; j < b.length; j++) { - if (result[i] === b[j]) { - result.splice(i, 1); - i--; - break; - } - } - } - return result; - } - - /** - * Determines whether an element exists in a given array or not. - * - * @method inArray - * @param {Any} elem - * @param {Array} array - * @return {Boolean} - */ - function inArray(elem, array) { - return array.indexOf(elem) !== -1; - } - - /** - * Makes a clone of an object using only Array or Object as base, - * and copies over the own enumerable properties. - * - * @param {Object} obj - * @return {Object} New object with only the own properties (recursively). - */ - function objectValues(obj) { - var key, - val, - vals = is("array", obj) ? [] : {}; - for (key in obj) { - if (hasOwn.call(obj, key)) { - val = obj[key]; - vals[key] = val === Object(val) ? objectValues(val) : val; - } - } - return vals; - } - - function extend(a, b, undefOnly) { - for (var prop in b) { - if (hasOwn.call(b, prop)) { - if (b[prop] === undefined) { - delete a[prop]; - } else if (!(undefOnly && typeof a[prop] !== "undefined")) { - a[prop] = b[prop]; - } - } - } - - return a; - } - - function objectType(obj) { - if (typeof obj === "undefined") { - return "undefined"; - } - - // Consider: typeof null === object - if (obj === null) { - return "null"; - } - - var match = toString.call(obj).match(/^\[object\s(.*)\]$/), - type = match && match[1]; - - switch (type) { - case "Number": - if (isNaN(obj)) { - return "nan"; - } - return "number"; - case "String": - case "Boolean": - case "Array": - case "Set": - case "Map": - case "Date": - case "RegExp": - case "Function": - case "Symbol": - return type.toLowerCase(); - } - - if ((typeof obj === "undefined" ? "undefined" : _typeof(obj)) === "object") { - return "object"; - } - } - - // Safe object type checking - function is(type, obj) { - return objectType(obj) === type; - } - - // Based on Java's String.hashCode, a simple but not - // rigorously collision resistant hashing function - function generateHash(module, testName) { - var str = module + "\x1C" + testName; - var hash = 0; - - for (var i = 0; i < str.length; i++) { - hash = (hash << 5) - hash + str.charCodeAt(i); - hash |= 0; - } - - // Convert the possibly negative integer hash code into an 8 character hex string, which isn't - // strictly necessary but increases user understanding that the id is a SHA-like hash - var hex = (0x100000000 + hash).toString(16); - if (hex.length < 8) { - hex = "0000000" + hex; - } - - return hex.slice(-8); - } - - // Test for equality any JavaScript type. - // Authors: Philippe Rathé , David Chan - var equiv = (function () { - - // Value pairs queued for comparison. Used for breadth-first processing order, recursion - // detection and avoiding repeated comparison (see below for details). - // Elements are { a: val, b: val }. - var pairs = []; - - var getProto = Object.getPrototypeOf || function (obj) { - return obj.__proto__; - }; - - function useStrictEquality(a, b) { - - // This only gets called if a and b are not strict equal, and is used to compare on - // the primitive values inside object wrappers. For example: - // `var i = 1;` - // `var j = new Number(1);` - // Neither a nor b can be null, as a !== b and they have the same type. - if ((typeof a === "undefined" ? "undefined" : _typeof(a)) === "object") { - a = a.valueOf(); - } - if ((typeof b === "undefined" ? "undefined" : _typeof(b)) === "object") { - b = b.valueOf(); - } - - return a === b; - } - - function compareConstructors(a, b) { - var protoA = getProto(a); - var protoB = getProto(b); - - // Comparing constructors is more strict than using `instanceof` - if (a.constructor === b.constructor) { - return true; - } - - // Ref #851 - // If the obj prototype descends from a null constructor, treat it - // as a null prototype. - if (protoA && protoA.constructor === null) { - protoA = null; - } - if (protoB && protoB.constructor === null) { - protoB = null; - } - - // Allow objects with no prototype to be equivalent to - // objects with Object as their constructor. - if (protoA === null && protoB === Object.prototype || protoB === null && protoA === Object.prototype) { - return true; - } - - return false; - } - - function getRegExpFlags(regexp) { - return "flags" in regexp ? regexp.flags : regexp.toString().match(/[gimuy]*$/)[0]; - } - - function isContainer(val) { - return ["object", "array", "map", "set"].indexOf(objectType(val)) !== -1; - } - - function breadthFirstCompareChild(a, b) { - - // If a is a container not reference-equal to b, postpone the comparison to the - // end of the pairs queue -- unless (a, b) has been seen before, in which case skip - // over the pair. - if (a === b) { - return true; - } - if (!isContainer(a)) { - return typeEquiv(a, b); - } - if (pairs.every(function (pair) { - return pair.a !== a || pair.b !== b; - })) { - - // Not yet started comparing this pair - pairs.push({ a: a, b: b }); - } - return true; - } - - var callbacks = { - "string": useStrictEquality, - "boolean": useStrictEquality, - "number": useStrictEquality, - "null": useStrictEquality, - "undefined": useStrictEquality, - "symbol": useStrictEquality, - "date": useStrictEquality, - - "nan": function nan() { - return true; - }, - - "regexp": function regexp(a, b) { - return a.source === b.source && - - // Include flags in the comparison - getRegExpFlags(a) === getRegExpFlags(b); - }, - - // abort (identical references / instance methods were skipped earlier) - "function": function _function() { - return false; - }, - - "array": function array(a, b) { - var i, len; - - len = a.length; - if (len !== b.length) { - - // Safe and faster - return false; - } - - for (i = 0; i < len; i++) { - - // Compare non-containers; queue non-reference-equal containers - if (!breadthFirstCompareChild(a[i], b[i])) { - return false; - } - } - return true; - }, - - // Define sets a and b to be equivalent if for each element aVal in a, there - // is some element bVal in b such that aVal and bVal are equivalent. Element - // repetitions are not counted, so these are equivalent: - // a = new Set( [ {}, [], [] ] ); - // b = new Set( [ {}, {}, [] ] ); - "set": function set$$1(a, b) { - var innerEq, - outerEq = true; - - if (a.size !== b.size) { - - // This optimization has certain quirks because of the lack of - // repetition counting. For instance, adding the same - // (reference-identical) element to two equivalent sets can - // make them non-equivalent. - return false; - } - - a.forEach(function (aVal) { - - // Short-circuit if the result is already known. (Using for...of - // with a break clause would be cleaner here, but it would cause - // a syntax error on older Javascript implementations even if - // Set is unused) - if (!outerEq) { - return; - } - - innerEq = false; - - b.forEach(function (bVal) { - var parentPairs; - - // Likewise, short-circuit if the result is already known - if (innerEq) { - return; - } - - // Swap out the global pairs list, as the nested call to - // innerEquiv will clobber its contents - parentPairs = pairs; - if (innerEquiv(bVal, aVal)) { - innerEq = true; - } - - // Replace the global pairs list - pairs = parentPairs; - }); - - if (!innerEq) { - outerEq = false; - } - }); - - return outerEq; - }, - - // Define maps a and b to be equivalent if for each key-value pair (aKey, aVal) - // in a, there is some key-value pair (bKey, bVal) in b such that - // [ aKey, aVal ] and [ bKey, bVal ] are equivalent. Key repetitions are not - // counted, so these are equivalent: - // a = new Map( [ [ {}, 1 ], [ {}, 1 ], [ [], 1 ] ] ); - // b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] ); - "map": function map(a, b) { - var innerEq, - outerEq = true; - - if (a.size !== b.size) { - - // This optimization has certain quirks because of the lack of - // repetition counting. For instance, adding the same - // (reference-identical) key-value pair to two equivalent maps - // can make them non-equivalent. - return false; - } - - a.forEach(function (aVal, aKey) { - - // Short-circuit if the result is already known. (Using for...of - // with a break clause would be cleaner here, but it would cause - // a syntax error on older Javascript implementations even if - // Map is unused) - if (!outerEq) { - return; - } - - innerEq = false; - - b.forEach(function (bVal, bKey) { - var parentPairs; - - // Likewise, short-circuit if the result is already known - if (innerEq) { - return; - } - - // Swap out the global pairs list, as the nested call to - // innerEquiv will clobber its contents - parentPairs = pairs; - if (innerEquiv([bVal, bKey], [aVal, aKey])) { - innerEq = true; - } - - // Replace the global pairs list - pairs = parentPairs; - }); - - if (!innerEq) { - outerEq = false; - } - }); - - return outerEq; - }, - - "object": function object(a, b) { - var i, - aProperties = [], - bProperties = []; - - if (compareConstructors(a, b) === false) { - return false; - } - - // Be strict: don't ensure hasOwnProperty and go deep - for (i in a) { - - // Collect a's properties - aProperties.push(i); - - // Skip OOP methods that look the same - if (a.constructor !== Object && typeof a.constructor !== "undefined" && typeof a[i] === "function" && typeof b[i] === "function" && a[i].toString() === b[i].toString()) { - continue; - } - - // Compare non-containers; queue non-reference-equal containers - if (!breadthFirstCompareChild(a[i], b[i])) { - return false; - } - } - - for (i in b) { - - // Collect b's properties - bProperties.push(i); - } - - // Ensures identical properties name - return typeEquiv(aProperties.sort(), bProperties.sort()); - } - }; - - function typeEquiv(a, b) { - var type = objectType(a); - - // Callbacks for containers will append to the pairs queue to achieve breadth-first - // search order. The pairs queue is also used to avoid reprocessing any pair of - // containers that are reference-equal to a previously visited pair (a special case - // this being recursion detection). - // - // Because of this approach, once typeEquiv returns a false value, it should not be - // called again without clearing the pair queue else it may wrongly report a visited - // pair as being equivalent. - return objectType(b) === type && callbacks[type](a, b); - } - - function innerEquiv(a, b) { - var i, pair; - - // We're done when there's nothing more to compare - if (arguments.length < 2) { - return true; - } - - // Clear the global pair queue and add the top-level values being compared - pairs = [{ a: a, b: b }]; - - for (i = 0; i < pairs.length; i++) { - pair = pairs[i]; - - // Perform type-specific comparison on any pairs that are not strictly - // equal. For container types, that comparison will postpone comparison - // of any sub-container pair to the end of the pair queue. This gives - // breadth-first search order. It also avoids the reprocessing of - // reference-equal siblings, cousins etc, which can have a significant speed - // impact when comparing a container of small objects each of which has a - // reference to the same (singleton) large object. - if (pair.a !== pair.b && !typeEquiv(pair.a, pair.b)) { - return false; - } - } - - // ...across all consecutive argument pairs - return arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1)); - } - - return innerEquiv; - })(); - - /** - * Config object: Maintain internal state - * Later exposed as QUnit.config - * `config` initialized at top of scope - */ - var config = { - - // The queue of tests to run - queue: [], - - // Block until document ready - blocking: true, - - // By default, run previously failed tests first - // very useful in combination with "Hide passed tests" checked - reorder: true, - - // By default, modify document.title when suite is done - altertitle: true, - - // HTML Reporter: collapse every test except the first failing test - // If false, all failing tests will be expanded - collapse: true, - - // By default, scroll to top of the page when suite is done - scrolltop: true, - - // Depth up-to which object will be dumped - maxDepth: 5, - - // When enabled, all tests must call expect() - requireExpects: false, - - // Placeholder for user-configurable form-exposed URL parameters - urlConfig: [], - - // Set of all modules. - modules: [], - - // The first unnamed module - currentModule: { - name: "", - tests: [], - childModules: [], - testsRun: 0, - unskippedTestsRun: 0 - }, - - callbacks: {}, - - // The storage module to use for reordering tests - storage: localSessionStorage - }; - - // take a predefined QUnit.config and extend the defaults - var globalConfig = window && window.QUnit && window.QUnit.config; - - // only extend the global config if there is no QUnit overload - if (window && window.QUnit && !window.QUnit.version) { - extend(config, globalConfig); - } - - // Push a loose unnamed module to the modules collection - config.modules.push(config.currentModule); - - // Based on jsDump by Ariel Flesler - // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html - var dump = (function () { - function quote(str) { - return "\"" + str.toString().replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\""; - } - function literal(o) { - return o + ""; - } - function join(pre, arr, post) { - var s = dump.separator(), - base = dump.indent(), - inner = dump.indent(1); - if (arr.join) { - arr = arr.join("," + s + inner); - } - if (!arr) { - return pre + post; - } - return [pre, inner + arr, base + post].join(s); - } - function array(arr, stack) { - var i = arr.length, - ret = new Array(i); - - if (dump.maxDepth && dump.depth > dump.maxDepth) { - return "[object Array]"; - } - - this.up(); - while (i--) { - ret[i] = this.parse(arr[i], undefined, stack); - } - this.down(); - return join("[", ret, "]"); - } - - function isArray(obj) { - return ( - - //Native Arrays - toString.call(obj) === "[object Array]" || - - // NodeList objects - typeof obj.length === "number" && obj.item !== undefined && (obj.length ? obj.item(0) === obj[0] : obj.item(0) === null && obj[0] === undefined) - ); - } - - var reName = /^function (\w+)/, - dump = { - - // The objType is used mostly internally, you can fix a (custom) type in advance - parse: function parse(obj, objType, stack) { - stack = stack || []; - var res, - parser, - parserType, - objIndex = stack.indexOf(obj); - - if (objIndex !== -1) { - return "recursion(" + (objIndex - stack.length) + ")"; - } - - objType = objType || this.typeOf(obj); - parser = this.parsers[objType]; - parserType = typeof parser === "undefined" ? "undefined" : _typeof(parser); - - if (parserType === "function") { - stack.push(obj); - res = parser.call(this, obj, stack); - stack.pop(); - return res; - } - return parserType === "string" ? parser : this.parsers.error; - }, - typeOf: function typeOf(obj) { - var type; - - if (obj === null) { - type = "null"; - } else if (typeof obj === "undefined") { - type = "undefined"; - } else if (is("regexp", obj)) { - type = "regexp"; - } else if (is("date", obj)) { - type = "date"; - } else if (is("function", obj)) { - type = "function"; - } else if (obj.setInterval !== undefined && obj.document !== undefined && obj.nodeType === undefined) { - type = "window"; - } else if (obj.nodeType === 9) { - type = "document"; - } else if (obj.nodeType) { - type = "node"; - } else if (isArray(obj)) { - type = "array"; - } else if (obj.constructor === Error.prototype.constructor) { - type = "error"; - } else { - type = typeof obj === "undefined" ? "undefined" : _typeof(obj); - } - return type; - }, - - separator: function separator() { - if (this.multiline) { - return this.HTML ? "
" : "\n"; - } else { - return this.HTML ? " " : " "; - } - }, - - // Extra can be a number, shortcut for increasing-calling-decreasing - indent: function indent(extra) { - if (!this.multiline) { - return ""; - } - var chr = this.indentChar; - if (this.HTML) { - chr = chr.replace(/\t/g, " ").replace(/ /g, " "); - } - return new Array(this.depth + (extra || 0)).join(chr); - }, - up: function up(a) { - this.depth += a || 1; - }, - down: function down(a) { - this.depth -= a || 1; - }, - setParser: function setParser(name, parser) { - this.parsers[name] = parser; - }, - - // The next 3 are exposed so you can use them - quote: quote, - literal: literal, - join: join, - depth: 1, - maxDepth: config.maxDepth, - - // This is the list of parsers, to modify them, use dump.setParser - parsers: { - window: "[Window]", - document: "[Document]", - error: function error(_error) { - return "Error(\"" + _error.message + "\")"; - }, - unknown: "[Unknown]", - "null": "null", - "undefined": "undefined", - "function": function _function(fn) { - var ret = "function", - - - // Functions never have name in IE - name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; - - if (name) { - ret += " " + name; - } - ret += "("; - - ret = [ret, dump.parse(fn, "functionArgs"), "){"].join(""); - return join(ret, dump.parse(fn, "functionCode"), "}"); - }, - array: array, - nodelist: array, - "arguments": array, - object: function object(map, stack) { - var keys, - key, - val, - i, - nonEnumerableProperties, - ret = []; - - if (dump.maxDepth && dump.depth > dump.maxDepth) { - return "[object Object]"; - } - - dump.up(); - keys = []; - for (key in map) { - keys.push(key); - } - - // Some properties are not always enumerable on Error objects. - nonEnumerableProperties = ["message", "name"]; - for (i in nonEnumerableProperties) { - key = nonEnumerableProperties[i]; - if (key in map && !inArray(key, keys)) { - keys.push(key); - } - } - keys.sort(); - for (i = 0; i < keys.length; i++) { - key = keys[i]; - val = map[key]; - ret.push(dump.parse(key, "key") + ": " + dump.parse(val, undefined, stack)); - } - dump.down(); - return join("{", ret, "}"); - }, - node: function node(_node) { - var len, - i, - val, - open = dump.HTML ? "<" : "<", - close = dump.HTML ? ">" : ">", - tag = _node.nodeName.toLowerCase(), - ret = open + tag, - attrs = _node.attributes; - - if (attrs) { - for (i = 0, len = attrs.length; i < len; i++) { - val = attrs[i].nodeValue; - - // IE6 includes all attributes in .attributes, even ones not explicitly - // set. Those have values like undefined, null, 0, false, "" or - // "inherit". - if (val && val !== "inherit") { - ret += " " + attrs[i].nodeName + "=" + dump.parse(val, "attribute"); - } - } - } - ret += close; - - // Show content of TextNode or CDATASection - if (_node.nodeType === 3 || _node.nodeType === 4) { - ret += _node.nodeValue; - } - - return ret + open + "/" + tag + close; - }, - - // Function calls it internally, it's the arguments part of the function - functionArgs: function functionArgs(fn) { - var args, - l = fn.length; - - if (!l) { - return ""; - } - - args = new Array(l); - while (l--) { - - // 97 is 'a' - args[l] = String.fromCharCode(97 + l); - } - return " " + args.join(", ") + " "; - }, - - // Object calls it internally, the key part of an item in a map - key: quote, - - // Function calls it internally, it's the content of the function - functionCode: "[code]", - - // Node calls it internally, it's a html attribute value - attribute: quote, - string: quote, - date: quote, - regexp: literal, - number: literal, - "boolean": literal, - symbol: function symbol(sym) { - return sym.toString(); - } - }, - - // If true, entities are escaped ( <, >, \t, space and \n ) - HTML: false, - - // Indentation unit - indentChar: " ", - - // If true, items in a collection, are separated by a \n, else just a space. - multiline: true - }; - - return dump; - })(); - - var LISTENERS = Object.create(null); - var SUPPORTED_EVENTS = ["runStart", "suiteStart", "testStart", "assertion", "testEnd", "suiteEnd", "runEnd"]; - - /** - * Emits an event with the specified data to all currently registered listeners. - * Callbacks will fire in the order in which they are registered (FIFO). This - * function is not exposed publicly; it is used by QUnit internals to emit - * logging events. - * - * @private - * @method emit - * @param {String} eventName - * @param {Object} data - * @return {Void} - */ - function emit(eventName, data) { - if (objectType(eventName) !== "string") { - throw new TypeError("eventName must be a string when emitting an event"); - } - - // Clone the callbacks in case one of them registers a new callback - var originalCallbacks = LISTENERS[eventName]; - var callbacks = originalCallbacks ? [].concat(toConsumableArray(originalCallbacks)) : []; - - for (var i = 0; i < callbacks.length; i++) { - callbacks[i](data); - } - } - - /** - * Registers a callback as a listener to the specified event. - * - * @public - * @method on - * @param {String} eventName - * @param {Function} callback - * @return {Void} - */ - function on(eventName, callback) { - if (objectType(eventName) !== "string") { - throw new TypeError("eventName must be a string when registering a listener"); - } else if (!inArray(eventName, SUPPORTED_EVENTS)) { - var events = SUPPORTED_EVENTS.join(", "); - throw new Error("\"" + eventName + "\" is not a valid event; must be one of: " + events + "."); - } else if (objectType(callback) !== "function") { - throw new TypeError("callback must be a function when registering a listener"); - } - - if (!LISTENERS[eventName]) { - LISTENERS[eventName] = []; - } - - // Don't register the same callback more than once - if (!inArray(callback, LISTENERS[eventName])) { - LISTENERS[eventName].push(callback); - } - } - - // Register logging callbacks - function registerLoggingCallbacks(obj) { - var i, - l, - key, - callbackNames = ["begin", "done", "log", "testStart", "testDone", "moduleStart", "moduleDone"]; - - function registerLoggingCallback(key) { - var loggingCallback = function loggingCallback(callback) { - if (objectType(callback) !== "function") { - throw new Error("QUnit logging methods require a callback function as their first parameters."); - } - - config.callbacks[key].push(callback); - }; - - return loggingCallback; - } - - for (i = 0, l = callbackNames.length; i < l; i++) { - key = callbackNames[i]; - - // Initialize key collection of logging callback - if (objectType(config.callbacks[key]) === "undefined") { - config.callbacks[key] = []; - } - - obj[key] = registerLoggingCallback(key); - } - } - - function runLoggingCallbacks(key, args) { - var i, l, callbacks; - - callbacks = config.callbacks[key]; - for (i = 0, l = callbacks.length; i < l; i++) { - callbacks[i](args); - } - } - - // Doesn't support IE9, it will return undefined on these browsers - // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack - var fileName = (sourceFromStacktrace(0) || "").replace(/(:\d+)+\)?/, "").replace(/.+\//, ""); - - function extractStacktrace(e, offset) { - offset = offset === undefined ? 4 : offset; - - var stack, include, i; - - if (e && e.stack) { - stack = e.stack.split("\n"); - if (/^error$/i.test(stack[0])) { - stack.shift(); - } - if (fileName) { - include = []; - for (i = offset; i < stack.length; i++) { - if (stack[i].indexOf(fileName) !== -1) { - break; - } - include.push(stack[i]); - } - if (include.length) { - return include.join("\n"); - } - } - return stack[offset]; - } - } - - function sourceFromStacktrace(offset) { - var error = new Error(); - - // Support: Safari <=7 only, IE <=10 - 11 only - // Not all browsers generate the `stack` property for `new Error()`, see also #636 - if (!error.stack) { - try { - throw error; - } catch (err) { - error = err; - } - } - - return extractStacktrace(error, offset); - } - - var priorityCount = 0; - var unitSampler = void 0; - - /** - * Advances the ProcessingQueue to the next item if it is ready. - * @param {Boolean} last - */ - function advance() { - var start = now(); - config.depth = (config.depth || 0) + 1; - - while (config.queue.length && !config.blocking) { - var elapsedTime = now() - start; - - if (!defined.setTimeout || config.updateRate <= 0 || elapsedTime < config.updateRate) { - if (priorityCount > 0) { - priorityCount--; - } - - config.queue.shift()(); - } else { - setTimeout(advance, 13); - break; - } - } - - config.depth--; - - if (!config.blocking && !config.queue.length && config.depth === 0) { - done(); - } - } - - function addToQueueImmediate(callback) { - if (objectType(callback) === "array") { - while (callback.length) { - addToQueueImmediate(callback.pop()); - } - - return; - } - - config.queue.unshift(callback); - priorityCount++; - } - - /** - * Adds a function to the ProcessingQueue for execution. - * @param {Function|Array} callback - * @param {Boolean} priority - * @param {String} seed - */ - function addToQueue(callback, prioritize, seed) { - if (prioritize) { - config.queue.splice(priorityCount++, 0, callback); - } else if (seed) { - if (!unitSampler) { - unitSampler = unitSamplerGenerator(seed); - } - - // Insert into a random position after all prioritized items - var index = Math.floor(unitSampler() * (config.queue.length - priorityCount + 1)); - config.queue.splice(priorityCount + index, 0, callback); - } else { - config.queue.push(callback); - } - } - - /** - * Creates a seeded "sample" generator which is used for randomizing tests. - */ - function unitSamplerGenerator(seed) { - - // 32-bit xorshift, requires only a nonzero seed - // http://excamera.com/sphinx/article-xorshift.html - var sample = parseInt(generateHash(seed), 16) || -1; - return function () { - sample ^= sample << 13; - sample ^= sample >>> 17; - sample ^= sample << 5; - - // ECMAScript has no unsigned number type - if (sample < 0) { - sample += 0x100000000; - } - - return sample / 0x100000000; - }; - } - - /** - * This function is called when the ProcessingQueue is done processing all - * items. It handles emitting the final run events. - */ - function done() { - var storage = config.storage; - - ProcessingQueue.finished = true; - - var runtime = now() - config.started; - var passed = config.stats.all - config.stats.bad; - - emit("runEnd", globalSuite.end(true)); - runLoggingCallbacks("done", { - passed: passed, - failed: config.stats.bad, - total: config.stats.all, - runtime: runtime - }); - - // Clear own storage items if all tests passed - if (storage && config.stats.bad === 0) { - for (var i = storage.length - 1; i >= 0; i--) { - var key = storage.key(i); - - if (key.indexOf("qunit-test-") === 0) { - storage.removeItem(key); - } - } - } - } - - var ProcessingQueue = { - finished: false, - add: addToQueue, - addImmediate: addToQueueImmediate, - advance: advance - }; - - var TestReport = function () { - function TestReport(name, suite, options) { - classCallCheck(this, TestReport); - - this.name = name; - this.suiteName = suite.name; - this.fullName = suite.fullName.concat(name); - this.runtime = 0; - this.assertions = []; - - this.skipped = !!options.skip; - this.todo = !!options.todo; - - this.valid = options.valid; - - this._startTime = 0; - this._endTime = 0; - - suite.pushTest(this); - } - - createClass(TestReport, [{ - key: "start", - value: function start(recordTime) { - if (recordTime) { - this._startTime = Date.now(); - } - - return { - name: this.name, - suiteName: this.suiteName, - fullName: this.fullName.slice() - }; - } - }, { - key: "end", - value: function end(recordTime) { - if (recordTime) { - this._endTime = Date.now(); - } - - return extend(this.start(), { - runtime: this.getRuntime(), - status: this.getStatus(), - errors: this.getFailedAssertions(), - assertions: this.getAssertions() - }); - } - }, { - key: "pushAssertion", - value: function pushAssertion(assertion) { - this.assertions.push(assertion); - } - }, { - key: "getRuntime", - value: function getRuntime() { - return this._endTime - this._startTime; - } - }, { - key: "getStatus", - value: function getStatus() { - if (this.skipped) { - return "skipped"; - } - - var testPassed = this.getFailedAssertions().length > 0 ? this.todo : !this.todo; - - if (!testPassed) { - return "failed"; - } else if (this.todo) { - return "todo"; - } else { - return "passed"; - } - } - }, { - key: "getFailedAssertions", - value: function getFailedAssertions() { - return this.assertions.filter(function (assertion) { - return !assertion.passed; - }); - } - }, { - key: "getAssertions", - value: function getAssertions() { - return this.assertions.slice(); - } - - // Remove actual and expected values from assertions. This is to prevent - // leaking memory throughout a test suite. - - }, { - key: "slimAssertions", - value: function slimAssertions() { - this.assertions = this.assertions.map(function (assertion) { - delete assertion.actual; - delete assertion.expected; - return assertion; - }); - } - }]); - return TestReport; - }(); - - var focused = false; - - function Test(settings) { - var i, l; - - ++Test.count; - - this.expected = null; - extend(this, settings); - this.assertions = []; - this.semaphore = 0; - this.module = config.currentModule; - this.stack = sourceFromStacktrace(3); - this.steps = []; - - this.testReport = new TestReport(settings.testName, this.module.suiteReport, { - todo: settings.todo, - skip: settings.skip, - valid: this.valid() - }); - - // Register unique strings - for (i = 0, l = this.module.tests; i < l.length; i++) { - if (this.module.tests[i].name === this.testName) { - this.testName += " "; - } - } - - this.testId = generateHash(this.module.name, this.testName); - - this.module.tests.push({ - name: this.testName, - testId: this.testId, - skip: !!settings.skip - }); - - if (settings.skip) { - - // Skipped tests will fully ignore any sent callback - this.callback = function () {}; - this.async = false; - this.expected = 0; - } else { - this.assert = new Assert(this); - } - } - - Test.count = 0; - - function getNotStartedModules(startModule) { - var module = startModule, - modules = []; - - while (module && module.testsRun === 0) { - modules.push(module); - module = module.parentModule; - } - - return modules; - } - - Test.prototype = { - before: function before() { - var i, - startModule, - module = this.module, - notStartedModules = getNotStartedModules(module); - - for (i = notStartedModules.length - 1; i >= 0; i--) { - startModule = notStartedModules[i]; - startModule.stats = { all: 0, bad: 0, started: now() }; - emit("suiteStart", startModule.suiteReport.start(true)); - runLoggingCallbacks("moduleStart", { - name: startModule.name, - tests: startModule.tests - }); - } - - config.current = this; - - this.testEnvironment = extend({}, module.testEnvironment); - - this.started = now(); - emit("testStart", this.testReport.start(true)); - runLoggingCallbacks("testStart", { - name: this.testName, - module: module.name, - testId: this.testId, - previousFailure: this.previousFailure - }); - - if (!config.pollution) { - saveGlobal(); - } - }, - - run: function run() { - var promise; - - config.current = this; - - this.callbackStarted = now(); - - if (config.notrycatch) { - runTest(this); - return; - } - - try { - runTest(this); - } catch (e) { - this.pushFailure("Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + (e.message || e), extractStacktrace(e, 0)); - - // Else next test will carry the responsibility - saveGlobal(); - - // Restart the tests if they're blocking - if (config.blocking) { - internalRecover(this); - } - } - - function runTest(test) { - promise = test.callback.call(test.testEnvironment, test.assert); - test.resolvePromise(promise); - } - }, - - after: function after() { - checkPollution(); - }, - - queueHook: function queueHook(hook, hookName, hookOwner) { - var promise, - test = this; - return function runHook() { - if (hookName === "before") { - if (hookOwner.unskippedTestsRun !== 0) { - return; - } - - test.preserveEnvironment = true; - } - - if (hookName === "after" && hookOwner.unskippedTestsRun !== numberOfUnskippedTests(hookOwner) - 1 && config.queue.length > 2) { - return; - } - - config.current = test; - if (config.notrycatch) { - callHook(); - return; - } - try { - callHook(); - } catch (error) { - test.pushFailure(hookName + " failed on " + test.testName + ": " + (error.message || error), extractStacktrace(error, 0)); - } - - function callHook() { - promise = hook.call(test.testEnvironment, test.assert); - test.resolvePromise(promise, hookName); - } - }; - }, - - // Currently only used for module level hooks, can be used to add global level ones - hooks: function hooks(handler) { - var hooks = []; - - function processHooks(test, module) { - if (module.parentModule) { - processHooks(test, module.parentModule); - } - if (module.hooks && objectType(module.hooks[handler]) === "function") { - hooks.push(test.queueHook(module.hooks[handler], handler, module)); - } - } - - // Hooks are ignored on skipped tests - if (!this.skip) { - processHooks(this, this.module); - } - return hooks; - }, - - finish: function finish() { - config.current = this; - if (config.requireExpects && this.expected === null) { - this.pushFailure("Expected number of assertions to be defined, but expect() was " + "not called.", this.stack); - } else if (this.expected !== null && this.expected !== this.assertions.length) { - this.pushFailure("Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack); - } else if (this.expected === null && !this.assertions.length) { - this.pushFailure("Expected at least one assertion, but none were run - call " + "expect(0) to accept zero assertions.", this.stack); - } - - var i, - module = this.module, - moduleName = module.name, - testName = this.testName, - skipped = !!this.skip, - todo = !!this.todo, - bad = 0, - storage = config.storage; - - this.runtime = now() - this.started; - - config.stats.all += this.assertions.length; - module.stats.all += this.assertions.length; - - for (i = 0; i < this.assertions.length; i++) { - if (!this.assertions[i].result) { - bad++; - config.stats.bad++; - module.stats.bad++; - } - } - - notifyTestsRan(module, skipped); - - // Store result when possible - if (storage) { - if (bad) { - storage.setItem("qunit-test-" + moduleName + "-" + testName, bad); - } else { - storage.removeItem("qunit-test-" + moduleName + "-" + testName); - } - } - - // After emitting the js-reporters event we cleanup the assertion data to - // avoid leaking it. It is not used by the legacy testDone callbacks. - emit("testEnd", this.testReport.end(true)); - this.testReport.slimAssertions(); - - runLoggingCallbacks("testDone", { - name: testName, - module: moduleName, - skipped: skipped, - todo: todo, - failed: bad, - passed: this.assertions.length - bad, - total: this.assertions.length, - runtime: skipped ? 0 : this.runtime, - - // HTML Reporter use - assertions: this.assertions, - testId: this.testId, - - // Source of Test - source: this.stack - }); - - if (module.testsRun === numberOfTests(module)) { - logSuiteEnd(module); - - // Check if the parent modules, iteratively, are done. If that the case, - // we emit the `suiteEnd` event and trigger `moduleDone` callback. - var parent = module.parentModule; - while (parent && parent.testsRun === numberOfTests(parent)) { - logSuiteEnd(parent); - parent = parent.parentModule; - } - } - - config.current = undefined; - - function logSuiteEnd(module) { - emit("suiteEnd", module.suiteReport.end(true)); - runLoggingCallbacks("moduleDone", { - name: module.name, - tests: module.tests, - failed: module.stats.bad, - passed: module.stats.all - module.stats.bad, - total: module.stats.all, - runtime: now() - module.stats.started - }); - } - }, - - preserveTestEnvironment: function preserveTestEnvironment() { - if (this.preserveEnvironment) { - this.module.testEnvironment = this.testEnvironment; - this.testEnvironment = extend({}, this.module.testEnvironment); - } - }, - - queue: function queue() { - var test = this; - - if (!this.valid()) { - return; - } - - function runTest() { - - // Each of these can by async - ProcessingQueue.addImmediate([function () { - test.before(); - }, test.hooks("before"), function () { - test.preserveTestEnvironment(); - }, test.hooks("beforeEach"), function () { - test.run(); - }, test.hooks("afterEach").reverse(), test.hooks("after").reverse(), function () { - test.after(); - }, function () { - test.finish(); - }]); - } - - var previousFailCount = config.storage && +config.storage.getItem("qunit-test-" + this.module.name + "-" + this.testName); - - // Prioritize previously failed tests, detected from storage - var prioritize = config.reorder && !!previousFailCount; - - this.previousFailure = !!previousFailCount; - - ProcessingQueue.add(runTest, prioritize, config.seed); - - // If the queue has already finished, we manually process the new test - if (ProcessingQueue.finished) { - ProcessingQueue.advance(); - } - }, - - - pushResult: function pushResult(resultInfo) { - if (this !== config.current) { - throw new Error("Assertion occured after test had finished."); - } - - // Destructure of resultInfo = { result, actual, expected, message, negative } - var source, - details = { - module: this.module.name, - name: this.testName, - result: resultInfo.result, - message: resultInfo.message, - actual: resultInfo.actual, - expected: resultInfo.expected, - testId: this.testId, - negative: resultInfo.negative || false, - runtime: now() - this.started, - todo: !!this.todo - }; - - if (!resultInfo.result) { - source = resultInfo.source || sourceFromStacktrace(); - - if (source) { - details.source = source; - } - } - - this.logAssertion(details); - - this.assertions.push({ - result: !!resultInfo.result, - message: resultInfo.message - }); - }, - - pushFailure: function pushFailure(message, source, actual) { - if (!(this instanceof Test)) { - throw new Error("pushFailure() assertion outside test context, was " + sourceFromStacktrace(2)); - } - - this.pushResult({ - result: false, - message: message || "error", - actual: actual || null, - expected: null, - source: source - }); - }, - - /** - * Log assertion details using both the old QUnit.log interface and - * QUnit.on( "assertion" ) interface. - * - * @private - */ - logAssertion: function logAssertion(details) { - runLoggingCallbacks("log", details); - - var assertion = { - passed: details.result, - actual: details.actual, - expected: details.expected, - message: details.message, - stack: details.source, - todo: details.todo - }; - this.testReport.pushAssertion(assertion); - emit("assertion", assertion); - }, - - - resolvePromise: function resolvePromise(promise, phase) { - var then, - resume, - message, - test = this; - if (promise != null) { - then = promise.then; - if (objectType(then) === "function") { - resume = internalStop(test); - then.call(promise, function () { - resume(); - }, function (error) { - message = "Promise rejected " + (!phase ? "during" : phase.replace(/Each$/, "")) + " \"" + test.testName + "\": " + (error && error.message || error); - test.pushFailure(message, extractStacktrace(error, 0)); - - // Else next test will carry the responsibility - saveGlobal(); - - // Unblock - resume(); - }); - } - } - }, - - valid: function valid() { - var filter = config.filter, - regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec(filter), - module = config.module && config.module.toLowerCase(), - fullName = this.module.name + ": " + this.testName; - - function moduleChainNameMatch(testModule) { - var testModuleName = testModule.name ? testModule.name.toLowerCase() : null; - if (testModuleName === module) { - return true; - } else if (testModule.parentModule) { - return moduleChainNameMatch(testModule.parentModule); - } else { - return false; - } - } - - function moduleChainIdMatch(testModule) { - return inArray(testModule.moduleId, config.moduleId) || testModule.parentModule && moduleChainIdMatch(testModule.parentModule); - } - - // Internally-generated tests are always valid - if (this.callback && this.callback.validTest) { - return true; - } - - if (config.moduleId && config.moduleId.length > 0 && !moduleChainIdMatch(this.module)) { - - return false; - } - - if (config.testId && config.testId.length > 0 && !inArray(this.testId, config.testId)) { - - return false; - } - - if (module && !moduleChainNameMatch(this.module)) { - return false; - } - - if (!filter) { - return true; - } - - return regexFilter ? this.regexFilter(!!regexFilter[1], regexFilter[2], regexFilter[3], fullName) : this.stringFilter(filter, fullName); - }, - - regexFilter: function regexFilter(exclude, pattern, flags, fullName) { - var regex = new RegExp(pattern, flags); - var match = regex.test(fullName); - - return match !== exclude; - }, - - stringFilter: function stringFilter(filter, fullName) { - filter = filter.toLowerCase(); - fullName = fullName.toLowerCase(); - - var include = filter.charAt(0) !== "!"; - if (!include) { - filter = filter.slice(1); - } - - // If the filter matches, we need to honour include - if (fullName.indexOf(filter) !== -1) { - return include; - } - - // Otherwise, do the opposite - return !include; - } - }; - - function pushFailure() { - if (!config.current) { - throw new Error("pushFailure() assertion outside test context, in " + sourceFromStacktrace(2)); - } - - // Gets current test obj - var currentTest = config.current; - - return currentTest.pushFailure.apply(currentTest, arguments); - } - - function saveGlobal() { - config.pollution = []; - - if (config.noglobals) { - for (var key in global$1) { - if (hasOwn.call(global$1, key)) { - - // In Opera sometimes DOM element ids show up here, ignore them - if (/^qunit-test-output/.test(key)) { - continue; - } - config.pollution.push(key); - } - } - } - } - - function checkPollution() { - var newGlobals, - deletedGlobals, - old = config.pollution; - - saveGlobal(); - - newGlobals = diff(config.pollution, old); - if (newGlobals.length > 0) { - pushFailure("Introduced global variable(s): " + newGlobals.join(", ")); - } - - deletedGlobals = diff(old, config.pollution); - if (deletedGlobals.length > 0) { - pushFailure("Deleted global variable(s): " + deletedGlobals.join(", ")); - } - } - - // Will be exposed as QUnit.test - function test(testName, callback) { - if (focused) { - return; - } - - var newTest = new Test({ - testName: testName, - callback: callback - }); - - newTest.queue(); - } - - function todo(testName, callback) { - if (focused) { - return; - } - - var newTest = new Test({ - testName: testName, - callback: callback, - todo: true - }); - - newTest.queue(); - } - - // Will be exposed as QUnit.skip - function skip(testName) { - if (focused) { - return; - } - - var test = new Test({ - testName: testName, - skip: true - }); - - test.queue(); - } - - // Will be exposed as QUnit.only - function only(testName, callback) { - if (focused) { - return; - } - - config.queue.length = 0; - focused = true; - - var newTest = new Test({ - testName: testName, - callback: callback - }); - - newTest.queue(); - } - - // Put a hold on processing and return a function that will release it. - function internalStop(test) { - var released = false; - - test.semaphore += 1; - config.blocking = true; - - // Set a recovery timeout, if so configured. - if (config.testTimeout && defined.setTimeout) { - clearTimeout(config.timeout); - config.timeout = setTimeout(function () { - pushFailure("Test timed out", sourceFromStacktrace(2)); - internalRecover(test); - }, config.testTimeout); - } - - return function resume() { - if (released) { - return; - } - - released = true; - test.semaphore -= 1; - internalStart(test); - }; - } - - // Forcefully release all processing holds. - function internalRecover(test) { - test.semaphore = 0; - internalStart(test); - } - - // Release a processing hold, scheduling a resumption attempt if no holds remain. - function internalStart(test) { - - // If semaphore is non-numeric, throw error - if (isNaN(test.semaphore)) { - test.semaphore = 0; - - pushFailure("Invalid value on test.semaphore", sourceFromStacktrace(2)); - return; - } - - // Don't start until equal number of stop-calls - if (test.semaphore > 0) { - return; - } - - // Throw an Error if start is called more often than stop - if (test.semaphore < 0) { - test.semaphore = 0; - - pushFailure("Tried to restart test while already started (test's semaphore was 0 already)", sourceFromStacktrace(2)); - return; - } - - // Add a slight delay to allow more assertions etc. - if (defined.setTimeout) { - if (config.timeout) { - clearTimeout(config.timeout); - } - config.timeout = setTimeout(function () { - if (test.semaphore > 0) { - return; - } - - if (config.timeout) { - clearTimeout(config.timeout); - } - - begin(); - }, 13); - } else { - begin(); - } - } - - function collectTests(module) { - var tests = [].concat(module.tests); - var modules = [].concat(toConsumableArray(module.childModules)); - - // Do a breadth-first traversal of the child modules - while (modules.length) { - var nextModule = modules.shift(); - tests.push.apply(tests, nextModule.tests); - modules.push.apply(modules, toConsumableArray(nextModule.childModules)); - } - - return tests; - } - - function numberOfTests(module) { - return collectTests(module).length; - } - - function numberOfUnskippedTests(module) { - return collectTests(module).filter(function (test) { - return !test.skip; - }).length; - } - - function notifyTestsRan(module, skipped) { - module.testsRun++; - if (!skipped) { - module.unskippedTestsRun++; - } - while (module = module.parentModule) { - module.testsRun++; - if (!skipped) { - module.unskippedTestsRun++; - } - } - } - - /** - * Returns a function that proxies to the given method name on the globals - * console object. The proxy will also detect if the console doesn't exist and - * will appropriately no-op. This allows support for IE9, which doesn't have a - * console if the developer tools are not open. - */ - function consoleProxy(method) { - return function () { - if (console) { - console[method].apply(console, arguments); - } - }; - } - - var Logger = { - warn: consoleProxy("warn") - }; - - var Assert = function () { - function Assert(testContext) { - classCallCheck(this, Assert); - - this.test = testContext; - } - - // Assert helpers - - // Documents a "step", which is a string value, in a test as a passing assertion - - - createClass(Assert, [{ - key: "step", - value: function step(message) { - var result = !!message; - - this.test.steps.push(message); - - return this.pushResult({ - result: result, - message: message || "You must provide a message to assert.step" - }); - } - - // Verifies the steps in a test match a given array of string values - - }, { - key: "verifySteps", - value: function verifySteps(steps, message) { - this.deepEqual(this.test.steps, steps, message); - } - - // Specify the number of expected assertions to guarantee that failed test - // (no assertions are run at all) don't slip through. - - }, { - key: "expect", - value: function expect(asserts) { - if (arguments.length === 1) { - this.test.expected = asserts; - } else { - return this.test.expected; - } - } - - // Put a hold on processing and return a function that will release it a maximum of once. - - }, { - key: "async", - value: function async(count) { - var test$$1 = this.test; - - var popped = false, - acceptCallCount = count; - - if (typeof acceptCallCount === "undefined") { - acceptCallCount = 1; - } - - var resume = internalStop(test$$1); - - return function done() { - if (config.current !== test$$1) { - throw Error("assert.async callback called after test finished."); - } - - if (popped) { - test$$1.pushFailure("Too many calls to the `assert.async` callback", sourceFromStacktrace(2)); - return; - } - - acceptCallCount -= 1; - if (acceptCallCount > 0) { - return; - } - - popped = true; - resume(); - }; - } - - // Exports test.push() to the user API - // Alias of pushResult. - - }, { - key: "push", - value: function push(result, actual, expected, message, negative) { - Logger.warn("assert.push is deprecated and will be removed in QUnit 3.0." + " Please use assert.pushResult instead (http://api.qunitjs.com/pushResult/)."); - - var currentAssert = this instanceof Assert ? this : config.current.assert; - return currentAssert.pushResult({ - result: result, - actual: actual, - expected: expected, - message: message, - negative: negative - }); - } - }, { - key: "pushResult", - value: function pushResult(resultInfo) { - - // Destructure of resultInfo = { result, actual, expected, message, negative } - var assert = this; - var currentTest = assert instanceof Assert && assert.test || config.current; - - // Backwards compatibility fix. - // Allows the direct use of global exported assertions and QUnit.assert.* - // Although, it's use is not recommended as it can leak assertions - // to other tests from async tests, because we only get a reference to the current test, - // not exactly the test where assertion were intended to be called. - if (!currentTest) { - throw new Error("assertion outside test context, in " + sourceFromStacktrace(2)); - } - - if (!(assert instanceof Assert)) { - assert = currentTest.assert; - } - - return assert.test.pushResult(resultInfo); - } - }, { - key: "ok", - value: function ok(result, message) { - if (!message) { - message = result ? "okay" : "failed, expected argument to be truthy, was: " + dump.parse(result); - } - - this.pushResult({ - result: !!result, - actual: result, - expected: true, - message: message - }); - } - }, { - key: "notOk", - value: function notOk(result, message) { - if (!message) { - message = !result ? "okay" : "failed, expected argument to be falsy, was: " + dump.parse(result); - } - - this.pushResult({ - result: !result, - actual: result, - expected: false, - message: message - }); - } - }, { - key: "equal", - value: function equal(actual, expected, message) { - - // eslint-disable-next-line eqeqeq - var result = expected == actual; - - this.pushResult({ - result: result, - actual: actual, - expected: expected, - message: message - }); - } - }, { - key: "notEqual", - value: function notEqual(actual, expected, message) { - - // eslint-disable-next-line eqeqeq - var result = expected != actual; - - this.pushResult({ - result: result, - actual: actual, - expected: expected, - message: message, - negative: true - }); - } - }, { - key: "propEqual", - value: function propEqual(actual, expected, message) { - actual = objectValues(actual); - expected = objectValues(expected); - - this.pushResult({ - result: equiv(actual, expected), - actual: actual, - expected: expected, - message: message - }); - } - }, { - key: "notPropEqual", - value: function notPropEqual(actual, expected, message) { - actual = objectValues(actual); - expected = objectValues(expected); - - this.pushResult({ - result: !equiv(actual, expected), - actual: actual, - expected: expected, - message: message, - negative: true - }); - } - }, { - key: "deepEqual", - value: function deepEqual(actual, expected, message) { - this.pushResult({ - result: equiv(actual, expected), - actual: actual, - expected: expected, - message: message - }); - } - }, { - key: "notDeepEqual", - value: function notDeepEqual(actual, expected, message) { - this.pushResult({ - result: !equiv(actual, expected), - actual: actual, - expected: expected, - message: message, - negative: true - }); - } - }, { - key: "strictEqual", - value: function strictEqual(actual, expected, message) { - this.pushResult({ - result: expected === actual, - actual: actual, - expected: expected, - message: message - }); - } - }, { - key: "notStrictEqual", - value: function notStrictEqual(actual, expected, message) { - this.pushResult({ - result: expected !== actual, - actual: actual, - expected: expected, - message: message, - negative: true - }); - } - }, { - key: "throws", - value: function throws(block, expected, message) { - var actual = void 0, - result = false; - - var currentTest = this instanceof Assert && this.test || config.current; - - // 'expected' is optional unless doing string comparison - if (objectType(expected) === "string") { - if (message == null) { - message = expected; - expected = null; - } else { - throw new Error("throws/raises does not accept a string value for the expected argument.\n" + "Use a non-string object value (e.g. regExp) instead if it's necessary."); - } - } - - currentTest.ignoreGlobalErrors = true; - try { - block.call(currentTest.testEnvironment); - } catch (e) { - actual = e; - } - currentTest.ignoreGlobalErrors = false; - - if (actual) { - var expectedType = objectType(expected); - - // We don't want to validate thrown error - if (!expected) { - result = true; - expected = null; - - // Expected is a regexp - } else if (expectedType === "regexp") { - result = expected.test(errorString(actual)); - - // Expected is a constructor, maybe an Error constructor - } else if (expectedType === "function" && actual instanceof expected) { - result = true; - - // Expected is an Error object - } else if (expectedType === "object") { - result = actual instanceof expected.constructor && actual.name === expected.name && actual.message === expected.message; - - // Expected is a validation function which returns true if validation passed - } else if (expectedType === "function" && expected.call({}, actual) === true) { - expected = null; - result = true; - } - } - - currentTest.assert.pushResult({ - result: result, - actual: actual, - expected: expected, - message: message - }); - } - }]); - return Assert; - }(); - - // Provide an alternative to assert.throws(), for environments that consider throws a reserved word - // Known to us are: Closure Compiler, Narwhal - // eslint-disable-next-line dot-notation - - - Assert.prototype.raises = Assert.prototype["throws"]; - - /** - * Converts an error into a simple string for comparisons. - * - * @param {Error} error - * @return {String} - */ - function errorString(error) { - var resultErrorString = error.toString(); - - if (resultErrorString.substring(0, 7) === "[object") { - var name = error.name ? error.name.toString() : "Error"; - var message = error.message ? error.message.toString() : ""; - - if (name && message) { - return name + ": " + message; - } else if (name) { - return name; - } else if (message) { - return message; - } else { - return "Error"; - } - } else { - return resultErrorString; - } - } - - /* global module, exports, define */ - function exportQUnit(QUnit) { - - if (defined.document) { - - // QUnit may be defined when it is preconfigured but then only QUnit and QUnit.config may be defined. - if (window.QUnit && window.QUnit.version) { - throw new Error("QUnit has already been defined."); - } - - window.QUnit = QUnit; - } - - // For nodejs - if (typeof module !== "undefined" && module && module.exports) { - module.exports = QUnit; - - // For consistency with CommonJS environments' exports - module.exports.QUnit = QUnit; - } - - // For CommonJS with exports, but without module.exports, like Rhino - if (typeof exports !== "undefined" && exports) { - exports.QUnit = QUnit; - } - - if (typeof define === "function" && define.amd) { - define(function () { - return QUnit; - }); - QUnit.config.autostart = false; - } - - // For Web/Service Workers - if (self$1 && self$1.WorkerGlobalScope && self$1 instanceof self$1.WorkerGlobalScope) { - self$1.QUnit = QUnit; - } - } - - var SuiteReport = function () { - function SuiteReport(name, parentSuite) { - classCallCheck(this, SuiteReport); - - this.name = name; - this.fullName = parentSuite ? parentSuite.fullName.concat(name) : []; - - this.tests = []; - this.childSuites = []; - - if (parentSuite) { - parentSuite.pushChildSuite(this); - } - } - - createClass(SuiteReport, [{ - key: "start", - value: function start(recordTime) { - if (recordTime) { - this._startTime = Date.now(); - } - - return { - name: this.name, - fullName: this.fullName.slice(), - tests: this.tests.map(function (test) { - return test.start(); - }), - childSuites: this.childSuites.map(function (suite) { - return suite.start(); - }), - testCounts: { - total: this.getTestCounts().total - } - }; - } - }, { - key: "end", - value: function end(recordTime) { - if (recordTime) { - this._endTime = Date.now(); - } - - return { - name: this.name, - fullName: this.fullName.slice(), - tests: this.tests.map(function (test) { - return test.end(); - }), - childSuites: this.childSuites.map(function (suite) { - return suite.end(); - }), - testCounts: this.getTestCounts(), - runtime: this.getRuntime(), - status: this.getStatus() - }; - } - }, { - key: "pushChildSuite", - value: function pushChildSuite(suite) { - this.childSuites.push(suite); - } - }, { - key: "pushTest", - value: function pushTest(test) { - this.tests.push(test); - } - }, { - key: "getRuntime", - value: function getRuntime() { - return this._endTime - this._startTime; - } - }, { - key: "getTestCounts", - value: function getTestCounts() { - var counts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { passed: 0, failed: 0, skipped: 0, todo: 0, total: 0 }; - - counts = this.tests.reduce(function (counts, test) { - if (test.valid) { - counts[test.getStatus()]++; - counts.total++; - } - - return counts; - }, counts); - - return this.childSuites.reduce(function (counts, suite) { - return suite.getTestCounts(counts); - }, counts); - } - }, { - key: "getStatus", - value: function getStatus() { - var _getTestCounts = this.getTestCounts(), - total = _getTestCounts.total, - failed = _getTestCounts.failed, - skipped = _getTestCounts.skipped, - todo = _getTestCounts.todo; - - if (failed) { - return "failed"; - } else { - if (skipped === total) { - return "skipped"; - } else if (todo === total) { - return "todo"; - } else { - return "passed"; - } - } - } - }]); - return SuiteReport; - }(); - - // Handle an unhandled exception. By convention, returns true if further - // error handling should be suppressed and false otherwise. - // In this case, we will only suppress further error handling if the - // "ignoreGlobalErrors" configuration option is enabled. - function onError(error) { - for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; - } - - if (config.current) { - if (config.current.ignoreGlobalErrors) { - return true; - } - pushFailure.apply(undefined, [error.message, error.fileName + ":" + error.lineNumber].concat(args)); - } else { - test("global failure", extend(function () { - pushFailure.apply(undefined, [error.message, error.fileName + ":" + error.lineNumber].concat(args)); - }, { validTest: true })); - } - - return false; - } - - var QUnit = {}; - var globalSuite = new SuiteReport(); - - // The initial "currentModule" represents the global (or top-level) module that - // is not explicitly defined by the user, therefore we add the "globalSuite" to - // it since each module has a suiteReport associated with it. - config.currentModule.suiteReport = globalSuite; - - var moduleStack = []; - var globalStartCalled = false; - var runStarted = false; - - // Figure out if we're running the tests from a server or not - QUnit.isLocal = !(defined.document && window.location.protocol !== "file:"); - - // Expose the current QUnit version - QUnit.version = "2.3.3"; - - function createModule(name, testEnvironment) { - var parentModule = moduleStack.length ? moduleStack.slice(-1)[0] : null; - var moduleName = parentModule !== null ? [parentModule.name, name].join(" > ") : name; - var parentSuite = parentModule ? parentModule.suiteReport : globalSuite; - - var module = { - name: moduleName, - parentModule: parentModule, - tests: [], - moduleId: generateHash(moduleName), - testsRun: 0, - unskippedTestsRun: 0, - childModules: [], - suiteReport: new SuiteReport(name, parentSuite) - }; - - var env = {}; - if (parentModule) { - parentModule.childModules.push(module); - extend(env, parentModule.testEnvironment); - } - extend(env, testEnvironment); - module.testEnvironment = env; - - config.modules.push(module); - return module; - } - - extend(QUnit, { - on: on, - - // Call on start of module test to prepend name to all tests - module: function module(name, testEnvironment, executeNow) { - if (arguments.length === 2) { - if (objectType(testEnvironment) === "function") { - executeNow = testEnvironment; - testEnvironment = undefined; - } - } - - var module = createModule(name, testEnvironment); - - // Move any hooks to a 'hooks' object - if (module.testEnvironment) { - module.hooks = { - before: module.testEnvironment.before, - beforeEach: module.testEnvironment.beforeEach, - afterEach: module.testEnvironment.afterEach, - after: module.testEnvironment.after - }; - - delete module.testEnvironment.before; - delete module.testEnvironment.beforeEach; - delete module.testEnvironment.afterEach; - delete module.testEnvironment.after; - } - - var moduleFns = { - before: setHook(module, "before"), - beforeEach: setHook(module, "beforeEach"), - afterEach: setHook(module, "afterEach"), - after: setHook(module, "after") - }; - - var currentModule = config.currentModule; - if (objectType(executeNow) === "function") { - moduleStack.push(module); - config.currentModule = module; - executeNow.call(module.testEnvironment, moduleFns); - moduleStack.pop(); - module = module.parentModule || currentModule; - } - - config.currentModule = module; - }, - - test: test, - - todo: todo, - - skip: skip, - - only: only, - - start: function start(count) { - var globalStartAlreadyCalled = globalStartCalled; - - if (!config.current) { - globalStartCalled = true; - - if (runStarted) { - throw new Error("Called start() while test already started running"); - } else if (globalStartAlreadyCalled || count > 1) { - throw new Error("Called start() outside of a test context too many times"); - } else if (config.autostart) { - throw new Error("Called start() outside of a test context when " + "QUnit.config.autostart was true"); - } else if (!config.pageLoaded) { - - // The page isn't completely loaded yet, so we set autostart and then - // load if we're in Node or wait for the browser's load event. - config.autostart = true; - - // Starts from Node even if .load was not previously called. We still return - // early otherwise we'll wind up "beginning" twice. - if (!defined.document) { - QUnit.load(); - } - - return; - } - } else { - throw new Error("QUnit.start cannot be called inside a test context."); - } - - scheduleBegin(); - }, - - config: config, - - is: is, - - objectType: objectType, - - extend: extend, - - load: function load() { - config.pageLoaded = true; - - // Initialize the configuration options - extend(config, { - stats: { all: 0, bad: 0 }, - started: 0, - updateRate: 1000, - autostart: true, - filter: "" - }, true); - - if (!runStarted) { - config.blocking = false; - - if (config.autostart) { - scheduleBegin(); - } - } - }, - - stack: function stack(offset) { - offset = (offset || 0) + 2; - return sourceFromStacktrace(offset); - }, - - onError: onError - }); - - QUnit.pushFailure = pushFailure; - QUnit.assert = Assert.prototype; - QUnit.equiv = equiv; - QUnit.dump = dump; - - registerLoggingCallbacks(QUnit); - - function scheduleBegin() { - - runStarted = true; - - // Add a slight delay to allow definition of more modules and tests. - if (defined.setTimeout) { - setTimeout(function () { - begin(); - }, 13); - } else { - begin(); - } - } - - function begin() { - var i, - l, - modulesLog = []; - - // If the test run hasn't officially begun yet - if (!config.started) { - - // Record the time of the test run's beginning - config.started = now(); - - // Delete the loose unnamed module if unused. - if (config.modules[0].name === "" && config.modules[0].tests.length === 0) { - config.modules.shift(); - } - - // Avoid unnecessary information by not logging modules' test environments - for (i = 0, l = config.modules.length; i < l; i++) { - modulesLog.push({ - name: config.modules[i].name, - tests: config.modules[i].tests - }); - } - - // The test run is officially beginning now - emit("runStart", globalSuite.start(true)); - runLoggingCallbacks("begin", { - totalTests: Test.count, - modules: modulesLog - }); - } - - config.blocking = false; - ProcessingQueue.advance(); - } - - function setHook(module, hookName) { - if (!module.hooks) { - module.hooks = {}; - } - - return function (callback) { - module.hooks[hookName] = callback; - }; - } - - exportQUnit(QUnit); - - (function () { - - if (typeof window === "undefined" || typeof document === "undefined") { - return; - } - - var config = QUnit.config, - hasOwn = Object.prototype.hasOwnProperty; - - // Stores fixture HTML for resetting later - function storeFixture() { - - // Avoid overwriting user-defined values - if (hasOwn.call(config, "fixture")) { - return; - } - - var fixture = document.getElementById("qunit-fixture"); - if (fixture) { - config.fixture = fixture.innerHTML; - } - } - - QUnit.begin(storeFixture); - - // Resets the fixture DOM element if available. - function resetFixture() { - if (config.fixture == null) { - return; - } - - var fixture = document.getElementById("qunit-fixture"); - if (fixture) { - fixture.innerHTML = config.fixture; - } - } - - QUnit.testStart(resetFixture); - })(); - - (function () { - - // Only interact with URLs via window.location - var location = typeof window !== "undefined" && window.location; - if (!location) { - return; - } - - var urlParams = getUrlParams(); - - QUnit.urlParams = urlParams; - - // Match module/test by inclusion in an array - QUnit.config.moduleId = [].concat(urlParams.moduleId || []); - QUnit.config.testId = [].concat(urlParams.testId || []); - - // Exact case-insensitive match of the module name - QUnit.config.module = urlParams.module; - - // Regular expression or case-insenstive substring match against "moduleName: testName" - QUnit.config.filter = urlParams.filter; - - // Test order randomization - if (urlParams.seed === true) { - - // Generate a random seed if the option is specified without a value - QUnit.config.seed = Math.random().toString(36).slice(2); - } else if (urlParams.seed) { - QUnit.config.seed = urlParams.seed; - } - - // Add URL-parameter-mapped config values with UI form rendering data - QUnit.config.urlConfig.push({ - id: "hidepassed", - label: "Hide passed tests", - tooltip: "Only show tests and assertions that fail. Stored as query-strings." - }, { - id: "noglobals", - label: "Check for Globals", - tooltip: "Enabling this will test if any test introduces new properties on the " + "global object (`window` in Browsers). Stored as query-strings." - }, { - id: "notrycatch", - label: "No try-catch", - tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + "exceptions in IE reasonable. Stored as query-strings." - }); - - QUnit.begin(function () { - var i, - option, - urlConfig = QUnit.config.urlConfig; - - for (i = 0; i < urlConfig.length; i++) { - - // Options can be either strings or objects with nonempty "id" properties - option = QUnit.config.urlConfig[i]; - if (typeof option !== "string") { - option = option.id; - } - - if (QUnit.config[option] === undefined) { - QUnit.config[option] = urlParams[option]; - } - } - }); - - function getUrlParams() { - var i, param, name, value; - var urlParams = Object.create(null); - var params = location.search.slice(1).split("&"); - var length = params.length; - - for (i = 0; i < length; i++) { - if (params[i]) { - param = params[i].split("="); - name = decodeQueryParam(param[0]); - - // Allow just a key to turn on a flag, e.g., test.html?noglobals - value = param.length === 1 || decodeQueryParam(param.slice(1).join("=")); - if (name in urlParams) { - urlParams[name] = [].concat(urlParams[name], value); - } else { - urlParams[name] = value; - } - } - } - - return urlParams; - } - - function decodeQueryParam(param) { - return decodeURIComponent(param.replace(/\+/g, "%20")); - } - })(); - - var stats = { - passedTests: 0, - failedTests: 0, - skippedTests: 0, - todoTests: 0 - }; - - // Escape text for attribute or text content. - function escapeText(s) { - if (!s) { - return ""; - } - s = s + ""; - - // Both single quotes and double quotes (for attributes) - return s.replace(/['"<>&]/g, function (s) { - switch (s) { - case "'": - return "'"; - case "\"": - return """; - case "<": - return "<"; - case ">": - return ">"; - case "&": - return "&"; - } - }); - } - - (function () { - - // Don't load the HTML Reporter on non-browser environments - if (typeof window === "undefined" || !window.document) { - return; - } - - var config = QUnit.config, - document$$1 = window.document, - collapseNext = false, - hasOwn = Object.prototype.hasOwnProperty, - unfilteredUrl = setUrl({ filter: undefined, module: undefined, - moduleId: undefined, testId: undefined }), - modulesList = []; - - function addEvent(elem, type, fn) { - elem.addEventListener(type, fn, false); - } - - function removeEvent(elem, type, fn) { - elem.removeEventListener(type, fn, false); - } - - function addEvents(elems, type, fn) { - var i = elems.length; - while (i--) { - addEvent(elems[i], type, fn); - } - } - - function hasClass(elem, name) { - return (" " + elem.className + " ").indexOf(" " + name + " ") >= 0; - } - - function addClass(elem, name) { - if (!hasClass(elem, name)) { - elem.className += (elem.className ? " " : "") + name; - } - } - - function toggleClass(elem, name, force) { - if (force || typeof force === "undefined" && !hasClass(elem, name)) { - addClass(elem, name); - } else { - removeClass(elem, name); - } - } - - function removeClass(elem, name) { - var set = " " + elem.className + " "; - - // Class name may appear multiple times - while (set.indexOf(" " + name + " ") >= 0) { - set = set.replace(" " + name + " ", " "); - } - - // Trim for prettiness - elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); - } - - function id(name) { - return document$$1.getElementById && document$$1.getElementById(name); - } - - function abortTests() { - var abortButton = id("qunit-abort-tests-button"); - if (abortButton) { - abortButton.disabled = true; - abortButton.innerHTML = "Aborting..."; - } - QUnit.config.queue.length = 0; - return false; - } - - function interceptNavigation(ev) { - applyUrlParams(); - - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - return false; - } - - function getUrlConfigHtml() { - var i, - j, - val, - escaped, - escapedTooltip, - selection = false, - urlConfig = config.urlConfig, - urlConfigHtml = ""; - - for (i = 0; i < urlConfig.length; i++) { - - // Options can be either strings or objects with nonempty "id" properties - val = config.urlConfig[i]; - if (typeof val === "string") { - val = { - id: val, - label: val - }; - } - - escaped = escapeText(val.id); - escapedTooltip = escapeText(val.tooltip); - - if (!val.value || typeof val.value === "string") { - urlConfigHtml += ""; - } else { - urlConfigHtml += ""; - } - } - - return urlConfigHtml; - } - - // Handle "click" events on toolbar checkboxes and "change" for select menus. - // Updates the URL with the new state of `config.urlConfig` values. - function toolbarChanged() { - var updatedUrl, - value, - tests, - field = this, - params = {}; - - // Detect if field is a select menu or a checkbox - if ("selectedIndex" in field) { - value = field.options[field.selectedIndex].value || undefined; - } else { - value = field.checked ? field.defaultValue || true : undefined; - } - - params[field.name] = value; - updatedUrl = setUrl(params); - - // Check if we can apply the change without a page refresh - if ("hidepassed" === field.name && "replaceState" in window.history) { - QUnit.urlParams[field.name] = value; - config[field.name] = value || false; - tests = id("qunit-tests"); - if (tests) { - toggleClass(tests, "hidepass", value || false); - } - window.history.replaceState(null, "", updatedUrl); - } else { - window.location = updatedUrl; - } - } - - function setUrl(params) { - var key, - arrValue, - i, - querystring = "?", - location = window.location; - - params = QUnit.extend(QUnit.extend({}, QUnit.urlParams), params); - - for (key in params) { - - // Skip inherited or undefined properties - if (hasOwn.call(params, key) && params[key] !== undefined) { - - // Output a parameter for each value of this key (but usually just one) - arrValue = [].concat(params[key]); - for (i = 0; i < arrValue.length; i++) { - querystring += encodeURIComponent(key); - if (arrValue[i] !== true) { - querystring += "=" + encodeURIComponent(arrValue[i]); - } - querystring += "&"; - } - } - } - return location.protocol + "//" + location.host + location.pathname + querystring.slice(0, -1); - } - - function applyUrlParams() { - var i, - selectedModules = [], - modulesList = id("qunit-modulefilter-dropdown-list").getElementsByTagName("input"), - filter = id("qunit-filter-input").value; - - for (i = 0; i < modulesList.length; i++) { - if (modulesList[i].checked) { - selectedModules.push(modulesList[i].value); - } - } - - window.location = setUrl({ - filter: filter === "" ? undefined : filter, - moduleId: selectedModules.length === 0 ? undefined : selectedModules, - - // Remove module and testId filter - module: undefined, - testId: undefined - }); - } - - function toolbarUrlConfigContainer() { - var urlConfigContainer = document$$1.createElement("span"); - - urlConfigContainer.innerHTML = getUrlConfigHtml(); - addClass(urlConfigContainer, "qunit-url-config"); - - addEvents(urlConfigContainer.getElementsByTagName("input"), "change", toolbarChanged); - addEvents(urlConfigContainer.getElementsByTagName("select"), "change", toolbarChanged); - - return urlConfigContainer; - } - - function abortTestsButton() { - var button = document$$1.createElement("button"); - button.id = "qunit-abort-tests-button"; - button.innerHTML = "Abort"; - addEvent(button, "click", abortTests); - return button; - } - - function toolbarLooseFilter() { - var filter = document$$1.createElement("form"), - label = document$$1.createElement("label"), - input = document$$1.createElement("input"), - button = document$$1.createElement("button"); - - addClass(filter, "qunit-filter"); - - label.innerHTML = "Filter: "; - - input.type = "text"; - input.value = config.filter || ""; - input.name = "filter"; - input.id = "qunit-filter-input"; - - button.innerHTML = "Go"; - - label.appendChild(input); - - filter.appendChild(label); - filter.appendChild(document$$1.createTextNode(" ")); - filter.appendChild(button); - addEvent(filter, "submit", interceptNavigation); - - return filter; - } - - function moduleListHtml() { - var i, - checked, - html = ""; - - for (i = 0; i < config.modules.length; i++) { - if (config.modules[i].name !== "") { - checked = config.moduleId.indexOf(config.modules[i].moduleId) > -1; - html += "
  • "; - } - } - - return html; - } - - function toolbarModuleFilter() { - var allCheckbox, - commit, - reset, - moduleFilter = document$$1.createElement("form"), - label = document$$1.createElement("label"), - moduleSearch = document$$1.createElement("input"), - dropDown = document$$1.createElement("div"), - actions = document$$1.createElement("span"), - dropDownList = document$$1.createElement("ul"), - dirty = false; - - moduleSearch.id = "qunit-modulefilter-search"; - addEvent(moduleSearch, "input", searchInput); - addEvent(moduleSearch, "input", searchFocus); - addEvent(moduleSearch, "focus", searchFocus); - addEvent(moduleSearch, "click", searchFocus); - - label.id = "qunit-modulefilter-search-container"; - label.innerHTML = "Module: "; - label.appendChild(moduleSearch); - - actions.id = "qunit-modulefilter-actions"; - actions.innerHTML = "" + "" + ""; - allCheckbox = actions.lastChild.firstChild; - commit = actions.firstChild; - reset = commit.nextSibling; - addEvent(commit, "click", applyUrlParams); - - dropDownList.id = "qunit-modulefilter-dropdown-list"; - dropDownList.innerHTML = moduleListHtml(); - - dropDown.id = "qunit-modulefilter-dropdown"; - dropDown.style.display = "none"; - dropDown.appendChild(actions); - dropDown.appendChild(dropDownList); - addEvent(dropDown, "change", selectionChange); - selectionChange(); - - moduleFilter.id = "qunit-modulefilter"; - moduleFilter.appendChild(label); - moduleFilter.appendChild(dropDown); - addEvent(moduleFilter, "submit", interceptNavigation); - addEvent(moduleFilter, "reset", function () { - - // Let the reset happen, then update styles - window.setTimeout(selectionChange); - }); - - // Enables show/hide for the dropdown - function searchFocus() { - if (dropDown.style.display !== "none") { - return; - } - - dropDown.style.display = "block"; - addEvent(document$$1, "click", hideHandler); - addEvent(document$$1, "keydown", hideHandler); - - // Hide on Escape keydown or outside-container click - function hideHandler(e) { - var inContainer = moduleFilter.contains(e.target); - - if (e.keyCode === 27 || !inContainer) { - if (e.keyCode === 27 && inContainer) { - moduleSearch.focus(); - } - dropDown.style.display = "none"; - removeEvent(document$$1, "click", hideHandler); - removeEvent(document$$1, "keydown", hideHandler); - moduleSearch.value = ""; - searchInput(); - } - } - } - - // Processes module search box input - function searchInput() { - var i, - item, - searchText = moduleSearch.value.toLowerCase(), - listItems = dropDownList.children; - - for (i = 0; i < listItems.length; i++) { - item = listItems[i]; - if (!searchText || item.textContent.toLowerCase().indexOf(searchText) > -1) { - item.style.display = ""; - } else { - item.style.display = "none"; - } - } - } - - // Processes selection changes - function selectionChange(evt) { - var i, - item, - checkbox = evt && evt.target || allCheckbox, - modulesList = dropDownList.getElementsByTagName("input"), - selectedNames = []; - - toggleClass(checkbox.parentNode, "checked", checkbox.checked); - - dirty = false; - if (checkbox.checked && checkbox !== allCheckbox) { - allCheckbox.checked = false; - removeClass(allCheckbox.parentNode, "checked"); - } - for (i = 0; i < modulesList.length; i++) { - item = modulesList[i]; - if (!evt) { - toggleClass(item.parentNode, "checked", item.checked); - } else if (checkbox === allCheckbox && checkbox.checked) { - item.checked = false; - removeClass(item.parentNode, "checked"); - } - dirty = dirty || item.checked !== item.defaultChecked; - if (item.checked) { - selectedNames.push(item.parentNode.textContent); - } - } - - commit.style.display = reset.style.display = dirty ? "" : "none"; - moduleSearch.placeholder = selectedNames.join(", ") || allCheckbox.parentNode.textContent; - moduleSearch.title = "Type to filter list. Current selection:\n" + (selectedNames.join("\n") || allCheckbox.parentNode.textContent); - } - - return moduleFilter; - } - - function appendToolbar() { - var toolbar = id("qunit-testrunner-toolbar"); - - if (toolbar) { - toolbar.appendChild(toolbarUrlConfigContainer()); - toolbar.appendChild(toolbarModuleFilter()); - toolbar.appendChild(toolbarLooseFilter()); - toolbar.appendChild(document$$1.createElement("div")).className = "clearfix"; - } - } - - function appendHeader() { - var header = id("qunit-header"); - - if (header) { - header.innerHTML = "" + header.innerHTML + " "; - } - } - - function appendBanner() { - var banner = id("qunit-banner"); - - if (banner) { - banner.className = ""; - } - } - - function appendTestResults() { - var tests = id("qunit-tests"), - result = id("qunit-testresult"), - controls; - - if (result) { - result.parentNode.removeChild(result); - } - - if (tests) { - tests.innerHTML = ""; - result = document$$1.createElement("p"); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore(result, tests); - result.innerHTML = "
    Running...
     
    " + "
    " + "
    "; - controls = id("qunit-testresult-controls"); - } - - if (controls) { - controls.appendChild(abortTestsButton()); - } - } - - function appendFilteredTest() { - var testId = QUnit.config.testId; - if (!testId || testId.length <= 0) { - return ""; - } - return "
    Rerunning selected tests: " + escapeText(testId.join(", ")) + " Run all tests
    "; - } - - function appendUserAgent() { - var userAgent = id("qunit-userAgent"); - - if (userAgent) { - userAgent.innerHTML = ""; - userAgent.appendChild(document$$1.createTextNode("QUnit " + QUnit.version + "; " + navigator.userAgent)); - } - } - - function appendInterface() { - var qunit = id("qunit"); - - if (qunit) { - qunit.innerHTML = "

    " + escapeText(document$$1.title) + "

    " + "

    " + "
    " + appendFilteredTest() + "

    " + "
      "; - } - - appendHeader(); - appendBanner(); - appendTestResults(); - appendUserAgent(); - appendToolbar(); - } - - function appendTestsList(modules) { - var i, l, x, z, test, moduleObj; - - for (i = 0, l = modules.length; i < l; i++) { - moduleObj = modules[i]; - - for (x = 0, z = moduleObj.tests.length; x < z; x++) { - test = moduleObj.tests[x]; - - appendTest(test.name, test.testId, moduleObj.name); - } - } - } - - function appendTest(name, testId, moduleName) { - var title, - rerunTrigger, - testBlock, - assertList, - tests = id("qunit-tests"); - - if (!tests) { - return; - } - - title = document$$1.createElement("strong"); - title.innerHTML = getNameHtml(name, moduleName); - - rerunTrigger = document$$1.createElement("a"); - rerunTrigger.innerHTML = "Rerun"; - rerunTrigger.href = setUrl({ testId: testId }); - - testBlock = document$$1.createElement("li"); - testBlock.appendChild(title); - testBlock.appendChild(rerunTrigger); - testBlock.id = "qunit-test-output-" + testId; - - assertList = document$$1.createElement("ol"); - assertList.className = "qunit-assert-list"; - - testBlock.appendChild(assertList); - - tests.appendChild(testBlock); - } - - // HTML Reporter initialization and load - QUnit.begin(function (details) { - var i, moduleObj, tests; - - // Sort modules by name for the picker - for (i = 0; i < details.modules.length; i++) { - moduleObj = details.modules[i]; - if (moduleObj.name) { - modulesList.push(moduleObj.name); - } - } - modulesList.sort(function (a, b) { - return a.localeCompare(b); - }); - - // Initialize QUnit elements - appendInterface(); - appendTestsList(details.modules); - tests = id("qunit-tests"); - if (tests && config.hidepassed) { - addClass(tests, "hidepass"); - } - }); - - QUnit.done(function (details) { - var banner = id("qunit-banner"), - tests = id("qunit-tests"), - abortButton = id("qunit-abort-tests-button"), - totalTests = stats.passedTests + stats.skippedTests + stats.todoTests + stats.failedTests, - html = [totalTests, " tests completed in ", details.runtime, " milliseconds, with ", stats.failedTests, " failed, ", stats.skippedTests, " skipped, and ", stats.todoTests, " todo.
      ", "", details.passed, " assertions of ", details.total, " passed, ", details.failed, " failed."].join(""), - test, - assertLi, - assertList; - - // Update remaing tests to aborted - if (abortButton && abortButton.disabled) { - html = "Tests aborted after " + details.runtime + " milliseconds."; - - for (var i = 0; i < tests.children.length; i++) { - test = tests.children[i]; - if (test.className === "" || test.className === "running") { - test.className = "aborted"; - assertList = test.getElementsByTagName("ol")[0]; - assertLi = document$$1.createElement("li"); - assertLi.className = "fail"; - assertLi.innerHTML = "Test aborted."; - assertList.appendChild(assertLi); - } - } - } - - if (banner && (!abortButton || abortButton.disabled === false)) { - banner.className = stats.failedTests ? "qunit-fail" : "qunit-pass"; - } - - if (abortButton) { - abortButton.parentNode.removeChild(abortButton); - } - - if (tests) { - id("qunit-testresult-display").innerHTML = html; - } - - if (config.altertitle && document$$1.title) { - - // Show ✖ for good, ✔ for bad suite result in title - // use escape sequences in case file gets loaded with non-utf-8-charset - document$$1.title = [stats.failedTests ? "\u2716" : "\u2714", document$$1.title.replace(/^[\u2714\u2716] /i, "")].join(" "); - } - - // Scroll back to top to show results - if (config.scrolltop && window.scrollTo) { - window.scrollTo(0, 0); - } - }); - - function getNameHtml(name, module) { - var nameHtml = ""; - - if (module) { - nameHtml = "" + escapeText(module) + ": "; - } - - nameHtml += "" + escapeText(name) + ""; - - return nameHtml; - } - - QUnit.testStart(function (details) { - var running, testBlock, bad; - - testBlock = id("qunit-test-output-" + details.testId); - if (testBlock) { - testBlock.className = "running"; - } else { - - // Report later registered tests - appendTest(details.name, details.testId, details.module); - } - - running = id("qunit-testresult-display"); - if (running) { - bad = QUnit.config.reorder && details.previousFailure; - - running.innerHTML = (bad ? "Rerunning previously failed test:
      " : "Running:
      ") + getNameHtml(details.name, details.module); - } - }); - - function stripHtml(string) { - - // Strip tags, html entity and whitespaces - return string.replace(/<\/?[^>]+(>|$)/g, "").replace(/\"/g, "").replace(/\s+/g, ""); - } - - QUnit.log(function (details) { - var assertList, - assertLi, - message, - expected, - actual, - diff, - showDiff = false, - testItem = id("qunit-test-output-" + details.testId); - - if (!testItem) { - return; - } - - message = escapeText(details.message) || (details.result ? "okay" : "failed"); - message = "" + message + ""; - message += "@ " + details.runtime + " ms"; - - // The pushFailure doesn't provide details.expected - // when it calls, it's implicit to also not show expected and diff stuff - // Also, we need to check details.expected existence, as it can exist and be undefined - if (!details.result && hasOwn.call(details, "expected")) { - if (details.negative) { - expected = "NOT " + QUnit.dump.parse(details.expected); - } else { - expected = QUnit.dump.parse(details.expected); - } - - actual = QUnit.dump.parse(details.actual); - message += ""; - - if (actual !== expected) { - - message += ""; - - if (typeof details.actual === "number" && typeof details.expected === "number") { - if (!isNaN(details.actual) && !isNaN(details.expected)) { - showDiff = true; - diff = details.actual - details.expected; - diff = (diff > 0 ? "+" : "") + diff; - } - } else if (typeof details.actual !== "boolean" && typeof details.expected !== "boolean") { - diff = QUnit.diff(expected, actual); - - // don't show diff if there is zero overlap - showDiff = stripHtml(diff).length !== stripHtml(expected).length + stripHtml(actual).length; - } - - if (showDiff) { - message += ""; - } - } else if (expected.indexOf("[object Array]") !== -1 || expected.indexOf("[object Object]") !== -1) { - message += ""; - } else { - message += ""; - } - - if (details.source) { - message += ""; - } - - message += "
      Expected:
      " + escapeText(expected) + "
      Result:
      " + escapeText(actual) + "
      Diff:
      " + diff + "
      Message: " + "Diff suppressed as the depth of object is more than current max depth (" + QUnit.config.maxDepth + ").

      Hint: Use QUnit.dump.maxDepth to " + " run with a higher max depth or " + "Rerun without max depth.

      Message: " + "Diff suppressed as the expected and actual results have an equivalent" + " serialization
      Source:
      " + escapeText(details.source) + "
      "; - - // This occurs when pushFailure is set and we have an extracted stack trace - } else if (!details.result && details.source) { - message += "" + "" + "
      Source:
      " + escapeText(details.source) + "
      "; - } - - assertList = testItem.getElementsByTagName("ol")[0]; - - assertLi = document$$1.createElement("li"); - assertLi.className = details.result ? "pass" : "fail"; - assertLi.innerHTML = message; - assertList.appendChild(assertLi); - }); - - QUnit.testDone(function (details) { - var testTitle, - time, - testItem, - assertList, - good, - bad, - testCounts, - skipped, - sourceName, - tests = id("qunit-tests"); - - if (!tests) { - return; - } - - testItem = id("qunit-test-output-" + details.testId); - - assertList = testItem.getElementsByTagName("ol")[0]; - - good = details.passed; - bad = details.failed; - - // This test passed if it has no unexpected failed assertions - var testPassed = details.failed > 0 ? details.todo : !details.todo; - - if (testPassed) { - - // Collapse the passing tests - addClass(assertList, "qunit-collapsed"); - } else if (config.collapse) { - if (!collapseNext) { - - // Skip collapsing the first failing test - collapseNext = true; - } else { - - // Collapse remaining tests - addClass(assertList, "qunit-collapsed"); - } - } - - // The testItem.firstChild is the test name - testTitle = testItem.firstChild; - - testCounts = bad ? "" + bad + ", " + "" + good + ", " : ""; - - testTitle.innerHTML += " (" + testCounts + details.assertions.length + ")"; - - if (details.skipped) { - stats.skippedTests++; - - testItem.className = "skipped"; - skipped = document$$1.createElement("em"); - skipped.className = "qunit-skipped-label"; - skipped.innerHTML = "skipped"; - testItem.insertBefore(skipped, testTitle); - } else { - addEvent(testTitle, "click", function () { - toggleClass(assertList, "qunit-collapsed"); - }); - - testItem.className = testPassed ? "pass" : "fail"; - - if (details.todo) { - var todoLabel = document$$1.createElement("em"); - todoLabel.className = "qunit-todo-label"; - todoLabel.innerHTML = "todo"; - testItem.className += " todo"; - testItem.insertBefore(todoLabel, testTitle); - } - - time = document$$1.createElement("span"); - time.className = "runtime"; - time.innerHTML = details.runtime + " ms"; - testItem.insertBefore(time, assertList); - - if (!testPassed) { - stats.failedTests++; - } else if (details.todo) { - stats.todoTests++; - } else { - stats.passedTests++; - } - } - - // Show the source of the test when showing assertions - if (details.source) { - sourceName = document$$1.createElement("p"); - sourceName.innerHTML = "Source: " + details.source; - addClass(sourceName, "qunit-source"); - if (testPassed) { - addClass(sourceName, "qunit-collapsed"); - } - addEvent(testTitle, "click", function () { - toggleClass(sourceName, "qunit-collapsed"); - }); - testItem.appendChild(sourceName); - } - }); - - // Avoid readyState issue with phantomjs - // Ref: #818 - var notPhantom = function (p) { - return !(p && p.version && p.version.major > 0); - }(window.phantom); - - if (notPhantom && document$$1.readyState === "complete") { - QUnit.load(); - } else { - addEvent(window, "load", QUnit.load); - } - - // Wrap window.onerror. We will call the original window.onerror to see if - // the existing handler fully handles the error; if not, we will call the - // QUnit.onError function. - var originalWindowOnError = window.onerror; - - // Cover uncaught exceptions - // Returning true will suppress the default browser handler, - // returning false will let it run. - window.onerror = function (message, fileName, lineNumber) { - var ret = false; - if (originalWindowOnError) { - for (var _len = arguments.length, args = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) { - args[_key - 3] = arguments[_key]; - } - - ret = originalWindowOnError.call.apply(originalWindowOnError, [this, message, fileName, lineNumber].concat(args)); - } - - // Treat return value as window.onerror itself does, - // Only do our handling if not suppressed. - if (ret !== true) { - var error = { - message: message, - fileName: fileName, - lineNumber: lineNumber - }; - - ret = QUnit.onError(error); - } - - return ret; - }; - })(); - - /* - * This file is a modified version of google-diff-match-patch's JavaScript implementation - * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), - * modifications are licensed as more fully set forth in LICENSE.txt. - * - * The original source of google-diff-match-patch is attributable and licensed as follows: - * - * Copyright 2006 Google Inc. - * https://code.google.com/p/google-diff-match-patch/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * More Info: - * https://code.google.com/p/google-diff-match-patch/ - * - * Usage: QUnit.diff(expected, actual) - * - */ - QUnit.diff = function () { - function DiffMatchPatch() {} - - // DIFF FUNCTIONS - - /** - * The data structure representing a diff is an array of tuples: - * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] - * which means: delete 'Hello', add 'Goodbye' and keep ' world.' - */ - var DIFF_DELETE = -1, - DIFF_INSERT = 1, - DIFF_EQUAL = 0; - - /** - * Find the differences between two texts. Simplifies the problem by stripping - * any common prefix or suffix off the texts before diffing. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean=} optChecklines Optional speedup flag. If present and false, - * then don't run a line-level diff first to identify the changed areas. - * Defaults to true, which does a faster, slightly less optimal diff. - * @return {!Array.} Array of diff tuples. - */ - DiffMatchPatch.prototype.DiffMain = function (text1, text2, optChecklines) { - var deadline, checklines, commonlength, commonprefix, commonsuffix, diffs; - - // The diff must be complete in up to 1 second. - deadline = new Date().getTime() + 1000; - - // Check for null inputs. - if (text1 === null || text2 === null) { - throw new Error("Null input. (DiffMain)"); - } - - // Check for equality (speedup). - if (text1 === text2) { - if (text1) { - return [[DIFF_EQUAL, text1]]; - } - return []; - } - - if (typeof optChecklines === "undefined") { - optChecklines = true; - } - - checklines = optChecklines; - - // Trim off common prefix (speedup). - commonlength = this.diffCommonPrefix(text1, text2); - commonprefix = text1.substring(0, commonlength); - text1 = text1.substring(commonlength); - text2 = text2.substring(commonlength); - - // Trim off common suffix (speedup). - commonlength = this.diffCommonSuffix(text1, text2); - commonsuffix = text1.substring(text1.length - commonlength); - text1 = text1.substring(0, text1.length - commonlength); - text2 = text2.substring(0, text2.length - commonlength); - - // Compute the diff on the middle block. - diffs = this.diffCompute(text1, text2, checklines, deadline); - - // Restore the prefix and suffix. - if (commonprefix) { - diffs.unshift([DIFF_EQUAL, commonprefix]); - } - if (commonsuffix) { - diffs.push([DIFF_EQUAL, commonsuffix]); - } - this.diffCleanupMerge(diffs); - return diffs; - }; - - /** - * Reduce the number of edits by eliminating operationally trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupEfficiency = function (diffs) { - var changes, equalities, equalitiesLength, lastequality, pointer, preIns, preDel, postIns, postDel; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - - // Is there an insertion operation before the last equality. - preIns = false; - - // Is there a deletion operation before the last equality. - preDel = false; - - // Is there an insertion operation after the last equality. - postIns = false; - - // Is there a deletion operation after the last equality. - postDel = false; - while (pointer < diffs.length) { - - // Equality found. - if (diffs[pointer][0] === DIFF_EQUAL) { - if (diffs[pointer][1].length < 4 && (postIns || postDel)) { - - // Candidate found. - equalities[equalitiesLength++] = pointer; - preIns = postIns; - preDel = postDel; - lastequality = diffs[pointer][1]; - } else { - - // Not a candidate, and can never become one. - equalitiesLength = 0; - lastequality = null; - } - postIns = postDel = false; - - // An insertion or deletion. - } else { - - if (diffs[pointer][0] === DIFF_DELETE) { - postDel = true; - } else { - postIns = true; - } - - /* - * Five types to be split: - * ABXYCD - * AXCD - * ABXC - * AXCD - * ABXC - */ - if (lastequality && (preIns && preDel && postIns && postDel || lastequality.length < 2 && preIns + preDel + postIns + postDel === 3)) { - - // Duplicate record. - diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]); - - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - equalitiesLength--; // Throw away the equality we just deleted; - lastequality = null; - if (preIns && preDel) { - - // No changes made which could affect previous entry, keep going. - postIns = postDel = true; - equalitiesLength = 0; - } else { - equalitiesLength--; // Throw away the previous equality. - pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - postIns = postDel = false; - } - changes = true; - } - } - pointer++; - } - - if (changes) { - this.diffCleanupMerge(diffs); - } - }; - - /** - * Convert a diff array into a pretty HTML report. - * @param {!Array.} diffs Array of diff tuples. - * @param {integer} string to be beautified. - * @return {string} HTML representation. - */ - DiffMatchPatch.prototype.diffPrettyHtml = function (diffs) { - var op, - data, - x, - html = []; - for (x = 0; x < diffs.length; x++) { - op = diffs[x][0]; // Operation (insert, delete, equal) - data = diffs[x][1]; // Text of change. - switch (op) { - case DIFF_INSERT: - html[x] = "" + escapeText(data) + ""; - break; - case DIFF_DELETE: - html[x] = "" + escapeText(data) + ""; - break; - case DIFF_EQUAL: - html[x] = "" + escapeText(data) + ""; - break; - } - } - return html.join(""); - }; - - /** - * Determine the common prefix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the start of each - * string. - */ - DiffMatchPatch.prototype.diffCommonPrefix = function (text1, text2) { - var pointermid, pointermax, pointermin, pointerstart; - - // Quick check for common null cases. - if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) { - return 0; - } - - // Binary search. - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min(text1.length, text2.length); - pointermid = pointermax; - pointerstart = 0; - while (pointermin < pointermid) { - if (text1.substring(pointerstart, pointermid) === text2.substring(pointerstart, pointermid)) { - pointermin = pointermid; - pointerstart = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); - } - return pointermid; - }; - - /** - * Determine the common suffix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of each string. - */ - DiffMatchPatch.prototype.diffCommonSuffix = function (text1, text2) { - var pointermid, pointermax, pointermin, pointerend; - - // Quick check for common null cases. - if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { - return 0; - } - - // Binary search. - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min(text1.length, text2.length); - pointermid = pointermax; - pointerend = 0; - while (pointermin < pointermid) { - if (text1.substring(text1.length - pointermid, text1.length - pointerend) === text2.substring(text2.length - pointermid, text2.length - pointerend)) { - pointermin = pointermid; - pointerend = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); - } - return pointermid; - }; - - /** - * Find the differences between two texts. Assumes that the texts do not - * have any common prefix or suffix. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean} checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster, slightly less optimal diff. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffCompute = function (text1, text2, checklines, deadline) { - var diffs, longtext, shorttext, i, hm, text1A, text2A, text1B, text2B, midCommon, diffsA, diffsB; - - if (!text1) { - - // Just add some text (speedup). - return [[DIFF_INSERT, text2]]; - } - - if (!text2) { - - // Just delete some text (speedup). - return [[DIFF_DELETE, text1]]; - } - - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - i = longtext.indexOf(shorttext); - if (i !== -1) { - - // Shorter text is inside the longer text (speedup). - diffs = [[DIFF_INSERT, longtext.substring(0, i)], [DIFF_EQUAL, shorttext], [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; - - // Swap insertions for deletions if diff is reversed. - if (text1.length > text2.length) { - diffs[0][0] = diffs[2][0] = DIFF_DELETE; - } - return diffs; - } - - if (shorttext.length === 1) { - - // Single character string. - // After the previous speedup, the character can't be an equality. - return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; - } - - // Check to see if the problem can be split in two. - hm = this.diffHalfMatch(text1, text2); - if (hm) { - - // A half-match was found, sort out the return data. - text1A = hm[0]; - text1B = hm[1]; - text2A = hm[2]; - text2B = hm[3]; - midCommon = hm[4]; - - // Send both pairs off for separate processing. - diffsA = this.DiffMain(text1A, text2A, checklines, deadline); - diffsB = this.DiffMain(text1B, text2B, checklines, deadline); - - // Merge the results. - return diffsA.concat([[DIFF_EQUAL, midCommon]], diffsB); - } - - if (checklines && text1.length > 100 && text2.length > 100) { - return this.diffLineMode(text1, text2, deadline); - } - - return this.diffBisect(text1, text2, deadline); - }; - - /** - * Do the two texts share a substring which is at least half the length of the - * longer text? - * This speedup can produce non-minimal diffs. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {Array.} Five element Array, containing the prefix of - * text1, the suffix of text1, the prefix of text2, the suffix of - * text2 and the common middle. Or null if there was no match. - * @private - */ - DiffMatchPatch.prototype.diffHalfMatch = function (text1, text2) { - var longtext, shorttext, dmp, text1A, text2B, text2A, text1B, midCommon, hm1, hm2, hm; - - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { - return null; // Pointless. - } - dmp = this; // 'this' becomes 'window' in a closure. - - /** - * Does a substring of shorttext exist within longtext such that the substring - * is at least half the length of longtext? - * Closure, but does not reference any external variables. - * @param {string} longtext Longer string. - * @param {string} shorttext Shorter string. - * @param {number} i Start index of quarter length substring within longtext. - * @return {Array.} Five element Array, containing the prefix of - * longtext, the suffix of longtext, the prefix of shorttext, the suffix - * of shorttext and the common middle. Or null if there was no match. - * @private - */ - function diffHalfMatchI(longtext, shorttext, i) { - var seed, j, bestCommon, prefixLength, suffixLength, bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; - - // Start with a 1/4 length substring at position i as a seed. - seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); - j = -1; - bestCommon = ""; - while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { - prefixLength = dmp.diffCommonPrefix(longtext.substring(i), shorttext.substring(j)); - suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), shorttext.substring(0, j)); - if (bestCommon.length < suffixLength + prefixLength) { - bestCommon = shorttext.substring(j - suffixLength, j) + shorttext.substring(j, j + prefixLength); - bestLongtextA = longtext.substring(0, i - suffixLength); - bestLongtextB = longtext.substring(i + prefixLength); - bestShorttextA = shorttext.substring(0, j - suffixLength); - bestShorttextB = shorttext.substring(j + prefixLength); - } - } - if (bestCommon.length * 2 >= longtext.length) { - return [bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB, bestCommon]; - } else { - return null; - } - } - - // First check if the second quarter is the seed for a half-match. - hm1 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 4)); - - // Check again based on the third quarter. - hm2 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 2)); - if (!hm1 && !hm2) { - return null; - } else if (!hm2) { - hm = hm1; - } else if (!hm1) { - hm = hm2; - } else { - - // Both matched. Select the longest. - hm = hm1[4].length > hm2[4].length ? hm1 : hm2; - } - - // A half-match was found, sort out the return data. - if (text1.length > text2.length) { - text1A = hm[0]; - text1B = hm[1]; - text2A = hm[2]; - text2B = hm[3]; - } else { - text2A = hm[0]; - text2B = hm[1]; - text1A = hm[2]; - text1B = hm[3]; - } - midCommon = hm[4]; - return [text1A, text1B, text2A, text2B, midCommon]; - }; - - /** - * Do a quick line-level diff on both strings, then rediff the parts for - * greater accuracy. - * This speedup can produce non-minimal diffs. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffLineMode = function (text1, text2, deadline) { - var a, diffs, linearray, pointer, countInsert, countDelete, textInsert, textDelete, j; - - // Scan the text on a line-by-line basis first. - a = this.diffLinesToChars(text1, text2); - text1 = a.chars1; - text2 = a.chars2; - linearray = a.lineArray; - - diffs = this.DiffMain(text1, text2, false, deadline); - - // Convert the diff back to original text. - this.diffCharsToLines(diffs, linearray); - - // Eliminate freak matches (e.g. blank lines) - this.diffCleanupSemantic(diffs); - - // Rediff any replacement blocks, this time character-by-character. - // Add a dummy entry at the end. - diffs.push([DIFF_EQUAL, ""]); - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ""; - textInsert = ""; - while (pointer < diffs.length) { - switch (diffs[pointer][0]) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[pointer][1]; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[pointer][1]; - break; - case DIFF_EQUAL: - - // Upon reaching an equality, check for prior redundancies. - if (countDelete >= 1 && countInsert >= 1) { - - // Delete the offending records and add the merged ones. - diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert); - pointer = pointer - countDelete - countInsert; - a = this.DiffMain(textDelete, textInsert, false, deadline); - for (j = a.length - 1; j >= 0; j--) { - diffs.splice(pointer, 0, a[j]); - } - pointer = pointer + a.length; - } - countInsert = 0; - countDelete = 0; - textDelete = ""; - textInsert = ""; - break; - } - pointer++; - } - diffs.pop(); // Remove the dummy entry at the end. - - return diffs; - }; - - /** - * Find the 'middle snake' of a diff, split the problem in two - * and return the recursively constructed diff. - * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisect = function (text1, text2, deadline) { - var text1Length, text2Length, maxD, vOffset, vLength, v1, v2, x, delta, front, k1start, k1end, k2start, k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; - - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - maxD = Math.ceil((text1Length + text2Length) / 2); - vOffset = maxD; - vLength = 2 * maxD; - v1 = new Array(vLength); - v2 = new Array(vLength); - - // Setting all elements to -1 is faster in Chrome & Firefox than mixing - // integers and undefined. - for (x = 0; x < vLength; x++) { - v1[x] = -1; - v2[x] = -1; - } - v1[vOffset + 1] = 0; - v2[vOffset + 1] = 0; - delta = text1Length - text2Length; - - // If the total number of characters is odd, then the front path will collide - // with the reverse path. - front = delta % 2 !== 0; - - // Offsets for start and end of k loop. - // Prevents mapping of space beyond the grid. - k1start = 0; - k1end = 0; - k2start = 0; - k2end = 0; - for (d = 0; d < maxD; d++) { - - // Bail out if deadline is reached. - if (new Date().getTime() > deadline) { - break; - } - - // Walk the front path one step. - for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { - k1Offset = vOffset + k1; - if (k1 === -d || k1 !== d && v1[k1Offset - 1] < v1[k1Offset + 1]) { - x1 = v1[k1Offset + 1]; - } else { - x1 = v1[k1Offset - 1] + 1; - } - y1 = x1 - k1; - while (x1 < text1Length && y1 < text2Length && text1.charAt(x1) === text2.charAt(y1)) { - x1++; - y1++; - } - v1[k1Offset] = x1; - if (x1 > text1Length) { - - // Ran off the right of the graph. - k1end += 2; - } else if (y1 > text2Length) { - - // Ran off the bottom of the graph. - k1start += 2; - } else if (front) { - k2Offset = vOffset + delta - k1; - if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) { - - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - v2[k2Offset]; - if (x1 >= x2) { - - // Overlap detected. - return this.diffBisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - - // Walk the reverse path one step. - for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { - k2Offset = vOffset + k2; - if (k2 === -d || k2 !== d && v2[k2Offset - 1] < v2[k2Offset + 1]) { - x2 = v2[k2Offset + 1]; - } else { - x2 = v2[k2Offset - 1] + 1; - } - y2 = x2 - k2; - while (x2 < text1Length && y2 < text2Length && text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)) { - x2++; - y2++; - } - v2[k2Offset] = x2; - if (x2 > text1Length) { - - // Ran off the left of the graph. - k2end += 2; - } else if (y2 > text2Length) { - - // Ran off the top of the graph. - k2start += 2; - } else if (!front) { - k1Offset = vOffset + delta - k2; - if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) { - x1 = v1[k1Offset]; - y1 = vOffset + x1 - k1Offset; - - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - x2; - if (x1 >= x2) { - - // Overlap detected. - return this.diffBisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - } - - // Diff took too long and hit the deadline or - // number of diffs equals number of characters, no commonality at all. - return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; - }; - - /** - * Given the location of the 'middle snake', split the diff in two parts - * and recurse. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} x Index of split point in text1. - * @param {number} y Index of split point in text2. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisectSplit = function (text1, text2, x, y, deadline) { - var text1a, text1b, text2a, text2b, diffs, diffsb; - text1a = text1.substring(0, x); - text2a = text2.substring(0, y); - text1b = text1.substring(x); - text2b = text2.substring(y); - - // Compute both diffs serially. - diffs = this.DiffMain(text1a, text2a, false, deadline); - diffsb = this.DiffMain(text1b, text2b, false, deadline); - - return diffs.concat(diffsb); - }; - - /** - * Reduce the number of edits by eliminating semantically trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupSemantic = function (diffs) { - var changes, equalities, equalitiesLength, lastequality, pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - - // Number of characters that changed prior to the equality. - lengthInsertions1 = 0; - lengthDeletions1 = 0; - - // Number of characters that changed after the equality. - lengthInsertions2 = 0; - lengthDeletions2 = 0; - while (pointer < diffs.length) { - if (diffs[pointer][0] === DIFF_EQUAL) { - // Equality found. - equalities[equalitiesLength++] = pointer; - lengthInsertions1 = lengthInsertions2; - lengthDeletions1 = lengthDeletions2; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = diffs[pointer][1]; - } else { - // An insertion or deletion. - if (diffs[pointer][0] === DIFF_INSERT) { - lengthInsertions2 += diffs[pointer][1].length; - } else { - lengthDeletions2 += diffs[pointer][1].length; - } - - // Eliminate an equality that is smaller or equal to the edits on both - // sides of it. - if (lastequality && lastequality.length <= Math.max(lengthInsertions1, lengthDeletions1) && lastequality.length <= Math.max(lengthInsertions2, lengthDeletions2)) { - - // Duplicate record. - diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]); - - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - - // Throw away the equality we just deleted. - equalitiesLength--; - - // Throw away the previous equality (it needs to be reevaluated). - equalitiesLength--; - pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - - // Reset the counters. - lengthInsertions1 = 0; - lengthDeletions1 = 0; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = null; - changes = true; - } - } - pointer++; - } - - // Normalize the diff. - if (changes) { - this.diffCleanupMerge(diffs); - } - - // Find any overlaps between deletions and insertions. - // e.g: abcxxxxxxdef - // -> abcxxxdef - // e.g: xxxabcdefxxx - // -> defxxxabc - // Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = 1; - while (pointer < diffs.length) { - if (diffs[pointer - 1][0] === DIFF_DELETE && diffs[pointer][0] === DIFF_INSERT) { - deletion = diffs[pointer - 1][1]; - insertion = diffs[pointer][1]; - overlapLength1 = this.diffCommonOverlap(deletion, insertion); - overlapLength2 = this.diffCommonOverlap(insertion, deletion); - if (overlapLength1 >= overlapLength2) { - if (overlapLength1 >= deletion.length / 2 || overlapLength1 >= insertion.length / 2) { - - // Overlap found. Insert an equality and trim the surrounding edits. - diffs.splice(pointer, 0, [DIFF_EQUAL, insertion.substring(0, overlapLength1)]); - diffs[pointer - 1][1] = deletion.substring(0, deletion.length - overlapLength1); - diffs[pointer + 1][1] = insertion.substring(overlapLength1); - pointer++; - } - } else { - if (overlapLength2 >= deletion.length / 2 || overlapLength2 >= insertion.length / 2) { - - // Reverse overlap found. - // Insert an equality and swap and trim the surrounding edits. - diffs.splice(pointer, 0, [DIFF_EQUAL, deletion.substring(0, overlapLength2)]); - - diffs[pointer - 1][0] = DIFF_INSERT; - diffs[pointer - 1][1] = insertion.substring(0, insertion.length - overlapLength2); - diffs[pointer + 1][0] = DIFF_DELETE; - diffs[pointer + 1][1] = deletion.substring(overlapLength2); - pointer++; - } - } - pointer++; - } - pointer++; - } - }; - - /** - * Determine if the suffix of one string is the prefix of another. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of the first - * string and the start of the second string. - * @private - */ - DiffMatchPatch.prototype.diffCommonOverlap = function (text1, text2) { - var text1Length, text2Length, textLength, best, length, pattern, found; - - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - - // Eliminate the null case. - if (text1Length === 0 || text2Length === 0) { - return 0; - } - - // Truncate the longer string. - if (text1Length > text2Length) { - text1 = text1.substring(text1Length - text2Length); - } else if (text1Length < text2Length) { - text2 = text2.substring(0, text1Length); - } - textLength = Math.min(text1Length, text2Length); - - // Quick check for the worst case. - if (text1 === text2) { - return textLength; - } - - // Start by looking for a single character match - // and increase length until no match is found. - // Performance analysis: https://neil.fraser.name/news/2010/11/04/ - best = 0; - length = 1; - while (true) { - pattern = text1.substring(textLength - length); - found = text2.indexOf(pattern); - if (found === -1) { - return best; - } - length += found; - if (found === 0 || text1.substring(textLength - length) === text2.substring(0, length)) { - best = length; - length++; - } - } - }; - - /** - * Split two texts into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {{chars1: string, chars2: string, lineArray: !Array.}} - * An object containing the encoded text1, the encoded text2 and - * the array of unique strings. - * The zeroth element of the array of unique strings is intentionally blank. - * @private - */ - DiffMatchPatch.prototype.diffLinesToChars = function (text1, text2) { - var lineArray, lineHash, chars1, chars2; - lineArray = []; // E.g. lineArray[4] === 'Hello\n' - lineHash = {}; // E.g. lineHash['Hello\n'] === 4 - - // '\x00' is a valid character, but various debuggers don't like it. - // So we'll insert a junk entry to avoid generating a null character. - lineArray[0] = ""; - - /** - * Split a text into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * Modifies linearray and linehash through being a closure. - * @param {string} text String to encode. - * @return {string} Encoded string. - * @private - */ - function diffLinesToCharsMunge(text) { - var chars, lineStart, lineEnd, lineArrayLength, line; - chars = ""; - - // Walk the text, pulling out a substring for each line. - // text.split('\n') would would temporarily double our memory footprint. - // Modifying text would create many large strings to garbage collect. - lineStart = 0; - lineEnd = -1; - - // Keeping our own length variable is faster than looking it up. - lineArrayLength = lineArray.length; - while (lineEnd < text.length - 1) { - lineEnd = text.indexOf("\n", lineStart); - if (lineEnd === -1) { - lineEnd = text.length - 1; - } - line = text.substring(lineStart, lineEnd + 1); - lineStart = lineEnd + 1; - - if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : lineHash[line] !== undefined) { - chars += String.fromCharCode(lineHash[line]); - } else { - chars += String.fromCharCode(lineArrayLength); - lineHash[line] = lineArrayLength; - lineArray[lineArrayLength++] = line; - } - } - return chars; - } - - chars1 = diffLinesToCharsMunge(text1); - chars2 = diffLinesToCharsMunge(text2); - return { - chars1: chars1, - chars2: chars2, - lineArray: lineArray - }; - }; - - /** - * Rehydrate the text in a diff from a string of line hashes to real lines of - * text. - * @param {!Array.} diffs Array of diff tuples. - * @param {!Array.} lineArray Array of unique strings. - * @private - */ - DiffMatchPatch.prototype.diffCharsToLines = function (diffs, lineArray) { - var x, chars, text, y; - for (x = 0; x < diffs.length; x++) { - chars = diffs[x][1]; - text = []; - for (y = 0; y < chars.length; y++) { - text[y] = lineArray[chars.charCodeAt(y)]; - } - diffs[x][1] = text.join(""); - } - }; - - /** - * Reorder and merge like edit sections. Merge equalities. - * Any edit section can move as long as it doesn't cross an equality. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupMerge = function (diffs) { - var pointer, countDelete, countInsert, textInsert, textDelete, commonlength, changes, diffPointer, position; - diffs.push([DIFF_EQUAL, ""]); // Add a dummy entry at the end. - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ""; - textInsert = ""; - - while (pointer < diffs.length) { - switch (diffs[pointer][0]) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[pointer][1]; - pointer++; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[pointer][1]; - pointer++; - break; - case DIFF_EQUAL: - - // Upon reaching an equality, check for prior redundancies. - if (countDelete + countInsert > 1) { - if (countDelete !== 0 && countInsert !== 0) { - - // Factor out any common prefixes. - commonlength = this.diffCommonPrefix(textInsert, textDelete); - if (commonlength !== 0) { - if (pointer - countDelete - countInsert > 0 && diffs[pointer - countDelete - countInsert - 1][0] === DIFF_EQUAL) { - diffs[pointer - countDelete - countInsert - 1][1] += textInsert.substring(0, commonlength); - } else { - diffs.splice(0, 0, [DIFF_EQUAL, textInsert.substring(0, commonlength)]); - pointer++; - } - textInsert = textInsert.substring(commonlength); - textDelete = textDelete.substring(commonlength); - } - - // Factor out any common suffixies. - commonlength = this.diffCommonSuffix(textInsert, textDelete); - if (commonlength !== 0) { - diffs[pointer][1] = textInsert.substring(textInsert.length - commonlength) + diffs[pointer][1]; - textInsert = textInsert.substring(0, textInsert.length - commonlength); - textDelete = textDelete.substring(0, textDelete.length - commonlength); - } - } - - // Delete the offending records and add the merged ones. - if (countDelete === 0) { - diffs.splice(pointer - countInsert, countDelete + countInsert, [DIFF_INSERT, textInsert]); - } else if (countInsert === 0) { - diffs.splice(pointer - countDelete, countDelete + countInsert, [DIFF_DELETE, textDelete]); - } else { - diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert, [DIFF_DELETE, textDelete], [DIFF_INSERT, textInsert]); - } - pointer = pointer - countDelete - countInsert + (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1; - } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { - - // Merge this equality with the previous one. - diffs[pointer - 1][1] += diffs[pointer][1]; - diffs.splice(pointer, 1); - } else { - pointer++; - } - countInsert = 0; - countDelete = 0; - textDelete = ""; - textInsert = ""; - break; - } - } - if (diffs[diffs.length - 1][1] === "") { - diffs.pop(); // Remove the dummy entry at the end. - } - - // Second pass: look for single edits surrounded on both sides by equalities - // which can be shifted sideways to eliminate an equality. - // e.g: ABAC -> ABAC - changes = false; - pointer = 1; - - // Intentionally ignore the first and last element (don't need checking). - while (pointer < diffs.length - 1) { - if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) { - - diffPointer = diffs[pointer][1]; - position = diffPointer.substring(diffPointer.length - diffs[pointer - 1][1].length); - - // This is a single edit surrounded by equalities. - if (position === diffs[pointer - 1][1]) { - - // Shift the edit over the previous equality. - diffs[pointer][1] = diffs[pointer - 1][1] + diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length); - diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; - diffs.splice(pointer - 1, 1); - changes = true; - } else if (diffPointer.substring(0, diffs[pointer + 1][1].length) === diffs[pointer + 1][1]) { - - // Shift the edit over the next equality. - diffs[pointer - 1][1] += diffs[pointer + 1][1]; - diffs[pointer][1] = diffs[pointer][1].substring(diffs[pointer + 1][1].length) + diffs[pointer + 1][1]; - diffs.splice(pointer + 1, 1); - changes = true; - } - } - pointer++; - } - - // If shifts were made, the diff needs reordering and another shift sweep. - if (changes) { - this.diffCleanupMerge(diffs); - } - }; - - return function (o, n) { - var diff, output, text; - diff = new DiffMatchPatch(); - output = diff.DiffMain(o, n); - diff.diffCleanupEfficiency(output); - text = diff.diffPrettyHtml(output); - - return text; - }; - }(); - -}((function() { return this; }()))); diff --git a/vendor/assets/stylesheets/qunit.css b/vendor/assets/stylesheets/qunit.css deleted file mode 100644 index 859544eb22..0000000000 --- a/vendor/assets/stylesheets/qunit.css +++ /dev/null @@ -1,436 +0,0 @@ -/*! - * QUnit 2.6.0 - * https://qunitjs.com/ - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2018-03-27T02:18Z - */ - -/** Font Family and Sizes */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { - font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; -} - -#qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } -#qunit-tests { font-size: smaller; } - - -/** Resets */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { - margin: 0; - padding: 0; -} - - -/** Header (excluding toolbar) */ - -#qunit-header { - padding: 0.5em 0 0.5em 1em; - - color: #8699A4; - background-color: #0D3349; - - font-size: 1.5em; - line-height: 1em; - font-weight: 400; - - border-radius: 5px 5px 0 0; -} - -#qunit-header a { - text-decoration: none; - color: #C2CCD1; -} - -#qunit-header a:hover, -#qunit-header a:focus { - color: #FFF; -} - -#qunit-banner { - height: 5px; -} - -#qunit-filteredTest { - padding: 0.5em 1em 0.5em 1em; - color: #366097; - background-color: #F4FF77; -} - -#qunit-userAgent { - padding: 0.5em 1em 0.5em 1em; - color: #FFF; - background-color: #2B81AF; - text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; -} - - -/** Toolbar */ - -#qunit-testrunner-toolbar { - padding: 0.5em 1em 0.5em 1em; - color: #5E740B; - background-color: #EEE; -} - -#qunit-testrunner-toolbar .clearfix { - height: 0; - clear: both; -} - -#qunit-testrunner-toolbar label { - display: inline-block; -} - -#qunit-testrunner-toolbar input[type=checkbox], -#qunit-testrunner-toolbar input[type=radio] { - margin: 3px; - vertical-align: -2px; -} - -#qunit-testrunner-toolbar input[type=text] { - box-sizing: border-box; - height: 1.6em; -} - -.qunit-url-config, -.qunit-filter, -#qunit-modulefilter { - display: inline-block; - line-height: 2.1em; -} - -.qunit-filter, -#qunit-modulefilter { - float: right; - position: relative; - margin-left: 1em; -} - -.qunit-url-config label { - margin-right: 0.5em; -} - -#qunit-modulefilter-search { - box-sizing: border-box; - width: 400px; -} - -#qunit-modulefilter-search-container:after { - position: absolute; - right: 0.3em; - content: "\25bc"; - color: black; -} - -#qunit-modulefilter-dropdown { - /* align with #qunit-modulefilter-search */ - box-sizing: border-box; - width: 400px; - position: absolute; - right: 0; - top: 50%; - margin-top: 0.8em; - - border: 1px solid #D3D3D3; - border-top: none; - border-radius: 0 0 .25em .25em; - color: #000; - background-color: #F5F5F5; - z-index: 99; -} - -#qunit-modulefilter-dropdown a { - color: inherit; - text-decoration: none; -} - -#qunit-modulefilter-dropdown .clickable.checked { - font-weight: bold; - color: #000; - background-color: #D2E0E6; -} - -#qunit-modulefilter-dropdown .clickable:hover { - color: #FFF; - background-color: #0D3349; -} - -#qunit-modulefilter-actions { - display: block; - overflow: auto; - - /* align with #qunit-modulefilter-dropdown-list */ - font: smaller/1.5em sans-serif; -} - -#qunit-modulefilter-dropdown #qunit-modulefilter-actions > * { - box-sizing: border-box; - max-height: 2.8em; - display: block; - padding: 0.4em; -} - -#qunit-modulefilter-dropdown #qunit-modulefilter-actions > button { - float: right; - font: inherit; -} - -#qunit-modulefilter-dropdown #qunit-modulefilter-actions > :last-child { - /* insert padding to align with checkbox margins */ - padding-left: 3px; -} - -#qunit-modulefilter-dropdown-list { - max-height: 200px; - overflow-y: auto; - margin: 0; - border-top: 2px groove threedhighlight; - padding: 0.4em 0 0; - font: smaller/1.5em sans-serif; -} - -#qunit-modulefilter-dropdown-list li { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -#qunit-modulefilter-dropdown-list .clickable { - display: block; - padding-left: 0.15em; -} - - -/** Tests: Pass/Fail */ - -#qunit-tests { - list-style-position: inside; -} - -#qunit-tests li { - padding: 0.4em 1em 0.4em 1em; - border-bottom: 1px solid #FFF; - list-style-position: inside; -} - -#qunit-tests > li { - display: none; -} - -#qunit-tests li.running, -#qunit-tests li.pass, -#qunit-tests li.fail, -#qunit-tests li.skipped, -#qunit-tests li.aborted { - display: list-item; -} - -#qunit-tests.hidepass { - position: relative; -} - -#qunit-tests.hidepass li.running, -#qunit-tests.hidepass li.pass:not(.todo) { - visibility: hidden; - position: absolute; - width: 0; - height: 0; - padding: 0; - border: 0; - margin: 0; -} - -#qunit-tests li strong { - cursor: pointer; -} - -#qunit-tests li.skipped strong { - cursor: default; -} - -#qunit-tests li a { - padding: 0.5em; - color: #C2CCD1; - text-decoration: none; -} - -#qunit-tests li p a { - padding: 0.25em; - color: #6B6464; -} -#qunit-tests li a:hover, -#qunit-tests li a:focus { - color: #000; -} - -#qunit-tests li .runtime { - float: right; - font-size: smaller; -} - -.qunit-assert-list { - margin-top: 0.5em; - padding: 0.5em; - - background-color: #FFF; - - border-radius: 5px; -} - -.qunit-source { - margin: 0.6em 0 0.3em; -} - -.qunit-collapsed { - display: none; -} - -#qunit-tests table { - border-collapse: collapse; - margin-top: 0.2em; -} - -#qunit-tests th { - text-align: right; - vertical-align: top; - padding: 0 0.5em 0 0; -} - -#qunit-tests td { - vertical-align: top; -} - -#qunit-tests pre { - margin: 0; - white-space: pre-wrap; - word-wrap: break-word; -} - -#qunit-tests del { - color: #374E0C; - background-color: #E0F2BE; - text-decoration: none; -} - -#qunit-tests ins { - color: #500; - background-color: #FFCACA; - text-decoration: none; -} - -/*** Test Counts */ - -#qunit-tests b.counts { color: #000; } -#qunit-tests b.passed { color: #5E740B; } -#qunit-tests b.failed { color: #710909; } - -#qunit-tests li li { - padding: 5px; - background-color: #FFF; - border-bottom: none; - list-style-position: inside; -} - -/*** Passing Styles */ - -#qunit-tests li li.pass { - color: #3C510C; - background-color: #FFF; - border-left: 10px solid #C6E746; -} - -#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } -#qunit-tests .pass .test-name { color: #366097; } - -#qunit-tests .pass .test-actual, -#qunit-tests .pass .test-expected { color: #999; } - -#qunit-banner.qunit-pass { background-color: #C6E746; } - -/*** Failing Styles */ - -#qunit-tests li li.fail { - color: #710909; - background-color: #FFF; - border-left: 10px solid #EE5757; - white-space: pre; -} - -#qunit-tests > li:last-child { - border-radius: 0 0 5px 5px; -} - -#qunit-tests .fail { color: #000; background-color: #EE5757; } -#qunit-tests .fail .test-name, -#qunit-tests .fail .module-name { color: #000; } - -#qunit-tests .fail .test-actual { color: #EE5757; } -#qunit-tests .fail .test-expected { color: #008000; } - -#qunit-banner.qunit-fail { background-color: #EE5757; } - - -/*** Aborted tests */ -#qunit-tests .aborted { color: #000; background-color: orange; } -/*** Skipped tests */ - -#qunit-tests .skipped { - background-color: #EBECE9; -} - -#qunit-tests .qunit-todo-label, -#qunit-tests .qunit-skipped-label { - background-color: #F4FF77; - display: inline-block; - font-style: normal; - color: #366097; - line-height: 1.8em; - padding: 0 0.5em; - margin: -0.4em 0.4em -0.4em 0; -} - -#qunit-tests .qunit-todo-label { - background-color: #EEE; -} - -/** Result */ - -#qunit-testresult { - color: #2B81AF; - background-color: #D2E0E6; - - border-bottom: 1px solid #FFF; -} -#qunit-testresult .clearfix { - height: 0; - clear: both; -} -#qunit-testresult .module-name { - font-weight: 700; -} -#qunit-testresult-display { - padding: 0.5em 1em 0.5em 1em; - width: 85%; - float:left; -} -#qunit-testresult-controls { - padding: 0.5em 1em 0.5em 1em; - width: 10%; - float:left; -} - -/** Fixture */ - -#qunit-fixture { - position: absolute; - top: -10000px; - left: -10000px; - width: 1000px; - height: 1000px; -} diff --git a/yarn.lock b/yarn.lock index 8c7d81dc03..71f20688cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -94,6 +94,10 @@ version "0.0.28" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-0.0.28.tgz#5562519bc7963caca8abf7f128cae3b594d41d06" +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + acorn-jsx@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" @@ -149,12 +153,42 @@ ansi-styles@^3.1.0: dependencies: color-convert "^1.9.0" +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + argparse@^1.0.7: version "1.0.9" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" dependencies: sprintf-js "~1.0.2" +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -165,14 +199,26 @@ array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + babel-code-frame@^6.22.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -200,6 +246,18 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + brace-expansion@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" @@ -207,10 +265,45 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +bser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" + dependencies: + node-int64 "^0.4.0" + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -221,6 +314,12 @@ callsites@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" +capture-exit@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" + dependencies: + rsvp "^3.3.3" + chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -243,6 +342,10 @@ chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + chrome-launcher@^0.10: version "0.10.2" resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.10.2.tgz#f7d860ddec627b6f01015736b5ae1e33b3d165b1" @@ -267,6 +370,15 @@ circular-json@^0.3.1: version "0.3.3" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -281,6 +393,17 @@ co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + color-convert@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" @@ -295,6 +418,14 @@ commander@2.11.x: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" +commander@2.12.2: + version "2.12.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" + +component-emitter@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -316,6 +447,14 @@ concat-stream@^1.6.0: readable-stream "^2.2.2" typedarray "^0.0.6" +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -328,7 +467,7 @@ cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" -debug@2.6.9, debug@^2.6.8: +debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -340,10 +479,37 @@ debug@^3.1.0: dependencies: ms "2.0.0" +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + del@^2.0.2: version "2.2.2" resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" @@ -356,12 +522,28 @@ del@^2.0.2: pinkie-promise "^2.0.0" rimraf "^2.2.8" +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" dependencies: esutils "^2.0.2" +ensure-posix-path@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ensure-posix-path/-/ensure-posix-path-1.0.2.tgz#a65b3e42d0b71cfc585eb774f9943c8d9b91b0c2" + es6-promise@^4.0.3: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" @@ -462,6 +644,47 @@ esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" +exec-sh@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" + dependencies: + merge "^1.2.0" + +exists-stat@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/exists-stat/-/exists-stat-1.0.0.tgz#0660e3525a2e89d9e446129440c272edfa24b529" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + dependencies: + homedir-polyfill "^1.0.1" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + external-editor@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48" @@ -470,6 +693,19 @@ external-editor@^2.0.4: iconv-lite "^0.4.17" tmp "^0.0.33" +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + extract-zip@^1.6.5: version "1.6.7" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" @@ -495,6 +731,12 @@ fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + dependencies: + bser "^2.0.0" + fd-slicer@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" @@ -514,6 +756,24 @@ file-entry-cache@^2.0.0: flat-cache "^1.2.1" object-assign "^4.0.1" +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +findup-sync@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" + dependencies: + detect-file "^1.0.0" + is-glob "^3.1.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + flat-cache@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" @@ -523,14 +783,54 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + dependencies: + map-cache "^0.2.2" + +fs-minipass@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + dependencies: + minipass "^2.2.1" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" +fsevents@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" + dependencies: + nan "^2.9.2" + node-pre-gyp "^0.10.0" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -542,6 +842,24 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + globals@^11.0.1: version "11.1.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.1.0.tgz#632644457f5f0e3ae711807183700ebf2e4633e4" @@ -575,6 +893,43 @@ has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +homedir-polyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" + dependencies: + parse-passwd "^1.0.0" + https-proxy-agent@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" @@ -586,6 +941,18 @@ iconv-lite@^0.4.17: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" +iconv-lite@^0.4.4: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + dependencies: + minimatch "^3.0.4" + ignore@^3.3.3: version "3.3.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" @@ -605,6 +972,10 @@ inherits@2, inherits@^2.0.3, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" +ini@^1.3.4, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + inquirer@^3.0.6: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" @@ -630,10 +1001,86 @@ invariant@^2.2.0: dependencies: loose-envify "^1.0.0" +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + dependencies: + kind-of "^6.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + is-path-cwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" @@ -650,6 +1097,12 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" @@ -658,11 +1111,15 @@ is-resolvable@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.1.tgz#acca1cd36dbe44b974b924321555a70ba03b1cf4" +is-windows@^1.0.1, is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + is-wsl@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" -isarray@~1.0.0: +isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -670,6 +1127,20 @@ isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + +js-reporters@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/js-reporters/-/js-reporters-1.2.1.tgz#f88c608e324a3373a95bcc45ad305e5c979c459b" + js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -693,6 +1164,26 @@ json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" @@ -723,6 +1214,50 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + dependencies: + tmpl "1.0.x" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + dependencies: + object-visit "^1.0.0" + +matcher-collection@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-1.0.5.tgz#2ee095438372cb8884f058234138c05c644ec339" + dependencies: + minimatch "^3.0.2" + +merge@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + +micromatch@^3.0.4, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + mime@^2.0.3: version "2.3.1" resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" @@ -741,7 +1276,31 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -mkdirp@0.5.1, mkdirp@^0.5.1: +minimist@^1.1.1, minimist@^1.2.0: + version "1.2.0" + resolved "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +minipass@^2.2.1, minipass@^2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957" + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" + dependencies: + minipass "^2.2.1" + +mixin-deep@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -755,14 +1314,118 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" +nan@^2.9.2: + version "2.11.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.0.tgz#574e360e4d954ab16966ec102c0c049fd961a099" + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" -object-assign@^4.0.1: +needle@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.3.tgz#c1b04da378cd634d8befe2de965dc2cfb0fd65ca" + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + +node-pre-gyp@^0.10.0: + version "0.10.3" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +npm-bundled@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" + +npm-packlist@^1.1.6: + version "1.1.11" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de" + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + dependencies: + isobject "^3.0.0" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + dependencies: + isobject "^3.0.1" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -786,10 +1449,29 @@ optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" -os-tmpdir@~1.0.2: +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -798,6 +1480,10 @@ path-is-inside@^1.0.1, path-is-inside@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" +path-parse@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -820,6 +1506,10 @@ pluralize@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -839,6 +1529,10 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + progress@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" @@ -864,6 +1558,39 @@ puppeteer@1.4: rimraf "^2.6.1" ws "^3.0.0" +qunit@2.6: + version "2.6.2" + resolved "https://registry.yarnpkg.com/qunit/-/qunit-2.6.2.tgz#551210c5cf857258a4fe39a7fe15d9e14dfef22c" + dependencies: + commander "2.12.2" + exists-stat "1.0.0" + findup-sync "2.0.0" + js-reporters "1.2.1" + resolve "1.5.0" + sane "^2.5.2" + walk-sync "0.3.2" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.6: + version "2.3.6" + resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@^2.2.2: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" @@ -876,10 +1603,29 @@ readable-stream@^2.2.2: string_decoder "~1.0.3" util-deprecate "~1.0.1" +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + regexpp@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + require-uncached@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" @@ -887,10 +1633,27 @@ require-uncached@^1.0.3: caller-path "^0.1.0" resolve-from "^1.0.0" +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + +resolve@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + dependencies: + path-parse "^1.0.5" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -898,6 +1661,10 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + rimraf@^2.2.8, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" @@ -908,6 +1675,10 @@ route-recognizer@^0.3.3: version "0.3.4" resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3" +rsvp@^3.3.3: + version "3.6.2" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -924,14 +1695,69 @@ rx-lite@*, rx-lite@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" +safe-buffer@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +sane@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa" + dependencies: + anymatch "^2.0.0" + capture-exit "^1.2.0" + exec-sh "^0.2.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.18.0" + optionalDependencies: + fsevents "^1.2.3" + +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + semver@^5.3.0: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-value@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -942,7 +1768,7 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" -signal-exit@^3.0.2: +signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -952,15 +1778,77 @@ slice-ansi@1.0.0: dependencies: is-fullwidth-code-point "^2.0.0" -source-map@^0.5.0: +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + +source-map@^0.5.0, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + dependencies: + extend-shallow "^3.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" -string-width@^2.1.0, string-width@^2.1.1: +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2", string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" dependencies: @@ -973,7 +1861,13 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" -strip-ansi@^3.0.0: +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" dependencies: @@ -1010,6 +1904,18 @@ table@4.0.2: slice-ansi "1.0.0" string-width "^2.1.1" +tar@^4: + version "4.4.6" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" + dependencies: + chownr "^1.0.1" + fs-minipass "^1.2.5" + minipass "^2.3.3" + minizlib "^1.1.0" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.2" + text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -1024,10 +1930,36 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" @@ -1046,16 +1978,72 @@ ultron@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" +union-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" +walk-sync@0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.2.tgz#4827280afc42d0e035367c4a4e31eeac0d136f75" + dependencies: + ensure-posix-path "^1.0.0" + matcher-collection "^1.0.0" + +walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + dependencies: + makeerror "1.0.x" + +watch@~0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" + dependencies: + exec-sh "^0.2.0" + minimist "^1.2.0" + +which@^1.2.14: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + dependencies: + isexe "^2.0.0" + which@^1.2.9: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" dependencies: isexe "^2.0.0" +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + dependencies: + string-width "^1.0.2 || 2" + wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -1090,6 +2078,10 @@ yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" +yallist@^3.0.0, yallist@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" + yauzl@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" From e847bb33d502ab7059dba0ffa147c0aa26a32bc2 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 11 Sep 2018 11:58:22 +0200 Subject: [PATCH 06/59] Better default settings for Japanese --- config/site_settings.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index d151832c73..37bcfc5bf5 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -479,6 +479,7 @@ posting: min: 1 default: 20 locale_default: + ja: 8 zh_CN: 8 zh_TW: 8 min_first_post_length: @@ -486,6 +487,7 @@ posting: min: 1 default: 20 locale_default: + ja: 8 zh_CN: 8 zh_TW: 8 min_personal_message_post_length: @@ -493,6 +495,7 @@ posting: min: 1 default: 10 locale_default: + ja: 3 zh_CN: 3 zh_TW: 3 max_post_length: @@ -513,6 +516,7 @@ posting: default: 15 min: 1 locale_default: + ja: 6 zh_CN: 6 zh_TW: 6 max_topic_title_length: @@ -529,7 +533,7 @@ posting: allow_uppercase_posts: default: false locale_default: - ja: false + ja: true title_prettify: default: true locale_default: @@ -551,6 +555,7 @@ posting: client: true default: 10 locale_default: + ja: 4 zh_CN: 4 zh_TW: 4 enable_personal_messages: @@ -629,6 +634,7 @@ posting: post_excerpt_maxlength: default: 300 locale_default: + ja: 120 zh_CN: 120 zh_TW: 120 show_pinned_excerpt_mobile: @@ -733,6 +739,7 @@ email: digest_min_excerpt_length: default: 100 locale_default: + ja: 50 zh_CN: 50 zh_TW: 50 digest_topics: @@ -1160,6 +1167,7 @@ onebox: post_onebox_maxlength: default: 500 locale_default: + ja: 200 zh_CN: 200 zh_TW: 200 onebox_domains_blacklist: @@ -1458,6 +1466,7 @@ uncategorized: default: 'ascii' enum: 'SlugSetting' locale_default: + ja: 'none' zh_CN: 'none' zh_TW: 'none' @@ -1601,6 +1610,7 @@ uncategorized: default: 500 client: true locale_default: + ja: 350 zh_CN: 350 zh_TW: 350 From 1a01385e886b52e1d2e77c5d38d81846a736727b Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 11 Sep 2018 13:20:42 +0200 Subject: [PATCH 07/59] FIX: "false" didn't work as locale_default --- lib/site_settings/defaults_provider.rb | 6 ++++-- .../site_settings/defaults_provider_spec.rb | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/site_settings/defaults_provider.rb b/lib/site_settings/defaults_provider.rb index 6340ff91b9..9ebae9aa51 100644 --- a/lib/site_settings/defaults_provider.rb +++ b/lib/site_settings/defaults_provider.rb @@ -38,8 +38,10 @@ class SiteSettings::DefaultsProvider end def get(name, locale = DEFAULT_LOCALE) - @defaults.dig(locale.to_sym, name.to_sym) || - @defaults.dig(DEFAULT_LOCALE.to_sym, name.to_sym) + value = @defaults.dig(locale.to_sym, name.to_sym) + return value unless value.nil? + + @defaults.dig(DEFAULT_LOCALE.to_sym, name.to_sym) end alias [] get diff --git a/spec/components/site_settings/defaults_provider_spec.rb b/spec/components/site_settings/defaults_provider_spec.rb index e4ca9a9109..948f66e0c0 100644 --- a/spec/components/site_settings/defaults_provider_spec.rb +++ b/spec/components/site_settings/defaults_provider_spec.rb @@ -37,14 +37,15 @@ describe SiteSettings::DefaultsProvider do describe 'expose default cache according to locale' do before do settings.setting(:test_override, 'default', locale_default: { zh_CN: 'cn' }) + settings.setting(:test_boolean_override, true, locale_default: { zh_CN: false }) settings.setting(:test_default, 'test', regex: '^\S+$') settings.refresh! end describe '.all' do it 'returns all values according to locale' do - expect(settings.defaults.all).to eq(test_override: 'default', test_default: 'test') - expect(settings.defaults.all('zh_CN')).to eq(test_override: 'cn', test_default: 'test') + expect(settings.defaults.all).to eq(test_override: 'default', test_default: 'test', test_boolean_override: true) + expect(settings.defaults.all('zh_CN')).to eq(test_override: 'cn', test_default: 'test', test_boolean_override: false) end end @@ -57,6 +58,15 @@ describe SiteSettings::DefaultsProvider do expect(settings.defaults.get('test_override')).to eq 'default' end + it 'returns the locale_default value if it exists' do + expect(settings.defaults.get(:test_override, :zh_CN)).to eq 'cn' + expect(settings.defaults.get(:test_override, :de)).to eq 'default' + expect(settings.defaults.get(:test_default, :zh_CN)).to eq 'test' + end + + it 'returns the correct locale_default for boolean site settings' do + expect(settings.defaults.get(:test_boolean_override, :zh_CN)).to eq false + end end describe '.set_regardless_of_locale' do From 3bb4f4c5efff46f3aa1607af53a0b0b3f7a02346 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 11 Sep 2018 12:02:06 -0400 Subject: [PATCH 08/59] Adds test to make sure moderators can't make master keys It wasn't obvious from the code, plus we'd never want this to regress! --- spec/requests/admin/api_controller_spec.rb | 87 +++++++++++++--------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/spec/requests/admin/api_controller_spec.rb b/spec/requests/admin/api_controller_spec.rb index feee32728b..9cf3b6dc73 100644 --- a/spec/requests/admin/api_controller_spec.rb +++ b/spec/requests/admin/api_controller_spec.rb @@ -7,57 +7,70 @@ describe Admin::ApiController do end let(:admin) { Fabricate(:admin) } - before do - sign_in(admin) - end - describe '#index' do - it "succeeds" do - get "/admin/api/keys.json" - expect(response.status).to eq(200) - end - end - - describe '#regenerate_key' do - let(:api_key) { Fabricate(:api_key) } - - it "returns 404 when there is no key" do - put "/admin/api/key.json", params: { id: 1234 } - expect(response.status).to eq(404) + context "as an admin" do + before do + sign_in(admin) end - it "delegates to the api key's `regenerate!` method" do - prev_value = api_key.key - put "/admin/api/key.json", params: { id: api_key.id } - expect(response.status).to eq(200) - - api_key.reload - expect(api_key.key).not_to eq(prev_value) - expect(api_key.created_by.id).to eq(admin.id) - end - end - - describe '#revoke_key' do - let(:api_key) { Fabricate(:api_key) } - - it "returns 404 when there is no key" do - delete "/admin/api/key.json", params: { id: 1234 } - expect(response.status).to eq(404) + describe '#index' do + it "succeeds" do + get "/admin/api/keys.json" + expect(response.status).to eq(200) + end end - it "delegates to the api key's `regenerate!` method" do - delete "/admin/api/key.json", params: { id: api_key.id } - expect(response.status).to eq(200) - expect(ApiKey.where(key: api_key.key).count).to eq(0) + describe '#regenerate_key' do + let(:api_key) { Fabricate(:api_key) } + + it "returns 404 when there is no key" do + put "/admin/api/key.json", params: { id: 1234 } + expect(response.status).to eq(404) + end + + it "delegates to the api key's `regenerate!` method" do + prev_value = api_key.key + put "/admin/api/key.json", params: { id: api_key.id } + expect(response.status).to eq(200) + + api_key.reload + expect(api_key.key).not_to eq(prev_value) + expect(api_key.created_by.id).to eq(admin.id) + end + end + + describe '#revoke_key' do + let(:api_key) { Fabricate(:api_key) } + + it "returns 404 when there is no key" do + delete "/admin/api/key.json", params: { id: 1234 } + expect(response.status).to eq(404) + end + + it "delegates to the api key's `regenerate!` method" do + delete "/admin/api/key.json", params: { id: api_key.id } + expect(response.status).to eq(200) + expect(ApiKey.where(key: api_key.key).count).to eq(0) + end end end describe '#create_master_key' do it "creates a record" do + sign_in(admin) expect do post "/admin/api/key.json" end.to change(ApiKey, :count).by(1) expect(response.status).to eq(200) end + + it "doesn't allow moderators to create master keys" do + sign_in(Fabricate(:moderator)) + expect do + post "/admin/api/key.json" + end.to change(ApiKey, :count).by(0) + expect(response.status).to eq(404) + end + end end From 02f582eca0958870f8466407aa28d4faac3ac323 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Tue, 11 Sep 2018 12:43:52 -0700 Subject: [PATCH 09/59] add recent security commits info --- docs/SECURITY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 01fc37fb19..27d4fefd37 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -6,6 +6,8 @@ We take security very seriously at Discourse. We welcome any peer review of our In order to give the community time to respond and upgrade we strongly urge you report all security issues privately. Please use our [vulnerability disclosure program at Hacker One](https://hackerone.com/discourse) to provide details and repro steps and we will respond ASAP. If you prefer not to use Hacker One, email us directly at `team@discourse.org` with details and repro steps. Security issues *always* take precedence over bug fixes and feature work. We can and do mark releases as "urgent" if they contain serious security fixes. +For a list of recent security commits, check [our GitHub commits prefixed with SECURITY](https://github.com/discourse/discourse/search?utf8=%E2%9C%93&q=SECURITY&type=Commits). + ### Password Storage Discourse uses the PBKDF2 algorithm to encrypt salted passwords. This algorithm is blessed by NIST. Security experts on the web [tend to agree that PBKDF2 is a secure choice](http://security.stackexchange.com/questions/4781/do-any-security-experts-recommend-bcrypt-for-password-storage). From f1cb4319686ce653ad9a6477c8b59c05a67b95ae Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Wed, 12 Sep 2018 09:03:12 +0800 Subject: [PATCH 10/59] FIX: rescue ActiveRecord::RecordInvalid in find_or_create_by_safe! (#6385) AR uniqueness validation could raise ActiveRecord::RecordInvalid --- lib/freedom_patches/active_record_base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/freedom_patches/active_record_base.rb b/lib/freedom_patches/active_record_base.rb index 4f71a4df38..39687ce2aa 100644 --- a/lib/freedom_patches/active_record_base.rb +++ b/lib/freedom_patches/active_record_base.rb @@ -15,7 +15,7 @@ class ActiveRecord::Base def self.find_or_create_by_safe!(hash) begin find_or_create_by!(hash) - rescue PG::UniqueViolation, ActiveRecord::RecordNotUnique + rescue PG::UniqueViolation, ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid # try again cause another transaction could have passed by now find_or_create_by!(hash) end From 16bd3f2cf26c8919766eac8af87b774f7c8ff60c Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Wed, 12 Sep 2018 04:04:58 +0300 Subject: [PATCH 11/59] FIX: use current user color scheme when filling `theme-color` attribute (#6384) * FIX: use current user color scheme when filling `meta` attribute `theme-color` * update manifest.webmanifest colors --- app/controllers/metadata_controller.rb | 4 ++-- app/helpers/application_helper.rb | 6 ++++++ app/models/color_scheme.rb | 12 +++++++----- app/models/theme.rb | 2 ++ app/views/layouts/_head.html.erb | 2 +- spec/models/color_scheme_spec.rb | 7 +++++++ 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/controllers/metadata_controller.rb b/app/controllers/metadata_controller.rb index 2305936dc6..1374eb57e2 100644 --- a/app/controllers/metadata_controller.rb +++ b/app/controllers/metadata_controller.rb @@ -27,8 +27,8 @@ class MetadataController < ApplicationController display: display, orientation: 'any', start_url: Discourse.base_uri.present? ? "#{Discourse.base_uri}/" : '.', - background_color: "##{ColorScheme.hex_for_name('secondary')}", - theme_color: "##{ColorScheme.hex_for_name('header_background')}", + background_color: "##{ColorScheme.hex_for_name('secondary', view_context.scheme_id)}", + theme_color: "##{ColorScheme.hex_for_name('header_background', view_context.scheme_id)}", icons: [ { src: logo, diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6ad17dfbce..2f6a1456c3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -362,6 +362,12 @@ module ApplicationHelper end end + def scheme_id + return if theme_ids.blank? + theme = Theme.find_by(id: theme_ids.first) + theme&.color_scheme_id + end + def current_homepage current_user&.user_option&.homepage || SiteSetting.anonymous_homepage end diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb index 9a47a14952..434765bef0 100644 --- a/app/models/color_scheme.rb +++ b/app/models/color_scheme.rb @@ -186,14 +186,16 @@ class ColorScheme < ActiveRecord::Base new_color_scheme end - def self.lookup_hex_for_name(name) - enabled_color_scheme = Theme.where(id: SiteSetting.default_theme_id).first&.color_scheme + def self.lookup_hex_for_name(name, scheme_id = nil) + enabled_color_scheme = find_by(id: scheme_id) if scheme_id + enabled_color_scheme ||= Theme.where(id: SiteSetting.default_theme_id).first&.color_scheme (enabled_color_scheme || base).colors.find { |c| c.name == name }.try(:hex) || "nil" end - def self.hex_for_name(name) - hex_cache[name] ||= lookup_hex_for_name(name) - hex_cache[name] == "nil" ? nil : hex_cache[name] + def self.hex_for_name(name, scheme_id = nil) + cache_key = scheme_id ? name + "_#{scheme_id}" : name + hex_cache[cache_key] ||= lookup_hex_for_name(name, scheme_id) + hex_cache[cache_key] == "nil" ? nil : hex_cache[cache_key] end def colors=(arr) diff --git a/app/models/theme.rb b/app/models/theme.rb index 71a4f793d5..467cc060fe 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -51,6 +51,7 @@ class Theme < ActiveRecord::Base remove_from_cache! clear_cached_settings! + ColorScheme.hex_cache.clear end after_destroy do @@ -72,6 +73,7 @@ class Theme < ActiveRecord::Base end Theme.expire_site_cache! + ColorScheme.hex_cache.clear end after_commit ->(theme) do diff --git a/app/views/layouts/_head.html.erb b/app/views/layouts/_head.html.erb index ee36992e6c..8edde92a2b 100644 --- a/app/views/layouts/_head.html.erb +++ b/app/views/layouts/_head.html.erb @@ -9,7 +9,7 @@ <%- if (SiteSetting.apple_touch_icon_url != "/images/default-apple-touch-icon.png") && SiteSetting.apple_touch_icon_url.present? %> <%- end %> - + <% if mobile_view? %> <% else %> diff --git a/spec/models/color_scheme_spec.rb b/spec/models/color_scheme_spec.rb index d1ea51532f..974f9aae1f 100644 --- a/spec/models/color_scheme_spec.rb +++ b/spec/models/color_scheme_spec.rb @@ -65,6 +65,13 @@ describe ColorScheme do expect(ColorScheme.hex_for_name('undefined')).to eq nil end + it "returns the base color for an attribute of a specified scheme" do + scheme = ColorScheme.create_from_base(name: "test scheme") + ColorSchemeRevisor.revise(scheme, colors: [{ name: "header_background", hex: "9dc927", default_hex: "949493" }]) + scheme.reload + expect(ColorScheme.hex_for_name("header_background", scheme.id)).to eq("9dc927") + end + it "returns the base color for an attribute" do expect(ColorScheme.hex_for_name('second_one')).to eq base_colors[:second_one] end From 921e2213b8dd92ee7823d9c5de2b144147709dbf Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 12 Sep 2018 03:12:28 +0200 Subject: [PATCH 12/59] FEATURE: Updated IPB import script * IPB import script replace PHP code tags with proper markdown remove excess newlines in code blocks decode HTML entities in code blocks add replacement for list items proper handling of attachments that are not images fix typo improved quote handling fix code style complaint from travis-ci build --- script/import_scripts/ipboard.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/script/import_scripts/ipboard.rb b/script/import_scripts/ipboard.rb index f1b10b6a5b..316aa983be 100644 --- a/script/import_scripts/ipboard.rb +++ b/script/import_scripts/ipboard.rb @@ -788,6 +788,10 @@ EOM raw.gsub!(/'/, "'") raw.gsub!(/\[url="(.+?)"\]http.+?\[\/url\]/, "\\1\n") raw.gsub!(/\[media\](.+?)\[\/media\]/, "\n\\1\n\n") + raw.gsub!(/\[php\](.+?)\[\/php\]/m) { |m| "\n\n```php\n\n" + @htmlentities.decode($1.gsub(/\n\n/, "\n")) + "\n\n```\n\n" } + raw.gsub!(/\[code\](.+?)\[\/code\]/m) { |m| "\n\n```\n\n" + @htmlentities.decode($1.gsub(/\n\n/, "\n")) + "\n\n```\n\n" } + raw.gsub!(/\[list\](.+?)\[\/list\]/m) { |m| "\n" + $1.gsub(/\[\*\]/, "\n- ") + "\n\n" } + raw.gsub!(/\[quote\]/, "\n[quote]\n") raw.gsub!(/\[\/quote\]/, "\n[/quote]\n") raw.gsub!(/date=\'(.+?)\'/, '') raw.gsub!(/timestamp=\'(.+?)\' /, '') @@ -829,9 +833,16 @@ EOM puts "Attachment #{attach_id} not found." attach_string = "Attachment #{attach_id} not found." else - attach_string = "#{attach_id}\n\n![#{attachments.first['filename']}](#{UPLOADS}/#{attachments.first['loc']})\n" + attach_url = "#{UPLOADS}/#{attachments.first['loc'].gsub(' ', '%20')}" + if attachments.first['filename'].match(/(png|jpg|jpeg|gif)$/) + # images are rendered as a link that contains the image + attach_string = "#{attach_id}\n\n[![#{attachments.first['filename']}](#{attach_url})](#{attach_url})\n" + else + # other attachments are simple download links + attach_string = "#{attach_id}\n\n[#{attachments.first['filename']}](#{attach_url})\n" + end end - raw.gsub!(attach_regex, attach_string) + raw.sub!(attach_regex, attach_string) end raw From d59e635a77044212daf9f995183ea1fb5e4538d5 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 10:03:44 +0800 Subject: [PATCH 13/59] Rename `FileHelper.images` to `FileHelper.supported_images`. --- lib/file_helper.rb | 8 +++----- lib/validators/upload_validator.rb | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/file_helper.rb b/lib/file_helper.rb index 21c0cd79bc..d5410e0005 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -100,14 +100,12 @@ class FileHelper ).optimize_image!(filename) end - private - - def self.images - @@images ||= Set.new %w{jpg jpeg png gif tif tiff bmp svg webp ico} + def self.supported_images + @@supported_images ||= Set.new %w{jpg jpeg png gif tif tiff bmp svg webp ico} end def self.images_regexp - @@images_regexp ||= /\.(#{images.to_a.join("|")})$/i + @@images_regexp ||= /\.(#{supported_images.to_a.join("|")})$/i end end diff --git a/lib/validators/upload_validator.rb b/lib/validators/upload_validator.rb index 60074a197f..72bc1febad 100644 --- a/lib/validators/upload_validator.rb +++ b/lib/validators/upload_validator.rb @@ -74,11 +74,11 @@ class Validators::UploadValidator < ActiveModel::Validator end def authorized_images(upload) - authorized_extensions(upload) & FileHelper.images + authorized_extensions(upload) & FileHelper.supported_images end def authorized_attachments(upload) - authorized_extensions(upload) - FileHelper.images + authorized_extensions(upload) - FileHelper.supported_images end def authorizes_all_extensions?(upload) From e1b16e445e523d652d6f94bb1549b4b01452ea62 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 10:22:45 +0800 Subject: [PATCH 14/59] Rename `FileHelper.is_image?` -> `FileHelper.is_supported_image?`. --- app/controllers/uploads_controller.rb | 2 +- app/jobs/regular/crawl_topic_link.rb | 2 +- app/models/upload.rb | 4 ++-- lib/email/receiver.rb | 2 +- lib/file_helper.rb | 8 ++++---- lib/file_store/s3_store.rb | 2 +- lib/upload_creator.rb | 4 ++-- lib/url_helper.rb | 2 +- lib/validators/upload_validator.rb | 2 +- script/downsize_uploads.rb | 2 +- script/import_scripts/base/uploader.rb | 2 +- 11 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 092c6a4dc9..922e4767e3 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -72,7 +72,7 @@ class UploadsController < ApplicationController content_type: MiniMime.lookup_by_filename(upload.original_filename)&.content_type, } opts[:disposition] = "inline" if params[:inline] - opts[:disposition] ||= "attachment" unless FileHelper.is_image?(upload.original_filename) + opts[:disposition] ||= "attachment" unless FileHelper.is_supported_image?(upload.original_filename) send_file(Discourse.store.path_for(upload), opts) else render_404 diff --git a/app/jobs/regular/crawl_topic_link.rb b/app/jobs/regular/crawl_topic_link.rb index 15c0c01ad0..54abf0a110 100644 --- a/app/jobs/regular/crawl_topic_link.rb +++ b/app/jobs/regular/crawl_topic_link.rb @@ -26,7 +26,7 @@ module Jobs # Special case: Images # If the link is to an image, put the filename as the title - if FileHelper.is_image?(topic_link.url) + if FileHelper.is_supported_image?(topic_link.url) uri = URI(topic_link.url) filename = File.basename(uri.path) crawled = (TopicLink.where(id: topic_link.id).update_all(["title = ?, crawled_at = CURRENT_TIMESTAMP", filename]) == 1) diff --git a/app/models/upload.rb b/app/models/upload.rb index 00f17cbead..51bd464493 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -112,7 +112,7 @@ class Upload < ActiveRecord::Base end def fix_dimensions! - return if !FileHelper.is_image?("image.#{extension}") + return if !FileHelper.is_supported_image?("image.#{extension}") path = if local? @@ -219,7 +219,7 @@ class Upload < ActiveRecord::Base upload.sha1 = Upload.generate_digest(path) end # optimize if image - FileHelper.optimize_image!(path) if FileHelper.is_image?(File.basename(path)) + FileHelper.optimize_image!(path) if FileHelper.is_supported_image?(File.basename(path)) # store to new location & update the filesize File.open(path) do |f| upload.url = Discourse.store.store_upload(f, upload) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 3419dbda06..0e445fdddc 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -910,7 +910,7 @@ module Email end def attachment_markdown(upload) - if FileHelper.is_image?(upload.original_filename) + if FileHelper.is_supported_image?(upload.original_filename) "" else "#{upload.original_filename} (#{number_to_human_size(upload.filesize)})" diff --git a/lib/file_helper.rb b/lib/file_helper.rb index d5410e0005..331b2636d8 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -11,8 +11,8 @@ class FileHelper ) end - def self.is_image?(filename) - filename =~ images_regexp + def self.is_supported_image?(filename) + filename =~ supported_images_regexp end class FakeIO @@ -104,8 +104,8 @@ class FileHelper @@supported_images ||= Set.new %w{jpg jpeg png gif tif tiff bmp svg webp ico} end - def self.images_regexp - @@images_regexp ||= /\.(#{supported_images.to_a.join("|")})$/i + def self.supported_images_regexp + @@supported_images_regexp ||= /\.(#{supported_images.to_a.join("|")})$/i end end diff --git a/lib/file_store/s3_store.rb b/lib/file_store/s3_store.rb index aa444ba1fc..6aca50f9f4 100644 --- a/lib/file_store/s3_store.rb +++ b/lib/file_store/s3_store.rb @@ -39,7 +39,7 @@ module FileStore content_type: opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type } # add a "content disposition" header for "attachments" - options[:content_disposition] = "attachment; filename=\"#{filename}\"" unless FileHelper.is_image?(filename) + options[:content_disposition] = "attachment; filename=\"#{filename}\"" unless FileHelper.is_supported_image?(filename) # if this fails, it will throw an exception path = @s3_helper.upload(file, path, options) # return the upload url diff --git a/lib/upload_creator.rb b/lib/upload_creator.rb index 25d2eef45d..2b138950e7 100644 --- a/lib/upload_creator.rb +++ b/lib/upload_creator.rb @@ -37,8 +37,8 @@ class UploadCreator # test for image regardless of input @image_info = FastImage.new(@file) rescue nil - is_image = FileHelper.is_image?(@filename) - is_image ||= @image_info && FileHelper.is_image?("test.#{@image_info.type}") + is_image = FileHelper.is_supported_image?(@filename) + is_image ||= @image_info && FileHelper.is_supported_image?("test.#{@image_info.type}") if is_image extract_image_info! diff --git a/lib/url_helper.rb b/lib/url_helper.rb index 5df27575f8..377c7d38cd 100644 --- a/lib/url_helper.rb +++ b/lib/url_helper.rb @@ -36,7 +36,7 @@ class UrlHelper uri = URI.parse(url) filename = File.basename(uri.path) - is_attachment = !FileHelper.is_image?(filename) + is_attachment = !FileHelper.is_supported_image?(filename) no_cdn = SiteSetting.login_required || SiteSetting.prevent_anons_from_downloading_files diff --git a/lib/validators/upload_validator.rb b/lib/validators/upload_validator.rb index 72bc1febad..b546ebf155 100644 --- a/lib/validators/upload_validator.rb +++ b/lib/validators/upload_validator.rb @@ -18,7 +18,7 @@ class Validators::UploadValidator < ActiveModel::Validator extension = File.extname(upload.original_filename)[1..-1] || "" if is_authorized?(upload, extension) - if FileHelper.is_image?(upload.original_filename) + if FileHelper.is_supported_image?(upload.original_filename) authorized_image_extension(upload, extension) maximum_image_file_size(upload) else diff --git a/script/downsize_uploads.rb b/script/downsize_uploads.rb index 9812ca7abf..fae1ab4374 100644 --- a/script/downsize_uploads.rb +++ b/script/downsize_uploads.rb @@ -11,7 +11,7 @@ Upload.where("lower(extension) in (?)", ['jpg', 'jpeg', 'gif', 'png', 'bmp', 'ti count += 1 print "\r%8d".freeze % count absolute_path = Discourse.store.path_for(upload) - if absolute_path && FileHelper.is_image?(upload.original_filename) + if absolute_path && FileHelper.is_supported_image?(upload.original_filename) file = File.new(absolute_path) rescue nil next unless file diff --git a/script/import_scripts/base/uploader.rb b/script/import_scripts/base/uploader.rb index 2d25a3e9ae..a9540112ba 100644 --- a/script/import_scripts/base/uploader.rb +++ b/script/import_scripts/base/uploader.rb @@ -40,7 +40,7 @@ module ImportScripts end def html_for_upload(upload, display_filename) - if FileHelper.is_image?(upload.url) + if FileHelper.is_supported_image?(upload.url) embedded_image_html(upload) else attachment_html(upload, display_filename) From 71caf7521d217babdcd2b87c2d9275d07de2d17c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 10 Sep 2018 10:49:01 +0800 Subject: [PATCH 15/59] Drop `tif`, `tiff`, `webp` and `bmp` from supported images. https://meta.discourse.org/t/cr2-raw-files-are-being-treated-as-tiff-files/96775/3?u=tgxworld --- lib/file_helper.rb | 2 +- spec/fixtures/images/webp_as.bin | Bin 0 -> 26 bytes spec/lib/upload_creator_spec.rb | 21 +++++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/images/webp_as.bin diff --git a/lib/file_helper.rb b/lib/file_helper.rb index 331b2636d8..f7d2a0907d 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -101,7 +101,7 @@ class FileHelper end def self.supported_images - @@supported_images ||= Set.new %w{jpg jpeg png gif tif tiff bmp svg webp ico} + @@supported_images ||= Set.new %w{jpg jpeg png gif svg ico} end def self.supported_images_regexp diff --git a/spec/fixtures/images/webp_as.bin b/spec/fixtures/images/webp_as.bin new file mode 100644 index 0000000000000000000000000000000000000000..2f8bf8fa1abcc8705d39a5815d7b0aad344c613f GIT binary patch literal 26 fcmWIYbaNA8U| Date: Wed, 12 Sep 2018 13:16:45 +1000 Subject: [PATCH 16/59] FIX: display a correct error when attempting to agree on a deferred flag Previously we would raise a 500 error if a moderator tried to agree on a flag another moderator deferred. This can happen cause the UX for flags does not live refresh as flags are handled --- app/controllers/admin/flags_controller.rb | 8 ++++++++ app/models/post_action.rb | 2 +- config/locales/server.en.yml | 3 +++ spec/requests/admin/flags_controller_spec.rb | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/flags_controller.rb b/app/controllers/admin/flags_controller.rb index 63f2ffdc24..a58dbaafcc 100644 --- a/app/controllers/admin/flags_controller.rb +++ b/app/controllers/admin/flags_controller.rb @@ -73,6 +73,14 @@ class Admin::FlagsController < Admin::AdminController post_action_type = PostAction.post_action_type_for_post(post.id) + if !post_action_type + render_json_error( + I18n.t("flags.errors.already_handled"), + status: 409 + ) + return + end + keep_post = ['silenced', 'suspended', 'keep'].include?(params[:action_on_post]) delete_post = params[:action_on_post] == "delete" restore_post = params[:action_on_post] == "restore" diff --git a/app/models/post_action.rb b/app/models/post_action.rb index b20d0650c4..67043291ca 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -633,7 +633,7 @@ class PostAction < ActiveRecord::Base def self.post_action_type_for_post(post_id) post_action = PostAction.find_by(deferred_at: nil, post_id: post_id, post_action_type_id: PostActionType.notify_flag_types.values, deleted_at: nil) - PostActionType.types[post_action.post_action_type_id] + PostActionType.types[post_action.post_action_type_id] if post_action end def self.target_moderators diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4e2767a502..ce8b16cd09 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -864,6 +864,9 @@ en: read: "Read all" write: "Write all" + flags: + errors: + already_handled: "Flag was already handled" reports: default: labels: diff --git a/spec/requests/admin/flags_controller_spec.rb b/spec/requests/admin/flags_controller_spec.rb index d252eedc77..13ed77dfe7 100644 --- a/spec/requests/admin/flags_controller_spec.rb +++ b/spec/requests/admin/flags_controller_spec.rb @@ -34,6 +34,21 @@ RSpec.describe Admin::FlagsController do end context '#agree' do + it 'should raise a reasonable error if a flag was deferred and then someone else agreed' do + SiteSetting.queue_jobs = false + + _post_action = PostAction.act(user, post_1, PostActionType.types[:spam], message: 'bad') + + post "/admin/flags/defer/#{post_1.id}.json" + expect(response.status).to eq(200) + + post "/admin/flags/agree/#{post_1.id}.json", params: { action_on_post: 'keep' } + # 409 means conflict which is what is happening here + expect(response.status).to eq(409) + error = JSON.parse(response.body)["errors"].first + expect(error).to eq(I18n.t("flags.errors.already_handled")) + end + it 'should be able to agree and keep content' do SiteSetting.queue_jobs = false From b3469bea2dc1e868a37d90faa97c0e8df4c0ffc1 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 11 Sep 2018 23:48:25 -0700 Subject: [PATCH 17/59] FIX: Uploads not being linked correctly to posts. Regression due to https://github.com/discourse/discourse/commit/1f636c445bcbc66b1765bcc3966887adcb14bdbf. --- app/models/upload.rb | 2 +- spec/models/upload_spec.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/upload.rb b/app/models/upload.rb index 51bd464493..a8c766a6c5 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -177,7 +177,7 @@ class Upload < ActiveRecord::Base end return if uri&.path.blank? - data = uri.path.match(/(\/original\/\dX\/[\/\.\w]+\/([a-zA-Z0-9]+)[\.\w]+)/) + data = uri.path.match(/(\/original\/\dX[\/\.\w]*\/([a-zA-Z0-9]+)[\.\w]+)/) return if data.blank? sha1 = data[2] upload = nil diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index b19bcd3d3e..35f0db6a9d 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -82,6 +82,15 @@ describe Upload do expect(Upload.get_from_url(upload.url)).to eq(upload) end + describe 'for a url without a tree' do + let(:url) { "/uploads/default/original/1X/#{sha1}.png" } + let(:upload) { Fabricate(:upload, url: url, sha1: sha1) } + + it 'should return the right upload' do + expect(Upload.get_from_url(upload.url)).to eq(upload) + end + end + it "works when using a cdn" do begin original_asset_host = Rails.configuration.action_controller.asset_host From 3884e99e8891125da92c30d33fa389c22c01e5e6 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Sep 2018 00:12:14 -0700 Subject: [PATCH 18/59] Add extra protection in `Upload#get_from_url`. In case the extension goes missing from the URL. --- app/models/upload.rb | 2 +- spec/models/upload_spec.rb | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/models/upload.rb b/app/models/upload.rb index a8c766a6c5..947481cb2f 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -177,7 +177,7 @@ class Upload < ActiveRecord::Base end return if uri&.path.blank? - data = uri.path.match(/(\/original\/\dX[\/\.\w]*\/([a-zA-Z0-9]+)[\.\w]+)/) + data = uri.path.match(/(\/original\/\dX[\/\.\w]*\/([a-zA-Z0-9]+)[\.\w]*)/) return if data.blank? sha1 = data[2] upload = nil diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 35f0db6a9d..0e86a0fb3a 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -82,9 +82,16 @@ describe Upload do expect(Upload.get_from_url(upload.url)).to eq(upload) end + describe 'for an extensionless url' do + let(:url) { "/uploads/default/original/1X/#{sha1}" } + + it 'should return the right upload' do + expect(Upload.get_from_url(upload.url)).to eq(upload) + end + end + describe 'for a url without a tree' do let(:url) { "/uploads/default/original/1X/#{sha1}.png" } - let(:upload) { Fabricate(:upload, url: url, sha1: sha1) } it 'should return the right upload' do expect(Upload.get_from_url(upload.url)).to eq(upload) From 6d01e0aa04e9e6756416816c82068a86c7c24542 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Sep 2018 01:06:51 -0700 Subject: [PATCH 19/59] DEV: Print the error class in `uploads:list_posts_with_broken_images`. --- lib/tasks/uploads.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index b190b3a5c8..242aa8e88b 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -764,7 +764,7 @@ def list_broken_posts(recover_from_s3: false) end end rescue => e - puts "#{post.full_url} Error: #{e.message}" + puts "#{post.full_url} Error: #{e.class}: #{e.message}" end end end From c053f8ccf6bbb97fef59f9ac3ab764c1c3530149 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Sep 2018 01:51:53 -0700 Subject: [PATCH 20/59] New rake task `uploads:recover`. --- lib/tasks/uploads.rake | 92 ++------------------------------- lib/upload_recovery.rb | 112 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 87 deletions(-) create mode 100644 lib/upload_recovery.rb diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 242aa8e88b..ea6e235706 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -716,96 +716,14 @@ task "uploads:fix_incorrect_extensions" => :environment do UploadFixer.fix_all_extensions end -task "uploads:list_posts_with_broken_images" => :environment do +task "uploads:recover" => :environment do + require_dependency "upload_recovery" + if ENV["RAILS_DB"] - list_broken_posts(recover_from_s3: !!ENV["RECOVER_FROM_S3"]) + UploadRecovery.new.recover else RailsMultisite::ConnectionManagement.each_connection do |db| - list_broken_posts(recover_from_s3: !!ENV["RECOVER_FROM_S3"]) - end - end -end - -def list_broken_posts(recover_from_s3: false) - object_keys = nil - - Post.where("raw LIKE '%upload:\/\/%'").find_each do |post| - begin - begin - analyzer = PostAnalyzer.new(post.raw, post.topic_id) - cooked_stripped = analyzer.send(:cooked_stripped) - end - - cooked_stripped.css("img").each do |img| - if dom_class = img["class"] - if (Post.white_listed_image_classes & dom_class.split).count > 0 - next - end - end - - if img["data-orig-src"] - puts "#{post.full_url} #{img["data-orig-src"]}" - - if recover_from_s3 && Discourse.store.external? - object_keys ||= begin - s3_helper = Discourse.store.s3_helper - - s3_helper.list("original").map(&:key).concat( - s3_helper.list("#{FileStore::S3Store::TOMBSTONE_PREFIX}original").map(&:key) - ) - end - - recover_from_s3_by_sha1( - post: post, - sha1: Upload.sha1_from_short_url(img["data-orig-src"]), - object_keys: object_keys - ) - end - end - end - rescue => e - puts "#{post.full_url} Error: #{e.class}: #{e.message}" - end - end -end - -def recover_from_s3_by_sha1(post:, sha1:, object_keys: []) - object_keys.each do |key| - if key =~ /#{sha1}/ - tombstone_prefix = FileStore::S3Store::TOMBSTONE_PREFIX - - if key.starts_with?(tombstone_prefix) - Discourse.store.s3_helper.copy( - key, - key.sub(tombstone_prefix, ""), - options: { acl: "public-read" } - ) - end - - url = "https:#{SiteSetting.Upload.absolute_base_url}/#{key}" - - begin - tmp = FileHelper.download( - url, - max_file_size: SiteSetting.max_image_size_kb.kilobytes, - tmp_file_name: "recover_from_s3" - ) - - if tmp - upload = UploadCreator.new( - tmp, - File.basename(key) - ).create_for(post.user_id) - - if upload.persisted? - post.rebake! - else - puts "#{post.full_url}: #{upload.errors.full_messages.join(", ")}" - end - end - ensure - tmp&.close - end + UploadRecovery.new.recover end end end diff --git a/lib/upload_recovery.rb b/lib/upload_recovery.rb new file mode 100644 index 0000000000..25f92eb399 --- /dev/null +++ b/lib/upload_recovery.rb @@ -0,0 +1,112 @@ +class UploadRecovery + def recover + Post.where("raw LIKE '%upload:\/\/%'").find_each do |post| + analyzer = PostAnalyzer.new(post.raw, post.topic_id) + cooked_stripped = analyzer.send(:cooked_stripped) + + cooked_stripped.css("img").each do |img| + if dom_class = img["class"] + if (Post.white_listed_image_classes & dom_class.split).count > 0 + next + end + end + + if img["data-orig-src"] + recover_post_upload(post, img["data-orig-src"]) + end + end + end + end + + private + + def recover_post_upload(post, short_url) + attributes = { + post: post, + sha1: Upload.sha1_from_short_url(short_url) + } + + if Discourse.store.external? + recover_from_s3(attributes) + else + recover_from_local(attributes) + end + end + + def recover_from_local(post:, sha1:) + public_path = Rails.root.join("public") + + @paths ||= begin + Dir.glob(File.join( + public_path, + 'uploads', + 'tombstone', + RailsMultisite::ConnectionManagement.current_db, + 'original', + '**', + '*.*' + )).concat(Dir.glob(File.join( + public_path, + 'uploads', + RailsMultisite::ConnectionManagement.current_db, + 'original', + '**', + '*.*' + ))) + end + + @paths.each do |path| + if path =~ /#{sha1}/ + begin + file = File.open(path, "r") + create_upload(file, File.basename(path), post) + ensure + file&.close + end + end + end + end + + def recover_from_s3(post:, sha1:) + @object_keys ||= begin + s3_helper = Discourse.store.s3_helper + + s3_helper.list("original").map(&:key).concat( + s3_helper.list("#{FileStore::S3Store::TOMBSTONE_PREFIX}original").map(&:key) + ) + end + + @object_keys.each do |key| + if key =~ /#{sha1}/ + tombstone_prefix = FileStore::S3Store::TOMBSTONE_PREFIX + + if key.starts_with?(tombstone_prefix) + Discourse.store.s3_helper.copy( + key, + key.sub(tombstone_prefix, ""), + options: { acl: "public-read" } + ) + end + + url = "https:#{SiteSetting.Upload.absolute_base_url}/#{key}" + + begin + tmp = FileHelper.download( + url, + max_file_size: SiteSetting.max_image_size_kb.kilobytes, + tmp_file_name: "recover_from_s3" + ) + + create_upload(tmp, File.basename(key), post) if tmp + ensure + tmp&.close + end + end + end + end + + def create_upload(file, filename, post) + upload = UploadCreator.new(tmp, filename).create_for(post.user_id) + post.rebake! if upload.persisted? + end +end From 2cc48cfd06716f157ec7b8e185e11cc6fbc29a2c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Sep 2018 02:06:14 -0700 Subject: [PATCH 21/59] Fix incorrect variable. --- lib/upload_recovery.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/upload_recovery.rb b/lib/upload_recovery.rb index 25f92eb399..90b21a36d8 100644 --- a/lib/upload_recovery.rb +++ b/lib/upload_recovery.rb @@ -106,7 +106,7 @@ class UploadRecovery end def create_upload(file, filename, post) - upload = UploadCreator.new(tmp, filename).create_for(post.user_id) + upload = UploadCreator.new(file, filename).create_for(post.user_id) post.rebake! if upload.persisted? end end From 38668818a5ba3c446008e5410f20119208533c45 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 12 Sep 2018 12:19:04 +0200 Subject: [PATCH 22/59] FIX: allows forcing unsafe string in select-kit (#6386) forceEscape will be defaulted to true before next release. --- .../admin/templates/customize-themes-show.hbs | 3 +- .../select-kit/components/list-setting.js.es6 | 9 +-- .../select-kit/components/multi-select.js.es6 | 9 +-- .../multi-select/multi-select-header.js.es6 | 2 + .../select-kit/components/select-kit.js.es6 | 13 +++- .../select-kit/select-kit-header.js.es6 | 2 + .../select-kit/select-kit-row.js.es6 | 2 + .../components/combo-box/combo-box-header.hbs | 6 +- .../future-date-input-selector-header.hbs | 6 +- .../multi-select/multi-select-header.hbs | 1 + .../components/multi-select/selected-name.hbs | 6 +- .../select-kit/select-kit-header.hbs | 6 +- .../components/select-kit/select-kit-row.hbs | 8 ++- .../components/tag-drop/tag-drop-header.hbs | 6 +- .../components/multi-select-test.js.es6 | 70 +++++++++++++++++++ .../components/single-select-test.js.es6 | 64 +++++++++++++++++ 16 files changed, 192 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs index 3ab5bf43ab..21090a03e9 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs @@ -40,6 +40,7 @@

      {{i18n "admin.customize.theme.color_scheme_select"}}

      {{combo-box content=colorSchemes filterable=true + forceEscape=true value=colorSchemeId icon="paint-brush"}} {{#if colorSchemeChanged}} @@ -142,7 +143,7 @@ {{/unless}} {{#if selectableChildThemes}}

      - {{combo-box filterable=true content=selectableChildThemes value=selectedChildThemeId}} + {{combo-box forceEscape=true filterable=true content=selectableChildThemes value=selectedChildThemeId}} {{#d-button action="addChildTheme" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}

      {{/if}} diff --git a/app/assets/javascripts/select-kit/components/list-setting.js.es6 b/app/assets/javascripts/select-kit/components/list-setting.js.es6 index 457cc62bba..6cf37e0589 100644 --- a/app/assets/javascripts/select-kit/components/list-setting.js.es6 +++ b/app/assets/javascripts/select-kit/components/list-setting.js.es6 @@ -17,12 +17,9 @@ export default MultiSelectComponent.extend({ } if (this.get("nameProperty").indexOf("color") > -1) { - this.set( - "headerComponentOptions", - Ember.Object.create({ - selectedNameComponent: "multi-select/selected-color" - }) - ); + this.get("headerComponentOptions").setProperties({ + selectedNameComponent: "multi-select/selected-color" + }); } }, diff --git a/app/assets/javascripts/select-kit/components/multi-select.js.es6 b/app/assets/javascripts/select-kit/components/multi-select.js.es6 index b2a71ebf88..79dc0c0f14 100644 --- a/app/assets/javascripts/select-kit/components/multi-select.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select.js.es6 @@ -31,12 +31,9 @@ export default SelectKitComponent.extend({ this.set("values", []); } - this.set( - "headerComponentOptions", - Ember.Object.create({ - selectedNameComponent: this.get("selectedNameComponent") - }) - ); + this.get("headerComponentOptions").setProperties({ + selectedNameComponent: this.get("selectedNameComponent") + }); }, @on("didRender") diff --git a/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 b/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 index b87881ec5b..9b82221f00 100644 --- a/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select/multi-select-header.js.es6 @@ -14,6 +14,8 @@ export default SelectKitHeaderComponent.extend({ "select-kit/templates/components/multi-select/multi-select-header", selectedNameComponent: Ember.computed.alias("options.selectedNameComponent"), + forceEscape: Ember.computed.alias("options.forceEscape"), + ariaLabel: Ember.computed.or("computedContent.ariaLabel", "title", "names"), title: Ember.computed.or("computedContent.title", "names"), diff --git a/app/assets/javascripts/select-kit/components/select-kit.js.es6 b/app/assets/javascripts/select-kit/components/select-kit.js.es6 index e490b61ea1..b69c1d5d04 100644 --- a/app/assets/javascripts/select-kit/components/select-kit.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit.js.es6 @@ -81,13 +81,22 @@ export default Ember.Component.extend( minimum: null, minimumLabel: null, maximumLabel: null, + forceEscape: false, init() { this._super(); this.noneValue = "__none__"; - this.set("headerComponentOptions", Ember.Object.create()); - this.set("rowComponentOptions", Ember.Object.create()); + this.set( + "headerComponentOptions", + Ember.Object.create({ forceEscape: this.get("forceEscape") }) + ); + this.set( + "rowComponentOptions", + Ember.Object.create({ + forceEscape: this.get("forceEscape") + }) + ); this.set("computedContent", []); this.set("highlightedSelection", []); diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 index 79464c9109..3253c5d9a2 100644 --- a/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 @@ -14,6 +14,8 @@ export default Ember.Component.extend({ "name:data-name" ], + forceEscape: Ember.computed.alias("options.forceEscape"), + isNone: Ember.computed.none("computedContent.value"), ariaHasPopup: true, diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 index a289beeccf..a1c7f6ba4a 100644 --- a/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-row.js.es6 @@ -18,6 +18,8 @@ export default Ember.Component.extend(UtilsMixin, { ], classNameBindings: ["isHighlighted", "isSelected"], + forceEscape: Ember.computed.alias("options.forceEscape"), + ariaLabel: Ember.computed.or("computedContent.ariaLabel", "title"), @computed("computedContent.title", "name") diff --git a/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs b/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs index b13075d3f2..c9fc965ee8 100644 --- a/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs @@ -1,7 +1,11 @@ {{#each icons as |icon|}} {{d-icon icon}} {{/each}} - {{{label}}} + {{#if forceEscape}} + {{label}} + {{else}} + {{{label}}} + {{/if}} {{#if shouldDisplayClearableButton}} diff --git a/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs index c566d3b1e5..a5c5cbd6a3 100644 --- a/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/future-date-input-selector/future-date-input-selector-header.hbs @@ -5,7 +5,11 @@ {{/if}} - {{{label}}} + {{#if forceEscape}} + {{label}} + {{else}} + {{{label}}} + {{/if}} {{#if computedContent.datetime}} diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs index 2627302c4e..e95d429d27 100644 --- a/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/multi-select-header.hbs @@ -3,6 +3,7 @@ {{component selectedNameComponent onClickSelectionItem=onClickSelectionItem highlightedSelection=highlightedSelection + forceEscape=forceEscape computedContent=selection}} {{/each}} diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs index ffa6a8f3b9..141ae2748f 100644 --- a/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs +++ b/app/assets/javascripts/select-kit/templates/components/multi-select/selected-name.hbs @@ -2,7 +2,11 @@
      - {{{label}}} + {{#if forceEscape}} + {{label}} + {{else}} + {{{label}}} + {{/if}}
      diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs index e65ce13500..77ea532816 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-header.hbs @@ -1,5 +1,9 @@ {{#each icons as |icon|}} {{d-icon icon}} {{/each}} - {{{label}}} + {{#if forceEscape}} + {{label}} + {{else}} + {{{label}}} + {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs index fc0df65835..984d709151 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs @@ -2,5 +2,11 @@ {{{template}}} {{else}} {{#each icons as |icon|}} {{d-icon icon}} {{/each}} - {{{label}}} + + {{#if forceEscape}} + {{label}} + {{else}} + {{{label}}} + {{/if}} + {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/tag-drop/tag-drop-header.hbs b/app/assets/javascripts/select-kit/templates/components/tag-drop/tag-drop-header.hbs index 7e3ea1baf4..440988e3ef 100644 --- a/app/assets/javascripts/select-kit/templates/components/tag-drop/tag-drop-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/tag-drop/tag-drop-header.hbs @@ -1,5 +1,9 @@ - {{{label}}} + {{#if forceEscape}} + {{label}} + {{else}} + {{{label}}} + {{/if}} {{d-icon caretIcon class="caret-icon fa-fw"}} diff --git a/test/javascripts/components/multi-select-test.js.es6 b/test/javascripts/components/multi-select-test.js.es6 index b1f78fdfae..ea13a61908 100644 --- a/test/javascripts/components/multi-select-test.js.es6 +++ b/test/javascripts/components/multi-select-test.js.es6 @@ -247,3 +247,73 @@ componentTest("with minimumLabel", { ); } }); + +componentTest("with forceEscape", { + template: "{{multi-select content=content forceEscape=true}}", + + beforeEach() { + this.set("content", ["
      sam
      "]); + }, + + async test(assert) { + await this.get("subject").expand(); + + const row = this.get("subject").rowByIndex(0); + assert.equal( + row + .el() + .find(".name") + .html() + .trim(), + "<div>sam</div>" + ); + + await this.get("subject").fillInFilter("
      jeff
      "); + await this.get("subject").keyboard("enter"); + + assert.equal( + this.get("subject") + .header() + .el() + .find(".name") + .html() + .trim(), + "<div>jeff</div>" + ); + } +}); + +componentTest("with forceEscape", { + template: "{{multi-select content=content forceEscape=false}}", + + beforeEach() { + this.set("content", ["
      sam
      "]); + }, + + async test(assert) { + await this.get("subject").expand(); + + const row = this.get("subject").rowByIndex(0); + assert.equal( + row + .el() + .find(".name") + .html() + .trim(), + "
      sam
      " + ); + + await this.get("subject").fillInFilter("
      jeff
      "); + await this.get("subject").keyboard("enter"); + + assert.equal( + this.get("subject") + .header() + .el() + .find(".name") + .html() + .trim(), + "
      jeff
      " + ); + } +}); diff --git a/test/javascripts/components/single-select-test.js.es6 b/test/javascripts/components/single-select-test.js.es6 index 7e6c6158a4..439781e202 100644 --- a/test/javascripts/components/single-select-test.js.es6 +++ b/test/javascripts/components/single-select-test.js.es6 @@ -771,3 +771,67 @@ componentTest("with no content and allowAny", { assert.ok(!$filter.hasClass("is-hidden")); } }); + +componentTest("with forceEscape", { + template: "{{single-select content=content forceEscape=true}}", + + beforeEach() { + this.set("content", ["
      sam
      "]); + }, + + async test(assert) { + await this.get("subject").expand(); + + const row = this.get("subject").rowByIndex(0); + assert.equal( + row + .el() + .find(".name") + .html() + .trim(), + "<div>sam</div>" + ); + + assert.equal( + this.get("subject") + .header() + .el() + .find(".selected-name") + .html() + .trim(), + "<div>sam</div>" + ); + } +}); + +componentTest("without forceEscape", { + template: "{{single-select content=content forceEscape=false}}", + + beforeEach() { + this.set("content", ["
      sam
      "]); + }, + + async test(assert) { + await this.get("subject").expand(); + + const row = this.get("subject").rowByIndex(0); + assert.equal( + row + .el() + .find(".name") + .html() + .trim(), + "
      sam
      " + ); + + assert.equal( + this.get("subject") + .header() + .el() + .find(".selected-name") + .html() + .trim(), + "
      sam
      " + ); + } +}); From 24c55bd61314dec667bcbb4cb148d812cd526c57 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 12 Sep 2018 21:53:01 +0800 Subject: [PATCH 23/59] Add dry run option to `UploadRecovery`. --- lib/tasks/uploads.rake | 6 ++++-- lib/upload_recovery.rb | 10 +++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index ea6e235706..559136dd22 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -719,11 +719,13 @@ end task "uploads:recover" => :environment do require_dependency "upload_recovery" + dry_run = ENV["DRY_RUN"].present? + if ENV["RAILS_DB"] - UploadRecovery.new.recover + UploadRecovery.new(dry_run: dry_run).recover else RailsMultisite::ConnectionManagement.each_connection do |db| - UploadRecovery.new.recover + UploadRecovery.new(dry_run: dry_run).recover end end end diff --git a/lib/upload_recovery.rb b/lib/upload_recovery.rb index 90b21a36d8..176ba6ca14 100644 --- a/lib/upload_recovery.rb +++ b/lib/upload_recovery.rb @@ -1,4 +1,8 @@ class UploadRecovery + def initialize(dry_run: false) + @dry_run = dry_run + end + def recover Post.where("raw LIKE '%upload:\/\/%'").find_each do |post| analyzer = PostAnalyzer.new(post.raw, post.topic_id) @@ -12,7 +16,11 @@ class UploadRecovery end if img["data-orig-src"] - recover_post_upload(post, img["data-orig-src"]) + if @dry_run + puts "#{post.full_url} #{img["data-orig-src"]}" + else + recover_post_upload(post, img["data-orig-src"]) + end end end end From 3a00c2adeb861d3d8a86183ddfbe739d8c543702 Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Wed, 12 Sep 2018 10:13:20 -0400 Subject: [PATCH 24/59] add test to ensure that userA cannot see drafts stream of userB --- spec/requests/drafts_controller_spec.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spec/requests/drafts_controller_spec.rb b/spec/requests/drafts_controller_spec.rb index bffc3900b5..d18b15ec85 100644 --- a/spec/requests/drafts_controller_spec.rb +++ b/spec/requests/drafts_controller_spec.rb @@ -24,4 +24,15 @@ describe DraftsController do parsed = JSON.parse(response.body) expect(parsed["drafts"].length).to eq(0) end + + it 'does not let userA see drafts by userB' do + userB = Fabricate(:user) + Draft.set(userB, 'xxx', 0, '{}') + + userA = sign_in(Fabricate(:user)) + get "/drafts.json", params: { username: userB.username } + expect(response.status).to eq(200) + parsed = JSON.parse(response.body) + expect(parsed["drafts"].length).to eq(0) + end end From 11fd18b254808450b549458bca8b27bea2f2b108 Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Wed, 12 Sep 2018 11:06:30 -0400 Subject: [PATCH 25/59] code-styling fixes --- spec/requests/drafts_controller_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/requests/drafts_controller_spec.rb b/spec/requests/drafts_controller_spec.rb index d18b15ec85..1521173aa0 100644 --- a/spec/requests/drafts_controller_spec.rb +++ b/spec/requests/drafts_controller_spec.rb @@ -26,11 +26,11 @@ describe DraftsController do end it 'does not let userA see drafts by userB' do - userB = Fabricate(:user) - Draft.set(userB, 'xxx', 0, '{}') + user_b = Fabricate(:user) + Draft.set(user_b, 'xxx', 0, '{}') - userA = sign_in(Fabricate(:user)) - get "/drafts.json", params: { username: userB.username } + sign_in(Fabricate(:user)) + get "/drafts.json", params: { username: user_b.username } expect(response.status).to eq(200) parsed = JSON.parse(response.body) expect(parsed["drafts"].length).to eq(0) From b8c0a29bec73530fd7bf50e37fe594e218608ec7 Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Wed, 12 Sep 2018 11:09:30 -0400 Subject: [PATCH 26/59] better test name --- spec/requests/drafts_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/drafts_controller_spec.rb b/spec/requests/drafts_controller_spec.rb index 1521173aa0..141b790d6a 100644 --- a/spec/requests/drafts_controller_spec.rb +++ b/spec/requests/drafts_controller_spec.rb @@ -25,7 +25,7 @@ describe DraftsController do expect(parsed["drafts"].length).to eq(0) end - it 'does not let userA see drafts by userB' do + it 'does not let a user see drafts stream of another user' do user_b = Fabricate(:user) Draft.set(user_b, 'xxx', 0, '{}') From 26bd67a865563a071e7ae911b3dc74d6e4258753 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 12 Sep 2018 16:43:21 +0100 Subject: [PATCH 27/59] DEV: Add travis_fold statements to docker_test --- lib/tasks/docker.rake | 12 +++++++++--- script/docker_test.rb | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/tasks/docker.rake b/lib/tasks/docker.rake index 6a7088dfd3..a92c5529ed 100644 --- a/lib/tasks/docker.rake +++ b/lib/tasks/docker.rake @@ -39,6 +39,7 @@ task 'docker:test' do begin @good = true unless ENV['SKIP_LINT'] + puts "travis_fold:start:lint" if ENV["TRAVIS"] puts "Running linters/prettyfiers" puts "eslint #{`yarn eslint -v`}" puts "prettier #{`yarn prettier -v`}" @@ -64,9 +65,11 @@ task 'docker:test' do @good &&= run_or_fail('yarn prettier --list-different "plugins/**/*.scss" "plugins/**/*.es6"') end end + puts "travis_fold:end:lint" if ENV["TRAVIS"] end unless ENV['SKIP_TESTS'] + puts "travis_fold:start:prepare_tests" if ENV["TRAVIS"] puts "Cleaning up old test tmp data in tmp/test_data" `rm -fr tmp/test_data && mkdir -p tmp/test_data/redis && mkdir tmp/test_data/pg` @@ -94,8 +97,10 @@ task 'docker:test' do @good &&= run_or_fail("bundle exec rake db:migrate") - unless ENV["JS_ONLY"] + puts "travis_fold:end:prepare_tests" if ENV["TRAVIS"] + unless ENV["JS_ONLY"] + puts "travis_fold:start:ruby_tests" if ENV["TRAVIS"] unless ENV["SKIP_CORE"] params = [] if ENV["BISECT"] @@ -114,10 +119,11 @@ task 'docker:test' do @good &&= run_or_fail("bundle exec rake plugin:spec") end end - + puts "travis_fold:end:ruby_tests" if ENV["TRAVIS"] end unless ENV["RUBY_ONLY"] + puts "travis_fold:start:js_tests" if ENV["TRAVIS"] unless ENV["SKIP_CORE"] @good &&= run_or_fail("bundle exec rake qunit:test['600000']") @good &&= run_or_fail("bundle exec rake qunit:test['600000','/wizard/qunit']") @@ -130,7 +136,7 @@ task 'docker:test' do @good &&= run_or_fail("bundle exec rake plugin:qunit['*','600000']") end end - + puts "travis_fold:end:js_tests" if ENV["TRAVIS"] end end diff --git a/script/docker_test.rb b/script/docker_test.rb index cb3a3bf523..a8f3a1cf45 100644 --- a/script/docker_test.rb +++ b/script/docker_test.rb @@ -6,6 +6,8 @@ # => RUN_SMOKE_TESTS executes the smoke tests instead of the regular tests from docker.rake # See lib/tasks/docker.rake and lib/tasks/smoke_test.rake for more information +puts "travis_fold:end:starting_docker_container" if ENV["TRAVIS"] + def run_or_fail(command) pid = Process.spawn(command) Process.wait(pid) @@ -13,11 +15,18 @@ def run_or_fail(command) end unless ENV['NO_UPDATE'] + puts "travis_fold:start:pulling_latest_discourse" if ENV["TRAVIS"] run_or_fail("git pull") checkout = ENV['COMMIT_HASH'] || "HEAD" run_or_fail("git checkout #{checkout}") + + puts "travis_fold:end:pulling_latest_discourse" if ENV["TRAVIS"] + puts "travis_fold:start:bundle" if ENV["TRAVIS"] + run_or_fail("bundle") + + puts "travis_fold:end:bundle" if ENV["TRAVIS"] end if ENV['RUN_SMOKE_TESTS'] From aa614e393c600f41d3e922ecf295cf2c9217700d Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Wed, 12 Sep 2018 13:08:02 -0400 Subject: [PATCH 28/59] return 403 when trying drafts of another user --- app/controllers/drafts_controller.rb | 8 ++------ config/locales/server.en.yml | 1 - spec/requests/drafts_controller_spec.rb | 6 ++---- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/app/controllers/drafts_controller.rb b/app/controllers/drafts_controller.rb index aa8840bd05..00afdf2f7c 100644 --- a/app/controllers/drafts_controller.rb +++ b/app/controllers/drafts_controller.rb @@ -16,8 +16,6 @@ class DraftsController < ApplicationController limit: params[:limit] } - help_key = "user_activity.no_drafts" - if user == current_user stream = Draft.stream(opts) stream.each do |d| @@ -31,15 +29,13 @@ class DraftsController < ApplicationController end end end - - help_key += ".self" else - help_key += ".others" + raise Discourse::InvalidAccess end render json: { drafts: stream ? serialize_data(stream, DraftSerializer) : [], - no_results_help: I18n.t(help_key) + no_results_help: I18n.t("user_activity.no_drafts.self") } end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index ce8b16cd09..202df12828 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -796,7 +796,6 @@ en: others: "No replies." no_drafts: self: "You have no drafts; begin composing a reply in any topic and it will be auto-saved as a new draft." - others: "You do not have permission to see drafts for this user." topic_flag_types: spam: diff --git a/spec/requests/drafts_controller_spec.rb b/spec/requests/drafts_controller_spec.rb index 141b790d6a..85e45b1176 100644 --- a/spec/requests/drafts_controller_spec.rb +++ b/spec/requests/drafts_controller_spec.rb @@ -1,4 +1,5 @@ require 'rails_helper' +require 'pp' describe DraftsController do it 'requires you to be logged in' do @@ -28,11 +29,8 @@ describe DraftsController do it 'does not let a user see drafts stream of another user' do user_b = Fabricate(:user) Draft.set(user_b, 'xxx', 0, '{}') - sign_in(Fabricate(:user)) get "/drafts.json", params: { username: user_b.username } - expect(response.status).to eq(200) - parsed = JSON.parse(response.body) - expect(parsed["drafts"].length).to eq(0) + expect(response.status).to eq(403) end end From 7f05af599503521e5d3a7055abddfdd21547b900 Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Wed, 12 Sep 2018 13:10:14 -0400 Subject: [PATCH 29/59] cleanup --- spec/requests/drafts_controller_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/requests/drafts_controller_spec.rb b/spec/requests/drafts_controller_spec.rb index 85e45b1176..b9ebf3ce1f 100644 --- a/spec/requests/drafts_controller_spec.rb +++ b/spec/requests/drafts_controller_spec.rb @@ -1,5 +1,4 @@ require 'rails_helper' -require 'pp' describe DraftsController do it 'requires you to be logged in' do From 9461c992100c6f942adbd6b3c40d42e7d9a49158 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 12 Sep 2018 18:59:48 +0100 Subject: [PATCH 30/59] DEV: Add travis_fold to termination stage --- lib/tasks/docker.rake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/tasks/docker.rake b/lib/tasks/docker.rake index a92c5529ed..9ee562e0fc 100644 --- a/lib/tasks/docker.rake +++ b/lib/tasks/docker.rake @@ -141,6 +141,7 @@ task 'docker:test' do end ensure + puts "travis_fold:start:terminating" if ENV["TRAVIS"] puts "Terminating" if ENV['PAUSE_ON_TERMINATE'] @@ -152,6 +153,7 @@ task 'docker:test' do Process.kill("TERM", @pg_pid) if @pg_pid Process.wait @redis_pid if @redis_pid Process.wait @pg_pid if @pg_pid + puts "travis_fold:end:terminating" if ENV["TRAVIS"] end if !@good From 4dc25ad2011200827cc0265d426cb1712115845f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Sep 2018 09:19:45 +0800 Subject: [PATCH 31/59] Fix s3 recovery from tombstone in `UploadRecovery`. --- lib/upload_recovery.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/upload_recovery.rb b/lib/upload_recovery.rb index 176ba6ca14..d9044c33b7 100644 --- a/lib/upload_recovery.rb +++ b/lib/upload_recovery.rb @@ -89,9 +89,12 @@ class UploadRecovery tombstone_prefix = FileStore::S3Store::TOMBSTONE_PREFIX if key.starts_with?(tombstone_prefix) + old_key = key + key = key.sub(tombstone_prefix, "") + Discourse.store.s3_helper.copy( + old_key, key, - key.sub(tombstone_prefix, ""), options: { acl: "public-read" } ) end From a3b3b0810d90abf9c49a6f04589f59aa25cc8719 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Sep 2018 11:57:51 +0800 Subject: [PATCH 32/59] Rescue errors when running dry run for `UploadRecovery`. --- lib/upload_recovery.rb | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/upload_recovery.rb b/lib/upload_recovery.rb index d9044c33b7..0bc6d10b33 100644 --- a/lib/upload_recovery.rb +++ b/lib/upload_recovery.rb @@ -5,23 +5,28 @@ class UploadRecovery def recover Post.where("raw LIKE '%upload:\/\/%'").find_each do |post| - analyzer = PostAnalyzer.new(post.raw, post.topic_id) - cooked_stripped = analyzer.send(:cooked_stripped) + begin + analyzer = PostAnalyzer.new(post.raw, post.topic_id) + cooked_stripped = analyzer.send(:cooked_stripped) - cooked_stripped.css("img").each do |img| - if dom_class = img["class"] - if (Post.white_listed_image_classes & dom_class.split).count > 0 - next - end - end - - if img["data-orig-src"] - if @dry_run - puts "#{post.full_url} #{img["data-orig-src"]}" - else - recover_post_upload(post, img["data-orig-src"]) + cooked_stripped.css("img").each do |img| + if dom_class = img["class"] + if (Post.white_listed_image_classes & dom_class.split).count > 0 + next + end + end + + if img["data-orig-src"] + if @dry_run + puts "#{post.full_url} #{img["data-orig-src"]}" + else + recover_post_upload(post, img["data-orig-src"]) + end end end + rescue => e + raise e unless @dry_run + puts "#{post.full_url} #{e.class}: #{e.message}" end end end From daa02431dfb5ea64990c776860ff634c088cf522 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Sep 2018 14:53:19 +1000 Subject: [PATCH 33/59] FIX: remove div used to measure textarea position Also corrects the positioning of autocomplete (when typing @ or emoji) Previously there were edge conditions where autocomplete would be hundreds of pixels away due to a bug measuring. This correct an issue where Firefox ends up having an enormous blank space at the bottom of topics after editing. --- .../discourse/lib/autocomplete.js.es6 | 5 +- vendor/assets/javascripts/caret_position.js | 64 +++++++++++++++---- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index 5900968a5e..c229d8f176 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -285,11 +285,10 @@ export default function(options) { hOffset = 0; } else { pos = me.caretPosition({ - pos: completeStart, - key: options.key + pos: completeStart + 1 }); - hOffset = 27; + hOffset = 10; if (options.treatAsTextarea) vOffset = -32; } diff --git a/vendor/assets/javascripts/caret_position.js b/vendor/assets/javascripts/caret_position.js index 8a7b2e0616..094d504d4b 100644 --- a/vendor/assets/javascripts/caret_position.js +++ b/vendor/assets/javascripts/caret_position.js @@ -1,3 +1,6 @@ +// TODO: This code should be moved to lib, it was heavily modified by us over the years, and mostly written by us +// except for the little snippet from StackOverflow +// // http://stackoverflow.com/questions/263743/how-to-get-caret-position-in-textarea var clone, getCaret; getCaret = function(el) { @@ -19,7 +22,7 @@ getCaret = function(el) { clone = null; -$.fn.caret = function(){ +$.fn.caret = function() { return getCaret(this[0]); }; @@ -29,7 +32,22 @@ $.fn.caret = function(){ @module $.fn.caretPosition **/ $.fn.caretPosition = function(options) { - var after, before, getStyles, guard, html, important, insertSpaceAfterBefore, letter, makeCursor, p, pPos, pos, span, styles, textarea, val; + var after, + before, + getStyles, + guard, + html, + important, + insertSpaceAfterBefore, + letter, + makeCursor, + p, + pPos, + pos, + span, + styles, + textarea, + val; if (clone) { clone.remove(); } @@ -44,17 +62,15 @@ $.fn.caretPosition = function(options) { } }; - styles = getStyles(textarea[0]); - clone = $("

      ").appendTo("body"); - p = clone.find("p"); - clone.width(textarea.width()); - clone.height(textarea.height()); - important = function(prop) { return styles.getPropertyValue(prop); }; - const isRTL = $('html').hasClass('rtl'); + styles = getStyles(textarea[0]); + clone = $("

      ").appendTo("body"); + p = clone.find("p"); + + const isRTL = $("html").hasClass("rtl"); clone.css({ border: "1px solid black", padding: important("padding"), @@ -77,7 +93,14 @@ $.fn.caretPosition = function(options) { "line-height": important("line-height") }); - pos = options && (options.pos || options.pos === 0) ? options.pos : getCaret(textarea[0]); + clone.width(textarea.width()); + clone.height(textarea.height()); + + pos = + options && (options.pos || options.pos === 0) + ? options.pos + : getCaret(textarea[0]); + val = textarea.val().replace("\r", ""); if (options && options.key) { val = val.substring(0, pos) + options.key + val.substring(pos); @@ -103,12 +126,23 @@ $.fn.caretPosition = function(options) { var l; l = val.substring(pos, pos + 1); if (l === "\n") return "
      "; - return "" + guard(l) + ""; + return ( + "" + + guard(l) + + "" + ); }; html = ""; + if (before >= 0) { - html += guard(val.substring(0, pos - 1)) + makeCursor(before, "before", "#d0ffff"); + html += + guard(val.substring(0, pos - 1)) + + makeCursor(before, "before", "#d0ffff"); if (insertSpaceAfterBefore) { html += makeCursor(0, "post-before", "#d0ffff"); } @@ -130,9 +164,11 @@ $.fn.caretPosition = function(options) { } pPos = p.offset(); - return { + let position = { left: pos.left - pPos.left, - top: (pos.top - pPos.top) - clone.scrollTop() + top: pos.top - pPos.top - clone.scrollTop() }; + clone.remove(); + return position; }; From 49f1f10f75c3431e50e431851da178dc10d040ef Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Sep 2018 14:58:45 +1000 Subject: [PATCH 34/59] file is a .js file so don't use let --- vendor/assets/javascripts/caret_position.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/assets/javascripts/caret_position.js b/vendor/assets/javascripts/caret_position.js index 094d504d4b..f811f7c073 100644 --- a/vendor/assets/javascripts/caret_position.js +++ b/vendor/assets/javascripts/caret_position.js @@ -164,7 +164,7 @@ $.fn.caretPosition = function(options) { } pPos = p.offset(); - let position = { + var position = { left: pos.left - pPos.left, top: pos.top - pPos.top - clone.scrollTop() }; From d99dd840e4436321e7963d6de65d73d7ce02763f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Sep 2018 13:24:08 +0800 Subject: [PATCH 35/59] Add basic test case for `UploadRecovery`. --- lib/upload_recovery.rb | 2 ++ spec/lib/upload_recovery_spec.rb | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 spec/lib/upload_recovery_spec.rb diff --git a/lib/upload_recovery.rb b/lib/upload_recovery.rb index 0bc6d10b33..70021d29ac 100644 --- a/lib/upload_recovery.rb +++ b/lib/upload_recovery.rb @@ -47,6 +47,8 @@ class UploadRecovery end def recover_from_local(post:, sha1:) + return unless sha1.present? + public_path = Rails.root.join("public") @paths ||= begin diff --git a/spec/lib/upload_recovery_spec.rb b/spec/lib/upload_recovery_spec.rb new file mode 100644 index 0000000000..32befa8539 --- /dev/null +++ b/spec/lib/upload_recovery_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' +require_dependency "upload_recovery" + +RSpec.describe UploadRecovery do + let(:user) { Fabricate(:user) } + + let(:upload) do + UploadCreator.new( + file_from_fixtures("logo.png"), + "logo.png" + ).create_for(user.id) + end + + let(:post) do + Fabricate(:post, + raw: "![logo.png](#{upload.short_url})", + user: user + ).link_post_uploads + end + + before do + SiteSetting.queue_jobs = false + end + + describe '#recover' do + it 'should recover the upload' do + begin + stub_request(:get, "http://test.localhost#{upload.url}") + .to_return(status: 200) + + expect do + upload.destroy! + end.to change { post.reload.uploads.count }.from(1).to(0) + + expect do + UploadRecovery.new.recover + end.to change { post.reload.uploads.count }.from(0).to(1) + ensure + public_path = "#{Discourse.store.public_dir}#{upload.url}" + + [ + public_path, + public_path.sub("uploads", "uploads/tombstone") + ].each { |path| File.delete(path) if File.exists?(path) } + end + end + end +end From 1afe7162e15a6fa94d2034130f6c0b32b050de8d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Sep 2018 13:41:38 +0800 Subject: [PATCH 36/59] Fix the build. --- spec/lib/upload_recovery_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/upload_recovery_spec.rb b/spec/lib/upload_recovery_spec.rb index 32befa8539..1d3f74a9d5 100644 --- a/spec/lib/upload_recovery_spec.rb +++ b/spec/lib/upload_recovery_spec.rb @@ -15,7 +15,7 @@ RSpec.describe UploadRecovery do Fabricate(:post, raw: "![logo.png](#{upload.short_url})", user: user - ).link_post_uploads + ).tap(&:link_post_uploads) end before do From 5eb65ad612ae7ea00d6dec39833508c511baeb07 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Sep 2018 13:59:17 +0800 Subject: [PATCH 37/59] FIX: Do not try to recover invalid `Upload#short_url` in `UploadRecovery`. --- lib/upload_recovery.rb | 7 ++--- spec/lib/upload_recovery_spec.rb | 46 +++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/lib/upload_recovery.rb b/lib/upload_recovery.rb index 70021d29ac..8eb1e7725a 100644 --- a/lib/upload_recovery.rb +++ b/lib/upload_recovery.rb @@ -34,9 +34,12 @@ class UploadRecovery private def recover_post_upload(post, short_url) + sha1 = Upload.sha1_from_short_url(short_url) + return unless sha1.present? + attributes = { post: post, - sha1: Upload.sha1_from_short_url(short_url) + sha1: sha1 } if Discourse.store.external? @@ -47,8 +50,6 @@ class UploadRecovery end def recover_from_local(post:, sha1:) - return unless sha1.present? - public_path = Rails.root.join("public") @paths ||= begin diff --git a/spec/lib/upload_recovery_spec.rb b/spec/lib/upload_recovery_spec.rb index 1d3f74a9d5..01334c5e7e 100644 --- a/spec/lib/upload_recovery_spec.rb +++ b/spec/lib/upload_recovery_spec.rb @@ -18,31 +18,45 @@ RSpec.describe UploadRecovery do ).tap(&:link_post_uploads) end + let(:upload_recovery) { UploadRecovery.new } + before do SiteSetting.queue_jobs = false end describe '#recover' do - it 'should recover the upload' do - begin - stub_request(:get, "http://test.localhost#{upload.url}") - .to_return(status: 200) + after do + public_path = "#{Discourse.store.public_dir}#{upload.url}" - expect do - upload.destroy! - end.to change { post.reload.uploads.count }.from(1).to(0) + [ + public_path, + public_path.sub("uploads", "uploads/tombstone") + ].each { |path| File.delete(path) if File.exists?(path) } + end - expect do - UploadRecovery.new.recover - end.to change { post.reload.uploads.count }.from(0).to(1) - ensure - public_path = "#{Discourse.store.public_dir}#{upload.url}" + describe 'when given an invalid sha1' do + it 'should not do anything' do + upload_recovery.expects(:recover_from_local).never - [ - public_path, - public_path.sub("uploads", "uploads/tombstone") - ].each { |path| File.delete(path) if File.exists?(path) } + post.update!( + raw: "![logo.png](upload://#{'a' * 28}.png)" + ) + + upload_recovery.recover end end + + it 'should recover the upload' do + stub_request(:get, "http://test.localhost#{upload.url}") + .to_return(status: 200) + + expect do + upload.destroy! + end.to change { post.reload.uploads.count }.from(1).to(0) + + expect do + upload_recovery.recover + end.to change { post.reload.uploads.count }.from(0).to(1) + end end end From 05a57d4f27ad7cadb6852da40c67dfae4c8a79f4 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Sep 2018 14:23:32 +0800 Subject: [PATCH 38/59] DEV: Clear cache after not before. * Clearing after ensures that state does not leak to specs in other files. --- spec/models/theme_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index 40ae4f164d..15ad9318ca 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -1,8 +1,7 @@ require 'rails_helper' describe Theme do - - before do + after do Theme.clear_cache! end From 6c657183012e68fdf9bd1bdcb21fde23a3413639 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Sep 2018 15:43:58 +0800 Subject: [PATCH 39/59] Include response body when raising an error in `FileHelper#download`. --- lib/file_helper.rb | 2 +- spec/components/file_helper_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/file_helper.rb b/lib/file_helper.rb index f7d2a0907d..f146b8b139 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -48,7 +48,7 @@ class FileHelper # attempt error API compatibility io = FakeIO.new io.status = [response.code, ""] - raise OpenURI::HTTPError.new("#{response.code} Error", io) + raise OpenURI::HTTPError.new("#{response.code} Error: #{response.body}", io) else log(:error, "FinalDestination did not work for: #{url}") if verbose throw :done diff --git a/spec/components/file_helper_spec.rb b/spec/components/file_helper_spec.rb index ec81a94b57..6baa22ccff 100644 --- a/spec/components/file_helper_spec.rb +++ b/spec/components/file_helper_spec.rb @@ -28,7 +28,7 @@ describe FileHelper do expect(e.io.status[0]).to eq("404") raise end - end.to raise_error(OpenURI::HTTPError) + end.to raise_error(OpenURI::HTTPError, "404 Error: 404") end it "correctly raises an OpenURI HTTP error if it gets a 404" do From 0a06b3d977d0f815a54d0c2aa501a1e4b0bbaca7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Sep 2018 16:32:35 +0800 Subject: [PATCH 40/59] Accept custom AR relation for `UploadRecovery`. --- lib/upload_recovery.rb | 4 ++-- spec/lib/upload_recovery_spec.rb | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/upload_recovery.rb b/lib/upload_recovery.rb index 8eb1e7725a..0bac5eb966 100644 --- a/lib/upload_recovery.rb +++ b/lib/upload_recovery.rb @@ -3,8 +3,8 @@ class UploadRecovery @dry_run = dry_run end - def recover - Post.where("raw LIKE '%upload:\/\/%'").find_each do |post| + def recover(posts = Post) + posts.where("raw LIKE '%upload:\/\/%'").find_each do |post| begin analyzer = PostAnalyzer.new(post.raw, post.topic_id) cooked_stripped = analyzer.send(:cooked_stripped) diff --git a/spec/lib/upload_recovery_spec.rb b/spec/lib/upload_recovery_spec.rb index 01334c5e7e..7a5929d8ff 100644 --- a/spec/lib/upload_recovery_spec.rb +++ b/spec/lib/upload_recovery_spec.rb @@ -46,6 +46,14 @@ RSpec.describe UploadRecovery do end end + it 'accepts a custom ActiveRecord relation' do + post.update!(updated_at: 2.days.ago) + upload.destroy! + + upload_recovery.expects(:recover_from_local).never + upload_recovery.recover(Post.where("updated_at >= ?", 1.day.ago)) + end + it 'should recover the upload' do stub_request(:get, "http://test.localhost#{upload.url}") .to_return(status: 200) From 529e4018f0c6fcb3e905b3323c81ad418fed4cc4 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Sep 2018 16:34:32 +0800 Subject: [PATCH 41/59] DEV: Avoid using `send` and make the method public instead. --- app/models/post_analyzer.rb | 4 ++-- lib/upload_recovery.rb | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index 4d4145f1e4..911b6d5c94 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -127,8 +127,6 @@ class PostAnalyzer raw_links.size end - private - def cooked_stripped @cooked_stripped ||= begin doc = Nokogiri::HTML.fragment(cook(@raw, topic_id: @topic_id)) @@ -137,6 +135,8 @@ class PostAnalyzer end end + private + def link_is_a_mention?(l) html_class = l['class'] return false if html_class.blank? diff --git a/lib/upload_recovery.rb b/lib/upload_recovery.rb index 0bac5eb966..d2a584fa88 100644 --- a/lib/upload_recovery.rb +++ b/lib/upload_recovery.rb @@ -7,9 +7,8 @@ class UploadRecovery posts.where("raw LIKE '%upload:\/\/%'").find_each do |post| begin analyzer = PostAnalyzer.new(post.raw, post.topic_id) - cooked_stripped = analyzer.send(:cooked_stripped) - cooked_stripped.css("img").each do |img| + analyzer.cooked_stripped.css("img").each do |img| if dom_class = img["class"] if (Post.white_listed_image_classes & dom_class.split).count > 0 next From e364547ff709ad15d888bd4e87c121d4ac5e8505 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 13 Sep 2018 13:29:17 +0530 Subject: [PATCH 42/59] FIX: ignore and log bad json values for custom fields --- app/models/concerns/has_custom_fields.rb | 22 ++++++++-- .../concern/has_custom_fields_spec.rb | 41 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb index a2bc01b254..0a0b2edebe 100644 --- a/app/models/concerns/has_custom_fields.rb +++ b/app/models/concerns/has_custom_fields.rb @@ -42,13 +42,20 @@ module HasCustomFields case type when :boolean then !!CUSTOM_FIELD_TRUE.include?(value) when :integer then value.to_i - when :json then ::JSON.parse(value) + when :json then parse_json_value(value, key) else value end array ? [result] : result end + + def self.parse_json_value(value, key) + ::JSON.parse(value) + rescue JSON::ParserError + Rails.logger.warn("Value '#{value}' for custom field '#{key}' is not json, it is being ignored.") + {} + end end included do @@ -85,6 +92,11 @@ module HasCustomFields @custom_field_types[name] = type end + def self.get_custom_field_type(name) + @custom_field_types ||= {} + @custom_field_types[name] + end + def self.preload_custom_fields(objects, fields) if objects.present? map = {} @@ -186,7 +198,7 @@ module HasCustomFields array_fields = {} _custom_fields.reload.each do |f| - if dup[f.name].is_a? Array + if dup[f.name].is_a?(Array) # we need to collect Arrays fully before we can compare them if !array_fields.has_key?(f.name) array_fields[f.name] = [f] @@ -221,12 +233,14 @@ module HasCustomFields end dup.each do |k, v| - if v.is_a? Array + field_type = self.class.get_custom_field_type(k) + + if v.is_a?(Array) && field_type != :json v.each { |subv| _custom_fields.create!(name: k, value: subv) } else _custom_fields.create!( name: k, - value: v.is_a?(Hash) ? v.to_json : v + value: v.is_a?(Hash) || field_type == :json ? v.to_json : v ) end end diff --git a/spec/components/concern/has_custom_fields_spec.rb b/spec/components/concern/has_custom_fields_spec.rb index 1a55fc9f7c..61f342658f 100644 --- a/spec/components/concern/has_custom_fields_spec.rb +++ b/spec/components/concern/has_custom_fields_spec.rb @@ -187,6 +187,47 @@ describe HasCustomFields do expect(test_item2.custom_fields).to eq("sixto" => "rodriguez", "de" => "la playa") end + it "supports arrays in json fields" do + field_type = "json_array" + CustomFieldsTestItem.register_custom_field_type(field_type, :json) + + item = CustomFieldsTestItem.new + item.custom_fields = { + "json_array" => [{ a: "test" }, { b: "another" }] + } + item.save + + item.reload + + expect(item.custom_fields[field_type]).to eq( + [{ "a" => "test" }, { "b" => "another" }] + ) + + item.custom_fields["json_array"] = ['a', 'b'] + item.save + + item.reload + + expect(item.custom_fields[field_type]).to eq(["a", "b"]) + end + + it "will not fail to load custom fields if json is corrupt" do + + field_type = "bad_json" + CustomFieldsTestItem.register_custom_field_type(field_type, :json) + + item = CustomFieldsTestItem.create! + + CustomFieldsTestItemCustomField.create!( + custom_fields_test_item_id: item.id, + name: field_type, + value: "{test" + ) + + item = item.reload + expect(item.custom_fields[field_type]).to eq({}) + end + it "supports bulk retrieval with a list of ids" do item1 = CustomFieldsTestItem.new item1.custom_fields = { "a" => ["b", "c", "d"], 'not_whitelisted' => 'secret' } From b8c0e7790880e21f1b8f8b7943876d1fa3d2e9eb Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 13 Sep 2018 12:39:54 +0100 Subject: [PATCH 43/59] DEV: apply plugin migrations when testing in Travis and Docker In `RAILS_ENV=test`, plugins are not loaded by default. Therefore we need to explicitly specify `LOAD_PLUGINS=1` when we want to apply plugin migrations. --- .travis.yml | 2 +- lib/tasks/docker.rake | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c93734e824..3e90410c5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,7 +68,7 @@ install: - bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu > /dev/null; fi" - bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3 > /dev/null; fi" - bash -c "if [ '$QUNIT_RUN' == '1' ] || [ '$RUN_LINT' == '1' ]; then yarn install --dev > /dev/null; fi" - - bash -c "if [ '$RUN_LINT' != '1' ]; then bundle exec rake db:create db:migrate > /dev/null; fi" + - bash -c "if [ '$RUN_LINT' != '1' ]; then LOAD_PLUGINS=1 bundle exec rake db:create db:migrate > /dev/null; fi" script: - | diff --git a/lib/tasks/docker.rake b/lib/tasks/docker.rake index 9ee562e0fc..65276b759c 100644 --- a/lib/tasks/docker.rake +++ b/lib/tasks/docker.rake @@ -95,7 +95,11 @@ task 'docker:test' do @good &&= run_or_fail("bundle exec rake plugin:install_all_official") end - @good &&= run_or_fail("bundle exec rake db:migrate") + if ENV["SKIP_PLUGINS"] + @good &&= run_or_fail("bundle exec rake db:migrate") + else + @good &&= run_or_fail("LOAD_PLUGINS=1 bundle exec rake db:migrate") + end puts "travis_fold:end:prepare_tests" if ENV["TRAVIS"] From 5bb7cc871067e59e5f41e55580a23b923ea57079 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 13 Sep 2018 21:57:20 +1000 Subject: [PATCH 44/59] FIX: update PG gem We need the newer PG gem to fix issues around rb_wait_for_single_fd that pop up in rare conditions --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 689fe3d92d..69567ed4a4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -271,7 +271,7 @@ GEM parallel (1.12.1) parser (2.5.1.0) ast (~> 2.4.0) - pg (1.1.0) + pg (1.1.3) powerpack (0.1.2) progress (3.4.0) pry (0.10.4) From 5bdf476de7a0368aeb823abaa28c77c3cb7e611f Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Thu, 13 Sep 2018 08:40:57 -0400 Subject: [PATCH 45/59] raise error early in drafts controller --- app/controllers/drafts_controller.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/controllers/drafts_controller.rb b/app/controllers/drafts_controller.rb index 00afdf2f7c..43def37185 100644 --- a/app/controllers/drafts_controller.rb +++ b/app/controllers/drafts_controller.rb @@ -10,27 +10,27 @@ class DraftsController < ApplicationController user = fetch_user_from_params + unless user == current_user + raise Discourse::InvalidAccess + end + opts = { user: user, offset: params[:offset], limit: params[:limit] } - if user == current_user - stream = Draft.stream(opts) - stream.each do |d| - parsed_data = JSON.parse(d.data) - if parsed_data - if parsed_data['reply'] - d.raw = parsed_data['reply'] - end - if parsed_data['categoryId'].present? && !d.category_id.present? - d.category_id = parsed_data['categoryId'] - end + stream = Draft.stream(opts) + stream.each do |d| + parsed_data = JSON.parse(d.data) + if parsed_data + if parsed_data['reply'] + d.raw = parsed_data['reply'] + end + if parsed_data['categoryId'].present? && !d.category_id.present? + d.category_id = parsed_data['categoryId'] end end - else - raise Discourse::InvalidAccess end render json: { From 7bf8b0d305fd3da6360aa0f6d4ba5d5e91e9c7fa Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 13 Sep 2018 17:35:54 +0200 Subject: [PATCH 46/59] FIX: treats users_(by_type|tl) as other reports (#6391) --- .../admin/templates/dashboard_next_general.hbs | 6 ++---- .../javascripts/discourse/lib/reports-loader.js.es6 | 2 +- app/models/admin_dashboard_next_general_data.rb | 8 -------- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs index 98ca3f0304..88f36a5d59 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs @@ -95,13 +95,11 @@ {{#conditional-loading-section isLoading=isLoading}} {{admin-report forcedModes="inline-table" - report=usersByTypeReport - lastRefreshedAt=lastRefreshedAt}} + dataSourceName="users_by_type"}} {{admin-report forcedModes="inline-table" - report=usersByTrustLevelReport - lastRefreshedAt=lastRefreshedAt}} + dataSourceName="users_by_trust_level"}} {{/conditional-loading-section}} diff --git a/app/assets/javascripts/discourse/lib/reports-loader.js.es6 b/app/assets/javascripts/discourse/lib/reports-loader.js.es6 index bcdd564ef6..d6c090d69c 100644 --- a/app/assets/javascripts/discourse/lib/reports-loader.js.es6 +++ b/app/assets/javascripts/discourse/lib/reports-loader.js.es6 @@ -5,7 +5,7 @@ let _queue = []; let _processing = 0; // max number of reports which will be requested in one bulk request -const MAX_JOB_SIZE = 5; +const MAX_JOB_SIZE = 4; // max number of concurrent bulk requests const MAX_CONCURRENCY = 3; diff --git a/app/models/admin_dashboard_next_general_data.rb b/app/models/admin_dashboard_next_general_data.rb index 5cff33db70..f2137c4712 100644 --- a/app/models/admin_dashboard_next_general_data.rb +++ b/app/models/admin_dashboard_next_general_data.rb @@ -1,14 +1,6 @@ class AdminDashboardNextGeneralData < AdminDashboardNextData - def reports - @reports ||= %w{ - users_by_type - users_by_trust_level - } - end - def get_json { - reports: self.class.reports(reports).compact, updated_at: Time.zone.now.as_json } end From 18fcd483f26e13a91c0b3b7a4294260e47bff4b2 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 13 Sep 2018 17:36:39 +0200 Subject: [PATCH 47/59] FIX: ensures we have a color for reports (#6396) --- app/models/report.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/report.rb b/app/models/report.rb index f5304184a9..dd36975790 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -29,8 +29,10 @@ class Report @modes = [:table, :chart] @prev_data = nil @dates_filtering = true - @primary_color = rgba_color(ColorScheme.hex_for_name('tertiary')) - @secondary_color = rgba_color(ColorScheme.hex_for_name('tertiary'), 0.1) + + tertiary = ColorScheme.hex_for_name('tertiary') || '0088cc' + @primary_color = rgba_color(tertiary) + @secondary_color = rgba_color(tertiary, 0.1) end def self.cache_key(report) From a6502ce8796d09698e99b3ce400aab3113353a45 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 13 Sep 2018 17:36:55 +0200 Subject: [PATCH 48/59] FIX: ensures errors in report initialization fail nicely (#6392) --- app/models/report.rb | 7 +++++++ spec/models/report_spec.rb | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/app/models/report.rb b/app/models/report.rb index dd36975790..8ca5bf8fd8 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -176,6 +176,13 @@ class Report report.error = :timeout end rescue Exception => e + # ensures that if anything unexpected prevents us from + # creating a report object we fail elegantly and log an error + if !report + Rails.logger.error("Couldn’t create report `#{type}`: <#{e.class} #{e.message}>") + return nil + end + report.error = :exception # given reports can be added by plugins we don’t want dashboard failures diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index 9cdd34cc4a..a432bb4632 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -813,6 +813,22 @@ describe Report do end end + describe "unexpected error on report initialization" do + it "returns no report" do + class ReportInitError < StandardError; end + + Report.stubs(:new).raises(ReportInitError.new("x")) + + Rails.logger.expects(:error) + .with('Couldn’t create report `signups`: ') + .once + + report = Report.find('signups') + + expect(report).to be_nil + end + end + describe 'posts' do let(:report) { Report.find('posts') } From 30619c244ce30efe83af2788f5023974d140ebd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 13 Sep 2018 18:53:53 +0200 Subject: [PATCH 49/59] FIX: don't index urls to local files --- app/services/search_indexer.rb | 8 +++++--- spec/services/search_indexer_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index 04dc32b697..f76b9037e8 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -167,6 +167,8 @@ class SearchIndexer class HtmlScrubber < Nokogiri::XML::SAX::Document + DIACRITICS ||= /([\u0300-\u036f]|[\u1AB0-\u1AFF]|[\u1DC0-\u1DFF]|[\u20D0-\u20FF])/ + def self.strip_diacritics(str) s = str.unicode_normalize(:nfkd) s.gsub!(DIACRITICS, "") @@ -196,12 +198,12 @@ class SearchIndexer attributes = Hash[*attributes.flatten] ATTRIBUTES.each do |name| - characters(attributes[name]) if attributes[name].present? + if attributes[name].present? + characters(attributes[name]) unless name == "href" && UrlHelper.is_local(attributes[name]) + end end end - DIACRITICS ||= /([\u0300-\u036f]|[\u1AB0-\u1AFF]|[\u1DC0-\u1DFF]|[\u20D0-\u20FF])/ - def characters(str) str = HtmlScrubber.strip_diacritics(str) if @strip_diacritics scrubbed << " #{str} " diff --git a/spec/services/search_indexer_spec.rb b/spec/services/search_indexer_spec.rb index e98d52e365..a9f309a489 100644 --- a/spec/services/search_indexer_spec.rb +++ b/spec/services/search_indexer_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' describe SearchIndexer do let(:post_id) { 99 } + it 'correctly indexes chinese' do SiteSetting.default_locale = 'zh_CN' data = "你好世界" @@ -37,6 +38,27 @@ describe SearchIndexer do expect(scrubbed).to eq(" HELLO Heterogeneite Здравствуите هتاف للترحيب 你好 ") end + it "doesn't index local files" do + html = <<~HTML +

      Discourse

      +

      51%20PM

      + + HTML + + scrubbed = SearchIndexer::HtmlScrubber.scrub(html).gsub(/\s+/, " ") + + expect(scrubbed).to eq(" Discourse 51%20PM Untitled design (21).jpg Untitled%20design%20(21) Untitled design (21).jpg 1280x1136 472 KB ") + end + it 'correctly indexes a post according to version' do # Preparing so that they can be indexed to right version SearchIndexer.update_posts_index(post_id, "dummy", "", nil, nil) From fd931b948dc271168524e5c5059146dbdd8b20c3 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 13 Sep 2018 21:31:00 +0200 Subject: [PATCH 50/59] Use a more helpful failure message in spec --- spec/integrity/site_setting_spec.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/integrity/site_setting_spec.rb b/spec/integrity/site_setting_spec.rb index 9c52e9fab6..3cf271cfbe 100644 --- a/spec/integrity/site_setting_spec.rb +++ b/spec/integrity/site_setting_spec.rb @@ -23,11 +23,13 @@ describe "site setting integrity checks" do it "no locale default has different type than default or invalid key" do yaml.each_value do |category| - category.each_value do |setting| + category.each do |setting_name, setting| next unless setting.is_a?(Hash) if setting['locale_default'] setting['locale_default'].each_pair do |k, v| - expect(LocaleSiteSetting.valid_value?(k.to_s)).to be_truthy + expect(LocaleSiteSetting.valid_value?(k.to_s)).to be_truthy, + "'#{k}' is not a valid locale_default key for '#{setting_name}' site setting" + case setting['default'] when TrueClass, FalseClass expect(v.class == TrueClass || v.class == FalseClass).to be_truthy From aa1af9fc2294742258a35951ab8355c03ed993da Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Sep 2018 16:08:15 +0800 Subject: [PATCH 51/59] FIX: Onceoff job to recover missing post uploads. This fixes the regression due to https://github.com/discourse/discourse/commit/1f636c445bcbc66b1765bcc3966887adcb14bdbf --- app/jobs/onceoff/recover_post_uploads.rb | 22 ++++++++++++++++++++++ spec/jobs/recover_post_uploads_spec.rb | 17 +++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 app/jobs/onceoff/recover_post_uploads.rb create mode 100644 spec/jobs/recover_post_uploads_spec.rb diff --git a/app/jobs/onceoff/recover_post_uploads.rb b/app/jobs/onceoff/recover_post_uploads.rb new file mode 100644 index 0000000000..ef3fb0b5aa --- /dev/null +++ b/app/jobs/onceoff/recover_post_uploads.rb @@ -0,0 +1,22 @@ +require_dependency "upload_recovery" + +module Jobs + class RecoverPostUploads < Jobs::Onceoff + MIN_PERIOD = 30 + MAX_PERIOD = 120 + + def execute_onceoff(args) + UploadRecovery.new.recover(Post.where( + "baked_at >= ?", + grace_period.days.ago + )) + end + + def grace_period + SiteSetting.purge_deleted_uploads_grace_period_days.clamp( + MIN_PERIOD, + MAX_PERIOD + ) + end + end +end diff --git a/spec/jobs/recover_post_uploads_spec.rb b/spec/jobs/recover_post_uploads_spec.rb new file mode 100644 index 0000000000..0448bc9adc --- /dev/null +++ b/spec/jobs/recover_post_uploads_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +RSpec.describe Jobs::RecoverPostUploads do + describe '#grace_period' do + it 'should restrict the grace period to the right range' do + SiteSetting.purge_deleted_uploads_grace_period_days = + described_class::MIN_PERIOD - 1 + + expect(described_class.new.grace_period).to eq(30) + + SiteSetting.purge_deleted_uploads_grace_period_days = + described_class::MAX_PERIOD + 1 + + expect(described_class.new.grace_period).to eq(120) + end + end +end From 419b14e58b0a52a652ff12aa80245c82b2818a18 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Sep 2018 12:54:11 +1000 Subject: [PATCH 52/59] FIX: correctly keep stylesheet cache entries The intent from day one was to keep MAX_TO_KEEP stylesheets per target however the DELETE statement did not perform target filtering This meant we often deleted the wrong stylesheets from the cache --- app/models/stylesheet_cache.rb | 12 ++++++++---- spec/models/stylesheet_cache_spec.rb | 12 ++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/models/stylesheet_cache.rb b/app/models/stylesheet_cache.rb index 52f6ef337e..b25fc51fa2 100644 --- a/app/models/stylesheet_cache.rb +++ b/app/models/stylesheet_cache.rb @@ -3,7 +3,8 @@ class StylesheetCache < ActiveRecord::Base MAX_TO_KEEP = 50 - def self.add(target, digest, content, source_map) + def self.add(target, digest, content, source_map, max_to_keep: nil) + max_to_keep ||= MAX_TO_KEEP old_logger = ActiveRecord::Base.logger return false if where(target: target, digest: digest).exists? @@ -15,16 +16,19 @@ class StylesheetCache < ActiveRecord::Base success = create(target: target, digest: digest, content: content, source_map: source_map) count = StylesheetCache.count - if count > MAX_TO_KEEP + if count > max_to_keep remove_lower = StylesheetCache .where(target: target) - .limit(MAX_TO_KEEP) + .limit(max_to_keep) .order('id desc') .pluck(:id) .last - DB.exec("DELETE FROM stylesheet_cache where id < :id", id: remove_lower) + DB.exec(<<~SQL, id: remove_lower, target: target) + DELETE FROM stylesheet_cache + WHERE id < :id AND target = :target + SQL end success diff --git a/spec/models/stylesheet_cache_spec.rb b/spec/models/stylesheet_cache_spec.rb index bffa8564d8..f42bc379ba 100644 --- a/spec/models/stylesheet_cache_spec.rb +++ b/spec/models/stylesheet_cache_spec.rb @@ -24,5 +24,17 @@ describe StylesheetCache do expect(StylesheetCache.first.content).to eq "c" end + it "it retains stylesheets for competing targets" do + StylesheetCache.destroy_all + + StylesheetCache.add("desktop", SecureRandom.hex, "body { }", "map", max_to_keep: 2) + StylesheetCache.add("desktop", SecureRandom.hex, "body { }", "map", max_to_keep: 2) + StylesheetCache.add("mobile", SecureRandom.hex, "body { }", "map", max_to_keep: 2) + StylesheetCache.add("mobile", SecureRandom.hex, "body { }", "map", max_to_keep: 2) + StylesheetCache.add("mobile", SecureRandom.hex, "body { }", "map", max_to_keep: 2) + + expect(StylesheetCache.order(:id).pluck(:target)).to eq(["desktop", "desktop", "mobile", "mobile"]) + end + end end From c3f6b4d96665d25122d216784c9d14f75ea27a94 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 14 Sep 2018 13:42:59 +0800 Subject: [PATCH 53/59] DEV: Test against real `Upload#url` format. --- spec/components/stylesheet/importer_spec.rb | 2 +- spec/fabricators/upload_fabricator.rb | 29 ++++++++++++++------- spec/models/upload_spec.rb | 27 +++++++++++++------ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/spec/components/stylesheet/importer_spec.rb b/spec/components/stylesheet/importer_spec.rb index 199fdb1df6..3b65ec307c 100644 --- a/spec/components/stylesheet/importer_spec.rb +++ b/spec/components/stylesheet/importer_spec.rb @@ -30,7 +30,7 @@ describe Stylesheet::Importer do background = Fabricate(:upload_s3) category = Fabricate(:category, uploaded_background: background) - expect(compile_css("category_backgrounds")).to include("body.category-#{category.full_slug}{background-image:url(https://s3.cdn/uploads") + expect(compile_css("category_backgrounds")).to include("body.category-#{category.full_slug}{background-image:url(https://s3.cdn/original") end end diff --git a/spec/fabricators/upload_fabricator.rb b/spec/fabricators/upload_fabricator.rb index d9df6633ce..c306a784b3 100644 --- a/spec/fabricators/upload_fabricator.rb +++ b/spec/fabricators/upload_fabricator.rb @@ -5,18 +5,27 @@ Fabricator(:upload) do filesize 1234 width 100 height 200 - url { sequence(:url) { |n| "/uploads/default/#{n}/1234567890123456.png" } } + + url do |attrs| + sequence(:url) do |n| + Discourse.store.get_path_for( + "original", n + 1, attrs[:sha1], ".#{attrs[:extension]}" + ) + end + end + extension "png" end Fabricator(:upload_s3, from: :upload) do - url { sequence(:url) { |n| "#{Discourse.store.absolute_base_url}/uploads/default/#{n}/1234567890123456.png" } } -end - -Fabricator(:attachment, from: :upload) do - id 42 - user - original_filename "archive.zip" - filesize 1234 - url "/uploads/default/42/66b3ed1503efc936.zip" + url do |attrs| + sequence(:url) do |n| + File.join( + Discourse.store.absolute_base_url, + Discourse.store.get_path_for( + "original", n + 1, attrs[:sha1], ".#{attrs[:extension]}" + ) + ) + end + end end diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 0e86a0fb3a..70b7760a38 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -75,23 +75,34 @@ describe Upload do context ".get_from_url" do let(:sha1) { "10f73034616a796dfd70177dc54b6def44c4ba6f" } - let(:url) { "/uploads/default/original/3X/1/0/#{sha1}.png" } - let(:upload) { Fabricate(:upload, url: url, sha1: sha1) } + let(:upload) { Fabricate(:upload, sha1: sha1) } it "works when the file has been uploaded" do expect(Upload.get_from_url(upload.url)).to eq(upload) end describe 'for an extensionless url' do - let(:url) { "/uploads/default/original/1X/#{sha1}" } + before do + upload.update!(url: upload.url.sub('.png', '')) + upload.reload + end it 'should return the right upload' do expect(Upload.get_from_url(upload.url)).to eq(upload) end end - describe 'for a url without a tree' do - let(:url) { "/uploads/default/original/1X/#{sha1}.png" } + describe 'for a url a tree' do + before do + upload.update!(url: + Discourse.store.get_path_for( + "original", + 16001, + upload.sha1, + ".#{upload.extension}" + ) + ) + end it 'should return the right upload' do expect(Upload.get_from_url(upload.url)).to eq(upload) @@ -124,8 +135,8 @@ describe Upload do end describe "s3 store" do - let(:path) { "/original/3X/1/0/10f73034616a796dfd70177dc54b6def44c4ba6f.png" } - let(:url) { "#{SiteSetting.Upload.absolute_base_url}#{path}" } + let(:upload) { Fabricate(:upload_s3) } + let(:path) { upload.url.sub(SiteSetting.Upload.s3_base_url, '') } before do SiteSetting.enable_s3_uploads = true @@ -136,7 +147,7 @@ describe Upload do it "should return the right upload when using base url (not CDN) for s3" do upload - expect(Upload.get_from_url(url)).to eq(upload) + expect(Upload.get_from_url(upload.url)).to eq(upload) end describe 'when using a cdn' do From aca195e4a7fb760009e3e57e2a884349ed56cf08 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Fri, 14 Sep 2018 09:49:32 +0200 Subject: [PATCH 54/59] Remove unused site setting. (#6398) --- config/locales/server.en.yml | 1 - config/site_settings.yml | 2 -- db/migrate/20180913200027_remove_enforce_square_emoji.rb | 5 +++++ 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20180913200027_remove_enforce_square_emoji.rb diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 202df12828..761176b97f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1728,7 +1728,6 @@ en: enable_emoji: "Enable emoji" enable_emoji_shortcuts: "Common smiley text such as :) :p :( will be converted to emojis" emoji_set: "How would you like your emoji?" - enforce_square_emoji: "Force a square aspect ratio to all emojis." emoji_autocomplete_min_chars: "Minimum number of characters required to trigger autocomplete emoji popup" approve_post_count: "The amount of posts from a new or basic user that must be approved" diff --git a/config/site_settings.yml b/config/site_settings.yml index 37bcfc5bf5..0360cc7a64 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -677,8 +677,6 @@ posting: default: 'twitter' client: true enum: 'EmojiSetSiteSetting' - enforce_square_emoji: - default: true emoji_autocomplete_min_chars: client: true default: 0 diff --git a/db/migrate/20180913200027_remove_enforce_square_emoji.rb b/db/migrate/20180913200027_remove_enforce_square_emoji.rb new file mode 100644 index 0000000000..1509fcb85e --- /dev/null +++ b/db/migrate/20180913200027_remove_enforce_square_emoji.rb @@ -0,0 +1,5 @@ +class RemoveEnforceSquareEmoji < ActiveRecord::Migration[5.2] + def change + execute "DELETE FROM site_settings WHERE name = 'enforce_square_emoji'" + end +end From 0bfb1f83fcbb2729816387758cf491a9094b5bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 14 Sep 2018 11:09:36 +0200 Subject: [PATCH 55/59] make migration irreversible --- db/migrate/20180913200027_remove_enforce_square_emoji.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/db/migrate/20180913200027_remove_enforce_square_emoji.rb b/db/migrate/20180913200027_remove_enforce_square_emoji.rb index 1509fcb85e..a774f94795 100644 --- a/db/migrate/20180913200027_remove_enforce_square_emoji.rb +++ b/db/migrate/20180913200027_remove_enforce_square_emoji.rb @@ -1,5 +1,9 @@ class RemoveEnforceSquareEmoji < ActiveRecord::Migration[5.2] - def change + def up execute "DELETE FROM site_settings WHERE name = 'enforce_square_emoji'" end + + def down + raise ActiveRecord::IrreversibleMigration + end end From d2ea618af13b8e7bf8153a5b6ecf37553d34506f Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 14 Sep 2018 12:13:00 +0200 Subject: [PATCH 56/59] adds a test for mini-tag-chooser --- .../components/mini-tag-chooser-test.js.es6 | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 test/javascripts/components/mini-tag-chooser-test.js.es6 diff --git a/test/javascripts/components/mini-tag-chooser-test.js.es6 b/test/javascripts/components/mini-tag-chooser-test.js.es6 new file mode 100644 index 0000000000..3896ab8515 --- /dev/null +++ b/test/javascripts/components/mini-tag-chooser-test.js.es6 @@ -0,0 +1,86 @@ +import componentTest from "helpers/component-test"; + +moduleForComponent("mini-tag-chooser", { + integration: true, + beforeEach: function() { + this.set("subject", selectKit()); + } +}); + +componentTest("default", { + template: "{{mini-tag-chooser allowAny=true filterable=true tags=tags}}", + + beforeEach() { + this.set("tags", ["jeff", "neil", "arpit"]); + + const response = object => { + return [200, { "Content-Type": "application/json" }, object]; + }; + + // prettier-ignore + server.get("/tags/filter/search", (params) => { //eslint-disable-line + if (params.queryParams.q === "rég") { + return response({ + results: [{ text: "régis", count: 5 }] + }); + } + + if (params.queryParams.q === "joffrey") { + return response({results: []}); + } + + return response({ + results: [{ text: "bianca", count: 3 }, { text: "régis", count: 5 }] + }); + }); + }, + + async test(assert) { + await this.get("subject").expand(); + + assert.equal( + this.get("subject") + .rowByIndex(0) + .name(), + "bianca", + "it has the correct tag" + ); + + assert.equal( + this.get("subject") + .rowByIndex(1) + .name(), + "régis", + "it has the correct tag" + ); + + await this.get("subject").fillInFilter("rég"); + await this.get("subject").keyboard("enter"); + assert.deepEqual( + this.get("tags"), + ["jeff", "neil", "arpit", "régis"], + "it selects the tag" + ); + + await this.get("subject").expand(); + await this.get("subject").fillInFilter("joffrey"); + await this.get("subject").keyboard("enter"); + assert.deepEqual( + this.get("tags"), + ["jeff", "neil", "arpit", "régis"], + "it creates the tag" + ); + + await click( + this.get("subject") + .el() + .find(".selected-tag") + .last() + ); + assert.deepEqual( + this.get("tags"), + ["jeff", "neil", "arpit"], + "it removes the tag" + ); + } +}); From 754d5b62a881a79bffd3e368614b11c3c9e0045a Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 14 Sep 2018 13:30:55 +0200 Subject: [PATCH 57/59] makes mini-tag-chooser test use can_create_tag (#6402) --- test/javascripts/components/mini-tag-chooser-test.js.es6 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/javascripts/components/mini-tag-chooser-test.js.es6 b/test/javascripts/components/mini-tag-chooser-test.js.es6 index 3896ab8515..062aa37cbf 100644 --- a/test/javascripts/components/mini-tag-chooser-test.js.es6 +++ b/test/javascripts/components/mini-tag-chooser-test.js.es6 @@ -11,6 +11,7 @@ componentTest("default", { template: "{{mini-tag-chooser allowAny=true filterable=true tags=tags}}", beforeEach() { + this.site.set("can_create_tag", true); this.set("tags", ["jeff", "neil", "arpit"]); const response = object => { @@ -19,7 +20,7 @@ componentTest("default", { // prettier-ignore server.get("/tags/filter/search", (params) => { //eslint-disable-line - if (params.queryParams.q === "rég") { + if (params.queryParams.q === "régis") { return response({ results: [{ text: "régis", count: 5 }] }); @@ -54,7 +55,7 @@ componentTest("default", { "it has the correct tag" ); - await this.get("subject").fillInFilter("rég"); + await this.get("subject").fillInFilter("régis"); await this.get("subject").keyboard("enter"); assert.deepEqual( this.get("tags"), @@ -67,7 +68,7 @@ componentTest("default", { await this.get("subject").keyboard("enter"); assert.deepEqual( this.get("tags"), - ["jeff", "neil", "arpit", "régis"], + ["jeff", "neil", "arpit", "régis", "joffrey"], "it creates the tag" ); @@ -79,7 +80,7 @@ componentTest("default", { ); assert.deepEqual( this.get("tags"), - ["jeff", "neil", "arpit"], + ["jeff", "neil", "arpit", "régis"], "it removes the tag" ); } From a147a856f80885a348ac286b1136a54f4c2ced54 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 14 Sep 2018 11:43:14 -0400 Subject: [PATCH 58/59] Update translations --- config/locales/client.ar.yml | 22 +++++----- config/locales/client.de.yml | 3 +- config/locales/client.fi.yml | 42 ++++++++++++++++++- config/locales/client.pl_PL.yml | 2 + config/locales/client.pt_BR.yml | 12 +++--- config/locales/server.ar.yml | 3 -- config/locales/server.bg.yml | 1 - config/locales/server.ca.yml | 3 -- config/locales/server.da.yml | 1 - config/locales/server.de.yml | 5 ++- config/locales/server.el.yml | 3 -- config/locales/server.es.yml | 2 - config/locales/server.fa_IR.yml | 3 -- config/locales/server.fi.yml | 16 +++++-- config/locales/server.fr.yml | 2 - config/locales/server.he.yml | 3 -- config/locales/server.it.yml | 3 -- config/locales/server.ko.yml | 3 -- config/locales/server.nl.yml | 2 - config/locales/server.pl_PL.yml | 3 -- config/locales/server.pt.yml | 3 -- config/locales/server.pt_BR.yml | 5 --- config/locales/server.ro.yml | 3 -- config/locales/server.ru.yml | 2 - config/locales/server.sk.yml | 2 - config/locales/server.sq.yml | 1 - config/locales/server.sr.yml | 1 - config/locales/server.sv.yml | 2 - config/locales/server.tr_TR.yml | 2 - config/locales/server.ur.yml | 3 -- config/locales/server.vi.yml | 2 - config/locales/server.zh_CN.yml | 3 -- config/locales/server.zh_TW.yml | 6 --- .../config/locales/server.fi.yml | 1 + .../config/locales/server.sw.yml | 1 + plugins/poll/config/locales/server.ar.yml | 1 + 36 files changed, 81 insertions(+), 91 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index ee39319494..aea80facd5 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -543,7 +543,7 @@ ar: categories: all: "كل الأقسام" no_subcategory: "لا شيء" - category: "قسم" + category: "تصنيف" category_list: "أعرض قائمة الأقسام" reorder: title: "إعادة ترتيب الأقسام" @@ -613,6 +613,7 @@ ar: notifications: "الإشعارات" statistics: "الأحصائيات" desktop_notifications: + label: "الإشعارات الحية" not_supported: "نأسف، لا يدعم المتصفّح الإشعارات." perm_default: "فعّل الإشعارات" perm_denied_btn: "رُفض التّصريح" @@ -824,7 +825,7 @@ ar: email_private_messages: "أرسل إلي رسالة إلكترونية عندما يبعث أحدهم رسالة إلي" email_always: "أرسل إلي الاشعارات عبر البريد حتى ولو كنت متّصلًا" other_settings: "أخرى" - categories_settings: "الأقسام" + categories_settings: "التصنيفات" new_topic_duration: label: "اعتبر الموضوعات جديدة لو" not_viewed: "لم أطالعها بعد" @@ -952,6 +953,7 @@ ar: most_liked_users: "أكثر من أعجبه" most_replied_to_users: "أكثر من رد عليه" no_likes: "لا يوجد إعجابات بعد." + top_categories: "أفضل التصنيفات" ip_address: title: "عنوان IP الأخير" registration_ip_address: @@ -1733,7 +1735,7 @@ ar: title: 'دعوة' username_placeholder: "اسم المستخدم" action: 'أرسل دعوة' - help: 'ادعُ الغير إلى هذا الموضوع عبر البريد الإلكترونيّ أو الإشعارات' + help: 'دعوة الآخرين إلى هذا الموضوع عبر البريد الإلكتروني أو الإشعارات' to_forum: "سنُرسل بريد إلكترني يتيح لصديقك الانضمام مباشرةً بنقر رابط فيه، تسجيل الدخول غير مطلوب." sso_enabled: "أدخل اسم مَن تريد دعوته إلى هذا الموضوع." to_topic_blank: "أدخل اسم او عنوان بريد الشخص الذي تريد دعوته إلى هذا الموضوع." @@ -1885,7 +1887,7 @@ ar: attachment_download_requires_login: "عذرا، عليك تسجيل الدخول لتحميل المرفقات." abandon: confirm: "أمتأكد من التخلي عن المنشور؟" - no_value: "لا، أبقها" + no_value: "لا، أبقه" yes_value: "نعم، لا أريده" via_email: "وصل هذا المنشور عبر البريد" via_auto_generated_email: "وصل هذا المنشور عبر بريد مولّد آلياً" @@ -2087,7 +2089,7 @@ ar: all: 'كل الأقسام' edit: 'تعديل' edit_long: "تعديل" - view: 'أظهار الموضوعات في القسم' + view: 'أظهار المواضيع في القسم' general: 'عام' settings: 'اعدادات' topic_template: "إطار الموضوع" @@ -2097,9 +2099,9 @@ ar: tags_placeholder: "(اختياري) قائمة الأوسمة المسموح بها" tag_groups_placeholder: "(اختياريّ) قائمة مجموعات الأوسمة المسموح بها" topic_featured_link_allowed: "اسمح بالروابط المُميزة بهذا القسم." - delete: 'احذف القسم' - create: 'قسم جديد' - create_long: 'أنشئ قسم جديد' + delete: 'احذف التصنيف' + create: 'تصنيف جديد' + create_long: 'أنشئ تصنيف جديد' save: 'احفظ القسم' slug: 'عنوان القسم في الURL' slug_placeholder: '(اختياريّ) كلمات مفصولة-بشرطة للعنوان' @@ -2115,8 +2117,8 @@ ar: foreground_color: "لون المقدمة" name_placeholder: "كلمة أو كلمتين على الأكثر" color_placeholder: "أيّ لون متوافق مع الانترنت" - delete_confirm: "هل تريد فعلاً حذف هذا القسم؟" - delete_error: "حدث خطأ في حذف القسم." + delete_confirm: "هل تريد فعلاً حذف هذا تصنيف؟" + delete_error: "حدث خطأ أثناء حذف هذا التصنيف" list: "عرض الأقسام" no_description: "من فضلك أضف وصفا لهذا القسم." change_in_category_topic: "عدّل الوصف" diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index f1047c0452..f7754f1174 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -162,6 +162,7 @@ de: ap_southeast_1: "Asien-Pazifik (Singapur)" ap_southeast_2: "Asien-Pazifik (Sydney)" cn_north_1: "China (Peking)" + cn_northwest_1: "China (Ningxia)" eu_central_1: "EU (Frankfurt)" eu_west_1: "EU (Irland)" eu_west_2: "EU (London)" @@ -520,7 +521,7 @@ de: topic_stat_sentence: one: "%{count} neues Thema seit 1 %{unit}." other: "%{count} neue Themen seit 1 %{unit}." - more: "(%{count}mehr) …" + more: " (%{count} weitere) …" ip_lookup: title: IP-Adressen-Abfrage hostname: Hostname diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 287961cc3b..834bd3a396 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -162,6 +162,7 @@ fi: ap_southeast_1: "Aasia ja Tyynimeri (Singapore)" ap_southeast_2: "Aasia ja Tyynimeri (Sydney)" cn_north_1: "Kiina (Peking)" + cn_northwest_1: "Kiina (Ningxia)" eu_central_1: "EU (Frankfurt)" eu_west_1: "EU (Irlanti)" eu_west_2: "EU (Lontoo)" @@ -196,6 +197,7 @@ fi: privacy_policy: "Tietosuojaseloste" privacy: "Tietosuoja" tos: "Käyttöehdot" + rules: "Säännöt" mobile_view: "Mobiilinäkymä" desktop_view: "Työpöytänäkymä" you: "Sinä" @@ -536,6 +538,7 @@ fi: post_count: "# viestiä" confirm_delete_other_accounts: "Oletko varma, että haluat poistaa nämä tunnukset?" powered_by: "voimanlähteenä 1ipinfo.io" + copied: "kopioitu" user_fields: none: "(valitse vaihtoehto)" user: @@ -639,6 +642,7 @@ fi: revoke_access: "Peru käyttöoikeus" undo_revoke_access: "Peru käyttöoikeuden peruminen" api_approved: "Sallittu:" + api_last_used_at: "Viimeksi käytetty:" theme: "Teema" home: "Oletusnäkymä" staged: "Esikäyttäjä" @@ -777,6 +781,18 @@ fi: any: "mikä tahansa" password_confirmation: title: "Salasana uudelleen" + auth_tokens: + title: "Viimeksi käytetyt laitteet" + title_logs: "Todennusloki" + ip_address: "IP-osoite" + created: "Luotu" + first_seen: "Ensimmäinen vierailu" + last_seen: "Viimeisin vierailu" + operating_system: "Käyttöjärjestelmä" + location: "Paikka" + action: "Toimi" + login: "Kirjaudu sisään" + logout: "Kirjaudu ulos kaikkialta" last_posted: "Viimeisin viesti" last_emailed: "Viimeksi lähetetty sähköpostitse" last_seen: "Nähty" @@ -1008,6 +1024,8 @@ fi: hide_session: "Muistuta huomenna" hide_forever: "ei kiitos" hidden_for_session: "OK, kysyn huomenna uudestaan. Voit aina myös käyttää 'Kirjaudu sisään' -linkkiä luodaksesi tilin." + intro: "Hei! Vaikuttaa siltä, että olet pitänyt keskusteluista, muttet ole luonut käyttäjätiliä." + value_prop: "Kun luot tilin, muistamme mitä olet lukenut, jotta voit aina palata keskusteluissa takaisin oikeaan kohtaan. Saat myös ilmoituksia - palstalla näkyviä ja sähköpostiisi saapuvia - kun joku vastaa sinulle. Ja voit myös tykätä viesteistä. :heartbeat:" summary: enabled_description: "Tarkastelet tiivistelmää tästä ketjusta, sen mielenkiintoisimpia viestejä käyttäjien toiminnan perusteella." description: "Vastauksia on {{replyCount}} kpl." @@ -1021,6 +1039,8 @@ fi: disable: "Näytä poistetut viestit" private_message_info: title: "Viesti" + invite: "Kutsu muita..." + edit: "Lisää tai poista..." leave_message: "Haluatko varmasti poistua yksityiskeskustelusta?" remove_allowed_user: "Haluatko varmasti poistaa käyttäjän {{name}} tästä keskustelusta?" remove_allowed_group: "Haluatko varmasti poistaa käyttäjän {{name}} tästä viestiketjusta?" @@ -1156,6 +1176,7 @@ fi: default_header_text: Valitse... no_content: Ei osumia filter_placeholder: Hae... + filter_placeholder_with_any: Etsi tai luo... create: "Luo: '{{content}}'" max_content_reached: one: "Voit valita vain {{count}} kohteen." @@ -1305,6 +1326,9 @@ fi: shared_draft: label: "Jaettu luonnos" desc: "Luonnostele ketju, joka näkyy vain henkilökunnalle" + toggle_topic_bump: + label: "Ketjun nosto päälle/pois" + desc: "Vastaa muuttamatta ketjun päiväysleimaa" notifications: tooltip: regular: @@ -1609,7 +1633,7 @@ fi: auto_close: "Ketju suljetaan ajastetusti %{timeLeft}." auto_publish_to_category: "Ketju julkaistaan alueella #%{categoryName} %{timeLeft}." auto_close_based_on_last_post: "Ketju sulkeutuu %{duration} kuluttua viimeisimmästä viestistä." - auto_delete: "Ketju poistetaan ajastetusti %{timeLeft} kuluttua." + auto_delete: "Ketju poistetaan ajastetusti %{timeLeft}." auto_reminder: "Sinua muistutetaan tästä ketjusta %{timeLeft}." auto_close_title: 'Automaattisen sulkemisen asetukset' auto_close_immediate: @@ -1692,6 +1716,7 @@ fi: reset_read: "Poista tieto lukemisista" make_public: "Tee ketjusta julkinen" make_private: "Muuta yksityiskeskusteluksi" + reset_bump_date: "Palauta ketjun päiväysleima" feature: pin: "Kiinnitä ketju" unpin: "Poista ketjun kiinnitys" @@ -2569,6 +2594,7 @@ fi: disabled: Pois käytöstä timeout_error: Kyselyssä kestää liian kauan. Valitse lyhyempi ajanjakso. exception_error: "Pahoittelut, kyselyä tehtäessä tapahtui virhe" + too_many_requests: Olet tehnyt näin liian monta kertaa. Odota ennen kuin yrität uudelleen. reports: trend_title: "%{percent} muutos. Nyt %{current}, viime jaksossa %{prev}." today: "Tänään" @@ -2935,9 +2961,16 @@ fi: revert: "Peru muutokset" revert_confirm: "Haluatko varmasti peruuttaa muutokset?" theme: + theme: "Teema" + component: "Osa" + components: "Osat" import_theme: "Tuo teema" customize_desc: "Mukauta:" title: "Teemat" + modal_title: "Luo teema" + create: "Luo" + create_type: "Tyyppi:" + create_name: "Nimi:" long_title: "Muuta sivustosi värejä, CSS:ää ja HTML-sisältöä" edit: "Muokkaa" edit_confirm: "Tämä on ulkoinen teema. Jos muokkaat CSS:ää tai HTML:ää, muutokset kumoutuvat kun päivität teeman." @@ -2952,6 +2985,10 @@ fi: color_scheme_select: "Valitse teemalle värit" custom_sections: "Mukautetut osiot:" theme_components: "Teeman osat" + switch_component: "Muuta teemaksi" + switch_component_alert: "Oletko varma että haluat muuttaa teeman osan teemaksi. Siitä tulee itsenäinen teema ja se ei ole enää tytärteema millekään teemalle." + switch_theme: "Muuta teeman osaksi" + switch_theme_alert: "Oletko varma, että haluat muuttaa teeman teeman osaksi? Tällöin se ei ole enää emoteema millekään teeman osalle." uploads: "Lataukset" no_uploads: "Voit ladata teemaasi liitteitä kuten fontteja ja kuvia" add_upload: "Lisää tiedosto" @@ -2974,6 +3011,7 @@ fi: public_key: "Anna repositorioon pääsy seuraavalle julkiselle avaimelle:" about_theme: "Tietoa teemasta" license: "Lisenssi" + component_of: "Teemojen osa:" update_to_latest: "Päivitä tuoreimpaan" check_for_updates: "Hae päivityksiä" updating: "Päivitetään..." @@ -2981,9 +3019,11 @@ fi: add: "Lisää" theme_settings: "Teeman asetukset" no_settings: "Teemalla ei ole asetuksia." + empty: "Ei mitään" commits_behind: one: "Teema on yhden muutoksen perässä!" other: "Teema on {{count}} muutosta perässä!" + compare_commits: "(Näytä uudet muutokset)" scss: text: "CSS" title: "Lisää mukautettua CSS:ää, hyväksymme käyvät CSS- ja SCSS-tyylit" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index bd231ab53b..81fecab50b 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -1044,6 +1044,8 @@ pl_PL: hide_session: "Przypomnij mi jutro" hide_forever: "nie, dziękuję" hidden_for_session: "Ok, zapytamy jutro. Pamiętaj, że konto możesz w każdej chwili założyć klikając na 'Logowanie'." + intro: "Hej! Wygląda na to, że zainteresowała Cię ta dyskusja, ale nie posiadasz jeszcze konta." + value_prop: "Jeśli założysz konto, będziemy pamiętać dokładnie to, co przeczytałeś, więc zawsze wrócisz tam, gdzie ostatnio opuściłeś temat. Otrzymasz również powiadomienia, tutaj i za pośrednictwem poczty email, gdy ktoś Ci odpowie. Możesz również polubić posty, aby dzielić się miłością. :heartpulse:" summary: enabled_description: "Przeglądasz podsumowanie tego tematu: widoczne są jedynie najbardziej wartościowe wpisy zdaniem uczestników. " description: "Jest {{replyCount}} odpowiedzi." diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index b4abe691d0..0d2b4aee44 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -256,14 +256,14 @@ pt_BR: new_private_message: "Novo rascunho de mensagem privada" topic_reply: "Resposta preliminar" topic_count_latest: - one: "Veja {{conta}} tópicos novos ou atualizados" - other: "Veja {{conta}} tópicos novos ou atualizados" + one: "Veja {{count}} tópicos novos ou atualizados" + other: "Veja {{count}} tópicos novos ou atualizados" topic_count_unread: - one: "Veja {{conta}} tópicos não lidos." - other: "Veja {{conta}} tópicos não lidos." + one: "Veja {{count}} tópicos não lidos." + other: "Veja {{count}} tópicos não lidos." topic_count_new: - one: "Veja {{conta}} novos tópicos." - other: "Veja {{conta}} novos tópicos." + one: "Veja {{count}} novos tópicos." + other: "Veja {{count}} novos tópicos." preview: "pré-visualização" cancel: "cancelar" save: "Salvar mudanças" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 134473c7cf..0d4ac1510d 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -1044,8 +1044,6 @@ ar: s3_cdn_url: "URL لـCDN يستخدم لكل أصول s3 (مثال: https://cdn.somewhere.com). تحذير : بعد تغيير هذا الإعداد يجب عمل rebake لكل المشاركات القديمة." avatar_sizes: "قائمة أحجام الرمزية إنشاؤه تلقائيا." external_system_avatars_enabled: "استخدم خدمات الهه النظام الخارجي " - default_opengraph_image_url: "عنوان رابط صورة ال اوبن جراف الثابتة" - twitter_summary_large_image_url: "عنوان الURL للصورة الافتراضية لكارت الملخص علي تويتر ( يجب ان تكون علي الاقل بعرض 280 بكسل و ارتفاع 150 بكسل )" allow_all_attachments_for_group_messages: "اسمح بجميع ملحقات البريد للرسائل الجماعيه " enable_flash_video_onebox: "فعّل تضمين روابط swf وflv (أدوبي فلاش) في لوحات المعاينة. تحذير: قد يتسبّب بمخاطر أمنيّة." default_invitee_trust_level: "مستوى الثقة الإفتراضي (0-4) للأعضاء المدعوين." @@ -1194,7 +1192,6 @@ ar: slug_generation_method: "اختر طريقه توليد سبيكه . 'مشفره' سوف تقوم بتوليد سلسله مئويه مشفره.'لاشي' سوف يعطل السبيكه" enable_emoji: "فعّل الإيموجي" emoji_set: "كيف تريد أن تبدو الإيموجي؟" - enforce_square_emoji: "أجبِر النّسبة الباعيّة لكلّ الإيموجي لتكون مربّعة." approve_post_count: "عدد المنشورات للمستخدم الجديد او الاساسي يجب ان تتم الموافقه عليه " approve_unless_trust_level: "مشاركات للأعضاء أدنى من مستوى الثقة هذا يجب أن تتم الموافقة عليها." default_email_direct: "ارسل بريد الكتروني عندما يقوم احدهم بالرد/الاقتباس الي/ذكر او دعوه مستخدم افتراضيا" diff --git a/config/locales/server.bg.yml b/config/locales/server.bg.yml index 188ca7a851..136a24f24e 100644 --- a/config/locales/server.bg.yml +++ b/config/locales/server.bg.yml @@ -837,7 +837,6 @@ bg: slug_generation_method: "Изберете метод за генериране на URL. 'кодирано' ще използва букви URL, кодирани с проценти. 'не' ще откаже генерацията като цяло." enable_emoji: "Разреши емотикони" emoji_set: "Как бихте искали да е вашата емотикона?" - enforce_square_emoji: "Направи квадратни всички емотикони." approve_unless_trust_level: "Постовете на потребители с по-ниско от това ниво на доверие трябва да бъдат одобрени" default_email_direct: "Изпращай имейл по подразбиране, когато някой цитира, отговаря на публикацията или споменава името на потребителя." default_email_mailing_list_mode: "Изпращай ми имейл по подразбиране за всяка нова публикация." diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml index b7c4aaadbd..4f13297583 100644 --- a/config/locales/server.ca.yml +++ b/config/locales/server.ca.yml @@ -926,8 +926,6 @@ ca: avatar_sizes: "Llista de mides d'avatar generades automàticament." external_system_avatars_enabled: "Fes servir un servei extern de sistema d'avatars." external_system_avatars_url: "Adreça URL del sitema extern de servei d'avatars. Les substitucions permeses són {username} {first_letter} {color} {size}" - default_opengraph_image_url: "Adreça URL del protocol opengraph d'imatge per defecte." - twitter_summary_large_image_url: "Adreça URL del resum de la targeta d'imatge per defecte de Twitter (hauria de fer almenys 280px d'amplada i com a míimim 150px d'alçada)." allow_all_attachments_for_group_messages: "Permet tots els adjunts de correu per a missatges de grup." enable_flash_video_onebox: "Habilita la incrustació d'enllaços swf i flv (Adobe Flash) emmarcats. ATENCIÓ: això pot comportar riscos de seguretat." default_invitee_trust_level: "Nivell de confiança per defecte (0-4) per a persones convidades." @@ -1119,7 +1117,6 @@ ca: slug_generation_method: "Tria un mètode de generació d'adreces amigables. 'encoded' generarà un percentatge de codificació. 'none' anul·larà totalment l'adreça amigable." enable_emoji: "Activa emojo" emoji_set: "Com t'agradaria el teu emoji?" - enforce_square_emoji: "Força una relació d'aspecte quadrat per a tots els emojis." approve_post_count: "Cal aprovar la quantitat de publicacions d'una persona usuària nova o bàsica" approve_unless_trust_level: "Cal aprovar les publicacions per a persones usuàries amb aquest nivell de confiança" approve_new_topics_unless_trust_level: "Cal aprovar els nous temes per a persones amb aquest nivell de confiança" diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index 218cc54934..cca9301661 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -870,7 +870,6 @@ da: max_flags_per_day: "Maksimalt antal flagmarkeringer per bruger per dag." clean_orphan_uploads_grace_period_hours: "Grace-periode (i timer) før et forældreløst upload bliver fjernet." purge_deleted_uploads_grace_period_days: "Grace-periode (i dage) før et slettet upload bliver fjernet." - twitter_summary_large_image_url: "URL til standard Twitter Summary Card billede (skal være mindst 280px i bredden og mindst 150px i højden)." tl1_requires_topics_entered: "Hvor mange emner en ny bruger skal læse før vedkommende forfremmes til tillidsniveau 1." tl1_requires_read_posts: "Hvor mange indlæg en ny bruger skal læse før vedkommende forfremmes til tillidsniveau 1." tl1_requires_time_spent_mins: "Hvor mange minutter en ny bruger skal læse indlæg før vedkommende forfremmes til tillidsniveau 1." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 22b21031a7..887cbc2cf8 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -702,7 +702,6 @@ de: others: "Keine Antworten." no_drafts: self: "Du hast keine Entwürfe; beginne eine Antwort in einem beliebigen Thema und es wird automatisch als neuer Entwurf gespeichert." - others: "Du hast keine Berechtigung die Entwürfe dieses Benutzers zu sehen." topic_flag_types: spam: title: 'Spam' @@ -763,6 +762,9 @@ de: session_info: "Informationen zur Benutzersitzung lesen" read: "Alles lesen" write: "Alles schreiben" + flags: + errors: + already_handled: "Die Meldung wurde bereits bearbeitet" reports: default: labels: @@ -1503,7 +1505,6 @@ de: enable_emoji: "Aktiviere emoji" enable_emoji_shortcuts: "Geläufige Text-Smileys wie :) :p :( werden in Emojis umgewandelt" emoji_set: "Welche Emoji sollen es sein?" - enforce_square_emoji: "Emojis immer mit quadratischem Seitenverhältnis darstellen." emoji_autocomplete_min_chars: "Erforderliche Minimalanzahl von Zeichen, um das Popup zur Emoji-Autovervollständigung zu öffnen." approve_post_count: "Anzahl der Beiträge eines neuen Benutzer oder Anwärters, die genehmigt werden müssen" approve_unless_trust_level: "Beiträge von Benutzer unterhalb dieser Vertrauensstufe müssen genehmigt werden" diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index 4f70d032e8..259cb0e32e 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -1015,8 +1015,6 @@ el: avatar_sizes: "Κατάλογος από αυτόματα δημιουργημένα μεγέθη άβαταρ." external_system_avatars_enabled: "Χρησιμοποιήσε εξωτερική υπηρεσία για avatars." external_system_avatars_url: "URL της εξωτερικής υπηρεσίας avatars. Επιτρεπτές αντικαταστάσεις είναι {username} {first_letter} {color} {size}" - default_opengraph_image_url: "Η διεύθυνση URL της προεπιλεγμένης εικόνας opengraph." - twitter_summary_large_image_url: "Η διεύθυνση URL της εικόνας της προκαθορισμένης συνοπτικής κάρτας του Twitter (θα πρέπει να είναι τουλάχιστον 280px σε πλάτος, και τουλάχιστον 150px σε ύψος)." allow_all_attachments_for_group_messages: "Επιτρέπονται όλοι οι τύποι συνημμένων αρχείων σε ομαδικά μηνύματα." png_to_jpg_quality: "Ποιότητα των αποθηκευμένων αρχείων JPG (1 χαμηλή ποιότητα, 99 μέγιστη ποιότητα, 100 για απενεργοποίηση)." allow_staff_to_upload_any_file_in_pm: "Επίτρεψε στο προσωπικό να ανεβάζει οποιοδήποτε τύπο αρχείου στα πμ" @@ -1224,7 +1222,6 @@ el: slug_generation_method: "Επιλέξτε μια μέθοδο παραγωγής slug. «Κωδικοποιημένη» θα δημιουργήσει μια συμβολοσειρά για ποσοστιαία κωδικοποίηση. «Κανένα» θα απενεργοποιήσει τα slug." enable_emoji: "Ενεργοποίηση emoji" emoji_set: "Πως θα ήθελες το emoji σου;" - enforce_square_emoji: "Αναγκαστική εφαρμογή τετράγωνων διαστάσεων σε όλα τα emoji." approve_post_count: "Ο όγκος των αναρτήσεων ενός καινούριου ή βασικού χρήστη που θα πρέπει να εγκριθεί." approve_unless_trust_level: "Οι αναρτήσεις για χρήστες κάτω από αυτό το επίπεδο εμπιστοσύνης πρέπει να εγκριθούν" approve_new_topics_unless_trust_level: "Τα καινούρια νήματα για χρήστες κάτω από αυτό το επίπεδο εμπιστοσύνης πρέπει να εγκριθούν." diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index d349691671..2ae7b70111 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -722,7 +722,6 @@ es: others: "Sin respuestas." no_drafts: self: "No tienes ningún borrador; empieza a escribir una respuesta en cualquier tema y se guardará automáticamente como nuevo borrador." - others: "No tienes permisos para ver los borradores de este usuario." topic_flag_types: spam: title: 'Spam' @@ -1522,7 +1521,6 @@ es: enable_emoji: "Habilitar emoji" enable_emoji_shortcuts: "Texto común de emoticones como :) :p :( serán convertidos a emojis" emoji_set: "¿De qué tipo os gustan los emoji?" - enforce_square_emoji: "Forzar una relación de aspecto cuadrada para todos los emojis." emoji_autocomplete_min_chars: "Número mínimo de caracteres necesarios para que aparezca el diálogo de selección de emoji" approve_post_count: "La cantidad de posts que deben ser aprobados de usuarios nuevos o de nivel básico" approve_unless_trust_level: "Los posts de usuarios con un nivel de confianza inferior a este deberán ser aprobados" diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 0129ac2c7c..dabef08ef0 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -977,8 +977,6 @@ fa_IR: avatar_sizes: "لیست اندازه‌های آواتار که به صورت خودکار تولید شده است." external_system_avatars_enabled: "استفاده از سرویس آواتار خارجی." external_system_avatars_url: "لینک سرویس آواتار خارجی. تغییرات مجاز {username} {first_letter} {color} {size} هستند" - default_opengraph_image_url: "لینک تصویر پیشفرض اپن‌گراف" - twitter_summary_large_image_url: "لینک پیشفرض خلاصه توییتر و تصویر (باید حداقل 280 در 150 پیکسل باشد)" allow_all_attachments_for_group_messages: "اجازه‌ی ضمیمه فایل در ایمیل پیام‌های گروهی." png_to_jpg_quality: "کیفیت تبدیل فایل‌های JPG (1 کمترین کیفیت، 99 بالاترین کیفیت، 100 برای غیر‌فعال)" allow_staff_to_upload_any_file_in_pm: "به مدیران اجازه بده که هر فایلی در پیغام خصوصی آپلود کنند." @@ -1177,7 +1175,6 @@ fa_IR: slug_generation_method: "یک روش ایجاد نام یکتا انتخاب کنید. 'encoded' رشته‌ای شامل درصد ایجاد می‌کند. 'none' اسم یکتا را به صورت کلی غیر‌فعال می‌نماید." enable_emoji: "فعالسازی شکلک" emoji_set: "می‌خواهید شکلک شما چطور باشد؟" - enforce_square_emoji: "تحمیل نسبت ابعاد مربع به تمام شکلک‌ها. " approve_post_count: "تعداد نوشته‌های کاربر جدید که باید تایید شود." approve_unless_trust_level: "نوشته‌ها برای کاربران پایین تر از این سطح اعتماد نیاز به تایید دارد. " approve_new_topics_unless_trust_level: "موضوعات جدید برای کاربرانی با کمتر از این سطح اعتماد باید تایید شود" diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 9d49627688..9c07e639d8 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -629,6 +629,17 @@ fi: email_login: invalid_token: "Pahoittelut, tämä sisäänkirjautumislinkki on liian vanha. Paina 'Kirjaudu sisään' nappia ja valitse 'Unohdin salasanani' saadaksesi uuden linkin." title: "Sähköpostikirjautuminen" + user_auth_tokens: + devices: + android: 'Android-laite' + linux: 'Linux-tietokone' + windows: 'Windows-tietokone' + mac: 'Mac' + iphone: 'iPhone' + ipad: 'iPad' + ipod: 'iPod' + mobile: 'Mobiililaite' + unknown: 'Tuntematon laite' change_email: confirmed: "Sähköpostiosoite päivitetty." please_continue: "Jatka sivustolle %{site_name}" @@ -712,7 +723,6 @@ fi: others: "Ei vastauksia." no_drafts: self: "Sinulla ei ole luonnoksia. Aloita kirjoittamaan viestiä mihin tahansa ketjuun niin se tallentuu automaattiseksi uudeksi luonnokseksi." - others: "Et voi nähdä tämän käyttäjän luonnoksia." topic_flag_types: spam: title: 'Roskaposti' @@ -1128,6 +1138,7 @@ fi: maximum_session_age: "Käyttäjä pysyy sisäänkirjautuneena n tuntia vierailunsa jälkeen" ga_universal_tracking_code: "Google Universal analytics (analytics.js) seurantakoodi, esim.: UA-12345678-9; katso http://google.com/analytics" ga_universal_domain_name: "Google Universal analytics (analytics.js) verkkotunnus, esim.: osoite.fi; katso http://google.com/analytics" + ga_universal_auto_link_domains: "Ota käyttöön verkkotunnusten välinen seurantapalvelu Google Universal Analytics (analytics.js). Poisvieviin näiden verkkotunnusten linkkeihin lisätään client id -tunniste. Katso lisää Googlen Cross-Domain Tracking -oppaasta." gtm_container_id: "Google Tag Manager -säiliön ID. Esim: GTM-ABCDEF" enable_escaped_fragments: "Käytä Googlen Ajax-sivustoille tarkoitettua API:a, jos webcrawleria ei tunnisteta. Katso https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Salli valvojien luoda uusia alueita" @@ -1263,8 +1274,6 @@ fi: external_system_avatars_url: "Ulkoisen avatarpalvelun URL. Sallitut vaihdokset ovat {username} {first_letter} {color} {size}" selectable_avatars_enabled: "Pakota käyttäjä valitsemaan avatarinsa listalta." selectable_avatars: "Avatarit, joista käyttäjä voi valita." - default_opengraph_image_url: "Oletuksena käytettävän opengraph-kuvan URL." - twitter_summary_large_image_url: "Oletuksena käytettävän Twitter-tiivistelmäkortin kuva (tulisi olla leveydeltään ainakin 280 px leveä ja 150 px korkea)." allow_all_attachments_for_group_messages: "Salli kaikki sähköpostiliitteet ryhmäviesteissä." png_to_jpg_quality: "Muunnetun JPG-tiedoston laatu (1 on huonoin laatu, 99 on paras laatu, 100 ottaa pois käytöstä)." allow_staff_to_upload_any_file_in_pm: "Salli henkilökunnan ladata minkätyyppisiä liitteitä tahansa yksityisviesteihin." @@ -1492,7 +1501,6 @@ fi: slug_generation_method: "Valitse polkulyhenteen luomisen metodi. 'encoded' käyttää prosenttikoodausta, 'none' poistaa polkulyhenteet käytöstä." enable_emoji: "Ota emoji käyttöön" emoji_set: "Millaiset emojit haluat?" - enforce_square_emoji: "Pakota neliö kuvasuhteeksi kaikille emojille." approve_post_count: "Viestien lukumäärä, joka tarkastetaan uusilta käyttäjiltä ja haastajilta." approve_unless_trust_level: "Tätä luottamustasoa alhaisempien käyttäjien viestit tarkastetaan" approve_new_topics_unless_trust_level: "Tätä luottamustasoa alhaisempien käyttäjien aloittamat ketjut tarkastetaan" diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index f0c4fccea3..f80db36eb8 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -722,7 +722,6 @@ fr: others: "Aucune réponse." no_drafts: self: "Vous n'avez pas d'ébauche; commencez à répondre à un sujet et la réponse sera automatiquement sauvegardée comme nouvelle ébauche." - others: "Vous n'avez pas la permission de consulter les ébauches de cet utilisateur." topic_flag_types: spam: title: 'Spam' @@ -1523,7 +1522,6 @@ fr: enable_emoji: "Activer les emojis" enable_emoji_shortcuts: "Les textes de smiley courants :) :p :( seront convertis en emojis" emoji_set: "Comment aimeriez-vous vos emoji ?" - enforce_square_emoji: "Forcer tous les Emojis à être carrés." emoji_autocomplete_min_chars: "Nombre minimum de caractères nécessaires pour invoquer la fenêtre contextuelle d'emoji" approve_post_count: "Le nombre de messages d'un utilisateur nouveau ou basique devant être approuvés" approve_unless_trust_level: "Les messages des utilisateurs qui n'ont pas atteint ce niveau de confiance doivent être approuvés" diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 4be76b3b7d..5dc18b1662 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -1061,8 +1061,6 @@ he: avatar_sizes: "רשימה של גדלי דמויות שנוצרת אוטומטית." external_system_avatars_enabled: "שימוש בשירות של מערכת אווטארים חיצונית." external_system_avatars_url: "כתובת של שירות דמויות חיצוני. החלפות מותרות הן {username} {first_letter} {color} {size}" - default_opengraph_image_url: "URL ברירת המחדל של תמונת opengraph." - twitter_summary_large_image_url: "URL של תמונה מסיכום ברירת המחדל מטוויטר (צריכה להיות ברוחב לפחות 280 פיקסלים ובגובה של לפחות 150 פיקסלים)." allow_all_attachments_for_group_messages: "אפשרו צירוף קבצים להודעות לקבוצות." png_to_jpg_quality: "האיכות של קובץ JPG שמומר (1 הנמוכה ביותר, 99 הטובה ביותר, 100 לניטרול)." allow_staff_to_upload_any_file_in_pm: "אפשרו לחברי צוות להעלות כל קובץ בהודעה פרטית." @@ -1261,7 +1259,6 @@ he: slug_generation_method: "בחרו צורת ייצור slug. צורה של 'encoded' תגרום למחרוזות עם קידוד אחוזים. 'none' ינטרל slug לחלוטין." enable_emoji: "הפעלת emoji" emoji_set: "איך אתם אוהבים את ה-emoji שלכם?" - enforce_square_emoji: "חובת מימדים ריבועיים בכל ה-emojis." approve_post_count: "מספר הפוסטים ממשתמשים חדשים או בסיסיים שחייבים לאשר אותם" approve_unless_trust_level: "פוסטים של משתמשים מתחת לרמת אמון זו חייבים לעבור אישור" approve_new_topics_unless_trust_level: "נושאים חדשים עבור משתמשים מתחת לרמת אמון זו חייבים להיות מאושרים" diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 293e36912b..f8adcce74d 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -1181,8 +1181,6 @@ it: avatar_sizes: "Elenco delle dimensioni degli avatar, generate automaticamente." external_system_avatars_enabled: "Utilizza un servizio esterno per gli avatar." external_system_avatars_url: "URL del servizio esterno di avatar. Le sostituzioni consentite sono {username} {first_letter} {color} {size}" - default_opengraph_image_url: "URL dell'immagine di default opengraph." - twitter_summary_large_image_url: "URL dell'immagine di default della scheda di riepilogo di Twitter (dovrebbe essere almeno 280px in larghezza e almeno 150px in altezza)." allow_all_attachments_for_group_messages: "Consenti tutti gli allegati sulle email per i messaggi di gruppo." png_to_jpg_quality: "Qualità del file JPG convertito (1 è la qualità più bassa, 99 è la qualità migliore, 100 per disabilitare)." allow_staff_to_upload_any_file_in_pm: "Consenti ai membri delle staff di caricare qualsiasi file nei PM." @@ -1394,7 +1392,6 @@ it: enable_emoji: "Attiva gli emoji" enable_emoji_shortcuts: "Smiley comuni come :) :p :( verranno convertiti in emoji" emoji_set: "Come ti piacerebbero le tue emoji?" - enforce_square_emoji: "Forza tutte le emoji ad un aspetto di proporzioni quadrate." approve_post_count: "La quantità di messaggi di un utente nuovo o di base che deve essere approvata" approve_unless_trust_level: "I messaggi degli utenti al di sotto di questo livello di esperienza devono essere approvati" approve_new_topics_unless_trust_level: "I nuovi argomenti degli utenti al di sotto di questo livello di esperienza devono essere approvati" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 9506c7f054..9f3b963a3c 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -958,8 +958,6 @@ ko: avatar_sizes: "자동 생성 아바타 사이즈 목록" external_system_avatars_enabled: "외부 아바타 시스템을 사용하기" external_system_avatars_url: "외부 아바타 서비스의 URL. {username} {first_letter} {color} {size} 의 대체가 허용됨" - default_opengraph_image_url: "기본 오픈그래프 이미지의 URL" - twitter_summary_large_image_url: "기본 Twitter 요약 카드 이미지의 URL( 가로 280px, 세로 150px 이상이어야 함)" allow_all_attachments_for_group_messages: "그룹 메시지에 모든 이메일 첨부 허용" png_to_jpg_quality: "변환된 JPG 파일의 퀄리티(1은 최하, 99는 최상, 100은 해제)" allow_staff_to_upload_any_file_in_pm: "운영진이 보내는 개인메시지에 모든 파일 첨부 허용" @@ -1151,7 +1149,6 @@ ko: slug_generation_method: "slug 생성 방식. 'encoded'는 퍼센트 기호로 인코딩해서 생성합니다. 'none'은 slug 자체를 비활성화합니다.." enable_emoji: "emoji 활성화" emoji_set: "어떤 emoji가 좋은가요?" - enforce_square_emoji: "모든 emoji를 정사각형 비율로 강제로 바꿉니다." approve_unless_trust_level: "허가 받고 글 올려야 하는 유저들 최저 회원등급" default_email_mailing_list_mode: "기본으로 새로운 포스트가 생길 때마다 이메일 보내기" default_email_mailing_list_mode_frequency: "이 빈도로 이메일 받기를 설정한 메일링 리스트 가입자" diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 531b25b983..7456137b84 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -954,7 +954,6 @@ nl: avatar_sizes: "Lijst van automatisch gegenereerde avatar groottes." external_system_avatars_enabled: "Gebruik een avatars service van een extern systeem." external_system_avatars_url: "URL van de externe avatar service. Toegestaande vervangingen zijn {username} {first_letter} {color} {size}" - default_opengraph_image_url: "URL van de standaard opengraph afbeelding." png_to_jpg_quality: "Kwaliteit van het geconverteerde JPG bestand (1 is laagste kwaliteit, 99 is beste kwaliteit, 100 om uit te schakelen)." strip_image_metadata: "Verwijder metadata uit afbeelding." enable_flash_video_onebox: "Embedden van swf en flv (Adobe Flash) links toestaan in oneboxen. LET OP: dit kan onveilig zijn" @@ -1105,7 +1104,6 @@ nl: slug_generation_method: "Kies een slug generate methode. 'encoded' zal een percentage encoderen string genereren. 'none' zal slug helemaal uitschakelen." enable_emoji: "Inschakelen emoji" emoji_set: "Welke emoji wilt u gebruiken?" - enforce_square_emoji: "Forceer een vierkant aspect ratio bij alle emojis." approve_post_count: "Het aantal berichten van een nieuwe of normale gebruiker dat moet worden goedgekeurd" approve_unless_trust_level: "Posts voor gebruikers onder dit vertrouwensniveau moeten worden goedgekeurd" approve_new_topics_unless_trust_level: "Nieuwe topics voor gebruikers onder dit vertrouwensniveau moeten worden goedgekeurd" diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 07942d334d..f8dd95f752 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -1048,8 +1048,6 @@ pl_PL: avatar_sizes: "Lista automatycznie wygenerowanych rozmiarów awatarów." external_system_avatars_enabled: "Użyj zewnętrzny system awatarów." external_system_avatars_url: "Adres URL zewnętrznego dostawcy awatarów. Dozwolone podstawienia: {username} {first_letter} {color} {size}" - default_opengraph_image_url: "Adres URL domyślnego obrazu otwarciawykresu." - twitter_summary_large_image_url: "Adres URL domyślnego obrazu karty podsumowanie Twitter (powinien mieć przynajmniej 280px szerokości oraz 150px wysokości)." allow_all_attachments_for_group_messages: "Zezwól na wszystkie załączniki email dla wiadomości grupowych." png_to_jpg_quality: "Jakość skonwertowanego pliku JPG (1 to najniższa jakość, 99 to najlepsza jakość, 100 aby dezaktywować)." allow_staff_to_upload_any_file_in_pm: "Pozwól personelowi przesyłać pliki w wiadomościach." @@ -1249,7 +1247,6 @@ pl_PL: slug_generation_method: "Wybierz metodę tworzenia ślimaka. 'Zakodowane\" stworzy procentowy ciąg kodujący, \"żadne\" wyłączy tą metodę." enable_emoji: "Włącz obsługę emoji" emoji_set: "Jaki jest twój preferowany styl emoji?" - enforce_square_emoji: "Narzuć kwadratowy stosunek dla wszystkich emoji." approve_post_count: "Liczba postów od nowego lub podstawowego użytkownika, które muszą zostać zatwierdzone" approve_unless_trust_level: "Posty użytkowników poniżej tego poziomu zaufania muszą być zatwierdzane" approve_new_topics_unless_trust_level: "Nowe wątki dodawane przez użytkowników z poziomem zaufania poniżej muszą zostać zaakceptowane" diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index 63912df65d..5c19747db4 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -907,8 +907,6 @@ pt: avatar_sizes: "Lista de tamanhos de avatar gerados automaticamente." external_system_avatars_enabled: "Utilize o serviço do sistema externo de avatars." external_system_avatars_url: "URL do serviço de avatares externo do sistema. Substituições permitidas são {username} {first_letter} {color} {size}" - default_opengraph_image_url: "URL da imagem opengraph por defeito." - twitter_summary_large_image_url: "URL da imagem do cartão de sumário de defeito do Twitter (deverá ser pelo menos 280px em largura, e pelo menos 150px em altura)." allow_all_attachments_for_group_messages: "Permitir qualquer anexo de email nas mensagens de grupo." enable_flash_video_onebox: "Ativar a incorporação de hiperligações swf e flv (Adobe Flash) em caixas únicas. AVISO: pode introduzir riscos de segurança." default_invitee_trust_level: "Nível de Confiança padrão (0-4) para utilizadores convidados." @@ -1095,7 +1093,6 @@ pt: slug_generation_method: "Escolha um método de geração slug. 'encoded' irá gerar sequências de caracteres com código percentual. 'none' irá desativar slug por completo." enable_emoji: "Ativar emoji" emoji_set: "Como gostaria de ter o seu emoji?" - enforce_square_emoji: "Forçar uma relação de aspecto quadrado para todos os emojis." approve_post_count: "A quantidade de publicações de um novo utilizador ou utilizador básico que devem ser aprovadas" approve_unless_trust_level: "Mensagens para utilizadores abaixo deste nível de confiança devem ser aprovados" approve_new_topics_unless_trust_level: "Novos tópicos para utilizadores abaixo deste nível de confiança devem ser aprovados" diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 5c8b246457..b9506e74f3 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -701,8 +701,6 @@ pt_BR: no_replies: self: "Você não respondeu a nenhuma postagem." others: "Sem Respostas." - no_drafts: - others: "Você não tem permissão para ver rascunhos desse usuário." topic_flag_types: spam: title: 'Spam' @@ -1264,8 +1262,6 @@ pt_BR: external_system_avatars_url: "URL do sistema externo do serviço de avatares. Substituições permitidas são {username} {first_letter} {color} {size}" selectable_avatars_enabled: "Force os usuários a escolher um avatar da lista." selectable_avatars: "Lista de usuários avatares podem escolher." - default_opengraph_image_url: "URL da imagem opengraph padrão." - twitter_summary_large_image_url: "URL da imagem do cartão de resumo padrão do Twitter (deve ter pelo menos 280px de largura e pelo menos 150px de altura)." allow_all_attachments_for_group_messages: "Permitir todos os anexos de e-mail para mensagens de grupo." png_to_jpg_quality: "Qualidade do arquivo JPG convertido (1 é a qualidade mais baixa, 99 é a melhor qualidade, 100 a desabilitar)." allow_staff_to_upload_any_file_in_pm: "Permitir que membros da equipe façam upload de arquivos no PM." @@ -1499,7 +1495,6 @@ pt_BR: enable_emoji: "Habilitar emoji" enable_emoji_shortcuts: "Texto em smiley comum como :): p :( será convertido em emojis" emoji_set: "Como você gostaria do seu emoji?" - enforce_square_emoji: "Forçar proporção quadrangular para todos emojis." emoji_autocomplete_min_chars: "Número mínimo de caracteres necessários para acionar o pop-up de emoji de preenchimento automático" approve_post_count: "A quantidade de postagens de um usuário novo ou básico que deve ser aprovado" approve_unless_trust_level: "Mensagens para os usuários abaixo deste nível de confiança devem ser aprovados" diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 950558c1d5..5ce98a13ee 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -914,8 +914,6 @@ ro: avatar_sizes: "Lista mărimilor avatarurilor generate automat." external_system_avatars_enabled: "Folosiți un serviciu extern de sisteme de avataruri" external_system_avatars_url: "URL-ul serviciului extern de sisteme de avataruri. Înlocuirile permise sunt {username} {first_letter} {color} {size}" - default_opengraph_image_url: "URL-ul pentru imaginea implicită pentru Open Graph" - twitter_summary_large_image_url: "URL-ul pentru imaginea implicită pentru imaginea card cu rezumatul Twitter (trebuie să aibă minim 280px lățime și 150px înălțime)." allow_all_attachments_for_group_messages: "Permite toate atașamentele de email pentru mesajele de grup." enable_flash_video_onebox: "Activează încastrarea adreselor swf și flv (Adobe Flash) în onebox. ATENȚIE: poate aduce riscuri de securitate." default_invitee_trust_level: "Nivelul de încredere implicit (0-4) pentru utilizatorii invitați." @@ -1101,7 +1099,6 @@ ro: slug_generation_method: "Alege o metodă de generare de identificator. 'encodat' va genera șir de caractere encodate cu procentaje. 'nimic' va dezactiva complet identificatorii." enable_emoji: "Activează emoji" emoji_set: "Cum ți-ar placea să fie emoji tăi?" - enforce_square_emoji: "Forțează un aspect pătrățos la toti emoji." approve_post_count: "Numărul de postări de la un utilizator simplu trebuie să fie aprobat" approve_unless_trust_level: "Postările utilizatorilor care au un nivel de încredere mai mic decât acesta trebuie să fie aprobate." approve_new_topics_unless_trust_level: "Crearea de subiecte noi pentru utilizatorii care se află sub acest nivel de încredere, trebuie aprobată" diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index bef3d2209b..45bef1063e 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -903,7 +903,6 @@ ru: s3_secret_access_key: "Amazon S3 secret key для загрузки и хранения изображений" s3_region: "Географический регион Amazon S3, который будет использоваться для хранения изображений" avatar_sizes: "Список автоматически сгенерированных размеров аватар." - default_opengraph_image_url: "URL картинки opengraph по-умолчанию." allow_all_attachments_for_group_messages: "Разрешить все почтовые вложения для групповых сообщений." enable_flash_video_onebox: "Разрешить умную вставку ссылок sqf и flv (Adobe Flash). ВНИМАНИЕ: повышает риски безопасности сайта." default_invitee_trust_level: "Уровень доверия приглашенных пользователей по-умолчанию (от 0 до 4)." @@ -1037,7 +1036,6 @@ ru: slug_generation_method: "Выберите метод генерации URL. 'encoded' будет изпользовать русские буквы в URL закодированные через проценты. 'none' не будет использовать перегенирацию." enable_emoji: "Активировать смайлы Emoji" emoji_set: "Какую коллекцию Emoji использовать?" - enforce_square_emoji: "Принудительно использовать квадратный формат для всех смайлов." approve_unless_trust_level: "Сообщения для пользователей ниже этого уровня доверия подлежат проверки" default_email_mailing_list_mode: "По умолчанию присылать почтовое уведомление, когда появляется новое сообщение." default_other_external_links_in_new_tab: "По умолчанию открывать внешние ссылки в новой вкладке." diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 94e185d75e..bc292cb6a8 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -879,7 +879,6 @@ sk: s3_cdn_url: "CDN URL ktoré sa použije na všetky dáta v s3 (napríklad https://cdn.niekde.com). VAROVANIE: po zmene tohoto nastavenia musíte použiť príkaz rebake na všetky staré príspevky." avatar_sizes: "Zoznam automaticky generovaných veľkostí avatarov." external_system_avatars_enabled: "Použiť externú avatar službu." - default_opengraph_image_url: "URL štandardného opengraph obrázku." enable_flash_video_onebox: "Povolenia vkladania odkazov na swf a flv (Adobe Flash) v onebox. VAROVANIE: môže vniesť bezpečnostné riziká." default_invitee_trust_level: "Predvolená stupeň dôvery (0-4) pre pozvaných používateľov." default_trust_level: "Východzí stupeň dôvery (0-4) pre všekých nových používateľov. UPOZORNENIE: Zmena nastavenia Vás môže vystaviť riziku spamovania. " @@ -1017,7 +1016,6 @@ sk: slug_generation_method: "Vybrať metódu generovania koncovej časti adresy. 'encoded' vygeneruje percentami zakódovaní reťazec. 'none' vypne koncovú časť adresy úplne." enable_emoji: "Zapnúť emoji" emoji_set: "Aké emoji chcete mať?" - enforce_square_emoji: "Vynútiť švorcový pomer strán na všetkých emoji." approve_unless_trust_level: "Príspevky používateľov pod touto úrovňou důvery musia byť schválené" auto_close_messages_post_count: "Maximálny počet povolených príspevkov v správe kým je automaticky uzavretá (0 znamená vypnuté)" auto_close_topics_post_count: "Maximálny počet povolených príspevkov v téme kým je automaticky uzavretá (0 znamená vypnuté)" diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index 28550a07c9..3dc52a7de0 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -759,7 +759,6 @@ sq: slug_generation_method: "Choose a slug generation method. 'encoded' will generate percent encoding string. 'none' will disable slug at all." enable_emoji: "Enable emoji" emoji_set: "How would you like your emoji?" - enforce_square_emoji: "Force a square aspect ratio to all emojis." approve_unless_trust_level: "Posts for users below this trust level must be approved" auto_close_messages_post_count: "Numri më i lartë i lejueshëm i postimeve të lejuara në një mesazh përpara se të mbyllet automatikisht (0 për ta çaktivizuar)" auto_close_topics_post_count: "Numri më i lartë i lejueshëm i postimeve të lejuara në një temë përpara se të mbyllet automatikisht (0 për ta çaktivizuar)" diff --git a/config/locales/server.sr.yml b/config/locales/server.sr.yml index d2b553851d..d3ffb11bc2 100644 --- a/config/locales/server.sr.yml +++ b/config/locales/server.sr.yml @@ -166,7 +166,6 @@ sr: show_create_topics_notice: "Ako sajt ima manje od 5 javnih tema, prikaži poruku koja sugeriše administratorima da kreiraju neke teme." delete_drafts_older_than_n_days: Izbriši radne verzije poruka starije od (n) dana enable_emoji: "Omogući emotikone" - enforce_square_emoji: "Forsiraj kvadratni oblik za sve emotikone." auto_close_messages_post_count: "Maksimalan broj dozvoljenih poruka pre nego se tema automatski zatvori (0 da se isključi)" default_email_mailing_list_mode: "Podrazumevano šalji email za svaku novu poruku" default_email_previous_replies: "Podrazumevano uključi prethodne odgovore u email poruku" diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 07d4b22155..92487130af 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -861,7 +861,6 @@ sv: s3_cdn_url: "CDN URL som används för alla S3-tillgångar (till exempel: https://cdn.somewhere.com). VARNING: efter ändring av den här inställningen så måste du uppdatera alla gamla inlägg med rake posts: rebake." avatar_sizes: "Lista på automatiskt genererade avatar-storlekar." external_system_avatars_enabled: "Använd externa system för avatar-tjänster." - default_opengraph_image_url: "URL för standard opengraphbilden." allow_all_attachments_for_group_messages: "Tillåt alla e-postbilagor för gruppmeddelanden." enable_flash_video_onebox: "Aktivera inbäddning av swf- och fic- (Adobe Flash) länkar i onebox:ar. VARNING: det kan introducera säkerhetsrisker." default_invitee_trust_level: "Förvald förtroendenivå (0-4) för inbjudna användare." @@ -1048,7 +1047,6 @@ sv: slug_generation_method: "Välj en genereringsmetod för etiketter. Ange 'none' för att inaktivera generering av etiketter. " enable_emoji: "Aktivera emoji" emoji_set: "Hur skulle du vilja ha din emoji?" - enforce_square_emoji: "Tvinga fram en kvadratisk bildkvot för alla emojis." approve_post_count: "Antalet inlägg från en ny användare eller basanvändare som måste godkännas" approve_unless_trust_level: "Inlägg för användare under den här förtroendenivån måste godkännas" approve_new_topics_unless_trust_level: "Nya ämnen för användare under denna förtroendenivå måste bli godkända" diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 475e5a604b..95c0532a4b 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -793,7 +793,6 @@ tr_TR: s3_cdn_url: "Tüm s3 öğeleri için CDN URL adresi. (örneğin: https://cdn.somewhere.com). UYARI: bu ayarı değiştirdikten sonra tüm eski gönderileri rebake etmelisiniz." avatar_sizes: "Otomatik üretilen avatar ölçülerinin listesi." external_system_avatars_enabled: "Dışsal sistem avatarları hizmeti kullan." - default_opengraph_image_url: "Öntanımlı opengraph imajının URL'si." allow_all_attachments_for_group_messages: "Grup iletileri için tüm e-posta eklentilerine izin ver." enable_flash_video_onebox: "Kutularda swf ve flv (Adobe Flash) yerleştirmelerine izin ver. UYARI: güvenlik açıkları doğurabilir" default_invitee_trust_level: "Davet edilen kullanıcılar için öntanımlı güven seviyesi (0-4)." @@ -946,7 +945,6 @@ tr_TR: slug_generation_method: "Slug üretim yöntemi seçin. 'kodlanmış' seçeneği yüzde kodlamalı metin oluşturur. 'hiçbiri' seçeneği slug'ı devre dışı bırakır." enable_emoji: "Emojiyi etkinleştir" emoji_set: "Emojinizi nasıl isterdiniz?" - enforce_square_emoji: "Tüm emojileri kare en-boy oranına zorla" approve_unless_trust_level: "Bu güven seviyesi altındaki kullanıcılardan gelen gönderilerin onaylanması gerekir" default_email_digest_frequency: "Öntanımlı olarak kullanıcılar ne sıklıkla e-posta özeti alacak." default_email_direct: "Öntanımlı olarak birisi bir kullanıcı hakkında alıntı yapma, cevaplama, bahsetme ya da davet etme eylemlerini gerçekleştirdiğinde e-posta gönder." diff --git a/config/locales/server.ur.yml b/config/locales/server.ur.yml index 3e5de752c7..535c43437a 100644 --- a/config/locales/server.ur.yml +++ b/config/locales/server.ur.yml @@ -1106,8 +1106,6 @@ ur: avatar_sizes: "خود کار طریقے سے تیار کردہ اوتار کے سائزوں کی فہرست۔" external_system_avatars_enabled: "بیرونی سسٹم کی اوتار سروس کا استعمال کریں۔" external_system_avatars_url: "بیرونی سسٹم کی اوتار سروس کا URL۔ اجازت شدہ متبادلات {username} {first_letter} {color} {size} ہیں" - default_opengraph_image_url: "ڈِیفالٹ اَوپَن٘گراف تصویر کا URL۔" - twitter_summary_large_image_url: "ٹویٹر خلاصہ کارڈ کی ڈِیفالٹ تصویر کا URL (کم از کم 280px چوڑائی، اور کم از کم 150px اونچائی کے سائز میں ہونا چاہئے)۔" allow_all_attachments_for_group_messages: "گروپ کے پیغامات کیلئے تمام ای میل منسلکات کی اجازت دیں۔" png_to_jpg_quality: "تبدیل شدہ JPG فائل کا معیار (1 سب سے کم معیار ہے، 99 بہترین معیار ہے، 100 غیر فعال)۔" allow_staff_to_upload_any_file_in_pm: "سٹاف ارکان کو PM میں کوئی بھی فائل اَپ لوڈ کرنے کی اجازت دیں۔" @@ -1332,7 +1330,6 @@ ur: enable_emoji: "اِیمَوجی فعال کریں" enable_emoji_shortcuts: "عام سمائلی ٹیکسٹ جیسا کہ :) :p :( اِیمَوجیوں میں تبدیل کر دیا جائے گا" emoji_set: "آپ اپنا اِیمَوجی کیسا پسند کریں گے؟" - enforce_square_emoji: "تمام اِیمَوجیوں کو ایک مربع اَیسپَیکٹ رَیشو پر مجبور کریں۔" approve_post_count: "ایک نئے یا بَیسِک صارف کی طرف سے پوسٹس کی تعداد جن کا منظور کیے جانا لاذمی ہے " approve_unless_trust_level: "اِس ٹرسٹ لَیول سے نیچے کے صارفین کیلئے پوسٹس کا منظور شدہ ہونا لازمی ہے" approve_new_topics_unless_trust_level: "اِس ٹرسٹ لَیول سے نیچے کے صارفین کیلئے نئے ٹاپکس کا منظور شدہ ہونا لازمی ہے" diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index 20a35869a9..01d4d1189e 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -722,7 +722,6 @@ vi: s3_cdn_url: "CDN URL được sử dụng cho tất cả các tài nguyên S3 (vd: https://cdn.somewhere.com). CHÚ Ý: sau khi thay đổi thiết lập này bạn phải tạo lại các bài viết cũ." avatar_sizes: "Danh sách những kích thước hình đại diện tự động khởi tạo." external_system_avatars_enabled: "Sử dụng dịch vụ ảnh đại diện bên ngoài." - default_opengraph_image_url: "URL của ảnh opengraph mặc định." allow_all_attachments_for_group_messages: "Cho phép các email có đính kèm trong tin nhắn nhóm." enable_flash_video_onebox: "Cho phép nhúng liên kết swf và flv (Adobe Flash). CHÚ Ý: có thể chứa đựng các rủi ro bảo mật." default_invitee_trust_level: "Bậc tin tưởng mặc định (0-4) cho thành viên được mời." @@ -872,7 +871,6 @@ vi: slug_generation_method: "Chọn phương thức tạo slug. 'encoded' sẽ tạo ra phần trăm chuỗi mã hóa. 'none' sẽ tắt slug." enable_emoji: "Kích hoạt emoji" emoji_set: "Bạn thích dùng gói emoji nào?" - enforce_square_emoji: "Sử dụng kích thước vuông cho tất cả các emoji." approve_post_count: "Số lượng bài viết từ một thành viên mới hoặc thành viên cũ phải được duyệt" approve_unless_trust_level: "Bài viết của thành viên dưới cấp độ tin cậy này phải được duyệt" default_email_direct: "Gửi email khi ai đó trích dẫn/trả lời/đề cập hoặc mời thành viên theo mặc định." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index f3778fbbaf..43161daa54 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -990,8 +990,6 @@ zh_CN: avatar_sizes: "自动生成的头像大小列表。" external_system_avatars_enabled: "使用外部系统头像服务。" external_system_avatars_url: "外部系统头像服务的 URL 地址。可选参数是 {username} {first_letter} {color} {size}" - default_opengraph_image_url: "opengraph图像的缺省URL。" - twitter_summary_large_image_url: "默认 Twitter summary 卡片的 URL(需要至少宽 280px 和高 150px)。" allow_all_attachments_for_group_messages: "允许群组私信中包含任何邮件附件。" png_to_jpg_quality: "转换 JPG 文件的质量(1 是最低,99 最高,100 禁用)。" allow_staff_to_upload_any_file_in_pm: "允许管理人员在私信中上传任何文件。" @@ -1201,7 +1199,6 @@ zh_CN: slug_generation_method: "选择一个链接生成方式。“encoded”将生成以百分号编码的链接。“none”将禁用自定义链接,只生成默认链接。" enable_emoji: "启用绘文字(emoji)" emoji_set: "你喜欢哪一种 emoji?" - enforce_square_emoji: "强制为所有 emojis 设置正方形比例。" approve_post_count: "新用户或基础用户需要被审核的帖子数量" approve_unless_trust_level: "该信任等级之下的用户的帖子必须被审核" approve_new_topics_unless_trust_level: "低于该信任等级的用户的新帖子需要被审核" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 8cbcf3595e..591fb78173 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -894,8 +894,6 @@ zh_TW: avatar_sizes: "自動生成的頭像大小列表。" external_system_avatars_enabled: "使用外部系統頭像服務。" external_system_avatars_url: "外部系統頭像服務的 URL 地址。可選參數是 {username} {first_letter} {color} {size}" - default_opengraph_image_url: "opengraph圖像的預設URL。" - twitter_summary_large_image_url: "預設 Twitter summary 卡片的 URL(需要至少寬 280px 和高 150px)。" allow_all_attachments_for_group_messages: "允許群組消息中包含任何郵件附件。" enable_flash_video_onebox: "在連結預覽中啟用 swf 和 flv (Adobe Flash) 嵌入。警告:可能增加安全風險。" default_invitee_trust_level: "預設的受邀用戶等級 (0-4)" @@ -1086,7 +1084,6 @@ zh_TW: slug_generation_method: "選擇一個連結生成方式。“encoded”將生成以百分號編碼的連結。“none”將禁用自定義連結,只生成預設連結。" enable_emoji: "啟用表情符號" emoji_set: "你喜歡怎麼樣的表情符號?" - enforce_square_emoji: "強制所有表情符號成為正方形比例。" approve_post_count: "新用戶或基礎用戶需要被審核的帖子數量" approve_unless_trust_level: "該信任等級之下的用戶的帖子必須被審核" approve_new_topics_unless_trust_level: "低於該信任等級的用戶的新帖子需要被審核" @@ -1188,9 +1185,6 @@ zh_TW: other: "%{count} 個帖子被分離到了新主題:%{topic_link}" existing_topic_moderator_post: other: "%{count} 個帖子被合併到現存主題:%{topic_link}" - change_owner: - post_revision_text: "所有權從 %{old_user} 轉移至 %{new_user}" - deleted_user: "已經刪除的用戶" topic_statuses: archived_enabled: "此討論話題已封存,即已經凍結,無法修改。" archived_disabled: "此討論話題已被解除封存,即不再凍結,可以修改。" diff --git a/plugins/discourse-narrative-bot/config/locales/server.fi.yml b/plugins/discourse-narrative-bot/config/locales/server.fi.yml index 7b40cefea9..32cd32bc65 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.fi.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.fi.yml @@ -7,6 +7,7 @@ fi: site_settings: + discourse_narrative_bot_enabled: 'Ota Discoursen opastava botti käyttöön (discobot)' disable_discourse_narrative_bot_welcome_post: "Estä bottia lähettämästä tervetuloviestejä" discourse_narrative_bot_ignored_usernames: "Käyttäjänimet, jotka botin tulisi jättää huomiotta" discourse_narrative_bot_disable_public_replies: "Estä bottia vastaamasta julkisesti ketjuihin" diff --git a/plugins/discourse-narrative-bot/config/locales/server.sw.yml b/plugins/discourse-narrative-bot/config/locales/server.sw.yml index 59103d5ca3..7e6540ed70 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.sw.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.sw.yml @@ -7,6 +7,7 @@ sw: site_settings: + discourse_narrative_bot_enabled: 'Ruhusu roboti wa maelezo ya Discourse (discobot)' disable_discourse_narrative_bot_welcome_post: "Sitisha Roboti wa Discourse kukaribisha watu na taarifa" discourse_narrative_bot_ignored_usernames: "Majina ya watumiaji ambayo Roboti wa Discourse aya dharau" discourse_narrative_bot_disable_public_replies: "Sitisha majibu ya umma ya roboti wa Discourse" diff --git a/plugins/poll/config/locales/server.ar.yml b/plugins/poll/config/locales/server.ar.yml index b7e922904e..ba4b95b947 100644 --- a/plugins/poll/config/locales/server.ar.yml +++ b/plugins/poll/config/locales/server.ar.yml @@ -7,6 +7,7 @@ ar: site_settings: + poll_enabled: "السماح بالإستفتاء" poll_maximum_options: "أقصى عدد للخيارات في كلّ تصويت." poll_edit_window_mins: "عدد الدقائق بعد نشر المنشور المسموح فيها بتعديل التصويت." poll: From 81dc36ba16e5a0f8a9dfe22c9b4b243b0fa7cebe Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 14 Sep 2018 11:43:35 -0400 Subject: [PATCH 59/59] Version bump to v2.2.0.beta2 --- lib/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/version.rb b/lib/version.rb index b68559aa66..56dd01add1 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 2 MINOR = 2 TINY = 0 - PRE = 'beta1' + PRE = 'beta2' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end