<commit_before>/**
 * Copyright JS Foundation and other contributors, http://js.foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

module.exports = function(RED) {
    "use strict";
    var os = require('os');
    var dgram = require('dgram');
    var udpInputPortsInUse = {};

    // The Input Node
    function UDPin(n) {
        RED.nodes.createNode(this,n);
        this.group = n.group;
        this.port = n.port;
        this.datatype = n.datatype;
        this.iface = n.iface || null;
        this.multicast = n.multicast;
        this.ipv = n.ipv || "udp4";
        var node = this;

        if (node.iface && node.iface.indexOf(".") === -1) {
            try {
                if ((os.networkInterfaces())[node.iface][0].hasOwnProperty("scopeid")) {
                    if (node.ipv === "udp4") {
                        node.iface = (os.networkInterfaces())[node.iface][1].address;
                    } else {
                        node.iface = (os.networkInterfaces())[node.iface][0].address;
                    }
                }
                else {
                    if (node.ipv === "udp4") {
                        node.iface = (os.networkInterfaces())[node.iface][0].address;
                    } else {
                        node.iface = (os.networkInterfaces())[node.iface][1].address;
                    }
                }
            }
            catch(e) {
                node.warn(RED._("udp.errors.ifnotfound",{iface:node.iface}));
                node.iface = null;
            }
        }

        var opts = {type:node.ipv, reuseAddr:true};
        if (process.version.indexOf("v0.10") === 0) { opts = node.ipv; }
        var server;

        if (!udpInputPortsInUse.hasOwnProperty(node.port)) {
            server = dgram.createSocket(opts);  // default to udp4
            server.bind(node.port, function() {
                if (node.multicast == "true") {
                    server.setBroadcast(true);
                    server.setMulticastLoopback(false);
                    try {
                        server.setMulticastTTL(128);
                        server.addMembership(node.group,node.iface);
                        if (node.iface) { node.status({text:n.iface+" : "+node.iface}); }
                        node.log(RED._("udp.status.mc-group",{group:node.group}));
                    } catch (e) {
                        if (e.errno == "EINVAL") {
                            node.error(RED._("udp.errors.bad-mcaddress"));
                        } else if (e.errno == "ENODEV") {
                            node.error(RED._("udp.errors.interface"));
                        } else {
                            node.error(RED._("udp.errors.error",{error:e.errno}));
                        }
                    }
                }
            });
            udpInputPortsInUse[node.port] = server;
        }
        else {
            node.log(RED._("udp.errors.alreadyused",{port:node.port}));
            server = udpInputPortsInUse[node.port];  // re-use existing
            if (node.iface) { node.status({text:n.iface+" : "+node.iface}); }
        }

        server.on("error", function (err) {
            if ((err.code == "EACCES") && (node.port < 1024)) {
                node.error(RED._("udp.errors.access-error"));
            } else {
                node.error(RED._("udp.errors.error",{error:err.code}));
            }
            server.close();
        });

        server.on('message', function (message, remote) {
            var msg;
            if (node.datatype =="base64") {
                msg = { payload:message.toString('base64'), fromip:remote.address+':'+remote.port, ip:remote.address, port:remote.port };
            } else if (node.datatype =="utf8") {
                msg = { payload:message.toString('utf8'), fromip:remote.address+':'+remote.port, ip:remote.address, port:remote.port };
            } else {
                msg = { payload:message, fromip:remote.address+':'+remote.port, ip:remote.address, port:remote.port };
            }
            node.send(msg);
        });

        server.on('listening', function () {
            var address = server.address();
            node.log(RED._("udp.status.listener-at",{host:node.iface||address.address,port:address.port}));

        });

        node.on("close", function() {
            try {
                if (node.multicast == "true") { server.dropMembership(node.group); }
                server.close();
                node.log(RED._("udp.status.listener-stopped"));
            } catch (err) {
                //node.error(err);
            }
            if (udpInputPortsInUse.hasOwnProperty(node.port)) {
                delete udpInputPortsInUse[node.port];
            }
            node.status({});
        });

    }
    RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-ports.read'), function(req,res) {
        res.json(Object.keys(udpInputPortsInUse));
    });
    RED.nodes.registerType("udp in",UDPin);

    // The Output Node
    function UDPout(n) {
        RED.nodes.createNode(this,n);
        //this.group = n.group;
        this.port = n.port;
        this.outport = n.outport||"";
        this.base64 = n.base64;
        this.addr = n.addr;
        this.iface = n.iface || null;
        this.multicast = n.multicast;
        this.ipv = n.ipv || "udp4";
        var node = this;

        if (node.iface && node.iface.indexOf(".") === -1) {
            try {
                if ((os.networkInterfaces())[node.iface][0].hasOwnProperty("scopeid")) {
                    if (node.ipv === "udp4") {
                        node.iface = (os.networkInterfaces())[node.iface][1].address;
                    } else {
                        node.iface = (os.networkInterfaces())[node.iface][0].address;
                    }
                }
                else {
                    if (node.ipv === "udp4") {
                        node.iface = (os.networkInterfaces())[node.iface][0].address;
                    } else {
                        node.iface = (os.networkInterfaces())[node.iface][1].address;
                    }
                }
            }
            catch(e) {
                node.warn(RED._("udp.errors.ifnotfound",{iface:node.iface}));
                node.iface = null;
            }
        }

        var opts = {type:node.ipv, reuseAddr:true};

        var sock;
        var p = this.outport || this.port || "0";
        node.tout = setTimeout(function() {
            if (udpInputPortsInUse[p]) {
                sock = udpInputPortsInUse[p];
                node.log(RED._("udp.status.re-use",{outport:node.outport,host:node.addr,port:node.port}));
                if (node.iface) { node.status({text:n.iface+" : "+node.iface}); }
            }
            else {
                sock = dgram.createSocket(opts);  // default to udp4
                if (node.multicast != "false") {
                    sock.bind(node.outport, function() {    // have to bind before you can enable broadcast...
                        sock.setBroadcast(true);            // turn on broadcast
                        sock.setMulticastLoopback(false);   // turn off loopback
                        if (node.multicast == "multi") {
                            try {
                                sock.setMulticastTTL(128);
                                sock.addMembership(node.addr,node.iface);   // Add to the multicast group
                                if (node.iface) { node.status({text:n.iface+" : "+node.iface}); }
                                node.log(RED._("udp.status.mc-ready",{iface:node.iface,outport:node.outport,host:node.addr,port:node.port}));
                            } catch (e) {
                                if (e.errno == "EINVAL") {
                                    node.error(RED._("udp.errors.bad-mcaddress"));
                                } else if (e.errno == "ENODEV") {
                                    node.error(RED._("udp.errors.interface"));
                                } else {
                                    node.error(RED._("udp.errors.error",{error:e.errno}));
                                }
                            }
                        } else {
                            node.log(RED._("udp.status.bc-ready",{outport:node.outport,host:node.addr,port:node.port}));
                        }
                    });
                } else if ((node.outport !== "") && (!udpInputPortsInUse[node.outport])) {
                    sock.bind(node.outport);
                    node.log(RED._("udp.status.ready",{outport:node.outport,host:node.addr,port:node.port}));
                } else {
                    node.log(RED._("udp.status.ready-nolocal",{host:node.addr,port:node.port}));
                }
                sock.on("error", function(err) {
                    // Any async error will also get reported in the sock.send call.
                    // This handler is needed to ensure the error marked as handled to
                    // prevent it going to the global error handler and shutting node-red
                    // down.
                });
                udpInputPortsInUse[p] = sock;
            }

            node.on("input", function(msg) {
                if (msg.hasOwnProperty("payload")) {
                    var add = node.addr || msg.ip || "";
                    var por = node.port || msg.port || 0;
                    if (add === "") {
                        node.warn(RED._("udp.errors.ip-notset"));
                    } else if (por === 0) {
                        node.warn(RED._("udp.errors.port-notset"));
                    } else if (isNaN(por) || (por < 1) || (por > 65535)) {
                        node.warn(RED._("udp.errors.port-invalid"));
                    } else {
                        var message;
                        if (node.base64) {
                            message = Buffer.from(msg.payload, 'base64');
                        } else if (msg.payload instanceof Buffer) {
                            message = msg.payload;
                        } else {
                            message = Buffer.from(""+msg.payload);
                        }
                        sock.send(message, 0, message.length, por, add, function(err, bytes) {
                            if (err) {
                                node.error("udp : "+err,msg);
                            }
                            message = null;
                        });
                    }
                }
            });
        }, 150);

        node.on("close", function() {
            if (node.tout) { clearTimeout(node.tout); }
            try {
                if (node.multicast == "multi") { sock.dropMembership(node.group); }
                sock.close();
                node.log(RED._("udp.status.output-stopped"));
            } catch (err) {
                //node.error(err);
            }
            if (udpInputPortsInUse.hasOwnProperty(p)) {
                delete udpInputPortsInUse[p];
            }
            node.status({});
        });
    }
    RED.nodes.registerType("udp out",UDPout);
}
<commit_msg>reduce udp out timeout to be less than default inject at start<commit_after>/**
 * Copyright JS Foundation and other contributors, http://js.foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

module.exports = function(RED) {
    "use strict";
    var os = require('os');
    var dgram = require('dgram');
    var udpInputPortsInUse = {};

    // The Input Node
    function UDPin(n) {
        RED.nodes.createNode(this,n);
        this.group = n.group;
        this.port = n.port;
        this.datatype = n.datatype;
        this.iface = n.iface || null;
        this.multicast = n.multicast;
        this.ipv = n.ipv || "udp4";
        var node = this;

        if (node.iface && node.iface.indexOf(".") === -1) {
            try {
                if ((os.networkInterfaces())[node.iface][0].hasOwnProperty("scopeid")) {
                    if (node.ipv === "udp4") {
                        node.iface = (os.networkInterfaces())[node.iface][1].address;
                    } else {
                        node.iface = (os.networkInterfaces())[node.iface][0].address;
                    }
                }
                else {
                    if (node.ipv === "udp4") {
                        node.iface = (os.networkInterfaces())[node.iface][0].address;
                    } else {
                        node.iface = (os.networkInterfaces())[node.iface][1].address;
                    }
                }
            }
            catch(e) {
                node.warn(RED._("udp.errors.ifnotfound",{iface:node.iface}));
                node.iface = null;
            }
        }

        var opts = {type:node.ipv, reuseAddr:true};
        if (process.version.indexOf("v0.10") === 0) { opts = node.ipv; }
        var server;

        if (!udpInputPortsInUse.hasOwnProperty(node.port)) {
            server = dgram.createSocket(opts);  // default to udp4
            server.bind(node.port, function() {
                if (node.multicast == "true") {
                    server.setBroadcast(true);
                    server.setMulticastLoopback(false);
                    try {
                        server.setMulticastTTL(128);
                        server.addMembership(node.group,node.iface);
                        if (node.iface) { node.status({text:n.iface+" : "+node.iface}); }
                        node.log(RED._("udp.status.mc-group",{group:node.group}));
                    } catch (e) {
                        if (e.errno == "EINVAL") {
                            node.error(RED._("udp.errors.bad-mcaddress"));
                        } else if (e.errno == "ENODEV") {
                            node.error(RED._("udp.errors.interface"));
                        } else {
                            node.error(RED._("udp.errors.error",{error:e.errno}));
                        }
                    }
                }
            });
            udpInputPortsInUse[node.port] = server;
        }
        else {
            node.log(RED._("udp.errors.alreadyused",{port:node.port}));
            server = udpInputPortsInUse[node.port];  // re-use existing
            if (node.iface) { node.status({text:n.iface+" : "+node.iface}); }
        }

        server.on("error", function (err) {
            if ((err.code == "EACCES") && (node.port < 1024)) {
                node.error(RED._("udp.errors.access-error"));
            } else {
                node.error(RED._("udp.errors.error",{error:err.code}));
            }
            server.close();
        });

        server.on('message', function (message, remote) {
            var msg;
            if (node.datatype =="base64") {
                msg = { payload:message.toString('base64'), fromip:remote.address+':'+remote.port, ip:remote.address, port:remote.port };
            } else if (node.datatype =="utf8") {
                msg = { payload:message.toString('utf8'), fromip:remote.address+':'+remote.port, ip:remote.address, port:remote.port };
            } else {
                msg = { payload:message, fromip:remote.address+':'+remote.port, ip:remote.address, port:remote.port };
            }
            node.send(msg);
        });

        server.on('listening', function () {
            var address = server.address();
            node.log(RED._("udp.status.listener-at",{host:node.iface||address.address,port:address.port}));

        });

        node.on("close", function() {
            try {
                if (node.multicast == "true") { server.dropMembership(node.group); }
                server.close();
                node.log(RED._("udp.status.listener-stopped"));
            } catch (err) {
                //node.error(err);
            }
            if (udpInputPortsInUse.hasOwnProperty(node.port)) {
                delete udpInputPortsInUse[node.port];
            }
            node.status({});
        });

    }
    RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-ports.read'), function(req,res) {
        res.json(Object.keys(udpInputPortsInUse));
    });
    RED.nodes.registerType("udp in",UDPin);

    // The Output Node
    function UDPout(n) {
        RED.nodes.createNode(this,n);
        //this.group = n.group;
        this.port = n.port;
        this.outport = n.outport||"";
        this.base64 = n.base64;
        this.addr = n.addr;
        this.iface = n.iface || null;
        this.multicast = n.multicast;
        this.ipv = n.ipv || "udp4";
        var node = this;

        if (node.iface && node.iface.indexOf(".") === -1) {
            try {
                if ((os.networkInterfaces())[node.iface][0].hasOwnProperty("scopeid")) {
                    if (node.ipv === "udp4") {
                        node.iface = (os.networkInterfaces())[node.iface][1].address;
                    } else {
                        node.iface = (os.networkInterfaces())[node.iface][0].address;
                    }
                }
                else {
                    if (node.ipv === "udp4") {
                        node.iface = (os.networkInterfaces())[node.iface][0].address;
                    } else {
                        node.iface = (os.networkInterfaces())[node.iface][1].address;
                    }
                }
            }
            catch(e) {
                node.warn(RED._("udp.errors.ifnotfound",{iface:node.iface}));
                node.iface = null;
            }
        }

        var opts = {type:node.ipv, reuseAddr:true};

        var sock;
        var p = this.outport || this.port || "0";
        node.tout = setTimeout(function() {
            if (udpInputPortsInUse[p]) {
                sock = udpInputPortsInUse[p];
                node.log(RED._("udp.status.re-use",{outport:node.outport,host:node.addr,port:node.port}));
                if (node.iface) { node.status({text:n.iface+" : "+node.iface}); }
            }
            else {
                sock = dgram.createSocket(opts);  // default to udp4
                if (node.multicast != "false") {
                    sock.bind(node.outport, function() {    // have to bind before you can enable broadcast...
                        sock.setBroadcast(true);            // turn on broadcast
                        sock.setMulticastLoopback(false);   // turn off loopback
                        if (node.multicast == "multi") {
                            try {
                                sock.setMulticastTTL(128);
                                sock.addMembership(node.addr,node.iface);   // Add to the multicast group
                                if (node.iface) { node.status({text:n.iface+" : "+node.iface}); }
                                node.log(RED._("udp.status.mc-ready",{iface:node.iface,outport:node.outport,host:node.addr,port:node.port}));
                            } catch (e) {
                                if (e.errno == "EINVAL") {
                                    node.error(RED._("udp.errors.bad-mcaddress"));
                                } else if (e.errno == "ENODEV") {
                                    node.error(RED._("udp.errors.interface"));
                                } else {
                                    node.error(RED._("udp.errors.error",{error:e.errno}));
                                }
                            }
                        } else {
                            node.log(RED._("udp.status.bc-ready",{outport:node.outport,host:node.addr,port:node.port}));
                        }
                    });
                } else if ((node.outport !== "") && (!udpInputPortsInUse[node.outport])) {
                    sock.bind(node.outport);
                    node.log(RED._("udp.status.ready",{outport:node.outport,host:node.addr,port:node.port}));
                } else {
                    node.log(RED._("udp.status.ready-nolocal",{host:node.addr,port:node.port}));
                }
                sock.on("error", function(err) {
                    // Any async error will also get reported in the sock.send call.
                    // This handler is needed to ensure the error marked as handled to
                    // prevent it going to the global error handler and shutting node-red
                    // down.
                });
                udpInputPortsInUse[p] = sock;
            }

            node.on("input", function(msg) {
                if (msg.hasOwnProperty("payload")) {
                    var add = node.addr || msg.ip || "";
                    var por = node.port || msg.port || 0;
                    if (add === "") {
                        node.warn(RED._("udp.errors.ip-notset"));
                    } else if (por === 0) {
                        node.warn(RED._("udp.errors.port-notset"));
                    } else if (isNaN(por) || (por < 1) || (por > 65535)) {
                        node.warn(RED._("udp.errors.port-invalid"));
                    } else {
                        var message;
                        if (node.base64) {
                            message = Buffer.from(msg.payload, 'base64');
                        } else if (msg.payload instanceof Buffer) {
                            message = msg.payload;
                        } else {
                            message = Buffer.from(""+msg.payload);
                        }
                        sock.send(message, 0, message.length, por, add, function(err, bytes) {
                            if (err) {
                                node.error("udp : "+err,msg);
                            }
                            message = null;
                        });
                    }
                }
            });
        }, 75);

        node.on("close", function() {
            if (node.tout) { clearTimeout(node.tout); }
            try {
                if (node.multicast == "multi") { sock.dropMembership(node.group); }
                sock.close();
                node.log(RED._("udp.status.output-stopped"));
            } catch (err) {
                //node.error(err);
            }
            if (udpInputPortsInUse.hasOwnProperty(p)) {
                delete udpInputPortsInUse[p];
            }
            node.status({});
        });
    }
    RED.nodes.registerType("udp out",UDPout);
}
<|endoftext|>


<commit_before>import React from 'react'
import {
  BrowserRouter as Router,
  Route,
  Link
} from 'react-router-dom'

const BasicExample = () => (
  <Router>

<Link to="/">Home</Link>

<Link to="/about">About</Link>

<Link to="/topics">Topics</Link>

      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/topics" component={Topics}/>

  </Router>
)

const Home = () => (

Home

)

const About = () => (

About

)

const Topics = ({ match }) => (

Topics

        <Link to={`${match.url}/rendering`}>
          Rendering with React
        </Link>

        <Link to={`${match.url}/components`}>
          Components
        </Link>

        <Link to={`${match.url}/props-v-state`}>
          Props v. State
        </Link>

    <Route path={`${match.url}/:topicId`} component={Topic}/>
    <Route exact path={match.url} render={() => (

Please select a topic.

    )}/>

)

const Topic = ({ match }) => (

{match.params.topicId}

)

export default BasicExample
<commit_msg>翻译代码basic<commit_after>import React from 'react'
import {
  BrowserRouter as Router,
  Route,
  Link
} from 'react-router-dom'

const BasicExample = () => (
  <Router>

<Link to="/">首页</Link>

<Link to="/about">关于</Link>

<Link to="/topics">主题列表</Link>

      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/topics" component={Topics}/>

  </Router>
)

const Home = () => (

首页

)

const About = () => (

关于

)

const Topics = ({ match }) => (

主题列表

        <Link to={`${match.url}/rendering`}>
          使用 React 渲染
        </Link>

        <Link to={`${match.url}/components`}>
          组件
        </Link>

        <Link to={`${match.url}/props-v-state`}>
          属性 v. 状态
        </Link>

    <Route path={`${match.url}/:topicId`} component={Topic}/>
    <Route exact path={match.url} render={() => (

请选择一个主题。

    )}/>

)

const Topic = ({ match }) => (

{match.params.topicId}

)

export default BasicExample
<|endoftext|>


<commit_before>/*globals OAuth*/

const logger = new Logger('CustomOAuth');

const Services = {};
const BeforeUpdateOrCreateUserFromExternalService = [];

export class CustomOAuth {
	constructor(name, options) {
		logger.debug('Init CustomOAuth', name, options);

		this.name = name;
		if (!Match.test(this.name, String)) {
			throw new Meteor.Error('CustomOAuth: Name is required and must be String');
		}

		if (Services[this.name]) {
			Services[this.name].configure(options);
			return;
		}

		Services[this.name] = this;

		this.configure(options);

		this.userAgent = 'Meteor';
		if (Meteor.release) {
			this.userAgent += '/' + Meteor.release;
		}

		Accounts.oauth.registerService(this.name);
		this.registerService();
		this.addHookToProcessUser();
	}

	configure(options) {
		if (!Match.test(options, Object)) {
			throw new Meteor.Error('CustomOAuth: Options is required and must be Object');
		}

		if (!Match.test(options.serverURL, String)) {
			throw new Meteor.Error('CustomOAuth: Options.serverURL is required and must be String');
		}

		if (!Match.test(options.tokenPath, String)) {
			options.tokenPath = '/oauth/token';
		}

		if (!Match.test(options.identityPath, String)) {
			options.identityPath = '/me';
		}

		this.serverURL = options.serverURL;
		this.tokenPath = options.tokenPath;
		this.identityPath = options.identityPath;
		this.tokenSentVia = options.tokenSentVia;
		this.usernameField = (options.usernameField || '').trim();
		this.mergeUsers = options.mergeUsers;

		if (!/^https?:\/\/.+/.test(this.tokenPath)) {
			this.tokenPath = this.serverURL + this.tokenPath;
		}

		if (!/^https?:\/\/.+/.test(this.identityPath)) {
			this.identityPath = this.serverURL + this.identityPath;
		}

		if (Match.test(options.addAutopublishFields, Object)) {
			Accounts.addAutopublishFields(options.addAutopublishFields);
		}
	}

	getAccessToken(query) {
		const config = ServiceConfiguration.configurations.findOne({service: this.name});
		if (!config) {
			throw new ServiceConfiguration.ConfigError();
		}

		let response = undefined;
		try {
			response = HTTP.post(this.tokenPath, {
				auth: config.clientId + ':' + OAuth.openSecret(config.secret),
				headers: {
					Accept: 'application/json',
					'User-Agent': this.userAgent
				},
				params: {
					code: query.code,
					client_id: config.clientId,
					client_secret: OAuth.openSecret(config.secret),
					redirect_uri: OAuth._redirectUri(this.name, config),
					grant_type: 'authorization_code',
					state: query.state
				}
			});
		} catch (err) {
			const error = new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${err.message}`);
			throw _.extend(error, {response: err.response});
		}

		if (response.data.error) { //if the http response was a json object with an error attribute
			throw new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${response.data.error}`);
		} else {
			return response.data.access_token;
		}
	}

	getIdentity(accessToken) {
		const params = {};
		const headers = {
			'User-Agent': this.userAgent // http://doc.gitlab.com/ce/api/users.html#Current-user
		};

		if (this.tokenSentVia === 'header') {
			headers['Authorization'] = 'Bearer ' + accessToken;
		} else {
			params['access_token'] = accessToken;
		}

		try {
			const response = HTTP.get(this.identityPath, {
				headers: headers,
				params: params
			});

			let data;

			if (response.data) {
				data = response.data;
			} else {
				data = JSON.parse(response.content);
			}

			logger.debug('Identity response', JSON.stringify(data, null, 2));

			return data;
		} catch (err) {
			const error = new Error(`Failed to fetch identity from ${this.name} at ${this.identityPath}. ${err.message}`);
			throw _.extend(error, {response: err.response});
		}
	}

	registerService() {
		const self = this;
		OAuth.registerService(this.name, 2, null, (query) => {
			const accessToken = self.getAccessToken(query);
			// console.log 'at:', accessToken

			let identity = self.getIdentity(accessToken);

			if (identity) {
				// Fix for Reddit
				if (identity.result) {
					identity = identity.result;
				}

				// Fix WordPress-like identities having 'ID' instead of 'id'
				if (identity.ID && !identity.id) {
					identity.id = identity.ID;
				}

				// Fix Auth0-like identities having 'user_id' instead of 'id'
				if (identity.user_id && !identity.id) {
					identity.id = identity.user_id;
				}

				if (identity.CharacterID && !identity.id) {
					identity.id = identity.CharacterID;
				}

				// Fix Dataporten having 'user.userid' instead of 'id'
				if (identity.user && identity.user.userid && !identity.id) {
					identity.id = identity.user.userid;
					identity.email = identity.user.email;
				}

				// Fix general 'phid' instead of 'id' from phabricator
				if (identity.phid && !identity.id) {
					identity.id = identity.phid;
				}

				// Fix Keycloak-like identities having 'sub' instead of 'id'
				if (identity.sub && !identity.id) {
					identity.id = identity.sub;
				}

				// Fix general 'userid' instead of 'id' from provider
				if (identity.userid && !identity.id) {
					identity.id = identity.userid;
				}
			}

			// console.log 'id:', JSON.stringify identity, null, '  '

			const serviceData = {
				_OAuthCustom: true,
				accessToken: accessToken
			};

			_.extend(serviceData, identity);

			const data = {
				serviceData: serviceData,
				options: {
					profile: {
						name: identity.name || identity.username || identity.nickname || identity.CharacterName || identity.userName || identity.preferred_username || (identity.user && identity.user.name)
					}
				}
			};

			// console.log data

			return data;
		});
	}

	retrieveCredential(credentialToken, credentialSecret) {
		return OAuth.retrieveCredential(credentialToken, credentialSecret);
	}

	addHookToProcessUser() {
		BeforeUpdateOrCreateUserFromExternalService.push((serviceName, serviceData/*, options*/) => {
			if (serviceName !== this.name) {
				return;
			}

			if (this.usernameField) {
				let username = '';

				if (this.usernameField.indexOf('#{') > -1) {
					username = this.usernameField.replace(/#{(.+?)}/g, function(match, field) {
						if (!serviceData[field]) {
							throw new Meteor.Error(`Username template item "${field}" not found in data`, serviceData);
						}
						return serviceData[field];
					});
				} else {
					username = serviceData[this.usernameField];
					if (!username) {
						return logger.error(`Username field "${this.usernameField}" not found in data`, serviceData);
					}
				}

				const user = RocketChat.models.Users.findOneByUsername(username);
				if (!user) {
					return;
				}

				if (this.mergeUsers !== true) {
					throw new Meteor.Error('CustomOAuth', `User with username ${user.username} already exists`);
				}

				const serviceIdKey = `services.${serviceName}.id`;
				const update = {
					$set: {
						[serviceIdKey]: serviceData.id
					}
				};

				RocketChat.models.Users.update({_id: user._id}, update);
			}
		});
	}
}

const updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService;
Accounts.updateOrCreateUserFromExternalService = function(/*serviceName, serviceData, options*/) {
	for (const hook of BeforeUpdateOrCreateUserFromExternalService) {
		hook.apply(this, arguments);
	}

	return updateOrCreateUserFromExternalService.apply(this, arguments);
};
<commit_msg>Set username automaticaly<commit_after>/*globals OAuth*/

const logger = new Logger('CustomOAuth');

const Services = {};
const BeforeUpdateOrCreateUserFromExternalService = [];

export class CustomOAuth {
	constructor(name, options) {
		logger.debug('Init CustomOAuth', name, options);

		this.name = name;
		if (!Match.test(this.name, String)) {
			throw new Meteor.Error('CustomOAuth: Name is required and must be String');
		}

		if (Services[this.name]) {
			Services[this.name].configure(options);
			return;
		}

		Services[this.name] = this;

		this.configure(options);

		this.userAgent = 'Meteor';
		if (Meteor.release) {
			this.userAgent += '/' + Meteor.release;
		}

		Accounts.oauth.registerService(this.name);
		this.registerService();
		this.addHookToProcessUser();
	}

	configure(options) {
		if (!Match.test(options, Object)) {
			throw new Meteor.Error('CustomOAuth: Options is required and must be Object');
		}

		if (!Match.test(options.serverURL, String)) {
			throw new Meteor.Error('CustomOAuth: Options.serverURL is required and must be String');
		}

		if (!Match.test(options.tokenPath, String)) {
			options.tokenPath = '/oauth/token';
		}

		if (!Match.test(options.identityPath, String)) {
			options.identityPath = '/me';
		}

		this.serverURL = options.serverURL;
		this.tokenPath = options.tokenPath;
		this.identityPath = options.identityPath;
		this.tokenSentVia = options.tokenSentVia;
		this.usernameField = (options.usernameField || '').trim();
		this.mergeUsers = options.mergeUsers;

		if (!/^https?:\/\/.+/.test(this.tokenPath)) {
			this.tokenPath = this.serverURL + this.tokenPath;
		}

		if (!/^https?:\/\/.+/.test(this.identityPath)) {
			this.identityPath = this.serverURL + this.identityPath;
		}

		if (Match.test(options.addAutopublishFields, Object)) {
			Accounts.addAutopublishFields(options.addAutopublishFields);
		}
	}

	getAccessToken(query) {
		const config = ServiceConfiguration.configurations.findOne({service: this.name});
		if (!config) {
			throw new ServiceConfiguration.ConfigError();
		}

		let response = undefined;
		try {
			response = HTTP.post(this.tokenPath, {
				auth: config.clientId + ':' + OAuth.openSecret(config.secret),
				headers: {
					Accept: 'application/json',
					'User-Agent': this.userAgent
				},
				params: {
					code: query.code,
					client_id: config.clientId,
					client_secret: OAuth.openSecret(config.secret),
					redirect_uri: OAuth._redirectUri(this.name, config),
					grant_type: 'authorization_code',
					state: query.state
				}
			});
		} catch (err) {
			const error = new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${err.message}`);
			throw _.extend(error, {response: err.response});
		}

		if (response.data.error) { //if the http response was a json object with an error attribute
			throw new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${response.data.error}`);
		} else {
			return response.data.access_token;
		}
	}

	getIdentity(accessToken) {
		const params = {};
		const headers = {
			'User-Agent': this.userAgent // http://doc.gitlab.com/ce/api/users.html#Current-user
		};

		if (this.tokenSentVia === 'header') {
			headers['Authorization'] = 'Bearer ' + accessToken;
		} else {
			params['access_token'] = accessToken;
		}

		try {
			const response = HTTP.get(this.identityPath, {
				headers: headers,
				params: params
			});

			let data;

			if (response.data) {
				data = response.data;
			} else {
				data = JSON.parse(response.content);
			}

			logger.debug('Identity response', JSON.stringify(data, null, 2));

			return data;
		} catch (err) {
			const error = new Error(`Failed to fetch identity from ${this.name} at ${this.identityPath}. ${err.message}`);
			throw _.extend(error, {response: err.response});
		}
	}

	registerService() {
		const self = this;
		OAuth.registerService(this.name, 2, null, (query) => {
			const accessToken = self.getAccessToken(query);
			// console.log 'at:', accessToken

			let identity = self.getIdentity(accessToken);

			if (identity) {
				// Fix for Reddit
				if (identity.result) {
					identity = identity.result;
				}

				// Fix WordPress-like identities having 'ID' instead of 'id'
				if (identity.ID && !identity.id) {
					identity.id = identity.ID;
				}

				// Fix Auth0-like identities having 'user_id' instead of 'id'
				if (identity.user_id && !identity.id) {
					identity.id = identity.user_id;
				}

				if (identity.CharacterID && !identity.id) {
					identity.id = identity.CharacterID;
				}

				// Fix Dataporten having 'user.userid' instead of 'id'
				if (identity.user && identity.user.userid && !identity.id) {
					identity.id = identity.user.userid;
					identity.email = identity.user.email;
				}

				// Fix general 'phid' instead of 'id' from phabricator
				if (identity.phid && !identity.id) {
					identity.id = identity.phid;
				}

				// Fix Keycloak-like identities having 'sub' instead of 'id'
				if (identity.sub && !identity.id) {
					identity.id = identity.sub;
				}

				// Fix general 'userid' instead of 'id' from provider
				if (identity.userid && !identity.id) {
					identity.id = identity.userid;
				}
			}

			// console.log 'id:', JSON.stringify identity, null, '  '

			const serviceData = {
				_OAuthCustom: true,
				accessToken: accessToken
			};

			_.extend(serviceData, identity);

			const data = {
				serviceData: serviceData,
				options: {
					profile: {
						name: identity.name || identity.username || identity.nickname || identity.CharacterName || identity.userName || identity.preferred_username || (identity.user && identity.user.name)
					}
				}
			};

			// console.log data

			return data;
		});
	}

	retrieveCredential(credentialToken, credentialSecret) {
		return OAuth.retrieveCredential(credentialToken, credentialSecret);
	}

	getUsername(data) {
		let username = '';

		if (this.usernameField.indexOf('#{') > -1) {
			username = this.usernameField.replace(/#{(.+?)}/g, function(match, field) {
				if (!data[field]) {
					throw new Meteor.Error(`Username template item "${field}" not found in data`, data);
				}
				return data[field];
			});
		} else {
			username = data[this.usernameField];
			if (!username) {
				throw new Meteor.Error(`Username field "${this.usernameField}" not found in data`, data);
			}
		}

		return username;
	}

	addHookToProcessUser() {
		BeforeUpdateOrCreateUserFromExternalService.push((serviceName, serviceData/*, options*/) => {
			if (serviceName !== this.name) {
				return;
			}

			if (this.usernameField) {
				const username = this.getUsername(serviceData);

				const user = RocketChat.models.Users.findOneByUsername(username);
				if (!user) {
					return;
				}

				if (this.mergeUsers !== true) {
					throw new Meteor.Error('CustomOAuth', `User with username ${user.username} already exists`);
				}

				const serviceIdKey = `services.${serviceName}.id`;
				const update = {
					$set: {
						[serviceIdKey]: serviceData.id
					}
				};

				RocketChat.models.Users.update({_id: user._id}, update);
			}
		});

		Accounts.validateNewUser((user) => {
			if (!user.services || !user.services[this.name] || !user.services[this.name].id) {
				return true;
			}

			if (this.usernameField) {
				user.username = this.getUsername(user.services[this.name]);
			}

			return true;
		});

	}
}

const updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService;
Accounts.updateOrCreateUserFromExternalService = function(/*serviceName, serviceData, options*/) {
	for (const hook of BeforeUpdateOrCreateUserFromExternalService) {
		hook.apply(this, arguments);
	}

	return updateOrCreateUserFromExternalService.apply(this, arguments);
};
<|endoftext|>


<commit_before>RocketChat.sendMessage = function(user, message, room, upsert = false) {
	if (!user || !message || !room._id) {
		return false;
	}
	if (message.ts == null) {
		message.ts = new Date();
	}
	message.u = _.pick(user, ['_id', 'username', 'name']);
	if (!Match.test(message.msg, String)) {
		message.msg = '';
	}
	message.rid = room._id;
	if (!room.usernames || room.usernames.length === 0) {
		const updated_room = RocketChat.models.Rooms.findOneById(room._id);
		if (updated_room != null) {
			room = updated_room;
		} else {
			room.usernames = [];
		}
	}
	if (message.parseUrls !== false) {
		const urls = message.msg.match(/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\(\)\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g);
		if (urls) {
			message.urls = urls.map(function(url) {
				return {
					url
				};
			});
		}
	}
	message = RocketChat.callbacks.run('beforeSaveMessage', message);
	// Avoid saving sandstormSessionId to the database
	let sandstormSessionId = null;
	if (message.sandstormSessionId) {
		sandstormSessionId = message.sandstormSessionId;
		delete message.sandstormSessionId;
	}
	if (message._id && upsert) {
		const _id = message._id;
		delete message._id;
		RocketChat.models.Messages.upsert({
			_id,
			'u._id': message.u._id
		}, message);
		message._id = _id;
	} else {
		message._id = RocketChat.models.Messages.insert(message);
	}

	/*
	Defer other updates as their return is not interesting to the user
	*/
	Meteor.defer(() => {
		// Execute all callbacks
		message.sandstormSessionId = sandstormSessionId;
		return RocketChat.callbacks.run('afterSaveMessage', message, room);
	});
	return message;
};
<commit_msg>Force travis build<commit_after>RocketChat.sendMessage = function(user, message, room, upsert = false) {
	if (!user || !message || !room._id) {
		return false;
	}
	if (message.ts == null) {
		message.ts = new Date();
	}
	message.u = _.pick(user, ['_id', 'username', 'name']);
	if (!Match.test(message.msg, String)) {
		message.msg = '';
	}
	message.rid = room._id;
	if (!room.usernames || room.usernames.length === 0) {
		const updated_room = RocketChat.models.Rooms.findOneById(room._id);
		if (updated_room != null) {
			room = updated_room;
		} else {
			room.usernames = [];
		}
	}
	if (message.parseUrls !== false) {
		const urls = message.msg.match(/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\(\)\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g);

		if (urls) {
			message.urls = urls.map(function(url) {
				return {
					url
				};
			});
		}
	}
	message = RocketChat.callbacks.run('beforeSaveMessage', message);
	// Avoid saving sandstormSessionId to the database
	let sandstormSessionId = null;
	if (message.sandstormSessionId) {
		sandstormSessionId = message.sandstormSessionId;
		delete message.sandstormSessionId;
	}
	if (message._id && upsert) {
		const _id = message._id;
		delete message._id;
		RocketChat.models.Messages.upsert({
			_id,
			'u._id': message.u._id
		}, message);
		message._id = _id;
	} else {
		message._id = RocketChat.models.Messages.insert(message);
	}

	/*
	Defer other updates as their return is not interesting to the user
	*/
	Meteor.defer(() => {
		// Execute all callbacks
		message.sandstormSessionId = sandstormSessionId;
		return RocketChat.callbacks.run('afterSaveMessage', message, room);
	});
	return message;
};
<|endoftext|>


<commit_before>// ==========================================================================
// Project:   SproutCore Handlebar Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License:   Licensed under MIT license (see license.js)
// ==========================================================================

require("sproutcore-views/views/view");
require("sproutcore-handlebars/ext");

var set = SC.set, get = SC.get;

// TODO: Be explicit in the class documentation that you
// *MUST* set the value of a checkbox through SproutCore.
// Updating the value of a checkbox directly via jQuery objects
// will not work.

SC.Checkbox = SC.View.extend({
  title: null,
  value: false,

  classNames: ['sc-checkbox'],

  defaultTemplate: SC.Handlebars.compile('<input type="checkbox" {{bindAttr checked="value"}}>{{title}}'),

  change: function() {
    SC.run.once(this, this._updateElementValue);
    return false;
  },

  _updateElementValue: function() {
    var input = this.$('input');
    set(this, 'value', input.prop('checked'));
  }
});

<commit_msg>Switch back to using input:checkbox.<commit_after>// ==========================================================================
// Project:   SproutCore Handlebar Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License:   Licensed under MIT license (see license.js)
// ==========================================================================

require("sproutcore-views/views/view");
require("sproutcore-handlebars/ext");

var set = SC.set, get = SC.get;

// TODO: Be explicit in the class documentation that you
// *MUST* set the value of a checkbox through SproutCore.
// Updating the value of a checkbox directly via jQuery objects
// will not work.

SC.Checkbox = SC.View.extend({
  title: null,
  value: false,

  classNames: ['sc-checkbox'],

  defaultTemplate: SC.Handlebars.compile('<input type="checkbox" {{bindAttr checked="value"}}>{{title}}'),

  change: function() {
    SC.run.once(this, this._updateElementValue);
    return false;
  },

  _updateElementValue: function() {
    var input = this.$('input:checkbox');
    set(this, 'value', input.prop('checked'));
  }
});

<|endoftext|>


<commit_before>import PropTypes from 'prop-types'
import React from 'react'
import {Icon} from 'tocco-ui'

const PropsTable = props => {
  const rows = []

  for (const propName in props.props) {
    if (props.props.hasOwnProperty(propName)) {
      const prop = props.props[propName]
      rows.push(

          {propName}
          {prop.description}
          {prop.required && <Icon icon="exclamation"/>}
          {prop.type.name}

      )
    }
  }

  return (

Properties

            Name
            Description
            Required
            Type

          {rows}

  )
}

PropsTable.propTypes = {
  props: PropTypes.object.isRequired
}

export default PropsTable
<commit_msg>refactor(tocco-ui-showcase): make props optional<commit_after>import PropTypes from 'prop-types'
import React from 'react'
import {Icon} from 'tocco-ui'

const PropsTable = props => {
  const rows = []

  for (const propName in props.props) {
    if (props.props.hasOwnProperty(propName)) {
      const prop = props.props[propName]
      rows.push(

          {propName}
          {prop.description}
          {prop.required && <Icon icon="exclamation"/>}
          {prop.type.name}

      )
    }
  }

  return (

Properties

            Name
            Description
            Required
            Type

          {rows}

  )
}

PropsTable.propTypes = {
  props: PropTypes.object
}

export default PropsTable
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { MorphIcon, PlayButton as PlayIcon } from 'react-svg-buttons'
import styled, { withTheme } from 'styled-components'

const Container = styled.div`
    margin-right: 6vmin;
    display: flex;
    align-items: center;
    height: 100%;
`

const Button = styled.span`
    display: flex;
    align-items: center;
    cursor: pointer;
    height: 100%;
    padding: 0 0.6vmin;
`

const PlayButton = Button.extend`
    margin-left: 0.6vmin;
`

const Page = styled.span`
    display: inline-block;
    width: 2vmin;
    text-align: center;
`

const Slash = styled.span`
    font-size: ${props => props.theme.dashboard.player.slash.fontSize};
    color: ${props => props.theme.dashboard.player.slash.color};
    margin: ${props => props.theme.dashboard.player.slash.margin};
    ${props => props.theme.dashboard.player.slash.extend.trim()};
`

class DashboardPlayer extends Component {
    static propTypes = {
        currentDashboardIndex: PropTypes.number.isRequired,
        dashboards: PropTypes.array.isRequired,
        isPlaying: PropTypes.bool.isRequired,
        play: PropTypes.func.isRequired,
        pause: PropTypes.func.isRequired,
        previous: PropTypes.func.isRequired,
        next: PropTypes.func.isRequired,
        theme: PropTypes.object.isRequired,
    }

    render() {
        const {
            dashboards,
            currentDashboardIndex,
            isPlaying,
            play,
            pause,
            previous,
            next,
            theme,
        } = this.props

        const iconColor = theme.colors.icon

        let icon
        let handler
        if (isPlaying) {
            icon = 'plus'
            handler = pause
        } else {
            icon = 'plusSparks'
            handler = play
        }

        return (
            <Container>
                <Button onClick={previous}>
                    <MorphIcon type="arrowLeft" size={32} color={iconColor} />
                </Button>
                <Page>{currentDashboardIndex + 1}</Page>
                <Slash>/</Slash>
                <Page>{dashboards.length}</Page>
                <PlayButton onClick={handler}>
                    <PlayIcon type={icon} size={32} isPlaying={isPlaying} color={iconColor} />
                </PlayButton>
                <Button onClick={next}>
                    <MorphIcon type="arrowRight" size={32} color={iconColor} />
                </Button>
            </Container>
        )
    }
}

export default withTheme(DashboardPlayer)
<commit_msg>fix(ui): fix dashboard player style<commit_after>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { MorphIcon, PlayButton as PlayIcon } from 'react-svg-buttons'
import styled, { withTheme } from 'styled-components'
import typography from '../../theming/typography'

const Container = styled.div`
    margin-right: 6vmin;
    display: flex;
    align-items: center;
    height: 100%;
    ${props => typography(props.theme)}
`

const Button = styled.span`
    display: flex;
    align-items: center;
    cursor: pointer;
    height: 100%;
    padding: 0 0.6vmin;
`

const PlayButton = Button.extend`
    margin-left: 0.6vmin;
`

const Page = styled.span`
    display: inline-block;
    width: 2vmin;
    text-align: center;
`

const Slash = styled.span`
    font-size: ${props => props.theme.dashboard.player.slash.fontSize};
    color: ${props => props.theme.dashboard.player.slash.color};
    margin: ${props => props.theme.dashboard.player.slash.margin};
    ${props => props.theme.dashboard.player.slash.extend.trim()};
`

class DashboardPlayer extends Component {
    static propTypes = {
        currentDashboardIndex: PropTypes.number.isRequired,
        dashboards: PropTypes.array.isRequired,
        isPlaying: PropTypes.bool.isRequired,
        play: PropTypes.func.isRequired,
        pause: PropTypes.func.isRequired,
        previous: PropTypes.func.isRequired,
        next: PropTypes.func.isRequired,
        theme: PropTypes.object.isRequired,
    }

    render() {
        const {
            dashboards,
            currentDashboardIndex,
            isPlaying,
            play,
            pause,
            previous,
            next,
            theme,
        } = this.props

        const iconColor = theme.colors.icon

        let icon
        let handler
        if (isPlaying) {
            icon = 'plus'
            handler = pause
        } else {
            icon = 'plusSparks'
            handler = play
        }

        return (
            <Container>
                <Button onClick={previous}>
                    <MorphIcon type="arrowLeft" size={32} color={iconColor} />
                </Button>
                <Page>{currentDashboardIndex + 1}</Page>
                <Slash>/</Slash>
                <Page>{dashboards.length}</Page>
                <PlayButton onClick={handler}>
                    <PlayIcon type={icon} size={32} isPlaying={isPlaying} color={iconColor} />
                </PlayButton>
                <Button onClick={next}>
                    <MorphIcon type="arrowRight" size={32} color={iconColor} />
                </Button>
            </Container>
        )
    }
}

export default withTheme(DashboardPlayer)
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
	addComponent,
	removeComponent,
	watchComponent,
	updateQuery,
	setQueryOptions,
} from '@appbaseio/reactivecore/lib/actions';
import {
	isEqual,
	checkValueChange,
	checkPropChange,
	checkSomePropChange,
	getClassName,
	pushToAndClause,
} from '@appbaseio/reactivecore/lib/utils/helper';
import types from '@appbaseio/reactivecore/lib/utils/types';
import Rheostat from 'rheostat';

import HistogramContainer from './addons/HistogramContainer';
import RangeLabel from './addons/RangeLabel';
import Slider from '../../styles/Slider';
import Title from '../../styles/Title';
import { rangeLabelsContainer } from '../../styles/Label';

class DynamicRangeSlider extends Component {
	constructor(props) {
		super(props);

		this.state = {
			currentValue: null,
			range: null,
			stats: [],
		};
		this.internalHistogramComponent = `${this.props.componentId}__histogram__internal`;
		this.internalRangeComponent = `${this.props.componentId}__range__internal`;
		this.internalMatchAllComponent = `${this.props.componentId}__match_all__internal`;
		this.locked = false;
	}

	componentWillMount() {
		this.props.addComponent(this.props.componentId);
		this.props.addComponent(this.internalHistogramComponent);
		this.props.addComponent(this.internalRangeComponent);

		// get range before executing other queries
		this.updateRangeQueryOptions(this.props);
		this.setReact(this.props);
	}

	componentWillReceiveProps(nextProps) {
		if (!isEqual(this.props.range, nextProps.range) && nextProps.range) {
			this.updateQueryOptions(nextProps, nextProps.range);
			// floor and ceil to take edge cases into account
			this.updateRange({
				start: Math.floor(nextProps.range.start),
				end: Math.ceil(nextProps.range.end),
			});

			if (nextProps.selectedValue) {
				this.handleChange(nextProps.selectedValue);
			} else if (nextProps.defaultSelected) {
				this.handleChange([
					nextProps.defaultSelected.start,
					nextProps.defaultSelected.end,
				]);
			} else {
				this.handleChange([
					Math.floor(nextProps.range.start),
					Math.ceil(nextProps.range.end),
				]);
			}
		} else if (!isEqual(this.props.defaultSelected, nextProps.defaultSelected)) {
			this.handleChange(
				[nextProps.defaultSelected.start, nextProps.defaultSelected.end],
				nextProps,
			);
		} else if (!isEqual(this.state.currentValue, nextProps.selectedValue)) {
			this.handleChange(nextProps.selectedValue || [nextProps.range.start, nextProps.range.end]);
		}
		checkPropChange(this.props.react, nextProps.react, () => {
			this.updateRangeQueryOptions(nextProps);
			this.setReact(nextProps);
		});
		checkPropChange(this.props.dataField, nextProps.dataField, () => {
			this.updateRangeQueryOptions(nextProps);
		});
		checkSomePropChange(
			this.props,
			nextProps,
			['showHistogram', 'interval'],
			() => this.updateQueryOptions(nextProps, nextProps.range || this.state.range),
		);
		checkPropChange(this.props.options, nextProps.options, () => {
			const { options } = nextProps;
			options.sort((a, b) => {
				if (a.key < b.key) return -1;
				if (a.key > b.key) return 1;
				return 0;
			});
			this.setState({
				stats: options,
			});
		});
	}

	shouldComponentUpdate(nextProps, nextState) {
		if (nextState.range) {
			const upperLimit = Math.floor((nextState.range.end - nextState.range.start) / 2);
			if (nextProps.stepValue < 1 || nextProps.stepValue > upperLimit) {
				console.warn(`stepValue for DynamicRangeSlider ${nextProps.componentId} should be greater than 0 and less than or equal to ${upperLimit}`);
				return false;
			}
			return true;
		}
		return true;
	}

	componentWillUnmount() {
		this.props.removeComponent(this.props.componentId);
		this.props.removeComponent(this.internalHistogramComponent);
		this.props.removeComponent(this.internalRangeComponent);
		this.props.removeComponent(this.internalMatchAllComponent);
	}

	setReact = (props) => {
		const { react } = props;
		if (react) {
			props.watchComponent(this.internalRangeComponent, props.react);
			const newReact = pushToAndClause(
				react,
				this.internalHistogramComponent,
			);
			props.watchComponent(props.componentId, newReact);
		} else {
			// internalRangeComponent watches internalMatchAll component allowing execution of query
			// in case of no react prop
			this.props.addComponent(this.internalMatchAllComponent);
			props.setQueryOptions(this.internalMatchAllComponent, { aggs: { match_all: {} } }, false);
			props.watchComponent(this.internalRangeComponent, { and: this.internalMatchAllComponent });
			props.watchComponent(props.componentId, { and: this.internalHistogramComponent });
		}
	};

	defaultQuery = (value, props) => {
		if (Array.isArray(value) && value.length) {
			return {
				range: {
					[props.dataField]: {
						gte: value[0],
						lte: value[1],
						boost: 2.0,
					},
				},
			};
		}
		return null;
	};

	getSnapPoints = () => {
		let snapPoints = [];
		let { stepValue } = this.props;
		const { range } = this.state;

		// limit the number of steps to prevent generating a large number of snapPoints
		if ((range.end - range.start) / stepValue > 100) {
			stepValue = (range.end - range.start) / 100;
		}

		for (let i = range.start; i <= range.end; i += stepValue) {
			snapPoints = snapPoints.concat(i);
		}
		if (snapPoints[snapPoints.length - 1] !== range.end) {
			snapPoints = snapPoints.concat(range.end);
		}
		return snapPoints;
	};

	getValidInterval = (props, range) => {
		const min = Math.ceil((range.end - range.start) / 100) || 1;
		if (!props.interval) {
			return min;
		} else if (props.interval < min) {
			console.error(`${props.componentId}: interval prop's value should be greater than or equal to ${min}`);
			return min;
		}
		return props.interval;
	};

	histogramQuery = (props, range) => ({
		[props.dataField]: {
			histogram: {
				field: props.dataField,
				interval: this.getValidInterval(props, range),
				offset: range.start,
			},
		},
	});

	rangeQuery = props => ({
		min: { min: { field: props.dataField } },
		max: { max: { field: props.dataField } },
	});

	handleChange = (currentValue, props = this.props) => {
		// ignore state updates when component is locked
		if (props.beforeValueChange && this.locked) {
			return;
		}

		this.locked = true;
		const performUpdate = () => {
			this.setState({
				currentValue,
			}, () => {
				this.updateQuery([currentValue[0], currentValue[1]], props);
				this.locked = false;
			});
		};
		checkValueChange(
			props.componentId,
			{
				start: currentValue[0],
				end: currentValue[1],
			},
			props.beforeValueChange,
			props.onValueChange,
			performUpdate,
		);
	};

	handleSlider = ({ values }) => {
		this.handleChange(values);
	};

	updateQuery = (value, props) => {
		const query = props.customQuery || this.defaultQuery;
		let onQueryChange = null;
		if (props.onQueryChange) {
			onQueryChange = props.onQueryChange;
		}
		props.updateQuery({
			componentId: props.componentId,
			query: query(value, props),
			value,
			label: props.filterLabel,
			showFilter: false, // disable filters for DynamicRangeSlider
			URLParams: props.URLParams,
			onQueryChange,
		});
	};

	updateQueryOptions = (props, range) => {
		if (props.showHistogram) {
			const queryOptions = {
				aggs: this.histogramQuery(props, range),
			};

			props.setQueryOptions(this.internalHistogramComponent, queryOptions, false);

			const query = props.customQuery || this.defaultQuery;

			props.updateQuery({
				componentId: this.internalHistogramComponent,
				query: query([range.start, range.end], props),
			});
		}
	};

	updateRange = (range) => {
		this.setState({
			range,
		});
	}

	updateRangeQueryOptions = (props) => {
		const queryOptions = {
			aggs: this.rangeQuery(props),
		};

		props.setQueryOptions(this.internalRangeComponent, queryOptions);
	};

	render() {
		if (!this.state.currentValue || !this.state.range) {
			return null;
		}
		return (
			<Slider primary style={this.props.style} className={this.props.className}>
				{this.props.title && (
					<Title
						className={getClassName(this.props.innerClass, 'title') || null}
					>
						{this.props.title}
					</Title>
				)}
				{this.state.stats.length && this.props.showHistogram ? (
					<HistogramContainer
						stats={this.state.stats}
						range={this.state.range}
						interval={this.getValidInterval(this.props, this.state.range)}
					/>
				) : null}
				<Rheostat
					min={this.state.range.start}
					max={this.state.range.end}
					values={this.state.currentValue}
					onChange={this.handleSlider}
					snap={this.props.snap}
					snapPoints={this.props.snap ? this.getSnapPoints() : null}
					className={getClassName(this.props.innerClass, 'slider')}
				/>
				<div className={rangeLabelsContainer}>
					<RangeLabel
						align="left"
						className={getClassName(this.props.innerClass, 'label') || null}
					>
						{this.state.range.start}
					</RangeLabel>
					<RangeLabel
						align="right"
						className={getClassName(this.props.innerClass, 'label') || null}
					>
						{this.state.range.end}
					</RangeLabel>
				</div>
			</Slider>
		);
	}
}

DynamicRangeSlider.propTypes = {
	range: types.range,
	componentId: types.stringRequired,
	addComponent: types.funcRequired,
	setQueryOptions: types.funcRequired,
	updateQuery: types.funcRequired,
	defaultSelected: types.range,
	react: types.react,
	options: types.options,
	removeComponent: types.funcRequired,
	dataField: types.stringRequired,
	interval: types.number,
	beforeValueChange: types.func,
	onValueChange: types.func,
	customQuery: types.func,
	onQueryChange: types.func,
	showHistogram: types.bool,
	stepValue: types.number,
	URLParams: types.boolRequired,
	title: types.title,
	filterLabel: types.string,
	selectedValue: types.selectedValue,
	style: types.style,
	className: types.string,
	snap: types.bool,
	innerClass: types.style,
};

DynamicRangeSlider.defaultProps = {
	stepValue: 1,
	showHistogram: true,
	URLParams: false,
	style: {},
	className: null,
	snap: true,
};

const mapStateToProps = (state, props) => ({
	options:
		(state.aggregations[props.componentId]
		&& state.aggregations[props.componentId][props.dataField]
		&& state.aggregations[props.componentId][props.dataField].buckets)
			? state.aggregations[props.componentId][props.dataField].buckets
			: [],
	range: state.aggregations[`${props.componentId}__range__internal`]
		&& state.aggregations[`${props.componentId}__range__internal`].min
		? {
			start: state.aggregations[`${props.componentId}__range__internal`].min.value,
			end: state.aggregations[`${props.componentId}__range__internal`].max.value,
		}
		: null,
	selectedValue: state.selectedValues[props.componentId]
		? state.selectedValues[props.componentId].value
		: null,
});

const mapDispatchtoProps = dispatch => ({
	addComponent: component => dispatch(addComponent(component)),
	removeComponent: component => dispatch(removeComponent(component)),
	watchComponent: (component, react) =>
		dispatch(watchComponent(component, react)),
	updateQuery: updateQueryObject => dispatch(updateQuery(updateQueryObject)),
	setQueryOptions: (component, props, execute) =>
		dispatch(setQueryOptions(component, props, execute)),
});

export default connect(mapStateToProps, mapDispatchtoProps)(DynamicRangeSlider);
<commit_msg>web: DynamicRangeSlider - onQueryChange prop destructing<commit_after>import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
	addComponent,
	removeComponent,
	watchComponent,
	updateQuery,
	setQueryOptions,
} from '@appbaseio/reactivecore/lib/actions';
import {
	isEqual,
	checkValueChange,
	checkPropChange,
	checkSomePropChange,
	getClassName,
	pushToAndClause,
} from '@appbaseio/reactivecore/lib/utils/helper';
import types from '@appbaseio/reactivecore/lib/utils/types';
import Rheostat from 'rheostat';

import HistogramContainer from './addons/HistogramContainer';
import RangeLabel from './addons/RangeLabel';
import Slider from '../../styles/Slider';
import Title from '../../styles/Title';
import { rangeLabelsContainer } from '../../styles/Label';

class DynamicRangeSlider extends Component {
	constructor(props) {
		super(props);

		this.state = {
			currentValue: null,
			range: null,
			stats: [],
		};
		this.internalHistogramComponent = `${this.props.componentId}__histogram__internal`;
		this.internalRangeComponent = `${this.props.componentId}__range__internal`;
		this.internalMatchAllComponent = `${this.props.componentId}__match_all__internal`;
		this.locked = false;
	}

	componentWillMount() {
		this.props.addComponent(this.props.componentId);
		this.props.addComponent(this.internalHistogramComponent);
		this.props.addComponent(this.internalRangeComponent);

		// get range before executing other queries
		this.updateRangeQueryOptions(this.props);
		this.setReact(this.props);
	}

	componentWillReceiveProps(nextProps) {
		if (!isEqual(this.props.range, nextProps.range) && nextProps.range) {
			this.updateQueryOptions(nextProps, nextProps.range);
			// floor and ceil to take edge cases into account
			this.updateRange({
				start: Math.floor(nextProps.range.start),
				end: Math.ceil(nextProps.range.end),
			});

			if (nextProps.selectedValue) {
				this.handleChange(nextProps.selectedValue);
			} else if (nextProps.defaultSelected) {
				this.handleChange([
					nextProps.defaultSelected.start,
					nextProps.defaultSelected.end,
				]);
			} else {
				this.handleChange([
					Math.floor(nextProps.range.start),
					Math.ceil(nextProps.range.end),
				]);
			}
		} else if (!isEqual(this.props.defaultSelected, nextProps.defaultSelected)) {
			this.handleChange(
				[nextProps.defaultSelected.start, nextProps.defaultSelected.end],
				nextProps,
			);
		} else if (!isEqual(this.state.currentValue, nextProps.selectedValue)) {
			this.handleChange(nextProps.selectedValue || [nextProps.range.start, nextProps.range.end]);
		}
		checkPropChange(this.props.react, nextProps.react, () => {
			this.updateRangeQueryOptions(nextProps);
			this.setReact(nextProps);
		});
		checkPropChange(this.props.dataField, nextProps.dataField, () => {
			this.updateRangeQueryOptions(nextProps);
		});
		checkSomePropChange(
			this.props,
			nextProps,
			['showHistogram', 'interval'],
			() => this.updateQueryOptions(nextProps, nextProps.range || this.state.range),
		);
		checkPropChange(this.props.options, nextProps.options, () => {
			const { options } = nextProps;
			options.sort((a, b) => {
				if (a.key < b.key) return -1;
				if (a.key > b.key) return 1;
				return 0;
			});
			this.setState({
				stats: options,
			});
		});
	}

	shouldComponentUpdate(nextProps, nextState) {
		if (nextState.range) {
			const upperLimit = Math.floor((nextState.range.end - nextState.range.start) / 2);
			if (nextProps.stepValue < 1 || nextProps.stepValue > upperLimit) {
				console.warn(`stepValue for DynamicRangeSlider ${nextProps.componentId} should be greater than 0 and less than or equal to ${upperLimit}`);
				return false;
			}
			return true;
		}
		return true;
	}

	componentWillUnmount() {
		this.props.removeComponent(this.props.componentId);
		this.props.removeComponent(this.internalHistogramComponent);
		this.props.removeComponent(this.internalRangeComponent);
		this.props.removeComponent(this.internalMatchAllComponent);
	}

	setReact = (props) => {
		const { react } = props;
		if (react) {
			props.watchComponent(this.internalRangeComponent, props.react);
			const newReact = pushToAndClause(
				react,
				this.internalHistogramComponent,
			);
			props.watchComponent(props.componentId, newReact);
		} else {
			// internalRangeComponent watches internalMatchAll component allowing execution of query
			// in case of no react prop
			this.props.addComponent(this.internalMatchAllComponent);
			props.setQueryOptions(this.internalMatchAllComponent, { aggs: { match_all: {} } }, false);
			props.watchComponent(this.internalRangeComponent, { and: this.internalMatchAllComponent });
			props.watchComponent(props.componentId, { and: this.internalHistogramComponent });
		}
	};

	defaultQuery = (value, props) => {
		if (Array.isArray(value) && value.length) {
			return {
				range: {
					[props.dataField]: {
						gte: value[0],
						lte: value[1],
						boost: 2.0,
					},
				},
			};
		}
		return null;
	};

	getSnapPoints = () => {
		let snapPoints = [];
		let { stepValue } = this.props;
		const { range } = this.state;

		// limit the number of steps to prevent generating a large number of snapPoints
		if ((range.end - range.start) / stepValue > 100) {
			stepValue = (range.end - range.start) / 100;
		}

		for (let i = range.start; i <= range.end; i += stepValue) {
			snapPoints = snapPoints.concat(i);
		}
		if (snapPoints[snapPoints.length - 1] !== range.end) {
			snapPoints = snapPoints.concat(range.end);
		}
		return snapPoints;
	};

	getValidInterval = (props, range) => {
		const min = Math.ceil((range.end - range.start) / 100) || 1;
		if (!props.interval) {
			return min;
		} else if (props.interval < min) {
			console.error(`${props.componentId}: interval prop's value should be greater than or equal to ${min}`);
			return min;
		}
		return props.interval;
	};

	histogramQuery = (props, range) => ({
		[props.dataField]: {
			histogram: {
				field: props.dataField,
				interval: this.getValidInterval(props, range),
				offset: range.start,
			},
		},
	});

	rangeQuery = props => ({
		min: { min: { field: props.dataField } },
		max: { max: { field: props.dataField } },
	});

	handleChange = (currentValue, props = this.props) => {
		// ignore state updates when component is locked
		if (props.beforeValueChange && this.locked) {
			return;
		}

		this.locked = true;
		const performUpdate = () => {
			this.setState({
				currentValue,
			}, () => {
				this.updateQuery([currentValue[0], currentValue[1]], props);
				this.locked = false;
			});
		};
		checkValueChange(
			props.componentId,
			{
				start: currentValue[0],
				end: currentValue[1],
			},
			props.beforeValueChange,
			props.onValueChange,
			performUpdate,
		);
	};

	handleSlider = ({ values }) => {
		this.handleChange(values);
	};

	updateQuery = (value, props) => {
		const query = props.customQuery || this.defaultQuery;

		const { onQueryChange = null } = props;

		props.updateQuery({
			componentId: props.componentId,
			query: query(value, props),
			value,
			label: props.filterLabel,
			showFilter: false, // disable filters for DynamicRangeSlider
			URLParams: props.URLParams,
			onQueryChange,
		});
	};

	updateQueryOptions = (props, range) => {
		if (props.showHistogram) {
			const queryOptions = {
				aggs: this.histogramQuery(props, range),
			};

			props.setQueryOptions(this.internalHistogramComponent, queryOptions, false);

			const query = props.customQuery || this.defaultQuery;

			props.updateQuery({
				componentId: this.internalHistogramComponent,
				query: query([range.start, range.end], props),
			});
		}
	};

	updateRange = (range) => {
		this.setState({
			range,
		});
	}

	updateRangeQueryOptions = (props) => {
		const queryOptions = {
			aggs: this.rangeQuery(props),
		};

		props.setQueryOptions(this.internalRangeComponent, queryOptions);
	};

	render() {
		if (!this.state.currentValue || !this.state.range) {
			return null;
		}
		return (
			<Slider primary style={this.props.style} className={this.props.className}>
				{this.props.title && (
					<Title
						className={getClassName(this.props.innerClass, 'title') || null}
					>
						{this.props.title}
					</Title>
				)}
				{this.state.stats.length && this.props.showHistogram ? (
					<HistogramContainer
						stats={this.state.stats}
						range={this.state.range}
						interval={this.getValidInterval(this.props, this.state.range)}
					/>
				) : null}
				<Rheostat
					min={this.state.range.start}
					max={this.state.range.end}
					values={this.state.currentValue}
					onChange={this.handleSlider}
					snap={this.props.snap}
					snapPoints={this.props.snap ? this.getSnapPoints() : null}
					className={getClassName(this.props.innerClass, 'slider')}
				/>
				<div className={rangeLabelsContainer}>
					<RangeLabel
						align="left"
						className={getClassName(this.props.innerClass, 'label') || null}
					>
						{this.state.range.start}
					</RangeLabel>
					<RangeLabel
						align="right"
						className={getClassName(this.props.innerClass, 'label') || null}
					>
						{this.state.range.end}
					</RangeLabel>
				</div>
			</Slider>
		);
	}
}

DynamicRangeSlider.propTypes = {
	range: types.range,
	componentId: types.stringRequired,
	addComponent: types.funcRequired,
	setQueryOptions: types.funcRequired,
	updateQuery: types.funcRequired,
	defaultSelected: types.range,
	react: types.react,
	options: types.options,
	removeComponent: types.funcRequired,
	dataField: types.stringRequired,
	interval: types.number,
	beforeValueChange: types.func,
	onValueChange: types.func,
	customQuery: types.func,
	onQueryChange: types.func,
	showHistogram: types.bool,
	stepValue: types.number,
	URLParams: types.boolRequired,
	title: types.title,
	filterLabel: types.string,
	selectedValue: types.selectedValue,
	style: types.style,
	className: types.string,
	snap: types.bool,
	innerClass: types.style,
};

DynamicRangeSlider.defaultProps = {
	stepValue: 1,
	showHistogram: true,
	URLParams: false,
	style: {},
	className: null,
	snap: true,
};

const mapStateToProps = (state, props) => ({
	options:
		(state.aggregations[props.componentId]
		&& state.aggregations[props.componentId][props.dataField]
		&& state.aggregations[props.componentId][props.dataField].buckets)
			? state.aggregations[props.componentId][props.dataField].buckets
			: [],
	range: state.aggregations[`${props.componentId}__range__internal`]
		&& state.aggregations[`${props.componentId}__range__internal`].min
		? {
			start: state.aggregations[`${props.componentId}__range__internal`].min.value,
			end: state.aggregations[`${props.componentId}__range__internal`].max.value,
		}
		: null,
	selectedValue: state.selectedValues[props.componentId]
		? state.selectedValues[props.componentId].value
		: null,
});

const mapDispatchtoProps = dispatch => ({
	addComponent: component => dispatch(addComponent(component)),
	removeComponent: component => dispatch(removeComponent(component)),
	watchComponent: (component, react) =>
		dispatch(watchComponent(component, react)),
	updateQuery: updateQueryObject => dispatch(updateQuery(updateQueryObject)),
	setQueryOptions: (component, props, execute) =>
		dispatch(setQueryOptions(component, props, execute)),
});

export default connect(mapStateToProps, mapDispatchtoProps)(DynamicRangeSlider);
<|endoftext|>


<commit_before>/**
 * Provides desktop notifications via periodic polling with an
 * increasing request delay on inactivity.
 *
 * @author      Alexander Ebert
 * @copyright   2001-2017 WoltLab GmbH
 * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @module      WoltLabSuite/Core/Notification/Handler
 */
define(['Ajax', 'Core', 'EventHandler'], function(Ajax, Core, EventHandler) {
	"use strict";

	if (!('Promise' in window) || !('Notification' in window)) {
		// fake object exposed to ancient browsers (*cough* IE11 *cough*)
		return {
			setup: function () {}
		}
	}

	var _allowNotification = false;
	var _icon = '';
	var _inactiveSince = 0;
	//noinspection JSUnresolvedVariable
	var _lastRequestTimestamp = window.TIME_NOW;
	var _requestTimer = null;
	var _sessionKeepAlive = 0;

	/**
	 * @exports     WoltLabSuite/Core/Notification/Handler
	 */
	return {
		/**
		 * Initializes the desktop notification system.
		 *
		 * @param       {Object}        options         initialization options
		 */
		setup: function (options) {
			options = Core.extend({
				enableNotifications: false,
				icon: '',
				sessionKeepAlive: 0
			}, options);

			_icon = options.icon;
			_sessionKeepAlive = options.sessionKeepAlive * 60;

			this._prepareNextRequest();

			document.addEventListener('visibilitychange', this._onVisibilityChange.bind(this));
			window.addEventListener('storage', this._onStorage.bind(this));

			this._onVisibilityChange(null);

			if (options.enableNotifications) {
				switch (window.Notification.permission) {
					case 'granted':
						_allowNotification = true;
						break;
					case 'default':
						window.Notification.requestPermission(function (result) {
							if (result === 'granted') {
								_allowNotification = true;
							}
						});
						break;
				}
			}
		},

		/**
		 * Detects when this window is hidden or restored.
		 *
		 * @param       {Event}         event
		 * @protected
		 */
		_onVisibilityChange: function(event) {
			// document was hidden before
			if (event !== null && !document.hidden) {
				var difference = (Date.now() - _inactiveSince) / 60000;
				if (difference > 4) {
					this._resetTimer();
					this._dispatchRequest();
				}
			}

			_inactiveSince = (document.hidden) ? Date.now() : 0;
		},

		/**
		 * Returns the delay in minutes before the next request should be dispatched.
		 *
		 * @return      {int}
		 * @protected
		 */
		_getNextDelay: function() {
			// TODO: DEBUG ONLY
			return 1;

			if (_inactiveSince === 0) return 5;

			// milliseconds -> minutes
			var inactiveMinutes = ~~((Date.now() - _inactiveSince) / 60000);
			if (inactiveMinutes < 15) {
				return 5;
			}
			else if (inactiveMinutes < 30) {
				return 10;
			}

			return 15;
		},

		/**
		 * Resets the request delay timer.
		 *
		 * @protected
		 */
		_resetTimer: function() {
			if (_requestTimer !== null) {
				window.clearTimeout(_requestTimer);
				_requestTimer = null;
			}
		},

		/**
		 * Schedules the next request using a calculated delay.
		 *
		 * @protected
		 */
		_prepareNextRequest: function() {
			this._resetTimer();

			var delay = Math.min(this._getNextDelay(), _sessionKeepAlive);
			_requestTimer = window.setTimeout(this._dispatchRequest.bind(this), delay * 60000);
		},

		/**
		 * Requests new data from the server.
		 *
		 * @protected
		 */
		_dispatchRequest: function() {
			var parameters = {};
			EventHandler.fire('com.woltlab.wcf.notification', 'beforePoll', parameters);

			// this timestamp is used to determine new notifications and to avoid
			// notifications being displayed multiple times due to different origins
			// (=subdomains) used, because we cannot synchronize them in the client
			parameters.lastRequestTimestamp = _lastRequestTimestamp;

			Ajax.api(this, {
				parameters: parameters
			});
		},

		/**
		 * Notifies subscribers for updated data received by another tab.
		 *
		 * @protected
		 */
		_onStorage: function() {
			// abort and re-schedule periodic request
			this._prepareNextRequest();

			var pollData, keepAliveData, abort = false;
			try {
				pollData = window.localStorage.getItem(Core.getStoragePrefix() + 'notification');
				keepAliveData = window.localStorage.getItem(Core.getStoragePrefix() + 'keepAliveData');

				pollData = JSON.parse(pollData);
				keepAliveData = JSON.parse(keepAliveData);
			}
			catch (e) {
				abort = true;
			}

			if (!abort) {
				EventHandler.fire('com.woltlab.wcf.notification', 'onStorage', {
					pollData: pollData,
					keepAliveData: keepAliveData
				});
			}
		},

		_ajaxSuccess: function(data) {
			var abort = false;
			var keepAliveData = data.returnValues.keepAliveData;
			var pollData = data.returnValues.pollData;

			// forward keep alive data
			window.WCF.System.PushNotification.executeCallbacks(keepAliveData);

			// store response data in local storage
			try {
				window.localStorage.setItem(Core.getStoragePrefix() + 'notification', JSON.stringify(pollData));
				window.localStorage.setItem(Core.getStoragePrefix() + 'keepAliveData', JSON.stringify(keepAliveData));
			}
			catch (e) {
				// storage is unavailable, e.g. in private mode, log error and disable polling
				abort = true;

				window.console.log(e);
			}

			if (!abort) {
				this._prepareNextRequest();
			}

			_lastRequestTimestamp = data.returnValues.lastRequestTimestamp;

			EventHandler.fire('com.woltlab.wcf.notification', 'afterPoll', pollData);

			this._showNotification(pollData);
		},

		/**
		 * Displays a desktop notification.
		 *
		 * @param       {Object}        pollData
		 * @protected
		 */
		_showNotification: function(pollData) {
			if (!_allowNotification) {
				return;
			}

			//noinspection JSUnresolvedVariable
			if (typeof pollData.notification === 'object' && typeof pollData.notification.message ===  'string') {
				//noinspection JSUnresolvedVariable
				var notification = new window.Notification(pollData.notification.title, {
					body: pollData.notification.message,
					icon: _icon
				});
				notification.onclick = function () {
					window.focus();
					notification.close();

					//noinspection JSUnresolvedVariable
					window.location = pollData.notification.link;
				};
			}
		},

		_ajaxSetup: function() {
			//noinspection JSUnresolvedVariable
			return {
				data: {
					actionName: 'poll',
					className: 'wcf\\data\\session\\SessionAction'
				},
				ignoreError: !window.ENABLE_DEBUG_MODE,
				silent: !window.ENABLE_DEBUG_MODE
			};
		}
	}
});
<commit_msg>Removed debug code<commit_after>/**
 * Provides desktop notifications via periodic polling with an
 * increasing request delay on inactivity.
 *
 * @author      Alexander Ebert
 * @copyright   2001-2017 WoltLab GmbH
 * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @module      WoltLabSuite/Core/Notification/Handler
 */
define(['Ajax', 'Core', 'EventHandler'], function(Ajax, Core, EventHandler) {
	"use strict";

	if (!('Promise' in window) || !('Notification' in window)) {
		// fake object exposed to ancient browsers (*cough* IE11 *cough*)
		return {
			setup: function () {}
		}
	}

	var _allowNotification = false;
	var _icon = '';
	var _inactiveSince = 0;
	//noinspection JSUnresolvedVariable
	var _lastRequestTimestamp = window.TIME_NOW;
	var _requestTimer = null;
	var _sessionKeepAlive = 0;

	/**
	 * @exports     WoltLabSuite/Core/Notification/Handler
	 */
	return {
		/**
		 * Initializes the desktop notification system.
		 *
		 * @param       {Object}        options         initialization options
		 */
		setup: function (options) {
			options = Core.extend({
				enableNotifications: false,
				icon: '',
				sessionKeepAlive: 0
			}, options);

			_icon = options.icon;
			_sessionKeepAlive = options.sessionKeepAlive * 60;

			this._prepareNextRequest();

			document.addEventListener('visibilitychange', this._onVisibilityChange.bind(this));
			window.addEventListener('storage', this._onStorage.bind(this));

			this._onVisibilityChange(null);

			if (options.enableNotifications) {
				switch (window.Notification.permission) {
					case 'granted':
						_allowNotification = true;
						break;
					case 'default':
						window.Notification.requestPermission(function (result) {
							if (result === 'granted') {
								_allowNotification = true;
							}
						});
						break;
				}
			}
		},

		/**
		 * Detects when this window is hidden or restored.
		 *
		 * @param       {Event}         event
		 * @protected
		 */
		_onVisibilityChange: function(event) {
			// document was hidden before
			if (event !== null && !document.hidden) {
				var difference = (Date.now() - _inactiveSince) / 60000;
				if (difference > 4) {
					this._resetTimer();
					this._dispatchRequest();
				}
			}

			_inactiveSince = (document.hidden) ? Date.now() : 0;
		},

		/**
		 * Returns the delay in minutes before the next request should be dispatched.
		 *
		 * @return      {int}
		 * @protected
		 */
		_getNextDelay: function() {
			if (_inactiveSince === 0) return 5;

			// milliseconds -> minutes
			var inactiveMinutes = ~~((Date.now() - _inactiveSince) / 60000);
			if (inactiveMinutes < 15) {
				return 5;
			}
			else if (inactiveMinutes < 30) {
				return 10;
			}

			return 15;
		},

		/**
		 * Resets the request delay timer.
		 *
		 * @protected
		 */
		_resetTimer: function() {
			if (_requestTimer !== null) {
				window.clearTimeout(_requestTimer);
				_requestTimer = null;
			}
		},

		/**
		 * Schedules the next request using a calculated delay.
		 *
		 * @protected
		 */
		_prepareNextRequest: function() {
			this._resetTimer();

			var delay = Math.min(this._getNextDelay(), _sessionKeepAlive);
			_requestTimer = window.setTimeout(this._dispatchRequest.bind(this), delay * 60000);
		},

		/**
		 * Requests new data from the server.
		 *
		 * @protected
		 */
		_dispatchRequest: function() {
			var parameters = {};
			EventHandler.fire('com.woltlab.wcf.notification', 'beforePoll', parameters);

			// this timestamp is used to determine new notifications and to avoid
			// notifications being displayed multiple times due to different origins
			// (=subdomains) used, because we cannot synchronize them in the client
			parameters.lastRequestTimestamp = _lastRequestTimestamp;

			Ajax.api(this, {
				parameters: parameters
			});
		},

		/**
		 * Notifies subscribers for updated data received by another tab.
		 *
		 * @protected
		 */
		_onStorage: function() {
			// abort and re-schedule periodic request
			this._prepareNextRequest();

			var pollData, keepAliveData, abort = false;
			try {
				pollData = window.localStorage.getItem(Core.getStoragePrefix() + 'notification');
				keepAliveData = window.localStorage.getItem(Core.getStoragePrefix() + 'keepAliveData');

				pollData = JSON.parse(pollData);
				keepAliveData = JSON.parse(keepAliveData);
			}
			catch (e) {
				abort = true;
			}

			if (!abort) {
				EventHandler.fire('com.woltlab.wcf.notification', 'onStorage', {
					pollData: pollData,
					keepAliveData: keepAliveData
				});
			}
		},

		_ajaxSuccess: function(data) {
			var abort = false;
			var keepAliveData = data.returnValues.keepAliveData;
			var pollData = data.returnValues.pollData;

			// forward keep alive data
			window.WCF.System.PushNotification.executeCallbacks(keepAliveData);

			// store response data in local storage
			try {
				window.localStorage.setItem(Core.getStoragePrefix() + 'notification', JSON.stringify(pollData));
				window.localStorage.setItem(Core.getStoragePrefix() + 'keepAliveData', JSON.stringify(keepAliveData));
			}
			catch (e) {
				// storage is unavailable, e.g. in private mode, log error and disable polling
				abort = true;

				window.console.log(e);
			}

			if (!abort) {
				this._prepareNextRequest();
			}

			_lastRequestTimestamp = data.returnValues.lastRequestTimestamp;

			EventHandler.fire('com.woltlab.wcf.notification', 'afterPoll', pollData);

			this._showNotification(pollData);
		},

		/**
		 * Displays a desktop notification.
		 *
		 * @param       {Object}        pollData
		 * @protected
		 */
		_showNotification: function(pollData) {
			if (!_allowNotification) {
				return;
			}

			//noinspection JSUnresolvedVariable
			if (typeof pollData.notification === 'object' && typeof pollData.notification.message ===  'string') {
				//noinspection JSUnresolvedVariable
				var notification = new window.Notification(pollData.notification.title, {
					body: pollData.notification.message,
					icon: _icon
				});
				notification.onclick = function () {
					window.focus();
					notification.close();

					//noinspection JSUnresolvedVariable
					window.location = pollData.notification.link;
				};
			}
		},

		_ajaxSetup: function() {
			//noinspection JSUnresolvedVariable
			return {
				data: {
					actionName: 'poll',
					className: 'wcf\\data\\session\\SessionAction'
				},
				ignoreError: !window.ENABLE_DEBUG_MODE,
				silent: !window.ENABLE_DEBUG_MODE
			};
		}
	}
});
<|endoftext|>