Commit 39f0ef71 authored by Benjamin Fischer's avatar Benjamin Fischer

Merge branch 'default' into 'windowManager'

parents 679aad45 6a10f473
......@@ -42,10 +42,20 @@ setup(
packages=packages,
package_data={"vispa": files},
scripts=[os.path.join(srcdir, 'bin', 'vispa'), os.path.join(srcdir, 'bin', 'vispad'), os.path.join(srcdir, 'bin', 'vispa-ldap-export')],
install_requires=["sqlalchemy >= 0.9.0", "mako", "cherrypy",
"paramiko", "rpyc",
"alembic >= 0.7.3", # for Operations.batch_alter_table
"passlib", "ws4py", "ldap3"],
install_requires=[
"sqlalchemy >= 0.9.0",
"mako",
"cherrypy<9.0.0", # cherrypy removed wsgiserver, see https://github.com/Lawouach/WebSocket-for-Python/issues/205
"paramiko",
"rpyc>3.3.0",
"alembic >= 0.7.3", # for Operations.batch_alter_table
"passlib",
"ws4py",
"ldap3"
],
dependency_links=[
'https://github.com/geromueller/rpyc/archive/master.zip#egg=rpyc-3.4.0',
],
extras_require={"doc": ["sphinx", "sphinx-bootstrap-theme"]},
classifiers=[
"Development Status :: 5 - Production/Stable",
......
#!/bin/bash
if [ ! -f /root/.admpwd ]; then
date | md5sum | head -c 16 > /root/.admpwd
fi
export ADMPWD=`cat /root/.admpwd`
export DEBCONF_FRONTEND=noninteractive
# ----------------------------------------------------------------------
......@@ -37,22 +42,22 @@ apt-get -y upgrade
# ----------------------------------------------------------------------
if [ ! -f /root/.my.cnf ]; then
echo "mariadb-server-10.0 mysql-server/root_password_again password zehdjkamam" | debconf-set-selections
echo "mariadb-server-10.0 mysql-server/root_password password zehdjkamam" | debconf-set-selections
echo "mariadb-server-10.0 mysql-server/root_password_again password $ADMPWD" | debconf-set-selections
echo "mariadb-server-10.0 mysql-server/root_password password $ADMPWD" | debconf-set-selections
echo "mariadb-server-10.0 mariadb-server/oneway_migration boolean true" | debconf-set-selections
apt-get -y -t jessie-backports install mariadb-server
cat > /root/.my.cnf <<-EOF
[client]
user = root
password = zehdjkamam
password = $ADMPWD
EOF
fi
echo "CREATE DATABASE vispa;" | mysql
echo "CREATE USER 'vispa'@'localhost' IDENTIFIED BY 'changeme';" | mysql
echo "GRANT ALL PRIVILEGES ON vispa.* TO 'vispa'@'localhost';" | mysql
echo "FLUSH PRIVILEGES;" | mysql
echo "CREATE DATABASE vispa;" | mysql
echo "CREATE USER 'vispa'@'localhost' IDENTIFIED BY '$ADMPWD';" | mysql
echo "GRANT ALL PRIVILEGES ON vispa.* TO 'vispa'@'localhost';" | mysql
echo "FLUSH PRIVILEGES;" | mysql
fi
# ----------------------------------------------------------------------
# LDAP Server
......@@ -61,8 +66,8 @@ echo "FLUSH PRIVILEGES;" | mysql
# LDAP SETUP: http://techpubs.spinlocksolutions.com/dklar/ldap.html
echo "slapd slapd/password2 password zehdjkamam" | debconf-set-selections
echo "slapd slapd/password1 password zehdjkamam" | debconf-set-selections
echo "slapd slapd/password2 password $ADMPWD" | debconf-set-selections
echo "slapd slapd/password1 password $ADMPWD" | debconf-set-selections
echo "slapd slapd/backend select HDB" | debconf-set-selections
echo "slapd slapd/allow_ldap_v2 boolean false" | debconf-set-selections
......@@ -72,7 +77,7 @@ apt-get -y -t jessie-backports install slapd ldap-utils
# LDAP Config
# ----------------------------------------------------------------------
SLAPD_PWD=`slappasswd -s zehdjkamam`
SLAPD_PWD=`slappasswd -s $ADMPWD`
cat > /tmp/tmp.ldif <<EOF
###########################################################
......@@ -119,7 +124,7 @@ olcDbIndex: uid pres,eq
dn: olcDatabase={1}hdb,cn=config
changeType: modify
add: olcDbIndex
olcDbIndex: cn,sn,mail pres,eq,approx,sub
olcDbIndex: cn,sn,mail,memberUid pres,eq,approx,sub
dn: olcDatabase={1}hdb,cn=config
changeType: modify
......@@ -174,7 +179,7 @@ userPassword: $SLAPD_PWD
description: LDAP administrator
EOF
ldapadd -c -x -D cn=admin,dc=vispa,dc=local -w zehdjkamam -f /tmp/tmp.ldif
ldapadd -c -x -D cn=admin,dc=vispa,dc=local -w $ADMPWD -f /tmp/tmp.ldif
cat > /tmp/tmp.ldif <<EOF
dn: ou=people,dc=vispa,dc=local
......@@ -186,23 +191,30 @@ ou: group
objectClass: organizationalUnit
EOF
ldapadd -c -x -D cn=admin,dc=vispa,dc=local -w zehdjkamam -f /tmp/tmp.ldif
ldapadd -c -x -D cn=admin,dc=vispa,dc=local -w $ADMPWD -f /tmp/tmp.ldif
# ----------------------------------------------------------------------
# VISPA
# ----------------------------------------------------------------------
apt-get -y -t jessie-backports install python-virtualenv python-dev libffi-dev libssl-dev
apt-get -y -t jessie-backports install python-virtualenv python-dev libffi-dev libssl-dev build-essential
if [ ! -d /srv/venv ]; then
virtualenv /srv/venv
fi
#/srv/venv/bin/pip install --upgrade pip
#/srv/venv/bin/pip install --upgrade -r /srv/vispa/requirements.txt
/srv/venv/bin/pip install --upgrade pymysql
/srv/venv/bin/pip install --upgrade pymysql
/srv/venv/bin/pip install --upgrade setuptools
/srv/venv/bin/pip install --upgrade https://github.com/tomerfiliba/rpyc/archive/master.zip
/srv/venv/bin/python /srv/vispa/setup.py develop
if [ -f /srv/vispa/setup.py ]; then
/srv/venv/bin/python /srv/vispa/setup.py develop
#elif [ -f ../../setup.py ]; then
# /srv/venv/bin/python ../../setup.py develop
else
/srv/venv/bin/pip install --upgrade https://forge.physik.rwth-aachen.de/hg/vispa-web/vispa/archive/2.0.zip
fi
mkdir -p /etc/vispa
cat > /etc/vispa/cherrypy.ini <<EOF
......@@ -218,7 +230,7 @@ EOF
cat > /etc/vispa/vispa.ini <<EOF
[database]
sqlalchemy.url = mysql+pymysql://vispa:changeme@localhost/vispa
sqlalchemy.url = mysql+pymysql://vispa:$ADMPWD@localhost/vispa
sqlalchemy.pool_size = 50
sqlalchemy.pool_recycle = 3600
sqlalchemy.max_overflow = 50
......@@ -226,7 +238,7 @@ sqlalchemy.max_overflow = 50
[alembic]
use_alembic = True
# inplace installation
script_location = vispa/models/alembic
#script_location = vispa/models/alembic
# global installation
#script_location = vispa:models/alembic
auto_migrate = True
......@@ -238,7 +250,7 @@ dev_mode = False
[ldap-export]
url = localhost
user = cn=admin,dc=vispa,dc=local
password = zehdjkamam
password = $ADMPWD
user_base = ou=people,dc=vispa,dc=local
group_base = ou=group,dc=vispa,dc=local
sync_on_startup = False
......@@ -261,7 +273,7 @@ EOF
cat > /etc/systemd/system/vispa.service <<"EOF"
[Unit]
After=network.target
After=network.target
[Service]
EnvironmentFile=-/etc/default/vispa
......@@ -272,7 +284,7 @@ RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
......@@ -320,3 +332,19 @@ Session:
EOF
pam-auth-update --package
# ----------------------------------------------------------------------
# default workspace
# ----------------------------------------------------------------------
while true; do
started=`systemctl status vispa | grep "Bus STARTED" | wc -l`
if [ -z $started ]; then
echo "waiting for VISPA to startup.."
sleep 1
else
echo "delete from workspace where user_id is null;" | mysql vispa
echo "insert into workspace (user_id, name, host, auto_connect, login_credentials, command) values (null, 'vispa.local', 'localhost', 1, 1, '/srv/venv/bin/python');" | mysql vispa
break
fi
done
......@@ -52,13 +52,14 @@ class AjaxController(AbstractController):
user_group = Group.get_or_create_by_name(session, user_group)
user_group.add_user(user, Group_User_Assoc.CONFIRMED)
hash = None
if vispa.config("web", "registration.autoactive", True):
return { "hash": user.hash }
hash = user.hash
elif vispa.config("web", "registration.sendmail", False):
User.send_registration_mail(user.name, user.email, user.hash)
return { "hash": None }
return { "hash": hash }
@cherrypy.expose
@cherrypy.tools.user(on=False)
......
......@@ -66,6 +66,12 @@ class RootController(AbstractController):
elif len(self.cache_bust) == 0:
self.cache_bust = None
@classmethod
def expire_cookie(cls, name):
cherrypy.response.cookie[name] = ""
cherrypy.response.cookie[name]["expires"] = 0
cherrypy.response.cookie[name]["max-age"] = 0
def mount_extension_controller(self, mountpoint, controller):
if hasattr(self.extensions, mountpoint):
logger.warning("Controller mountpoint already exists: %s" % mountpoint)
......@@ -147,6 +153,12 @@ class RootController(AbstractController):
if "user_id" in cherrypy.session:
raise cherrypy.HTTPRedirect(path)
# delete all cookies except for the session id
session_key = cherrypy.serving.request.config.get("tools.sessions.name", "session_id")
for key in cherrypy.response.cookie.keys():
if key != session_key:
self.expire_cookie(key)
login = cherrypy.request.login
if login and vispa.config("user", "remote.enabled", False):
user = User.get_by_name(db, login)
......@@ -169,7 +181,6 @@ class RootController(AbstractController):
vispa.fire_callback("user.login", user)
raise cherrypy.HTTPRedirect(path)
welcome_phrase = vispa.config("web", "text.welcome", "")
login_text = vispa.config("web", "text.login", "")
registration_text = vispa.config("web", "text.registration", "")
......@@ -220,15 +231,14 @@ class RootController(AbstractController):
@cherrypy.expose
@cherrypy.tools.method(accept="GET")
def logout(self, path="/"):
# remove some cookies
cookies = ["ROUTEID"]
for c in cookies:
if c in cherrypy.response.cookie:
cherrypy.response.cookie[c]["expires"] = 0
vispa.fire_callback("user.logout", cherrypy.request.user)
vispa.socketbus.remove_session(self.get("window_id"))
cherrypy.lib.sessions.expire()
# remove all cookies
for key in cherrypy.response.cookie.keys():
self.expire_cookie(key)
raise cherrypy.HTTPRedirect(vispa.url.dynamic(path))
@cherrypy.expose
......@@ -241,7 +251,7 @@ class RootController(AbstractController):
"welcome_phrase": vispa.config("web", "text.welcome", ""),
"username": None if not isinstance(user, User) else user.name,
"hash": hash,
"cache_bust" : self.cache_bust
"cache_bust": self.cache_bust
}
return data
......
......@@ -222,7 +222,7 @@ define(["jquery", "emitter"], function($, Emitter) {
},
attachToTemporary: function(text) {
this.temporaryText = this.temporaryText + String(text);
this.temporaryText = this.temporaryText + String(text).replace(/\r/g, "\n");
},
append: function(text) {
......
......@@ -11,20 +11,19 @@ import select
import pty
import signal
from subprocess import Popen, PIPE
import fcntl
logger = logging.getLogger(__name__)
from vispa.remote.helper import UTF8Buffer
def expand(path):
return os.path.expanduser(os.path.expandvars(path))
class CodeEditorRpc:
MAX_BURST = 1000 # max bytes transmitted at once
MAX_RATE = 2000 # desired baud rate for transmission
BURST_DELAY = 0.25 # delay between transmission when burst buffer is exausted
BURST_BUFFER = 8000 # burst buffer size
MAX_BURST = 1024*1024 # max bytes transmitted at once
MAX_RATE = 1024*1024 # desired baud rate for transmission
BURST_DELAY = 0 # delay between transmission when burst buffer is exausted
BURST_BUFFER = 1024*1024 # burst buffer size
SIGTEM_SIGKILL_DELAY = 0.1
def __init__(self, window_id, view_id):
......@@ -34,16 +33,13 @@ class CodeEditorRpc:
self._topic = "extension.%s.socket" % self._view_id
self._thread = None
self._abort = False
self._kill = False
self._pty_master, self._pty_slave = pty.openpty()
self._pty_fd = os.fdopen(self._pty_master, 'r')
self._pty_fno = self._pty_fd.fileno()
self._popen = None
logger.debug("CodeEditorRpc created")
#so far equivalent to abort (might be extended in the future)
# so far equivalent to abort (might be extended in the future)
def close(self):
self._abort = True
......@@ -52,6 +48,15 @@ class CodeEditorRpc:
self._topic + "." + topic, window_id=self._window_id, data=data)
def runningjob(self):
if self._popen is None:
return False
returncode = self._popen.poll()
if returncode is None:
return True
else:
return False
return bool(self._thread and self._thread.is_alive())
def start(self, cmd, base):
......@@ -64,8 +69,9 @@ class CodeEditorRpc:
try:
self._popen = Popen(self._cmd, cwd=self._base, shell=True,
stdin=PIPE, stdout=self._pty_slave, stderr=self._pty_slave,
close_fds=True, preexec_fn=os.setsid)
stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=1,
preexec_fn=os.setsid # so kill group does not kill this process
)
except Exception as e:
return str(e)
......@@ -88,17 +94,41 @@ class CodeEditorRpc:
self._abort = False
read_time = time.time()
read_amount = 0
buf = UTF8Buffer()
while not self._abort:
r, _, _ = select.select([self._pty_fno], [], [], 0.1)
if read_amount >= CodeEditorRpc.BURST_BUFFER:
time.sleep(CodeEditorRpc.BURST_DELAY)
elif self._pty_fno in r:
data = buf.passThru(os.read(self._pty_fno, CodeEditorRpc.MAX_BURST))
read_amount += len(data)
self._send('data', data)
# store utf-8 remainders
stdout = self._popen.stdout #UTF8Reader(self._popen.stdout)
stderr = self._popen.stderr #UTF8Reader(self._popen.stderr)
outno = self._popen.stdout.fileno()
fl = fcntl.fcntl(outno, fcntl.F_GETFL)
fcntl.fcntl(outno, fcntl.F_SETFL, fl | os.O_NONBLOCK)
errno = self._popen.stderr.fileno()
fl = fcntl.fcntl(errno, fcntl.F_GETFL)
fcntl.fcntl(errno, fcntl.F_SETFL, fl | os.O_NONBLOCK)
reads = [outno, errno]
while reads:
rs, _, _ = select.select(reads, [], [], 0.1)
for r in rs:
if read_amount >= CodeEditorRpc.BURST_BUFFER:
time.sleep(CodeEditorRpc.BURST_DELAY)
elif r == outno:
data = stdout.read(CodeEditorRpc.MAX_BURST)
if not data:
reads.remove(outno)
else:
read_amount += len(data)
self._send('stdout', data)
elif r == errno:
data = stderr.read(CodeEditorRpc.MAX_BURST)
if not data:
reads.remove(errno)
else:
read_amount += len(data)
self._send('stderr', data)
if read_amount > 0:
read_amount = int(max(0, read_amount - CodeEditorRpc.MAX_RATE*(time.time()-read_time)))
read_amount = int(max(0, read_amount - CodeEditorRpc.MAX_RATE * (time.time() - read_time)))
read_time = time.time()
returncode = self._popen.poll()
......@@ -119,18 +149,33 @@ class CodeEditorRpc:
returncode = -signal.SIGKILL
# check remaing data
read_burst = min( int(CodeEditorRpc.MAX_RATE*CodeEditorRpc.BURST_DELAY),
read_burst = min(int(CodeEditorRpc.MAX_RATE * CodeEditorRpc.BURST_DELAY),
CodeEditorRpc.MAX_BURST)
read_delay = 0
r = True
while r:
r, _, _ = select.select([self._pty_fno], [], [], 0)
if self._pty_fno in r:
if read_delay:
time.sleep(read_delay)
else:
read_delay = float(read_burst)/CodeEditorRpc.MAX_RATE
self._send('data', os.read(self._pty_fno, read_burst))
while reads:
rs, _, _ = select.select(reads, [], [], 0)
for r in rs:
if r in reads:
if read_delay:
time.sleep(read_delay)
else:
read_delay = float(read_burst) / CodeEditorRpc.MAX_RATE
if r == outno:
data = stdout.read()
if not data:
reads.remove(outno)
else:
self._send('stdout', data)
elif r == errno:
data = stderr.read()
if not data:
reads.remove(errno)
else:
self._send('stderr', data)
returncode = self._popen.poll()
if returncode is not None:
break
runtime = round(time.time() - self._starttime, 2)
......@@ -140,7 +185,13 @@ class CodeEditorRpc:
logger.debug("CodeEditorExecuteRpc _stream finished")
def abort(self):
if not self.runningjob():
if self.runningjob():
pgid = os.getpgid(self._popen.pid)
if self._kill:
os.killpg(pgid, signal.SIGKILL)
else:
self._kill = True
os.killpg(pgid, signal.SIGTERM)
return self.runningjob()
else:
return False
self._abort = True
return True
......@@ -11,7 +11,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker
import ldap3
from ldap3.utils.log import set_library_log_detail_level, OFF, BASIC, NETWORK, EXTENDED
from ldap3.core.exceptions import LDAPNoSuchObjectResult
from ldap3.core.exceptions import LDAPNoSuchObjectResult, LDAPAttributeOrValueExistsResult, LDAPEntryAlreadyExistsResult
logger = logging.getLogger(__name__)
......@@ -120,7 +120,7 @@ class LDAPExport(object):
logger.info("Add user: %s, %s, %s", dn, classes, attributes)
try:
self.connection.add(dn, classes, attributes)
except ldap3.LDAPEntryAlreadyExistsResult:
except LDAPEntryAlreadyExistsResult:
logger.info(" -> updated")
changes = {
'uid': _r(username),
......@@ -147,7 +147,7 @@ class LDAPExport(object):
logger.info("Add group: %s, %s, %s", dn, classes, attributes)
try:
self.connection.add(dn, classes, attributes)
except ldap3.LDAPEntryAlreadyExistsResult:
except LDAPEntryAlreadyExistsResult:
logger.info(" -> updated")
changes = {
'cn': _r(name),
......@@ -162,7 +162,7 @@ class LDAPExport(object):
}
try:
self.connection.modify(dn, change)
except ldap3.LDAPAttributeOrValueExistsResult:
except LDAPAttributeOrValueExistsResult:
pass
def user_set_password(self, name, password):
......
......@@ -88,6 +88,7 @@ class TerminalController(AbstractController):
self.release_session()
windowId = cherrypy.request.private_params.get("_windowId", None)
viewId = cherrypy.request.private_params.get("_viewId", None)
# TODO: add userid to tid
tid = windowId + "-" + viewId
terminal = self._terminal(tid)
......
......@@ -28,14 +28,14 @@ class User(Base):
NAME_LENGTH = [6, 30]
PASSWORD_RESET_DELAY = 30 # minutes
NAME_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890_-+'
FORBIDDEN_NAMES = ['data', 'guest', 'global', 'user', 'delete',
'select', 'insert', 'update', 'drop']
FORBIDDEN_NAMES = ['data', 'guest', 'global', 'user', 'delete', 'select', 'insert',
'update', 'drop', 'vispa-admin', 'root', 'vispa', 'admin']
__tablename__ = 'user'
id = Column(Integer, nullable=False, primary_key=True)
name = Column(Unicode(255), nullable=False, unique=True)
name = Column(Unicode(128), nullable=False, unique=True)
password = Column(UnicodeText(), nullable=False)
email = Column(Unicode(255), nullable=False, unique=True)
email = Column(Unicode(128), nullable=False, unique=True)
created = Column(DateTime, nullable=False, default=datetime.now)
last_request = Column(DateTime, nullable=False, default=datetime.now)
last_password_reset = Column(DateTime, nullable=True, default=None)
......
......@@ -20,7 +20,7 @@ class Workspace(Base):
ondelete="CASCADE",
onupdate="CASCADE"),
nullable=True)
name = Column(Unicode(255), nullable=False)
name = Column(Unicode(128), nullable=False)
host = Column(UnicodeText, nullable=False)
login = Column(UnicodeText, nullable=True, default=None)
key = Column(Text, nullable=True, default=None)
......
def truncated_utf8(utf8_bytes):
""" returns the bytes containing complete and truncated bytes """
if not utf8_bytes:
return b"", b""
# https://en.wikipedia.org/wiki/UTF-8#Description
# last byte is 1 byte code pint
if not utf8_bytes[-1] & 0b10000000:
return utf8_bytes, b""
else:
# find initial code point
for i in [-2, -3, -4]:
if utf8_bytes[i] & 0b11000000:
return utf8_bytes[:i+1], utf8_bytes[i+1:]
raise Exception("invalid utf-8 string")
class UTF8Reader(object):
def __init__(self, f):
self._file = f
self._remainder = None
def read(self, n=-1):
utf8_bytes = self._file.read(n)
if utf8_bytes is None:
return None
if self._remainder:
data, self._remainder = truncated_utf8(self._remainder + utf8_bytes)
else:
data, self._remainder = truncated_utf8(utf8_bytes)
return data
class UTF8Buffer(object):
"""
Buffers incoming UTF8 encoded data, and prevents chunks from being created in the middle of a
......
......@@ -166,7 +166,7 @@ class Server(object):
'tools.sessions.path': urlparse(base_dynamic).path,
'tools.sessions.storage_type': 'file',
'tools.sessions.storage_path': vispa.datapath('sessions'),
'tools.sessions.timeout': 1440,
'tools.sessions.timeout': 180,
'tools.staticdir.on': False,
'tools.gzip.on': True,
'tools.gzip.mime_types': ['text/html', 'text/css',
......@@ -234,6 +234,7 @@ class Server(object):
if not os.path.exists(self.cache_dir):
os.mkdir(self.cache_dir)
# conf dir
self.conf_dir = os.path.abspath(kwargs.get('configdir', ''))
vispa.setup_config(self.conf_dir)
......@@ -243,7 +244,8 @@ class Server(object):
if vispa.config('alembic', 'use_alembic', False):
logger.info("Use alembic")
vispa.models.alembic.migrate(self._engine)
if vispa.config('alembic', 'auto_migrate', True):
vispa.models.alembic.migrate(self._engine)
else:
logger.info("Do not use alembic")
sql_base.metadata.create_all(self._engine)
......@@ -349,11 +351,9 @@ class Server(object):
from vispa.tools.permission import PermissionTool
cherrypy.tools.permission = PermissionTool()
try:
if vispa.config('websockets', 'enabled', False):
from ws4py.server.cherrypyserver import WebSocketTool
cherrypy.tools.websocket = WebSocketTool()
except:
pass
def __init_platform(self, **kwargs):
cherrypy.config.update(self.__default_server_config)
......
......@@ -97,8 +97,10 @@ define([
$("#register-alert").fadeIn(100).render({alert: err.message});
$("#register-name").focus();
} else {
if (res.hash) {
window.location.href = self.dynamicURL("password/" + res.hash);
if (res.data.hash) {
setTimeout(function() {
window.location.href = self.url.dynamic("password/" + res.data.hash);
}, 1000);
} else {
$("#register-success").fadeIn(100);
setTimeout(function() {
......
......@@ -565,6 +565,70 @@ define([
});
},
showFeedbackDialog: function() {
var self = this;
if (!this.args.global.useFeedback) {
return this;
}
var $body = $(bodyTmpl);
var $footer = $(footerTmpl);
vispa.messenger.dialog({
header: "<i class='glyphicon glyphicon-comment'></i> Feedback",
body: $body,
footer: $footer,
wrapFooter: false,
onRender: function() {
var dialog = this;
// cancel
$footer.find("button#cancel").click(this.close.bind(this));
// send
$footer.find("button#send").click(function() {
var content = $body.find("textarea").val();
if (content) {
content += "\nhref: " + window.location.href + "\n";
var anonymous = $body.find("input").prop("checked");
self.POST(vispa.url.dynamic("ajax/feedback"), {
content: content,
anonymous: anonymous
});
}
dialog.close();
});
// acquire data from user agent
var uaData = (new UAParser()).getResult();
var bd = uaData.browser;
var ed = uaData.engine;
var od = uaData.os;
var dd = uaData.device;
var uaText = "Your feedback\n\n";
if (bd.name) {
uaText += "Browser: " + bd.name + " " + bd.major + " (" + bd.version + ")\n";
}
if (ed.name) {
uaText += "Engine: " + ed.name + " (" + ed.version + ")\n";
}
if (od.name) {
uaText += "OS: " + od.name + " (" + od.version + ")\n";
}
if (dd.vendor) {
uaText += "Device: " + dd.vendor + " " + dd.type + " (" + dd.model + ")\n";
}
var ta = $body.find("textarea").html(uaText).get(0);
ta.focus();
ta.setSelectionRange(0, 13);
ta.focus();
}
});
return this;
},
nextTick: function(callback, delay) {
window