MailPoet Newsletters (New) - Version 3.0.0-beta.10

Version Description

  • 2016-12-27 =
  • Improved: newsletter is saved prior to sending an email preview;
  • Improved: subscription management page conditionally displays the "bounced" status;
  • Improved: deleted lists are displayed in newsletter listings;
  • Fixed: newsletter/subscriber/list/form dates are properly formatted according to WP settings;
  • Fixed: emails' "Return-path" header is set to the bounce address configured in Settings->Advanced;
  • Fixed: archived newsletters' shortcode works for site visitors;
  • Fixed: unicode support for newsletters.
Download this release

Release Info

Developer wysija
Plugin Icon 128x128 MailPoet Newsletters (New)
Version 3.0.0-beta.10
Comparing to
See all releases

Code changes from version 3.0.0-beta.9 to 3.0.0-beta.10

assets/css/admin.css CHANGED
@@ -1799,8 +1799,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
1799
  position: relative;
1800
  overflow: visible;
1801
  -webkit-tap-highlight-color: transparent;
1802
- -webkit-font-variant-ligatures: none;
1803
- font-variant-ligatures: none;
1804
  }
1805
  .CodeMirror-wrap pre {
1806
  word-wrap: break-word;
1799
  position: relative;
1800
  overflow: visible;
1801
  -webkit-tap-highlight-color: transparent;
1802
+ -webkit-font-variant-ligatures: contextual;
1803
+ font-variant-ligatures: contextual;
1804
  }
1805
  .CodeMirror-wrap pre {
1806
  word-wrap: break-word;
assets/js/admin.js CHANGED
@@ -839,12 +839,18 @@ webpackJsonp([0],[
839
  * will remain to ensure logic does not differ in production.
840
  */
841
 
842
- function invariant(condition, format, a, b, c, d, e, f) {
843
- if (process.env.NODE_ENV !== 'production') {
 
 
844
  if (format === undefined) {
845
  throw new Error('invariant requires an error message argument');
846
  }
847
- }
 
 
 
 
848
 
849
  if (!condition) {
850
  var error;
839
  * will remain to ensure logic does not differ in production.
840
  */
841
 
842
+ var validateFormat = function validateFormat(format) {};
843
+
844
+ if (process.env.NODE_ENV !== 'production') {
845
+ validateFormat = function validateFormat(format) {
846
  if (format === undefined) {
847
  throw new Error('invariant requires an error message argument');
848
  }
849
+ };
850
+ }
851
+
852
+ function invariant(condition, format, a, b, c, d, e, f) {
853
+ validateFormat(format);
854
 
855
  if (!condition) {
856
  var error;
assets/js/form_editor.js CHANGED
@@ -2077,12 +2077,12 @@ webpackJsonp([1],{
2077
  var bidiOrdering = (function() {
2078
  // Character types for codepoints 0 to 0xff
2079
  var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"
2080
- // Character types for codepoints 0x600 to 0x6ff
2081
- var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"
2082
  function charType(code) {
2083
  if (code <= 0xf7) { return lowTypes.charAt(code) }
2084
  else if (0x590 <= code && code <= 0x5f4) { return "R" }
2085
- else if (0x600 <= code && code <= 0x6ed) { return arabicTypes.charAt(code - 0x600) }
2086
  else if (0x6ee <= code && code <= 0x8ac) { return "r" }
2087
  else if (0x2000 <= code && code <= 0x200b) { return "w" }
2088
  else if (code == 0x200c) { return "b" }
@@ -4388,7 +4388,7 @@ webpackJsonp([1],{
4388
  }
4389
  }
4390
 
4391
- function NativeScrollbars(place, scroll, cm) {
4392
  this.cm = cm
4393
  var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar")
4394
  var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar")
@@ -4404,91 +4404,92 @@ webpackJsonp([1],{
4404
  this.checkedZeroWidth = false
4405
  // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
4406
  if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = "18px" }
4407
- }
4408
-
4409
- NativeScrollbars.prototype = copyObj({
4410
- update: function(measure) {
4411
- var needsH = measure.scrollWidth > measure.clientWidth + 1
4412
- var needsV = measure.scrollHeight > measure.clientHeight + 1
4413
- var sWidth = measure.nativeBarWidth
4414
-
4415
- if (needsV) {
4416
- this.vert.style.display = "block"
4417
- this.vert.style.bottom = needsH ? sWidth + "px" : "0"
4418
- var totalHeight = measure.viewHeight - (needsH ? sWidth : 0)
4419
- // A bug in IE8 can cause this value to be negative, so guard it.
4420
- this.vert.firstChild.style.height =
4421
- Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"
4422
- } else {
4423
- this.vert.style.display = ""
4424
- this.vert.firstChild.style.height = "0"
4425
- }
4426
-
4427
- if (needsH) {
4428
- this.horiz.style.display = "block"
4429
- this.horiz.style.right = needsV ? sWidth + "px" : "0"
4430
- this.horiz.style.left = measure.barLeft + "px"
4431
- var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0)
4432
- this.horiz.firstChild.style.width =
4433
- (measure.scrollWidth - measure.clientWidth + totalWidth) + "px"
4434
- } else {
4435
- this.horiz.style.display = ""
4436
- this.horiz.firstChild.style.width = "0"
4437
- }
4438
-
4439
- if (!this.checkedZeroWidth && measure.clientHeight > 0) {
4440
- if (sWidth == 0) { this.zeroWidthHack() }
4441
- this.checkedZeroWidth = true
4442
- }
4443
-
4444
- return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}
4445
- },
4446
- setScrollLeft: function(pos) {
4447
- if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos }
4448
- if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz) }
4449
- },
4450
- setScrollTop: function(pos) {
4451
- if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos }
4452
- if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert) }
4453
- },
4454
- zeroWidthHack: function() {
4455
- var w = mac && !mac_geMountainLion ? "12px" : "18px"
4456
- this.horiz.style.height = this.vert.style.width = w
4457
- this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"
4458
- this.disableHoriz = new Delayed
4459
- this.disableVert = new Delayed
4460
- },
4461
- enableZeroWidthBar: function(bar, delay) {
4462
- bar.style.pointerEvents = "auto"
4463
- function maybeDisable() {
4464
- // To find out whether the scrollbar is still visible, we
4465
- // check whether the element under the pixel in the bottom
4466
- // left corner of the scrollbar box is the scrollbar box
4467
- // itself (when the bar is still visible) or its filler child
4468
- // (when the bar is hidden). If it is still visible, we keep
4469
- // it enabled, if it's hidden, we disable pointer events.
4470
- var box = bar.getBoundingClientRect()
4471
- var elt = document.elementFromPoint(box.left + 1, box.bottom - 1)
4472
- if (elt != bar) { bar.style.pointerEvents = "none" }
4473
- else { delay.set(1000, maybeDisable) }
4474
- }
4475
- delay.set(1000, maybeDisable)
4476
- },
4477
- clear: function() {
4478
- var parent = this.horiz.parentNode
4479
- parent.removeChild(this.horiz)
4480
- parent.removeChild(this.vert)
4481
  }
4482
- }, NativeScrollbars.prototype)
4483
-
4484
- function NullScrollbars() {}
4485
 
4486
- NullScrollbars.prototype = copyObj({
4487
- update: function() { return {bottom: 0, right: 0} },
4488
- setScrollLeft: function() {},
4489
- setScrollTop: function() {},
4490
- clear: function() {}
4491
- }, NullScrollbars.prototype)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4492
 
4493
  function updateScrollbars(cm, measure) {
4494
  if (!measure) { measure = measureForScrollbars(cm) }
@@ -5067,7 +5068,7 @@ webpackJsonp([1],{
5067
 
5068
  // DISPLAY DRAWING
5069
 
5070
- function DisplayUpdate(cm, viewport, force) {
5071
  var display = cm.display
5072
 
5073
  this.viewport = viewport
@@ -5080,18 +5081,18 @@ webpackJsonp([1],{
5080
  this.force = force
5081
  this.dims = getDimensions(cm)
5082
  this.events = []
5083
- }
5084
 
5085
- DisplayUpdate.prototype.signal = function(emitter, type) {
5086
  if (hasHandler(emitter, type))
5087
  { this.events.push(arguments) }
5088
- }
5089
- DisplayUpdate.prototype.finish = function() {
5090
- var this$1 = this;
5091
 
5092
  for (var i = 0; i < this.events.length; i++)
5093
  { signal.apply(null, this$1.events[i]) }
5094
- }
5095
 
5096
  function maybeClipScrollbars(cm) {
5097
  var display = cm.display
@@ -8219,7 +8220,7 @@ webpackJsonp([1],{
8219
  for (var i = newBreaks.length - 1; i >= 0; i--)
8220
  { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)) }
8221
  })
8222
- option("specialChars", /[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function (cm, val, old) {
8223
  cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g")
8224
  if (old != Init) { cm.refresh() }
8225
  })
@@ -8309,7 +8310,7 @@ webpackJsonp([1],{
8309
  function guttersChanged(cm) {
8310
  updateGutters(cm)
8311
  regChange(cm)
8312
- setTimeout(function () { return alignHorizontally(cm); }, 20)
8313
  }
8314
 
8315
  function dragDropChanged(cm, value, old) {
@@ -8364,7 +8365,6 @@ webpackJsonp([1],{
8364
  themeChanged(this)
8365
  if (options.lineWrapping)
8366
  { this.display.wrapper.className += " CodeMirror-wrap" }
8367
- if (options.autofocus && !mobile) { display.input.focus() }
8368
  initScrollbars(this)
8369
 
8370
  this.state = {
@@ -8383,6 +8383,8 @@ webpackJsonp([1],{
8383
  specialChars: null
8384
  }
8385
 
 
 
8386
  // Override magic textarea content restore that IE sometimes does
8387
  // on our hidden textarea on reload
8388
  if (ie && ie_version < 11) { setTimeout(function () { return this$1.display.input.reset(true); }, 20) }
@@ -8738,6 +8740,7 @@ webpackJsonp([1],{
8738
  options[option] = value
8739
  if (optionHandlers.hasOwnProperty(option))
8740
  { operation(this, optionHandlers[option])(this, value, old) }
 
8741
  },
8742
 
8743
  getOption: function(option) {return this.options[option]},
@@ -9245,331 +9248,333 @@ webpackJsonp([1],{
9245
 
9246
  // CONTENTEDITABLE INPUT STYLE
9247
 
9248
- function ContentEditableInput(cm) {
9249
  this.cm = cm
9250
  this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null
9251
  this.polling = new Delayed()
9252
  this.composing = null
9253
  this.gracePeriod = false
9254
  this.readDOMTimeout = null
9255
- }
9256
 
9257
- ContentEditableInput.prototype = copyObj({
9258
- init: function(display) {
9259
  var this$1 = this;
9260
 
9261
- var input = this, cm = input.cm
9262
- var div = input.div = display.lineDiv
9263
- disableBrowserMagic(div, cm.options.spellcheck)
9264
 
9265
- on(div, "paste", function (e) {
9266
- if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return }
9267
- // IE doesn't fire input events, so we schedule a read for the pasted content in this way
9268
- if (ie_version <= 11) { setTimeout(operation(cm, function () {
9269
- if (!input.pollContent()) { regChange(cm) }
9270
- }), 20) }
9271
- })
9272
 
9273
- on(div, "compositionstart", function (e) {
9274
- this$1.composing = {data: e.data}
9275
- })
9276
- on(div, "compositionupdate", function (e) {
9277
- if (!this$1.composing) { this$1.composing = {data: e.data} }
9278
- })
9279
- on(div, "compositionend", function (e) {
9280
- if (this$1.composing) {
9281
- if (e.data != this$1.composing.data) { this$1.readFromDOMSoon() }
9282
- this$1.composing = null
9283
- }
9284
- })
9285
 
9286
- on(div, "touchstart", function () { return input.forceCompositionEnd(); })
9287
 
9288
- on(div, "input", function () {
9289
- if (!this$1.composing) { this$1.readFromDOMSoon() }
9290
- })
9291
 
9292
- function onCopyCut(e) {
9293
- if (signalDOMEvent(cm, e)) { return }
9294
- if (cm.somethingSelected()) {
9295
- setLastCopied({lineWise: false, text: cm.getSelections()})
9296
- if (e.type == "cut") { cm.replaceSelection("", null, "cut") }
9297
- } else if (!cm.options.lineWiseCopyCut) {
9298
- return
9299
- } else {
9300
- var ranges = copyableRanges(cm)
9301
- setLastCopied({lineWise: true, text: ranges.text})
9302
- if (e.type == "cut") {
9303
- cm.operation(function () {
9304
- cm.setSelections(ranges.ranges, 0, sel_dontScroll)
9305
- cm.replaceSelection("", null, "cut")
9306
- })
9307
- }
9308
  }
9309
- if (e.clipboardData) {
9310
- e.clipboardData.clearData()
9311
- var content = lastCopied.text.join("\n")
9312
- // iOS exposes the clipboard API, but seems to discard content inserted into it
9313
- e.clipboardData.setData("Text", content)
9314
- if (e.clipboardData.getData("Text") == content) {
9315
- e.preventDefault()
9316
- return
9317
- }
9318
  }
9319
- // Old-fashioned briefly-focus-a-textarea hack
9320
- var kludge = hiddenTextarea(), te = kludge.firstChild
9321
- cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild)
9322
- te.value = lastCopied.text.join("\n")
9323
- var hadFocus = document.activeElement
9324
- selectInput(te)
9325
- setTimeout(function () {
9326
- cm.display.lineSpace.removeChild(kludge)
9327
- hadFocus.focus()
9328
- if (hadFocus == div) { input.showPrimarySelection() }
9329
- }, 50)
9330
  }
9331
- on(div, "copy", onCopyCut)
9332
- on(div, "cut", onCopyCut)
9333
- },
9334
-
9335
- prepareSelection: function() {
9336
- var result = prepareSelection(this.cm, false)
9337
- result.focus = this.cm.state.focused
9338
- return result
9339
- },
9340
-
9341
- showSelection: function(info, takeFocus) {
9342
- if (!info || !this.cm.display.view.length) { return }
9343
- if (info.focus || takeFocus) { this.showPrimarySelection() }
9344
- this.showMultipleSelections(info)
9345
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9346
 
9347
- showPrimarySelection: function() {
9348
- var sel = window.getSelection(), prim = this.cm.doc.sel.primary()
9349
- var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset)
9350
- var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset)
9351
- if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad &&
9352
- cmp(minPos(curAnchor, curFocus), prim.from()) == 0 &&
9353
- cmp(maxPos(curAnchor, curFocus), prim.to()) == 0)
9354
- { return }
9355
-
9356
- var start = posToDOM(this.cm, prim.from())
9357
- var end = posToDOM(this.cm, prim.to())
9358
- if (!start && !end) { return }
9359
-
9360
- var view = this.cm.display.view
9361
- var old = sel.rangeCount && sel.getRangeAt(0)
9362
- if (!start) {
9363
- start = {node: view[0].measure.map[2], offset: 0}
9364
- } else if (!end) { // FIXME dangerously hacky
9365
- var measure = view[view.length - 1].measure
9366
- var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map
9367
- end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}
9368
- }
9369
-
9370
- var rng
9371
- try { rng = range(start.node, start.offset, end.offset, end.node) }
9372
- catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible
9373
- if (rng) {
9374
- if (!gecko && this.cm.state.focused) {
9375
- sel.collapse(start.node, start.offset)
9376
- if (!rng.collapsed) {
9377
- sel.removeAllRanges()
9378
- sel.addRange(rng)
9379
- }
9380
- } else {
9381
  sel.removeAllRanges()
9382
  sel.addRange(rng)
9383
  }
9384
- if (old && sel.anchorNode == null) { sel.addRange(old) }
9385
- else if (gecko) { this.startGracePeriod() }
 
9386
  }
9387
- this.rememberSelection()
9388
- },
 
 
 
9389
 
9390
- startGracePeriod: function() {
9391
  var this$1 = this;
9392
 
9393
- clearTimeout(this.gracePeriod)
9394
- this.gracePeriod = setTimeout(function () {
9395
- this$1.gracePeriod = false
9396
- if (this$1.selectionChanged())
9397
- { this$1.cm.operation(function () { return this$1.cm.curOp.selectionChanged = true; }) }
9398
- }, 20)
9399
- },
9400
-
9401
- showMultipleSelections: function(info) {
9402
- removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors)
9403
- removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection)
9404
- },
9405
-
9406
- rememberSelection: function() {
9407
- var sel = window.getSelection()
9408
- this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset
9409
- this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset
9410
- },
9411
-
9412
- selectionInEditor: function() {
9413
- var sel = window.getSelection()
9414
- if (!sel.rangeCount) { return false }
9415
- var node = sel.getRangeAt(0).commonAncestorContainer
9416
- return contains(this.div, node)
9417
- },
9418
-
9419
- focus: function() {
9420
- if (this.cm.options.readOnly != "nocursor") {
9421
- if (!this.selectionInEditor())
9422
- { this.showSelection(this.prepareSelection(), true) }
9423
- this.div.focus()
9424
- }
9425
- },
9426
- blur: function() { this.div.blur() },
9427
- getField: function() { return this.div },
9428
-
9429
- supportsTouch: function() { return true },
9430
-
9431
- receivedFocus: function() {
9432
- var input = this
9433
- if (this.selectionInEditor())
9434
- { this.pollSelection() }
9435
- else
9436
- { runInOp(this.cm, function () { return input.cm.curOp.selectionChanged = true; }) }
9437
 
9438
- function poll() {
9439
- if (input.cm.state.focused) {
9440
- input.pollSelection()
9441
- input.polling.set(input.cm.options.pollInterval, poll)
9442
- }
9443
- }
9444
- this.polling.set(this.cm.options.pollInterval, poll)
9445
- },
9446
 
9447
- selectionChanged: function() {
9448
- var sel = window.getSelection()
9449
- return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
9450
- sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset
9451
- },
 
9452
 
9453
- pollSelection: function() {
9454
- if (!this.composing && this.readDOMTimeout == null && !this.gracePeriod && this.selectionChanged()) {
9455
- var sel = window.getSelection(), cm = this.cm
9456
- this.rememberSelection()
9457
- var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset)
9458
- var head = domToPos(cm, sel.focusNode, sel.focusOffset)
9459
- if (anchor && head) { runInOp(cm, function () {
9460
- setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll)
9461
- if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true }
9462
- }) }
9463
  }
9464
- },
 
 
9465
 
9466
- pollContent: function() {
9467
- if (this.readDOMTimeout != null) {
9468
- clearTimeout(this.readDOMTimeout)
9469
- this.readDOMTimeout = null
9470
- }
9471
 
9472
- var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary()
9473
- var from = sel.from(), to = sel.to()
9474
- if (from.ch == 0 && from.line > cm.firstLine())
9475
- { from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length) }
9476
- if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine())
9477
- { to = Pos(to.line + 1, 0) }
9478
- if (from.line < display.viewFrom || to.line > display.viewTo - 1) { return false }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9479
 
9480
- var fromIndex, fromLine, fromNode
9481
- if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) {
9482
- fromLine = lineNo(display.view[0].line)
9483
- fromNode = display.view[0].node
9484
- } else {
9485
- fromLine = lineNo(display.view[fromIndex].line)
9486
- fromNode = display.view[fromIndex - 1].node.nextSibling
9487
- }
9488
- var toIndex = findViewIndex(cm, to.line)
9489
- var toLine, toNode
9490
- if (toIndex == display.view.length - 1) {
9491
- toLine = display.viewTo - 1
9492
- toNode = display.lineDiv.lastChild
9493
- } else {
9494
- toLine = lineNo(display.view[toIndex + 1].line) - 1
9495
- toNode = display.view[toIndex + 1].node.previousSibling
9496
- }
9497
-
9498
- if (!fromNode) { return false }
9499
- var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine))
9500
- var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length))
9501
- while (newText.length > 1 && oldText.length > 1) {
9502
- if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine-- }
9503
- else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++ }
9504
- else { break }
9505
- }
9506
-
9507
- var cutFront = 0, cutEnd = 0
9508
- var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length)
9509
- while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront))
9510
- { ++cutFront }
9511
- var newBot = lst(newText), oldBot = lst(oldText)
9512
- var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0),
9513
- oldBot.length - (oldText.length == 1 ? cutFront : 0))
9514
- while (cutEnd < maxCutEnd &&
9515
- newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1))
9516
- { ++cutEnd }
9517
-
9518
- newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, "")
9519
- newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, "")
9520
-
9521
- var chFrom = Pos(fromLine, cutFront)
9522
- var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0)
9523
- if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) {
9524
- replaceRange(cm.doc, newText, chFrom, chTo, "+input")
9525
- return true
9526
- }
9527
- },
9528
 
9529
- ensurePolled: function() {
9530
- this.forceCompositionEnd()
9531
- },
9532
- reset: function() {
9533
- this.forceCompositionEnd()
9534
- },
9535
- forceCompositionEnd: function() {
9536
- if (!this.composing) { return }
9537
- this.composing = null
9538
- if (!this.pollContent()) { regChange(this.cm) }
9539
- this.div.blur()
9540
- this.div.focus()
9541
- },
9542
- readFromDOMSoon: function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9543
  var this$1 = this;
9544
 
9545
- if (this.readDOMTimeout != null) { return }
9546
- this.readDOMTimeout = setTimeout(function () {
9547
- this$1.readDOMTimeout = null
9548
- if (this$1.composing) { return }
9549
- if (this$1.cm.isReadOnly() || !this$1.pollContent())
9550
- { runInOp(this$1.cm, function () { return regChange(this$1.cm); }) }
9551
- }, 80)
9552
- },
 
 
 
9553
 
9554
- setUneditable: function(node) {
9555
- node.contentEditable = "false"
9556
- },
9557
 
9558
- onKeyPress: function(e) {
9559
- e.preventDefault()
9560
- if (!this.cm.isReadOnly())
9561
- { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0) }
9562
- },
9563
 
9564
- readOnlyChanged: function(val) {
9565
- this.div.contentEditable = String(val != "nocursor")
9566
- },
9567
 
9568
- onContextMenu: nothing,
9569
- resetPosition: nothing,
9570
 
9571
- needsContentAttribute: true
9572
- }, ContentEditableInput.prototype)
9573
 
9574
  function posToDOM(cm, pos) {
9575
  var view = findViewForLine(cm, pos.line)
@@ -9706,7 +9711,7 @@ webpackJsonp([1],{
9706
 
9707
  // TEXTAREA INPUT STYLE
9708
 
9709
- function TextareaInput(cm) {
9710
  this.cm = cm
9711
  // See input.poll and input.reset
9712
  this.prevInput = ""
@@ -9723,335 +9728,333 @@ webpackJsonp([1],{
9723
  // Used to work around IE issue with selection being forgotten when focus moves away from textarea
9724
  this.hasSelection = false
9725
  this.composing = null
9726
- }
9727
 
9728
- TextareaInput.prototype = copyObj({
9729
- init: function(display) {
9730
  var this$1 = this;
9731
 
9732
- var input = this, cm = this.cm
9733
 
9734
- // Wraps and hides input textarea
9735
- var div = this.wrapper = hiddenTextarea()
9736
- // The semihidden textarea that is focused when the editor is
9737
- // focused, and receives input.
9738
- var te = this.textarea = div.firstChild
9739
- display.wrapper.insertBefore(div, display.wrapper.firstChild)
9740
 
9741
- // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
9742
- if (ios) { te.style.width = "0px" }
9743
 
9744
- on(te, "input", function () {
9745
- if (ie && ie_version >= 9 && this$1.hasSelection) { this$1.hasSelection = null }
9746
- input.poll()
9747
- })
9748
 
9749
- on(te, "paste", function (e) {
9750
- if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return }
9751
 
9752
- cm.state.pasteIncoming = true
9753
- input.fastPoll()
9754
- })
9755
 
9756
- function prepareCopyCut(e) {
9757
- if (signalDOMEvent(cm, e)) { return }
9758
- if (cm.somethingSelected()) {
9759
- setLastCopied({lineWise: false, text: cm.getSelections()})
9760
- if (input.inaccurateSelection) {
9761
- input.prevInput = ""
9762
- input.inaccurateSelection = false
9763
- te.value = lastCopied.text.join("\n")
9764
- selectInput(te)
9765
- }
9766
- } else if (!cm.options.lineWiseCopyCut) {
9767
- return
 
 
 
 
 
9768
  } else {
9769
- var ranges = copyableRanges(cm)
9770
- setLastCopied({lineWise: true, text: ranges.text})
9771
- if (e.type == "cut") {
9772
- cm.setSelections(ranges.ranges, null, sel_dontScroll)
9773
- } else {
9774
- input.prevInput = ""
9775
- te.value = ranges.text.join("\n")
9776
- selectInput(te)
9777
- }
9778
  }
9779
- if (e.type == "cut") { cm.state.cutIncoming = true }
9780
  }
9781
- on(te, "cut", prepareCopyCut)
9782
- on(te, "copy", prepareCopyCut)
9783
-
9784
- on(display.scroller, "paste", function (e) {
9785
- if (eventInWidget(display, e) || signalDOMEvent(cm, e)) { return }
9786
- cm.state.pasteIncoming = true
9787
- input.focus()
9788
- })
9789
-
9790
- // Prevent normal selection in the editor (we handle our own)
9791
- on(display.lineSpace, "selectstart", function (e) {
9792
- if (!eventInWidget(display, e)) { e_preventDefault(e) }
9793
- })
9794
 
9795
- on(te, "compositionstart", function () {
9796
- var start = cm.getCursor("from")
9797
- if (input.composing) { input.composing.range.clear() }
9798
- input.composing = {
9799
- start: start,
9800
- range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"})
9801
- }
9802
- })
9803
- on(te, "compositionend", function () {
9804
- if (input.composing) {
9805
- input.poll()
9806
- input.composing.range.clear()
9807
- input.composing = null
9808
- }
9809
- })
9810
- },
9811
 
9812
- prepareSelection: function() {
9813
- // Redraw the selection and/or cursor
9814
- var cm = this.cm, display = cm.display, doc = cm.doc
9815
- var result = prepareSelection(cm)
9816
 
9817
- // Move the hidden textarea near the cursor to prevent scrolling artifacts
9818
- if (cm.options.moveInputWithCursor) {
9819
- var headPos = cursorCoords(cm, doc.sel.primary().head, "div")
9820
- var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect()
9821
- result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
9822
- headPos.top + lineOff.top - wrapOff.top))
9823
- result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
9824
- headPos.left + lineOff.left - wrapOff.left))
9825
  }
9826
-
9827
- return result
9828
- },
9829
-
9830
- showSelection: function(drawn) {
9831
- var cm = this.cm, display = cm.display
9832
- removeChildrenAndAdd(display.cursorDiv, drawn.cursors)
9833
- removeChildrenAndAdd(display.selectionDiv, drawn.selection)
9834
- if (drawn.teTop != null) {
9835
- this.wrapper.style.top = drawn.teTop + "px"
9836
- this.wrapper.style.left = drawn.teLeft + "px"
9837
  }
9838
- },
9839
-
9840
- // Reset the input to correspond to the selection (or to be empty,
9841
- // when not typing and nothing is selected)
9842
- reset: function(typing) {
9843
- if (this.contextMenuPending) { return }
9844
- var minimal, selected, cm = this.cm, doc = cm.doc
9845
- if (cm.somethingSelected()) {
9846
- this.prevInput = ""
9847
- var range = doc.sel.primary()
9848
- minimal = hasCopyEvent &&
9849
- (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000)
9850
- var content = minimal ? "-" : selected || cm.getSelection()
9851
- this.textarea.value = content
9852
- if (cm.state.focused) { selectInput(this.textarea) }
9853
- if (ie && ie_version >= 9) { this.hasSelection = content }
9854
- } else if (!typing) {
9855
- this.prevInput = this.textarea.value = ""
9856
- if (ie && ie_version >= 9) { this.hasSelection = null }
9857
- }
9858
- this.inaccurateSelection = minimal
9859
- },
9860
 
9861
- getField: function() { return this.textarea },
 
 
 
9862
 
9863
- supportsTouch: function() { return false },
 
 
 
 
 
 
 
 
9864
 
9865
- focus: function() {
9866
- if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) {
9867
- try { this.textarea.focus() }
9868
- catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM
9869
- }
9870
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9871
 
9872
- blur: function() { this.textarea.blur() },
9873
 
9874
- resetPosition: function() {
9875
- this.wrapper.style.top = this.wrapper.style.left = 0
9876
- },
9877
 
9878
- receivedFocus: function() { this.slowPoll() },
 
 
 
 
 
9879
 
9880
- // Poll for input changes, using the normal rate of polling. This
9881
- // runs as long as the editor is focused.
9882
- slowPoll: function() {
9883
- var this$1 = this;
9884
 
9885
- if (this.pollingFast) { return }
9886
- this.polling.set(this.cm.options.pollInterval, function () {
9887
- this$1.poll()
9888
- if (this$1.cm.state.focused) { this$1.slowPoll() }
9889
- })
9890
- },
9891
 
9892
- // When an event has just come in that is likely to add or change
9893
- // something in the input textarea, we poll faster, to ensure that
9894
- // the change appears on the screen quickly.
9895
- fastPoll: function() {
9896
- var missed = false, input = this
9897
- input.pollingFast = true
9898
- function p() {
9899
- var changed = input.poll()
9900
- if (!changed && !missed) {missed = true; input.polling.set(60, p)}
9901
- else {input.pollingFast = false; input.slowPoll()}
9902
- }
9903
- input.polling.set(20, p)
9904
- },
9905
 
9906
- // Read input from the textarea, and update the document to match.
9907
- // When something is selected, it is present in the textarea, and
9908
- // selected (unless it is huge, in which case a placeholder is
9909
- // used). When nothing is selected, the cursor sits after previously
9910
- // seen text (can be empty), which is stored in prevInput (we must
9911
- // not reset the textarea when typing, because that breaks IME).
9912
- poll: function() {
9913
  var this$1 = this;
9914
 
9915
- var cm = this.cm, input = this.textarea, prevInput = this.prevInput
9916
- // Since this is called a *lot*, try to bail out as cheaply as
9917
- // possible when it is clear that nothing happened. hasSelection
9918
- // will be the case when there is a lot of text in the textarea,
9919
- // in which case reading its value would be expensive.
9920
- if (this.contextMenuPending || !cm.state.focused ||
9921
- (hasSelection(input) && !prevInput && !this.composing) ||
9922
- cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)
9923
- { return false }
9924
-
9925
- var text = input.value
9926
- // If nothing changed, bail.
9927
- if (text == prevInput && !cm.somethingSelected()) { return false }
9928
- // Work around nonsensical selection resetting in IE9/10, and
9929
- // inexplicable appearance of private area unicode characters on
9930
- // some key combos in Mac (#2689).
9931
- if (ie && ie_version >= 9 && this.hasSelection === text ||
9932
- mac && /[\uf700-\uf7ff]/.test(text)) {
9933
- cm.display.input.reset()
9934
- return false
9935
- }
9936
-
9937
- if (cm.doc.sel == cm.display.selForContextMenu) {
9938
- var first = text.charCodeAt(0)
9939
- if (first == 0x200b && !prevInput) { prevInput = "\u200b" }
9940
- if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") }
9941
- }
9942
- // Find the part of the input that is actually new
9943
- var same = 0, l = Math.min(prevInput.length, text.length)
9944
- while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) { ++same }
9945
 
9946
- runInOp(cm, function () {
9947
- applyTextInput(cm, text.slice(same), prevInput.length - same,
9948
- null, this$1.composing ? "*compose" : null)
 
 
 
 
 
 
9949
 
9950
- // Don't leave long text in the textarea, since it makes further polling slow
9951
- if (text.length > 1000 || text.indexOf("\n") > -1) { input.value = this$1.prevInput = "" }
9952
- else { this$1.prevInput = text }
 
 
 
 
 
 
 
 
9953
 
9954
- if (this$1.composing) {
9955
- this$1.composing.range.clear()
9956
- this$1.composing.range = cm.markText(this$1.composing.start, cm.getCursor("to"),
9957
- {className: "CodeMirror-composing"})
9958
- }
9959
- })
9960
- return true
9961
- },
9962
 
9963
- ensurePolled: function() {
9964
- if (this.pollingFast && this.poll()) { this.pollingFast = false }
9965
- },
9966
 
9967
- onKeyPress: function() {
9968
- if (ie && ie_version >= 9) { this.hasSelection = null }
9969
- this.fastPoll()
9970
- },
9971
 
9972
- onContextMenu: function(e) {
9973
- var input = this, cm = input.cm, display = cm.display, te = input.textarea
9974
- var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop
9975
- if (!pos || presto) { return } // Opera is difficult.
9976
-
9977
- // Reset the current text selection only if the click is done outside of the selection
9978
- // and 'resetSelectionOnContextMenu' option is true.
9979
- var reset = cm.options.resetSelectionOnContextMenu
9980
- if (reset && cm.doc.sel.contains(pos) == -1)
9981
- { operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll) }
9982
-
9983
- var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText
9984
- input.wrapper.style.cssText = "position: absolute"
9985
- var wrapperBox = input.wrapper.getBoundingClientRect()
9986
- te.style.cssText = "position: absolute; width: 30px; height: 30px;\n top: " + (e.clientY - wrapperBox.top - 5) + "px; left: " + (e.clientX - wrapperBox.left - 5) + "px;\n z-index: 1000; background: " + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + ";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"
9987
- var oldScrollY
9988
- if (webkit) { oldScrollY = window.scrollY } // Work around Chrome issue (#2712)
9989
- display.input.focus()
9990
- if (webkit) { window.scrollTo(null, oldScrollY) }
9991
- display.input.reset()
9992
- // Adds "Select all" to context menu in FF
9993
- if (!cm.somethingSelected()) { te.value = input.prevInput = " " }
9994
- input.contextMenuPending = true
9995
- display.selForContextMenu = cm.doc.sel
9996
- clearTimeout(display.detectingSelectAll)
9997
-
9998
- // Select-all will be greyed out if there's nothing to select, so
9999
- // this adds a zero-width space so that we can later check whether
10000
- // it got selected.
10001
- function prepareSelectAllHack() {
10002
- if (te.selectionStart != null) {
10003
- var selected = cm.somethingSelected()
10004
- var extval = "\u200b" + (selected ? te.value : "")
10005
- te.value = "\u21da" // Used to catch context-menu undo
10006
- te.value = extval
10007
- input.prevInput = selected ? "" : "\u200b"
10008
- te.selectionStart = 1; te.selectionEnd = extval.length
10009
- // Re-set this, in case some other handler touched the
10010
- // selection in the meantime.
10011
- display.selForContextMenu = cm.doc.sel
10012
- }
10013
  }
10014
- function rehide() {
10015
- input.contextMenuPending = false
10016
- input.wrapper.style.cssText = oldWrapperCSS
10017
- te.style.cssText = oldCSS
10018
- if (ie && ie_version < 9) { display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos) }
10019
-
10020
- // Try to detect the user choosing select-all
10021
- if (te.selectionStart != null) {
10022
- if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack() }
10023
- var i = 0, poll = function () {
10024
- if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 &&
10025
- te.selectionEnd > 0 && input.prevInput == "\u200b")
10026
- { operation(cm, selectAll)(cm) }
10027
- else if (i++ < 10) { display.detectingSelectAll = setTimeout(poll, 500) }
10028
- else { display.input.reset() }
10029
- }
10030
- display.detectingSelectAll = setTimeout(poll, 200)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10031
  }
 
10032
  }
 
10033
 
10034
- if (ie && ie_version >= 9) { prepareSelectAllHack() }
10035
- if (captureRightClick) {
10036
- e_stop(e)
10037
- var mouseup = function () {
10038
- off(window, "mouseup", mouseup)
10039
- setTimeout(rehide, 20)
10040
- }
10041
- on(window, "mouseup", mouseup)
10042
- } else {
10043
- setTimeout(rehide, 50)
10044
  }
10045
- },
 
 
 
 
10046
 
10047
- readOnlyChanged: function(val) {
10048
- if (!val) { this.reset() }
10049
- },
10050
 
10051
- setUneditable: nothing,
10052
 
10053
- needsContentAttribute: false
10054
- }, TextareaInput.prototype)
10055
 
10056
  function fromTextArea(textarea, options) {
10057
  options = options ? copyObj(options) : {}
@@ -10202,7 +10205,7 @@ webpackJsonp([1],{
10202
 
10203
  addLegacyProps(CodeMirror)
10204
 
10205
- CodeMirror.version = "5.21.0"
10206
 
10207
  return CodeMirror;
10208
 
@@ -10739,7 +10742,7 @@ webpackJsonp([1],{
10739
  "transition-property", "transition-timing-function", "unicode-bidi",
10740
  "user-select", "vertical-align", "visibility", "voice-balance", "voice-duration",
10741
  "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
10742
- "voice-volume", "volume", "white-space", "widows", "width", "word-break",
10743
  "word-spacing", "word-wrap", "z-index",
10744
  // SVG-specific
10745
  "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color",
@@ -10813,7 +10816,7 @@ webpackJsonp([1],{
10813
  "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch",
10814
  "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
10815
  "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse",
10816
- "compact", "condensed", "contain", "content",
10817
  "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop",
10818
  "cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
10819
  "decimal-leading-zero", "default", "default-button", "dense", "destination-atop",
@@ -10856,7 +10859,7 @@ webpackJsonp([1],{
10856
  "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize",
10857
  "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
10858
  "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
10859
- "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote",
10860
  "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
10861
  "outside", "outside-shape", "overlay", "overline", "padding", "padding-box",
10862
  "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter",
@@ -10868,7 +10871,7 @@ webpackJsonp([1],{
10868
  "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY",
10869
  "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running",
10870
  "s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
10871
- "scroll", "scrollbar", "se-resize", "searchfield",
10872
  "searchfield-cancel-button", "searchfield-decoration",
10873
  "searchfield-results-button", "searchfield-results-decoration",
10874
  "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama",
@@ -10886,9 +10889,9 @@ webpackJsonp([1],{
10886
  "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
10887
  "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
10888
  "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
10889
- "trad-chinese-formal", "trad-chinese-informal",
10890
  "translate", "translate3d", "translateX", "translateY", "translateZ",
10891
- "transparent", "ultra-condensed", "ultra-expanded", "underline", "up",
10892
  "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
10893
  "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
10894
  "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
2077
  var bidiOrdering = (function() {
2078
  // Character types for codepoints 0 to 0xff
2079
  var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"
2080
+ // Character types for codepoints 0x600 to 0x6f9
2081
+ var arabicTypes = "nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111"
2082
  function charType(code) {
2083
  if (code <= 0xf7) { return lowTypes.charAt(code) }
2084
  else if (0x590 <= code && code <= 0x5f4) { return "R" }
2085
+ else if (0x600 <= code && code <= 0x6f9) { return arabicTypes.charAt(code - 0x600) }
2086
  else if (0x6ee <= code && code <= 0x8ac) { return "r" }
2087
  else if (0x2000 <= code && code <= 0x200b) { return "w" }
2088
  else if (code == 0x200c) { return "b" }
4388
  }
4389
  }
4390
 
4391
+ var NativeScrollbars = function(place, scroll, cm) {
4392
  this.cm = cm
4393
  var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar")
4394
  var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar")
4404
  this.checkedZeroWidth = false
4405
  // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
4406
  if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = "18px" }
4407
+ };
4408
+
4409
+ NativeScrollbars.prototype.update = function (measure) {
4410
+ var needsH = measure.scrollWidth > measure.clientWidth + 1
4411
+ var needsV = measure.scrollHeight > measure.clientHeight + 1
4412
+ var sWidth = measure.nativeBarWidth
4413
+
4414
+ if (needsV) {
4415
+ this.vert.style.display = "block"
4416
+ this.vert.style.bottom = needsH ? sWidth + "px" : "0"
4417
+ var totalHeight = measure.viewHeight - (needsH ? sWidth : 0)
4418
+ // A bug in IE8 can cause this value to be negative, so guard it.
4419
+ this.vert.firstChild.style.height =
4420
+ Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"
4421
+ } else {
4422
+ this.vert.style.display = ""
4423
+ this.vert.firstChild.style.height = "0"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4424
  }
 
 
 
4425
 
4426
+ if (needsH) {
4427
+ this.horiz.style.display = "block"
4428
+ this.horiz.style.right = needsV ? sWidth + "px" : "0"
4429
+ this.horiz.style.left = measure.barLeft + "px"
4430
+ var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0)
4431
+ this.horiz.firstChild.style.width =
4432
+ (measure.scrollWidth - measure.clientWidth + totalWidth) + "px"
4433
+ } else {
4434
+ this.horiz.style.display = ""
4435
+ this.horiz.firstChild.style.width = "0"
4436
+ }
4437
+
4438
+ if (!this.checkedZeroWidth && measure.clientHeight > 0) {
4439
+ if (sWidth == 0) { this.zeroWidthHack() }
4440
+ this.checkedZeroWidth = true
4441
+ }
4442
+
4443
+ return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}
4444
+ };
4445
+
4446
+ NativeScrollbars.prototype.setScrollLeft = function (pos) {
4447
+ if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos }
4448
+ if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz) }
4449
+ };
4450
+
4451
+ NativeScrollbars.prototype.setScrollTop = function (pos) {
4452
+ if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos }
4453
+ if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert) }
4454
+ };
4455
+
4456
+ NativeScrollbars.prototype.zeroWidthHack = function () {
4457
+ var w = mac && !mac_geMountainLion ? "12px" : "18px"
4458
+ this.horiz.style.height = this.vert.style.width = w
4459
+ this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"
4460
+ this.disableHoriz = new Delayed
4461
+ this.disableVert = new Delayed
4462
+ };
4463
+
4464
+ NativeScrollbars.prototype.enableZeroWidthBar = function (bar, delay) {
4465
+ bar.style.pointerEvents = "auto"
4466
+ function maybeDisable() {
4467
+ // To find out whether the scrollbar is still visible, we
4468
+ // check whether the element under the pixel in the bottom
4469
+ // left corner of the scrollbar box is the scrollbar box
4470
+ // itself (when the bar is still visible) or its filler child
4471
+ // (when the bar is hidden). If it is still visible, we keep
4472
+ // it enabled, if it's hidden, we disable pointer events.
4473
+ var box = bar.getBoundingClientRect()
4474
+ var elt = document.elementFromPoint(box.left + 1, box.bottom - 1)
4475
+ if (elt != bar) { bar.style.pointerEvents = "none" }
4476
+ else { delay.set(1000, maybeDisable) }
4477
+ }
4478
+ delay.set(1000, maybeDisable)
4479
+ };
4480
+
4481
+ NativeScrollbars.prototype.clear = function () {
4482
+ var parent = this.horiz.parentNode
4483
+ parent.removeChild(this.horiz)
4484
+ parent.removeChild(this.vert)
4485
+ };
4486
+
4487
+ var NullScrollbars = function () {};
4488
+
4489
+ NullScrollbars.prototype.update = function () { return {bottom: 0, right: 0} };
4490
+ NullScrollbars.prototype.setScrollLeft = function () {};
4491
+ NullScrollbars.prototype.setScrollTop = function () {};
4492
+ NullScrollbars.prototype.clear = function () {};
4493
 
4494
  function updateScrollbars(cm, measure) {
4495
  if (!measure) { measure = measureForScrollbars(cm) }
5068
 
5069
  // DISPLAY DRAWING
5070
 
5071
+ var DisplayUpdate = function(cm, viewport, force) {
5072
  var display = cm.display
5073
 
5074
  this.viewport = viewport
5081
  this.force = force
5082
  this.dims = getDimensions(cm)
5083
  this.events = []
5084
+ };
5085
 
5086
+ DisplayUpdate.prototype.signal = function (emitter, type) {
5087
  if (hasHandler(emitter, type))
5088
  { this.events.push(arguments) }
5089
+ };
5090
+ DisplayUpdate.prototype.finish = function () {
5091
+ var this$1 = this;
5092
 
5093
  for (var i = 0; i < this.events.length; i++)
5094
  { signal.apply(null, this$1.events[i]) }
5095
+ };
5096
 
5097
  function maybeClipScrollbars(cm) {
5098
  var display = cm.display
8220
  for (var i = newBreaks.length - 1; i >= 0; i--)
8221
  { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)) }
8222
  })
8223
+ option("specialChars", /[\u0000-\u001f\u007f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/g, function (cm, val, old) {
8224
  cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g")
8225
  if (old != Init) { cm.refresh() }
8226
  })
8310
  function guttersChanged(cm) {
8311
  updateGutters(cm)
8312
  regChange(cm)
8313
+ alignHorizontally(cm)
8314
  }
8315
 
8316
  function dragDropChanged(cm, value, old) {
8365
  themeChanged(this)
8366
  if (options.lineWrapping)
8367
  { this.display.wrapper.className += " CodeMirror-wrap" }
 
8368
  initScrollbars(this)
8369
 
8370
  this.state = {
8383
  specialChars: null
8384
  }
8385
 
8386
+ if (options.autofocus && !mobile) { display.input.focus() }
8387
+
8388
  // Override magic textarea content restore that IE sometimes does
8389
  // on our hidden textarea on reload
8390
  if (ie && ie_version < 11) { setTimeout(function () { return this$1.display.input.reset(true); }, 20) }
8740
  options[option] = value
8741
  if (optionHandlers.hasOwnProperty(option))
8742
  { operation(this, optionHandlers[option])(this, value, old) }
8743
+ signal(this, "optionChange", this, option)
8744
  },
8745
 
8746
  getOption: function(option) {return this.options[option]},
9248
 
9249
  // CONTENTEDITABLE INPUT STYLE
9250
 
9251
+ var ContentEditableInput = function(cm) {
9252
  this.cm = cm
9253
  this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null
9254
  this.polling = new Delayed()
9255
  this.composing = null
9256
  this.gracePeriod = false
9257
  this.readDOMTimeout = null
9258
+ };
9259
 
9260
+ ContentEditableInput.prototype.init = function (display) {
 
9261
  var this$1 = this;
9262
 
9263
+ var input = this, cm = input.cm
9264
+ var div = input.div = display.lineDiv
9265
+ disableBrowserMagic(div, cm.options.spellcheck)
9266
 
9267
+ on(div, "paste", function (e) {
9268
+ if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return }
9269
+ // IE doesn't fire input events, so we schedule a read for the pasted content in this way
9270
+ if (ie_version <= 11) { setTimeout(operation(cm, function () {
9271
+ if (!input.pollContent()) { regChange(cm) }
9272
+ }), 20) }
9273
+ })
9274
 
9275
+ on(div, "compositionstart", function (e) {
9276
+ this$1.composing = {data: e.data, done: false}
9277
+ })
9278
+ on(div, "compositionupdate", function (e) {
9279
+ if (!this$1.composing) { this$1.composing = {data: e.data, done: false} }
9280
+ })
9281
+ on(div, "compositionend", function (e) {
9282
+ if (this$1.composing) {
9283
+ if (e.data != this$1.composing.data) { this$1.readFromDOMSoon() }
9284
+ this$1.composing.done = true
9285
+ }
9286
+ })
9287
 
9288
+ on(div, "touchstart", function () { return input.forceCompositionEnd(); })
9289
 
9290
+ on(div, "input", function () {
9291
+ if (!this$1.composing) { this$1.readFromDOMSoon() }
9292
+ })
9293
 
9294
+ function onCopyCut(e) {
9295
+ if (signalDOMEvent(cm, e)) { return }
9296
+ if (cm.somethingSelected()) {
9297
+ setLastCopied({lineWise: false, text: cm.getSelections()})
9298
+ if (e.type == "cut") { cm.replaceSelection("", null, "cut") }
9299
+ } else if (!cm.options.lineWiseCopyCut) {
9300
+ return
9301
+ } else {
9302
+ var ranges = copyableRanges(cm)
9303
+ setLastCopied({lineWise: true, text: ranges.text})
9304
+ if (e.type == "cut") {
9305
+ cm.operation(function () {
9306
+ cm.setSelections(ranges.ranges, 0, sel_dontScroll)
9307
+ cm.replaceSelection("", null, "cut")
9308
+ })
 
9309
  }
9310
+ }
9311
+ if (e.clipboardData) {
9312
+ e.clipboardData.clearData()
9313
+ var content = lastCopied.text.join("\n")
9314
+ // iOS exposes the clipboard API, but seems to discard content inserted into it
9315
+ e.clipboardData.setData("Text", content)
9316
+ if (e.clipboardData.getData("Text") == content) {
9317
+ e.preventDefault()
9318
+ return
9319
  }
 
 
 
 
 
 
 
 
 
 
 
9320
  }
9321
+ // Old-fashioned briefly-focus-a-textarea hack
9322
+ var kludge = hiddenTextarea(), te = kludge.firstChild
9323
+ cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild)
9324
+ te.value = lastCopied.text.join("\n")
9325
+ var hadFocus = document.activeElement
9326
+ selectInput(te)
9327
+ setTimeout(function () {
9328
+ cm.display.lineSpace.removeChild(kludge)
9329
+ hadFocus.focus()
9330
+ if (hadFocus == div) { input.showPrimarySelection() }
9331
+ }, 50)
9332
+ }
9333
+ on(div, "copy", onCopyCut)
9334
+ on(div, "cut", onCopyCut)
9335
+ };
9336
+
9337
+ ContentEditableInput.prototype.prepareSelection = function () {
9338
+ var result = prepareSelection(this.cm, false)
9339
+ result.focus = this.cm.state.focused
9340
+ return result
9341
+ };
9342
+
9343
+ ContentEditableInput.prototype.showSelection = function (info, takeFocus) {
9344
+ if (!info || !this.cm.display.view.length) { return }
9345
+ if (info.focus || takeFocus) { this.showPrimarySelection() }
9346
+ this.showMultipleSelections(info)
9347
+ };
9348
+
9349
+ ContentEditableInput.prototype.showPrimarySelection = function () {
9350
+ var sel = window.getSelection(), prim = this.cm.doc.sel.primary()
9351
+ var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset)
9352
+ var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset)
9353
+ if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad &&
9354
+ cmp(minPos(curAnchor, curFocus), prim.from()) == 0 &&
9355
+ cmp(maxPos(curAnchor, curFocus), prim.to()) == 0)
9356
+ { return }
9357
 
9358
+ var start = posToDOM(this.cm, prim.from())
9359
+ var end = posToDOM(this.cm, prim.to())
9360
+ if (!start && !end) { return }
9361
+
9362
+ var view = this.cm.display.view
9363
+ var old = sel.rangeCount && sel.getRangeAt(0)
9364
+ if (!start) {
9365
+ start = {node: view[0].measure.map[2], offset: 0}
9366
+ } else if (!end) { // FIXME dangerously hacky
9367
+ var measure = view[view.length - 1].measure
9368
+ var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map
9369
+ end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}
9370
+ }
9371
+
9372
+ var rng
9373
+ try { rng = range(start.node, start.offset, end.offset, end.node) }
9374
+ catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible
9375
+ if (rng) {
9376
+ if (!gecko && this.cm.state.focused) {
9377
+ sel.collapse(start.node, start.offset)
9378
+ if (!rng.collapsed) {
 
 
 
 
 
 
 
 
 
 
 
 
 
9379
  sel.removeAllRanges()
9380
  sel.addRange(rng)
9381
  }
9382
+ } else {
9383
+ sel.removeAllRanges()
9384
+ sel.addRange(rng)
9385
  }
9386
+ if (old && sel.anchorNode == null) { sel.addRange(old) }
9387
+ else if (gecko) { this.startGracePeriod() }
9388
+ }
9389
+ this.rememberSelection()
9390
+ };
9391
 
9392
+ ContentEditableInput.prototype.startGracePeriod = function () {
9393
  var this$1 = this;
9394
 
9395
+ clearTimeout(this.gracePeriod)
9396
+ this.gracePeriod = setTimeout(function () {
9397
+ this$1.gracePeriod = false
9398
+ if (this$1.selectionChanged())
9399
+ { this$1.cm.operation(function () { return this$1.cm.curOp.selectionChanged = true; }) }
9400
+ }, 20)
9401
+ };
9402
+
9403
+ ContentEditableInput.prototype.showMultipleSelections = function (info) {
9404
+ removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors)
9405
+ removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection)
9406
+ };
9407
+
9408
+ ContentEditableInput.prototype.rememberSelection = function () {
9409
+ var sel = window.getSelection()
9410
+ this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset
9411
+ this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset
9412
+ };
9413
+
9414
+ ContentEditableInput.prototype.selectionInEditor = function () {
9415
+ var sel = window.getSelection()
9416
+ if (!sel.rangeCount) { return false }
9417
+ var node = sel.getRangeAt(0).commonAncestorContainer
9418
+ return contains(this.div, node)
9419
+ };
9420
+
9421
+ ContentEditableInput.prototype.focus = function () {
9422
+ if (this.cm.options.readOnly != "nocursor") {
9423
+ if (!this.selectionInEditor())
9424
+ { this.showSelection(this.prepareSelection(), true) }
9425
+ this.div.focus()
9426
+ }
9427
+ };
9428
+ ContentEditableInput.prototype.blur = function () { this.div.blur() };
9429
+ ContentEditableInput.prototype.getField = function () { return this.div };
 
 
 
 
 
 
 
 
 
9430
 
9431
+ ContentEditableInput.prototype.supportsTouch = function () { return true };
 
 
 
 
 
 
 
9432
 
9433
+ ContentEditableInput.prototype.receivedFocus = function () {
9434
+ var input = this
9435
+ if (this.selectionInEditor())
9436
+ { this.pollSelection() }
9437
+ else
9438
+ { runInOp(this.cm, function () { return input.cm.curOp.selectionChanged = true; }) }
9439
 
9440
+ function poll() {
9441
+ if (input.cm.state.focused) {
9442
+ input.pollSelection()
9443
+ input.polling.set(input.cm.options.pollInterval, poll)
 
 
 
 
 
 
9444
  }
9445
+ }
9446
+ this.polling.set(this.cm.options.pollInterval, poll)
9447
+ };
9448
 
9449
+ ContentEditableInput.prototype.selectionChanged = function () {
9450
+ var sel = window.getSelection()
9451
+ return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
9452
+ sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset
9453
+ };
9454
 
9455
+ ContentEditableInput.prototype.pollSelection = function () {
9456
+ if (!this.composing && this.readDOMTimeout == null && !this.gracePeriod && this.selectionChanged()) {
9457
+ var sel = window.getSelection(), cm = this.cm
9458
+ this.rememberSelection()
9459
+ var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset)
9460
+ var head = domToPos(cm, sel.focusNode, sel.focusOffset)
9461
+ if (anchor && head) { runInOp(cm, function () {
9462
+ setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll)
9463
+ if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true }
9464
+ }) }
9465
+ }
9466
+ };
9467
+
9468
+ ContentEditableInput.prototype.pollContent = function () {
9469
+ if (this.readDOMTimeout != null) {
9470
+ clearTimeout(this.readDOMTimeout)
9471
+ this.readDOMTimeout = null
9472
+ }
9473
+
9474
+ var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary()
9475
+ var from = sel.from(), to = sel.to()
9476
+ if (from.ch == 0 && from.line > cm.firstLine())
9477
+ { from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length) }
9478
+ if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine())
9479
+ { to = Pos(to.line + 1, 0) }
9480
+ if (from.line < display.viewFrom || to.line > display.viewTo - 1) { return false }
9481
+
9482
+ var fromIndex, fromLine, fromNode
9483
+ if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) {
9484
+ fromLine = lineNo(display.view[0].line)
9485
+ fromNode = display.view[0].node
9486
+ } else {
9487
+ fromLine = lineNo(display.view[fromIndex].line)
9488
+ fromNode = display.view[fromIndex - 1].node.nextSibling
9489
+ }
9490
+ var toIndex = findViewIndex(cm, to.line)
9491
+ var toLine, toNode
9492
+ if (toIndex == display.view.length - 1) {
9493
+ toLine = display.viewTo - 1
9494
+ toNode = display.lineDiv.lastChild
9495
+ } else {
9496
+ toLine = lineNo(display.view[toIndex + 1].line) - 1
9497
+ toNode = display.view[toIndex + 1].node.previousSibling
9498
+ }
9499
 
9500
+ if (!fromNode) { return false }
9501
+ var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine))
9502
+ var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length))
9503
+ while (newText.length > 1 && oldText.length > 1) {
9504
+ if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine-- }
9505
+ else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++ }
9506
+ else { break }
9507
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9508
 
9509
+ var cutFront = 0, cutEnd = 0
9510
+ var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length)
9511
+ while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront))
9512
+ { ++cutFront }
9513
+ var newBot = lst(newText), oldBot = lst(oldText)
9514
+ var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0),
9515
+ oldBot.length - (oldText.length == 1 ? cutFront : 0))
9516
+ while (cutEnd < maxCutEnd &&
9517
+ newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1))
9518
+ { ++cutEnd }
9519
+
9520
+ newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, "")
9521
+ newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, "")
9522
+
9523
+ var chFrom = Pos(fromLine, cutFront)
9524
+ var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0)
9525
+ if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) {
9526
+ replaceRange(cm.doc, newText, chFrom, chTo, "+input")
9527
+ return true
9528
+ }
9529
+ };
9530
+
9531
+ ContentEditableInput.prototype.ensurePolled = function () {
9532
+ this.forceCompositionEnd()
9533
+ };
9534
+ ContentEditableInput.prototype.reset = function () {
9535
+ this.forceCompositionEnd()
9536
+ };
9537
+ ContentEditableInput.prototype.forceCompositionEnd = function () {
9538
+ if (!this.composing) { return }
9539
+ clearTimeout(this.readDOMTimeout)
9540
+ this.composing = null
9541
+ if (!this.pollContent()) { regChange(this.cm) }
9542
+ this.div.blur()
9543
+ this.div.focus()
9544
+ };
9545
+ ContentEditableInput.prototype.readFromDOMSoon = function () {
9546
  var this$1 = this;
9547
 
9548
+ if (this.readDOMTimeout != null) { return }
9549
+ this.readDOMTimeout = setTimeout(function () {
9550
+ this$1.readDOMTimeout = null
9551
+ if (this$1.composing) {
9552
+ if (this$1.composing.done) { this$1.composing = null }
9553
+ else { return }
9554
+ }
9555
+ if (this$1.cm.isReadOnly() || !this$1.pollContent())
9556
+ { runInOp(this$1.cm, function () { return regChange(this$1.cm); }) }
9557
+ }, 80)
9558
+ };
9559
 
9560
+ ContentEditableInput.prototype.setUneditable = function (node) {
9561
+ node.contentEditable = "false"
9562
+ };
9563
 
9564
+ ContentEditableInput.prototype.onKeyPress = function (e) {
9565
+ e.preventDefault()
9566
+ if (!this.cm.isReadOnly())
9567
+ { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0) }
9568
+ };
9569
 
9570
+ ContentEditableInput.prototype.readOnlyChanged = function (val) {
9571
+ this.div.contentEditable = String(val != "nocursor")
9572
+ };
9573
 
9574
+ ContentEditableInput.prototype.onContextMenu = function () {};
9575
+ ContentEditableInput.prototype.resetPosition = function () {};
9576
 
9577
+ ContentEditableInput.prototype.needsContentAttribute = true
 
9578
 
9579
  function posToDOM(cm, pos) {
9580
  var view = findViewForLine(cm, pos.line)
9711
 
9712
  // TEXTAREA INPUT STYLE
9713
 
9714
+ var TextareaInput = function(cm) {
9715
  this.cm = cm
9716
  // See input.poll and input.reset
9717
  this.prevInput = ""
9728
  // Used to work around IE issue with selection being forgotten when focus moves away from textarea
9729
  this.hasSelection = false
9730
  this.composing = null
9731
+ };
9732
 
9733
+ TextareaInput.prototype.init = function (display) {
 
9734
  var this$1 = this;
9735
 
9736
+ var input = this, cm = this.cm
9737
 
9738
+ // Wraps and hides input textarea
9739
+ var div = this.wrapper = hiddenTextarea()
9740
+ // The semihidden textarea that is focused when the editor is
9741
+ // focused, and receives input.
9742
+ var te = this.textarea = div.firstChild
9743
+ display.wrapper.insertBefore(div, display.wrapper.firstChild)
9744
 
9745
+ // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
9746
+ if (ios) { te.style.width = "0px" }
9747
 
9748
+ on(te, "input", function () {
9749
+ if (ie && ie_version >= 9 && this$1.hasSelection) { this$1.hasSelection = null }
9750
+ input.poll()
9751
+ })
9752
 
9753
+ on(te, "paste", function (e) {
9754
+ if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return }
9755
 
9756
+ cm.state.pasteIncoming = true
9757
+ input.fastPoll()
9758
+ })
9759
 
9760
+ function prepareCopyCut(e) {
9761
+ if (signalDOMEvent(cm, e)) { return }
9762
+ if (cm.somethingSelected()) {
9763
+ setLastCopied({lineWise: false, text: cm.getSelections()})
9764
+ if (input.inaccurateSelection) {
9765
+ input.prevInput = ""
9766
+ input.inaccurateSelection = false
9767
+ te.value = lastCopied.text.join("\n")
9768
+ selectInput(te)
9769
+ }
9770
+ } else if (!cm.options.lineWiseCopyCut) {
9771
+ return
9772
+ } else {
9773
+ var ranges = copyableRanges(cm)
9774
+ setLastCopied({lineWise: true, text: ranges.text})
9775
+ if (e.type == "cut") {
9776
+ cm.setSelections(ranges.ranges, null, sel_dontScroll)
9777
  } else {
9778
+ input.prevInput = ""
9779
+ te.value = ranges.text.join("\n")
9780
+ selectInput(te)
 
 
 
 
 
 
9781
  }
 
9782
  }
9783
+ if (e.type == "cut") { cm.state.cutIncoming = true }
9784
+ }
9785
+ on(te, "cut", prepareCopyCut)
9786
+ on(te, "copy", prepareCopyCut)
 
 
 
 
 
 
 
 
 
9787
 
9788
+ on(display.scroller, "paste", function (e) {
9789
+ if (eventInWidget(display, e) || signalDOMEvent(cm, e)) { return }
9790
+ cm.state.pasteIncoming = true
9791
+ input.focus()
9792
+ })
 
 
 
 
 
 
 
 
 
 
 
9793
 
9794
+ // Prevent normal selection in the editor (we handle our own)
9795
+ on(display.lineSpace, "selectstart", function (e) {
9796
+ if (!eventInWidget(display, e)) { e_preventDefault(e) }
9797
+ })
9798
 
9799
+ on(te, "compositionstart", function () {
9800
+ var start = cm.getCursor("from")
9801
+ if (input.composing) { input.composing.range.clear() }
9802
+ input.composing = {
9803
+ start: start,
9804
+ range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"})
 
 
9805
  }
9806
+ })
9807
+ on(te, "compositionend", function () {
9808
+ if (input.composing) {
9809
+ input.poll()
9810
+ input.composing.range.clear()
9811
+ input.composing = null
 
 
 
 
 
9812
  }
9813
+ })
9814
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9815
 
9816
+ TextareaInput.prototype.prepareSelection = function () {
9817
+ // Redraw the selection and/or cursor
9818
+ var cm = this.cm, display = cm.display, doc = cm.doc
9819
+ var result = prepareSelection(cm)
9820
 
9821
+ // Move the hidden textarea near the cursor to prevent scrolling artifacts
9822
+ if (cm.options.moveInputWithCursor) {
9823
+ var headPos = cursorCoords(cm, doc.sel.primary().head, "div")
9824
+ var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect()
9825
+ result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
9826
+ headPos.top + lineOff.top - wrapOff.top))
9827
+ result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
9828
+ headPos.left + lineOff.left - wrapOff.left))
9829
+ }
9830
 
9831
+ return result
9832
+ };
9833
+
9834
+ TextareaInput.prototype.showSelection = function (drawn) {
9835
+ var cm = this.cm, display = cm.display
9836
+ removeChildrenAndAdd(display.cursorDiv, drawn.cursors)
9837
+ removeChildrenAndAdd(display.selectionDiv, drawn.selection)
9838
+ if (drawn.teTop != null) {
9839
+ this.wrapper.style.top = drawn.teTop + "px"
9840
+ this.wrapper.style.left = drawn.teLeft + "px"
9841
+ }
9842
+ };
9843
+
9844
+ // Reset the input to correspond to the selection (or to be empty,
9845
+ // when not typing and nothing is selected)
9846
+ TextareaInput.prototype.reset = function (typing) {
9847
+ if (this.contextMenuPending) { return }
9848
+ var minimal, selected, cm = this.cm, doc = cm.doc
9849
+ if (cm.somethingSelected()) {
9850
+ this.prevInput = ""
9851
+ var range = doc.sel.primary()
9852
+ minimal = hasCopyEvent &&
9853
+ (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000)
9854
+ var content = minimal ? "-" : selected || cm.getSelection()
9855
+ this.textarea.value = content
9856
+ if (cm.state.focused) { selectInput(this.textarea) }
9857
+ if (ie && ie_version >= 9) { this.hasSelection = content }
9858
+ } else if (!typing) {
9859
+ this.prevInput = this.textarea.value = ""
9860
+ if (ie && ie_version >= 9) { this.hasSelection = null }
9861
+ }
9862
+ this.inaccurateSelection = minimal
9863
+ };
9864
 
9865
+ TextareaInput.prototype.getField = function () { return this.textarea };
9866
 
9867
+ TextareaInput.prototype.supportsTouch = function () { return false };
 
 
9868
 
9869
+ TextareaInput.prototype.focus = function () {
9870
+ if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) {
9871
+ try { this.textarea.focus() }
9872
+ catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM
9873
+ }
9874
+ };
9875
 
9876
+ TextareaInput.prototype.blur = function () { this.textarea.blur() };
 
 
 
9877
 
9878
+ TextareaInput.prototype.resetPosition = function () {
9879
+ this.wrapper.style.top = this.wrapper.style.left = 0
9880
+ };
 
 
 
9881
 
9882
+ TextareaInput.prototype.receivedFocus = function () { this.slowPoll() };
 
 
 
 
 
 
 
 
 
 
 
 
9883
 
9884
+ // Poll for input changes, using the normal rate of polling. This
9885
+ // runs as long as the editor is focused.
9886
+ TextareaInput.prototype.slowPoll = function () {
 
 
 
 
9887
  var this$1 = this;
9888
 
9889
+ if (this.pollingFast) { return }
9890
+ this.polling.set(this.cm.options.pollInterval, function () {
9891
+ this$1.poll()
9892
+ if (this$1.cm.state.focused) { this$1.slowPoll() }
9893
+ })
9894
+ };
9895
+
9896
+ // When an event has just come in that is likely to add or change
9897
+ // something in the input textarea, we poll faster, to ensure that
9898
+ // the change appears on the screen quickly.
9899
+ TextareaInput.prototype.fastPoll = function () {
9900
+ var missed = false, input = this
9901
+ input.pollingFast = true
9902
+ function p() {
9903
+ var changed = input.poll()
9904
+ if (!changed && !missed) {missed = true; input.polling.set(60, p)}
9905
+ else {input.pollingFast = false; input.slowPoll()}
9906
+ }
9907
+ input.polling.set(20, p)
9908
+ };
9909
+
9910
+ // Read input from the textarea, and update the document to match.
9911
+ // When something is selected, it is present in the textarea, and
9912
+ // selected (unless it is huge, in which case a placeholder is
9913
+ // used). When nothing is selected, the cursor sits after previously
9914
+ // seen text (can be empty), which is stored in prevInput (we must
9915
+ // not reset the textarea when typing, because that breaks IME).
9916
+ TextareaInput.prototype.poll = function () {
9917
+ var this$1 = this;
 
9918
 
9919
+ var cm = this.cm, input = this.textarea, prevInput = this.prevInput
9920
+ // Since this is called a *lot*, try to bail out as cheaply as
9921
+ // possible when it is clear that nothing happened. hasSelection
9922
+ // will be the case when there is a lot of text in the textarea,
9923
+ // in which case reading its value would be expensive.
9924
+ if (this.contextMenuPending || !cm.state.focused ||
9925
+ (hasSelection(input) && !prevInput && !this.composing) ||
9926
+ cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)
9927
+ { return false }
9928
 
9929
+ var text = input.value
9930
+ // If nothing changed, bail.
9931
+ if (text == prevInput && !cm.somethingSelected()) { return false }
9932
+ // Work around nonsensical selection resetting in IE9/10, and
9933
+ // inexplicable appearance of private area unicode characters on
9934
+ // some key combos in Mac (#2689).
9935
+ if (ie && ie_version >= 9 && this.hasSelection === text ||
9936
+ mac && /[\uf700-\uf7ff]/.test(text)) {
9937
+ cm.display.input.reset()
9938
+ return false
9939
+ }
9940
 
9941
+ if (cm.doc.sel == cm.display.selForContextMenu) {
9942
+ var first = text.charCodeAt(0)
9943
+ if (first == 0x200b && !prevInput) { prevInput = "\u200b" }
9944
+ if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") }
9945
+ }
9946
+ // Find the part of the input that is actually new
9947
+ var same = 0, l = Math.min(prevInput.length, text.length)
9948
+ while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) { ++same }
9949
 
9950
+ runInOp(cm, function () {
9951
+ applyTextInput(cm, text.slice(same), prevInput.length - same,
9952
+ null, this$1.composing ? "*compose" : null)
9953
 
9954
+ // Don't leave long text in the textarea, since it makes further polling slow
9955
+ if (text.length > 1000 || text.indexOf("\n") > -1) { input.value = this$1.prevInput = "" }
9956
+ else { this$1.prevInput = text }
 
9957
 
9958
+ if (this$1.composing) {
9959
+ this$1.composing.range.clear()
9960
+ this$1.composing.range = cm.markText(this$1.composing.start, cm.getCursor("to"),
9961
+ {className: "CodeMirror-composing"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9962
  }
9963
+ })
9964
+ return true
9965
+ };
9966
+
9967
+ TextareaInput.prototype.ensurePolled = function () {
9968
+ if (this.pollingFast && this.poll()) { this.pollingFast = false }
9969
+ };
9970
+
9971
+ TextareaInput.prototype.onKeyPress = function () {
9972
+ if (ie && ie_version >= 9) { this.hasSelection = null }
9973
+ this.fastPoll()
9974
+ };
9975
+
9976
+ TextareaInput.prototype.onContextMenu = function (e) {
9977
+ var input = this, cm = input.cm, display = cm.display, te = input.textarea
9978
+ var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop
9979
+ if (!pos || presto) { return } // Opera is difficult.
9980
+
9981
+ // Reset the current text selection only if the click is done outside of the selection
9982
+ // and 'resetSelectionOnContextMenu' option is true.
9983
+ var reset = cm.options.resetSelectionOnContextMenu
9984
+ if (reset && cm.doc.sel.contains(pos) == -1)
9985
+ { operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll) }
9986
+
9987
+ var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText
9988
+ input.wrapper.style.cssText = "position: absolute"
9989
+ var wrapperBox = input.wrapper.getBoundingClientRect()
9990
+ te.style.cssText = "position: absolute; width: 30px; height: 30px;\n top: " + (e.clientY - wrapperBox.top - 5) + "px; left: " + (e.clientX - wrapperBox.left - 5) + "px;\n z-index: 1000; background: " + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + ";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"
9991
+ var oldScrollY
9992
+ if (webkit) { oldScrollY = window.scrollY } // Work around Chrome issue (#2712)
9993
+ display.input.focus()
9994
+ if (webkit) { window.scrollTo(null, oldScrollY) }
9995
+ display.input.reset()
9996
+ // Adds "Select all" to context menu in FF
9997
+ if (!cm.somethingSelected()) { te.value = input.prevInput = " " }
9998
+ input.contextMenuPending = true
9999
+ display.selForContextMenu = cm.doc.sel
10000
+ clearTimeout(display.detectingSelectAll)
10001
+
10002
+ // Select-all will be greyed out if there's nothing to select, so
10003
+ // this adds a zero-width space so that we can later check whether
10004
+ // it got selected.
10005
+ function prepareSelectAllHack() {
10006
+ if (te.selectionStart != null) {
10007
+ var selected = cm.somethingSelected()
10008
+ var extval = "\u200b" + (selected ? te.value : "")
10009
+ te.value = "\u21da" // Used to catch context-menu undo
10010
+ te.value = extval
10011
+ input.prevInput = selected ? "" : "\u200b"
10012
+ te.selectionStart = 1; te.selectionEnd = extval.length
10013
+ // Re-set this, in case some other handler touched the
10014
+ // selection in the meantime.
10015
+ display.selForContextMenu = cm.doc.sel
10016
+ }
10017
+ }
10018
+ function rehide() {
10019
+ input.contextMenuPending = false
10020
+ input.wrapper.style.cssText = oldWrapperCSS
10021
+ te.style.cssText = oldCSS
10022
+ if (ie && ie_version < 9) { display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos) }
10023
+
10024
+ // Try to detect the user choosing select-all
10025
+ if (te.selectionStart != null) {
10026
+ if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack() }
10027
+ var i = 0, poll = function () {
10028
+ if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 &&
10029
+ te.selectionEnd > 0 && input.prevInput == "\u200b")
10030
+ { operation(cm, selectAll)(cm) }
10031
+ else if (i++ < 10) { display.detectingSelectAll = setTimeout(poll, 500) }
10032
+ else { display.input.reset() }
10033
  }
10034
+ display.detectingSelectAll = setTimeout(poll, 200)
10035
  }
10036
+ }
10037
 
10038
+ if (ie && ie_version >= 9) { prepareSelectAllHack() }
10039
+ if (captureRightClick) {
10040
+ e_stop(e)
10041
+ var mouseup = function () {
10042
+ off(window, "mouseup", mouseup)
10043
+ setTimeout(rehide, 20)
 
 
 
 
10044
  }
10045
+ on(window, "mouseup", mouseup)
10046
+ } else {
10047
+ setTimeout(rehide, 50)
10048
+ }
10049
+ };
10050
 
10051
+ TextareaInput.prototype.readOnlyChanged = function (val) {
10052
+ if (!val) { this.reset() }
10053
+ };
10054
 
10055
+ TextareaInput.prototype.setUneditable = function () {};
10056
 
10057
+ TextareaInput.prototype.needsContentAttribute = false
 
10058
 
10059
  function fromTextArea(textarea, options) {
10060
  options = options ? copyObj(options) : {}
10205
 
10206
  addLegacyProps(CodeMirror)
10207
 
10208
+ CodeMirror.version = "5.22.0"
10209
 
10210
  return CodeMirror;
10211
 
10742
  "transition-property", "transition-timing-function", "unicode-bidi",
10743
  "user-select", "vertical-align", "visibility", "voice-balance", "voice-duration",
10744
  "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
10745
+ "voice-volume", "volume", "white-space", "widows", "width", "will-change", "word-break",
10746
  "word-spacing", "word-wrap", "z-index",
10747
  // SVG-specific
10748
  "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color",
10816
  "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch",
10817
  "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
10818
  "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse",
10819
+ "compact", "condensed", "contain", "content", "contents",
10820
  "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop",
10821
  "cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
10822
  "decimal-leading-zero", "default", "default-button", "dense", "destination-atop",
10859
  "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize",
10860
  "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
10861
  "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
10862
+ "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "opacity", "open-quote",
10863
  "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
10864
  "outside", "outside-shape", "overlay", "overline", "padding", "padding-box",
10865
  "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter",
10871
  "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY",
10872
  "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running",
10873
  "s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
10874
+ "scroll", "scrollbar", "scroll-position", "se-resize", "searchfield",
10875
  "searchfield-cancel-button", "searchfield-decoration",
10876
  "searchfield-results-button", "searchfield-results-decoration",
10877
  "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama",
10889
  "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
10890
  "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
10891
  "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
10892
+ "trad-chinese-formal", "trad-chinese-informal", "transform",
10893
  "translate", "translate3d", "translateX", "translateY", "translateZ",
10894
+ "transparent", "ultra-condensed", "ultra-expanded", "underline", "unset", "up",
10895
  "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
10896
  "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
10897
  "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
assets/js/mailpoet.js CHANGED
@@ -17048,9 +17048,7 @@ webpackJsonp([2],[
17048
  var convertedFormat = [];
17049
  var escapeToken = false;
17050
 
17051
- for (var index in format) {
17052
- var token = format[index];
17053
-
17054
  if (escapeToken === true) {
17055
  convertedFormat.push('['+token+']');
17056
  escapeToken = false;
17048
  var convertedFormat = [];
17049
  var escapeToken = false;
17050
 
17051
+ for(var index = 0, token = ''; token = format.charAt(index); index++){
 
 
17052
  if (escapeToken === true) {
17053
  convertedFormat.push('['+token+']');
17054
  escapeToken = false;
assets/js/newsletter_editor.js CHANGED
@@ -14624,7 +14624,7 @@ webpackJsonp([3],{
14624
  /***/ function(module, exports, __webpack_require__) {
14625
 
14626
  /**
14627
- * interact.js v1.2.6
14628
  *
14629
  * Copyright (c) 2012-2015 Taye Adeyemi <dev@taye.me>
14630
  * Open source under the MIT License.
@@ -14869,7 +14869,8 @@ webpackJsonp([3],{
14869
  supportsTouch = (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch),
14870
 
14871
  // Does the browser support PointerEvents
14872
- supportsPointerEvent = !!PointerEvent,
 
14873
 
14874
  // Less Precision with touch input
14875
  margin = supportsTouch || supportsPointerEvent? 20: 10,
@@ -16002,14 +16003,14 @@ webpackJsonp([3],{
16002
 
16003
  this.pointerHover(pointer, event, this.matches, this.matchElements);
16004
  events.add(eventTarget,
16005
- PointerEvent? pEventTypes.move : 'mousemove',
16006
  listeners.pointerHover);
16007
  }
16008
  else if (this.target) {
16009
  if (nodeContains(prevTargetElement, eventTarget)) {
16010
  this.pointerHover(pointer, event, this.matches, this.matchElements);
16011
  events.add(this.element,
16012
- PointerEvent? pEventTypes.move : 'mousemove',
16013
  listeners.pointerHover);
16014
  }
16015
  else {
@@ -16061,7 +16062,7 @@ webpackJsonp([3],{
16061
  // Remove temporary event listeners for selector Interactables
16062
  if (!interactables.get(eventTarget)) {
16063
  events.remove(eventTarget,
16064
- PointerEvent? pEventTypes.move : 'mousemove',
16065
  listeners.pointerHover);
16066
  }
16067
 
@@ -16373,7 +16374,7 @@ webpackJsonp([3],{
16373
 
16374
  // set the startCoords if there was no prepared action
16375
  if (!this.prepared.name) {
16376
- this.setEventXY(this.startCoords);
16377
  }
16378
 
16379
  this.prepared.name = action.name;
@@ -18555,7 +18556,7 @@ webpackJsonp([3],{
18555
 
18556
  if (isElement(element, _window)) {
18557
 
18558
- if (PointerEvent) {
18559
  events.add(this._element, pEventTypes.down, listeners.pointerDown );
18560
  events.add(this._element, pEventTypes.move, listeners.pointerHover);
18561
  }
@@ -20400,7 +20401,7 @@ webpackJsonp([3],{
20400
  events.add(doc, eventType, delegateUseCapture, true);
20401
  }
20402
 
20403
- if (PointerEvent) {
20404
  if (PointerEvent === win.MSPointerEvent) {
20405
  pEventTypes = {
20406
  up: 'MSPointerUp', down: 'MSPointerDown', over: 'mouseover',
@@ -28413,19 +28414,25 @@ webpackJsonp([3],{
28413
  // send test email
28414
  MailPoet.Modal.loading(true);
28415
 
28416
- CommunicationComponent.previewNewsletter(data).always(function() {
28417
- MailPoet.Modal.loading(false);
28418
- }).done(function(response) {
28419
- MailPoet.Notice.success(
28420
- MailPoet.I18n.t('newsletterPreviewSent'),
28421
- { scroll: true });
28422
- }).fail(function(response) {
28423
- if (response.errors.length > 0) {
28424
- MailPoet.Notice.error(
28425
- response.errors.map(function(error) { return error.message; }),
28426
- { scroll: true, static: true }
28427
- );
28428
- }
 
 
 
 
 
 
28429
  });
28430
  },
28431
  });
@@ -28767,7 +28774,7 @@ webpackJsonp([3],{
28767
  App.getChannel().trigger('beforeEditorSave', json);
28768
 
28769
  // save newsletter
28770
- CommunicationComponent.saveNewsletter(json).done(function(response) {
28771
  if(response.success !== undefined && response.success === true) {
28772
  // TODO: Handle translations
28773
  //MailPoet.Notice.success("<?php _e('Newsletter has been saved.'); ?>");
@@ -28793,6 +28800,14 @@ webpackJsonp([3],{
28793
  });
28794
  };
28795
 
 
 
 
 
 
 
 
 
28796
  Module.getThumbnail = function(element, options) {
28797
  var promise = html2canvas(element, options || {});
28798
 
@@ -29062,12 +29077,12 @@ webpackJsonp([3],{
29062
  };
29063
 
29064
  App.on('before:start', function(options) {
29065
- App.save = Module.save;
29066
  App.getChannel().on('autoSave', Module.autoSave);
29067
 
29068
  window.onbeforeunload = Module.beforeExitWithUnsavedChanges;
29069
 
29070
- App.getChannel().on('save', function() { App.save(); });
29071
  });
29072
 
29073
  App.on('start', function(options) {
14624
  /***/ function(module, exports, __webpack_require__) {
14625
 
14626
  /**
14627
+ * interact.js v1.2.8
14628
  *
14629
  * Copyright (c) 2012-2015 Taye Adeyemi <dev@taye.me>
14630
  * Open source under the MIT License.
14869
  supportsTouch = (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch),
14870
 
14871
  // Does the browser support PointerEvents
14872
+ // Avoid PointerEvent bugs introduced in Chrome 55
14873
+ supportsPointerEvent = PointerEvent && !/Chrome/.test(navigator.userAgent),
14874
 
14875
  // Less Precision with touch input
14876
  margin = supportsTouch || supportsPointerEvent? 20: 10,
16003
 
16004
  this.pointerHover(pointer, event, this.matches, this.matchElements);
16005
  events.add(eventTarget,
16006
+ supportsPointerEvent? pEventTypes.move : 'mousemove',
16007
  listeners.pointerHover);
16008
  }
16009
  else if (this.target) {
16010
  if (nodeContains(prevTargetElement, eventTarget)) {
16011
  this.pointerHover(pointer, event, this.matches, this.matchElements);
16012
  events.add(this.element,
16013
+ supportsPointerEvent? pEventTypes.move : 'mousemove',
16014
  listeners.pointerHover);
16015
  }
16016
  else {
16062
  // Remove temporary event listeners for selector Interactables
16063
  if (!interactables.get(eventTarget)) {
16064
  events.remove(eventTarget,
16065
+ supportsPointerEvent? pEventTypes.move : 'mousemove',
16066
  listeners.pointerHover);
16067
  }
16068
 
16374
 
16375
  // set the startCoords if there was no prepared action
16376
  if (!this.prepared.name) {
16377
+ this.setEventXY(this.startCoords, this.pointers);
16378
  }
16379
 
16380
  this.prepared.name = action.name;
18556
 
18557
  if (isElement(element, _window)) {
18558
 
18559
+ if (supportsPointerEvent) {
18560
  events.add(this._element, pEventTypes.down, listeners.pointerDown );
18561
  events.add(this._element, pEventTypes.move, listeners.pointerHover);
18562
  }
20401
  events.add(doc, eventType, delegateUseCapture, true);
20402
  }
20403
 
20404
+ if (supportsPointerEvent) {
20405
  if (PointerEvent === win.MSPointerEvent) {
20406
  pEventTypes = {
20407
  up: 'MSPointerUp', down: 'MSPointerDown', over: 'mouseover',
28414
  // send test email
28415
  MailPoet.Modal.loading(true);
28416
 
28417
+ // save before sending
28418
+ var saveResult = {promise: null};
28419
+ App.getChannel().trigger('save', saveResult);
28420
+
28421
+ saveResult.promise.always(function() {
28422
+ CommunicationComponent.previewNewsletter(data).always(function() {
28423
+ MailPoet.Modal.loading(false);
28424
+ }).done(function(response) {
28425
+ MailPoet.Notice.success(
28426
+ MailPoet.I18n.t('newsletterPreviewSent'),
28427
+ { scroll: true });
28428
+ }).fail(function(response) {
28429
+ if (response.errors.length > 0) {
28430
+ MailPoet.Notice.error(
28431
+ response.errors.map(function(error) { return error.message; }),
28432
+ { scroll: true, static: true }
28433
+ );
28434
+ }
28435
+ });
28436
  });
28437
  },
28438
  });
28774
  App.getChannel().trigger('beforeEditorSave', json);
28775
 
28776
  // save newsletter
28777
+ return CommunicationComponent.saveNewsletter(json).done(function(response) {
28778
  if(response.success !== undefined && response.success === true) {
28779
  // TODO: Handle translations
28780
  //MailPoet.Notice.success("<?php _e('Newsletter has been saved.'); ?>");
28800
  });
28801
  };
28802
 
28803
+ // For getting a promise after triggering save event
28804
+ Module.saveAndProvidePromise = function(saveResult) {
28805
+ var promise = Module.save();
28806
+ if (saveResult !== undefined) {
28807
+ saveResult.promise = promise;
28808
+ }
28809
+ };
28810
+
28811
  Module.getThumbnail = function(element, options) {
28812
  var promise = html2canvas(element, options || {});
28813
 
29077
  };
29078
 
29079
  App.on('before:start', function(options) {
29080
+ App.save = Module.saveAndProvidePromise;
29081
  App.getChannel().on('autoSave', Module.autoSave);
29082
 
29083
  window.onbeforeunload = Module.beforeExitWithUnsavedChanges;
29084
 
29085
+ App.getChannel().on('save', function(saveResult) { App.save(saveResult); });
29086
  });
29087
 
29088
  App.on('start', function(options) {
lang/index.php CHANGED
@@ -1,3 +0,0 @@
1
- <?php
2
-
3
- // Silence is golden
 
 
 
lang/mailpoet.pot CHANGED
@@ -4,7 +4,7 @@ msgid ""
4
  msgstr ""
5
  "Project-Id-Version: \n"
6
  "Report-Msgid-Bugs-To: http://support.mailpoet.com/\n"
7
- "POT-Creation-Date: 2016-12-20 16:24:46+00:00\n"
8
  "MIME-Version: 1.0\n"
9
  "Content-Type: text/plain; charset=utf-8\n"
10
  "Content-Transfer-Encoding: 8bit\n"
@@ -79,7 +79,7 @@ msgstr ""
79
  msgid "Copy of %s"
80
  msgstr ""
81
 
82
- #: lib/API/Endpoints/Mailer.php:26 lib/API/Endpoints/Newsletters.php:283
83
  msgid "The email could not be sent: %s"
84
  msgstr ""
85
 
@@ -91,7 +91,7 @@ msgstr ""
91
  #: lib/API/Endpoints/Newsletters.php:28 lib/API/Endpoints/Newsletters.php:117
92
  #: lib/API/Endpoints/Newsletters.php:138 lib/API/Endpoints/Newsletters.php:154
93
  #: lib/API/Endpoints/Newsletters.php:170 lib/API/Endpoints/Newsletters.php:184
94
- #: lib/API/Endpoints/Newsletters.php:216 lib/API/Endpoints/Newsletters.php:245
95
  #: lib/API/Endpoints/SendingQueue.php:32 lib/API/Endpoints/SendingQueue.php:122
96
  #: lib/API/Endpoints/SendingQueue.php:148
97
  msgid "This newsletter does not exist."
@@ -105,7 +105,7 @@ msgstr ""
105
  msgid "Newsletter data is missing."
106
  msgstr ""
107
 
108
- #: lib/API/Endpoints/Newsletters.php:236
109
  msgid "Please specify receiver information."
110
  msgstr ""
111
 
@@ -959,11 +959,11 @@ msgid ""
959
  "fix this issue."
960
  msgstr ""
961
 
962
- #: lib/Config/Shortcodes.php:84
963
  msgid "Oops! There are no newsletters to display."
964
  msgstr ""
965
 
966
- #: lib/Config/Shortcodes.php:119
967
  msgid "Preview in a new tab"
968
  msgstr ""
969
 
@@ -1132,15 +1132,15 @@ msgstr ""
1132
  msgid "Create a new form"
1133
  msgstr ""
1134
 
1135
- #: lib/Mailer/Mailer.php:78
1136
  msgid "Mailing method does not exist"
1137
  msgstr ""
1138
 
1139
- #: lib/Mailer/Mailer.php:86
1140
  msgid "Mailer is not configured"
1141
  msgstr ""
1142
 
1143
- #: lib/Mailer/Mailer.php:103
1144
  msgid "Sender name and email are not configured"
1145
  msgstr ""
1146
 
@@ -1156,12 +1156,12 @@ msgstr ""
1156
  msgid "Sending frequency limit has been reached."
1157
  msgstr ""
1158
 
1159
- #: lib/Mailer/Methods/AmazonSES.php:33
1160
  msgid "Unsupported Amazon SES region."
1161
  msgstr ""
1162
 
1163
- #: lib/Mailer/Methods/AmazonSES.php:60 lib/Mailer/Methods/PHPMail.php:29
1164
- #: lib/Mailer/Methods/SMTP.php:43 lib/Mailer/Methods/SendGrid.php:32
1165
  msgid "%s has returned an unknown error."
1166
  msgstr ""
1167
 
@@ -1173,18 +1173,18 @@ msgstr ""
1173
  msgid "Please specify a name"
1174
  msgstr ""
1175
 
1176
- #: lib/Models/CustomField.php:17 lib/Models/Newsletter.php:26
1177
  #: views/form/templates/settings/field_form.hbs:16
1178
  msgid "Please specify a type"
1179
  msgstr ""
1180
 
1181
- #: lib/Models/Form.php:50 lib/Models/Newsletter.php:411
1182
- #: lib/Models/Segment.php:126 lib/Models/Subscriber.php:328
1183
  msgid "All"
1184
  msgstr ""
1185
 
1186
- #: lib/Models/Form.php:55 lib/Models/Newsletter.php:481
1187
- #: lib/Models/Segment.php:131 lib/Models/Subscriber.php:353 views/forms.html:56
1188
  #: views/newsletters.html:75 views/segments.html:50
1189
  #: views/subscribers/subscribers.html:34
1190
  msgid "Trash"
@@ -1194,34 +1194,38 @@ msgstr ""
1194
  msgid "Another record already exists. Please specify a different \"%1$s\"."
1195
  msgstr ""
1196
 
1197
- #: lib/Models/Newsletter.php:312 lib/Models/Subscriber.php:266
 
 
 
 
1198
  #: lib/Subscribers/ImportExport/Export/Export.php:170
1199
  msgid "All Lists"
1200
  msgstr ""
1201
 
1202
- #: lib/Models/Newsletter.php:423
1203
  #: views/newsletter/templates/blocks/posts/settingsSelection.hbs:12
1204
  msgid "Draft"
1205
  msgstr ""
1206
 
1207
- #: lib/Models/Newsletter.php:431
1208
  #: views/newsletter/templates/blocks/posts/settingsSelection.hbs:11
1209
  msgid "Scheduled"
1210
  msgstr ""
1211
 
1212
- #: lib/Models/Newsletter.php:439
1213
  msgid "Sending"
1214
  msgstr ""
1215
 
1216
- #: lib/Models/Newsletter.php:447
1217
  msgid "Sent"
1218
  msgstr ""
1219
 
1220
- #: lib/Models/Newsletter.php:461 views/newsletters.html:82
1221
  msgid "Active"
1222
  msgstr ""
1223
 
1224
- #: lib/Models/Newsletter.php:469
1225
  msgid "Not active"
1226
  msgstr ""
1227
 
@@ -1273,30 +1277,30 @@ msgstr ""
1273
  msgid "Your email address is invalid!"
1274
  msgstr ""
1275
 
1276
- #: lib/Models/Subscriber.php:203
1277
  msgid "You need to wait before subscribing again."
1278
  msgstr ""
1279
 
1280
- #: lib/Models/Subscriber.php:272
1281
  msgid "Subscribers without a list (%s)"
1282
  msgstr ""
1283
 
1284
- #: lib/Models/Subscriber.php:333 lib/Subscription/Pages.php:280
1285
  #: views/segments.html:30 views/subscribers/subscribers.html:51
1286
  msgid "Subscribed"
1287
  msgstr ""
1288
 
1289
- #: lib/Models/Subscriber.php:338 views/segments.html:31
1290
  #: views/subscribers/subscribers.html:50
1291
  msgid "Unconfirmed"
1292
  msgstr ""
1293
 
1294
- #: lib/Models/Subscriber.php:343 lib/Subscription/Pages.php:288
1295
  #: views/segments.html:32 views/subscribers/subscribers.html:52
1296
  msgid "Unsubscribed"
1297
  msgstr ""
1298
 
1299
- #: lib/Models/Subscriber.php:348 lib/Subscription/Pages.php:296
1300
  #: views/segments.html:33 views/subscribers/subscribers.html:53
1301
  msgid "Bounced"
1302
  msgstr ""
@@ -1305,17 +1309,17 @@ msgstr ""
1305
  msgid "Click here to view media."
1306
  msgstr ""
1307
 
1308
- #: lib/Newsletter/Shortcodes/Categories/Link.php:27
1309
  #: views/newsletter/editor.html:1037
1310
  msgid "Unsubscribe"
1311
  msgstr ""
1312
 
1313
- #: lib/Newsletter/Shortcodes/Categories/Link.php:46
1314
  #: views/newsletter/editor.html:1037
1315
  msgid "Manage subscription"
1316
  msgstr ""
1317
 
1318
- #: lib/Newsletter/Shortcodes/Categories/Link.php:63
1319
  msgid "View in your browser"
1320
  msgstr ""
1321
 
@@ -1546,7 +1550,7 @@ msgstr ""
1546
  msgid "You are now subscribed!"
1547
  msgstr ""
1548
 
1549
- #: lib/Subscription/Pages.php:177 lib/Subscription/Pages.php:391
1550
  msgid "Manage your subscription"
1551
  msgstr ""
1552
 
@@ -1558,31 +1562,31 @@ msgstr ""
1558
  msgid "Yup, we've added you to our email list. You'll hear from us shortly."
1559
  msgstr ""
1560
 
1561
- #: lib/Subscription/Pages.php:315
1562
  msgid "Your lists"
1563
  msgstr ""
1564
 
1565
- #: lib/Subscription/Pages.php:323 views/form/editor.html:29
1566
  #: views/form/editor.html:383 views/newsletter/templates/components/save.hbs:3
1567
  #: views/segments.html:55 views/subscribers/subscribers.html:91
1568
  msgid "Save"
1569
  msgstr ""
1570
 
1571
- #: lib/Subscription/Pages.php:355
1572
  msgid "[link]Edit your profile[/link] to update your email."
1573
  msgstr ""
1574
 
1575
- #: lib/Subscription/Pages.php:361
1576
  msgid "[link]Log in to your account[/link] to update your email."
1577
  msgstr ""
1578
 
1579
- #: lib/Subscription/Pages.php:365
1580
  msgid ""
1581
  "Need to change your email address? Unsubscribe here, then simply sign up "
1582
  "again."
1583
  msgstr ""
1584
 
1585
- #: lib/Subscription/Pages.php:379
1586
  msgid "Accidentally unsubscribed?"
1587
  msgstr ""
1588
 
4
  msgstr ""
5
  "Project-Id-Version: \n"
6
  "Report-Msgid-Bugs-To: http://support.mailpoet.com/\n"
7
+ "POT-Creation-Date: 2016-12-27 22:02:35+00:00\n"
8
  "MIME-Version: 1.0\n"
9
  "Content-Type: text/plain; charset=utf-8\n"
10
  "Content-Transfer-Encoding: 8bit\n"
79
  msgid "Copy of %s"
80
  msgstr ""
81
 
82
+ #: lib/API/Endpoints/Mailer.php:26 lib/API/Endpoints/Newsletters.php:285
83
  msgid "The email could not be sent: %s"
84
  msgstr ""
85
 
91
  #: lib/API/Endpoints/Newsletters.php:28 lib/API/Endpoints/Newsletters.php:117
92
  #: lib/API/Endpoints/Newsletters.php:138 lib/API/Endpoints/Newsletters.php:154
93
  #: lib/API/Endpoints/Newsletters.php:170 lib/API/Endpoints/Newsletters.php:184
94
+ #: lib/API/Endpoints/Newsletters.php:216 lib/API/Endpoints/Newsletters.php:247
95
  #: lib/API/Endpoints/SendingQueue.php:32 lib/API/Endpoints/SendingQueue.php:122
96
  #: lib/API/Endpoints/SendingQueue.php:148
97
  msgid "This newsletter does not exist."
105
  msgid "Newsletter data is missing."
106
  msgstr ""
107
 
108
+ #: lib/API/Endpoints/Newsletters.php:238
109
  msgid "Please specify receiver information."
110
  msgstr ""
111
 
959
  "fix this issue."
960
  msgstr ""
961
 
962
+ #: lib/Config/Shortcodes.php:85
963
  msgid "Oops! There are no newsletters to display."
964
  msgstr ""
965
 
966
+ #: lib/Config/Shortcodes.php:125
967
  msgid "Preview in a new tab"
968
  msgstr ""
969
 
1132
  msgid "Create a new form"
1133
  msgstr ""
1134
 
1135
+ #: lib/Mailer/Mailer.php:83
1136
  msgid "Mailing method does not exist"
1137
  msgstr ""
1138
 
1139
+ #: lib/Mailer/Mailer.php:91
1140
  msgid "Mailer is not configured"
1141
  msgstr ""
1142
 
1143
+ #: lib/Mailer/Mailer.php:108
1144
  msgid "Sender name and email are not configured"
1145
  msgstr ""
1146
 
1156
  msgid "Sending frequency limit has been reached."
1157
  msgstr ""
1158
 
1159
+ #: lib/Mailer/Methods/AmazonSES.php:34
1160
  msgid "Unsupported Amazon SES region."
1161
  msgstr ""
1162
 
1163
+ #: lib/Mailer/Methods/AmazonSES.php:63 lib/Mailer/Methods/PHPMail.php:33
1164
+ #: lib/Mailer/Methods/SMTP.php:47 lib/Mailer/Methods/SendGrid.php:32
1165
  msgid "%s has returned an unknown error."
1166
  msgstr ""
1167
 
1173
  msgid "Please specify a name"
1174
  msgstr ""
1175
 
1176
+ #: lib/Models/CustomField.php:17 lib/Models/Newsletter.php:27
1177
  #: views/form/templates/settings/field_form.hbs:16
1178
  msgid "Please specify a type"
1179
  msgstr ""
1180
 
1181
+ #: lib/Models/Form.php:50 lib/Models/Newsletter.php:459
1182
+ #: lib/Models/Segment.php:126 lib/Models/Subscriber.php:332
1183
  msgid "All"
1184
  msgstr ""
1185
 
1186
+ #: lib/Models/Form.php:55 lib/Models/Newsletter.php:529
1187
+ #: lib/Models/Segment.php:131 lib/Models/Subscriber.php:357 views/forms.html:56
1188
  #: views/newsletters.html:75 views/segments.html:50
1189
  #: views/subscribers/subscribers.html:34
1190
  msgid "Trash"
1194
  msgid "Another record already exists. Please specify a different \"%1$s\"."
1195
  msgstr ""
1196
 
1197
+ #: lib/Models/Newsletter.php:229
1198
+ msgid "Deleted list"
1199
+ msgstr ""
1200
+
1201
+ #: lib/Models/Newsletter.php:360 lib/Models/Subscriber.php:270
1202
  #: lib/Subscribers/ImportExport/Export/Export.php:170
1203
  msgid "All Lists"
1204
  msgstr ""
1205
 
1206
+ #: lib/Models/Newsletter.php:471
1207
  #: views/newsletter/templates/blocks/posts/settingsSelection.hbs:12
1208
  msgid "Draft"
1209
  msgstr ""
1210
 
1211
+ #: lib/Models/Newsletter.php:479
1212
  #: views/newsletter/templates/blocks/posts/settingsSelection.hbs:11
1213
  msgid "Scheduled"
1214
  msgstr ""
1215
 
1216
+ #: lib/Models/Newsletter.php:487
1217
  msgid "Sending"
1218
  msgstr ""
1219
 
1220
+ #: lib/Models/Newsletter.php:495
1221
  msgid "Sent"
1222
  msgstr ""
1223
 
1224
+ #: lib/Models/Newsletter.php:509 views/newsletters.html:82
1225
  msgid "Active"
1226
  msgstr ""
1227
 
1228
+ #: lib/Models/Newsletter.php:517
1229
  msgid "Not active"
1230
  msgstr ""
1231
 
1277
  msgid "Your email address is invalid!"
1278
  msgstr ""
1279
 
1280
+ #: lib/Models/Subscriber.php:207
1281
  msgid "You need to wait before subscribing again."
1282
  msgstr ""
1283
 
1284
+ #: lib/Models/Subscriber.php:276
1285
  msgid "Subscribers without a list (%s)"
1286
  msgstr ""
1287
 
1288
+ #: lib/Models/Subscriber.php:337 lib/Subscription/Pages.php:280
1289
  #: views/segments.html:30 views/subscribers/subscribers.html:51
1290
  msgid "Subscribed"
1291
  msgstr ""
1292
 
1293
+ #: lib/Models/Subscriber.php:342 views/segments.html:31
1294
  #: views/subscribers/subscribers.html:50
1295
  msgid "Unconfirmed"
1296
  msgstr ""
1297
 
1298
+ #: lib/Models/Subscriber.php:347 lib/Subscription/Pages.php:288
1299
  #: views/segments.html:32 views/subscribers/subscribers.html:52
1300
  msgid "Unsubscribed"
1301
  msgstr ""
1302
 
1303
+ #: lib/Models/Subscriber.php:352 lib/Subscription/Pages.php:296
1304
  #: views/segments.html:33 views/subscribers/subscribers.html:53
1305
  msgid "Bounced"
1306
  msgstr ""
1309
  msgid "Click here to view media."
1310
  msgstr ""
1311
 
1312
+ #: lib/Newsletter/Shortcodes/Categories/Link.php:31
1313
  #: views/newsletter/editor.html:1037
1314
  msgid "Unsubscribe"
1315
  msgstr ""
1316
 
1317
+ #: lib/Newsletter/Shortcodes/Categories/Link.php:52
1318
  #: views/newsletter/editor.html:1037
1319
  msgid "Manage subscription"
1320
  msgstr ""
1321
 
1322
+ #: lib/Newsletter/Shortcodes/Categories/Link.php:76
1323
  msgid "View in your browser"
1324
  msgstr ""
1325
 
1550
  msgid "You are now subscribed!"
1551
  msgstr ""
1552
 
1553
+ #: lib/Subscription/Pages.php:177 lib/Subscription/Pages.php:395
1554
  msgid "Manage your subscription"
1555
  msgstr ""
1556
 
1562
  msgid "Yup, we've added you to our email list. You'll hear from us shortly."
1563
  msgstr ""
1564
 
1565
+ #: lib/Subscription/Pages.php:319
1566
  msgid "Your lists"
1567
  msgstr ""
1568
 
1569
+ #: lib/Subscription/Pages.php:327 views/form/editor.html:29
1570
  #: views/form/editor.html:383 views/newsletter/templates/components/save.hbs:3
1571
  #: views/segments.html:55 views/subscribers/subscribers.html:91
1572
  msgid "Save"
1573
  msgstr ""
1574
 
1575
+ #: lib/Subscription/Pages.php:359
1576
  msgid "[link]Edit your profile[/link] to update your email."
1577
  msgstr ""
1578
 
1579
+ #: lib/Subscription/Pages.php:365
1580
  msgid "[link]Log in to your account[/link] to update your email."
1581
  msgstr ""
1582
 
1583
+ #: lib/Subscription/Pages.php:369
1584
  msgid ""
1585
  "Need to change your email address? Unsubscribe here, then simply sign up "
1586
  "again."
1587
  msgstr ""
1588
 
1589
+ #: lib/Subscription/Pages.php:383
1590
  msgid "Accidentally unsubscribed?"
1591
  msgstr ""
1592
 
lib/API/Endpoints/Newsletters.php CHANGED
@@ -220,7 +220,9 @@ class Newsletters extends APIEndpoint {
220
  $newsletter->save();
221
  $subscriber = Subscriber::getCurrentWPUser();
222
  $preview_url = NewsletterUrl::getViewInBrowserUrl(
223
- $data, $subscriber, $queue = false, $preview = true
 
 
224
  );
225
 
226
  return $this->successResponse(
@@ -310,7 +312,7 @@ class Newsletters extends APIEndpoint {
310
 
311
  if($newsletter->type === Newsletter::TYPE_STANDARD) {
312
  $newsletter
313
- ->withSegments()
314
  ->withSendingQueue()
315
  ->withStatistics();
316
  } else if($newsletter->type === Newsletter::TYPE_WELCOME) {
@@ -321,11 +323,11 @@ class Newsletters extends APIEndpoint {
321
  } else if($newsletter->type === Newsletter::TYPE_NOTIFICATION) {
322
  $newsletter
323
  ->withOptions()
324
- ->withSegments()
325
  ->withChildrenCount();
326
  } else if($newsletter->type === Newsletter::TYPE_NOTIFICATION_HISTORY) {
327
  $newsletter
328
- ->withSegments()
329
  ->withSendingQueue()
330
  ->withStatistics();
331
  }
@@ -339,7 +341,11 @@ class Newsletters extends APIEndpoint {
339
  // get preview url
340
  $subscriber = Subscriber::getCurrentWPUser();
341
  $newsletter->preview_url = NewsletterUrl::getViewInBrowserUrl(
342
- $newsletter, $subscriber, $queue, $preview = true);
 
 
 
 
343
 
344
  $data[] = $newsletter->asArray();
345
  }
220
  $newsletter->save();
221
  $subscriber = Subscriber::getCurrentWPUser();
222
  $preview_url = NewsletterUrl::getViewInBrowserUrl(
223
+ NewsletterUrl::TYPE_LISTING_EDITOR,
224
+ $newsletter,
225
+ $subscriber
226
  );
227
 
228
  return $this->successResponse(
312
 
313
  if($newsletter->type === Newsletter::TYPE_STANDARD) {
314
  $newsletter
315
+ ->withSegments(true)
316
  ->withSendingQueue()
317
  ->withStatistics();
318
  } else if($newsletter->type === Newsletter::TYPE_WELCOME) {
323
  } else if($newsletter->type === Newsletter::TYPE_NOTIFICATION) {
324
  $newsletter
325
  ->withOptions()
326
+ ->withSegments(true)
327
  ->withChildrenCount();
328
  } else if($newsletter->type === Newsletter::TYPE_NOTIFICATION_HISTORY) {
329
  $newsletter
330
+ ->withSegments(true)
331
  ->withSendingQueue()
332
  ->withStatistics();
333
  }
341
  // get preview url
342
  $subscriber = Subscriber::getCurrentWPUser();
343
  $newsletter->preview_url = NewsletterUrl::getViewInBrowserUrl(
344
+ NewsletterUrl::TYPE_LISTING_EDITOR,
345
+ $newsletter,
346
+ $subscriber,
347
+ $queue
348
+ );
349
 
350
  $data[] = $newsletter->asArray();
351
  }
lib/Config/Env.php CHANGED
@@ -29,6 +29,7 @@ class Env {
29
  static $db_password;
30
  static $db_charset;
31
  static $db_timezone_offset;
 
32
 
33
  static function init($file, $version) {
34
  global $wpdb;
29
  static $db_password;
30
  static $db_charset;
31
  static $db_timezone_offset;
32
+ static $required_permission = 'manage_options';
33
 
34
  static function init($file, $version) {
35
  global $wpdb;
lib/Config/Initializer.php CHANGED
@@ -53,8 +53,7 @@ class Initializer {
53
  \ORM::configure('logging', WP_DEBUG);
54
  \ORM::configure('driver_options', array(
55
  \PDO::MYSQL_ATTR_INIT_COMMAND =>
56
- 'SET NAMES utf8, ' .
57
- 'TIME_ZONE = "' . Env::$db_timezone_offset . '", ' .
58
  'sql_mode=(SELECT REPLACE(@@sql_mode,"ONLY_FULL_GROUP_BY",""))'
59
  ));
60
 
53
  \ORM::configure('logging', WP_DEBUG);
54
  \ORM::configure('driver_options', array(
55
  \PDO::MYSQL_ATTR_INIT_COMMAND =>
56
+ 'SET TIME_ZONE = "' . Env::$db_timezone_offset . '", ' .
 
57
  'sql_mode=(SELECT REPLACE(@@sql_mode,"ONLY_FULL_GROUP_BY",""))'
58
  ));
59
 
lib/Config/Menu.php CHANGED
@@ -45,7 +45,7 @@ class Menu {
45
  add_menu_page(
46
  'MailPoet',
47
  'MailPoet',
48
- 'manage_options',
49
  $main_page_slug,
50
  null,
51
  $this->assets_url . '/img/menu_icon.png',
@@ -56,7 +56,7 @@ class Menu {
56
  $main_page_slug,
57
  $this->setPageTitle(__('Emails', 'mailpoet')),
58
  __('Emails', 'mailpoet'),
59
- 'manage_options',
60
  $main_page_slug,
61
  array($this, 'newsletters')
62
  );
@@ -76,7 +76,7 @@ class Menu {
76
  $main_page_slug,
77
  $this->setPageTitle(__('Forms', 'mailpoet')),
78
  __('Forms', 'mailpoet'),
79
- 'manage_options',
80
  'mailpoet-forms',
81
  array($this, 'forms')
82
  );
@@ -95,7 +95,7 @@ class Menu {
95
  $main_page_slug,
96
  $this->setPageTitle(__('Subscribers', 'mailpoet')),
97
  __('Subscribers', 'mailpoet'),
98
- 'manage_options',
99
  'mailpoet-subscribers',
100
  array($this, 'subscribers')
101
  );
@@ -114,7 +114,7 @@ class Menu {
114
  $main_page_slug,
115
  $this->setPageTitle(__('Lists', 'mailpoet')),
116
  __('Lists', 'mailpoet'),
117
- 'manage_options',
118
  'mailpoet-segments',
119
  array($this, 'segments')
120
  );
@@ -134,7 +134,7 @@ class Menu {
134
  $main_page_slug,
135
  $this->setPageTitle( __('Settings', 'mailpoet')),
136
  __('Settings', 'mailpoet'),
137
- 'manage_options',
138
  'mailpoet-settings',
139
  array($this, 'settings')
140
  );
@@ -142,7 +142,7 @@ class Menu {
142
  'admin.php?page=mailpoet-subscribers',
143
  $this->setPageTitle( __('Import', 'mailpoet')),
144
  __('Import', 'mailpoet'),
145
- 'manage_options',
146
  'mailpoet-import',
147
  array($this, 'import')
148
  );
@@ -151,7 +151,7 @@ class Menu {
151
  true,
152
  $this->setPageTitle(__('Export', 'mailpoet')),
153
  __('Export', 'mailpoet'),
154
- 'manage_options',
155
  'mailpoet-export',
156
  array($this, 'export')
157
  );
@@ -160,7 +160,7 @@ class Menu {
160
  true,
161
  $this->setPageTitle(__('Welcome', 'mailpoet')),
162
  __('Welcome', 'mailpoet'),
163
- 'manage_options',
164
  'mailpoet-welcome',
165
  array($this, 'welcome')
166
  );
@@ -169,7 +169,7 @@ class Menu {
169
  true,
170
  $this->setPageTitle(__('Update', 'mailpoet')),
171
  __('Update', 'mailpoet'),
172
- 'manage_options',
173
  'mailpoet-update',
174
  array($this, 'update')
175
  );
@@ -178,7 +178,7 @@ class Menu {
178
  true,
179
  $this->setPageTitle(__('Form Editor', 'mailpoet')),
180
  __('Form Editor', 'mailpoet'),
181
- 'manage_options',
182
  'mailpoet-form-editor',
183
  array($this, 'formEditor')
184
  );
@@ -187,7 +187,7 @@ class Menu {
187
  true,
188
  $this->setPageTitle(__('Newsletter', 'mailpoet')),
189
  __('Newsletter Editor', 'mailpoet'),
190
- 'manage_options',
191
  'mailpoet-newsletter-editor',
192
  array($this, 'newletterEditor')
193
  );
45
  add_menu_page(
46
  'MailPoet',
47
  'MailPoet',
48
+ Env::$required_permission,
49
  $main_page_slug,
50
  null,
51
  $this->assets_url . '/img/menu_icon.png',
56
  $main_page_slug,
57
  $this->setPageTitle(__('Emails', 'mailpoet')),
58
  __('Emails', 'mailpoet'),
59
+ Env::$required_permission,
60
  $main_page_slug,
61
  array($this, 'newsletters')
62
  );
76
  $main_page_slug,
77
  $this->setPageTitle(__('Forms', 'mailpoet')),
78
  __('Forms', 'mailpoet'),
79
+ Env::$required_permission,
80
  'mailpoet-forms',
81
  array($this, 'forms')
82
  );
95
  $main_page_slug,
96
  $this->setPageTitle(__('Subscribers', 'mailpoet')),
97
  __('Subscribers', 'mailpoet'),
98
+ Env::$required_permission,
99
  'mailpoet-subscribers',
100
  array($this, 'subscribers')
101
  );
114
  $main_page_slug,
115
  $this->setPageTitle(__('Lists', 'mailpoet')),
116
  __('Lists', 'mailpoet'),
117
+ Env::$required_permission,
118
  'mailpoet-segments',
119
  array($this, 'segments')
120
  );
134
  $main_page_slug,
135
  $this->setPageTitle( __('Settings', 'mailpoet')),
136
  __('Settings', 'mailpoet'),
137
+ Env::$required_permission,
138
  'mailpoet-settings',
139
  array($this, 'settings')
140
  );
142
  'admin.php?page=mailpoet-subscribers',
143
  $this->setPageTitle( __('Import', 'mailpoet')),
144
  __('Import', 'mailpoet'),
145
+ Env::$required_permission,
146
  'mailpoet-import',
147
  array($this, 'import')
148
  );
151
  true,
152
  $this->setPageTitle(__('Export', 'mailpoet')),
153
  __('Export', 'mailpoet'),
154
+ Env::$required_permission,
155
  'mailpoet-export',
156
  array($this, 'export')
157
  );
160
  true,
161
  $this->setPageTitle(__('Welcome', 'mailpoet')),
162
  __('Welcome', 'mailpoet'),
163
+ Env::$required_permission,
164
  'mailpoet-welcome',
165
  array($this, 'welcome')
166
  );
169
  true,
170
  $this->setPageTitle(__('Update', 'mailpoet')),
171
  __('Update', 'mailpoet'),
172
+ Env::$required_permission,
173
  'mailpoet-update',
174
  array($this, 'update')
175
  );
178
  true,
179
  $this->setPageTitle(__('Form Editor', 'mailpoet')),
180
  __('Form Editor', 'mailpoet'),
181
+ Env::$required_permission,
182
  'mailpoet-form-editor',
183
  array($this, 'formEditor')
184
  );
187
  true,
188
  $this->setPageTitle(__('Newsletter', 'mailpoet')),
189
  __('Newsletter Editor', 'mailpoet'),
190
+ Env::$required_permission,
191
  'mailpoet-newsletter-editor',
192
  array($this, 'newletterEditor')
193
  );
lib/Config/Migrator.php CHANGED
@@ -177,6 +177,7 @@ class Migrator {
177
  function newsletters() {
178
  $attributes = array(
179
  'id mediumint(9) NOT NULL AUTO_INCREMENT,',
 
180
  'parent_id mediumint(9) NULL,',
181
  'subject varchar(250) NOT NULL DEFAULT "",',
182
  'type varchar(20) NOT NULL DEFAULT "standard",',
177
  function newsletters() {
178
  $attributes = array(
179
  'id mediumint(9) NOT NULL AUTO_INCREMENT,',
180
+ 'hash varchar(150) NULL DEFAULT NULL,',
181
  'parent_id mediumint(9) NULL,',
182
  'subject varchar(250) NOT NULL DEFAULT "",',
183
  'type varchar(20) NOT NULL DEFAULT "standard",',
lib/Config/Shortcodes.php CHANGED
@@ -3,7 +3,6 @@ namespace MailPoet\Config;
3
  use \MailPoet\Models\Newsletter;
4
  use \MailPoet\Models\Subscriber;
5
  use \MailPoet\Models\SubscriberSegment;
6
- use \MailPoet\Subscription;
7
  use MailPoet\Newsletter\Url as NewsletterUrl;
8
 
9
  class Shortcodes {
@@ -33,7 +32,7 @@ class Shortcodes {
33
  ), 2);
34
  add_filter('mailpoet_archive_subject', array(
35
  $this, 'renderArchiveSubject'
36
- ), 2);
37
  }
38
 
39
  function formWidget($params = array()) {
@@ -78,6 +77,8 @@ class Shortcodes {
78
 
79
  $newsletters = Newsletter::getArchives($segment_ids);
80
 
 
 
81
  if(empty($newsletters)) {
82
  return apply_filters(
83
  'mailpoet_archive_no_newsletters',
@@ -91,12 +92,13 @@ class Shortcodes {
91
 
92
  $html .= '<ul class="mailpoet_archive">';
93
  foreach($newsletters as $newsletter) {
 
94
  $html .= '<li>'.
95
  '<span class="mailpoet_archive_date">'.
96
  apply_filters('mailpoet_archive_date', $newsletter).
97
  '</span>
98
  <span class="mailpoet_archive_subject">'.
99
- apply_filters('mailpoet_archive_subject', $newsletter).
100
  '</span>
101
  </li>';
102
  }
@@ -112,13 +114,16 @@ class Shortcodes {
112
  );
113
  }
114
 
115
- function renderArchiveSubject($newsletter) {
116
- $preview_url = NewsletterUrl::getViewInBrowserUrl($newsletter);
117
-
 
 
 
 
118
  return '<a href="'.esc_attr($preview_url).'" target="_blank" title="'
119
  .esc_attr(__('Preview in a new tab', 'mailpoet')).'">'
120
  .esc_attr($newsletter->subject).
121
  '</a>';
122
  }
123
-
124
- }
3
  use \MailPoet\Models\Newsletter;
4
  use \MailPoet\Models\Subscriber;
5
  use \MailPoet\Models\SubscriberSegment;
 
6
  use MailPoet\Newsletter\Url as NewsletterUrl;
7
 
8
  class Shortcodes {
32
  ), 2);
33
  add_filter('mailpoet_archive_subject', array(
34
  $this, 'renderArchiveSubject'
35
+ ), 2, 3);
36
  }
37
 
38
  function formWidget($params = array()) {
77
 
78
  $newsletters = Newsletter::getArchives($segment_ids);
79
 
80
+ $subscriber = Subscriber::getCurrentWPUser();
81
+
82
  if(empty($newsletters)) {
83
  return apply_filters(
84
  'mailpoet_archive_no_newsletters',
92
 
93
  $html .= '<ul class="mailpoet_archive">';
94
  foreach($newsletters as $newsletter) {
95
+ $queue = $newsletter->queue()->findOne();
96
  $html .= '<li>'.
97
  '<span class="mailpoet_archive_date">'.
98
  apply_filters('mailpoet_archive_date', $newsletter).
99
  '</span>
100
  <span class="mailpoet_archive_subject">'.
101
+ apply_filters('mailpoet_archive_subject', $newsletter, $subscriber, $queue).
102
  '</span>
103
  </li>';
104
  }
114
  );
115
  }
116
 
117
+ function renderArchiveSubject($newsletter, $subscriber, $queue) {
118
+ $preview_url = NewsletterUrl::getViewInBrowserUrl(
119
+ NewsletterUrl::TYPE_ARCHIVE,
120
+ $newsletter,
121
+ $subscriber,
122
+ $queue
123
+ );
124
  return '<a href="'.esc_attr($preview_url).'" target="_blank" title="'
125
  .esc_attr(__('Preview in a new tab', 'mailpoet')).'">'
126
  .esc_attr($newsletter->subject).
127
  '</a>';
128
  }
129
+ }
 
lib/Form/Block/Select.php CHANGED
@@ -29,11 +29,17 @@ class Select extends Base {
29
  );
30
 
31
  foreach($options as $option) {
 
 
 
 
32
  $is_selected = (
33
  (isset($option['is_checked']) && $option['is_checked'])
34
  ||
35
  (self::getFieldValue($block) === $option['value'])
36
- ) ? 'selected="selected"' : '';
 
 
37
 
38
  if(is_array($option['value'])) {
39
  $value = key($option['value']);
@@ -43,7 +49,7 @@ class Select extends Base {
43
  $label = $option['value'];
44
  }
45
 
46
- $html .= '<option value="'.$value.'" '.$is_selected.'>';
47
  $html .= esc_attr($label);
48
  $html .= '</option>';
49
  }
29
  );
30
 
31
  foreach($options as $option) {
32
+ if(!empty($option['is_hidden'])) {
33
+ continue;
34
+ }
35
+
36
  $is_selected = (
37
  (isset($option['is_checked']) && $option['is_checked'])
38
  ||
39
  (self::getFieldValue($block) === $option['value'])
40
+ ) ? ' selected="selected"' : '';
41
+
42
+ $is_disabled = (!empty($option['is_disabled'])) ? ' disabled="disabled"' : '';
43
 
44
  if(is_array($option['value'])) {
45
  $value = key($option['value']);
49
  $label = $option['value'];
50
  }
51
 
52
+ $html .= '<option value="'.$value.'"' . $is_selected . $is_disabled . '>';
53
  $html .= esc_attr($label);
54
  $html .= '</option>';
55
  }
lib/Mailer/Mailer.php CHANGED
@@ -10,6 +10,7 @@ class Mailer {
10
  public $mailer_config;
11
  public $sender;
12
  public $reply_to;
 
13
  public $mailer_instance;
14
  const MAILER_CONFIG_SETTING_NAME = 'mta';
15
  const SENDING_LIMIT_INTERVAL_MULTIPLIER = 60;
@@ -19,10 +20,11 @@ class Mailer {
19
  const METHOD_PHPMAIL = 'PHPMail';
20
  const METHOD_SMTP = 'SMTP';
21
 
22
- function __construct($mailer = false, $sender = false, $reply_to = false) {
23
  $this->mailer_config = self::getMailerConfig($mailer);
24
  $this->sender = $this->getSenderNameAndAddress($sender);
25
  $this->reply_to = $this->getReplyToNameAndAddress($reply_to);
 
26
  $this->mailer_instance = $this->buildMailer();
27
  }
28
 
@@ -39,7 +41,8 @@ class Mailer {
39
  $this->mailer_config['access_key'],
40
  $this->mailer_config['secret_key'],
41
  $this->sender,
42
- $this->reply_to
 
43
  );
44
  break;
45
  case self::METHOD_MAILPOET:
@@ -59,7 +62,8 @@ class Mailer {
59
  case self::METHOD_PHPMAIL:
60
  $mailer_instance = new $this->mailer_config['class'](
61
  $this->sender,
62
- $this->reply_to
 
63
  );
64
  break;
65
  case self::METHOD_SMTP:
@@ -71,7 +75,8 @@ class Mailer {
71
  $this->mailer_config['password'],
72
  $this->mailer_config['encryption'],
73
  $this->sender,
74
- $this->reply_to
 
75
  );
76
  break;
77
  default:
@@ -112,7 +117,7 @@ class Mailer {
112
 
113
  function getReplyToNameAndAddress($reply_to = array()) {
114
  if(!$reply_to) {
115
- $reply_to = Setting::getValue('reply_to', null);
116
  $reply_to['name'] = (!empty($reply_to['name'])) ?
117
  $reply_to['name'] :
118
  $this->sender['from_name'];
@@ -131,6 +136,12 @@ class Mailer {
131
  );
132
  }
133
 
 
 
 
 
 
 
134
  function formatSubscriberNameAndEmailAddress($subscriber) {
135
  $subscriber = (is_object($subscriber)) ? $subscriber->asArray() : $subscriber;
136
  if(!is_array($subscriber)) return $subscriber;
10
  public $mailer_config;
11
  public $sender;
12
  public $reply_to;
13
+ public $return_path;
14
  public $mailer_instance;
15
  const MAILER_CONFIG_SETTING_NAME = 'mta';
16
  const SENDING_LIMIT_INTERVAL_MULTIPLIER = 60;
20
  const METHOD_PHPMAIL = 'PHPMail';
21
  const METHOD_SMTP = 'SMTP';
22
 
23
+ function __construct($mailer = false, $sender = false, $reply_to = false, $return_path = false) {
24
  $this->mailer_config = self::getMailerConfig($mailer);
25
  $this->sender = $this->getSenderNameAndAddress($sender);
26
  $this->reply_to = $this->getReplyToNameAndAddress($reply_to);
27
+ $this->return_path = $this->getReturnPathAddress($return_path);
28
  $this->mailer_instance = $this->buildMailer();
29
  }
30
 
41
  $this->mailer_config['access_key'],
42
  $this->mailer_config['secret_key'],
43
  $this->sender,
44
+ $this->reply_to,
45
+ $this->return_path
46
  );
47
  break;
48
  case self::METHOD_MAILPOET:
62
  case self::METHOD_PHPMAIL:
63
  $mailer_instance = new $this->mailer_config['class'](
64
  $this->sender,
65
+ $this->reply_to,
66
+ $this->return_path
67
  );
68
  break;
69
  case self::METHOD_SMTP:
75
  $this->mailer_config['password'],
76
  $this->mailer_config['encryption'],
77
  $this->sender,
78
+ $this->reply_to,
79
+ $this->return_path
80
  );
81
  break;
82
  default:
117
 
118
  function getReplyToNameAndAddress($reply_to = array()) {
119
  if(!$reply_to) {
120
+ $reply_to = Setting::getValue('reply_to');
121
  $reply_to['name'] = (!empty($reply_to['name'])) ?
122
  $reply_to['name'] :
123
  $this->sender['from_name'];
136
  );
137
  }
138
 
139
+ function getReturnPathAddress($return_path) {
140
+ return ($return_path) ?
141
+ $return_path :
142
+ Setting::getValue('bounce.address');
143
+ }
144
+
145
  function formatSubscriberNameAndEmailAddress($subscriber) {
146
  $subscriber = (is_object($subscriber)) ? $subscriber->asArray() : $subscriber;
147
  if(!is_array($subscriber)) return $subscriber;
lib/Mailer/Methods/AmazonSES.php CHANGED
@@ -17,6 +17,7 @@ class AmazonSES {
17
  public $url;
18
  public $sender;
19
  public $reply_to;
 
20
  public $date;
21
  public $date_without_time;
22
  private $available_regions = array(
@@ -25,7 +26,7 @@ class AmazonSES {
25
  'EU (Ireland)' => 'eu-west-1'
26
  );
27
 
28
- function __construct($region, $access_key, $secret_key, $sender, $reply_to) {
29
  $this->aws_access_key = $access_key;
30
  $this->aws_secret_key = $secret_key;
31
  $this->aws_region = (in_array($region, $this->available_regions)) ? $region : false;
@@ -40,11 +41,13 @@ class AmazonSES {
40
  $this->url = 'https://' . $this->aws_endpoint;
41
  $this->sender = $sender;
42
  $this->reply_to = $reply_to;
 
 
 
43
  $this->date = gmdate('Ymd\THis\Z');
44
  $this->date_without_time = gmdate('Ymd');
45
  }
46
 
47
-
48
  function send($newsletter, $subscriber) {
49
  $result = wp_remote_post(
50
  $this->url,
@@ -71,7 +74,7 @@ class AmazonSES {
71
  'Source' => $this->sender['from_name_email'],
72
  'ReplyToAddresses.member.1' => $this->reply_to['reply_to_name_email'],
73
  'Message.Subject.Data' => $newsletter['subject'],
74
- 'ReturnPath' => $this->sender['from_name_email'],
75
  );
76
  if(!empty($newsletter['body']['html'])) {
77
  $body['Message.Body.Html.Data'] = $newsletter['body']['html'];
17
  public $url;
18
  public $sender;
19
  public $reply_to;
20
+ public $return_path;
21
  public $date;
22
  public $date_without_time;
23
  private $available_regions = array(
26
  'EU (Ireland)' => 'eu-west-1'
27
  );
28
 
29
+ function __construct($region, $access_key, $secret_key, $sender, $reply_to, $return_path) {
30
  $this->aws_access_key = $access_key;
31
  $this->aws_secret_key = $secret_key;
32
  $this->aws_region = (in_array($region, $this->available_regions)) ? $region : false;
41
  $this->url = 'https://' . $this->aws_endpoint;
42
  $this->sender = $sender;
43
  $this->reply_to = $reply_to;
44
+ $this->return_path = ($return_path) ?
45
+ $return_path :
46
+ $this->sender['from_email'];
47
  $this->date = gmdate('Ymd\THis\Z');
48
  $this->date_without_time = gmdate('Ymd');
49
  }
50
 
 
51
  function send($newsletter, $subscriber) {
52
  $result = wp_remote_post(
53
  $this->url,
74
  'Source' => $this->sender['from_name_email'],
75
  'ReplyToAddresses.member.1' => $this->reply_to['reply_to_name_email'],
76
  'Message.Subject.Data' => $newsletter['subject'],
77
+ 'ReturnPath' => $this->return_path
78
  );
79
  if(!empty($newsletter['body']['html'])) {
80
  $body['Message.Body.Html.Data'] = $newsletter['body']['html'];
lib/Mailer/Methods/PHPMail.php CHANGED
@@ -8,11 +8,15 @@ if(!defined('ABSPATH')) exit;
8
  class PHPMail {
9
  public $sender;
10
  public $reply_to;
 
11
  public $mailer;
12
 
13
- function __construct($sender, $reply_to) {
14
  $this->sender = $sender;
15
  $this->reply_to = $reply_to;
 
 
 
16
  $this->mailer = $this->buildMailer();
17
  }
18
 
@@ -44,6 +48,7 @@ class PHPMail {
44
  ->setReplyTo(array(
45
  $this->reply_to['reply_to_email'] => $this->reply_to['reply_to_name']
46
  ))
 
47
  ->setSubject($newsletter['subject']);
48
  if(!empty($newsletter['body']['html'])) {
49
  $message = $message->setBody($newsletter['body']['html'], 'text/html');
8
  class PHPMail {
9
  public $sender;
10
  public $reply_to;
11
+ public $return_path;
12
  public $mailer;
13
 
14
+ function __construct($sender, $reply_to, $return_path) {
15
  $this->sender = $sender;
16
  $this->reply_to = $reply_to;
17
+ $this->return_path = ($return_path) ?
18
+ $return_path :
19
+ $this->sender['from_email'];
20
  $this->mailer = $this->buildMailer();
21
  }
22
 
48
  ->setReplyTo(array(
49
  $this->reply_to['reply_to_email'] => $this->reply_to['reply_to_name']
50
  ))
51
+ ->setReturnPath($this->return_path)
52
  ->setSubject($newsletter['subject']);
53
  if(!empty($newsletter['body']['html'])) {
54
  $message = $message->setBody($newsletter['body']['html'], 'text/html');
lib/Mailer/Methods/SMTP.php CHANGED
@@ -14,11 +14,12 @@ class SMTP {
14
  public $encryption;
15
  public $sender;
16
  public $reply_to;
 
17
  public $mailer;
18
 
19
  function __construct(
20
  $host, $port, $authentication, $login = null, $password = null, $encryption,
21
- $sender, $reply_to) {
22
  $this->host = $host;
23
  $this->port = $port;
24
  $this->authentication = $authentication;
@@ -27,6 +28,9 @@ class SMTP {
27
  $this->encryption = $encryption;
28
  $this->sender = $sender;
29
  $this->reply_to = $reply_to;
 
 
 
30
  $this->mailer = $this->buildMailer();
31
  }
32
 
@@ -65,6 +69,7 @@ class SMTP {
65
  ->setReplyTo(array(
66
  $this->reply_to['reply_to_email'] => $this->reply_to['reply_to_name']
67
  ))
 
68
  ->setSubject($newsletter['subject']);
69
  if(!empty($newsletter['body']['html'])) {
70
  $message = $message->setBody($newsletter['body']['html'], 'text/html');
14
  public $encryption;
15
  public $sender;
16
  public $reply_to;
17
+ public $return_path;
18
  public $mailer;
19
 
20
  function __construct(
21
  $host, $port, $authentication, $login = null, $password = null, $encryption,
22
+ $sender, $reply_to, $return_path) {
23
  $this->host = $host;
24
  $this->port = $port;
25
  $this->authentication = $authentication;
28
  $this->encryption = $encryption;
29
  $this->sender = $sender;
30
  $this->reply_to = $reply_to;
31
+ $this->return_path = ($return_path) ?
32
+ $return_path :
33
+ $this->sender['from_email'];
34
  $this->mailer = $this->buildMailer();
35
  }
36
 
69
  ->setReplyTo(array(
70
  $this->reply_to['reply_to_email'] => $this->reply_to['reply_to_name']
71
  ))
72
+ ->setReturnPath($this->return_path)
73
  ->setSubject($newsletter['subject']);
74
  if(!empty($newsletter['body']['html'])) {
75
  $message = $message->setBody($newsletter['body']['html'], 'text/html');
lib/Models/Newsletter.php CHANGED
@@ -2,6 +2,7 @@
2
  namespace MailPoet\Models;
3
  use MailPoet\Newsletter\Renderer\Renderer;
4
  use MailPoet\Util\Helpers;
 
5
 
6
  if(!defined('ABSPATH')) exit;
7
 
@@ -11,7 +12,6 @@ class Newsletter extends Model {
11
  const TYPE_WELCOME = 'welcome';
12
  const TYPE_NOTIFICATION = 'notification';
13
  const TYPE_NOTIFICATION_HISTORY = 'notification_history';
14
-
15
  // standard newsletters
16
  const STATUS_DRAFT = 'draft';
17
  const STATUS_SCHEDULED = 'scheduled';
@@ -19,6 +19,7 @@ class Newsletter extends Model {
19
  const STATUS_SENT = 'sent';
20
  // automatic newsletters status
21
  const STATUS_ACTIVE = 'active';
 
22
 
23
  function __construct() {
24
  parent::__construct();
@@ -27,6 +28,10 @@ class Newsletter extends Model {
27
  ));
28
  }
29
 
 
 
 
 
30
  function save() {
31
  if(is_string($this->deleted_at) && strlen(trim($this->deleted_at)) === 0) {
32
  $this->set_expr('deleted_at', 'NULL');
@@ -37,6 +42,12 @@ class Newsletter extends Model {
37
  ? json_encode($this->body)
38
  : $this->body
39
  );
 
 
 
 
 
 
40
  return parent::save();
41
  }
42
 
@@ -74,6 +85,9 @@ class Newsletter extends Model {
74
  // reset status
75
  $duplicate->set('status', self::STATUS_DRAFT);
76
 
 
 
 
77
  $duplicate->save();
78
 
79
  if($duplicate->getErrors() === false) {
@@ -130,6 +144,9 @@ class Newsletter extends Model {
130
  $notification_history->set_expr('updated_at', 'NOW()');
131
  $notification_history->set_expr('deleted_at', 'NULL');
132
 
 
 
 
133
  $notification_history->save();
134
 
135
  if($notification_history->getErrors() === false) {
@@ -182,8 +199,39 @@ class Newsletter extends Model {
182
  );
183
  }
184
 
185
- function withSegments() {
186
  $this->segments = $this->segments()->findArray();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  return $this;
188
  }
189
 
@@ -634,6 +682,7 @@ class Newsletter extends Model {
634
  'queues'
635
  )
636
  ->where('queues.status', SendingQueue::STATUS_COMPLETED)
 
637
  ->select('queues.processed_at')
638
  ->orderByDesc('queues.processed_at');
639
 
@@ -647,4 +696,17 @@ class Newsletter extends Model {
647
  }
648
  return $orm->findMany();
649
  }
650
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  namespace MailPoet\Models;
3
  use MailPoet\Newsletter\Renderer\Renderer;
4
  use MailPoet\Util\Helpers;
5
+ use MailPoet\Util\Security;
6
 
7
  if(!defined('ABSPATH')) exit;
8
 
12
  const TYPE_WELCOME = 'welcome';
13
  const TYPE_NOTIFICATION = 'notification';
14
  const TYPE_NOTIFICATION_HISTORY = 'notification_history';
 
15
  // standard newsletters
16
  const STATUS_DRAFT = 'draft';
17
  const STATUS_SCHEDULED = 'scheduled';
19
  const STATUS_SENT = 'sent';
20
  // automatic newsletters status
21
  const STATUS_ACTIVE = 'active';
22
+ const NEWSLETTER_HASH_LENGTH = 6;
23
 
24
  function __construct() {
25
  parent::__construct();
28
  ));
29
  }
30
 
31
+ function queue() {
32
+ return $this->has_one(__NAMESPACE__ . '\SendingQueue', 'newsletter_id', 'id');
33
+ }
34
+
35
  function save() {
36
  if(is_string($this->deleted_at) && strlen(trim($this->deleted_at)) === 0) {
37
  $this->set_expr('deleted_at', 'NULL');
42
  ? json_encode($this->body)
43
  : $this->body
44
  );
45
+
46
+ $this->set('hash',
47
+ ($this->hash)
48
+ ? $this->hash
49
+ : self::generateHash()
50
+ );
51
  return parent::save();
52
  }
53
 
85
  // reset status
86
  $duplicate->set('status', self::STATUS_DRAFT);
87
 
88
+ // reset hash
89
+ $duplicate->set('hash', null);
90
+
91
  $duplicate->save();
92
 
93
  if($duplicate->getErrors() === false) {
144
  $notification_history->set_expr('updated_at', 'NOW()');
145
  $notification_history->set_expr('deleted_at', 'NULL');
146
 
147
+ // reset hash
148
+ $notification_history->set('hash', null);
149
+
150
  $notification_history->save();
151
 
152
  if($notification_history->getErrors() === false) {
199
  );
200
  }
201
 
202
+ function withSegments($incl_deleted = false) {
203
  $this->segments = $this->segments()->findArray();
204
+ if($incl_deleted) {
205
+ $this->withDeletedSegments();
206
+ }
207
+ return $this;
208
+ }
209
+
210
+ // Intermediary table only
211
+ function segmentLinks() {
212
+ return $this->has_many(
213
+ __NAMESPACE__.'\NewsletterSegment',
214
+ 'newsletter_id',
215
+ 'id'
216
+ );
217
+ }
218
+
219
+ function withDeletedSegments() {
220
+ if(!empty($this->segments)) {
221
+ $segment_ids = Helpers::arrayColumn($this->segments, 'id');
222
+ $links = $this->segmentLinks()
223
+ ->whereNotIn('segment_id', $segment_ids)->findArray();
224
+ $deleted_segments = array();
225
+
226
+ foreach($links as $link) {
227
+ $deleted_segments[] = array(
228
+ 'id' => $link['segment_id'],
229
+ 'name' => __('Deleted list', 'mailpoet')
230
+ );
231
+ }
232
+ $this->segments = array_merge($this->segments, $deleted_segments);
233
+ }
234
+
235
  return $this;
236
  }
237
 
682
  'queues'
683
  )
684
  ->where('queues.status', SendingQueue::STATUS_COMPLETED)
685
+ ->whereNull('newsletters.deleted_at')
686
  ->select('queues.processed_at')
687
  ->orderByDesc('queues.processed_at');
688
 
696
  }
697
  return $orm->findMany();
698
  }
699
+
700
+ static function getByHash($hash) {
701
+ return parent::where('hash', $hash)
702
+ ->findOne();
703
+ }
704
+
705
+ static function generateHash() {
706
+ return substr(
707
+ md5(AUTH_KEY . Security::generateRandomString(15)),
708
+ 0,
709
+ self::NEWSLETTER_HASH_LENGTH
710
+ );
711
+ }
712
+ }
lib/Models/SendingQueue.php CHANGED
@@ -70,10 +70,13 @@ class SendingQueue extends Model {
70
  return $subscribers;
71
  }
72
 
73
- function getNewsletterRenderedBody() {
74
- return (!is_serialized($this->newsletter_rendered_body)) ?
75
  $this->newsletter_rendered_body :
76
  unserialize($this->newsletter_rendered_body);
 
 
 
77
  }
78
 
79
  function isSubscriberProcessed($subscriber_id) {
70
  return $subscribers;
71
  }
72
 
73
+ function getNewsletterRenderedBody($type = false) {
74
+ $rendered_newsletter = (!is_serialized($this->newsletter_rendered_body)) ?
75
  $this->newsletter_rendered_body :
76
  unserialize($this->newsletter_rendered_body);
77
+ return ($type && !empty($rendered_newsletter[$type])) ?
78
+ $rendered_newsletter[$type] :
79
+ $rendered_newsletter;
80
  }
81
 
82
  function isSubscriberProcessed($subscriber_id) {
lib/Models/Subscriber.php CHANGED
@@ -14,8 +14,8 @@ class Subscriber extends Model {
14
  const STATUS_UNSUBSCRIBED = 'unsubscribed';
15
  const STATUS_UNCONFIRMED = 'unconfirmed';
16
  const STATUS_BOUNCED = 'bounced';
17
-
18
  const SUBSCRIPTION_LIMIT_COOLDOWN = 60;
 
19
 
20
  function __construct() {
21
  parent::__construct();
@@ -154,13 +154,17 @@ class Subscriber extends Model {
154
 
155
  static function generateToken($email = null) {
156
  if($email !== null) {
157
- return md5(AUTH_KEY.$email);
158
  }
159
  return false;
160
  }
161
 
162
  static function verifyToken($email, $token) {
163
- return call_user_func('hash_equals', self::generateToken($email), $token);
 
 
 
 
164
  }
165
 
166
  static function subscribe($subscriber_data = array(), $segment_ids = array()) {
14
  const STATUS_UNSUBSCRIBED = 'unsubscribed';
15
  const STATUS_UNCONFIRMED = 'unconfirmed';
16
  const STATUS_BOUNCED = 'bounced';
 
17
  const SUBSCRIPTION_LIMIT_COOLDOWN = 60;
18
+ const SUBSCRIBER_TOKEN_LENGTH = 6;
19
 
20
  function __construct() {
21
  parent::__construct();
154
 
155
  static function generateToken($email = null) {
156
  if($email !== null) {
157
+ return substr(md5(AUTH_KEY . $email), 0, self::SUBSCRIBER_TOKEN_LENGTH);
158
  }
159
  return false;
160
  }
161
 
162
  static function verifyToken($email, $token) {
163
+ return call_user_func(
164
+ 'hash_equals',
165
+ self::generateToken($email),
166
+ substr($token, 0, self::SUBSCRIBER_TOKEN_LENGTH)
167
+ );
168
  }
169
 
170
  static function subscribe($subscriber_data = array(), $segment_ids = array()) {
lib/Newsletter/Links/Links.php CHANGED
@@ -6,6 +6,7 @@ use MailPoet\Router\Router;
6
  use MailPoet\Router\Endpoints\Track as TrackEndpoint;
7
  use MailPoet\Models\NewsletterLink;
8
  use MailPoet\Newsletter\Shortcodes\Shortcodes;
 
9
  use MailPoet\Util\Security;
10
 
11
  class Links {
@@ -101,24 +102,19 @@ class Links {
101
  $preview = false
102
  ) {
103
  // match data tags
104
- $regex = sprintf(
105
- '/((%s|%s)(?:-\w+)?)/',
106
- preg_quote(self::DATA_TAG_CLICK),
107
- preg_quote(self::DATA_TAG_OPEN)
108
- );
109
  $subscriber = Subscriber::findOne($subscriber_id);
110
- preg_match_all($regex, $content, $matches);
111
  foreach($matches[1] as $index => $match) {
112
  $hash = null;
113
  if(preg_match('/-/', $match)) {
114
  list(, $hash) = explode('-', $match);
115
  }
116
- $data = array(
117
- 'subscriber_id' => $subscriber->id,
118
- 'subscriber_token' => Subscriber::generateToken($subscriber->email),
119
- 'queue_id' => $queue_id,
120
- 'link_hash' => $hash,
121
- 'preview' => $preview
122
  );
123
  $router_action = ($matches[2][$index] === self::DATA_TAG_CLICK) ?
124
  TrackEndpoint::ACTION_CLICK :
@@ -144,4 +140,54 @@ class Links {
144
  $newsletter_link->save();
145
  }
146
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  }
6
  use MailPoet\Router\Endpoints\Track as TrackEndpoint;
7
  use MailPoet\Models\NewsletterLink;
8
  use MailPoet\Newsletter\Shortcodes\Shortcodes;
9
+ use MailPoet\Util\Helpers;
10
  use MailPoet\Util\Security;
11
 
12
  class Links {
102
  $preview = false
103
  ) {
104
  // match data tags
 
 
 
 
 
105
  $subscriber = Subscriber::findOne($subscriber_id);
106
+ preg_match_all(self::getLinkRegex(), $content, $matches);
107
  foreach($matches[1] as $index => $match) {
108
  $hash = null;
109
  if(preg_match('/-/', $match)) {
110
  list(, $hash) = explode('-', $match);
111
  }
112
+ $data = self::createUrlDataObject(
113
+ $subscriber->id,
114
+ $subscriber->email,
115
+ $queue_id,
116
+ $hash,
117
+ $preview
118
  );
119
  $router_action = ($matches[2][$index] === self::DATA_TAG_CLICK) ?
120
  TrackEndpoint::ACTION_CLICK :
140
  $newsletter_link->save();
141
  }
142
  }
143
+
144
+ static function convertHashedLinksToShortcodesAndUrls($content, $convert_all = false) {
145
+ preg_match_all(self::getLinkRegex(), $content, $links);
146
+ $links = array_unique(Helpers::flattenArray($links));
147
+ foreach($links as $link) {
148
+ $link_hash = explode('-', $link);
149
+ if(!isset($link_hash[1])) continue;
150
+ $newsletter_link = NewsletterLink::getByHash($link_hash[1]);
151
+ // convert either only link shortcodes or all hashes links if "convert all"
152
+ // option is specified
153
+ if($newsletter_link &&
154
+ (preg_match('/\[link:/', $newsletter_link->url) || $convert_all)
155
+ ) {
156
+ $content = str_replace($link, $newsletter_link->url, $content);
157
+ }
158
+ }
159
+ return $content;
160
+ }
161
+
162
+ static function getLinkRegex() {
163
+ return sprintf(
164
+ '/((%s|%s)(?:-\w+)?)/',
165
+ preg_quote(self::DATA_TAG_CLICK),
166
+ preg_quote(self::DATA_TAG_OPEN)
167
+ );
168
+ }
169
+
170
+ static function createUrlDataObject(
171
+ $subscriber_id, $subscriber_email, $queue_id, $link_hash, $preview
172
+ ) {
173
+ return array(
174
+ $subscriber_id,
175
+ Subscriber::generateToken($subscriber_email),
176
+ $queue_id,
177
+ $link_hash,
178
+ $preview
179
+ );
180
+ }
181
+
182
+ static function transformUrlDataObject($data) {
183
+ reset($data);
184
+ if(!is_int(key($data))) return $data;
185
+ $transformed_data = array();
186
+ $transformed_data['subscriber_id'] = (!empty($data[0])) ? $data[0] : false;
187
+ $transformed_data['subscriber_token'] = (!empty($data[1])) ? $data[1] : false;
188
+ $transformed_data['queue_id'] = (!empty($data[2])) ? $data[2] : false;
189
+ $transformed_data['link_hash'] = (!empty($data[3])) ? $data[3] : false;
190
+ $transformed_data['preview'] = (!empty($data[4])) ? $data[4] : false;
191
+ return $transformed_data;
192
+ }
193
  }
lib/Newsletter/Renderer/PostProcess/OpenTracking.php CHANGED
@@ -21,7 +21,7 @@ class OpenTracking {
21
  }
22
 
23
  static function addTrackingImage() {
24
- add_filter(Renderer::POST_PROCESS_FILTER, function ($template) {
25
  return OpenTracking::process($template);
26
  });
27
  return true;
21
  }
22
 
23
  static function addTrackingImage() {
24
+ add_filter(Renderer::FILTER_POST_PROCESS, function ($template) {
25
  return OpenTracking::process($template);
26
  });
27
  return true;
lib/Newsletter/Renderer/Renderer.php CHANGED
@@ -11,7 +11,7 @@ class Renderer {
11
  public $newsletter;
12
  public $preview;
13
  const NEWSLETTER_TEMPLATE = 'Template.html';
14
- const POST_PROCESS_FILTER = 'mailpoet_rendering_post_process';
15
 
16
  function __construct($newsletter, $preview = false) {
17
  // TODO: remove ternary condition, refactor to use model objects
@@ -24,7 +24,7 @@ class Renderer {
24
  $this->template = file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE);
25
  }
26
 
27
- function render() {
28
  $newsletter = $this->newsletter;
29
  $body = (is_array($newsletter['body']))
30
  ? $newsletter['body']
@@ -48,10 +48,14 @@ class Renderer {
48
  $template = $this->inlineCSSStyles($template);
49
  $template = $this->postProcessTemplate($template);
50
 
51
- return array(
52
  'html' => $template,
53
  'text' => $this->renderTextVersion($template)
54
  );
 
 
 
 
55
  }
56
 
57
  function renderBody($content) {
@@ -124,7 +128,7 @@ class Renderer {
124
  str_replace('&', '&amp;', $template->html())
125
  );
126
  $template = apply_filters(
127
- self::POST_PROCESS_FILTER,
128
  $DOM->__toString()
129
  );
130
  return $template;
11
  public $newsletter;
12
  public $preview;
13
  const NEWSLETTER_TEMPLATE = 'Template.html';
14
+ const FILTER_POST_PROCESS = 'mailpoet_rendering_post_process';
15
 
16
  function __construct($newsletter, $preview = false) {
17
  // TODO: remove ternary condition, refactor to use model objects
24
  $this->template = file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE);
25
  }
26
 
27
+ function render($type = false) {
28
  $newsletter = $this->newsletter;
29
  $body = (is_array($newsletter['body']))
30
  ? $newsletter['body']
48
  $template = $this->inlineCSSStyles($template);
49
  $template = $this->postProcessTemplate($template);
50
 
51
+ $rendered_newsletter = array(
52
  'html' => $template,
53
  'text' => $this->renderTextVersion($template)
54
  );
55
+
56
+ return ($type && !empty($rendered_newsletter[$type])) ?
57
+ $rendered_newsletter[$type] :
58
+ $rendered_newsletter;
59
  }
60
 
61
  function renderBody($content) {
128
  str_replace('&', '&amp;', $template->html())
129
  );
130
  $template = apply_filters(
131
+ self::FILTER_POST_PROCESS,
132
  $DOM->__toString()
133
  );
134
  return $template;
lib/Newsletter/Shortcodes/Categories/Link.php CHANGED
@@ -7,19 +7,23 @@ use MailPoet\Statistics\Track\Unsubscribes;
7
  use MailPoet\Subscription\Url as SubscriptionUrl;
8
 
9
  class Link {
10
- static function process($action,
 
11
  $default_value,
12
  $newsletter,
13
  $subscriber,
14
- $queue
 
 
15
  ) {
16
  switch($action) {
17
  case 'subscription_unsubscribe':
18
  $action = 'subscription_unsubscribe_url';
19
  $url = self::processUrl(
20
  $action,
21
- esc_attr(SubscriptionUrl::getUnsubscribeUrl($subscriber)),
22
- $queue
 
23
  );
24
  return sprintf(
25
  '<a target="_blank" href="%s">%s</a>',
@@ -31,14 +35,16 @@ class Link {
31
  return self::processUrl(
32
  $action,
33
  SubscriptionUrl::getUnsubscribeUrl($subscriber),
34
- $queue
 
35
  );
36
 
37
  case 'subscription_manage':
38
  $url = self::processUrl(
39
  $action = 'subscription_manage_url',
40
- esc_attr(SubscriptionUrl::getManageUrl($subscriber)),
41
- $queue
 
42
  );
43
  return sprintf(
44
  '<a target="_blank" href="%s">%s</a>',
@@ -50,13 +56,20 @@ class Link {
50
  return self::processUrl(
51
  $action,
52
  SubscriptionUrl::getManageUrl($subscriber),
53
- $queue
 
54
  );
55
 
56
  case 'newsletter_view_in_browser':
57
  $action = 'newsletter_view_in_browser_url';
58
- $url = esc_attr(NewsletterUrl::getViewInBrowserUrl($newsletter, $subscriber, $queue));
59
- $url = self::processUrl($action, $url, $queue);
 
 
 
 
 
 
60
  return sprintf(
61
  '<a target="_blank" href="%s">%s</a>',
62
  $url,
@@ -64,8 +77,14 @@ class Link {
64
  );
65
 
66
  case 'newsletter_view_in_browser_url':
67
- $url = NewsletterUrl::getViewInBrowserUrl($newsletter, $subscriber, $queue);
68
- return self::processUrl($action, $url, $queue);
 
 
 
 
 
 
69
 
70
  default:
71
  $shortcode = self::getShortcode($action);
@@ -74,15 +93,17 @@ class Link {
74
  $shortcode,
75
  $newsletter,
76
  $subscriber,
77
- $queue
 
78
  );
79
  return ($url !== $shortcode) ?
80
- self::processUrl($action, $url, $queue) :
81
  false;
82
  }
83
  }
84
 
85
- static function processUrl($action, $url, $queue) {
 
86
  return ($queue !== false && (boolean)Setting::getValue('tracking.enabled')) ?
87
  self::getShortcode($action) :
88
  $url;
@@ -104,7 +125,12 @@ class Link {
104
  $url = SubscriptionUrl::getManageUrl($subscriber);
105
  break;
106
  case 'newsletter_view_in_browser_url':
107
- $url = NewsletterUrl::getViewInBrowserUrl($newsletter, $subscriber, $queue);
 
 
 
 
 
108
  break;
109
  default:
110
  $shortcode = self::getShortcode($shortcode_action);
7
  use MailPoet\Subscription\Url as SubscriptionUrl;
8
 
9
  class Link {
10
+ static function process(
11
+ $action,
12
  $default_value,
13
  $newsletter,
14
  $subscriber,
15
+ $queue,
16
+ $content,
17
+ $wp_user_preview
18
  ) {
19
  switch($action) {
20
  case 'subscription_unsubscribe':
21
  $action = 'subscription_unsubscribe_url';
22
  $url = self::processUrl(
23
  $action,
24
+ SubscriptionUrl::getUnsubscribeUrl($subscriber),
25
+ $queue,
26
+ $wp_user_preview
27
  );
28
  return sprintf(
29
  '<a target="_blank" href="%s">%s</a>',
35
  return self::processUrl(
36
  $action,
37
  SubscriptionUrl::getUnsubscribeUrl($subscriber),
38
+ $queue,
39
+ $wp_user_preview
40
  );
41
 
42
  case 'subscription_manage':
43
  $url = self::processUrl(
44
  $action = 'subscription_manage_url',
45
+ SubscriptionUrl::getManageUrl($subscriber),
46
+ $queue,
47
+ $wp_user_preview
48
  );
49
  return sprintf(
50
  '<a target="_blank" href="%s">%s</a>',
56
  return self::processUrl(
57
  $action,
58
  SubscriptionUrl::getManageUrl($subscriber),
59
+ $queue,
60
+ $wp_user_preview
61
  );
62
 
63
  case 'newsletter_view_in_browser':
64
  $action = 'newsletter_view_in_browser_url';
65
+ $url = NewsletterUrl::getViewInBrowserUrl(
66
+ $type = null,
67
+ $newsletter,
68
+ $subscriber,
69
+ $queue,
70
+ $wp_user_preview
71
+ );
72
+ $url = self::processUrl($action, $url, $queue, $wp_user_preview);
73
  return sprintf(
74
  '<a target="_blank" href="%s">%s</a>',
75
  $url,
77
  );
78
 
79
  case 'newsletter_view_in_browser_url':
80
+ $url = NewsletterUrl::getViewInBrowserUrl(
81
+ $type = null,
82
+ $newsletter,
83
+ $subscriber,
84
+ $queue,
85
+ $wp_user_preview
86
+ );
87
+ return self::processUrl($action, $url, $queue, $wp_user_preview);
88
 
89
  default:
90
  $shortcode = self::getShortcode($action);
93
  $shortcode,
94
  $newsletter,
95
  $subscriber,
96
+ $queue,
97
+ $wp_user_preview
98
  );
99
  return ($url !== $shortcode) ?
100
+ self::processUrl($action, $url, $queue, $wp_user_preview) :
101
  false;
102
  }
103
  }
104
 
105
+ static function processUrl($action, $url, $queue, $wp_user_preview = false) {
106
+ if($wp_user_preview) return '#';
107
  return ($queue !== false && (boolean)Setting::getValue('tracking.enabled')) ?
108
  self::getShortcode($action) :
109
  $url;
125
  $url = SubscriptionUrl::getManageUrl($subscriber);
126
  break;
127
  case 'newsletter_view_in_browser_url':
128
+ $url = NewsletterUrl::getViewInBrowserUrl(
129
+ $type = null,
130
+ $newsletter,
131
+ $subscriber,
132
+ $queue
133
+ );
134
  break;
135
  default:
136
  $shortcode = self::getShortcode($shortcode_action);
lib/Newsletter/Shortcodes/Shortcodes.php CHANGED
@@ -5,16 +5,19 @@ class Shortcodes {
5
  public $newsletter;
6
  public $subscriber;
7
  public $queue;
 
8
  const SHORTCODE_CATEGORY_NAMESPACE = 'MailPoet\Newsletter\Shortcodes\Categories\\';
9
 
10
  function __construct(
11
  $newsletter = false,
12
  $subscriber = false,
13
- $queue = false
 
14
  ) {
15
  $this->newsletter = $newsletter;
16
  $this->subscriber = $subscriber;
17
  $this->queue = $queue;
 
18
  }
19
 
20
  function extract($content, $categories = false) {
@@ -64,7 +67,8 @@ class Shortcodes {
64
  $_this->newsletter,
65
  $_this->subscriber,
66
  $_this->queue,
67
- $content
 
68
  );
69
  return ($custom_shortcode === $shortcode) ?
70
  false :
@@ -76,7 +80,8 @@ class Shortcodes {
76
  $_this->newsletter,
77
  $_this->subscriber,
78
  $_this->queue,
79
- $content
 
80
  );
81
  }, $shortcodes);
82
  return $processed_shortcodes;
5
  public $newsletter;
6
  public $subscriber;
7
  public $queue;
8
+ public $wp_user_preview;
9
  const SHORTCODE_CATEGORY_NAMESPACE = 'MailPoet\Newsletter\Shortcodes\Categories\\';
10
 
11
  function __construct(
12
  $newsletter = false,
13
  $subscriber = false,
14
+ $queue = false,
15
+ $wp_user_preview = false
16
  ) {
17
  $this->newsletter = $newsletter;
18
  $this->subscriber = $subscriber;
19
  $this->queue = $queue;
20
+ $this->wp_user_preview = $wp_user_preview;
21
  }
22
 
23
  function extract($content, $categories = false) {
67
  $_this->newsletter,
68
  $_this->subscriber,
69
  $_this->queue,
70
+ $content,
71
+ $_this->wp_user_preview
72
  );
73
  return ($custom_shortcode === $shortcode) ?
74
  false :
80
  $_this->newsletter,
81
  $_this->subscriber,
82
  $_this->queue,
83
+ $content,
84
+ $_this->wp_user_preview
85
  );
86
  }, $shortcodes);
87
  return $processed_shortcodes;
lib/Newsletter/Url.php CHANGED
@@ -1,52 +1,80 @@
1
  <?php
2
  namespace MailPoet\Newsletter;
3
 
4
- use MailPoet\Models\SendingQueue;
5
  use MailPoet\Router\Router;
6
  use MailPoet\Router\Endpoints\ViewInBrowser as ViewInBrowserEndpoint;
7
- use MailPoet\Models\Subscriber;
 
8
 
9
  class Url {
 
 
 
10
  static function getViewInBrowserUrl(
11
- $newsletter,
 
12
  $subscriber = false,
13
  $queue = false,
14
  $preview = false
15
  ) {
16
- if(is_object($newsletter)) {
17
- $newsletter = $newsletter->asArray();
18
- }
19
- if(is_object($subscriber)) {
20
- $subscriber = $subscriber->asArray();
21
- } else if(!$subscriber) {
22
- $subscriber = Subscriber::getCurrentWPUser();
23
- $subscriber = ($subscriber) ? $subscriber->asArray() : false;
24
  }
25
- if(is_object($queue)) {
26
- $queue = $queue->asArray();
27
- } else if(!$preview && !empty($newsletter['id'])) {
28
- $queue = SendingQueue::where('newsletter_id', $newsletter['id'])->findOne();
29
- $queue = ($queue) ? $queue->asArray() : false;
 
 
 
 
 
 
 
 
 
 
30
  }
31
- $data = array(
32
- 'newsletter_id' => (!empty($newsletter['id'])) ?
33
- $newsletter['id'] :
34
- $newsletter,
35
- 'subscriber_id' => (!empty($subscriber['id'])) ?
36
- $subscriber['id'] :
37
- $subscriber,
38
- 'subscriber_token' => (!empty($subscriber['id'])) ?
39
- Subscriber::generateToken($subscriber['email']) :
40
- false,
41
- 'queue_id' => (!empty($queue['id'])) ?
42
- $queue['id'] :
43
- $queue,
44
- 'preview' => $preview
45
- );
46
  return Router::buildRequest(
47
  ViewInBrowserEndpoint::ENDPOINT,
48
  ViewInBrowserEndpoint::ACTION_VIEW,
49
  $data
50
  );
51
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
1
  <?php
2
  namespace MailPoet\Newsletter;
3
 
 
4
  use MailPoet\Router\Router;
5
  use MailPoet\Router\Endpoints\ViewInBrowser as ViewInBrowserEndpoint;
6
+ use MailPoet\Models\Newsletter as NewsletterModel;
7
+ use MailPoet\Models\Subscriber as SubscriberModel;
8
 
9
  class Url {
10
+ const TYPE_ARCHIVE = 'display_archive';
11
+ const TYPE_LISTING_EDITOR = 'display_listing_editor';
12
+
13
  static function getViewInBrowserUrl(
14
+ $type,
15
+ NewsletterModel $newsletter,
16
  $subscriber = false,
17
  $queue = false,
18
  $preview = false
19
  ) {
20
+ if($subscriber instanceof SubscriberModel) {
21
+ $subscriber->token = SubscriberModel::generateToken($subscriber->email);
 
 
 
 
 
 
22
  }
23
+ switch($type) {
24
+ case self::TYPE_ARCHIVE:
25
+ // do not expose newsletter id when displaying archive newsletters
26
+ $newsletter->id = null;
27
+ $preview = true;
28
+ break;
29
+ case self::TYPE_LISTING_EDITOR:
30
+ // enable preview and hide newsletter hash when displaying from editor or listings
31
+ $newsletter->hash = null;
32
+ $preview = true;
33
+ break;
34
+ default:
35
+ // hide hash for all other display types
36
+ $newsletter->hash = null;
37
+ break;
38
  }
39
+ $data = self::createUrlDataObject($newsletter, $subscriber, $queue, $preview);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  return Router::buildRequest(
41
  ViewInBrowserEndpoint::ENDPOINT,
42
  ViewInBrowserEndpoint::ACTION_VIEW,
43
  $data
44
  );
45
  }
46
+
47
+ static function createUrlDataObject($newsletter, $subscriber, $queue, $preview) {
48
+ return array(
49
+ (!empty($newsletter->id)) ?
50
+ (int)$newsletter->id :
51
+ 0,
52
+ (!empty($newsletter->hash)) ?
53
+ $newsletter->hash :
54
+ 0,
55
+ (!empty($subscriber->id)) ?
56
+ (int)$subscriber->id :
57
+ 0,
58
+ (!empty($subscriber->token)) ?
59
+ $subscriber->token :
60
+ 0,
61
+ (!empty($queue->id)) ?
62
+ (int)$queue->id :
63
+ 0,
64
+ (int)$preview
65
+ );
66
+ }
67
+
68
+ static function transformUrlDataObject($data) {
69
+ reset($data);
70
+ if (!is_int(key($data))) return $data;
71
+ $transformed_data = array();
72
+ $transformed_data['newsletter_id'] = (!empty($data[0])) ? $data[0] : false;
73
+ $transformed_data['newsletter_hash'] = (!empty($data[1])) ? $data[1] : false;
74
+ $transformed_data['subscriber_id'] = (!empty($data[2])) ? $data[2] : false;
75
+ $transformed_data['subscriber_token'] = (!empty($data[3])) ? $data[3] : false;
76
+ $transformed_data['queue_id'] = (!empty($data[4])) ? $data[4] : false;
77
+ $transformed_data['preview'] = (!empty($data[5])) ? $data[5] : false;
78
+ return $transformed_data;
79
+ }
80
  }
lib/Newsletter/ViewInBrowser.php CHANGED
@@ -2,13 +2,17 @@
2
  namespace MailPoet\Newsletter;
3
 
4
  use MailPoet\Models\Setting;
 
5
  use MailPoet\Newsletter\Links\Links;
6
  use MailPoet\Newsletter\Renderer\Renderer;
7
  use MailPoet\Newsletter\Shortcodes\Shortcodes;
8
 
9
  class ViewInBrowser {
10
  function view($data) {
11
- $wp_user_preview = ($data->preview && $data->subscriber->isWPUser());
 
 
 
12
  return $this->renderNewsletter(
13
  $data->newsletter,
14
  $data->subscriber,
@@ -18,25 +22,36 @@ class ViewInBrowser {
18
  }
19
 
20
  function renderNewsletter($newsletter, $subscriber, $queue, $wp_user_preview) {
21
- if($queue && $queue->newsletter_rendered_body) {
22
- $newsletter_body = $queue->getNewsletterRenderedBody();
 
 
 
 
 
 
 
 
 
 
 
 
23
  } else {
24
  $renderer = new Renderer($newsletter, $wp_user_preview);
25
- $newsletter_body = $renderer->render();
26
  }
27
  $shortcodes = new Shortcodes(
28
  $newsletter,
29
  $subscriber,
30
- $queue
31
-
32
  );
33
- $rendered_newsletter = $shortcodes->replace($newsletter_body['html']);
34
- if($queue && (boolean)Setting::getValue('tracking.enabled')) {
35
  $rendered_newsletter = Links::replaceSubscriberData(
36
  $subscriber->id,
37
  $queue->id,
38
- $rendered_newsletter,
39
- $wp_user_preview
40
  );
41
  }
42
  return $rendered_newsletter;
2
  namespace MailPoet\Newsletter;
3
 
4
  use MailPoet\Models\Setting;
5
+ use MailPoet\Models\Subscriber;
6
  use MailPoet\Newsletter\Links\Links;
7
  use MailPoet\Newsletter\Renderer\Renderer;
8
  use MailPoet\Newsletter\Shortcodes\Shortcodes;
9
 
10
  class ViewInBrowser {
11
  function view($data) {
12
+ $wp_user_preview = (
13
+ ($data->subscriber && $data->subscriber->isWPUser() && $data->preview) ||
14
+ ($data->preview && $data->newsletter_hash)
15
+ );
16
  return $this->renderNewsletter(
17
  $data->newsletter,
18
  $data->subscriber,
22
  }
23
 
24
  function renderNewsletter($newsletter, $subscriber, $queue, $wp_user_preview) {
25
+ if($queue && $queue->getNewsletterRenderedBody()) {
26
+ $newsletter_body = $queue->getNewsletterRenderedBody('html');
27
+ // rendered newsletter body has shortcodes converted to links; we need to
28
+ // isolate "view in browser", "unsubscribe" and "manage subscription" links
29
+ // and convert them to shortcodes, which later will be replaced with "#" when
30
+ // newsletter is previewed
31
+ if($wp_user_preview && preg_match(Links::getLinkRegex(), $newsletter_body)) {
32
+ $newsletter_body = Links::convertHashedLinksToShortcodesAndUrls(
33
+ $newsletter_body,
34
+ $convert_all = true
35
+ );
36
+ // remove open tracking link
37
+ $newsletter_body = str_replace(Links::DATA_TAG_OPEN, '', $newsletter_body);
38
+ }
39
  } else {
40
  $renderer = new Renderer($newsletter, $wp_user_preview);
41
+ $newsletter_body = $renderer->render('html');
42
  }
43
  $shortcodes = new Shortcodes(
44
  $newsletter,
45
  $subscriber,
46
+ $queue,
47
+ $wp_user_preview
48
  );
49
+ $rendered_newsletter = $shortcodes->replace($newsletter_body);
50
+ if(!$wp_user_preview && $queue && $subscriber && (boolean)Setting::getValue('tracking.enabled')) {
51
  $rendered_newsletter = Links::replaceSubscriberData(
52
  $subscriber->id,
53
  $queue->id,
54
+ $rendered_newsletter
 
55
  );
56
  }
57
  return $rendered_newsletter;
lib/Router/Endpoints/Track.php CHANGED
@@ -5,6 +5,7 @@ use MailPoet\Models\Newsletter;
5
  use MailPoet\Models\NewsletterLink;
6
  use MailPoet\Models\SendingQueue;
7
  use MailPoet\Models\Subscriber;
 
8
  use MailPoet\Statistics\Track\Clicks;
9
  use MailPoet\Statistics\Track\Opens;
10
 
@@ -35,10 +36,10 @@ class Track {
35
  }
36
 
37
  function _processTrackData($data) {
38
- $data = (object)$data;
39
  if(empty($data->queue_id) ||
40
- empty($data->subscriber_id) ||
41
- empty($data->subscriber_token)
42
  ) {
43
  return false;
44
  }
5
  use MailPoet\Models\NewsletterLink;
6
  use MailPoet\Models\SendingQueue;
7
  use MailPoet\Models\Subscriber;
8
+ use MailPoet\Newsletter\Links\Links;
9
  use MailPoet\Statistics\Track\Clicks;
10
  use MailPoet\Statistics\Track\Opens;
11
 
36
  }
37
 
38
  function _processTrackData($data) {
39
+ $data = (object)Links::transformUrlDataObject($data);
40
  if(empty($data->queue_id) ||
41
+ empty($data->subscriber_id) ||
42
+ empty($data->subscriber_token)
43
  ) {
44
  return false;
45
  }
lib/Router/Endpoints/ViewInBrowser.php CHANGED
@@ -1,13 +1,17 @@
1
  <?php
2
  namespace MailPoet\Router\Endpoints;
3
 
 
4
  use MailPoet\Models\Newsletter;
5
  use MailPoet\Models\SendingQueue;
6
  use MailPoet\Models\Subscriber;
 
7
  use MailPoet\Newsletter\ViewInBrowser as NewsletterViewInBrowser;
8
 
9
  if(!defined('ABSPATH')) exit;
10
 
 
 
11
  class ViewInBrowser {
12
  const ENDPOINT = 'view_in_browser';
13
  const ACTION_VIEW = 'view';
@@ -24,37 +28,52 @@ class ViewInBrowser {
24
  }
25
 
26
  function _processBrowserPreviewData($data) {
27
- $data = (object)$data;
28
- if(empty($data->subscriber_id) ||
29
- empty($data->subscriber_token) ||
30
- empty($data->newsletter_id)
31
- ) {
32
  $this->_abort();
33
- } else {
34
- $data->newsletter = Newsletter::findOne($data->newsletter_id);
35
- $data->subscriber = Subscriber::findOne($data->subscriber_id);
36
- $data->queue = ($data->queue_id) ?
37
- SendingQueue::findOne($data->queue_id) :
38
- false;
39
- return ($this->_validateBrowserPreviewData($data)) ?
40
- $data :
41
- $this->_abort();
42
- }
43
  }
44
 
45
  function _validateBrowserPreviewData($data) {
46
- if(!$data || !$data->subscriber || !$data->newsletter) return false;
47
- $subscriber_token_match =
48
- Subscriber::verifyToken($data->subscriber->email, $data->subscriber_token);
49
- if(!$subscriber_token_match) return false;
50
- // return if this is a WP user previewing the newsletter
51
- if($data->subscriber->isWPUser() && $data->preview) {
52
- return $data;
53
- }
54
- // if queue exists, check if the newsletter was sent to the subscriber
55
- if($data->queue && !$data->queue->isSubscriberProcessed($data->subscriber->id)) {
56
- $data = false;
 
 
 
 
57
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  return $data;
59
  }
60
 
1
  <?php
2
  namespace MailPoet\Router\Endpoints;
3
 
4
+ use MailPoet\Config\Env;
5
  use MailPoet\Models\Newsletter;
6
  use MailPoet\Models\SendingQueue;
7
  use MailPoet\Models\Subscriber;
8
+ use MailPoet\Newsletter\Url as NewsletterUrl;
9
  use MailPoet\Newsletter\ViewInBrowser as NewsletterViewInBrowser;
10
 
11
  if(!defined('ABSPATH')) exit;
12
 
13
+ require_once(ABSPATH . 'wp-includes/pluggable.php');
14
+
15
  class ViewInBrowser {
16
  const ENDPOINT = 'view_in_browser';
17
  const ACTION_VIEW = 'view';
28
  }
29
 
30
  function _processBrowserPreviewData($data) {
31
+ $data = (object)NewsletterUrl::transformUrlDataObject($data);
32
+ return ($this->_validateBrowserPreviewData($data)) ?
33
+ $data :
 
 
34
  $this->_abort();
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
  function _validateBrowserPreviewData($data) {
38
+ // either newsletter ID or hash must be defined, and newsletter must exist
39
+ if(empty($data->newsletter_id) && empty($data->newsletter_hash)) return false;
40
+ $data->newsletter = (!empty($data->newsletter_hash)) ?
41
+ Newsletter::getByHash($data->newsletter_hash) :
42
+ Newsletter::findOne($data->newsletter_id);
43
+ if(!$data->newsletter) return false;
44
+
45
+ // subscriber is optional; if exists, token must validate
46
+ $data->subscriber = (!empty($data->subscriber_id)) ?
47
+ Subscriber::findOne($data->subscriber_id) :
48
+ false;
49
+ if($data->subscriber) {
50
+ if(empty($data->subscriber_token) ||
51
+ !Subscriber::verifyToken($data->subscriber->email, $data->subscriber_token)
52
+ ) return false;
53
  }
54
+
55
+ // if newsletter ID is defined then subscriber must exist
56
+ if($data->newsletter_id && !$data->subscriber) return false;
57
+
58
+ // queue is optional; if defined, get it
59
+ $data->queue = (!empty($data->queue_id)) ?
60
+ SendingQueue::findOne($data->queue_id) :
61
+ SendingQueue::where('newsletter_id', $data->newsletter->id)->findOne();
62
+
63
+ // allow users with 'manage_options' permission to preview any newsletter
64
+ if(!empty($data->preview) && current_user_can(Env::$required_permission)
65
+ ) return $data;
66
+
67
+ // allow others to preview newsletters only when newsletter hash is defined
68
+ if(!empty($data->preview) && empty($data->newsletter_hash)
69
+ ) return false;
70
+
71
+ // if queue and subscriber exist, subscriber must have received the newsletter
72
+ if($data->queue &&
73
+ $data->subscriber &&
74
+ !$data->queue->isSubscriberProcessed($data->subscriber->id)
75
+ ) return false;
76
+
77
  return $data;
78
  }
79
 
lib/Subscription/Pages.php CHANGED
@@ -297,6 +297,10 @@ class Pages {
297
  ),
298
  'is_checked' => (
299
  $subscriber->status === Subscriber::STATUS_BOUNCED
 
 
 
 
300
  )
301
  )
302
  )
297
  ),
298
  'is_checked' => (
299
  $subscriber->status === Subscriber::STATUS_BOUNCED
300
+ ),
301
+ 'is_disabled' => true,
302
+ 'is_hidden' => (
303
+ $subscriber->status !== Subscriber::STATUS_BOUNCED
304
  )
305
  )
306
  )
mailpoet.php CHANGED
@@ -5,7 +5,7 @@ use MailPoet\Config\Initializer;
5
 
6
  /*
7
  * Plugin Name: MailPoet
8
- * Version: 3.0.0-beta.9
9
  * Plugin URI: http://www.mailpoet.com
10
  * Description: Create and send beautiful email newsletters, autoresponders, and post notifications without leaving WordPress. This is a beta version of our brand new plugin!
11
  * Author: MailPoet
@@ -24,7 +24,7 @@ use MailPoet\Config\Initializer;
24
  $mailpoet_loader = dirname(__FILE__) . '/vendor/autoload.php';
25
  if(file_exists($mailpoet_loader)) {
26
  require $mailpoet_loader;
27
- define('MAILPOET_VERSION', '3.0.0-beta.9');
28
  $initializer = new Initializer(
29
  array(
30
  'file' => __FILE__,
5
 
6
  /*
7
  * Plugin Name: MailPoet
8
+ * Version: 3.0.0-beta.10
9
  * Plugin URI: http://www.mailpoet.com
10
  * Description: Create and send beautiful email newsletters, autoresponders, and post notifications without leaving WordPress. This is a beta version of our brand new plugin!
11
  * Author: MailPoet
24
  $mailpoet_loader = dirname(__FILE__) . '/vendor/autoload.php';
25
  if(file_exists($mailpoet_loader)) {
26
  require $mailpoet_loader;
27
+ define('MAILPOET_VERSION', '3.0.0-beta.10');
28
  $initializer = new Initializer(
29
  array(
30
  'file' => __FILE__,
readme.txt CHANGED
@@ -3,7 +3,7 @@ Contributors: mailpoet, wysija
3
  Tags: newsletter, email, welcome email, post notification, autoresponder, mailchimp, signup, smtp
4
  Requires at least: 4.6
5
  Tested up to: 4.7
6
- Stable tag: 3.0.0-beta.9
7
  Create and send beautiful emails and newsletters from WordPress.
8
 
9
  == Description ==
@@ -83,13 +83,22 @@ Our [support site](https://docs.mailpoet.com/) has plenty of articles. You can w
83
 
84
  == Changelog ==
85
 
 
 
 
 
 
 
 
 
 
86
  = 3.0.0-beta.9 - 2016-12-20 =
87
- * Improved: the plugin is now tested up to WP 4.7
88
- * Improved: MailPoet's sending service bounce status API update
89
- * Improved: change duplicate subscribers import message to be more descriptive
90
- * Fixed: database character set and time zone setup
91
- * Fixed: alignment of post titles inside notificaiton emails
92
- * Fixed: partially generated or missing translations from .pot file
93
 
94
  = 3.0.0-beta.8 - 2016-12-13 =
95
  * Added: MailPoet's sending service can now sync hard bounced addresses with the plugin to keep your lists tidy and clean;
@@ -151,12 +160,12 @@ Our [support site](https://docs.mailpoet.com/) has plenty of articles. You can w
151
  * Fixed newsletter number shortcode for notification newsletters;
152
  * Enhanced HelpScout support beacon report with extra support data;
153
  * Fixed email renderer to not throw entity warnings on earlier PHP versions;
154
- * Fixed newsletter preview incompatibility errors for earlier PHP versions;
155
 
156
  = 3.0.0-beta.2 - 2016-10 =
157
 
158
  * Fixed compatibility issues with PHP versions earlier than PHP 5.6;
159
- * Renamed 'Emails' email type to 'Newsletters';
160
 
161
  = 3.0.0-beta.1 - 2016-10 =
162
 
3
  Tags: newsletter, email, welcome email, post notification, autoresponder, mailchimp, signup, smtp
4
  Requires at least: 4.6
5
  Tested up to: 4.7
6
+ Stable tag: 3.0.0-beta.10
7
  Create and send beautiful emails and newsletters from WordPress.
8
 
9
  == Description ==
83
 
84
  == Changelog ==
85
 
86
+ = 3.0.0-beta.10 - 2016-12-27 =
87
+ * Improved: newsletter is saved prior to sending an email preview;
88
+ * Improved: subscription management page conditionally displays the "bounced" status;
89
+ * Improved: deleted lists are displayed in newsletter listings;
90
+ * Fixed: newsletter/subscriber/list/form dates are properly formatted according to WP settings;
91
+ * Fixed: emails' "Return-path" header is set to the bounce address configured in Settings->Advanced;
92
+ * Fixed: archived newsletters' shortcode works for site visitors;
93
+ * Fixed: unicode support for newsletters.
94
+
95
  = 3.0.0-beta.9 - 2016-12-20 =
96
+ * Improved: the plugin is now tested up to WP 4.7;
97
+ * Improved: MailPoet's sending service bounce status API update;
98
+ * Improved: change duplicate subscribers import message to be more descriptive;
99
+ * Fixed: database character set and time zone setup;
100
+ * Fixed: alignment of post titles inside notificaiton emails;
101
+ * Fixed: partially generated or missing translations from .pot file.
102
 
103
  = 3.0.0-beta.8 - 2016-12-13 =
104
  * Added: MailPoet's sending service can now sync hard bounced addresses with the plugin to keep your lists tidy and clean;
160
  * Fixed newsletter number shortcode for notification newsletters;
161
  * Enhanced HelpScout support beacon report with extra support data;
162
  * Fixed email renderer to not throw entity warnings on earlier PHP versions;
163
+ * Fixed newsletter preview incompatibility errors for earlier PHP versions.
164
 
165
  = 3.0.0-beta.2 - 2016-10 =
166
 
167
  * Fixed compatibility issues with PHP versions earlier than PHP 5.6;
168
+ * Renamed 'Emails' email type to 'Newsletters'.
169
 
170
  = 3.0.0-beta.1 - 2016-10 =
171
 
vendor/autoload.php CHANGED
@@ -4,4 +4,4 @@
4
 
5
  require_once __DIR__ . '/composer/autoload_real.php';
6
 
7
- return ComposerAutoloaderInitba2ce543f2a04ae11d74280bc49a2040::getLoader();
4
 
5
  require_once __DIR__ . '/composer/autoload_real.php';
6
 
7
+ return ComposerAutoloaderInit9478c93be8e1a8371b13c6b1b2795224::getLoader();
vendor/composer/ClassLoader.php CHANGED
@@ -55,6 +55,7 @@ class ClassLoader
55
  private $classMap = array();
56
  private $classMapAuthoritative = false;
57
  private $missingClasses = array();
 
58
 
59
  public function getPrefixes()
60
  {
@@ -271,6 +272,26 @@ class ClassLoader
271
  return $this->classMapAuthoritative;
272
  }
273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  /**
275
  * Registers this instance as an autoloader.
276
  *
@@ -313,11 +334,6 @@ class ClassLoader
313
  */
314
  public function findFile($class)
315
  {
316
- // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
317
- if ('\\' == $class[0]) {
318
- $class = substr($class, 1);
319
- }
320
-
321
  // class map lookup
322
  if (isset($this->classMap[$class])) {
323
  return $this->classMap[$class];
@@ -325,6 +341,12 @@ class ClassLoader
325
  if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
326
  return false;
327
  }
 
 
 
 
 
 
328
 
329
  $file = $this->findFileWithExtension($class, '.php');
330
 
@@ -333,6 +355,10 @@ class ClassLoader
333
  $file = $this->findFileWithExtension($class, '.hh');
334
  }
335
 
 
 
 
 
336
  if (false === $file) {
337
  // Remember that this class does not exist.
338
  $this->missingClasses[$class] = true;
55
  private $classMap = array();
56
  private $classMapAuthoritative = false;
57
  private $missingClasses = array();
58
+ private $apcuPrefix;
59
 
60
  public function getPrefixes()
61
  {
272
  return $this->classMapAuthoritative;
273
  }
274
 
275
+ /**
276
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
277
+ *
278
+ * @param string|null $apcuPrefix
279
+ */
280
+ public function setApcuPrefix($apcuPrefix)
281
+ {
282
+ $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
283
+ }
284
+
285
+ /**
286
+ * The APCu prefix in use, or null if APCu caching is not enabled.
287
+ *
288
+ * @return string|null
289
+ */
290
+ public function getApcuPrefix()
291
+ {
292
+ return $this->apcuPrefix;
293
+ }
294
+
295
  /**
296
  * Registers this instance as an autoloader.
297
  *
334
  */
335
  public function findFile($class)
336
  {
 
 
 
 
 
337
  // class map lookup
338
  if (isset($this->classMap[$class])) {
339
  return $this->classMap[$class];
341
  if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
342
  return false;
343
  }
344
+ if (null !== $this->apcuPrefix) {
345
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
346
+ if ($hit) {
347
+ return $file;
348
+ }
349
+ }
350
 
351
  $file = $this->findFileWithExtension($class, '.php');
352
 
355
  $file = $this->findFileWithExtension($class, '.hh');
356
  }
357
 
358
+ if (null !== $this->apcuPrefix) {
359
+ apcu_add($this->apcuPrefix.$class, $file);
360
+ }
361
+
362
  if (false === $file) {
363
  // Remember that this class does not exist.
364
  $this->missingClasses[$class] = true;
vendor/composer/autoload_real.php CHANGED
@@ -2,7 +2,7 @@
2
 
3
  // autoload_real.php @generated by Composer
4
 
5
- class ComposerAutoloaderInitba2ce543f2a04ae11d74280bc49a2040
6
  {
7
  private static $loader;
8
 
@@ -19,15 +19,15 @@ class ComposerAutoloaderInitba2ce543f2a04ae11d74280bc49a2040
19
  return self::$loader;
20
  }
21
 
22
- spl_autoload_register(array('ComposerAutoloaderInitba2ce543f2a04ae11d74280bc49a2040', 'loadClassLoader'), true, true);
23
  self::$loader = $loader = new \Composer\Autoload\ClassLoader();
24
- spl_autoload_unregister(array('ComposerAutoloaderInitba2ce543f2a04ae11d74280bc49a2040', 'loadClassLoader'));
25
 
26
- $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
27
  if ($useStaticLoader) {
28
  require_once __DIR__ . '/autoload_static.php';
29
 
30
- call_user_func(\Composer\Autoload\ComposerStaticInitba2ce543f2a04ae11d74280bc49a2040::getInitializer($loader));
31
  } else {
32
  $map = require __DIR__ . '/autoload_namespaces.php';
33
  foreach ($map as $namespace => $path) {
@@ -48,19 +48,19 @@ class ComposerAutoloaderInitba2ce543f2a04ae11d74280bc49a2040
48
  $loader->register(true);
49
 
50
  if ($useStaticLoader) {
51
- $includeFiles = Composer\Autoload\ComposerStaticInitba2ce543f2a04ae11d74280bc49a2040::$files;
52
  } else {
53
  $includeFiles = require __DIR__ . '/autoload_files.php';
54
  }
55
  foreach ($includeFiles as $fileIdentifier => $file) {
56
- composerRequireba2ce543f2a04ae11d74280bc49a2040($fileIdentifier, $file);
57
  }
58
 
59
  return $loader;
60
  }
61
  }
62
 
63
- function composerRequireba2ce543f2a04ae11d74280bc49a2040($fileIdentifier, $file)
64
  {
65
  if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
66
  require $file;
2
 
3
  // autoload_real.php @generated by Composer
4
 
5
+ class ComposerAutoloaderInit9478c93be8e1a8371b13c6b1b2795224
6
  {
7
  private static $loader;
8
 
19
  return self::$loader;
20
  }
21
 
22
+ spl_autoload_register(array('ComposerAutoloaderInit9478c93be8e1a8371b13c6b1b2795224', 'loadClassLoader'), true, true);
23
  self::$loader = $loader = new \Composer\Autoload\ClassLoader();
24
+ spl_autoload_unregister(array('ComposerAutoloaderInit9478c93be8e1a8371b13c6b1b2795224', 'loadClassLoader'));
25
 
26
+ $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
27
  if ($useStaticLoader) {
28
  require_once __DIR__ . '/autoload_static.php';
29
 
30
+ call_user_func(\Composer\Autoload\ComposerStaticInit9478c93be8e1a8371b13c6b1b2795224::getInitializer($loader));
31
  } else {
32
  $map = require __DIR__ . '/autoload_namespaces.php';
33
  foreach ($map as $namespace => $path) {
48
  $loader->register(true);
49
 
50
  if ($useStaticLoader) {
51
+ $includeFiles = Composer\Autoload\ComposerStaticInit9478c93be8e1a8371b13c6b1b2795224::$files;
52
  } else {
53
  $includeFiles = require __DIR__ . '/autoload_files.php';
54
  }
55
  foreach ($includeFiles as $fileIdentifier => $file) {
56
+ composerRequire9478c93be8e1a8371b13c6b1b2795224($fileIdentifier, $file);
57
  }
58
 
59
  return $loader;
60
  }
61
  }
62
 
63
+ function composerRequire9478c93be8e1a8371b13c6b1b2795224($fileIdentifier, $file)
64
  {
65
  if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
66
  require $file;
vendor/composer/autoload_static.php CHANGED
@@ -4,7 +4,7 @@
4
 
5
  namespace Composer\Autoload;
6
 
7
- class ComposerStaticInitba2ce543f2a04ae11d74280bc49a2040
8
  {
9
  public static $files = array (
10
  '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
@@ -555,10 +555,10 @@ class ComposerStaticInitba2ce543f2a04ae11d74280bc49a2040
555
  public static function getInitializer(ClassLoader $loader)
556
  {
557
  return \Closure::bind(function () use ($loader) {
558
- $loader->prefixLengthsPsr4 = ComposerStaticInitba2ce543f2a04ae11d74280bc49a2040::$prefixLengthsPsr4;
559
- $loader->prefixDirsPsr4 = ComposerStaticInitba2ce543f2a04ae11d74280bc49a2040::$prefixDirsPsr4;
560
- $loader->prefixesPsr0 = ComposerStaticInitba2ce543f2a04ae11d74280bc49a2040::$prefixesPsr0;
561
- $loader->classMap = ComposerStaticInitba2ce543f2a04ae11d74280bc49a2040::$classMap;
562
 
563
  }, null, ClassLoader::class);
564
  }
4
 
5
  namespace Composer\Autoload;
6
 
7
+ class ComposerStaticInit9478c93be8e1a8371b13c6b1b2795224
8
  {
9
  public static $files = array (
10
  '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
555
  public static function getInitializer(ClassLoader $loader)
556
  {
557
  return \Closure::bind(function () use ($loader) {
558
+ $loader->prefixLengthsPsr4 = ComposerStaticInit9478c93be8e1a8371b13c6b1b2795224::$prefixLengthsPsr4;
559
+ $loader->prefixDirsPsr4 = ComposerStaticInit9478c93be8e1a8371b13c6b1b2795224::$prefixDirsPsr4;
560
+ $loader->prefixesPsr0 = ComposerStaticInit9478c93be8e1a8371b13c6b1b2795224::$prefixesPsr0;
561
+ $loader->classMap = ComposerStaticInit9478c93be8e1a8371b13c6b1b2795224::$classMap;
562
 
563
  }, null, ClassLoader::class);
564
  }
vendor/composer/installed.json CHANGED
@@ -14,7 +14,7 @@
14
  "reference": "b0c1bda3be5a35da44ba1ac28cc61c67d2ada465",
15
  "shasum": ""
16
  },
17
- "time": "2015-11-28 21:47:43",
18
  "type": "library",
19
  "installation-source": "dist",
20
  "autoload": {
@@ -52,7 +52,7 @@
52
  "require": {
53
  "php": ">=5.2.0"
54
  },
55
- "time": "2014-06-23 13:08:57",
56
  "type": "library",
57
  "installation-source": "dist",
58
  "autoload": {
@@ -113,7 +113,7 @@
113
  "j4mie/idiorm": "1.5.*",
114
  "php": ">=5.2.0"
115
  },
116
- "time": "2014-09-23 10:49:36",
117
  "type": "library",
118
  "installation-source": "dist",
119
  "autoload": {
@@ -177,7 +177,7 @@
177
  "require-dev": {
178
  "phpunit/phpunit": "~4.0|~5.0"
179
  },
180
- "time": "2016-01-26 21:23:30",
181
  "type": "library",
182
  "installation-source": "dist",
183
  "autoload": {
@@ -223,7 +223,7 @@
223
  "suggest": {
224
  "ext-mbstring": "For best performance"
225
  },
226
- "time": "2016-05-18 14:26:46",
227
  "type": "library",
228
  "extra": {
229
  "branch-alias": {
@@ -296,7 +296,7 @@
296
  "symfony/config": "",
297
  "symfony/yaml": ""
298
  },
299
- "time": "2016-10-18 04:28:30",
300
  "type": "library",
301
  "extra": {
302
  "branch-alias": {
@@ -351,7 +351,7 @@
351
  "require-dev": {
352
  "phpunit/phpunit": "~4.0|~5.0"
353
  },
354
- "time": "2015-11-04 20:07:17",
355
  "type": "library",
356
  "installation-source": "dist",
357
  "autoload": {
@@ -402,7 +402,7 @@
402
  "phpunit/phpunit": ">=4.0",
403
  "soundasleep/component-tests": "dev-master"
404
  },
405
- "time": "2016-07-28 01:09:53",
406
  "type": "library",
407
  "installation-source": "dist",
408
  "autoload": {
@@ -454,7 +454,7 @@
454
  "require-dev": {
455
  "mockery/mockery": "~0.9.1"
456
  },
457
- "time": "2016-07-08 11:51:25",
458
  "type": "library",
459
  "extra": {
460
  "branch-alias": {
@@ -509,7 +509,7 @@
509
  "require-dev": {
510
  "htmlawed/htmlawed": "dev-master"
511
  },
512
- "time": "2016-01-14 20:55:00",
513
  "type": "library",
514
  "installation-source": "dist",
515
  "autoload": {
@@ -564,7 +564,7 @@
564
  "symfony/debug": "~2.7",
565
  "symfony/phpunit-bridge": "~2.7"
566
  },
567
- "time": "2016-10-25 19:17:17",
568
  "type": "library",
569
  "extra": {
570
  "branch-alias": {
14
  "reference": "b0c1bda3be5a35da44ba1ac28cc61c67d2ada465",
15
  "shasum": ""
16
  },
17
+ "time": "2015-11-28T21:47:43+00:00",
18
  "type": "library",
19
  "installation-source": "dist",
20
  "autoload": {
52
  "require": {
53
  "php": ">=5.2.0"
54
  },
55
+ "time": "2014-06-23T13:08:57+00:00",
56
  "type": "library",
57
  "installation-source": "dist",
58
  "autoload": {
113
  "j4mie/idiorm": "1.5.*",
114
  "php": ">=5.2.0"
115
  },
116
+ "time": "2014-09-23T10:49:36+00:00",
117
  "type": "library",
118
  "installation-source": "dist",
119
  "autoload": {
177
  "require-dev": {
178
  "phpunit/phpunit": "~4.0|~5.0"
179
  },
180
+ "time": "2016-01-26T21:23:30+00:00",
181
  "type": "library",
182
  "installation-source": "dist",
183
  "autoload": {
223
  "suggest": {
224
  "ext-mbstring": "For best performance"
225
  },
226
+ "time": "2016-05-18T14:26:46+00:00",
227
  "type": "library",
228
  "extra": {
229
  "branch-alias": {
296
  "symfony/config": "",
297
  "symfony/yaml": ""
298
  },
299
+ "time": "2016-10-18T04:28:30+00:00",
300
  "type": "library",
301
  "extra": {
302
  "branch-alias": {
351
  "require-dev": {
352
  "phpunit/phpunit": "~4.0|~5.0"
353
  },
354
+ "time": "2015-11-04T20:07:17+00:00",
355
  "type": "library",
356
  "installation-source": "dist",
357
  "autoload": {
402
  "phpunit/phpunit": ">=4.0",
403
  "soundasleep/component-tests": "dev-master"
404
  },
405
+ "time": "2016-07-28T01:09:53+00:00",
406
  "type": "library",
407
  "installation-source": "dist",
408
  "autoload": {
454
  "require-dev": {
455
  "mockery/mockery": "~0.9.1"
456
  },
457
+ "time": "2016-07-08T11:51:25+00:00",
458
  "type": "library",
459
  "extra": {
460
  "branch-alias": {
509
  "require-dev": {
510
  "htmlawed/htmlawed": "dev-master"
511
  },
512
+ "time": "2016-01-14T20:55:00+00:00",
513
  "type": "library",
514
  "installation-source": "dist",
515
  "autoload": {
564
  "symfony/debug": "~2.7",
565
  "symfony/phpunit-bridge": "~2.7"
566
  },
567
+ "time": "2016-10-25T19:17:17+00:00",
568
  "type": "library",
569
  "extra": {
570
  "branch-alias": {