User:Patrick Welsh/common.css

  • /

//

// ======================== BEGIN DEPENDENCIES ========================

var rw_includes = {"dialogAnimations.default.css":"dialog[open]{-webkit-animation:show .25s ease normal}@-webkit-keyframes show{from{opacity:0;transform:scale(0.5) translateY(-10%)}to{opacity:1;transform:scale(1) translateY(0%)}}dialog.closeAnimate{-webkit-animation:close .25s ease normal}@-webkit-keyframes close{from{opacity:1;transform:scale(1) translateY(0%)}to{opacity:0;transform:scale(0.5) translateY(-10%)}}","dialogAnimations.none.css":"dialog[open]{-webkit-animation:show .01s ease normal}@-webkit-keyframes show{from{opacity:0}to{opacity:1}}dialog.closeAnimate{-webkit-animation:close .01s ease normal}@-webkit-keyframes close{from{opacity:1}to{opacity:0}}","dialogAnimations.spinny.css":"dialog[open]{-webkit-animation:show .25s ease normal}@-webkit-keyframes show{from{opacity:0;transform:scale(0.5) translateY(-10%) rotate(30deg)}to{opacity:1;transform:scale(1) translateY(0%) rotate(0deg)}}dialog.closeAnimate{-webkit-animation:close .25s ease normal}@-webkit-keyframes close{from{opacity:1;transform:scale(1) translateY(0%) rotate(0deg)}to{opacity:0;transform:scale(0.5) translateY(-10%) rotate(-30deg)}}","dialogAnimations.mega.css":"dialog[open]{-webkit-animation:show .25s ease normal}@-webkit-keyframes show{from{opacity:0;transform:scale(3)}to{opacity:1;transform:scale(1)}}dialog.closeAnimate{-webkit-animation:close .25s ease normal}@-webkit-keyframes close{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(3)}}","style.css":"@keyframes pgIconsFadeIn{from{opacity:0}to{opacity:1}}@keyframes pgIconsIndicatorsFadeIn{from{max-width:0}to{max-width:100%}}.mdl-dialog.rw-fullscreen-dialog{margin:0;max-width:100vw;max-height:100vh}.mw-indicators,#rwPGIconContainer{z-index:1;overflow:unset;float:right}#mw_header #rwPGIconContainer,#content #rwPGIconContainer{margin:0 0 0 12px;float:right;font-size:.875em;line-height:1.6;position:relative;overflow:hidden;height:2em}.skin-vector-legacy #content #rwPGIconContainer{padding-top:.4em}#rwCurrentRevRollbackBtns>div{overflow:hidden}#mw_header #rwPGIconContainer.rw--upgraded,#content #rwPGIconContainer.rw--upgraded{animation:pgIconsFadeIn .2s ease-in-out,pgIconsIndicatorsFadeIn .3s ease-in-out}#rwCurrentRevRollbackBtns>div,#rOld1,#rOld1 span,#rOld2,#rOld2 span{overflow:hidden}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1) forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1) forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(0.99);transform:scale(0.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(0.99);transform:scale(0.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}body>#dialogEngineContainer{color:black}body:not(.toast-active) #rw-toast{display:none}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:Roboto,Helvetica,Arial,sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(0.4,0,1,1);transition:transform .25s cubic-bezier(0.4,0,1,1),-webkit-transform .25s cubic-bezier(0.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0;border:0;color:rgb(83,109,254);float:right;padding:14px 24px 14px 12px;font-family:Roboto,Helvetica,Arial,sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:0;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-dialog{border:0;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-button{background:0;border:0;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:Roboto,Helvetica,Arial,sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(0.4,0,1,1),background-color .2s cubic-bezier(0.4,0,.2,1),color .2s cubic-bezier(0.4,0,.2,1);outline:0;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:rgb(33,150,243)}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type=submit]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:rgb(33,150,243);color:rgb(255,255,255)}.mdl-button--raised.mdl-button--colored:hover{background-color:rgb(33,150,243)}.mdl-button--raised.mdl-button--colored:active{background-color:rgb(33,150,243)}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:rgb(33,150,243)}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:rgb(83,109,254);color:rgb(255,255,255)}.mdl-button--fab.mdl-button--colored:hover{background-color:rgb(83,109,254)}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:rgb(83,109,254)}.mdl-button--fab.mdl-button--colored:active{background-color:rgb(83,109,254)}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:rgb(33,150,243)}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:rgb(255,255,255)}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(33,150,243)}.mdl-button--accent.mdl-button--accent{color:rgb(83,109,254)}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:rgb(255,255,255)}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(83,109,254)}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-menu__container{display:block;margin:0;padding:0;border:0;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:0;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0;transform-origin:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(0.4,0,.2,1),opacity .2s cubic-bezier(0.4,0,.2,1);transition:transform .3s cubic-bezier(0.4,0,.2,1),opacity .2s cubic-bezier(0.4,0,.2,1),-webkit-transform .3s cubic-bezier(0.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(0.4,0,.2,1),clip .3s cubic-bezier(0.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:0;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(0.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:0;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0;transition:width .2s cubic-bezier(0.4,0,.2,1)}.mdl-progress>.progressbar{background-color:rgb(33,150,243);z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(33,150,243),rgb(33,150,243));z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(33,150,243),rgb(33,150,243));-webkit-mask:url(data:image\/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=);mask:url(data:image\/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=)}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.9),rgba(255,255,255,.9)),linear-gradient(to right,rgb(33,150,243),rgb(33,150,243))}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:rgb(33,150,243);-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0;width:0}50%{left:25%;width:75%}75%{left:100%;width:0}}@keyframes indeterminate1{0%{left:0;width:0}50%{left:25%;width:75%}75%{left:100%;width:0}}@-webkit-keyframes indeterminate2{0%,50%{left:0;width:0}75%{left:0;width:25%}100%{left:100%;width:0}}@keyframes indeterminate2{0%,50%{left:0;width:0}75%{left:0;width:25%}100%{left:100%;width:0}}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:0}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid rgb(33,150,243)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(33,150,243,.26);background-color:rgba(33,150,243,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url(data:image\/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==);mask:url(data:image\/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==);background:0;transition-duration:.28s;transition-timing-function:cubic-bezier(0.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:rgb(33,150,243)\nurl(data:image\/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K)}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)\nurl(data:image\/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K)}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;mask-image:radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:rgb(33,150,243)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0}.rwHighlighter{width:1em;height:1em}","recentChanges.html":"`\r\n\r\n \r\n \r\n

`;

// global rw dialog css

content += ""

// Themes

let theme = "";

if (rw.config.colTheme) {

theme = rw.config.colTheme;

} else {

theme = "blue-indigo"; // default theme

}

content += `

${rw.config.neopolitan != null ? `

Attention: Tampering with RedWarn's permission system without good reason is prohibited. If you are seeing this message, your account has been automatically

flagged to RedWarn's team. We review abuse on a case by case basis. To dismiss this warning, remove all modifications and reset your RedWarn config manually at

you redwarnConfig.js user subpage. If you haven't tampered with RedWarn, you may be seeing this message due to a bug. If so, please let us know ASAP.

Close dialog



` : innerContent}

`;

return content; // return

},

/**

* Generates an iFrame with the specified HTML content, width and height.

* @method generateContainer

* @param {string} innerContent HTML content

* @param {number} width width of iFrame

* @param {number} height height of iFrame

* @param {boolean} fill if set true will expand the iframe to the parent offset height and width when resized, essentially filling the page.

* @returns {string} iFrame HTML

* @extends mdlContainers

*/

"generateContainer": function (innerContent, width, height, fill) { // fill sizes mdl containers in dialogEngine to ALWAYS be screen size

let style = "max-height: 100%;";

if (fill) {

// If fill mode on, fit to window

$(window).resize(()=>{

$(dialogEngine.dialog.getElementsByTagName("iframe")[0]).attr("height", window.innerWidth);

$(dialogEngine.dialog.getElementsByTagName("iframe")[0]).attr("width", window.innerWidth);

});

} else {

$(window).resize(()=>{}); // do nothing

}

let url = URL.createObjectURL(new Blob([mdlContainers.generateHtml(innerContent)], { type: 'text/html' })); // blob url

return ``;

}

};

// rw-source: rules.js

// Data originally processed from Twinkle Source at https://github.com/azatoth/twinkle

rw.rules = {};

// Functions for rw.rules

rw.rulesFunc = {

"resync": callback => {

$.getJSON("https://en.wikipedia.org/w/index.php?title=Wikipedia:RedWarn/Default_Warnings&action=raw&ctype=text/json", rules => {

// Sanitize (XSS SECURITY AND COVID-19!!)

for (const i in rules) {

rules[i].name = rules[i].name.replace(/[^\w\s!?-]/g, '');

rules[i].category = rules[i].category.replace(/[^\w\s!?-]/g, '');

rules[i].template = rules[i].template.replace(/[^\w\s!?-]/g, '');

}

rw.rules = rules;

// Now save

rw.rulesFunc.save(callback);

}).fail(() => {

// Download failed, maybe page is messed up? Either way don't callback

rw.ui.loadDialog.close();

rw.ui.confirmDialog("Sorry, we couldn't load the rule database due to an error.",

"OKAY", () => dialogEngine.closeDialog(),

"", () => { }, 0);

});

},

"load": callback => {

$.getJSON("https://en.wikipedia.org/w/index.php?title=User:" + encodeURIComponent(rw.info.getUsername()) + "/redwarnRules.json&action=raw&ctype=text/json", rules => {

if ($.isEmptyObject(rules)) {

// wee woo empty, resync

rw.rulesFunc.resync(callback);

return;

}

rw.rules = rules; // that's it lol

if (callback != null) callback();

}).fail(() => {

// Assume it doesn't exist

rw.rulesFunc.resync(callback);

});

},

"save": callback => {

// Stringify and save

$.post(rw.wikiAPI, { // LOCALISATION ISSUE!!

"action": "edit",

"format": "json",

"token": mw.user.tokens.get("csrfToken"),

"title": "User:" + rw.info.getUsername() + "/redwarnRules.json",

"summary": "Updating user Rule Database (RW " + rw.version + ")", // summary sign here

"text": JSON.stringify(rw.rules),

"tags": ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

}).done(dt => {

// We done. Check for errors, then callback appropriately

if (!dt.edit) {

// Error occured or other issue

console.error(dt);

rw.visuals.toast.show("Sorry, there was an error. See the console for more info. Your changes to your rules have not been saved.");

} else {

// Success!

if (callback != null) callback();

}

});

}

};

// rw-source: toast.js

// Used to manage the toast notifications

// init is seperate here as it isn't always needed or used

/*

EXAMPLE SYNTAX:

Required somewhere: rw.visuals.toast.init();

Then:

(for no button) rw.visuals.toast.show("Text", false, false, 2000);

(for button) rw.visuals.toast.show("Text", "BtnTxt", function() {

// your code here

}, 5000);

  • /

rw.visuals.toast = {

"active" : false,

"init" : function(){

if (!rw.visuals.toast.active) { // If init already done, no need

var body = $('body');

body.append(`

`); // init

(function() {

'use strict';

window['counter'] = 0;

var toast = document.querySelector('#rw-toast');

rw.visuals.register(toast); // register comp

// create function

rw.visuals.toast.show = (text, buttonTxt, btnClick, tOut) => {

'use strict';

body.addClass("toast-active");

if (buttonTxt) {

// Show with action and button

toast.MaterialSnackbar.showSnackbar({message: text, actionHandler: btnClick, actionText: buttonTxt, timeout: tOut});

} else {

// Show just message

toast.MaterialSnackbar.showSnackbar({message: text, timeout: tOut});

}

setTimeout(() => {

body.removeClass("toast-active");

}, tOut);

};

}());

// Init done. Register.

rw.visuals.toast.active = true;

}

},

"show" : function(text, buttonTxt, btnClick) {} // made in init()

}

// rw-source: info.js

/**

* rw.info performes misc. actions, inlcuding adding text to user pages, loading user config and more.

* @class rw.info

*/

// API calls ext.

rw.info = { // API

/**

* If a user is in the "rollbacker" user group, this will be automatically set via window.initRW() to the users rollback token.

* @property rollbackToken

* @type {string}

* @extends rw.info

*/

// Rollback token

"rollbackToken": "",

/**

* Sets rw.info.rollbackToken with the users rollback token if they have they are in the "rollbacker" user group.

* @method getRollbackToken

* @extends rw.info

*/

"getRollbackToken": () => {

// Ran on load to allow for ?action=rollback request

rw.info.featureRestrictPermissionLevel("rollbacker", () => {

$.getJSON(rw.wikiAPI + "?action=query&meta=tokens&type=rollback&format=json", r => {

rw.info.rollbackToken = r.query.tokens.rollbacktoken; // Set from response

});

}, () => { });

},

/**

* Gets the target username of an action, or username argument if defined.

*

* @param {string} un Optional: Username. If set, will just return this parameter.

* @returns {string} Target Username

* @method targetUsername

* @extends rw.info

*/

"targetUsername": un => {

if (un) { return un; } // return username if defined

if (mw.config.get("wgRelevantUserName") == null) {

// Try getting revision user and returning that

try {

const target = $($("#mw-diff-ntitle2").find(".mw-userlink")[0]).text();

if (target == "" || target == null) throw Error(); // go to catch if target is still empty

return target; // return target

} catch (error) {

// On error

// No target found, only show dialog if on userpage

if (mw.config.get("wgCanonicalNamespace").includes("User")) setTimeout(() => { // wait 500 ms to make sure we don't get overriden by a new opening dialog

// Close and show a note to the user

dialogEngine.closeDialog(() => rw.ui.confirmDialog(`

It looks like this user doesn't actually exist.

If you're trying to use a sandbox, try WP:UWSB instead.

Else, you should request the speedy deletion of this user page or user talk page under criterion U2 by adding:

${(true ? "\u007B\u007B" : "wacky formatting to not delete RW page")}Db-u2\u007D\u007D

to the top of this page. If you're still having issues, please let a member of the RedWarn team know.

`,

"OKAY", () => dialogEngine.closeDialog(),

"", null, 65));

}, 500);

return void 0; // to make sure other things handle it properly

}

}

return mw.config.get("wgRelevantUserName");

},

/**

* Gets the logged in user's username

*

* @returns {string} Logged in username

* @method getUsername

* @extends rw.info

*/

"getUsername": () => { return mw.config.get("wgUserName"); },

/**

* Detects if the given username is an IP address

*

* @param {string} un Username

* @returns {boolean}

* @method isUserAnon

* @extends rw.info

*/

"isUserAnon": un => {

// Detect if user is an IP address

let regEx = un.match(/([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(\d{1,3}\.){3}\d{1,3}/g); // this regex matches all ipv4 and ipv6 addresses. thanks: http://regexlib.com/UserPatterns.aspx?authorId=3e359e7e-cff5-4149-ba94-7baeae099d9c

return (regEx != null); // If matches is not null then yes

},

/**

* Sets rw.config to the current user config. If rw.config is already set, it will immediately callback.

*

* @param {function} callback

* @param {boolean} resetToDefault If set to true, the user config will be reset to default. Callback will not be called in these cases.

* @method getConfig

* @extends rw.info

*/

"getConfig": (callback, resetToDefault) => { // IF RESETTODEFAULT IS TRUE IT WILL DO IT

let defaultConfig = { // Default config on reset or anything like that

"lastVersion": rw.version

};

if (resetToDefault) { rw.config = defaultConfig; rw.rulesFunc.resync(); rw.info.writeConfig(); return; } // If reset to default, do it

if (rw.config) { callback(); } // if config loaded, no need to reload

// gets user config from their page.

let user = rw.info.getUsername();

$.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=User:" + encodeURIComponent(user) + "/redwarnConfig.js&rvslots=*&rvprop=content&formatversion=2&format=json", latestR => {

// Grab text from latest revision of talk page

// Check if exists

let revisionWikitext = "";

if (!latestR.query.pages[0].missing) { // If page isn't missing, i.e exists

revisionWikitext = latestR.query.pages[0].revisions[0].slots.main.content;

} else {

// Config doesn't exist we need to make it

console.log("creating config file");

rw.config = defaultConfig;

rw.info.writeConfig(true, () => { if (callback != null) callback(); }); // write new config file and callback if possible, else, add welcome screen here

return;

}

// Now that's done, verify config file / load it

try {

eval(revisionWikitext); // exec script

if (!rw.config) throw "no config";

// Process template packs (b64encoded string)

if (rw.config.templatePacks != null) {

rw.config.templatePacks = JSON.parse(deserialize(rw.config.templatePacks));

// Verify

if (typeof rw.config.templatePacks == "string") rw.config.templatePacks = JSON.parse(deserialize(rw.config.templatePacks)); // if issue, throw error and return to default

}

// Load rollback icons

if (rw.config.rwRollbackIcons != null) {

// More info in preferences.html and rollback.html

let newRwIcons = []; // object containing the new object

rw.config.rwRollbackIcons.forEach(icon => { // for each icon

// Add to ours at the new location

newRwIcons[icon.shift] = rw.rollback.icons[icon.index];

// Now modify for each modifier in modify object

for (const key in icon.modify) {

if (icon.modify.hasOwnProperty(key)) {

const value = icon.modify[key];

newRwIcons[icon.shift][key] = value; // apply modifier

}

}

// Set original index for preferences

newRwIcons[icon.shift].originalIndex = icon.index; // DO NOT set this to iconIndex as iconIndex is for rendering - this is for config and preferences ONLY

});

// Now update rwrollbackicons

rw.rollback.icons = newRwIcons;

}

if (rw.config.rwRollbackShorten == "enable") { // if rollback shortened

rw.rollback.icons.forEach((el, i) => {

el.name = el.name.replace("Quick rollback", "QRB"); // replace

el.name = el.name.replace("Rollback", "RB"); // replace

el.name = el.name.replace("rollback", "RB"); // replace

rw.rollback.icons[i] = el; // set back

});

}

// Load page icons

if (rw.config.rwPageIcons != null) {

// More info in preferences.html and rollback.html

let newRwIcons = []; // object containing the new object

rw.config.rwPageIcons.forEach(icon => { // for each icon

// Add to ours at the new location

newRwIcons[icon.shift] = rw.topIcons.icons[icon.index];

// Now modify for each modifier in modify object

for (const key in icon.modify) {

if (icon.modify.hasOwnProperty(key)) {

const value = icon.modify[key];

newRwIcons[icon.shift][key] = value; // apply modifier

}

}

// Set original index for preferences

newRwIcons[icon.shift].originalIndex = icon.index; // DO NOT set this to iconIndex as iconIndex is for rendering - this is for config and preferences ONLY

});

// Now update rwrollbackicons

rw.topIcons.icons = newRwIcons;

}

// Load rules, will handle all for us including creation, loading, etc.

rw.rulesFunc.load();

// Todo, process favourites and other modifiers

// Done!

} catch (err) {

// Corrupt config file

console.log(rw.config);

rw.config = defaultConfig;

console.error(err);

// Reset config file to defaults

rw.info.writeConfig(true, () => rw.ui.confirmDialog(`Sorry, but an issue has caused your RedWarn preferences to be reset to default. Would you like to report a bug?`,

"Report Bug", () => {

rw.ui.reportBug(`

redwarnConfig load - Error info:

${err.stack}

Open user redwarnConfig.js`);

},

"DISMISS", () => {

dialogEngine.closeDialog();

}, 20));

}

callback(); // we done

});

},

/**

* Writes to a users redwarnConfig.js file with the current configuration set in rw.config

*

* @param {boolean} noRedirect If false, the page will reload on completion and also show a loading dialog.

* @param {function} callback Callback function if noRedirect is set to true.

* @method writeConfig

* @extends rw.info

*/

"writeConfig": (noRedirect, callback) => { // CALLBACK ONLY IF NOREDIRECT IS TRUE.

// Save rule database first

rw.rulesFunc.save(() => {

// Then

let rwConfigTemplate = rw.config.templatePacks; // for restore

// Handle templates (saved as b64 string)

if (rw.config.templatePacks != null) rw.config.templatePacks = serialize(JSON.stringify(rw.config.templatePacks));

if (!noRedirect) rw.ui.loadDialog.show("Saving preferences...");

// Write config to the users page and refresh

let finalTxt = `

/*

This is your RedWarn configuration file. It is recommended that you don't edit this yourself and use RedWarn preferences instead.

It is writen in JSON formatting and is excecuted every time RedWarn loads.

If somebody has asked you to add code to this page, DO NOT do so as it may comprimise your account and will be reverted as soon as any configuration value changes.

!!! Do not edit below this line unless you understand the risks! If rw.config isn't defined, this file will be reset. !!!

  • /

window.rw = window.rw || {}, window.rw.config = ` + JSON.stringify(rw.config) + "; //"; // generate config text

$.post(rw.wikiAPI, { // LOCALISATION ISSUE!!

"action": "edit",

"format": "json",

"token": mw.user.tokens.get("csrfToken"),

"title": "User:" + rw.info.getUsername() + "/redwarnConfig.js",

"summary": "Updating user configuration (RW " + rw.version + ")", // summary sign here

"text": finalTxt,

"tags": ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

}).done(dt => {

// We done. Check for errors, then callback appropriately

if (!dt.edit) {

// Error occured or other issue

console.error(dt);

rw.visuals.toast.show("Sorry, there was an error. See the console for more info. Your changes have not been saved.");

} else {

// Success!

if (noRedirect) {

rw.config.templatePacks = rwConfigTemplate;

callback();

return;

}

; // DO NOT continue if no redirect is requested

window.location.hash = "#configChange";

window.location.reload(); // we done

}

});

});

},

/**

* Restrict this feature to a user group. This will be overridden if the user is in the "sysop" group.

*

* @param {string} l User group

* @param {function} callback Callback that will be called if user is in the defined user group.

* @param {function} callbackIfNot Callback that will be called if user is not in the defined user group.

* @method featureRestrictPermissionLevel

* @extends rw.info

*/

"featureRestrictPermissionLevel": (l, callback, callbackIfNot) => {

// Restrict feature to users in this group

mw.user.getGroups(g => {

let hasPerm = g.includes(l);

if (!hasPerm) hasPerm = g.includes("sysop"); // admins override all feature restrictions if we don't have them

if ((l == "confirmed") && !hasPerm) { hasPerm = g.includes("autoconfirmed"); } // Due to 2 types of confirmed user, confirmed and autoconfirmed, we have to check both

if (hasPerm) {

// Has the permission needed

if (callback) {

callback();

}

} else {

if (callbackIfNot) {

// Make no perm callback

callbackIfNot();

} else {

// Show no perm toast

rw.visuals.toast.show("Your account doesn't have permission to do that yet.", false, false, 5000);

}

}

});

},

/**

* Gets the related page for this action.

*

* @param {string} pg If set, this function will return this parameter

* @returns {string} Related page

* @method getRelatedPage

* @extends rw.info

*/

"getRelatedPage": (pg) => {

if (pg) { return pg; } // return page if defined

try {

let x = mw.util.getParamValue('vanarticle');

if (x != null) { return x; } else { return ""; }

} catch (er) {

// If none

return "error";

}

},

/**

* Uses MediaWiki's parser API to convert given WikiText to HTML

*

* @param {string} wikiTxt

* @param {function} callback callback(parsedHTML)

* @method parseWikitext

* @extends rw.info

*/

"parseWikitext": (wikiTxt, callback) => { // Uses Wikipedia's API to turn Wikitext to string. NEED TO USE POST IF USERPAGE IS LARGE EXT..

$.post(rw.wikiAPI, {

"action": "parse",

"format": "json",

"contentmodel": "wikitext",

"prop": "text",

"pst": true,

"assert": "user",

"text": wikiTxt,

"title": mw.config.get("wgRelevantPageName")

}).done(r => {

let processedResult = r.parse.text['*'].replace(/\/\//g, "https://").replace(/href=\"\/wiki/g, `href="${rw.wikiBase}/wiki`); // regex replace w direct urls

// sanitize

processedResult = rw.sanitizeHTML(processedResult);

callback(processedResult); // make callback w HTML

});

},

/**

* Detects and calls back with the highest warning level this user has recieved this month.

*

* @param {string} user

* @param {function} callback callback(int warningLevel [0 none 1 notice 2 caution 3 warning 4 final warning], string thisMonthsNotices (wikitext), string userPg (wikitext))

* @method lastWarningLevel

* @extends rw.info

*/

"lastWarningLevel": (user, callback) => { // callback(wLevel. thisMonthsNotices, userPg) 0 none 1 notice 2 caution 3 warning 4 final warning

// Get the last warning level of a user this month

$.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=User_talk:" + encodeURIComponent(user) + "&rvslots=*&rvprop=content&formatversion=2&format=json", latestR => {

// Grab text from latest revision of talk page

// Check if exists

let revisionWikitext = "";

if (!latestR.query.pages[0].missing) { // If page isn't missing, i.e exists

revisionWikitext = latestR.query.pages[0].revisions[0].slots.main.content;

} else {

// Return that record is clean as no past warnings due to page not existing

callback(0, "Talk page doesn't exist.", "Talk page doesn't exist."); // exit

return;

}

let wikiTxtLines = revisionWikitext.split("\n");

// let's continue

// Returns date in == Month Year == format and matches

let currentDateHeading = ((d) => { return "== " + ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][d.getMonth()] + " " + (1900 + d.getYear()) + " ==" })(new Date);

// rev13, add alt without space

let currentAltDateHeading = ((d) => { return "==" + ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][d.getMonth()] + " " + (1900 + d.getYear()) + "==" })(new Date);

let pageIncludesCurrentDate = wikiTxtLines.includes(currentDateHeading);

let pageIncludesCurrentAltDate = wikiTxtLines.includes(currentAltDateHeading);

if ((!pageIncludesCurrentDate) && (!pageIncludesCurrentAltDate)) {

// No warnings this month

callback(0, "No notices for this month.", revisionWikitext);

return;

} else if ((!pageIncludesCurrentDate) && (pageIncludesCurrentAltDate)) currentDateHeading = currentAltDateHeading; // If ==Date== is there but == Date == isn't, use ==Date== instead.

let highestWarningLevel = 0; // Set highest to nothing so if there is a date title w nothing in then that will be reported

let thisMonthsNotices = ""; // for dialog

// For each line

for (let i = wikiTxtLines.indexOf(currentDateHeading) + 1; i < wikiTxtLines.length; i++) {

if (wikiTxtLines[i].startsWith("==")) {

// New section

break; // exit the loop

}

// Check if it contains logo for each level

thisMonthsNotices += wikiTxtLines[i]; // Add to this months

if (wikiTxtLines[i].match(/(File:|Image:)Stop hand nuvola.svg/gi)) { // Level 4 warning

// This is the highest warning level. We can leave now

highestWarningLevel = 4;

break; // exit the loop

}

// Not using elseif in case of formatting ext..

if (wikiTxtLines[i].match(/(File:|Image:)(Nuvola apps important.svg|Ambox warning pn.svg)/gi)) { // Level 3 warning

highestWarningLevel = 3; // No need for if check as highest level exits

}

if (wikiTxtLines[i].match(/(File:|Image:)Information orange.svg/gi)) { // Level 2 warning

if (highestWarningLevel < 3) {

// We can set

highestWarningLevel = 2;

}

}

if (wikiTxtLines[i].match(/(File:|Image:)Information.svg/gi)) { // Level 1 notice

if (highestWarningLevel < 2) {

// We can set

highestWarningLevel = 1;

}

}

} // End for loop

callback(highestWarningLevel, thisMonthsNotices, revisionWikitext); // We done

});

},// End lastWarningLevel

/**

* Scans the past 50 revisions for warnings from this month for a user - WARNING: this is pretty CPU intensive - make sure you show a load dialog!

*

* @param {string} username

* @param {function} callback

* @method warningInfo

* @extends rw.info

*/

"warningInfo": (username, callback) => {

// Get past 51 page revisions, we calculate a diff for 50 only

$.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=User_talk:" + encodeURIComponent(username) + "&rvslots=*&rvprop=content|user|timestamp|size&formatversion=2&rvlimit=51&format=json", r => {

if (r.query.pages[0].missing) { // If page is missing, i.e it doesn't exist

callback([]); // nothing, no warnings recorded

return; // exit

}

let warningArray = []; // included in callback

// Now for each revision

r.query.pages[0].revisions.forEach((edit, i) => {

if (i == 49 || i > r.query.pages[0].revisions.length - 2) return; // we can't process the 51st edit, so exit

const editSize = edit.size - r.query.pages[0].revisions[i + 1].size; // size difference betweem this and the prev edit

if (editSize > 7500 || editSize < 0) return; // skip edits over 7.5KB, we can safely assume these aren't warnings are we don't wanna crash the browser, we also ignore removals

const editedBy = edit.user;

const editTimestamp = edit.timestamp;

const editContent = edit.slots.main.content;

// Find what was added in that edit by comparing last revision (index up)

let editChange = Diff.diffChars(r.query.pages[0].revisions[i + 1].slots.main.content, editContent);

// Merge all addition changes into one string

const addedWikiText = (() => { let result = ""; editChange.forEach(change => { if (change.added === true) result += change.value; }); return result; })();

// Now locate warnings within those changes

// Run regex on it

const regexResult = //gi.exec(addedWikiText);

if (regexResult == null) return; // no match, move on to next rev

// Note down the template

const warningTemplate = regexResult.pop(); // last in array = last warning name, we always favour the last one because warnings may have been restored

console.log("Located warning template uw-" + warningTemplate);

let warningLevel = 6; // assume 6 = unknown here

let matchedRule = { "name": "Unknown - this warning doesn't seem to be in RedWarn's database", "template": "uw-" + warningTemplate, "key": "" };

// Now locate within our rules

for (const ruleKey in rw.rules) {

if (rw.rules.hasOwnProperty.call(rw.rules, ruleKey)) {

const rule = rw.rules[ruleKey];

if (("uw-" + warningTemplate).includes(rule.template)) {

// Find warning level and map

warningLevel = ({

// handle nothing as a 0 reminder

undefined: 0,

"": 0,

"1": 1,

"2": 2,

"3": 3,

"4": 4,

"4im": 5

})[("uw-" + warningTemplate).replace(rule.template, "")]; // select by rming template from the regexMatch

rule.key = ruleKey; // set key for dialog

matchedRule = rule;

break; // we're done in this loop as we've found it

}

}

}

// We've finished looking through all the rules, so add it to the array

warningArray.push({

"from": editedBy,

"rule": matchedRule,

"level": warningLevel,

"timestamp": editTimestamp

});

});

// All done

callback(warningArray);

});

},

/**

* Adds given WikiText to a users talk page.

*

* @param {string} user Username of the account to add text to

* @param {string} text Wikitext to append

* @param {boolean} underDate If set true, the edit will be appended under this months date header, e.g. July 2020

* @param {string} summary The summary for this edit, excluding any RedWarn branding (this function automatically appends this)

* @param {string} blacklist If a userpage contains this text, the edit will not be made and the text in blackListToast will be shown in a toast message. Set to null to disable.

* @param {string} blacklistToast Toast message to show if blacklist is matched.

* @param {function} callback If no callback set, a saving message dialog will be shown and a redirect will occur on completion.

* @method addWikiTextToUserPage

* @extends rw.info

*/

"addWikiTextToUserPage": (user, text, underDate, summary, blacklist, blacklistToast, callback) => {

if ((user == null) || (user.toLowerCase() == "undefined") || (user.toLowerCase() == "null")) {

// Stop it from being sent to User:undefined or User:null

// TODO: Add callback because likely bug

rw.visuals.toast.show("Sorry, an error occured. (user undef.)");

return;

}

if (callback == null) rw.ui.loadDialog.show("Saving message..."); // show load if no callback

// Add text to a page. If underdate true, add it under a date marker

$.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=User_talk:" + encodeURIComponent(user) + "&rvslots=*&rvprop=content&formatversion=2&format=json", latestR => {

// Grab text from latest revision of talk page

// Check if exists

let revisionWikitext = "";

if (!latestR.query.pages[0].missing) { // If page isn't missing, i.e exists

revisionWikitext = latestR.query.pages[0].revisions[0].slots.main.content;

} // else we keep to ""

let wikiTxtLines = revisionWikitext.split("\n");

let finalTxt = "";

// Check blacklist (if defined)

if (blacklist) {

if (revisionWikitext.includes(blacklist)) {

// Don't continue and show toast

rw.ui.loadDialog.close();

rw.visuals.toast.show(blacklistToast, false, false, 5000);

return;

}

}

// let's continue

// Returns date in == Month Year == format and matches

let currentDateHeading = ((d) => { return "== " + ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][d.getMonth()] + " " + (1900 + d.getYear()) + " ==" })(new Date);

let pageIncludesCurrentDate = wikiTxtLines.includes(currentDateHeading);

// rev13, add alt without space (i.e ==Month Year==)

let currentAltDateHeading = ((d) => { return "==" + ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][d.getMonth()] + " " + (1900 + d.getYear()) + "==" })(new Date);

let pageIncludesCurrentAltDate = wikiTxtLines.includes(currentAltDateHeading);

if ((!pageIncludesCurrentDate) && (pageIncludesCurrentAltDate)) { // If ==Date== is there but == Date == isn't, use ==Date== instead.

currentDateHeading = currentAltDateHeading;

pageIncludesCurrentDate = true;

}

// Let's continue :)

if (underDate) {

if (pageIncludesCurrentDate) {

// Locate and add text in section

// Locate where the current date section ends so we can append ours to the bottom

let locationOfLastLine = wikiTxtLines.indexOf(currentDateHeading) + 1; // in case of date heading w nothing under it

for (let i = wikiTxtLines.indexOf(currentDateHeading) + 1; i < wikiTxtLines.length; i++) {

if (wikiTxtLines[i].startsWith("==")) {

// New section

locationOfLastLine = i - 1; // the line above is therefore the last

console.log("exiting loop: " + wikiTxtLines[locationOfLastLine]);

break; // exit the loop

} else if (i == wikiTxtLines.length - 1) {

// End of page, let's break and set location of last line.

locationOfLastLine = i;

break; // exit loop

}

}

console.log(locationOfLastLine);

if (locationOfLastLine == wikiTxtLines.length - 1) {

// To prevent to end notices squishing against eachother

// Same as without, but we just include the date string at bottom of page

wikiTxtLines.push("", text);

} else {

// Place right before the start of the next section.

wikiTxtLines.splice(locationOfLastLine + 1, 0, ...(

wikiTxtLines[locationOfLastLine].trim() == "" ? [text] : ["", text]

)); // Add notice to array at correct position. Note the "" is for a newline to seperate from prev content

}

} else { // Page doesn't have current date

// Same as without, but we just include the date string at bottom of page

wikiTxtLines.push("", currentDateHeading, text);

}

} else {

// No need to add to date. Just shove at the bottom of the page

wikiTxtLines.push("", text);

}

// Process final string

finalTxt = wikiTxtLines.join("\n"); // Remap to lines

const attemptEdit = () => {

// Push edit using CSRF token

$.post(rw.wikiAPI, {

"action": "edit",

"format": "json",

"token": mw.user.tokens.get("csrfToken"),

"title": "User talk:" + user,

"summary": summary + " (RW " + rw.version + ")", // summary sign here

"text": finalTxt.trim(),

"tags": ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

}).done(dt => {

// We done. Check for errors, then callback appropriately

if (!dt.edit) {

if (dt.error) {

// Check if this is an abusefilter error

if (dt.error.code === "abusefilter-warning") {

var abuseFilter = dt.error.abusefilter;

if (abuseFilter.actions.includes("disallow")) {

alert(`Your edit was disallowed by abuse filter #${abuseFilter.id} ("${abuseFilter.description}"). No changes have been made to the page.`);

} else if (abuseFilter.actions.includes("warn")) {

// Since I don't know how to make dialogs, I'll just copy

// the text from the warning page here. If someone can make

// a dialog with a preview of the warning, that would be great.

var abWarning = (abuseFilter.info || "No information provided.").replace(/⧼(.+?)⧽/g, `(see page "MediaWiki:$1")`);

if (abuseFilter.id == 602) {

// https://en.wikipedia.org/wiki/MediaWiki:Abusefilter-warning-DS

var abWarning = "You are trying to alert a user to the existence of discretionary sanctions.\n\nSpecial rules (https://en.wikipedia.org/wiki/Wikipedia:Arbitration_Committee/Discretionary_sanctions#Awareness_and_alerts) govern alerts. You must not given an editor an alert if they have already received one for the same area of conflict within the last twelve months. Please now check that this editor has not already been alerted to this area of conflict in the last twelve months.";

}

var abProceed = confirm(`Your edit was temporarily disallowed by an abuse filter in order to show you a warning:\n\n=====\n\n${abWarning}\n\n=====\n\nIf you wish to proceed, please confirm your decision.`);

if (abProceed) {

attemptEdit();

}

rw.ui.loadDialog.close();

return;

}

rw.ui.loadDialog.close();

return; // Stop here.

}

// Otherwise, continue standard error message.

}

// Error occured or other issue

console.error(dt);

rw.ui.loadDialog.close();

rw.visuals.toast.show("Sorry, there was an error. See the console for more info. Your message has not been sent.");

// Reshow dialog

dialogEngine.dialog.showModal();

} else {

// Success!

// Check if watchlist preference is enabled

if (rw.config.rwWatchTime != null) {

// Add page to watchlist

rw.info.watchPage("User talk:" + user, rw.config.rwWatchTime, wCb => {

// Was the page added to the watchlist successfully?

if (wCb != false || wCb != null) {

rw.visuals.toast.show("Page successfully added to watchlist");

} else {

rw.visuals.toast.show("Sorry, there was an error adding this page to your watchlist.");

}

});

}

if (callback != null) { callback(); return; }; // callback and stop if set, else..

// Redirect to complete page

let reloadNeeded = window.location.href.includes(rw.wikiBase + "/wiki/User_talk:" + encodeURIComponent(user)); // if we are already on the talk page we need to refresh as this would just change the hash

redirect(rw.wikiBase + "/wiki/User_talk:" + encodeURIComponent(user) + "#noticeApplied-" + dt.edit.newrevid + "-" + dt.edit.oldrevid); // go to talk page

if (reloadNeeded) { location.reload(); }

}

});

};

attemptEdit();

});

}, // end addTextToUserPage

/**

* Quick welcomes the given user. Deprecated in rev12.

*

* @param {string} un Username to append the welcome template to

* @method quickWelcome

* @extends rw.info

* @deprecated Use rw.quickTemplate instead.

*

*/

"quickWelcome": un => {

// Quickly welcome the current user

// Check if registered or unregistered user

if (rw.info.isUserAnon(rw.info.targetUsername(un))) {

// IP Editor - send IP welcome

rw.info.addWikiTextToUserPage(rw.info.targetUsername(un), "\n" + rw.welcomeIP() + " " + rw.sign() + "\n", false, "Welcome! (IP)");

} else {

// Registered user

rw.info.addWikiTextToUserPage(rw.info.targetUsername(un), "\n" + rw.welcome() + " " + rw.sign() + "\n", false, "Welcome!");

}

},

// Used for rollback

/**

* Check if the given revID is the latest revision of the given page name and will callback with the username of whoever made that edit

*

* @param {string} name Title of the page to check

* @param {string} revID Revision ID to check

* @param {function} callback callback(username) Will only be called if this is the latest revision, else will redirect to the latest revision diff page.

* @param {function} noRedirectCallback If set, this will be called instead of a redirect if it isn't the latest revision

* @method isLatestRevision

* @extends rw.info

*/

"isLatestRevision": (name, revID, callback, noRedirectCallback) => { // callback(username) only if successful!! in other cases, will REDIRECT to latest revison compare page

// Check if revsion is the latest revision

$.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=" + encodeURIComponent(name) + "&rvslots=*&rvprop=ids%7Cuser&formatversion=2&format=json", r => {

// We got the response

let latestRId = r.query.pages[0].revisions[0].revid;

let parentRId = r.query.pages[0].revisions[0].parentid;

let latestUsername = r.query.pages[0].revisions[0].user;

if (latestRId == revID) {

// Yup! Send the callback

callback(latestUsername, latestRId);

} else {

// Nope :(

// Check for a noredirect callback, if so, call and return

if (noRedirectCallback != null) { noRedirectCallback(); return; }

// Load the preview page of the latest one

try { if (dialogEngine.dialog.open) { return; } } catch (error) { } // DO NOT REDIRECT IF DIALOG IS OPEN.

// Redirect and open in new tab if requested

redirect(rw.wikiIndex + "?title=" + encodeURIComponent(name) + "&diff=" + latestRId + "&oldid=" + parentRId + "&diffmode=source#redirectLatestRevision", (rw.config.rwLatestRevisionOption == "newtab"));

}

});

},

/**

* Gets the latest revision not made by the specified user on the specified page. Will prepare a summary string for rollback-like reverts.

*

* @param {string} name Title of the page to check

* @param {string} username Username to exclude

* @param {function} callback callback(revisionWikiText, preparedRevertSummary, revisionID, parentRevisionID)

* @method latestRevisionNotByUser

* @extends rw.info

*/

"latestRevisionNotByUser": (name, username, callback) => { // CALLBACK revision, summaryText, rId

// Name is page name, username is bad username

$.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=" + encodeURIComponent(name) + "&rvslots=*&rvprop=ids%7Cuser%7Ccontent&rvexcludeuser=" + encodeURIComponent(username) + "&formatversion=2&format=json", r => {

// We got the response

let _r;

try {

_r = r.query.pages[0].revisions[0]; // get latest revision

if (_r == null) { throw "can't be null"; } // if empty error

} catch (error) {

// Probably no other edits. Redirect to history page and show the notice

redirect(rw.wikiIndex + "?title=" + encodeURIComponent(name) + "&action=history#rollbackFailNoRev");

return; // exit

}

let latestContent = _r.slots.main.content;

let summary = "Reverting edit(s) by " + username + " (talk) to rev. " + _r.revid + " by " + _r.user;

callback(latestContent, summary, _r.revid, _r.parentid);

});

},

/**

* Calls back with the pronouns for the users given gender

*

* @param {string} user Username to check

* @param {function} callback callback(pronouns) - either he/him, she/her, they/them.

* @method getUserPronouns

* @extends rw.info

*/

"getUserPronouns": (user, callback) => {

// Trying mediawiki api here rather than a jquery get

new mw.Api().get({

action: 'query',

list: 'users',

usprop: 'gender',

ususers: user

}).then(r => {

let gender = r.query.users[0].gender;

callback((gender == "male") ? "he/him" : ((gender == "female") ? "she/her" : "they/them")); // callback with our pronouns

});

},

/**

* Calls back with the edit count of the given user

*

* @param {string} user Username to check

* @param {function} callback callback(editCount)

*/

"getUserEditCount": (user, callback) => {

// Trying mediawiki api here rather than a jquery get

new mw.Api().get({

action: 'query',

list: 'users',

usprop: 'editcount',

ususers: user

}).then(r => {

callback(r.query.users[0].editcount); // edit count

});

},

/**

* Sends an email to the specified user

*

* @param {string} user Username to email

* @param {string} content Email content

* @method sendEmail

* @extends rw.info

*/

"sendEmail": (user, content) => {

rw.ui.loadDialog.show("Sending email...");

var params = {

action: 'emailuser',

target: user,

subject: `Email from User:${rw.info.getUsername()} (using RW)`, // i.e. email from User:RedWarn (using RW)

text: content,

ccme: rw.config.rwEmailCCMe != "disable", // by defauly copy back to me unless specifically disabled

format: 'json'

},

api = new mw.Api();

api.postWithToken('csrf', params).done((data) => {

console.log(data);

if (data.errors == null || data.errors.length < 1) {

// No errors, success!

rw.ui.loadDialog.close();

rw.ui.confirmDialog(`Email sent. A copy of your email has been sent to you.`,

"OKAY", () => {

dialogEngine.closeDialog();

},

"", () => { }, 0);

} else {

// Error may have occured - give them back the email bc we don't want to screw the user over

rw.ui.loadDialog.close();

rw.ui.confirmDialog(`

An error may have occured. Please check your inbox. If no email is sent to you soon, please try again.

Here is the email you were trying to send:

${content}

`,

"OKAY", () => {

dialogEngine.closeDialog();

},

"", () => { }, 50);

}

});

},

// CLASSES

/**

* RedWarn's "notify on change" feature, which watches for changes on a page, then notifies the user.

* @class rw.info.changeWatch

*/

"changeWatch": {// Watches for changes on a page, always latest version and notifies

/**

* Defines whether or not the feature is activated.

*

* @property active

* @type {boolean}

* @extends rw.info.changeWatch

*/

"active": false,

"timecheck": "",

/**

* Defines the latest revsion ID of this page if feature is enabled

*

* @property latestRevID

* @type {string}

* @extends rw.info.changeWatch

*/

"lastRevID": "",

/**

* Toggles this feature on/off

* @method toggle

* @extends rw.info.changeWatch

*/

"toggle": () => {

if (!rw.info.changeWatch.active) {

// We're not active, make UI changes

// Request notification perms

if (Notification.permission !== 'granted') Notification.requestPermission();

$(".rwSpyIcon").css("color", "green");

rw.topIcons.icons[rw.topIcons.icons.findIndex(i => i.title == "Alert on Change")].colorModifier = "green";

rw.visuals.toast.show("Alerts Enabled - please keep this tab open.");

rw.info.changeWatch.active = true;

// Get latest rev id

$.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=" + encodeURIComponent(mw.config.get("wgRelevantPageName")) + "&rvslots=*&rvprop=ids&formatversion=2&format=json", r => {

// We got the response, set our ID

rw.info.changeWatch.lastRevID = r.query.pages[0].revisions[0].revid;

rw.info.changeWatch.timecheck = setInterval(() => { // Check for new revision every 5 seconds

$.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=" + encodeURIComponent(mw.config.get("wgRelevantPageName")) + "&rvslots=*&rvprop=ids&formatversion=2&format=json", r2 => {

// Got response, compare

if (rw.info.changeWatch.lastRevID != r2.query.pages[0].revisions[0].revid) {

// New Revision! Redirect.

clearInterval(rw.info.changeWatch.timecheck); // clear updates

let latestRId = r2.query.pages[0].revisions[0].revid;

let parentRId = r2.query.pages[0].revisions[0].parentid;

if (windowFocused) {

// Redirect and don't do anything else

redirect(rw.wikiIndex + "?title=" + encodeURIComponent(mw.config.get("wgRelevantPageName")) + "&diff=" + latestRId + "&oldid=" + parentRId + "&diffmode=source#watchLatestRedirect");

} else {

// Push notification

document.title = "**New Edit!** " + document.title; // Add alert to title

if (Notification.permission !== 'granted') {

Notification.requestPermission();

} else {

let notification = new Notification('New Change to ' + mw.config.get("wgRelevantPageName"), {

icon: 'https://upload.wikimedia.org/wikipedia/commons/6/63/Wikipedia-logo.png',

body: 'Click here to view',

});

notification.onclick = function () {

window.focus(); // When focused, we'll redirect anyways

this.close(); // focus our tab and close notif

};

window.onfocus = function () {

// Redirect on focus

redirect(rw.wikiIndex + "?title=" + encodeURIComponent(mw.config.get("wgRelevantPageName")) + "&diff=" + latestRId + "&oldid=" + parentRId + "&diffmode=source#watchLatestRedirect");

};

}

}

}

});

}, 5000);

});

} else {

clearInterval(rw.info.changeWatch.timecheck); // clear updates

$(".rwSpyIcon").css("color", ""); // clear colour from icon

rw.topIcons.icons[rw.topIcons.icons.findIndex(i => i.title == "Alert on Change")].colorModifier = null;

rw.visuals.toast.show("Alerts Disabled.");

rw.info.changeWatch.active = false;

}

}

},

/**

* Adds a page to the user's watchlist

*

* @param {string} page Page title

* @param {string} expiry Relative natural language expiry (e.g. "1 week")

* @param {function} callback callback(successful) Bool of API result

* @method watchPage

* @extends rw.info

*/

"watchPage": (page, expiry, callback) => {

// https://www.mediawiki.org/wiki/API:Watch

// MediawikiJS api

const expireParam = expiry == 0 ? { unwatch: true } : { expiry: expiry };

new mw.Api().postWithToken('watch', { action: 'watch', titles: page, format: 'json', ...expireParam }).done((data) => {

if (data.errors == null || data.errors.length < 1) {

callback(true);

} else {

console.error(data);

callback(false);

}

});

}

};

// rw-source: rollback.js

rw.rollback = { // Rollback features - this is where the business happens, people!

"clickHandlers": {}, // set in code

"icons": [ // rev14, icon IDs and everything for current rollback - from left to right - usually loaded from config

// WARNING: CHANGING ORDER WILL MESS UP CONFIGS.

// DEFAULT ENABLED ICONS

{

"enabled": true, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback vandalism",

"color": "red", // css colour

"icon": "delete_forever",

"actionType": "rollback",

"promptReason": false, // add extra info? false = quick rollback, otherwise not

"summary": "Vandalism", // Set summary

"ruleIndex": "vandalism" // used for autowarn

},

{

"enabled": true, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback unexplained content removal",

"color": "orange", // css colour

"icon": "format_indent_increase",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Unexplained content removal", // Set summary

"ruleIndex": "delete" // used for autowarn

},

{

"enabled": true, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback non-constructive edit",

"color": "gold", // css colour

"icon": "work_outline",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "non-constructive" // Set summary

},

{

"enabled": true, // true is a default rollback icon, false can be added via preferences

"name": "Rollback",

"color": "blue", // css colour

"icon": "replay",

"actionType": "rollback",

"promptReason": true, // add extra info?

"summary": "" // Set summary

},

{

"enabled": true, // true is a default rollback icon, false can be added via preferences

"name": "Assume Good Faith and Rollback",

"color": "green", // css colour

"icon": "thumb_up",

"actionType": "rollback",

"promptReason": true, // add extra info?

"summary": "Reverting good faith edits" // Set summary

},

{

"enabled": true, // true is a default rollback icon, false can be added via preferences

"name": "Preview Rollback",

"color": "black", // css colour

"icon": "compare_arrows",

"actionType": "func",

"action": () => rw.rollback.preview() // Callback

},

{

"enabled": true, // true is a default rollback icon, false can be added via preferences

"name": "Quick Template",

"color": "black", // css colour

"icon": "library_add",

"actionType": "func",

"action": () => rw.rollback.welcomeRevUsr() // Callback

},

{

"enabled": true, // true is a default rollback icon, false can be added via preferences

"name": "More Options",

"color": "black", // css colour

"icon": "more_vert",

"actionType": "func",

"action": () => rw.ui.openExtendedOptionsDialog() // Callback

},

// END DEFAULT ENABLED ICONS

// DEFAULT DISABLED ICONS

// RED

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback 3RR",

"color": "red", // css colour

"icon": "filter_3",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "WP:3RR", // Set summary

"ruleIndex": "_3rr" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback personal attacks towards another editor",

"color": "red", // css colour

"icon": "offline_bolt",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Personal attack towards another editor (WP:NPA)", // Set summary

"ruleIndex": "npa" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback copyright violation",

"color": "red", // css colour

"icon": "copyright",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Likely copyright violation", // Set summary

"ruleIndex": "copyright" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback BLP violation",

"color": "red", // css colour

"icon": "face",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Fails WP:BLP", // Set summary

"ruleIndex": "biog" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback advertising/promotional",

"color": "red", // css colour

"icon": "monetization_on",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Using Wikipedia for advertising and/or promotion is not permitted.", // Set summary

"ruleIndex": "advert" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback unnecessary or inappropriate external links",

"color": "red", // css colour

"icon": "link_off",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Addition of unnecessary/inappropriate external links", // Set summary

"ruleIndex": "spam" // used for autowarn

},

// ORANGE

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback no reliable source",

"color": "orange", // css colour

"icon": "history_edu",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Not providing a reliable source", // Set summary

"ruleIndex": "unsourced" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback disruptive editing",

"color": "orange", // css colour

"icon": "error",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Disruptive editing", // Set summary

"ruleIndex": "disruptive" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback factual errors",

"color": "orange", // css colour

"icon": "menu_book",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "likely factual errors", // Set summary

"ruleIndex": "error" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback joke edit",

"color": "orange", // css colour

"icon": "child_care",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Joke edit", // Set summary

"ruleIndex": "joke" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback NPOV issues",

"color": "orange", // css colour

"icon": "campaign",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "per WP:NPOV", // Set summary

"ruleIndex": "npov" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback talk in article",

"color": "orange", // css colour

"icon": "announcement",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Please use the article talk page or be bold and fix the problem", // Set summary

"ruleIndex": "talkinarticle" // used for autowarn

},

// BLUE

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback manual of style issues",

"color": "blue", // css colour

"icon": "brush",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "Manual of Style issues", // Set summary

"ruleIndex": "mos" // used for autowarn

},

{

"enabled": false, // true is a default rollback icon, false can be added via preferences

"name": "Quick rollback test edits",

"color": "blue", // css colour

"icon": "build",

"actionType": "rollback",

"promptReason": false, // add extra info?

"summary": "test edits", // Set summary

"ruleIndex": "test" // used for autowarn

}

],

"loadIcons": () => { // Adds icons to a diff page - see rw.rollback.icons to set defaults here.

// Check if page is editable, if not, don't show

if (!mw.config.get("wgIsProbablyEditable")) {

// Can't edit, so exit

return;

}

// else, continue :)

let isLatest = $("#mw-diff-ntitle1").text().includes("Latest revision"); // is this the latest revision diff page?

let isLeftLatest = $("#mw-diff-otitle1").text().includes("Latest revision"); // is the left side the latest revision? (rev13 bug fix)

let currentRevIcons = "";

// Load Rollback current rev icons (rev14)

rw.rollback.icons.forEach((icon, i) => {

let elID = "rwRollback_" + i; // generate an ID for the new icons

// Establish element with all the info

if (icon.enabled) currentRevIcons += `

onclick="rw.rollback.clickHandlers.${elID}();">

${icon.icon}

${icon.name}

`;

// Add click handler

let clickHandler = () => { };

if (icon.actionType == "func") {

// Function callback

clickHandler = icon.action;

} else if (icon.actionType == "rollback") {

// Rollback on click

if (!icon.promptReason) {

// quick rollback

clickHandler = () => rw.rollback.apply(icon.summary, null, icon.ruleIndex); // if icon.ruleIndex is undef it'll be ignored anyway

} else {

// Ask for a reason

clickHandler = () => rw.rollback.promptRollbackReason(icon.summary);

}

}

// Now add the handler to the handlers object (because the elements aren't on page yet)

rw.rollback.clickHandlers[elID] = clickHandler; // we done

});

// Finally, wrap and add more info

currentRevIcons = eval(rw_includes["rollbackCurrentRevFormatting.html"]); // see HTML file

// RESTORE THIS VERSION ICONS. DO NOT FORGET TO CHANGE BOTH FOR LEFT AND RIGHT

let twinkleLoadedBeforeUs = $('div[id^="tw-revert"]').length;

// On left side

// DO NOT FORGET TO CHANGE BOTH!!

let left = isLeftLatest ? currentRevIcons : `

onclick="rw.rollback.promptRestoreReason($('#mw-diff-otitle1 > strong > a').attr('href').split('&')[1].split('=')[1]);">

history

Restore this version

`;

if (twinkleLoadedBeforeUs) {

$('.diff-otitle > div[id^="tw-revert"]').after(left);

} else {

$('.diff-otitle').prepend(left);

}

// On the right side

let right = isLatest ? currentRevIcons : `

onclick="rw.rollback.promptRestoreReason($('#mw-diff-ntitle1 > strong > a').attr('href').split('&')[1].split('=')[1]);">

history

Restore this version

`; // if the latest rev, show the accurate revs, else, don't

const twinkleNtitleLinks = $('.diff-ntitle > div[id^="tw-revert"]');

if (twinkleLoadedBeforeUs && twinkleNtitleLinks.length) {

$(twinkleNtitleLinks).after(right);

} else {

$('.diff-ntitle').prepend(right);

}

setTimeout(() => {

// Register all tooltips after 50ms (just some processing time)

for (let item of document.getElementsByClassName("mdl-tooltip")) {

rw.visuals.register(item);

}

// Register progressbar

var progressBar;

if ((progressBar = $("#rwRollbackInProgressBar")[0])) {

rw.visuals.register(progressBar);

}

}, 100);

},

"loadRevisionPermalinkPage": () => {

// add a purple restore button to the top of the page, and redirect to the cur comparison

// probably could have been done nicer, but this seems to work fine

$("#contentSub").prepend(`

style="cursor: pointer; font-size:28px; padding-right:5px; color:purple;"

onclick="location.search += '&diff=cur'"

> history

Restore this version

`);

setTimeout(() => {

// Register all tooltips after 50ms (just some processing time)

for (let item of document.getElementsByClassName("mdl-tooltip")) {

rw.visuals.register(item);

}

}, 100);

},

"getDisabledHTMLandHandlers": () => {

// Open a new dialog with all the disabled icons so user can select one. Click handlers are already registered, so we just call rw.rollback.clickHandlers.[elID]();

// Load Rollback current rev icons (rev14)

let finalIconStr = "";

rw.rollback.icons.forEach((icon, i) => {

if (icon.enabled) return; // if icon is enabled, we can skip

if (icon.name == "More Options") return; // does nothing here, so not needed

let elID = "rwRollback_" + i; // get the ID for the new icons

// Establish element with all the info

finalIconStr += `

${icon.icon}

40 ? `style="font-size: 12px;"` : "")}>${icon.name}


`;

// Add click event handler

addMessageHandler("rwRollbackBtn" + elID, () => {

dialogEngine.closeDialog(() => {// close dialog, then

rw.rollback.clickHandlers[elID](); // send our callback

});

});

});

// Now return HTML (rw16)

return finalIconStr;

},

"getRollbackrevID": () => { // Get the revision ID of what we want to rollback

let oldId = mw.config.get("wgDiffOldId");

let newId = mw.config.get("wgDiffNewId");

if (newId == null && oldId != null) {

return oldId;

} else if (newId != null && oldId != null) {

return newId > oldId ? newId : oldId;

} else {

let isNLatest = $("#mw-diff-ntitle1").text().includes("Latest revision");

let isOLatest = $("#mw-diff-otitle1").text().includes("Latest revision");

if (isNLatest) {

// Return the revID of the edit on the right

return $('#mw-diff-ntitle1 > strong > a').attr('href').split('&')[1].split('=')[1];

} else if (isOLatest) {

return $('#mw-diff-otitle1 > strong > a').attr('href').split('&')[1].split('=')[1];

} else {

// BUG!

rw.ui.confirmDialog("A very weird error occured. (rollback getRollbackRevID failed via final else!)",

"REPORT BUG", () => rw.ui.reportBug("rollback getRollbackRevID failed via final else! related URL: " + window.location.href),

"", () => { }, 0);

}

}

},

"preview": () => { // Redirect to the preview of the rollback in a new tab (compare page)

rw.ui.loadDialog.show("Loading preview...");

// Check if latest, else redirect

rw.info.isLatestRevision(mw.config.get("wgRelevantPageName"), rw.rollback.getRollbackrevID(), un => {

// Fetch latest revision not by user

rw.info.latestRevisionNotByUser(mw.config.get("wgRelevantPageName"), un, (content, summary, rID) => {

// Got it! Now open preview dialog

// Add handler for when page loaded

let url = rw.wikiIndex + "?title=" + mw.config.get("wgRelevantPageName") + "&diff=" + rID + "&oldid=" + mw.util.getParamValue("diff") + "&diffmode=source#rollbackPreview";

redirect(url); // goto in current tab

});

});

},

"apply": (reason, callback, defaultWarnIndex) => { // if callback set, no UW prompt will be shown, but a callback instead

// Now do

// bug fix rev10, get revid from html

// added rev13 if has rollback perms and set to use in settings, use that - prompt first time

// Show progress bar

$("#rwCurrentRevRollbackBtns").hide();

$("#rwRollbackInProgress").show();

// Set progress status to buffer 25

rw.rollback.progressBar(0);

rw.info.isLatestRevision(mw.config.get("wgRelevantPageName"), rw.rollback.getRollbackrevID(), (un, crID) => {

// Set progress bar status

// Set handlers for each method

let pseudoRollbackCallback = () => { // pseudoRollback

// Set progress

rw.rollback.progressBar(25);

// Fetch latest revision not by user

rw.info.latestRevisionNotByUser(mw.config.get("wgRelevantPageName"), un, (content, summary, rID, pID) => {

rw.rollback.progressBar(70, 70);

// Verify that pID is NOT the thing rev we want to rollback, else it's been overwritten

if (pID == rw.rollback.getRollbackrevID()) {

// looks like that there is a newer revision! redirect to it.

rw.info.isLatestRevision(mw.config.get("wgRelevantPageName"), 0, () => { });

return; // stop here.

}

// Got it! Now set page content to summary

// Push UNDO using CSRF token

$.post(rw.wikiAPI, {

"action": "edit",

"format": "json",

"token": mw.user.tokens.get("csrfToken"),

"title": mw.config.get("wgRelevantPageName"),

"summary": summary + ": " + reason + " (RW " + rw.version + ")", // summary sign here

"undo": crID, // current

"undoafter": rID, // restore version

"tags": ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

}).done(dt => {

// We done. Check for errors, then callback appropriately

if (!dt.edit) {

// Error occured or other issue

console.error(dt);

// Show rollback icons again (todo)

$("#rwCurrentRevRollbackBtns").show();

$("#rwRollbackInProgress").hide();

rw.visuals.toast.show("Sorry, there was an error, likely an edit conflict. Your rollback has not been applied.");

} else {

// Success!

// Hide progressbar (todo)

rw.rollback.progressBar(100);

// Wait a bit (100ms) to stop loadDialog glitch

setTimeout(() => {

// If callback set, call it and exit, else continue

if (callback != null) { callback(); return; }

// Now show the done icons

rw.rollback.showRollbackDoneOps(un, defaultWarnIndex);

}, 100); // done!

}

});

});

};

let rollbackCallback = () => { // using rollback API

rw.rollback.progressBar(70, 70); // progress

// PUSH ROLLBACK

$.post(rw.wikiAPI, {

"action": "rollback",

"format": "json",

"token": rw.info.rollbackToken,

"title": mw.config.get("wgRelevantPageName"),

"summary": "Rollback edit(s) by " + un + " (talk): " + reason + " (RW " + rw.version + ")", // summary sign here

"user": un, // rollback user

"tags": ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

}).done(dt => {

// THESE CALLBACKS ARE NO INTERCHANGABLE!

// We done. Check for errors, then callback appropriately

if (!dt.rollback) {

// Error occured or other issue

console.error(dt);

// Show rollback icons again

$("#rwCurrentRevRollbackBtns").show();

$("#rwRollbackInProgress").hide();

rw.visuals.toast.show("Sorry, there was an error, likely an edit conflict. Your rollback has not been applied.");

} else {

// Success!

rw.rollback.progressBar(100); // progress

// Wait a bit (100ms) to stop loadDialog glitch

setTimeout(() => {

// If callback set, call it and exit, else continue

if (callback != null) { callback(); return; }

// Now show the done icons

rw.rollback.showRollbackDoneOps(un, defaultWarnIndex);

}, 100); // done!

}

});

};

// Check config for rollback perms

rw.info.featureRestrictPermissionLevel("rollbacker", () => {

// Check if config is set or not

if (rw.config.rollbackMethod == null) {

rw.ui.confirmDialog(`

You have rollback permissions!

Would you like to use the faster rollback API in future or continue using a rollback-like setting?

You can change this in your preferences at any time.`,

"USE ROLLBACK", () => {

dialogEngine.closeDialog();

rw.config.rollbackMethod = "rollback";

rw.info.writeConfig(true, () => rollbackCallback()); // save config and callback

},

"KEEP USING ROLLBACK-LIKE", () => {

dialogEngine.closeDialog();

rw.config.rollbackMethod = "pseudoRollback";

rw.info.writeConfig(true, () => pseudoRollbackCallback()); // save config and callback

}, 45);

} else {

// Config set, complete callback - remember, this is feature restricted so we won't get here without RB perms

if (rw.config.rollbackMethod == "rollback") { // Rollback selected

rollbackCallback(); // Do rollback

} else {

pseudoRollbackCallback(); // rollback-like

}

}

}, () => pseudoRollbackCallback()); // if no perms follow pseudo rollback

});

},

"restore": (revID, reason) => {

// Restore revision by ID

rw.ui.loadDialog.show("Restoring...");

// Ask API for latest revision

$.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=" + encodeURIComponent(mw.config.get("wgRelevantPageName")) + "&rvslots=*&rvprop=ids%7Cuser&formatversion=2&format=json", r => {

// We got the response

let crID = r.query.pages[0].revisions[0].revid;

// Ask API for the restore revision

$.getJSON(rw.wikiAPI + "?action=query&prop=revisions&rvprop=user&rvstartid=" + revID + "&rvendid=" + revID + "&titles=" + encodeURI(mw.config.get("wgRelevantPageName")) + "&formatversion=2&rvslots=*&format=json", r => {

let revUsr = r.query.pages[0].revisions[0].user; // get user

let summary = "Restoring revision " + revID + " by " + revUsr; // gen our summary

// Now we've got that, we just need to submit. the undo

$.post(rw.wikiAPI, {

"action": "edit",

"format": "json",

"token": mw.user.tokens.get("csrfToken"),

"title": mw.config.get("wgRelevantPageName"),

"summary": summary + (reason != null ? ": " + reason : "") + " (RW " + rw.version + ")", // summary sign here

"undo": crID, // current

"undoafter": revID, // restore version

"tags": ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

}).done(dt => {

// Request done. Check for errors, then go to the latest revision

if (!dt.edit) {

// Error occured or other issue

console.error(dt);

rw.ui.loadDialog.close();

rw.visuals.toast.show("Sorry, there was an error, likely an edit conflict. This edit has not been restored.");

} else {

rw.info.isLatestRevision(mw.config.get('wgRelevantPageName'), 0, () => { }); // we done, go to the latest revision

}

});

});

});

},

"promptRollbackReason": reason => {

rw.info.isLatestRevision(mw.config.get("wgRelevantPageName"), rw.rollback.getRollbackrevID(), un => { // validate is latest

// Show dialog then rollback

// Add submit handler

addMessageHandler("reason`*", rs => rw.rollback.apply(rs.split("`")[1])); // When reason recieved, submit rollback

// CREATE DIALOG

// MDL FULLY SUPPORTED HERE (container).

dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["rollbackReason.html"]), 500, 120)).showModal(); // 500x120 dialog, see rollbackReason.html for code

});

},

"promptRestoreReason": revID => {

// Prompt for reason to restore. very sim to rollback reason

let reason = ""; // Needed for rollback reason page - do not remove

// Add submit handler

addMessageHandler("reason`*", rs => rw.rollback.restore(revID, rs.split("`")[1])); // When reason recieved, submit rollback

// CREATE DIALOG

// MDL FULLY SUPPORTED HERE (container).

dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["rollbackReason.html"]), 500, 120)).showModal(); // 500x120 dialog, see rollbackReason.html for code

},

"welcomeRevUsr": () => {

// Send welcome to user who made most recent revision

rw.visuals.toast.show("Please wait...", false, false, 1000);

rw.info.isLatestRevision(mw.config.get("wgRelevantPageName"), rw.rollback.getRollbackrevID(), un => {

// We got the username, send the welcome

rw.quickTemplate.openSelectPack(un);

});

},

// CONTRIBS PAGE

"contribsPageIcons": () => { // Adds rollback/restore links

// For each (current) tag

$("span.mw-uctop").each((i, el) => {

// Add rollback options (${i} inserts i at that point to ensure it is a unique ID)

$(el).html(`current

 

prev  

rvv  

rb

Preview Rollback

Quick rollback Vandalism

Rollback

`);

});

// Now register tooltips

setTimeout(() => {

// Register all tooltips after 50ms (just some processing time)

for (let item of document.getElementsByClassName("mdl-tooltip")) {

rw.visuals.register(item);

}

}, 100);

},

"contribsPageRollbackPreview": i => {

// Get revision ID

let revID = $("#rw-currentRev" + i).closest("li").attr("data-mw-revid");

let pageName = $("#rw-currentRev" + i).closest("li").find("a.mw-contributions-title").text();

console.log(revID);

console.log(pageName);

rw.ui.loadDialog.show("Loading preview...");

// Now verify is still latest

rw.info.isLatestRevision(pageName, revID, un => {

// Is latest revision! Let's continue

// Fetch latest revision not by user

rw.info.latestRevisionNotByUser(pageName, un, (content, summary, rID) => {

// Assemble URL

let url = rw.wikiIndex + "?title=" + pageName + "&diff=" + rID + "&oldid=" + revID + "&diffmode=source#rollbackPreview";

redirect(url, true); // open URL in new tab

rw.ui.loadDialog.close(); // close load dialog

});

}, () => {

rw.ui.loadDialog.close(); // close load dialog

// Isn't the latest revision, set note to match

$("#rw-currentRev" + i).html(

`No longer the latest revision.`

);

});

},

"contribsPageRollbackVandal": i => {

// Get revision ID

let revID = $("#rw-currentRev" + i).closest("li").attr("data-mw-revid");

let pageName = $("#rw-currentRev" + i).closest("li").find("a.mw-contributions-title").text();

console.log(revID);

console.log(pageName);

$("#rw-currentRev" + i).html(

`reverting...`

);

// Now verify is still latest

rw.info.isLatestRevision(pageName, revID, un => {

// Is latest revision! Let's continue

// Overwrite function and values used on diff pages as we aren't on a diff page

rw.rollback.getRollbackrevID = () => { return revID; };

mw.config.values.wgRelevantPageName = pageName;

rw.rollback.apply("Vandalism (from contribs)", () => { // apply the rollback

// Rollback complete!

$("#rw-currentRev" + i).html(

`

}">reverted!`

);

});

}, () => {

// Isn't the latest revision, set note to match

$("#rw-currentRev" + i).html(

`No longer the latest revision.`

);

});

},

"contribsPageRollback": i => {

// First, prompt for reason

let reason = ""; // Needed for rollback reason page - do not remove

// Add submit handler

addMessageHandler("reason`*", rs => { // On submit

let rollbackReason = rs.split("`")[1];

// Get revision ID

let revID = $("#rw-currentRev" + i).closest("li").attr("data-mw-revid");

let pageName = $("#rw-currentRev" + i).closest("li").find("a.mw-contributions-title").text();

console.log(revID);

console.log(pageName);

$("#rw-currentRev" + i).html(

`reverting...`

);

// Now verify is still latest

rw.info.isLatestRevision(pageName, revID, un => {

// Is latest revision! Let's continue

// Overwrite function and values used on diff pages as we aren't on a diff page

rw.rollback.getRollbackrevID = () => { return revID; };

mw.config.values.wgRelevantPageName = pageName;

rw.rollback.apply(rollbackReason + " (from contribs)", () => { // apply the rollback

// Rollback complete!

$("#rw-currentRev" + i).html(

`

}">reverted!`

);

});

}, () => {

// Isn't the latest revision, set note to match

$("#rw-currentRev" + i).html(

`No longer the latest revision.`

);

});

});

// CREATE DIALOG

// MDL FULLY SUPPORTED HERE (container).

dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["rollbackReason.html"]), 500, 120)).showModal(); // 500x120 dialog, see rollbackReason.html for code

},

"progressBar": (progress, buffer) => {

// Only if set and existing (bug)

if ($("#rwRollbackInProgressBar").length < 1) return;

// Update the progress bar

$("#rwRollbackInProgressBar")[0].MaterialProgress.setProgress(progress);

$("#rwRollbackInProgressBar")[0].MaterialProgress.setBuffer(buffer);

},

"showRollbackDoneOps": (un, warnIndex) => {

// Clear get hidden handler to stop errors in more options menu

rw.rollback.getDisabledHTMLandHandlers = () => { return ""; }; // return w empty string

// Add click handlers

$("#RWRBDONEmrevPg").click(() => rw.info.isLatestRevision(mw.config.get('wgRelevantPageName'), 0, () => { })); // go to latest revision

$("#RWRBDONEnewUsrMsg").click(() => rw.ui.newMsg(un)); // send message

$("#RWRBDONEwelcomeUsr").click(() => rw.quickTemplate.openSelectPack(un)); // quick template

$("#RWRBDONEwarnUsr").click(() => rw.ui.beginWarn(false, un, mw.config.get("wgRelevantPageName"), null, null, null, (warnIndex != null ? warnIndex : null))); // Warn User

$("#RWRBDONEreportUsr").click(() => rw.ui.adminReportSelector(un)); // report to admin

// Now perform default (if set)

let rollbackDoneOption = rw.config.rwRollbackDoneOption;

if (rollbackDoneOption === "RWRBDONEwarnUsr" && un === rw.info.getUsername()) { // warning ourselves

rollbackDoneOption = "none";

}

// TODO: && instead of ||?

if ((rollbackDoneOption != null) || (rollbackDoneOption !== "none")) $(`#${rollbackDoneOption}`).click();

// Hides other icons and shows the rollback done options and also checks for defaults, also adds click handlers

$("#rwRollbackInProgress").fadeOut(() => { // fade out - looks smoother

$("#rwRollbackDoneIcons").fadeIn(); //show our icons

});

}

};

// rw-source: ui.js

/**

* Most of RedWarn's UI elements and functions that require user input are here.

* @class rw.ui

*/

// Most UI elements

// See also dialog.js (dialogEngine) and mdlContainer.js (mdlContainer)

rw.ui = {

"beginWarn": (ignoreWarnings, un, pg, customCallback, callback, hideUserInfo, autoSelectReasonIndex, advancedMode) => { // if customCallback = false, callback(templatestr) (rev12) autoSelectReasonIndex(rev13) for quick rollbacks for vandalism ext..

// Give user a warning (show dialog)

// If we're in advanced mode by default

if (rw.config.rwWarnUserAdvanced == "enable") advancedMode = true;

if (hideUserInfo === true) advancedMode = false; // force off if hiding user info

let autoLevelSelectEnable = (!hideUserInfo) && (rw.userIsNotEC == null) && (rw.config.rwautoLevelSelectDisable != "disable"); // If autolevelselect enabled (always disabled on hideUserInfo options), non-EC always disabled (rw16)

// Let's continue

// Assemble rule listbox

let finalListBox = "";

let currentHeading = "";

for (let i in rw.rules) {

if (!rw.rules.hasOwnProperty(i)) continue;

let rule = rw.rules[i];

// Check if category is different to current heading first

if (rule.category != currentHeading) {

// Now generate a new heading and section for search to hide

finalListBox += `

${rule.category}
`;

// A new heading is needed

currentHeading = rule.category; // set to ours for detection

}

// Add appropriate list format per config

if (rw.config.rwNoticeListByTemplateName != "enable") {

// Standard listing by rule description

let style = "";

if (rule.name.length > 62) {

// Too long to fit

style = "font-size:14px";

} // TODO 03/01/2021:

finalListBox += `

class="mdl-menu__item"

data-val="${i}"

onmousedown="refreshLevels('${i}');"

style="${style}">

${rule.name}

`; // add dataselected if = autoSelectReasonIndex & autoselect is enabled

} else {

// List by template name

let style = "";

if (rule.name.length > 62) {

// Too long to fit

style = "font-size:14px"; // style here applies to span that will show on hover

}

finalListBox += `

class="mdl-menu__item"

data-val="`+ i + `"

id="rwTemplateSelect${i}"

onmousedown="refreshLevels('${i}');">

${rule.template}

`;

}

};

finalListBox += ``; // close final catagory

// Setup preview handling

addMessageHandler("generatePreview`*", m => {

rw.info.parseWikitext(m.split("`")[1], parsed => { // Split to Wikitext and send over to the API to be handled

dialogEngine.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage({

"action": "parseWikiTxt",

"result": parsed

}, '*'); // push to container for handling in dialog and add https:// to stop image breaking

});

});

// Add toast handler

addMessageHandler("pushToast`*", m => rw.visuals.toast.show(m.split('`')[1], false, false, 5000));

// Add admin report handler

addMessageHandler("adminR", () => rw.ui.adminReportSelector(un));

// Add toggle advanced mode handler (RW16.1)

addMessageHandler("advancedToggle", () => {

// Close and reopen with same args but toggle advancedMode

dialogEngine.closeDialog(() => rw.ui.beginWarn(ignoreWarnings, un, pg, customCallback, callback, hideUserInfo, autoSelectReasonIndex, (advancedMode === true ? false : true)));

});

// Add recent page handelr

addMessageHandler("openRecentPageSelector", () => rw.ui.recentlyVisitedSelector.showDialog(p => {

// Send page back to container

dialogEngine.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage({

"action": "recentPage",

"result": p.replace(/_/g, ' ')

}, '*');

}));

// Add submit handler

addMessageHandler("applyNotice`*", eD => {

// i.e applyNotice`user`wikitext`summary

// TODO: maybe b64 encode?

let _eD = eD.split("`"); // params

let user = _eD[1];

let wikiTxt = _eD[2];

let rule = _eD[3];

let template = _eD[4].split("|")[0];

let warningLevel = "N/A";

if ((customCallback == null) || (customCallback == false)) { // if not set

// Map warning level

(['1', '2', '3', '4', '4im']).forEach(e => {

if (template.includes(e)) warningLevel = e; // if includes this level, add

});

console.log({ user, wikiTxt, rule, template, warningLevel }); // debug

// MAKE EDIT - summary with warning info

let summary = `${({

"N/A": "Notice:",

"1": "Note:",

"2": "Caution:",

"3": "Warning:",

"4": "Final Warning:",

"4im": "ONLY Warning:"

})[warningLevel]

} ${rule}`;

// MAKE EDIT

rw.info.addWikiTextToUserPage(user, wikiTxt, true, summary);

} else {

// Send callback

callback(wikiTxt);

}

});

// Check most recent warning level

rw.info.lastWarningLevel(rw.info.targetUsername(un), (w, usrPgMonth, userPg) => {

const lastWarning = [ // Return HTML for last warning level.

// NO PAST WARNING

`

thumb_up

No notices this month.

`,

// NOTICE

`

info

Has been given a Level 1 notice this month.

`,

// CAUTION

`

announcement

Has been given a Level 2 caution this month.

`,

// Warning- in red. RedWarn, get it? This is the peak of programming humour.

`

report_problem

Has been given a Level 3 warning this month.

`,

// Final/Only Warning (dark red)

`

report

Has been given a Level 4 Final or ONLY warning.

Click here to report to admins for vandalism. Review user page first.

`

][w];

// CREATE DIALOG

// MDL FULLY SUPPORTED HERE (container).

// Advanced mode things here

if (!advancedMode) {

// we don't need to do anything else, just open the dialog

dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["warnUserDialog.html"]), 500, 630)).showModal(); // 500x630 dialog, see warnUserDialog.html for code

} else {

// This isn't called immediately, collapse and see below

const continueFunc = () => { // split to allow for the intro dialog

// Check we're not warning ourselves

if (rw.info.getUsername() == rw.info.targetUsername(un)) {

rw.ui.confirmDialog(`You cannot warn yourself.`, "OKAY", () => dialogEngine.closeDialog(), "", () => { }, 0);

return; // stop here

}

// Show loading dialog as this takes time and CPU

rw.ui.loadDialog.show(`

Looking for past warnings, please wait...

`, true);

// Get warning info

rw.info.warningInfo(rw.info.targetUsername(un), warningInfo => {

// Generate our list

let finalWarningHistoryHTML = ``;

warningInfo.forEach((warning, i) => { //todo

finalWarningHistoryHTML += `


${[ // Return HTML for last warning level.

// Reminder

`

error_outline

Recieved a reminder/policy violation notice

${warning.from} gave ${rw.info.targetUsername(un)} a reminder or policy violation warning for:

`,

// NOTICE

`

info

Level 1 notice

${warning.from} gave ${rw.info.targetUsername(un)} level 1 notice for:

`,

// CAUTION

`

announcement

Level 2 caution

${warning.from} gave ${rw.info.targetUsername(un)} level 2 caution for:

`,

// Warning- in red. RedWarn, get it? This is the peak of programming humour.

`

report_problem

Level 3 warning

${warning.from} gave ${rw.info.targetUsername(un)} level 3 warning for:

`,

// Final Warning (dark red)

`

report

Level 4 Final warning

Click here to report to admins for vandalism. Review user page first.

${warning.from} gave ${rw.info.targetUsername(un)} final warning for:

`,

// Only Warning (red)

`

report

Level 4 ONLY warning.

Click here to report to admins for vandalism. Review user page first.

${warning.from} gave ${rw.info.targetUsername(un)} ONLY warning for:

`,

// Unknown

`

error_outline

Unknown warning

${warning.from} gave ${rw.info.targetUsername(un)} an unknown warning

`

][warning.level]

}


${warning.rule.name}

${new Date(warning.timestamp).toUTCString()} ${

// Only show extra buttons if it's recognised, else there will be an error

(warning.rule.key != "" ?

`- REUSE`

: ``)}


`;

});

// Close loading dialog

rw.ui.loadDialog.close();

// Now just show our dialog but bigger, code in warnUserDialog.html handles everything else

dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["warnUserDialog.html"]), 800, 630)).showModal();

});

};

// Prompt user with the dialog (for freezing issue)

if (rw.config.rwWarnUserAdvancedNoteDismissed == null) {

rw.ui.confirmDialog(`

Advanced Mode

Welcome to advanced warning mode! This feature looks through the past 50 user talk page revisions, allowing you to find and restore older warnings, and is useful for problematic editors who may have hidden warnings in the page history.

Important: Please be aware that this can take a while to process on longer pages with larger changes, so if the progress bar freezes, please be paitent as RedWarn is still processing in the background.

`,

"Got it, thanks",

() => dialogEngine.closeDialog(continueFunc), // continue on close

"Don't show again", () => {

// write into config that this is the case

dialogEngine.closeDialog();

rw.config.rwWarnUserAdvancedNoteDismissed = true;

rw.info.writeConfig(true, () => { }); // save

continueFunc(); // continue

}, 200);

} else continueFunc(); // continue if dismissed

}

});

}, // end beginWarn

/**

* Open the "new talk page message" dialog for the specified username

*

* @param {string} un Target username

* @param {boolean} noRedirect If true, a callback will be used rather than submitting the notice.

* @param {string} buttonTxt Text to use instead of default "Send Message" for submit button

* @param {function} callback Callback to use if noRedirect is true

* @method newMessage

* @extends rw.ui

*/

"newMsg": (un, noRedirect, buttonTxt, callback) => {

// New message dialog

// Setup preview handling

addMessageHandler("generatePreview`*", m => {

rw.info.parseWikitext(m.split("`")[1], parsed => { // Split to Wikitext and send over to the API to be handled

dialogEngine.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage({

"action": "parseWikiTxt",

"result": parsed

}, '*'); // push to container for handling in dialog and add https:// to stop image breaking

});

});

// Add toast handler

addMessageHandler("pushToast`*", m => rw.visuals.toast.show(m.split('`')[1], false, false, 15000));

// Add submit handler

addMessageHandler("applyNotice`*", eD => {

// i.e applyNotice`user`wikitext`summary

// TODO: maybe b64 encode?

let _eD = eD.split("`"); // params

let user = _eD[1];

let wikiTxt = _eD[2];

let summary = _eD[3];

if (noRedirect) { // If no redirect, callback

callback(wikiTxt);

} else {

// MAKE EDIT

rw.info.addWikiTextToUserPage(user, wikiTxt, false, summary); // This requires title.

}

});

// CREATE DIALOG

// MDL FULLY SUPPORTED HERE (container).

dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["newMsg.html"]), 500, 390)).showModal(); // 500x390 dialog, see newMsg.html for code

},

/**

* Registers the right-click context menu feature for user links.

*

* @method registerContextMenu

* @extends rw.ui

*/

"registerContextMenu": () => { // Register context menus for right-click actions

// More docs at https://swisnl.github.io/jQuery-contextMenu/demo/trigger-custom.html

// USER TALK ACTIONS - check if not disabled then continue

if (rw.config.rwDisableRightClickUser != "disable") {

rw.visuals.contextMenuPromise.then(() => {

$(() => {

$('a[href*="User_talk:"], a[href*="User:"], a[href*="Special:Contributions/"]').each((_, e) => {

// CALLBACK

let hrefOfSelection = $(e).attr("href"); // href of userpage or contribs

let url = new URL(hrefOfSelection, window.location.href);

let pageTitle = (url.searchParams.get("title") || url.pathname

.replace(/^\/wiki\//, "")

.replace(/\/$/, ""))

.replace(/_/g, " "); // Get page title from URL

let targetUsername;

if (pageTitle.startsWith("User:") || pageTitle.startsWith("User talk:")) {

if (/^User(?: talk)?:.+\/.+/g.test(pageTitle)) {

//subpage

return;

}

targetUsername = pageTitle.replace(/^User(?: talk)?:([^\/]+)/g, "$1");

} else {

// Avoid popping up if clicking contribs link on contribs page

// Might avoid someone from using the normal context menu

if (mw.config.get('wgPageName').startsWith("Special:Contributions/")) {

return;

}

// Contribs link, go split at last slash

targetUsername = pageTitle.replace(/^Special:Contributions\/(.+)/g, "$1");

}

$(e).attr("data-rw-username", targetUsername);

});

// REV15 - only trigger on shift+right-click unless if set in settings - If config is set to "Opt2", to open on right-click set in preferences, set below in trigger

if (rw.config.rwDisableRightClickUser != "Opt2") {

$('a[data-rw-username]').on('contextmenu', e => {

// if shift key not down, don't show the context menu.

if (!e.shiftKey) return;

e.preventDefault();

$(e.currentTarget).contextMenu();

});

}

$.contextMenu({

trigger: (rw.config.rwDisableRightClickUser === "Opt2" ? undefined : 'none'), // if set in options, activate as usual

selector: 'a[data-rw-username]', // Select all appropriate user links

callback: (act, info) => {

// CALLBACK

let targetUsername = $(info.$trigger[0]).attr("data-rw-username"); // href of userpage or contribs

// Do the action for each action now.

({

"usrPg": un => redirect(rw.wikiBase + "/wiki/User:" + encodeURIComponent(un), true), // Open user page in new tab

"tlkPg": un => redirect(rw.wikiBase + "/wiki/User_talk:" + encodeURIComponent(un), true), // Open talk page in new tab

"contribs": un => redirect(rw.wikiBase + "/wiki/Special:Contributions/" + encodeURIComponent(un), true), // Redirect to contribs page in new tab

"accInfo": un => redirect(rw.wikiBase + "/wiki/Special:CentralAuth?target=" + encodeURIComponent(un), true), // Redirect to Special:CentralAuth page in new tab

"sendMsg": un => rw.ui.newMsg(un), // show new msg dialog

"quickWel": un => rw.quickTemplate.openSelectPack(un), // Submit Quick Template

"newNotice": un => rw.ui.beginWarn(false, un), // show new warning dialog

"adminReport": un => rw.ui.adminReportSelector(un),

"usrPronouns": un => { // Show a tost with this users prefered pronouns

rw.info.getUserPronouns(un, p => {

rw.visuals.toast.show(un + "'s pronouns are " + p, false, false, 3000);

});

},

"usrEditCount": un => { // Show a tost with this users prefered pronouns

rw.info.getUserEditCount(un, count => {

if (count == null) count = "an unknown number of"; // stop undefined message

rw.visuals.toast.show(un + " has made " + count + " edits.", false, false, 3000);

});

},

"usrStanding": un => {

// Show toast with last warning level

rw.info.lastWarningLevel(un, level => {

rw.visuals.toast.show(un + " has recieved " + [

"no warnings",

"a level 1 notice",

"a level 2 caution",

"a level 3 warning",

"a level 4 final or ONLY warning"

][level] + " this month.", false, false, 4000);

});

},

"filterLog": un => redirect("https://en.wikipedia.org/w/index.php?title=Special:AbuseLog&wpSearchUser=" + encodeURIComponent(un), true), // Redirect to filter log page in new tab

"blockLog": un => redirect("https://en.wikipedia.org/w/index.php?title=Special:Log/block&page=User:" + encodeURIComponent(un), true), // Redirect to block log page in new tab

"allLog": un => redirect("https://en.wikipedia.org/wiki/Special:Log/" + encodeURIComponent(un), true) // Redirect to filter log page in new tab

})[act](targetUsername.trim());

},

items: { // TODO: add extra options like logs ext. ext.

"usrPg": { name: "User Page" },

"tlkPg": { name: "Talk Page" },

"aAsubmenu": {

"name": "Quick Actions",

"items": {

"sendMsg": { name: "New Message" },

"newNotice": { name: "Warn User" },

"quickWel": { name: "Quick Template" },

"adminReport": { name: "Report to Admin" }

}

},

"aIsubmenu": {

"name": "Account info",

"items": {

"contribs": { name: "Contributions" },

"accInfo": { name: "Central Auth" },

"usrPronouns": { "name": "Pronouns" },

"usrEditCount": { "name": "Edit Count" },

"usrStanding": { "name": "Highest Warning" },

"filterLog": { name: "Edit Filter Log" },

"blockLog": { name: "Block Log" },

"allLog": { name: "All Logs" }

}

}

}

});

});

})

} // END USER ACTIONS CONTEXT MENU

// TODO: add more, like Quick Template options ext.. and right-click on article link to begin rollback ext.

}, // end context menus

/**

* Requests the speedy deletion of a page. Not currently used and will not currently work without adding speedyDeleteReasons.js into build.php

*

* @param {string} pg Page name

* @method requestSpeedyDelete

* @extends rw.ui

*/

"requestSpeedyDelete": (pg) => {

// Open Speedy Deletion dialog for first selection, i.e I'm requesting the speedy deletion of..

// Programming this is proving to be very boring.

// Add toast handler

addMessageHandler("pushToast`*", m => rw.visuals.toast.show(m.split('`')[1], false, false, 15000));

addMessageHandler("csdR`*", rs => {

// Reason recieved.

let reason = eval(rs.split("`")[1]);

let reasonTitle = reason.title;

let additionalInfoReq = reason.input != ""; // if special info needed

let additionalInfo = "";

if (additionalInfoReq) {

if (rs.split("`")[2] == "undefined") {

// No reason specified

additionalInfo = "Not specified.";

} else {

additionalInfo = rs.split("`")[2]; // set to the additional info

}

}

console.log(`Deleting under: ` + reasonTitle + `

`+ reason.input + additionalInfo + ` (redwarn)

`);

});

let finalStr = ``;

for (const key in speedyDeleteReasons) {

speedyDeleteReasons[key].forEach((e, i) => {

let style = "";

if ((key + e.title).length > 62) {

// Too long to fit

style = "font-size:10px;";

}

finalStr += `

  • ` + key + e.title + `
  • `;;

    });

    }

    // CREATE DIALOG

    // MDL FULLY SUPPORTED HERE (container).

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["speedyDeletionp1.html"]), 500, 450)).showModal(); // 500x300 dialog, see speedyDeletionp1.html for code

    },

    /**

    * Opens RedWarn preferences

    *

    * @method openPreferences

    * @extends rw.ui

    */

    "openPreferences": () => { // Open Preferences page

    // Add toast handler

    addMessageHandler("pushToast`*", m => rw.visuals.toast.show(m.split('`')[1], false, false, 15000));

    addMessageHandler("config`*", rs => { // On config change

    // New config recieved

    let config = JSON.parse(deserialize(rs.split("`")[1])); // b64 encoded json string

    //Write to our config

    for (const key in config) {

    if (config.hasOwnProperty(key)) {

    const element = config[key];

    rw.config[key] = element; // add or change value

    }

    }

    // Push change

    rw.info.writeConfig();

    });

    addMessageHandler("resetConfig", rs => {

    // Reset config recieved, set config back to default

    rw.info.getConfig(() => { }, true); // TRUE HERE MEANS RESET TO DEAULT

    });

    addMessageHandler("resyncWarnings", rs => {

    // Resync warning database

    dialogEngine.closeDialog(() => {

    rw.ui.loadDialog.show("Syncing warning database...");

    rw.rulesFunc.resync(() => {

    rw.ui.loadDialog.close();

    rw.ui.openPreferences();

    });

    });

    });

    // Add install quick template handler

    addMessageHandler("installQTP", () => {

    // Show warning and confirm

    rw.ui.confirmDialog(`

    WARNING: Only install packs from users you trust. Installing a quick template pack gives the installer full access to your account to write to RedWarn's config files.



    To install, click "install from pack code" and paste the code into the browser dialog that appears.

    `, "Install from pack code", () => {

    // Time to install

    importScript(rw.quickTemplate.packCodeToPageName(prompt("Please enter the pack code, then click OK to install:"))); // using mediawiki importscript which does it from pagename

    },

    "CANCEL", () => dialogEngine.closeDialog(), 98);

    });

    // Add new QTPack handler

    addMessageHandler("newQTP", () => rw.quickTemplate.newPack());

    // Add load new theme handler

    addMessageHandler("newThemeDialog", () => rw.ui.loadDialog.show("Changing theme..."));

    addMessageHandler("loadDialogClose", () => rw.ui.loadDialog.close());

    // Lock scrolling

    dialogEngine.freezeScrolling();

    // Open preferences page with no padding, full screen

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["preferences.html"]), window.innerWidth, window.innerHeight, true), true).showModal(); // TRUE HERE MEANS NO PADDING.

    },

    /**

    * Opens the AIV report dialog

    *

    * @param {string} un Username to report

    * @param {boolean} doNotShowDialog If set to true, will be set to just generate slim HTMl and handlers

    * @method openAdminReport

    * @extends rw.ui

    */

    "openAdminReport": (un, doNotShowDialog) => { // Open admin report dialog

    // Setup AIV page for development or production

    const aivPage = (rw.debugMenu == null ? "Wikipedia:Administrator_intervention_against_vandalism" : "User:Chlod/playground");

    // Add toast handler

    addMessageHandler("pushToast`*", m => rw.visuals.toast.show(m.split('`')[1], false, false, 2500));

    // On report

    addMessageHandler("AIVreport`*", m => {

    let reportContent = m.split('`')[1]; // report content

    let target = m.split('`')[2]; // target username

    let targetIsIP = rw.info.isUserAnon(target); // is the target an IP? (2 different types of reports)

    console.log("reporting " + target + ": " + reportContent);

    console.log("is ip? " + (targetIsIP ? "yes" : "no"));

    rw.visuals.toast.show("Reporting " + target + "...", false, false, 2000); // show toast

    // Submit the report.

    $.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=" + aivPage + "&rvslots=*&rvprop=content&formatversion=2&format=json", latestR => {

    // Grab text from latest revision of AIV page

    // Check if exists

    let revisionWikitext = latestR.query.pages[0].revisions[0].slots.main.content; // Set wikitext

    if (revisionWikitext.toLowerCase().includes(target.toLowerCase())) {// If report is already there

    rw.visuals.toast.show("This user has already been reported.", false, false, 5000); // show already reported toast

    return; // Exit

    }

    // Let's continue

    // We don't need to do anything special. Just shove our report at the bottom of the page, although, may be advisiable to change this if ARV format changes

    let textToAdd = "*" + (targetIsIP ? "{{IPvandal|" : "{{vandal|") + target + "}} " + reportContent; // DANGER! WIKITEXT (here is fine. be careful w changes.) - if target IP give correct template, else normal

    let finalTxt = revisionWikitext + "\n" + textToAdd + "\n"; // compile final string

    // Now we just submit

    $.post(rw.wikiAPI, {

    "action": "edit",

    "format": "json",

    "token": mw.user.tokens.get("csrfToken"),

    "title": aivPage,

    "summary": `Reporting ${target} (RW ${rw.version})`, // summary sign here

    "text": finalTxt,

    "tags": ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

    }).done(dt => {

    // We done. Check for errors, then callback appropriately

    if (!dt.edit) {

    // Error occured or other issue

    console.error(dt);

    dialogEngine.dialog.showModal(); // reshow dialog

    rw.visuals.toast.show("Sorry, there was an error, likely an edit conflict. Try reporting again."); // That's it

    } else {

    // Success! No need to do anything else.

    rw.visuals.toast.show("User reported.", false, false, 5000); // we done

    }

    });

    });

    }); // END ON REPORT EVENT

    // Check matching user

    if (rw.info.targetUsername(un) == rw.info.getUsername()) {

    // Usernames are the same, give toast.

    if (doNotShowDialog !== true) rw.visuals.toast.show("You can not report yourself, nor can you test this feature except in a genuine case.", false, false, 7500);

    return `Sorry, you cannot report yourself, nor can you test this feature except in a genuine case.`; // DO NOT continue.

    }

    const dialogContent = eval(rw_includes["adminReport.html"]);

    // Push a message if report has already been made

    $.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=" + aivPage + "&rvslots=*&rvprop=content&formatversion=2&format=json", latestR => {

    // Grab text from latest revision of AIV page

    // Check if exists

    let revisionWikitext = latestR.query.pages[0].revisions[0].slots.main.content; // Set wikitext

    if (revisionWikitext.toLowerCase().includes(rw.info.targetUsername(un).replace(/_/g, ' ').toLowerCase())) {// If report is already there

    setTimeout(() => dialogEngine.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage("AIVReportExist"), 500); // let dialog know after 500ms to allow dialog to open

    }

    });

    if (doNotShowDialog !== true) {

    // See adminReport.html for code

    dialogEngine.create(mdlContainers.generateContainer(dialogContent, 500, 410)).showModal();

    } else {

    // Return the code for use elsewhere

    return dialogContent;

    }

    },

    /**

    * Opens a confirmation dialog with one or two buttons

    *

    * @param {string} content Dialog content

    * @param {string} pBtnTxt Primary button text

    * @param {function} pBtnClick Callback when primary button clicked

    * @param {string} sBtnTxt Secondary button text (Will not show if empty)

    * @param {function} sBtnClick Secondary button callback

    * @param {number} extraHeight Extra height to add to the dialog in pixels

    * @param {boolean} noExtraLines Removes line breaks from the dialog

    * @method confirmDialog

    * @extends rw.ui

    */

    "confirmDialog": (content, pBtnTxt, pBtnClick, sBtnTxt, sBtnClick, extraHeight, noExtraLines) => { // noExtraLines removes the
    tags

    // Confirm dialog (yes, no, ext...)

    addMessageHandler("sBtn", sBtnClick);

    addMessageHandler("pBtn", pBtnClick);

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["confirmDialog.html"]), 500, 80 + extraHeight)).showModal();

    },

    /**

    * Shows the feedback dialog to leave bug reports and feedback

    *

    * @param {string} extraInfo

    * @method reportBug

    * @extends rw.ui

    */

    "reportBug": extraInfo => {

    // Open feedback dialog, basically same as newmsg

    // Setup preview handling

    addMessageHandler("generatePreview`*", m => {

    rw.info.parseWikitext(m.split("`")[1], parsed => { // Split to Wikitext and send over to the API to be handled

    dialogEngine.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage({

    "action": "parseWikiTxt",

    "result": parsed

    }, '*'); // push to container for handling in dialog and add https:// to stop image breaking

    });

    });

    // Add toast handler

    addMessageHandler("pushToast`*", m => rw.visuals.toast.show(m.split('`')[1], false, false, 15000));

    // Add submit handler

    addMessageHandler("applyNotice`*", eD => {

    // i.e applyNotice`user`wikitext`summary

    // TODO: maybe b64 encode?

    let _eD = eD.split("`"); // params

    let user = _eD[1];

    let wikiTxt = _eD[2];

    let summary = _eD[3];

    // MAKE EDIT

    rw.info.addWikiTextToUserPage(user, wikiTxt, true, summary); // Save under date

    });

    // CREATE DIALOG

    // MDL FULLY SUPPORTED HERE (container).

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["sendFeedback.html"]), 500, 390)).showModal(); // 500x390 dialog, see sendFeedback.html for code

    },

    /**

    * Opens the administrator report venue selector for the specified username

    *

    * @param {string} un Username to report

    * @method adminReportSelector

    * @extends rw.ui

    */

    "adminReportSelector": un => { // DON'T FORGET TO USE un ATTR!

    un = rw.info.targetUsername(un); // get target

    // Handle events

    addMessageHandler("openAIV", () => rw.ui.openAdminReport(un)); // AIV report

    addMessageHandler("openUAA", () => rw.ui.beginUAAReport(un)); // UAA report

    // Open the admin report selector dialog

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["adminReportSelector.html"]), 250, 280)).showModal();

    },

    /**

    * Opens the UAA report dialog for a specified user

    *

    * @param {string} un Username to report

    * @param {boolean} htmlOnly For expanding elements, adds handlers but returns HTML

    * @method beginUAAReport

    * @extends rw.ui

    */

    "beginUAAReport": (un, htmlOnly) => { // Report to UAA

    // Check if IP - if so, exit

    if (rw.info.isUserAnon(un)) {

    if (htmlOnly) return "As IPs don't have usernames, you can't report them to UAA.";

    rw.ui.confirmDialog("As IPs don't have usernames, you can't report them to UAA.", "OKAY", () => dialogEngine.closeDialog(), "", () => { }, 0);

    return; // stop

    }

    const uaaPage = (rw.debugMenu == null ? "Wikipedia:Usernames_for_administrator_attention" : "User:Chlod/playground"); // set UAA based on debug mode

    // Add toast handler

    addMessageHandler("pushToast`*", m => rw.visuals.toast.show(m.split('`')[1], false, false, 2500));

    // On report

    addMessageHandler("UAA`*", m => {

    let reportContent = m.split('`')[1]; // report content

    let target = m.split('`')[2]; // target username

    console.log("reporting " + target + ": " + reportContent);

    rw.visuals.toast.show("Reporting " + target + "...", false, false, 2000); // show toast

    // Submit the report. MUST REPLACE WITH REAL AIV WHEN DONE AND WITH SANDBOX IN DEV!

    $.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=" + uaaPage + "&rvslots=*&rvprop=content&formatversion=2&format=json", latestR => {

    // Grab text from latest revision of AIV page

    // Check if exists

    let revisionWikitext = latestR.query.pages[0].revisions[0].slots.main.content; // Set wikitext

    if (revisionWikitext.toLowerCase().includes(target.toLowerCase())) {// If report is already there

    rw.visuals.toast.show("This user has already been reported.", false, false, 5000); // show already reported toast

    return; // Exit

    }

    // Let's continue

    // We don't need to do anything special. Just shove our report at the bottom of the page, although, may be advisiable to change this if ARV format changes

    let textToAdd = "*" + "{{user-uaa|1=" + target + "}} – " + reportContent; // DANGER! WIKITEXT (here is fine. be careful w changes.) - if target IP give correct template, else normal

    let finalTxt = revisionWikitext + "\n" + textToAdd + "\n"; // compile final string

    // Now we just submit

    $.post(rw.wikiAPI, {

    "action": "edit",

    "format": "json",

    "token": mw.user.tokens.get("csrfToken"),

    "title": uaaPage,

    "summary": `Reporting ${target} (RW ${rw.version})`, // summary sign here

    "text": finalTxt,

    "tags": ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

    }).done(dt => {

    // We done. Check for errors, then callback appropriately

    if (!dt.edit) {

    // Error occured or other issue

    console.error(dt);

    dialogEngine.dialog.showModal(); // reshow dialog

    rw.visuals.toast.show("Sorry, there was an error, likely an edit conflict. Try reporting again."); // That's it

    } else {

    // Success! No need to do anything else.

    rw.visuals.toast.show("User reported.", false, false, 5000); // we done

    }

    });

    });

    }); // END ON REPORT EVENT

    // Check matching user

    if (rw.info.targetUsername(un) == rw.info.getUsername()) {

    // If HTML only

    if (htmlOnly) return `You can not report yourself, nor can you test this feature except in a genuine case.`;

    // Usernames are the same, give toast.

    rw.visuals.toast.show("You can not report yourself, nor can you test this feature except in a genuine case.", false, false, 7500);

    return; // DO NOT continue.

    }

    const dialogContent = eval(rw_includes["uaaReport.html"]);

    // Push a message if report has already been made

    $.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles=" + uaaPage + "&rvslots=*&rvprop=content&formatversion=2&format=json", latestR => {

    // Grab text from latest revision of AIV page

    // Check if exists

    let revisionWikitext = latestR.query.pages[0].revisions[0].slots.main.content; // Set wikitext

    if (revisionWikitext.toLowerCase().includes(rw.info.targetUsername(un).replace(/_/g, ' ').toLowerCase())) {// If report is already there

    setTimeout(() => dialogEngine.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage("UAAReportExist"), 500); // let dialog know after 500ms to allow dialog to open

    }

    });

    if (htmlOnly) return dialogContent; // return dialog if HTMl only

    // See uaaReport.html for code

    dialogEngine.create(mdlContainers.generateContainer(dialogContent, 500, 410)).showModal();

    },

    /**

    * Opens extended options that can be opened from any page (preferences, oversight and TAS reporting)

    * @param {string} un Username for reports. Can also be revision ID (todo)

    * @method openExtendedOptionsDialog

    * @extends rw.ui

    */

    "openExtendedOptionsDialog": un => {

    rw.ui.loadDialog.show("Please wait...");

    // Get email info before loading and showing dialog (for OS and TAS reporting)

    $.getJSON(rw.wikiAPI + "?action=query&meta=userinfo&uiprop=email&format=json", r => {

    // HTML for tabs

    // USER THINGS ONLY - try and catch as will error out on non-user pages

    let adminReportContent = eval(rw_includes["genericError.html"]); // placeholder

    try {

    adminReportContent = rw.ui.openAdminReport(null, true); // this sets up our handlers and generates the appropraite HTML

    } catch (e) { console.log(e); adminReportContent += `


    ${e}
    `; }

    // UAA report

    let uaaReportContent = eval(rw_includes["genericError.html"]); // placeholder

    try {

    uaaReportContent = rw.ui.beginUAAReport(rw.info.targetUsername(), true); // this sets up our handlers and generates the appropraite HTML

    } catch (e) { console.log(e); uaaReportContent += `


    ${e}
    `; }

    // Event handlers

    addMessageHandler("redwarnPref", () => dialogEngine.closeDialog(() => rw.ui.openPreferences())); // open preferences for button press

    addMessageHandler("redwarnTalk", () => redirect("https://en.wikipedia.org/wiki/Wikipedia_talk:RedWarn", true));

    // Email to TAS THIS IS LIVE!!

    addMessageHandler("TASEmail`*", e => dialogEngine.closeDialog(() => rw.info.sendEmail("Emergency", deserialize(e.split("`")[1]))));

    // Email to OS - THESE ARE LIVE

    addMessageHandler("OSEmail`*", e => dialogEngine.closeDialog(() => rw.info.sendEmail("Oversight", deserialize(e.split("`")[1]))));

    const isUserPage = mw.config.get("wgRelevantPageName").includes("User:") || mw.config.get("wgRelevantPageName").includes("User_talk:");

    const isOnRevPage = window.location.href.includes("diff=") || window.location.href.includes("oldid="); // for reporting revisions

    let rollbackOptsHTML = "";

    // Generate rollback options if on rev page

    if (isOnRevPage) rollbackOptsHTML = rw.rollback.getDisabledHTMLandHandlers(); // generates our HTML and all assosicated handlers for us

    // Email information for TAS and OS reports

    const emailInfo = r.query.userinfo;

    // Close loading dialog#

    rw.ui.loadDialog.close();

    // Make dialog

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["extendedOptions.html"]), 500, 500)).showModal(); // todo: also shrink more when not on user page or revision page

    });

    },

    // For things that send details to the RedWarn team - NOT USED OR SET UP ATM, for later use.

    "acceptLegalPolicyDialog": callback => {

    const legalPol = eval(rw_includes["legalPolicy.html"]);

    rw.ui.confirmDialog(`

    This feature may share personal information with the RedWarn team. Please accept our legal and privacy policies to continue.

    ${legalPol}

    `, "ACCEPT", () => { }, "DECLINE", () => dialogEngine.closeDialog(), 400);

    },

    // CLASSES from here

    /**

    * A static loading dialog while processes occur. Seperate from dialogEngine.

    * @class rw.ui.loadDialog

    */

    "loadDialog": {

    // Loading dialog

    "hasInit": false,

    "init": (text, biggerStyle) => {

    if (!rw.ui.loadDialog.hasInit) { // Only continue if we haven't already appended our container div

    $("body").append(`

    `);

    }

    $("#rwUILoad").html(`

    ${mdlContainers.generateContainer(eval(rw_includes["loadingSpinner.html"]), (biggerStyle ? 400 : 300), (biggerStyle ? 60 : 30))}

    `); // Create dialog with content from loadingSpinner.html

    rw.ui.loadDialog.dialog = document.querySelector('#rwUILoadDialog'); // set dialog var

    // Firefox issue fix

    if (!rw.ui.loadDialog.dialog.showModal) {

    dialogPolyfill.registerDialog(rw.ui.loadDialog.dialog);

    }

    rw.ui.loadDialog.hasInit = true;

    },

    /**

    * Opens a loading dialog with the given text

    *

    * @param {string} text

    * @param {boolean} biggerStyle - a diffferent larger style with a vertical progress bar

    * @method show

    * @extends rw.ui.loadDialog

    */

    "show": (text, biggerStyle) => { // Init and create a new loading dialog

    rw.ui.loadDialog.init(text, biggerStyle); // init

    rw.ui.loadDialog.setText(text); // set our text

    // Show dialog

    rw.ui.loadDialog.dialog.showModal();

    // We done

    },

    /**

    * Changes the text of the current loading dialog

    *

    * @param {string} text

    * @method setText

    * @extends rw.ui.loadDialog

    */

    "setText": text => $("#rwUILoadDialog > iframe")[0].contentWindow.postMessage(text, '*'), // Set text of loading by just sending the message to the container

    /**

    * Closes the current loading dialog

    *

    * @method close

    * @extends rw.ui.loadDialog

    */

    "close": () => { // Close the dialog and animate

    $("#rwUILoadDialog")

    .addClass("closeAnimate")

    .on("webkitAnimationEnd", () => {

    // Animation finished

    rw.ui.loadDialog.dialog.close();

    });

    }

    },

    /**

    * A dialog used to select a page from a users 20 recently visited pages

    *

    * @class rw.ui.recentlyVisitedSelector

    */

    "recentlyVisitedSelector": { // Used to select recently visited page from a dropdown dialog

    "dialog": null,

    "init": content => {

    if ($("#rwRecentVistedSelectContainer").length < 1) {

    // container hasn't already been init

    $("body").append(`

    `);

    }

    // let's continue

    $("#rwRecentVistedSelectContainer").html(`

    ` + content + `

    `);

    rw.ui.recentlyVisitedSelector.dialog = document.querySelector('#rwUIRVisDialog'); // set dialog var

    // Firefox issue fix

    if (!rw.ui.recentlyVisitedSelector.dialog.showModal) {

    dialogPolyfill.registerDialog(rw.ui.recentlyVisitedSelector.dialog);

    }

    },

    /**

    * Shows the selection dialog and calls back when user has made their selection

    *

    * @param {function} callback callback(selectedPageTitle)

    * @method showDialog

    * @extends rw.ui.recentlyVisitedSelector

    */

    "showDialog": callback => { // Show dialog and callback(selected article)

    // Assemble revent visits listbox

    let recentlyVisited = JSON.parse(window.localStorage.rwRecentlyVisited);

    // Check if empty, if so, show dialog and exit

    if ((recentlyVisited == null) || (recentlyVisited.length < 1)) {

    rw.ui.confirmDialog("There are no recent pages to show.", "OKAY", () => dialogEngine.closeDialog(), "", () => { }, 0);

    return; //exit, don't callback as not complete

    }

    // Let's continue

    let finalRVList = "";

    recentlyVisited.forEach((page, i) => {

    finalRVList += `

    ${page.replace(/_/g, ' ')}


    `;

    });

    // Add close handler

    addMessageHandler("closeRecentPageDialog", () => rw.ui.recentlyVisitedSelector.close());

    // Add continue handler

    addMessageHandler("RecentPageDialogSel`*", m => {

    let selectedI = m.split("`")[1];

    callback(recentlyVisited[selectedI]); // send callback

    });

    // Now show dialog

    rw.ui.recentlyVisitedSelector.init(mdlContainers.generateContainer(eval(rw_includes["recentPageSelect.html"]), 420, 500)); // 420 hahahaha

    rw.ui.recentlyVisitedSelector.dialog.showModal();

    },

    /**

    * Closes the currently open dialog

    *

    * @method close

    * @extends rw.ui.recentlyVisitedSelector

    */

    "close": () => {

    // Close dialog

    $(rw.ui.recentlyVisitedSelector.dialog)

    .addClass("closeAnimate")

    .on("webkitAnimationEnd", () => {

    // Animation finished

    rw.ui.recentlyVisitedSelector.dialog.close();

    });

    }

    }

    }

    // rw-source: pendingChanges.js

    /**

    * RedWarn's pending changes review feature

    * @class rw.PendingChangesReview

    */

    rw.PendingChangesReview = {

    /**

    * If on a review page, and user in "reviewer" group, this initalies review controls

    *

    * @method reviewPage

    * @extends rw.PendingChangesReview

    */

    "reviewPage" : ()=> {

    // Check config if disabled

    if (rw.config.rwDisablePendingChanges == "disable") return; // if disabled, exit

    // Pending changes buttons and warning (ONLY on review pages)

    if (($("#mw-fr-reviewform").length > 0) && !($("#mw-fr-reviewformlegend").text().includes("Re-review"))) {

    rw.info.featureRestrictPermissionLevel("reviewer", ()=>{ // Restrict to pending changes reviewers

    // Add to accept header

    $('.flaggedrevs_reviewform').prepend(`

    done

    Accept these changes

    close

    Revert changes


    `);

    // Hide original review box

    $(".fr-rating-controls").hide();

    // Handlers

    // ON ACCEPT

    let acceptHandler = ()=>{

    // When accept button clicked.

    rw.PendingChangesReview.confirmLatestRev(()=>{

    rw.PendingChangesReview.getPendingChangesUsers((usr, count, users, userCount)=>{

    // SUBMIT HANDLER, SCROLL DOWN FOR DIALOG

    addMessageHandler("reason`*", reasonIn=> {

    let comment = reasonIn.split("`")[1]; // params

    // Time to submit 302 success, 200 FAIL

    rw.ui.loadDialog.show("Accepting...");

    $.ajax({

    url : 'https://en.wikipedia.org/w/index.php?title=Special:RevisionReview&action=submit',

    type : 'post',

    data : {

    "action": "approve",

    "title": "Special:RevisionReview",

    "wpApprove" : 1,

    "target" : mw.config.get("wgRelevantPageName"),

    "wpEditToken" : mw.user.tokens.get("csrfToken"),

    "refid" : $("#mw-fr-input-refid").attr("value"),

    "oldid" : $("#mw-fr-input-oldid").attr("value"),

    "changetime" : $("#mw-fr-input-changetime").attr("value"),

    "userreviewing" : $("#mw-fr-input-reviewing").attr("value"),

    "templateParams": "",

    "imageParams": "",

    "fileVersion": "",

    "validatedParams": $('input[name ="validatedParams"]').attr("value"),

    "wpReason" : comment + " (RW "+ rw.version + ")",

    "wpSubmit": "Accept revision"

    }}).done((r, sT, x)=>{ // TODO: ADD AUTOWARNING, DETECT CHANGE AND OTHER

    rw.ui.loadDialog.close();

    // Review response (this gens a new dom thing then we get the content text and error in first p), get response of 302 or 200 with below else error

    //Revision of Joel Osteen flagged. (view reviewed versions)

    if (x.status == 200){

    // We done :)

    let parser = new DOMParser();

    let el = parser.parseFromString(r, 'text/html');

    let resultStr = el.getElementById("mw-content-text").getElementsByTagName("p")[0].innerText;

    if (resultStr.includes("Revision of") && resultStr.includes("flagged")) {

    // Success!

    // Reload the page with tags

    rw.ui.loadDialog.show("Reloading...");

    window.location.hash = "#rwPendingAccept";

    window.location.reload();

    } else {

    // Probably error

    rw.visuals.toast.show("Sorry, an error occured and your review has not been submitted.", false, false, 5000);

    }

    } else {

    // Error

    rw.visuals.toast.show("Sorry, an error occured and your review has not been submitted.", false, false, 5000);

    }

    });

    });

    // SHOW DIALOG

    let reviewAction = "Accept "+ count +" Revision"+ ((count > 1) ? "s" : ""); // i.e accept 1 revision / accept 2 revisions

    let reviewCaption = `

    You are about to accept `+ count +` edit`+ ((count > 1) ? "s" : "") + ` by `+ userCount +` user`+ ((userCount > 1) ? "s" : "") + `



    Enter an optional comment, then confirm your review by clicking 'Submit Review' or by pressing ENTER.`;

    let autoAccept = rw.config.rwDisableReviewAutoAccept != "disable" ? "true" : "false";

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["pendingReviewReason.html"]), 500, 350)).showModal();

    });

    });

    };

    $("#rReviewAccept").click(acceptHandler);

    // END ACCEPT

    // ON DENY

    let denyHandler = ()=>{

    // When DENY button clicked.

    rw.PendingChangesReview.confirmLatestRev(()=>{

    rw.PendingChangesReview.getPendingChangesUsers((usr, count, users, userCount)=>{

    addMessageHandler("reason`*", reasonIn=> { // ON REASON RECIEVED

    // Generate revert string

    let revertString = "Reverting "+ count +" pending edit"+ ((count > 1) ? "s" : "") + " by ";

    // Build list of users (feeding in object keys)

    revertString += (a=>{

    a.forEach((v,i)=>{ // Wrap contribs link around usernammes

    a[i] = ""+ v +"";

    });

    // Build x, y and z list

    a.length == 1 ? a[0] : [ a.slice(0, a.length - 1).join(", "), a[a.length - 1] ].join(" and ");

    return a;

    })(Object.keys(users));

    revertString += " to last accepted version by "+ usr +"";

    let comment = reasonIn.split("`")[1]; // params

    rw.ui.loadDialog.show("Reverting...");

    $.ajax({

    url : 'https://en.wikipedia.org/w/index.php?title=Special:RevisionReview&action=submit',

    type : 'post',

    data : { // send request (reversed from form)

    "action": "reject",

    "title": "Special:RevisionReview",

    "wpReject" : 1,

    "wpRejectConfirm" : 1,

    "target" : mw.config.get("wgRelevantPageName"),

    "wpEditToken" : mw.user.tokens.get("csrfToken"),

    "refid" : $("#mw-fr-input-refid").attr("value"),

    "oldid" : $("#mw-fr-input-oldid").attr("value"),

    "changetime" : $("#mw-fr-input-changetime").attr("value"),

    "userreviewing" : $("#mw-fr-input-reviewing").attr("value"),

    "templateParams": "",

    "imageParams": "",

    "fileVersion": "",

    "validatedParams": $('input[name ="validatedParams"]').attr("value"),

    "wpReason" : revertString+ (comment.length > 0 ? ": "+ comment : "") + " (RW "+ rw.version + ")",

    "wpSubmit": "Revert these changes"

    }}).done((r,sT,x)=>{

    // Cannot reject these changes because someone already accepted some (or all) of the edits.

    rw.ui.loadDialog.close();

    let successHandler = ()=>{

    // On success

    if (rw.config.rwPendingMATDisable != "disable") rw.multiAct.open(users); // open MAT if not disabled in config

    };

    if (x.status == 302) {

    // Redirect, so success!

    dialogEngine.closeDialog();

    successHandler();

    } else {

    // Oops, likely error (DENY ONLY)

    let parser = new DOMParser();

    let el = parser.parseFromString(r, 'text/html');

    let resultStr = el.getElementById("mw-content-text").getElementsByTagName("p")[0].innerHTML;

    if (resultStr.includes("Cannot reject these changes because someone already accepted some (or all) of the edits")) {

    // Show the issue

    rw.PendingChangesReview.confirmLatestRev(()=>successHandler(), true); // true here note a different wording

    } else if (r.toLowerCase().includes("internal error")) {

    rw.visuals.toast.show("Sorry, an error occured and your review has not been submitted");

    } else {

    // Likely success

    successHandler();

    }

    }

    });

    });

    let reviewAction = "Revert "+ count +" Revision"+ ((count > 1) ? "s" : "");

    let reviewCaption = `

    You are about to revert `+ count +` edit`+ ((count > 1) ? "s" : "") + ` by `+ userCount +` user`+ ((userCount > 1) ? "s" : "") + `



    If you are not reverting vandalism, please enter a summary for the revert.

    Confirm your review by clicking 'Submit Review' or by pressing ENTER.

    `;

    let autoAccept = rw.config.rwEnableReviewAutoRevert == "enable" ? "true" : "false";

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["pendingReviewReason.html"]), 500, 350)).showModal();

    });

    });

    };

    $("#rReviewDeny").click(denyHandler);

    // END DENY

    }, ()=>{});

    } else if (($("#mw-fr-reviewform").length > 0) && ($("#mw-fr-reviewformlegend").text().includes("Re-review"))) {

    // Re-review page, all here is just unaccept

    // Add to accept header

    $('.flaggedrevs_reviewform').prepend(`

    close

    Unaccept changes


    `);

    // Hide original review box

    $(".fr-rating-controls").hide();

    // Handlers

    $("#rReviewUnAccept").click(()=>{

    addMessageHandler("reason`*", reasonIn=> { // ON REASON RECIEVED

    // Dumbest one of all, just send, reload and hope for the best

    let comment = reasonIn.split("`")[1];

    rw.ui.loadDialog.show("Unaccepting...");

    $.post("https://en.wikipedia.org/w/index.php?title=Special:RevisionReview&action=submit", {

    "action": "submit",

    "title": "Special:RevisionReview",

    "wpUnapprove" : 1,

    "target" : mw.config.get("wgRelevantPageName"),

    "wpEditToken" : mw.user.tokens.get("csrfToken"),

    "refid" : $("#mw-fr-input-refid").attr("value"),

    "oldid" : $("#mw-fr-input-oldid").attr("value"),

    "changetime" : $("#mw-fr-input-changetime").attr("value"),

    "userreviewing" : $("#mw-fr-input-reviewing").attr("value"),

    "templateParams": "",

    "imageParams": "",

    "fileVersion": "",

    "validatedParams": $('input[name ="validatedParams"]').attr("value"),

    "wpReason" : "Unapproving"+ (comment.length > 0 ? ": "+ comment : "") + " (RW "+ rw.version + ")",

    "wpSubmit": "Unaccept revision"

    }).done(r=>{

    window.location.hash = "#rwReviewUnaccept";

    window.location.reload();

    });

    });

    let reviewAction = "Unaccept Revision";

    let reviewCaption = `

    You are about to unaccept this revision.



    If you are not reverting vandalism, please enter a summary for the revert.

    Confirm your review by clicking 'Submit Review' or by pressing ENTER.

    `;

    let autoAccept = rw.config.rwEnableReviewAutoRevert == "enable" ? "true" : "false";

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["pendingReviewReason.html"]), 500, 350)).showModal();

    });

    }

    }, // end init

    /**

    * Confirm that we are still confirming this revision - if not, the user will be prompted whether they still wish to initate the callback.

    *

    * @param {function} callback

    * @param {boolean} isFailedRevert Whether or not this function was initiated via a failed revert for a different dialog wording.

    * @method confirmLatestRev

    * @extends rw.PendingChangesReview

    */

    "confirmLatestRev" : (callback, isFailedRevert)=> { // Verify that this confirm is the latest revision

    $.getJSON("https://en.wikipedia.org/w/api.php?action=query&prop=revisions&titles="+ encodeURIComponent(mw.config.get("wgRelevantPageName")) +"&rvslots=*&rvprop=ids%7Cuser%7Ccomment&formatversion=2&format=json", r=>{

    // We got the response

    let latestRId = r.query.pages[0].revisions[0].revid;

    let parentRId = r.query.pages[0].revisions[0].parentid;

    let latestUsername = r.query.pages[0].revisions[0].user;

    let latestComment = r.query.pages[0].revisions[0].comment;

    // Get restore ID from right side review

    if ($('#mw-diff-ntitle1 > strong > a').attr('href').split('&')[1].split('=')[1] == latestRId) {callback();} else { // If latest revision send callback else show dialog

    rw.ui.confirmDialog(`

    `+ (!isFailedRevert ? `This is not the latest revision and may have been reverted by another reviewer.` :

    `This edit could not be reverted as another editor has reverted it.`)

    +` Would you like to review the latest revision`+ (isFailedRevert ? " or continue to the multiple action screen anyway" : "") +`?


    Latest revision by `+ latestUsername +`: `+ latestComment +`

    `,

    "GO TO LATEST REVISION", ()=>{

    dialogEngine.closeDialog();

    rw.ui.loadDialog.show("Redirecting...");

    // Redirect to latest version

    redirect("https://en.wikipedia.org/w/index.php?title="+ encodeURIComponent(mw.config.get("wgRelevantPageName")) +"&diff="+ latestRId +"&oldid="+ parentRId +"&diffmode=source#redirectLatestRevision");

    },

    "CONTINUE ANYWAY", ()=>{

    dialogEngine.closeDialog();

    callback();

    }, isFailedRevert? 135 : 115); // continue anyway: close and callback

    }

    });

    },

    /**

    * Calls back with the past 10 users who have had pending edits

    *

    * @param {function} callback callback(lastAcceptedUser, countofEdits, rwMultiActuserObject, userCount)

    * @method getPendingChangesUsers

    * @extends rw.PendingChangesReview

    */

    "getPendingChangesUsers" : callback=> { // CALLBACK(last accepted usr, count, obj, userCount)

    // Get all revisions between last accepted and this one

    $.getJSON("https://en.wikipedia.org/w/api.php?action=query&prop=revisions&format=json&titles="+

    mw.config.get("wgRelevantPageName")+"&rvprop=ids%7Cuser%7Ccomment&rvstartid="+

    $("#mw-fr-input-oldid").attr("value")+"&rvendid="+$("#mw-fr-input-refid").attr("value"),

    r=>{

    // Done

    let revs = Object.values(r.query.pages)[0].revisions

    let lastAcceptedUser = revs.pop().user; // Remove & Return Last item as is the last accepted - Remove last user as not pending

    let userObj = {};

    let editCount = 0;

    let userCount = 0;

    revs.forEach(rev=>{ // For each revision

    let user = rev.user;

    // Summary - revID: summary (click revid to open diff in new tab)

    let summary = ""+ rev.revid +": " + rev.comment;

    // If user not in userObj, add

    if (!(user in userObj)) {

    userObj[user] = {

    "edits" : 0,

    "summaries": []

    };

    userCount += 1;

    };

    // Now add our info

    userObj[user].edits += 1;

    userObj[user].summaries.push(summary);

    editCount += 1;

    });

    // We're done. Callback

    callback(lastAcceptedUser, editCount, userObj, userCount);

    });

    }

    }

    // rw-source: multiAct.js

    /**

    * RedWarn's multiple action tool

    * @class rw.multiAct

    */

    rw.multiAct = { // Multi action screen

    /**

    * Initalise the multiple action tool buttons on a revision history page

    * @method initHistoryPage

    * @extends rw.multiAct

    */

    "initHistoryPage" : ()=> {

    // If on history page, add button to mass warn between edits

    if(window.location.href.includes("/index.php?title=") && window.location.href.includes("&action=history")) {

    // On history page, add button

    $(".mw-history-compareselectedversions").append(`

    `);

    // Result handler

    let userObj = {};

    let editCount = 0;

    let userCount = 0;

    let resultHandler = r=>{

    // Done

    console.log(r);

    let revs = Object.values(r.query.pages)[0].revisions

    revs.forEach(rev=>{ // For each revision

    let user = rev.user;

    // Summary - revID: summary (click revid to open diff in new tab)

    let summary = ""+ rev.revid +": " +

    ((rev.comment != null && rev.comment.length > 0) ? rev.comment : "No summary provided");

    // If user not in userObj, add

    if (!(user in userObj)) {

    userObj[user] = {

    "edits" : 0,

    "summaries": []

    };

    userCount += 1;

    };

    // Now add our info

    userObj[user].edits += 1;

    userObj[user].summaries.push(summary);

    editCount += 1;

    });

    // Check if continue

    if (r.continue != null) {

    // Still more to do, so resubmit

    let latestID = $('#mw-history-compare').find('input[name="diff"]:checked').val();

    let oldestID = $('#mw-history-compare').find('input[name="oldid"]:checked').val();

    $.getJSON(rw.wikiAPI + "?action=query&prop=revisions&format=json&titles="+

    mw.config.get("wgRelevantPageName")+"&rvprop=ids%7Cuser%7Ccomment&rvstartid="+

    latestID+"&rvendid="+oldestID + "&rvcontinue="+ r.continue.rvcontinue,

    resultHandler); // continueee

    } else {

    // We're done.

    rw.ui.loadDialog.close(); // close load dialog

    rw.multiAct.open(userObj); // open with userobj

    userObj = {}; // clear to prevent 1 edit... 2 edits.. 3 edits ext.

    }

    }

    $(".rwMAThist").click(e=>{ // On click handler - don't use ID as there are two buttons

    e.preventDefault(); // stop the default redirect

    rw.ui.loadDialog.show("Please wait..."); // Show loading dialog

    // Load all revisions between oldid and diff - remember we are checking between this range

    let latestID = $('#mw-history-compare').find('input[name="diff"]:checked').val();

    let oldestID = $('#mw-history-compare').find('input[name="oldid"]:checked').val();

    // Get all revisions

    $.getJSON(rw.wikiAPI + "?action=query&prop=revisions&format=json&titles="+

    mw.config.get("wgRelevantPageName")+"&rvprop=ids%7Cuser%7Ccomment&rvstartid="+

    latestID+"&rvendid="+oldestID,

    resultHandler);

    });

    }

    },

    /**

    * Open the multiple action tool using the specified user object

    *

    * @param {object} userObj Object in format of {username1: {edits: [(array of revIDs)]}, username2: ..., username3: ...}

    * @method open

    * @extends rw.multiAct

    */

    "open" : userObj=>{ // userobj format or just {}

    // To prevent a new user from mass spamming loads of users, extendedconfirmed only

    rw.info.featureRestrictPermissionLevel("extendedconfirmed", ()=>{

    // Generate list

    let listHTML = ``;

    for (const usr in userObj) {

    if (userObj.hasOwnProperty(usr)) {

    if (usr.toLowerCase().includes("bot")) continue; // skip bots

    if (usr.toLowerCase() == "undefined" || usr == null) continue; // don't do undefined, bug

    const userInfo = userObj[usr];

    // Append table row

    listHTML += `

    `+ usr +`

    ` + userInfo.edits + ` edit` + (userInfo.edits > 1 ? "s" : "") +`

    -

    help

    Level is unknown or loading

    -

    -

    `;

    // Add event handler for edit click

    addMessageHandler("summaries "+ usr, ()=>{

    let revList = ``;

    userInfo.summaries.forEach(el=>{

    revList += el + "


    ";

    });

    rw.ui.confirmDialog(`

    Related Changes

    `+ revList +

    `

    `,

    "CLOSE", ()=>dialogEngine.closeDialog(),

    "", ()=>{}, 500);

    });

    // Add event handler for notice dialog

    addMessageHandler("RWMATnewNotice", ()=>{

    rw.ui.beginWarn(true, "(RedWarn MAT)", mw.config.get("wgRelevantPageName"), "APPLY TO CHECKED", r=>{

    // Now push to iframe

    rw.multiAct.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage(

    "applyToChecked`" + serialize(r) +

    "`Yes`Warn User"

    , '*');

    }, true); // Show Warn User dialog, true at end hides user info

    });

    // Event Handler for new msg

    addMessageHandler("RWMATnewMsg", ()=>{

    rw.ui.newMsg("(RedWarn MAT)", true, "APPLY TO CHECKED", r=>{ // new talk page message dialog

    // Now push to iframe

    rw.multiAct.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage(

    "applyToChecked`" + serialize(r) +

    "`No`New Message"

    , '*');

    });

    });

    // TODO: event handler for quick template

    // Event handler for Commit Changes Confirmation Dialog

    addMessageHandler("RWMATcommitSelectedConfirm`*", cI=>{

    let userCount = cI.split("`")[1];

    // Show confirm dialog

    rw.ui.confirmDialog(`

    Are you sure you want to complete actions for `+ userCount + ` user` + (userCount > 1 ? "s" : "") +`?`,

    "CONFIRM", ()=>{

    dialogEngine.closeDialog(); // close dialog

    rw.ui.loadDialog.show("Editing talk pages..."); // loading dialog

    rw.multiAct.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage(

    "confirmSelected", '*'); // Set the wheels in motion

    },

    "CANCEL", ()=>{

    dialogEngine.closeDialog(); // close dialog on cancel

    }

    ,0);

    });

    // Now where the business happens

    let actionsToTake = []; // array of objects

    // Assemble array handler

    addMessageHandler("RWMATToAdd`*", cI=>{

    actionsToTake.push({

    "user" : cI.split("`")[3],

    "underDate" : cI.split("`")[2] == "true",

    "wikiTxt" : deserialize(cI.split("`")[1])

    });

    });

    // Actuall edit handler

    addMessageHandler("RWMATfinishedandcommit", ()=>{

    // Assemble commit handler

    let editHandler = i=>{

    let action = actionsToTake[i];

    // Time to make the actual contrib

    rw.info.addWikiTextToUserPage(action.user, action.wikiTxt, action.underDate, "Complete MAT action", false, false, ()=>{

    // Check we can continue

    if (i == actionsToTake.length - 1) {

    // We done

    rw.ui.loadDialog.close();

    // Notify of completion

    rw.ui.confirmDialog(`

    Actions complete!`,

    "CLOSE", ()=>{

    dialogEngine.closeDialog(); // close dialog

    },

    "", ()=>{},0);

    } else {

    // Continue

    editHandler(i+1);

    }

    });

    };

    editHandler(0); // start it all

    });

    }

    }

    // Open screen - do not use dialogEngine as other dialogs use this - allow for overlay

    // Generate container

    let content = mdlContainers.generateContainer(eval(rw_includes["multipleAction.html"]), document.body.offsetWidth, document.body.offsetHeight);

    // Dialog gubbins

    // Init if needed

    if ($("#MAdialogContainer").length < 1) {

    // Need to init

    $("body").prepend(`

    `);

    // Add close event

    addMessageHandler("closeDialogMA", ()=>{rw.multiAct.dialog.close();}); // closing

    }

    $("#MAdialogContainer").html(`

    `+ content +`

    `);

    rw.multiAct.dialog = document.querySelector('#rwMAdialog'); // set dialog

    $("#rwMAdialog").attr("style", "padding:inherit;"); // set no padding

    // Firefox issue fix

    if (! rw.multiAct.dialog.showModal) {

    dialogPolyfill.registerDialog(rw.multiAct.dialog);

    }

    rw.multiAct.dialog.showModal(); // Show dialog

    // Now load all the edit counts and last warning levels

    let usernames = Object.keys(userObj);

    let userPages = {};

    // Add userpage preview handler

    addMessageHandler("RWMAviewUserPg`*", cI=>{

    let userPgWikiTxt = userPages[cI.split("`")[1]]; // get wikitext from obj

    // Parse wikitext (show loading dialog first)

    rw.ui.loadDialog.show("Parsing userpage...");

    rw.info.parseWikitext(userPgWikiTxt, parsed=>{ // Request

    rw.ui.loadDialog.close(); // close load dialog

    // Show preview dialog

    rw.ui.confirmDialog(`

    `+parsed+`

    `,

    "CLOSE", ()=>dialogEngine.closeDialog(),

    "", ()=>{}, 500);

    });

    });

    // Last Warn level and userpg handler (done as essentially foreach, called from editcount)

    let lastWarnLevelHandler = (i, callback) => { // LAST WARNING

    let un = usernames[i];

    rw.info.lastWarningLevel(rw.info.targetUsername(un), (w, usrPgMonth, userPg)=>{

    let lastWarning = [ // Return HTML for last warning level.

    // NO PAST WARNING

    `

    thumb_up

    No notices this month.

    `,

    // NOTICE

    `

    info

    Has been given a Level 1 notice
    this month.

    `,

    // CAUTION

    `

    announcement

    Has been given a Level 2 caution
    this month.

    `,

    // Warning- in red. RedWarn, get it? This is the peak of programming humour.

    /// Haha, whoops? This joke has turned up twice. Who'd ever think they'd see reused code?

    `

    report_problem

    Has been given a Level 3 warning
    this month.

    `,

    // Final/Only Warning (dark red)

    `

    report

    Has been given a Level 4 Final
    or ONLY warning.
    this month.

    `

    ][w];

    // Set userpage var for preview

    userPages[un] = userPg;

    // Now push to iframe

    rw.multiAct.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage("PastWarning-"+ un + "`" + lastWarning, '*');

    callback(); // Send callback

    });

    };

    let editCounterHandler = (i, isRetry)=>{ // EDIT COUNT

    if (rw.info.isUserAnon(usernames[i])){ // skip edits by anon IPs as they will not return, just set lastwarnlevel

    lastWarnLevelHandler(i, ()=>{

    editCounterHandler(i + 1);

    });

    return;

    }

    rw.info.getUserEditCount(usernames[i], cI=>{

    // Push to iframe

    // Set c to blank space if edit count undefined or edit count with number sorted if is

    let c = cI == null ? "-" : cI.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); // regex adds commas to number to make more readable

    rw.multiAct.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage("editCount-"+ usernames[i] + "`" + c, '*');

    // Make sure there wasn't an issue, if so redo, and if redo fails just continue

    if ((cI == null) && !isRetry) {

    editCounterHandler(i, true); // do it again with retry flag

    return;

    }

    // Now last warning handler

    lastWarnLevelHandler(i, ()=>{

    // Make sure there is a next

    if (i != usernames.length - 1) {

    editCounterHandler(i + 1); // do it again with next user

    } // else we done!

    });

    });

    };

    editCounterHandler(0); // call and begin load

    }, ()=>{

    // When no perms

    rw.ui.confirmDialog(`

    Sorry, but to prevent misuse you have to be an extended confirmed user to use the multiple action tool. Check back later!

    `,

    "OKAY", ()=>dialogEngine.closeDialog(),

    "", ()=>{}, 23);

    });

    }

    };

    // rw-source: quickTemplate.js

    rw.quickTemplate = { // Quick template UI and loader

    "packStore": [],

    "packs": () => { // Get packs from config and default and merge to packstore

    if (rw.quickTemplate.packStore.length > 0) return rw.quickTemplate.packStore; // return if already set

    if (rw.config.templatePacks == null) { // if templates not set

    rw.config.templatePacks = []; // set to empty

    rw.info.writeConfig(true, () => { }); // update config page

    }

    rw.quickTemplate.packStore = rw.quickTemplate.packStore.concat(rw.config.templatePacks);

    return rw.quickTemplate.packStore; // return

    },

    "packCodeToPageName": packCode => { // Convert a pack code to it's real page name

    return "User:" + packCode.split("/")[0] + "/RedWarn_QuickTemplate_packInstall_" + packCode.split("/")[1] + ".js";

    },

    "openSelectPack": un => {

    // Assemble buttons for each pack

    let finalBtnStr = "";

    rw.quickTemplate.packs().forEach((pack, i) => {

    if (pack.name == null) return; // skip "undefined by undefined"

    finalBtnStr += `



    `;

    });

    // Add toast handler

    addMessageHandler("pushToast`*", m => rw.visuals.toast.show(m.split('`')[1], false, false, 4000));

    // Add new pack handler

    addMessageHandler("qTnewPack", () => rw.quickTemplate.newPack());

    // Show pack selection dialog

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["quickTemplateSelectPack.html"]), 500, 530)).showModal();

    // Pack Selected Handler

    addMessageHandler("selectPack`*", cI => {

    dialogEngine.closeDialog();

    let i = parseInt(cI.split("`")[1]); // get index from call

    rw.quickTemplate.selectTemplate(un, i); // open select template screen

    });

    // Pack edit handler

    addMessageHandler("editPack`*", cI => {

    dialogEngine.closeDialog();

    let i = parseInt(cI.split("`")[1]); // get index from call

    rw.quickTemplate.selectTemplate(un, i, true); // open select template screen, true denotes edit

    });

    },

    "selectTemplate": (un, i, editMode) => {

    if (editMode) un = "(edit mode)";

    let selectedPack = rw.quickTemplate.packs()[i];

    let finalSelectStr = "


    "; // final select string

    selectedPack.templates.forEach((template, i) => {

    finalSelectStr += `

    `+ template.title + `


    `+ template.about + `


    `;

    });

    // Add edit mode handlers

    if (editMode) {

    addMessageHandler("qTdeletePack", () => {

    // Confirm

    rw.ui.confirmDialog(`

    Are you sure you want to delete this pack?

    Note: This will only delete the pack from your account. If it is published, this won't unpublish it.

    `,

    "YES, DELETE.", () => {

    dialogEngine.closeDialog(); // close prompt

    rw.ui.loadDialog.show("Deleting...");

    // Now remove at index

    rw.config.templatePacks.splice(i, 1);

    // Now refresh and save config

    rw.info.writeConfig(); // save config, will also refresh

    },

    "CANCEL", () => {

    // On cancel just recall

    dialogEngine.closeDialog();

    rw.quickTemplate.selectTemplate(un, i, editMode); // reshow, then done!

    }, 45

    );

    });

    // Handle edit and new

    addMessageHandler("qTNew`*", cI => { //create new template

    dialogEngine.closeDialog();

    // Set up vars

    let nTitle = cI.split("`")[1];

    let nAbout = cI.split("`")[2];

    // Add to our template array

    rw.config.templatePacks[i].templates.push({

    "title": nTitle,

    "about": nAbout,

    "content": ""

    });

    // Clear and reload config

    rw.ui.loadDialog.show("Creating...");

    rw.info.writeConfig(true, () => { // save config

    rw.ui.loadDialog.close();

    rw.quickTemplate.packStore = []; // clear out packs

    // Refresh selected pack

    selectedPack = rw.quickTemplate.packs()[i];

    // Open editor

    rw.quickTemplate.editTemplate(i, selectedPack.templates.length - 1); // w pack index and template index

    });

    });

    addMessageHandler("qTEdit`*", cI => {

    // Open editor

    rw.quickTemplate.editTemplate(i, cI.split("`")[1]); // w pack index and template index

    });

    // Publish

    addMessageHandler("qTPublish", () => {

    if (selectedPack.packCode == null) { // Pack isn't published

    rw.ui.confirmDialog("This pack isn't published. If you'd like to let other editors use your pack, click 'Publish Now'",

    "PUBLISH NOW", () => {

    // Publish new template

    dialogEngine.closeDialog(); // close dialog

    rw.quickTemplate.publish(selectedPack, true, i); // true here denotes is new

    },

    "CANCEL", () => dialogEngine.closeDialog(), 18);

    } else {

    // Pack is published

    rw.ui.confirmDialog("This pack is published. If you'd like to update your pack for other editors, click 'Update Now'",

    "UPDATE NOW", () => {

    // Publish new template

    dialogEngine.closeDialog(); // close dialog

    rw.quickTemplate.publish(selectedPack); // just normal update

    },

    "CANCEL", () => dialogEngine.closeDialog(), 18);

    }

    });

    }

    // END edit mode handlers

    // Now we need to assemble the select screen

    // Show template selection dialog

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["quickTemplateSelectTemplate.html"]), 500, 530)).showModal();

    // Continue Handler (not called in edit mode)

    addMessageHandler("qTNext`*", cI2 => {

    let i2 = parseInt(cI2.split("`")[1]); // i from above frame

    rw.quickTemplate.applyTemplate(selectedPack, i2, un); // open apply template screen

    });

    },

    "applyTemplate": (selectedPack, i2, unI) => {

    let selectedTemplate = selectedPack.templates[i2];

    let contentStr = selectedTemplate.content;

    let un = rw.info.targetUsername(unI);

    let addUnderDate = contentStr.includes("##RW UNDERDATE##");

    // Now we need to assemble inputs for this template

    // Add dialog handlers for preview

    addMessageHandler("generatePreview`*", m => {

    rw.info.parseWikitext(m.split("`")[1], parsed => { // Split to Wikitext and send over to the API to be handled

    dialogEngine.dialog.getElementsByTagName("iframe")[0].contentWindow.postMessage({

    "action": "parseWikiTxt",

    "result": parsed

    }, '*'); // push to container for handling in dialog and add https:// to stop image breaking

    });

    });

    // Add handlers for submit

    addMessageHandler("qtDone`*", eD => {

    let wikiTxtToAdd = deserialize(eD.split("`")[1]); // params

    // MAKE EDIT

    rw.info.addWikiTextToUserPage(rw.info.targetUsername(un), wikiTxtToAdd, addUnderDate, "" + selectedPack.name + " - " + selectedTemplate.title + "");

    });

    // Now generate text input code - SYNTAX {{RWTEXT|Label|ID}}

    let finalAdditionalInputs = ``;

    // NORMAL TEXT INPUT

    (m => {

    if (m != null) { // to stop errors

    m.forEach(match => { // for each match to regex

    let v = match.split("|"); // split at pipe for varibles

    let label = v[1]; // strip label

    let id = v[2].split("}")[0]; // strip ID

    // Now add our textbox

    finalAdditionalInputs += `

    `;

    });

    }

    })(selectedTemplate.content.match(/{{RWTEXT\|[^}}]*\|[^{{]*?}}/g)); // regex here for above function

    // Finally, show final submit dialog

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["quickTemplateSubmit.html"]), 500, 580)).showModal();

    },

    "newPack": () => {

    // Creates a new pack and saves to config

    // Add toast handler

    addMessageHandler("pushToast`*", m => rw.visuals.toast.show(m.split('`')[1], false, false, 4000));

    addMessageHandler("qTcreateNew`*", cI => {

    // Handle calls the create new pack

    let packName = cI.split("`")[1];

    rw.config.templatePacks.push(

    {

    "name": packName,

    "createdBy": rw.info.getUsername(),

    "templates": []

    }

    ); // add to config

    rw.ui.loadDialog.show("Creating...");

    rw.info.writeConfig(true, () => { // save config

    rw.ui.loadDialog.close();

    rw.quickTemplate.packStore = []; // clear out packs

    // Refresh packs and open selection screen in edit mode

    rw.quickTemplate.selectTemplate("", rw.quickTemplate.packs().length - 1, true); // true here distingishes edit mode

    });

    });

    // Finally, show the dialog

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["quickTemplateNewPack.html"]), 500, 200)).showModal();

    },

    "editTemplate": (selectedPackI, selectedTemplateI) => {

    // Used to edit template

    let selectedPack = rw.quickTemplate.packs()[selectedPackI];

    let selectedTemplate = selectedPack.templates[selectedTemplateI];

    let saveHandler = (b64data, callback) => { // Save Data handler

    // Set vars

    let title = deserialize(b64data.split("`")[1]);

    let about = deserialize(b64data.split("`")[2]);

    let content = deserialize(b64data.split("`")[3]);

    rw.config.templatePacks[selectedPackI].templates[selectedTemplateI] = {

    "title": title,

    "about": about,

    "content": content

    };

    // Clear and reload config

    rw.ui.loadDialog.show("Saving...");

    rw.info.writeConfig(true, () => { // save config

    rw.ui.loadDialog.close();

    rw.quickTemplate.packStore = []; // clear out packs

    // Refresh selected pack

    selectedPack = rw.quickTemplate.packs()[selectedPackI];

    // Refresh selected template

    selectedTemplate = selectedPack.templates[selectedTemplateI];

    if (callback != null) callback(); // send callback if set

    });

    };

    // Save changes handler

    addMessageHandler("qTSave`*", cI => {

    saveHandler(cI);

    });

    // Close Editor Handler

    addMessageHandler("qTClose`*", cI => {

    saveHandler(cI, () => dialogEngine.closeDialog());

    });

    // Test Handler

    addMessageHandler("qTTest`*", cI => {

    saveHandler(cI, () => {

    // Open normal apply window with sandbox target

    rw.quickTemplate.applyTemplate(selectedPack, selectedTemplateI, "Sandbox for user warnings");

    });

    });

    // Finally, open the edit template dialog

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["quickTemplateEditTemplate.html"]), 500, 550)).showModal();

    },

    "publish": (selectedPack, isNew, selectedPackI) => { // makes a listing on the WP:REDWARN/QTPACKS page if isnew is set to true

    // Publish new pack

    rw.ui.loadDialog.show("Publishing...");

    let packCode = selectedPack.packCode; // get packcode from selected pack

    if (packCode == null) packCode = rw.info.getUsername() + "/" + rw.makeID(10); // Generate a new pack code if not already published

    let packPage = rw.quickTemplate.packCodeToPageName(packCode); // get page to write script to

    // Now, generate script that installs this pack

    let packInstallScript = `

    /*

    +------------------------------------+

    | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |

    | Generated automatically. |

    | DO NOT EDIT THIS FILE OR ADD TO |

    | YOUR COMMON.JS FILE. DOING SO |

    | WILL CAUSE SERIOUS ISSUES. |

    +------------------------------------+

    Install script (c) Ed. E (User:Ed6767) - license: https://gitlab.com/redwarn/redwarn-web

    • /

    rw.ui.loadDialog.show("Installing...");

    let packSource = JSON.parse((${deserialize.toString()})("${serialize(JSON.stringify(selectedPack))}")); // Load source from currentpack

    rw.config.templatePacks.push(packSource); // Push into config

    rw.info.writeConfig(true, ()=>{ // save config

    rw.ui.loadDialog.close();

    rw.quickTemplate.packStore = []; // clear out packs

    // Show success dialog

    rw.ui.confirmDialog(

    "Pack installed!",

    "RELOAD PAGE", ()=>window.location.reload(),

    "", ()=>{}, 0);

    });

    //

    `;

    // Script generated, let's continue

    //Commit to script page

    $.post(rw.wikiAPI, {

    "action": "edit",

    "format": "json",

    "token": mw.user.tokens.get("csrfToken"),

    "title": packPage,

    "summary": "Publish Autogenerated QTPack Install (RedWarn " + rw.version + ")", // summary sign here

    "text": packInstallScript,

    "tags": ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

    }).done(dt => {

    // We done. Check for errors, then callback appropriately

    if (!dt.edit) {

    // Error occured or other issue

    console.error(dt);

    rw.ui.loadDialog.close();

    rw.visuals.toast.show("Sorry, there was an error. See the console for more info. Your pack has not been published.");

    } else {

    // Success!

    if (!isNew) { // if not a new pack, show update done screen and exit

    rw.ui.loadDialog.close();

    rw.ui.confirmDialog("Pack updated successfully!", "DONE", () => dialogEngine.closeDialog(), "", () => { }, 0);

    return;

    }

    // CONINUES ONLY IF NEW

    // Now make change to config in our preferences re new change

    rw.config.templatePacks[selectedPackI].packCode = packCode; // add packcode to template config

    rw.info.writeConfig(true, () => { // save config

    rw.quickTemplate.packStore = []; // clear out packs

    // Now add to QTPacks page

    $.post(rw.wikiAPI, {

    "action": "edit",

    "format": "json",

    "token": mw.user.tokens.get("csrfToken"),

    "title": "Wikipedia:RedWarn/Documentation/Quick_Template/templates",

    "summary": "Publish new pack (RedWarn " + rw.version + ")", // summary sign here

    "appendtext": // Add our section wikitxt here

    `

    = ${selectedPack.name} - by ${selectedPack.createdBy} =

    No additional info provided - pack owner, please edit this section and add additional info here or your pack may be removed.

    == Pack Code ==

    ${packCode}`,

    "tags": ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

    }).done(dt2 => {

    if (!dt.edit) {

    // Error occured or other issue

    console.error(dt);

    rw.ui.loadDialog.close();

    rw.visuals.toast.show("Sorry, there was an error. See the console for more info. Your pack has not been published to the Quick Template Pack page, maybe add it manually.");

    } else {

    // Done! Load QTPack page

    redirect("https://en.wikipedia.org/wiki/WP:REDWARN/QTPACKS"); // No rw.wikiBase due to lack of page there

    }

    });

    });

    }

    });

    }

    };

    // rw-source: pageProtect.js

    /**

    * RedWarn's RFPP feature

    * @class rw.pageProtect

    */

    rw.pageProtect = { // Used for WP:RFPP

    /**

    * The RFPP page that RedWarn will edit for protection upgrades

    * @property rfppPageUp

    * @type {string}

    * @extends rw.pageProtect

    */

    "rfppPageUp" : "Wikipedia:Requests_for_page_protection/Increase", // !!! FOR PRODUCTION USE Wikipedia:Requests_for_page_protection ELSE USE A DIFFERENT PAGE !!!

    /**

    * The RFPP page that RedWarn will edit for protection downgrades

    * @property rfppPageDown

    * @type {string}

    * @extends rw.pageProtect

    */

    "rfppPageDown" : "Wikipedia:Requests_for_page_protection/Decrease", // !!! FOR PRODUCTION USE Wikipedia:Requests_for_page_protection ELSE USE A DIFFERENT PAGE !!!

    // THIS MODULE IS NOW LIVE!!!

    /**

    * An object containing the protection levels for this Wiki

    * @property editProtectionLevels

    * @type {object}

    * @extends rw.pageProtect

    */

    // Define protection levels

    "editProtectionLevels" : { //edit protection - SET IN ORDER AS DISTINGUISHES BETWEEN UPGRADE AND DOWNGRADE

    "unprotected" : { // unprotected

    "title" : "Page is not protected",

    "name" : "no protection",

    "image" : "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Semi-protection-unlocked.svg/320px-Semi-protection-unlocked.svg.png"

    },

    "pendingChanges" : {

    // Edits only

    "title" : "Page is pending changes protected",

    "name" : "pending changes protection",

    "image" : "https://upload.wikimedia.org/wikipedia/en/thumb/b/b7/Pending-protection-shackle.svg/240px-Pending-protection-shackle.svg.png"

    },

    "autoconfirmed" : {

    "title" : "Page is semi-protected",

    "name" : "semi-protection",

    "image" : "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Semi-protection-shackle.svg/240px-Semi-protection-shackle.svg.png"

    },

    "extendedconfirmed" : {

    "title" : "Page is extended-protected",

    "name" : "extended protection",

    "image" : "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Extended-protection-shackle.svg/240px-Extended-protection-shackle.svg.png"

    },

    "templateeditor" : { // template protection

    "title" : "Page is template protected",

    "name" : "template protection",

    "image" : "https://upload.wikimedia.org/wikipedia/en/thumb/5/53/Template-protection-shackle.svg/240px-Template-protection-shackle.svg.png"

    },

    "sysop" : {

    "title" : "Page is fully protected",

    "name" : "full protection",

    "image" : "https://upload.wikimedia.org/wikipedia/en/thumb/4/44/Full-protection-shackle.svg/240px-Full-protection-shackle.svg.png"

    },

    "cascading" : {

    // Cascading protection

    "title" : "Page is cascade protected",

    "name" : "cascade protection",

    "image" : "https://upload.wikimedia.org/wikipedia/en/thumb/0/0f/Cascade-protection-shackle.svg/240px-Cascade-protection-shackle.svg.png"

    }

    },

    // for move protection add smallprint: move protection limit: x

    /**

    * Calls back with the current protection level of the current page

    *

    * @param {function} callback callback(object {edit: level, move:level})

    * @method getCurrentPageProtection

    * @extends rw.pageProtect

    */

    "getCurrentPageProtection" : callback=>{ // Callback {edit: level, move:level}

    // Request protection level from mediawiki

    $.getJSON(rw.wikiAPI + "?action=query&format=json&prop=info%7Cflagged&inprop=protection&titles="+ encodeURIComponent(mw.config.get("wgRelevantPageName")), r=>{

    let pageInfo = r.query.pages[Object.keys(r.query.pages)[0]];

    let editProtection = "unprotected";

    let moveProtection = "unprotected";

    pageInfo.protection.forEach(protection => {

    if ((editProtection == "unprotected") && (protection.type == "edit")) editProtection = protection.level; // set edit protection level

    if ((moveProtection == "unprotected") && (protection.type == "move")) moveProtection = protection.level; // set move protection level

    if ((protection.cascade != null) && (protection.type == "edit")) editProtection = "cascading"; // cascading edit protection

    });

    // Detect pending changes protection

    if (pageInfo.flagged != null) {

    if (pageInfo.flagged.protection_level != null) editProtection = "pendingChanges";

    }

    callback({"edit": editProtection, "move": moveProtection}); // done, send callback

    });

    },

    /**

    * Opens the page protection dialog for this page

    *

    * @method open

    * @extends rw.pageProtect

    */

    "open" : ()=>{ // Open page protection dialog for this page

    // Get current page protection level

    rw.pageProtect.getCurrentPageProtection(protection=>{

    let protectionInfo = rw.pageProtect.editProtectionLevels[protection.edit];

    // Create the list of levels

    let finLevelListStr = ``;

    for (let levelKey in rw.pageProtect.editProtectionLevels) {

    if (rw.pageProtect.editProtectionLevels.hasOwnProperty(levelKey)) {

    let level = rw.pageProtect.editProtectionLevels[levelKey];

    finLevelListStr += `

    ${level.name.charAt(0).toUpperCase() + level.name.slice(1)}

    ${(levelKey == protection.edit) ? `

    This is the current protection level.

    ` : ``}


    `;

    }

    }

    // Add event handlers

    // Add toast handler

    addMessageHandler("pushToast`*", m=>rw.visuals.toast.show(m.split('`')[1],false,false,2500));

    // SUBMIT HANDLER

    addMessageHandler("submitRFPP`*", m=>{

    // Get info from string in

    let requestLevel = m.split("`")[1];

    let requestProtect = rw.pageProtect.editProtectionLevels[requestLevel];

    let changeCoreReason = m.split("`")[2];

    let changeExtraInfo = m.split("`")[3];

    let requestDuration = m.split("`")[4];

    let requestType = "";

    // Distingush whether or not this is an upgrade or download request

    if (Object.keys(rw.pageProtect.editProtectionLevels).indexOf(requestLevel) > Object.keys(rw.pageProtect.editProtectionLevels).indexOf(protection.edit)) {

    // Is upgrade request

    requestType = "upgrade";

    } else {

    requestType = "downgrade";

    }

    // Let's submit - show load dialog

    rw.ui.loadDialog.show("Submitting request...");

    // TIME TO EDIT :)

    // generate heading to add under for upgrade or downgrade

    let headingToInsertUnder = "== "+ (requestType == "upgrade" ? "Current requests for increase in protection level" : "Current requests for reduction in protection level") +" ==";

    // Assemble text to add per page template

    let text = `=== :${mw.config.get("wgRelevantPageName").replace(/_/g, ' ')} ===

    • {{pagelinks|${mw.config.get("wgRelevantPageName").replace(/_/g, ' ')}}}

    ${(requestProtect.name == "no protection" ? "Unprotection" : requestDuration + " " + requestProtect.name)}: ${(changeCoreReason == "Other rationale" ? "" : changeCoreReason + `. `)}${changeExtraInfo} `+ rw.sign();

    // New req current page.

    $.getJSON(rw.wikiAPI + "?action=query&prop=revisions&titles="+ encodeURIComponent(requestType == "upgrade" ? rw.pageProtect.rfppPageUp : rw.pageProtect.rfppPageDown ) +"&rvslots=*&rvprop=content&formatversion=2&format=json", latestR=>{

    // Grab text from latest revision of talk page

    // Check if exists

    let revisionWikitext = "";

    if (!latestR.query.pages[0].missing) { // If page isn't missing, i.e exists

    revisionWikitext = latestR.query.pages[0].revisions[0].slots.main.content;

    } else {

    // Bug!

    rw.ui.loadDialog.text("ERR: RFPP page nonexist!");return; // send basic and EXIT as unlikely to be seen expect by dev

    }

    let wikiTxtLines = revisionWikitext.split("\n");

    let finalTxt = "";

    // Check if page already reported

    if (revisionWikitext.includes(mw.config.get("wgRelevantPageName").replace(/_/g, ' '))) {

    // Don't continue and show toast

    rw.ui.loadDialog.close();

    rw.visuals.toast.show("It looks like an RFPP request already exists for this page.", false, false, 5000);

    return;

    }

    // Now submit under heading

    // Locate where the current section is and add ours

    let locationOfLastLine = wikiTxtLines.indexOf(headingToInsertUnder) + 1; // in case of date heading w nothing under it

    for (let i = wikiTxtLines.indexOf(headingToInsertUnder) + 1; i < wikiTxtLines.length; i++) {

    if (wikiTxtLines[i].startsWith("== ")) {

    // New section

    locationOfLastLine = i - 1; // the line above is therefore the last

    console.log("exiting loop: " +wikiTxtLines[locationOfLastLine]);

    break; // exit the loop

    } else if (i == wikiTxtLines.length - 1) {

    // End of page, let's break and set location of last line.

    locationOfLastLine = i;

    break; // exit loop

    }

    }

    if (locationOfLastLine == wikiTxtLines.length - 1) {

    // To prevent to end notices squishing against eachother

    // Same as without, but we just include the date string at bottom of page

    wikiTxtLines.push(["\n" + text]);

    } else {

    wikiTxtLines.splice(locationOfLastLine, 0, ["\n" + text]); // Add notice to array at correct position. Note the "" at the start is for a newline to seperate from prev content

    }

    // Process final string

    wikiTxtLines.forEach(ln => finalTxt = finalTxt + ln + "\n"); // Remap to lines

    console.log(finalTxt);

    // Push edit using CSRF token

    $.post(rw.wikiAPI, {

    "action": "edit",

    "format": "json",

    "token" : mw.user.tokens.get("csrfToken"),

    "title" : requestType == "upgrade" ? rw.pageProtect.rfppPageUp : rw.pageProtect.rfppPageDown,

    "summary" : `Requesting protection change for ${mw.config.get("wgRelevantPageName").replace(/_/g, ' ')} (RW ${rw.version})`, // summary sign here

    "text": finalTxt,

    "tags" : ((rw.wikiID == "enwiki") ? "RedWarn" : null) // Only add tags if on english wikipedia

    }).done(dt => {

    // We done. Check for errors, then callback appropriately

    if (!dt.edit) {

    // Error occured or other issue

    console.error(dt);

    rw.ui.loadDialog.close();

    rw.visuals.toast.show("Sorry, there was an error. See the console for more info. Your RFPP has not been sent.");

    // Reshow dialog

    dialogEngine.dialog.showModal();

    } else {

    // Success!

    rw.ui.loadDialog.close();

    rw.visuals.toast.show("RFPP requested.");

    }

    });

    });

    });

    // Open dialog

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["requestPageProtect.html"]), 600, 630)).showModal();

    });

    }

    };

    // rw-source: firstTimeSetup.js

    /**

    * rw.firstTimeSetup provides a welcome and first time setup screen for new users

    * @class rw.firstTimeSetup

    */

    rw.firstTimeSetup = {

    /**

    * Launches the first time setup wizard

    * @method launch

    * @extends rw.firstTimeSetup

    */

    "launch": () => {

    addMessageHandler("config`*", rs => { // On config change

    // New config recieved

    let config = JSON.parse(deserialize(rs.split("`")[1])); // b64 encoded json string

    //Write to our config

    for (const key in config) {

    if (config.hasOwnProperty(key)) {

    const element = config[key];

    rw.config[key] = element; // add or change value

    }

    }

    // Push change

    rw.ui.loadDialog.show("Saving...");

    rw.info.writeConfig(false);

    });

    addMessageHandler("resetConfig", rs => {

    // Reset config received, set config back to default

    rw.info.getConfig(() => { }, true); // TRUE HERE MEANS RESET TO DEFAULT

    });

    // Add load new theme handler

    addMessageHandler("newThemeDialog", () => rw.ui.loadDialog.show("Changing theme..."));

    addMessageHandler("loadDialogClose", () => rw.ui.loadDialog.close());

    // Add reload handler

    addMessageHandler("reload", () => window.location.reload());

    // Lock scrolling

    dialogEngine.freezeScrolling();

    // Open preferences page with no padding, full screen

    dialogEngine.create(mdlContainers.generateContainer(eval(rw_includes["firstTimeSetup.html"]), window.innerWidth, window.innerWidth, true), true).showModal(); // TRUE HERE MEANS NO PADDING.

    }

    };

    // rw-source: preferences.js

    // Used to handle the new preferences screen in RW16

    rw.preferences = {

    "options" : [ // Holds all the preferences in JSON format in order, some options, such as reoganising icons, are templates and can be referred to

    // THESE ARE RENDERED AS TABS AS OF 16.1, add a new tab, then load your card with a parameter matching the cartTitle

    // CARDS HERE

    {

    "cardTitle" : "Appearance",

    "supportingImage" : "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Sunset_in_Ashbourne.jpg/1280px-Sunset_in_Ashbourne.jpg",

    "content" : { // values here

    // Colour theme

    "colTheme" : { // config value as title

    // UI text

    "optionTitle" : "Theme",

    "supportingText": "Customise RedWarn by setting your theme.",

    "customHTMLOpt": `onchange="updateColourTheme();"`, // update within the UI for all

    // Config options

    "options" : { // human readable: actual value - END HUMAN READABLE WITH * for default option

    "WikiBlue*" : "blue-indigo",

    "Sunshine" : "amber-yellow",

    "Purple Power" : "purple-deep_purple",

    "RedWarn Minimal": "blue_grey-red",

    "Lime Forrest": "brown-light_green",

    "Orange Juice": "orange-deep_orange",

    "Candy Floss": "pink-red"

    }

    }, // end

    // Dialog animations

    "dialogAnimation" : { // config value as title

    // UI text

    "optionTitle" : "Dialog Animations",

    "supportingText": "Change the animation used when a RedWarn dialog opens/closes.",

    // Config options

    "options" : {

    "Default*" : "default",

    "Spinny" : "spinny",

    "Mega" : "mega",

    "Disable Animation": "none"

    }

    }, // end

    // Page icon locations

    "pgIconsLocation" : { // config value as title

    // UI text

    "optionTitle" : "Location of RedWarn controls",

    "supportingText": "Change the location of where the RedWarn page controls appear. Depending on your Skin, your preferences may or may not be honored.",

    // Config options

    "options" : {

    "After Page Icons (iconified)*" : "default",

    "Page Sidebar/Navigation (iconified)": "sidebar",

    "In the \"more\" dropdown menu (like Twinkle, plain links)": "dropdown",

    "At the top of the page (plain links)": "toplinks"

    }

    }, // end

    // Replace references to quick rollback and rollback with QRB and RB

    "rwRollbackShorten" : { // config value as title

    // UI text

    "optionTitle" : "Shorten references to rollback",

    "supportingText": "Shortern the rollback and quick rollback buttons to RB and QRB respectively. If you're experienced, this can help reduce reading times.",

    // Config options

    "options" : {

    "Enable" : "enable",

    "Disable*": "disable"

    }

    }, // end

    // Patrol appearence, we'll just remove, not really sure people change these settings

    }

    },

    {

    "cardTitle" : "Behaviour",

    "supportingImage" : "https://upload.wikimedia.org/wikipedia/commons/thumb/0/07/Wilkin_River_close_to_its_confluence_with_Makarora_River%2C_New_Zealand.jpg/1280px-Wilkin_River_close_to_its_confluence_with_Makarora_River%2C_New_Zealand.jpg",

    "content" : {

    // User right-click settings

    "rwDisableRightClickUser" : { // config value as title

    // UI text

    "optionTitle" : "Open Quick User action menu with...",

    "supportingText": "Change the way you open the Quick User action menu. This menu allows you to access features such as warning a user, reporting them and other tools when right-clicking on a user link or signature. Some browsers, such as Firefox, may override the default option of opening the menu on Shift+Right-click.",

    // Config options

    "options" : {

    "Open when I hold shift and right-click a user link*" : "enable",

    "Open when I right-click a user link": "Opt2",

    "Disable the Quick User action menu": "disable"

    }

    },

    // Warn user automation

    "rwautoLevelSelectDisable" : {

    "optionTitle" : "Automation",

    "supportingText": "Enable or disable RedWarn's automation features, such as automatically choosing a warning level and template for you. Please note that in order to reduce abuse, your preference will not be honored unless you are a extended-confirmed user. If you have a legitimate alternate account, you can request that the extended-confirmed right be granted to it.",

    // Config options

    "options" : {

    "Enable*" : "enable",

    "Disable": "disable"

    }

    },

    // Warn user advanced by default

    "rwWarnUserAdvanced" : {

    "optionTitle" : "Automatically open Warn User in advanced mode",

    "supportingText": "Open RedWarn's warn user dialog in advanced mode by default. Please note you will have to wait for longer for the advanced mode to initalise.",

    // Config options

    "options" : {

    "Enable" : "enable",

    "Disable*": "disable"

    }

    },

    // On rollback completion

    "rwRollbackDoneOption" : {

    "optionTitle" : "Once a rollback is complete...",

    "supportingText": "Change what occurs when a rollback is successful. Selecting \"Warn User\" is recommended for most users.",

    // Config options

    "options" : {

    "Let me choose*" : "none",

    "Warn User (recommended)": "RWRBDONEwarnUsr",

    "Write a new user talk page message": "RWRBDONEnewUsrMsg",

    "Open the Quick Template menu": "RWRBDONEwelcomeUsr",

    "Go to the latest revision": "RWRBDONEmrevPg"

    }

    },

    // On rollback completion

    "rwLatestRevisionOption" : {

    "optionTitle" : "When I am redirected to the latest revision...",

    "supportingText": "Change what happens when you click the \"Latest Revision\" button, or when you are automatically redirected to the latest revision. Please note that the new tab option may be blocked by your browser pop-up blocker.",

    // Config options

    "options" : {

    "Redirect me in the current tab*" : "stayintab",

    "Open a new tab for the latest revision": "newtab"

    }

    },

    // Warn user dialog with

    "rwNoticeListByTemplateName" : {

    "optionTitle" : "When selecting a user template, let me see...",

    "supportingText": "Change what you see when selecting a warning template in the warn user dialog. If you choose to see template names by default, the descriptions will show when you hover your mouse over a template name.",

    // Config options

    "options" : {

    "Template description (e.g. Vandalism)*" : "disable",

    "Template name (i.e. uw-vandalism)": "enable"

    }

    },

    // Rollback method

    "rollbackMethod" : {

    "optionTitle" : "Rollback method",

    "supportingText": "Change the way RedWarn reverts edits. Rollback-like uses the undo feature to revert vandalism, similar to Twinkle and other tools. Meanwhile, rollback uses MediaWiki's \"rollback\" link feature. Both of these are identical in use, although rollback is much faster and more reliable.
    TL/DR: If you have a rollback-enabled account, using the rollback option is highly recommended. If you do not have a rollback-enabled account and select the latter option, your preference will not be honored.",

    // Config options

    "options" : {

    "Rollback-like (slower)*" : "rollbackLike",

    "Rollback (faster, requires permissions)": "rollback"

    }

    },

    // Pending changes

    "rwDisablePendingChanges" : {

    "optionTitle" : "Pending change review",

    "supportingText": "Choose whether to use RedWarn or the built-in tools for reviewing pending changes.",

    // Config options

    "options" : {

    "Use RedWarn*" : "enable",

    "Use built-in tools": "disable"

    }

    },

    // Pending changes auto accept

    "rwDisableReviewAutoAccept" : {

    "optionTitle" : "Pending change review - auto accept",

    "supportingText": "After accepting revisions choose whether to automatically dismiss the reason prompt and accept them after five seconds. Override this countdown by interacting with the dialog",

    // Config options

    "options" : {

    "Enable*" : "enable",

    "Disable": "disable"

    }

    },

    // auto reject

    "rwEnableReviewAutoRevert" : {

    "optionTitle" : "Pending change review - auto revert",

    "supportingText": "After declining revisions choose whether to automatically dismiss the reason prompt and decline them after five seconds. Override this countdown by interacting with the dialog",

    // Config options

    "options" : {

    "Enable" : "enable",

    "Disable*": "disable"

    }

    },

    // Pending changes open MAT

    "rwPendingMATDisable" : {

    "optionTitle" : "Pending change review - open MAT after reverting",

    "supportingText": "Choose whether to automatically open the Multiple Action Tool after you have reviewed a change.",

    // Config options

    "options" : {

    "Enable*" : "enable",

    "Disable": "disable"

    }

    },

    // Email send a copy

    "rwEmailCCMe" : {

    "optionTitle" : "Send me a copy of the emails I send with RedWarn",

    "supportingText": "Change whether or not you wish to send a copy of the emails you send with RedWarn (such as to Wikimedia Trust and Safety and Oversight) to your email address.",

    // Config options

    "options" : {

    "Send me a copy*" : "enable",

    "Don't send me a copy": "disable"

    }

    },

    // Add user talk page to watchlist

    "rwWatchTime" : {

    "optionTitle" : "Watch Time",

    "supportingText": "Change how long RedWarn watches a page for after leaving a warning. This will follow your MediaWiki preferences by default.",

    // Config options

    "options" : {

    "Disable*": null,

    "Don't watch" : 0,

    "Indefinite": "indefinite",

    "1 day": "1 day",

    "1 week": "1 week",

    "1 month": "1 month"

    }

    },

    // developer safe mode

    "debugMode" : {

    "optionTitle" : "Developer safe mode",

    "supportingText": "This option allows developers to test RedWarn safely, and will only work on a RedWarn script provided by a development sever. IMPORTANT: To safely disable debug mode, open the debug menu and enable production behaviour.",

    // Config options

    "options" : {

    "Enable" : "enable",

    "Disable*": "disable"

    }

    }

    }

    }

    ], // about card included by default

    "generateHTML" : cardTitle=>{

    // Generate HTML for UI to use given cardTitle matches

    let finalHTML = ``;

    rw.preferences.options.forEach(card=>{

    if (card.cardTitle != cardTitle) return; // skip if not our card

    // Add the full card html

    finalHTML += `

    ${card.cardTitle}

    ${(()=>{

    // Generate the content and return it

    let fullOptionsStr = ``;

    // For each config option in card

    for (const configKey in card.content) {

    const option = card.content[configKey];

    // Append our HTML

    fullOptionsStr += `

    ${option.optionTitle}

    ${option.supportingText}

    ${(()=>{

    // Generate our options

    let finalRadioButtonHTML = ``;

    for (const optText in option.options) {

    const optID = option.options[optText];

    const elID = configKey + optID;

    finalRadioButtonHTML += `

    ${optText.replace("*", " (default)")}


    `;

    }

    return finalRadioButtonHTML;

    })()}



    `;

    }

    return fullOptionsStr; // finally, add it back to the card

    })()}



    `;

    });

    return finalHTML; // return for preferences pane

    }

    };

    // rw-source: campaigns.js

    // Used to show WikiText headers to users, that can be dimissed.

    rw.campaigns = {

    "load" : ()=>{ // the only function that handles everything

    // Request campaign info from User:RedWarn/campaign.json

    $.getJSON("https://en.wikipedia.org/w/index.php?title=User:RedWarn/campaign.json&action=raw&ctype=text/json", c=>{

    if (c.active && rw.config["campaign_"+ c.id] == null) { // only show if active and not dismissed

    // Finally, add a notification above the string

    $("#rwPGIconContainer").append(`

    new_releases

    Announcement from the RedWarn team!

    `);

    // Now register that

    for (let item of document.getElementsByClassName("mdl-tooltip")) {

    rw.visuals.register(item);

    }

    // Now add onclick handler

    $("#rwCampaignTopicon").click(()=>{

    // Show dialog with campaign info

    rw.ui.confirmDialog(c.text,

    "READ MORE (in new tab)", ()=>{

    // Open in new tab with campaign details

    dialogEngine.closeDialog(()=>redirect(c.actionTarget, true));

    // Save

    rw.config["campaign_"+ c.id] = "opened"; // setting this value hides this campaign

    rw.info.writeConfig(true, ()=>{}); // save config

    },

    "DISMISS", ()=>{

    dialogEngine.closeDialog();//this thing turns it off, but will still be shown on page until refresh ext.

    // Save info

    rw.config["campaign_"+ c.id] = "dismissed"; // setting this value hides this campaign

    rw.info.writeConfig(true, ()=>{}); // save config

    rw.visuals.toast.show("This message will be dismissed the next time RedWarn loads.");

    },c.dialogHeight);

    });

    }

    });

    }

    };

    if (window.rwLoaded) {

    console.warn("RedWarn is being loaded twice!");

    return;

    } else {

    window.rwLoaded = true;

    }

    window.rw = rw;

    $(document).ready(async function() {

    // Initialize RedWarn once the page is loaded.

    try {

    await initRW();

    } catch (err) {

    mw.notify("Sorry, an error occured while loading RedWarn.");

    console.error(err);

    }

    });

    })();

    // =========================== END REDWARN ============================

    //