diff --git a/packages/driver/package.json b/packages/driver/package.json index 07e0c7e20985..d3566e07a640 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -63,6 +63,7 @@ "url-parse": "1.4.7", "vanilla-text-mask": "5.1.1", "wait-on": "3.2.0", + "@cypress/what-is-circular": "1.0.0", "zone.js": "0.9.0" } } diff --git a/packages/driver/src/cy/commands/navigation.coffee b/packages/driver/src/cy/commands/navigation.coffee index 3658b8453f89..c300df70e1a8 100644 --- a/packages/driver/src/cy/commands/navigation.coffee +++ b/packages/driver/src/cy/commands/navigation.coffee @@ -1,4 +1,5 @@ _ = require("lodash") +whatIsCircular = require("@cypress/what-is-circular") moment = require("moment") UrlParse = require("url-parse") Promise = require("bluebird") @@ -522,6 +523,9 @@ module.exports = (Commands, Cypress, cy, state, config) -> if not _.isObject(options.headers) $utils.throwErrByPath("visit.invalid_headers") + if _.isObject(options.body) and path = whatIsCircular(options.body) + $utils.throwErrByPath("visit.body_circular", { args: { path }}) + if options.log message = url diff --git a/packages/driver/src/cy/commands/request.coffee b/packages/driver/src/cy/commands/request.coffee index ea2db52d7db1..f7e95bfc8cd8 100644 --- a/packages/driver/src/cy/commands/request.coffee +++ b/packages/driver/src/cy/commands/request.coffee @@ -1,4 +1,5 @@ _ = require("lodash") +whatIsCircular = require("@cypress/what-is-circular") Promise = require("bluebird") $utils = require("../../cypress/utils") @@ -137,6 +138,9 @@ module.exports = (Commands, Cypress, cy, state, config) -> if needsFormSpecified(options) options.form = true + if _.isObject(options.body) and path = whatIsCircular(options.body) + $utils.throwErrByPath("request.body_circular", { args: { path }}) + ## only set json to true if form isnt true ## and we have a valid object for body if options.form isnt true and isValidJsonObj(options.body) diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index 16d26dea288c..be3165755d7a 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -593,6 +593,11 @@ module.exports = { invalid_arguments: "#{cmd('reload')} can only accept a boolean or options as its arguments." request: + body_circular: ({ path }) -> """ + The `body` parameter supplied to #{cmd('request')} contained a circular reference at the path "#{path.join(".")}". + + `body` can only be a string or an object with no circular references. + """ status_code_flags_invalid: """ #{cmd('request')} was invoked with { failOnStatusCode: false, retryOnStatusCodeFailure: true }. @@ -931,6 +936,11 @@ module.exports = { missing_preset: "#{cmd('viewport')} could not find a preset for: '{{preset}}'. Available presets are: {{presets}}" visit: + body_circular: ({ path }) -> """ + The `body` parameter supplied to #{cmd('visit')} contained a circular reference at the path "#{path.join(".")}". + + `body` can only be a string or an object with no circular references. + """ status_code_flags_invalid: """ #{cmd('visit')} was invoked with { failOnStatusCode: false, retryOnStatusCodeFailure: true }. diff --git a/packages/driver/test/cypress/integration/commands/navigation_spec.coffee b/packages/driver/test/cypress/integration/commands/navigation_spec.coffee index f1c9d0a08048..833b3728114d 100644 --- a/packages/driver/test/cypress/integration/commands/navigation_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/navigation_spec.coffee @@ -1455,6 +1455,34 @@ describe "src/cy/commands/navigation", -> cy.visit("https://google.com/foo") + it "displays body_circular when body is circular", (done) -> + foo = { + bar: { + baz: {} + } + } + + foo.bar.baz.quux = foo + + cy.visit({ + method: "POST" + url: "http://foo.invalid/" + body: foo + }) + + cy.on "fail", (err) => + lastLog = @lastLog + expect(@logs.length).to.eq(1) + expect(lastLog.get("error")).to.eq(err) + expect(lastLog.get("state")).to.eq("failed") + expect(err.message).to.eq """ + The `body` parameter supplied to cy.visit() contained a circular reference at the path "bar.baz.quux". + + `body` can only be a string or an object with no circular references. + """ + + done() + context "#page load", -> it "sets initial=true and then removes", -> Cookie.remove("__cypress.initial") diff --git a/packages/driver/test/cypress/integration/commands/request_spec.coffee b/packages/driver/test/cypress/integration/commands/request_spec.coffee index bcb79f7f51d7..c4aeecfbd029 100644 --- a/packages/driver/test/cypress/integration/commands/request_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/request_spec.coffee @@ -751,6 +751,34 @@ describe "src/cy/commands/request", -> } }) + it "displays body_circular when body is circular", (done) -> + foo = { + bar: { + baz: {} + } + } + + foo.bar.baz.quux = foo + + cy.request({ + method: "POST" + url: "http://foo.invalid/" + body: foo + }) + + cy.on "fail", (err) => + lastLog = @lastLog + expect(@logs.length).to.eq(1) + expect(lastLog.get("error")).to.eq(err) + expect(lastLog.get("state")).to.eq("failed") + expect(err.message).to.eq """ + The `body` parameter supplied to cy.request() contained a circular reference at the path "bar.baz.quux". + + `body` can only be a string or an object with no circular references. + """ + + done() + it "does not include redirects when there were no redirects", (done) -> backend = Cypress.backend .withArgs("http:request") diff --git a/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee b/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee index e69ef13af196..183d16971dba 100644 --- a/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee +++ b/packages/driver/test/cypress/integration/cypress/cypress_spec.coffee @@ -47,6 +47,20 @@ describe "driver/src/cypress/index", -> done() + ## https://github.com/cypress-io/cypress/issues/4346 + it "can complete if a circular reference is sent", -> + foo = { + bar: {} + } + + foo.bar.baz = foo + + Cypress.backend("foo", foo) + .then -> + throw new Error("should not reach") + .catch (e) -> + expect(e.message).to.eq("You requested a backend event we cannot handle: foo") + context ".isCy", -> it "returns true on cy, cy chainable", -> expect(Cypress.isCy(cy)).to.be.true @@ -75,4 +89,4 @@ describe "driver/src/cypress/index", -> fn = -> Cypress.log({ message: 'My Log' }) - expect(fn).to.not.throw() \ No newline at end of file + expect(fn).to.not.throw() diff --git a/packages/extension/app/background.coffee b/packages/extension/app/background.coffee index 296bb6972c03..026c8ba1a16d 100644 --- a/packages/extension/app/background.coffee +++ b/packages/extension/app/background.coffee @@ -2,6 +2,7 @@ map = require("lodash/map") pick = require("lodash/pick") once = require("lodash/once") Promise = require("bluebird") +{ client, circularParser } = require("@packages/socket") HOST = "CHANGE_ME_HOST" PATH = "CHANGE_ME_PATH" @@ -12,19 +13,14 @@ firstOrNull = (cookies) -> ## normalize into null when empty array cookies[0] ? null -connect = (host, path, io) -> - io ?= global.io - - ## bail if io isnt defined - return if not io - +connect = (host, path) -> listenToCookieChanges = once -> chrome.cookies.onChanged.addListener (info) -> if info.cause isnt "overwrite" - client.emit("automation:push:request", "change:cookie", info) + ws.emit("automation:push:request", "change:cookie", info) fail = (id, err) -> - client.emit("automation:response", id, { + ws.emit("automation:response", id, { __error: err.message __stack: err.stack __name: err.name @@ -32,18 +28,20 @@ connect = (host, path, io) -> invoke = (method, id, args...) -> respond = (data) -> - client.emit("automation:response", id, {response: data}) + ws.emit("automation:response", id, {response: data}) Promise.try -> automation[method].apply(automation, args.concat(respond)) .catch (err) -> fail(id, err) - ## cannot use required socket here due - ## to bug in socket io client with browserify - client = io.connect(host, {path: path, transports: ["websocket"]}) + ws = client.connect(host, { + path: path, + transports: ["websocket"] + parser: circularParser + }) - client.on "automation:request", (id, msg, data) -> + ws.on "automation:request", (id, msg, data) -> switch msg when "get:cookies" invoke("getCookies", id, data) @@ -64,18 +62,18 @@ connect = (host, path, io) -> else fail(id, {message: "No handler registered for: '#{msg}'"}) - client.on "connect", -> + ws.on "connect", -> listenToCookieChanges() - client.emit("automation:client:connected") + ws.emit("automation:client:connected") - return client + return ws ## initially connect -connect(HOST, PATH, global.io) +connect(HOST, PATH) automation = { - connect: connect + connect getUrl: (cookie = {}) -> prefix = if cookie.secure then "https://" else "http://" diff --git a/packages/extension/app/manifest.json b/packages/extension/app/manifest.json index 4248dddbc603..98c2a503dd2e 100644 --- a/packages/extension/app/manifest.json +++ b/packages/extension/app/manifest.json @@ -24,7 +24,6 @@ }, "background": { "scripts": [ - "socket.io.js", "background.js" ] }, diff --git a/packages/extension/gulpfile.coffee b/packages/extension/gulpfile.coffee index d417e11841c2..91fd9b08ad99 100644 --- a/packages/extension/gulpfile.coffee +++ b/packages/extension/gulpfile.coffee @@ -9,11 +9,6 @@ Promise = require("bluebird") coffeeify = require("coffeeify") browserify = require("browserify") icons = require("@cypress/icons") -ext = require("./") - -gulp.task "copy:socket:client", -> - gulp.src(require("../socket").getPathToClientSource()) - .pipe(gulp.dest("dist")) gulp.task "clean", -> gulp.src("dist") @@ -71,7 +66,6 @@ gulp.task "watch", ["build"], -> gulp.task "build", -> runSeq "clean", [ - "copy:socket:client" "icons" "logos" "manifest" diff --git a/packages/extension/test/integration/background_spec.coffee b/packages/extension/test/integration/background_spec.coffee index c7f4d3c44444..0a43b0ea25b2 100644 --- a/packages/extension/test/integration/background_spec.coffee +++ b/packages/extension/test/integration/background_spec.coffee @@ -94,10 +94,6 @@ tab3 = { } describe "app/background", -> - before -> - @io = global.io - global.io = socket.client - beforeEach (done) -> @httpSrv = http.createServer() @server = socket.server(@httpSrv, {path: "/__socket.io"}) @@ -107,9 +103,6 @@ describe "app/background", -> @server.close() @httpSrv.close -> done() - after -> - global.io = @io - context ".connect", -> it "can connect", (done) -> @server.on "connection", -> done() diff --git a/packages/extension/test/spec_helper.coffee b/packages/extension/test/spec_helper.coffee index 5a4a28bde0dd..7be29a687d06 100644 --- a/packages/extension/test/spec_helper.coffee +++ b/packages/extension/test/spec_helper.coffee @@ -5,13 +5,9 @@ sinonChai = require("sinon-chai") chai.use(sinonChai) global.expect = chai.expect -global.io = { - connect: -> - return {on: ->} -} beforeEach -> @sandbox = sinon.sandbox.create() afterEach -> - @sandbox.restore() \ No newline at end of file + @sandbox.restore() diff --git a/packages/network/test/support/servers.ts b/packages/network/test/support/servers.ts index de12fdba407d..340cdc607c00 100644 --- a/packages/network/test/support/servers.ts +++ b/packages/network/test/support/servers.ts @@ -14,13 +14,16 @@ export interface AsyncServer { function addDestroy(server: http.Server | https.Server) { let connections = [] - server.on('connection', function(conn) { + function trackConn(conn) { connections.push(conn) conn.on('close', () => { connections = connections.filter(connection => connection !== conn) }) - }) + } + + server.on('connection', trackConn) + server.on('secureConnection', trackConn) // @ts-ignore Property 'destroy' does not exist on type 'Server'. server.destroy = function(cb) { diff --git a/packages/network/test/unit/agent_spec.ts b/packages/network/test/unit/agent_spec.ts index 687c53d1f398..99dd08cf92e5 100644 --- a/packages/network/test/unit/agent_spec.ts +++ b/packages/network/test/unit/agent_spec.ts @@ -1,6 +1,5 @@ import Bluebird from 'bluebird' import chai from 'chai' -import { EventEmitter } from 'events' import http from 'http' import https from 'https' import net from 'net' @@ -175,7 +174,8 @@ describe('lib/agent', function() { return new Bluebird((resolve) => { Io.client(`http://localhost:${HTTP_PORT}`, { agent: this.agent, - transports: ['websocket'] + transports: ['websocket'], + rejectUnauthorized: false }).on('message', resolve) }) .then(msg => { @@ -191,7 +191,8 @@ describe('lib/agent', function() { return new Bluebird((resolve) => { Io.client(`https://localhost:${HTTPS_PORT}`, { agent: this.agent, - transports: ['websocket'] + transports: ['websocket'], + rejectUnauthorized: false }).on('message', resolve) }) .then(msg => { @@ -357,7 +358,8 @@ describe('lib/agent', function() { Io.client(`${testCase.protocol}://foo.bar.baz.invalid`, { agent: testCase.agent, transports: ['websocket'], - timeout: 1 + timeout: 1, + rejectUnauthorized: false }) .on('message', reject) .on('connect_error', resolve) diff --git a/packages/runner/lib/test-setup.js b/packages/runner/lib/test-setup.js index b47aa416a4cf..c13891df3f64 100644 --- a/packages/runner/lib/test-setup.js +++ b/packages/runner/lib/test-setup.js @@ -2,7 +2,7 @@ const chai = require('chai') const JSDOM = require('jsdom').JSDOM const sinonChai = require('sinon-chai') const $Cypress = require('@packages/driver') -const io = require('@packages/socket') +const { client } = require('@packages/socket') // http://airbnb.io/enzyme/docs/guides/jsdom.html const jsdom = new JSDOM('') @@ -52,6 +52,6 @@ class Runner { global.Mocha = { Runnable, Runner } $Cypress.create = () => {} -io.connect = () => { +client.connect = () => { return { emit: () => {}, on: () => {} } } diff --git a/packages/runner/src/lib/event-manager.js b/packages/runner/src/lib/event-manager.js index d449e67c037b..796094697a8d 100644 --- a/packages/runner/src/lib/event-manager.js +++ b/packages/runner/src/lib/event-manager.js @@ -2,20 +2,21 @@ import _ from 'lodash' import { EventEmitter } from 'events' import Promise from 'bluebird' import { action } from 'mobx' -import io from '@packages/socket' +import { client, circularParser } from '@packages/socket' import automation from './automation' import logger from './logger' import $Cypress, { $ } from '@packages/driver' -const channel = io.connect({ +const ws = client.connect({ path: '/__socket.io', transports: ['websocket'], + parser: circularParser, }) -channel.on('connect', () => { - channel.emit('runner:connected') +ws.on('connect', () => { + ws.emit('runner:connected') }) const driverToReporterEvents = 'paused'.split(' ') @@ -42,18 +43,18 @@ const eventManager = { return this._reRun(state) } - channel.emit('is:automation:client:connected', connectionInfo, action('automationEnsured', (isConnected) => { + ws.emit('is:automation:client:connected', connectionInfo, action('automationEnsured', (isConnected) => { state.automation = isConnected ? automation.CONNECTED : automation.MISSING - channel.on('automation:disconnected', action('automationDisconnected', () => { + ws.on('automation:disconnected', action('automationDisconnected', () => { state.automation = automation.DISCONNECTED })) })) - channel.on('change:to:url', (url) => { + ws.on('change:to:url', (url) => { window.location.href = url }) - channel.on('automation:push:message', (msg, data = {}) => { + ws.on('automation:push:message', (msg, data = {}) => { if (!Cypress) return switch (msg) { @@ -66,7 +67,7 @@ const eventManager = { }) _.each(socketRerunEvents, (event) => { - channel.on(event, rerun) + ws.on(event, rerun) }) reporterBus.on('runner:console:error', (testId) => { @@ -140,7 +141,7 @@ const eventManager = { }) reporterBus.on('external:open', (url) => { - channel.emit('external:open', url) + ws.emit('external:open', url) }) const $window = $(window) @@ -170,7 +171,7 @@ const eventManager = { start (config) { if (config.socketId) { - channel.emit('app:connect', config.socketId) + ws.emit('app:connect', config.socketId) } }, @@ -182,7 +183,7 @@ const eventManager = { this._addListeners() - channel.emit('watch:test:file', specPath) + ws.emit('watch:test:file', specPath) }, initialize ($autIframe, config) { @@ -190,7 +191,7 @@ const eventManager = { // get the current runnable in case we reran mid-test due to a visit // to a new domain - channel.emit('get:existing:run:state', (state = {}) => { + ws.emit('get:existing:run:state', (state = {}) => { const runnables = Cypress.normalizeAll(state.tests) const run = () => { this._runDriver(state) @@ -214,7 +215,7 @@ const eventManager = { } if (config.isTextTerminal && !state.currentId) { - channel.emit('set:runnables', runnables, run) + ws.emit('set:runnables', runnables, run) } else { run() } @@ -223,12 +224,12 @@ const eventManager = { _addListeners () { Cypress.on('message', (msg, data, cb) => { - channel.emit('client:request', msg, data, cb) + ws.emit('client:request', msg, data, cb) }) _.each(driverToSocketEvents, (event) => { Cypress.on(event, (...args) => { - return channel.emit(event, ...args) + return ws.emit(event, ...args) }) }) @@ -315,7 +316,7 @@ const eventManager = { stop () { localBus.removeAllListeners() - channel.off() + ws.off() }, _reRun (state) { @@ -355,11 +356,11 @@ const eventManager = { }, notifyRunningSpec (specFile) { - channel.emit('spec:changed', specFile) + ws.emit('spec:changed', specFile) }, focusTests () { - channel.emit('focus:tests') + ws.emit('focus:tests') }, snapshotUnpinned () { @@ -377,7 +378,7 @@ const eventManager = { }, launchBrowser (browser) { - channel.emit('reload:browser', window.location.toString(), browser && browser.name) + ws.emit('reload:browser', window.location.toString(), browser && browser.name) }, // clear all the cypress specific cookies @@ -396,7 +397,7 @@ const eventManager = { }, saveState (state) { - channel.emit('save:app:state', state) + ws.emit('save:app:state', state) }, } diff --git a/packages/server/lib/socket.coffee b/packages/server/lib/socket.coffee index ee662ade0b78..af47a7df876d 100644 --- a/packages/server/lib/socket.coffee +++ b/packages/server/lib/socket.coffee @@ -124,6 +124,7 @@ class Socket destroyUpgrade: false serveClient: false cookie: cookie + parser: socketIo.circularParser }) startListening: (server, automation, config, options) -> diff --git a/packages/server/test/integration/websockets_spec.coffee b/packages/server/test/integration/websockets_spec.coffee index 7668c5df4dcf..a818f5983001 100644 --- a/packages/server/test/integration/websockets_spec.coffee +++ b/packages/server/test/integration/websockets_spec.coffee @@ -175,6 +175,7 @@ describe "Web Sockets", -> @wsClient = socketIo.client(@cfg.proxyUrl, { path: @cfg.socketIoRoute transports: ["websocket"] + parser: socketIo.circularParser }) @wsClient.on "connect", -> done() @@ -197,6 +198,7 @@ describe "Web Sockets", -> agent: agent path: @cfg.socketIoRoute transports: ["websocket"] + parser: socketIo.circularParser }) @wsClient.on "connect", -> done() @@ -220,6 +222,8 @@ describe "Web Sockets", -> @wsClient = socketIo.client("https://localhost:#{wssPort}", { agent: agent path: @cfg.socketIoRoute + parser: socketIo.circularParser + rejectUnauthorized: false }) @wsClient.on "connect", -> done() diff --git a/packages/server/test/unit/socket_spec.coffee b/packages/server/test/unit/socket_spec.coffee index cc07d6f713d6..fd1ebfebae83 100644 --- a/packages/server/test/unit/socket_spec.coffee +++ b/packages/server/test/unit/socket_spec.coffee @@ -64,12 +64,29 @@ describe "lib/socket", -> agent: agent path: socketIoRoute transports: ["websocket"] + parser: socketIo.circularParser }) return afterEach -> @client.disconnect() + ## https://github.com/cypress-io/cypress/issues/4346 + it "can emit a circular object without crashing", (done) -> + foo = { + bar: {} + } + + foo.bar.baz = foo + + ## going to stub exec here just so we have something that we can + ## control the resolved value of + sinon.stub(exec, 'run').resolves(foo) + + @client.emit "backend:request", "exec", "quuz", (res) -> + expect(res.response).to.deep.eq(foo) + done() + context "on(automation:request)", -> describe "#onAutomation", -> before -> diff --git a/packages/socket/index.js b/packages/socket/index.js index 77ca6ef899ed..c482fe1cee9d 100644 --- a/packages/socket/index.js +++ b/packages/socket/index.js @@ -1 +1 @@ -module.exports = require("./lib/socket") \ No newline at end of file +module.exports = require('./lib/socket') diff --git a/packages/socket/lib/browser.js b/packages/socket/lib/browser.js new file mode 100644 index 000000000000..36c1f83138bd --- /dev/null +++ b/packages/socket/lib/browser.js @@ -0,0 +1,8 @@ +const client = require('socket.io-client') +const circularParser = require('socket.io-circular-parser') + +module.exports = { + client, + + circularParser, +} diff --git a/packages/socket/lib/client.js b/packages/socket/lib/client.js deleted file mode 100644 index e13ac9d1b45d..000000000000 --- a/packages/socket/lib/client.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('socket.io-client') diff --git a/packages/socket/lib/socket.js b/packages/socket/lib/socket.js index 7eff210b2d72..3b90991616fe 100644 --- a/packages/socket/lib/socket.js +++ b/packages/socket/lib/socket.js @@ -1,15 +1,18 @@ const fs = require('fs') const path = require('path') const server = require('socket.io') -const version = require('socket.io-client/package.json').version +const { version } = require('socket.io-client/package.json') +const { client, circularParser } = require('./browser') + const clientPath = require.resolve('socket.io-client') -const client = require('./client') module.exports = { server, client, + circularParser, + getPathToClientSource () { // clientPath returns the path to socket.io-client/lib/index.js // so walk up two levels to get to the root diff --git a/packages/socket/package.json b/packages/socket/package.json index a74615514b86..f45473ff91f0 100644 --- a/packages/socket/package.json +++ b/packages/socket/package.json @@ -3,8 +3,9 @@ "version": "0.0.0", "private": true, "main": "index.js", - "browser": "./lib/client.js", + "browser": "./lib/browser.js", "scripts": { + "postinstall": "patch-package", "pretest": "npm run check-deps-pre", "test": "cross-env NODE_ENV=test bin-up mocha --reporter mocha-multi-reporters --reporter-options configFile=../../mocha-reporter-config.json", "pretest-watch": "npm run check-deps-pre", @@ -17,12 +18,14 @@ "lib" ], "dependencies": { - "socket.io": "1.7.4", - "socket.io-client": "1.7.4" + "socket.io": "2.2.0", + "socket.io-circular-parser": "cypress-io/socket.io-circular-parser#c5a895a0fef6d19d457a38a515dbb5c18feeb591", + "socket.io-client": "2.2.0" }, "devDependencies": { "bin-up": "1.2.0", "chai": "3.5.0", - "cross-env": "5.2.0" + "cross-env": "5.2.0", + "patch-package": "6.1.2" } } diff --git a/packages/socket/patches/has-binary2+1.0.3.patch b/packages/socket/patches/has-binary2+1.0.3.patch new file mode 100644 index 000000000000..1da07225c972 --- /dev/null +++ b/packages/socket/patches/has-binary2+1.0.3.patch @@ -0,0 +1,94 @@ +diff --git a/node_modules/has-binary2/index.js b/node_modules/has-binary2/index.js +index cf756a3..277ed03 100644 +--- a/node_modules/has-binary2/index.js ++++ b/node_modules/has-binary2/index.js +@@ -4,19 +4,19 @@ + * Module requirements. + */ + +-var isArray = require('isarray'); ++let isArray = require('isarray') + +-var toString = Object.prototype.toString; +-var withNativeBlob = typeof Blob === 'function' || +- typeof Blob !== 'undefined' && toString.call(Blob) === '[object BlobConstructor]'; +-var withNativeFile = typeof File === 'function' || +- typeof File !== 'undefined' && toString.call(File) === '[object FileConstructor]'; ++let toString = Object.prototype.toString ++let withNativeBlob = typeof global.Blob === 'function' || toString.call(global.Blob) === '[object BlobConstructor]' ++let withNativeFile = typeof global.File === 'function' || toString.call(global.File) === '[object FileConstructor]' + + /** + * Module exports. + */ + +-module.exports = hasBinary; ++module.exports = function hasBinaryCircular (obj) { ++ return hasBinary(obj, []) ++} + + /** + * Checks for binary data. +@@ -27,38 +27,45 @@ module.exports = hasBinary; + * @api public + */ + +-function hasBinary (obj) { ++function hasBinary (obj, known) { + if (!obj || typeof obj !== 'object') { +- return false; ++ return false ++ } ++ ++ if (known.indexOf(obj) >= 0) { ++ return false + } + ++ known.push(obj) ++ + if (isArray(obj)) { +- for (var i = 0, l = obj.length; i < l; i++) { +- if (hasBinary(obj[i])) { +- return true; ++ for (let i = 0, l = obj.length; i < l; i++) { ++ if (hasBinary(obj[i], known)) { ++ return true + } + } +- return false; ++ ++ return false + } + +- if ((typeof Buffer === 'function' && Buffer.isBuffer && Buffer.isBuffer(obj)) || +- (typeof ArrayBuffer === 'function' && obj instanceof ArrayBuffer) || +- (withNativeBlob && obj instanceof Blob) || +- (withNativeFile && obj instanceof File) ++ if ((typeof global.Buffer === 'function' && global.Buffer.isBuffer && global.Buffer.isBuffer(obj)) || ++ (typeof global.ArrayBuffer === 'function' && obj instanceof ArrayBuffer) || ++ (withNativeBlob && obj instanceof Blob) || ++ (withNativeFile && obj instanceof File) + ) { +- return true; ++ return true + } + + // see: https://github.com/Automattic/has-binary/pull/4 +- if (obj.toJSON && typeof obj.toJSON === 'function' && arguments.length === 1) { +- return hasBinary(obj.toJSON(), true); ++ if (obj.toJSON && typeof obj.toJSON === 'function') { ++ return hasBinary(obj.toJSON(), known) + } + +- for (var key in obj) { +- if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) { +- return true; ++ for (let key in obj) { ++ if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key], known)) { ++ return true + } + } + +- return false; ++ return false + }