commit 30ca11109450d46a5dd5546aa4df505dcf0fb480 Author: Eric Schultz Date: Sat Oct 24 10:17:28 2015 -0700 initial commit diff --git a/boot b/boot new file mode 120000 index 000000000..9c73e0588 --- /dev/null +++ b/boot @@ -0,0 +1 @@ +/boot \ No newline at end of file diff --git a/log b/log new file mode 120000 index 000000000..38d1670db --- /dev/null +++ b/log @@ -0,0 +1 @@ +/var/log \ No newline at end of file diff --git a/logging.htm b/logging.htm new file mode 100644 index 000000000..e88ae1191 --- /dev/null +++ b/logging.htm @@ -0,0 +1,55 @@ + + + + + + + + + diff --git a/mnt b/mnt new file mode 120000 index 000000000..cca5abd8e --- /dev/null +++ b/mnt @@ -0,0 +1 @@ +/mnt \ No newline at end of file diff --git a/plugins/dynamix.apcupsd/LICENSE b/plugins/dynamix.apcupsd/LICENSE new file mode 100644 index 000000000..ab0d140e7 --- /dev/null +++ b/plugins/dynamix.apcupsd/LICENSE @@ -0,0 +1,11 @@ +Copyright 2015, by Dan Landon + +This plugin provides APCUPSD support for unRAID V6. The plugin was modified from the original +work done by seeDrs. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 2, +as published by the Free Software Foundation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. diff --git a/plugins/dynamix.apcupsd/UPSdetails.page b/plugins/dynamix.apcupsd/UPSdetails.page new file mode 100644 index 000000000..461cf7b10 --- /dev/null +++ b/plugins/dynamix.apcupsd/UPSdetails.page @@ -0,0 +1,36 @@ +Menu="UPSsettings" +Title="UPS Details" +--- + + + + + + +
KeyValueKeyValue
Please wait, retrieving UPS information...
diff --git a/plugins/dynamix.apcupsd/UPSsettings.page b/plugins/dynamix.apcupsd/UPSsettings.page new file mode 100644 index 000000000..de32e878a --- /dev/null +++ b/plugins/dynamix.apcupsd/UPSsettings.page @@ -0,0 +1,153 @@ +Menu="OtherSettings" +Type="xmenu" +Title="UPS Settings" +Icon="dynamix.apcupsd.png" +--- + + + + + + +style="display:none"> + + +
UPS StatusBattery ChargeRuntime LeftNominal PowerUPS LoadUPS Load %
 
+ +
+ + + Online Manual + +Start APC UPS daemon: +: + +> Set to 'Yes' to enable apcupsd and start the daemon, set to 'No' to disable apcupsd and stop the daemon. + +UPS cable: +: + +> Defines the type of cable connecting the UPS to your computer.Possible generic choices for 'cable' are: +> +> + USB, Simple, Smart, Ether, or Custom to specify a special cable. + +Custom UPS cable: +: + +> Specify a special cable by model number, only applicable when *UPS cable* is set to Custom. +> +> + 940-0119A, 940-0127A, 940-0128A, 940-0020B +> + 940-0020C, 940-0023A, 940-0024B, 940-0024C +> + 940-1524C, 940-0024G, 940-0095A, 940-0095B +> + 940-0095C, 940-0625A, M-04-02-2000 + +UPS type: +: + +> Define a *UPS type*, which corresponds to the type of UPS you have (see the Description for more details). +> +> + **USB** - most new UPSes are USB +> + **APCsmart** - newer serial character device, appropriate for SmartUPS models using a serial cable (not USB) +> + **Net** - network link to a master apcupsd through apcupsd's Network Information Server. This is used if the UPS powering your computer is connected to a different computer for monitoring +> + **SNMP** - SNMP network link to an SNMP-enabled UPS device +> + **Dumb** - old serial character device for use with simple-signaling UPSes +> + **PCnet** - PowerChute Network Shutdown protocol which can be used as an alternative to SNMP with the AP9617 family of smart slot cards +> + **ModBus** - serial device for use with newest SmartUPS models supporting the MODBUS protocol + +Device: +: + +> Enter the *device* which correspondes to your situation, only applicable when *UPS type* is not set to USB. +> +> + **apcsmart** - /dev/tty** +> + **net** - hostname:port. Hostname is the IP address of the NIS server. The deafult port is 3551 +> + **snmp** - hostname:port:vendor:community. Hostname is the ip address or hostname of the UPS on the network. Vendor can be can be "APC" or "APC_NOTRAP". "APC_NOTRAP" will disable SNMP trap catching; you usually want "APC". Port is usually 161. Community is usually "private" +> + **dumb** - /dev/tty** +> + **pcnet** - ipaddr:username:passphrase:port. ipaddr is the IP address of the UPS management card. username and passphrase are the credentials for which the card has been configured. port is the port number on which to listen for messages from the UPS, normally 3052. If this parameter is empty or missing, the default of 3052 will be used +> + **modbus** - /dev/tty** + +Battery level to initiate shutdown (%): +: + +> If during a power failure, the remaining battery percentage (as reported by the UPS) is below or equal to *Battery level*, apcupsd will initiate a system shutdown. + +Runtime left to initiate shutdown (minutes): +: + +> If during a power failure, the remaining runtime in minutes (as calculated internally by the UPS) is below or equal to *minutes*, apcupsd, will initiate a system shutdown. + +Time on battery before shutdown (seconds): +: + +> If during a power failure, the UPS has run on batteries for *time-out* many seconds or longer; apcupsd will initiate a system shutdown. A value of zero disables this timer. +> +> If you have a Smart UPS, you will most likely want to disable this timer by setting it to zero. +> That way, your UPS will continue on batteries until either the % charge remaining drops to or below *Battery level* or the remaining battery runtime drops to or below *minutes*. +> +> Of course - when testing - setting this to 60 causes a quick system shutdown if you pull the power plug. +> If you have an older dumb UPS, you will want to set this to less than the time you know you can run on batteries. +
+> **Note:** *Battery level*, *Runtime left*, and *Time on battery* work in conjunction, so the first that occurs will cause the initiation of a shutdown. + +Turn off UPS after shutdown: +: + +> Set to *Yes* to turn off the power to the UPS after a shutdown. + + +: +
diff --git a/plugins/dynamix.apcupsd/UPSsummary.page b/plugins/dynamix.apcupsd/UPSsummary.page new file mode 100644 index 000000000..ef809de20 --- /dev/null +++ b/plugins/dynamix.apcupsd/UPSsummary.page @@ -0,0 +1,36 @@ +Menu="Dashboard:2" +Title="UPS Summary" +Cond="file_exists('/var/run/apcupsd.pid')" +--- + + + + + + +
UPS StatusBattery ChargeRuntime LeftNominal PowerUPS LoadUPS Load %
 
diff --git a/plugins/dynamix.apcupsd/apcupsd.notify b/plugins/dynamix.apcupsd/apcupsd.notify new file mode 100755 index 000000000..cc0319a10 --- /dev/null +++ b/plugins/dynamix.apcupsd/apcupsd.notify @@ -0,0 +1,5 @@ +# +# Send system notify message from apcupsd +# +read MESSAGE +/usr/local/emhttp/webGui/scripts/notify -e "unRAID Server Alert" -s "UPS Alert" -d "$MESSAGE" -i "alert" diff --git a/plugins/dynamix.apcupsd/default.cfg b/plugins/dynamix.apcupsd/default.cfg new file mode 100644 index 000000000..8ad7bb8c1 --- /dev/null +++ b/plugins/dynamix.apcupsd/default.cfg @@ -0,0 +1,9 @@ +SERVICE="disable" +UPSCABLE="usb" +CUSTOMUPSCABLE="" +UPSTYPE="usb" +DEVICE="" +BATTERYLEVEL="10" +MINUTES="10" +TIMEOUT="0" +KILLUPS="no" diff --git a/plugins/dynamix.apcupsd/event/driver_loaded b/plugins/dynamix.apcupsd/event/driver_loaded new file mode 100755 index 000000000..ab6c7ba10 --- /dev/null +++ b/plugins/dynamix.apcupsd/event/driver_loaded @@ -0,0 +1,30 @@ +#!/bin/bash +conf=/etc/apcupsd/apcupsd.conf +cfg=/boot/config/plugins/dynamix.apcupsd/dynamix.apcupsd.cfg + +# Daemon already running or no custom file? +[[ -f /var/run/apcupsd.pid || ! -f $cfg ]] && exit + +# Read settings +source $cfg + +# Apply settings +sed -i -e '/^NISIP/c\\NISIP 0.0.0.0' $conf +sed -i -e '/^UPSTYPE/c\\UPSTYPE '$UPSTYPE'' $conf +sed -i -e '/^DEVICE/c\\DEVICE '$DEVICE'' $conf +sed -i -e '/^BATTERYLEVEL/c\\BATTERYLEVEL '$BATTERYLEVEL'' $conf +sed -i -e '/^MINUTES/c\\MINUTES '$MINUTES'' $conf +sed -i -e '/^TIMEOUT/c\\TIMEOUT '$TIMEOUT'' $conf +if [[ $UPSCABLE == custom ]]; then + sed -i -e '/^UPSCABLE/c\\UPSCABLE '$CUSTOMUPSCABLE'' $conf +else + sed -i -e '/^UPSCABLE/c\\UPSCABLE '$UPSCABLE'' $conf +fi +if [[ $KILLUPS == yes && $SERVICE == enable ]]; then + ! grep -q apccontrol /etc/rc.d/rc.6 && sed -i -e 's:/sbin/poweroff:/etc/apcupsd/apccontrol killpower; /sbin/poweroff:' /etc/rc.d/rc.6 +else + grep -q apccontrol /etc/rc.d/rc.6 && sed -i -e 's:/etc/apcupsd/apccontrol killpower; /sbin/poweroff:/sbin/poweroff:' /etc/rc.d/rc.6 +fi + +# Start daemon +[[ $SERVICE == enable ]] && /etc/rc.d/rc.apcupsd start |& logger diff --git a/plugins/dynamix.apcupsd/icons/upsdetails.png b/plugins/dynamix.apcupsd/icons/upsdetails.png new file mode 100644 index 000000000..55b2a5997 Binary files /dev/null and b/plugins/dynamix.apcupsd/icons/upsdetails.png differ diff --git a/plugins/dynamix.apcupsd/icons/upssettings.png b/plugins/dynamix.apcupsd/icons/upssettings.png new file mode 100644 index 000000000..db3bfcd9e Binary files /dev/null and b/plugins/dynamix.apcupsd/icons/upssettings.png differ diff --git a/plugins/dynamix.apcupsd/icons/upssummary.png b/plugins/dynamix.apcupsd/icons/upssummary.png new file mode 100644 index 000000000..db3bfcd9e Binary files /dev/null and b/plugins/dynamix.apcupsd/icons/upssummary.png differ diff --git a/plugins/dynamix.apcupsd/images/dynamix.apcupsd.png b/plugins/dynamix.apcupsd/images/dynamix.apcupsd.png new file mode 100644 index 000000000..a854e1de1 Binary files /dev/null and b/plugins/dynamix.apcupsd/images/dynamix.apcupsd.png differ diff --git a/plugins/dynamix.apcupsd/include/UPSstatus.php b/plugins/dynamix.apcupsd/include/UPSstatus.php new file mode 100644 index 000000000..23e519f31 --- /dev/null +++ b/plugins/dynamix.apcupsd/include/UPSstatus.php @@ -0,0 +1,69 @@ + + 'Online (trim)', + 'BOOST ONLINE' => 'Online (boost)', + 'ONLINE' => 'Online', + 'ONBATT' => 'On battery', + 'COMMLOST' => 'Lost communication', + 'NOBATT' => 'No battery detected' +]; + +$red = "class='red-text'"; +$green = "class='green-text'"; +$orange = "class='orange-text'"; +$status = array_fill(0,6,"-"); +$all = $_GET['all']=='true'; +$result = array(); + +if (file_exists("/var/run/apcupsd.pid")) { + exec("/sbin/apcaccess 2>/dev/null", $rows); + for ($i=0; $i$val" : "$val") : "Refreshing..."; + break; + case 'BCHARGE': + $status[1] = strtok($val,' ')<=10 ? "$val" : "$val"; + break; + case 'TIMELEFT': + $status[2] = strtok($val,' ')<=5 ? "$val" : "$val"; + break; + case 'NOMPOWER': + $power = strtok($val,' '); + $status[3] = $power==0 ? "$val" : "$val"; + break; + case 'LOADPCT': + $load = strtok($val,' '); + $status[5] = $load>=90 ? "$val" : "$val"; + break; + } + if ($all) { + if ($i%2==0) $result[] = ""; + $result[]= "$key$val"; + if ($i%2==1) $result[] = ""; + } + } + if ($all && count($rows)%2==1) $result[] = ""; + if ($power && $load) $status[4] = ($load>=90 ? "" : "").intval($power*$load/100)." Watts"; +} +if ($all && !$rows) $result[] = "
No information available
"; + +echo "".implode('', $status).""; +if ($all) echo "\n".implode('', $result); +?> diff --git a/plugins/dynamix.apcupsd/include/update.apcupsd.php b/plugins/dynamix.apcupsd/include/update.apcupsd.php new file mode 100644 index 000000000..0def9fa87 --- /dev/null +++ b/plugins/dynamix.apcupsd/include/update.apcupsd.php @@ -0,0 +1,34 @@ + + diff --git a/plugins/dynamix.docker.manager/AddContainer.page b/plugins/dynamix.docker.manager/AddContainer.page new file mode 100644 index 000000000..0840d9fcc --- /dev/null +++ b/plugins/dynamix.docker.manager/AddContainer.page @@ -0,0 +1,22 @@ +Title="Add Container" +Cond="(pgrep('docker')!==false)" +Markdown="false" +--- + +
+ Advanced View +
+ \ No newline at end of file diff --git a/plugins/dynamix.docker.manager/Docker.page b/plugins/dynamix.docker.manager/Docker.page new file mode 100644 index 000000000..360c5e05c --- /dev/null +++ b/plugins/dynamix.docker.manager/Docker.page @@ -0,0 +1,3 @@ +Menu="Tasks:60" +Type="xmenu" +Cond="(pgrep('docker')!==false)" \ No newline at end of file diff --git a/plugins/dynamix.docker.manager/DockerContainers.page b/plugins/dynamix.docker.manager/DockerContainers.page new file mode 100644 index 000000000..604f7970c --- /dev/null +++ b/plugins/dynamix.docker.manager/DockerContainers.page @@ -0,0 +1,247 @@ +Menu="Docker:1" +Title="Docker Containers" +Cond="(pgrep('docker')!==false)" +Markdown="false" +--- + + + + + + + +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + getDockerContainers(); + if ( ! $all_containers) { + $all_containers = array(); + echo ""; + } + $info = $DockerTemplates->getAllInfo(); + $contextMenus = array(); + + $IP = $var["IPADDR"]; + foreach($all_containers as $ct){ + $name = $ct["Name"]; + $is_autostart = ( $info[$name]['autostart'] ) ? 'true' : 'false'; + $updateStatus = $info[$name]['updated']; + $updateStatus = ($updateStatus == "true" or $updateStatus == "undef" ) ? 'true' : 'false'; + $running = ($ct['Running']) ? 'true' : 'false'; + $webGuiUrl = $info[$name]['url']; + $contextMenus[] = sprintf("addDockerContainerContext('%s', '%s', '%s', %s, %s, %s, '%s');", addslashes($ct['Name']), addslashes($ct['ImageId']), addslashes($info[$name]['template']), $running, $updateStatus, $is_autostart, addslashes($webGuiUrl)); + $shape = ($ct["Running"]) ? "play" : "square"; + $status = ($ct["Running"]) ? "started" : "stopped"; + + $Icon = $info[$name]['icon']; + if ( $Icon == "#" ){ + $Icon = "/plugins/dynamix.docker.manager/images/question.png"; + } + + $ports = array(); + foreach ($ct['Ports'] as $p) { + if (strlen($p['PublicPort'])){ + $ipAddr = sprintf("%s:%s", $IP, $p['PublicPort']); + $outFormat = sprintf('%s/%s %s', $ipAddr, $p['PrivatePort'], $p['Type'], htmlspecialchars($ipAddr)); + } else { + $outFormat = sprintf("%s/%s", $p['PrivatePort'], $p['Type']); + } + $ports[] = $outFormat; + } + $paths = array(); + if (count($ct['Volumes'])){ + foreach ($ct['Volumes'] as $value) { + if (preg_match('/localtime/', $value) == TRUE){ continue; } + list($host_path, $container_path, $access_mode) = explode(":", $value); + + $tip = 'Container volume \'' . $container_path . '\' has ' . ($access_mode == 'ro' ? 'read-only' : 'read-write') . ' access to Host path \'' . $host_path . '\''; + + $paths[] = sprintf('%s %s', urlencode($host_path), htmlspecialchars($tip), htmlspecialchars($container_path), ($access_mode == 'ro' ? 'long-arrow-left' : 'arrows-h'), htmlspecialchars($host_path)); + } + } + ?> + + + + + + + + + + + + + + getDockerImages(); + if ( ! $all_images) { $all_images = array(); } + + foreach($all_images as $image){ + if (count($image['usedBy'])) { + continue; + } + + $contextMenus[] = sprintf("addDockerImageContext('%s', '%s');", $image['Id'], implode(', ', $image['Tags'])); + + ?> + + + + + + + + + + + + +
 ApplicationAuthor / RepoVersionPort Mappings (App to Host)Volume Mappings (App to Host)AutostartLog
No Docker Containers Installed
+ +
+ + +
+ "; + + ?> +
+ + + + + +
Container ID:
+
+ %s", htmlspecialchars($Registry), htmlspecialchars($ct['Image']) ); + } else { + echo htmlspecialchars($ct['Image']); + } + ?> + + update ready"; + } else if ($updateStatus == "true"){ + echo " up-to-date"; + echo ""; + } else { + echo " not available"; + echo ""; + } + ?> + ", $ports); ?>", $paths); ?>>
Created
+ +
+ +
+ "; + + ?> +
+ (orphan image) +
Image ID:
+
", $image['Tags']);?>    
Created
+ + + + + + + diff --git a/plugins/dynamix.docker.manager/DockerRepositories.page b/plugins/dynamix.docker.manager/DockerRepositories.page new file mode 100644 index 000000000..cae401b17 --- /dev/null +++ b/plugins/dynamix.docker.manager/DockerRepositories.page @@ -0,0 +1,39 @@ +Menu="Docker:2" +Title="Docker Repositories" +Cond="(pgrep('docker')!==false)" +--- + + +
+ +Template repositories: +: + +> Use this field to add template repositories. +> Docker templates are used to facilitate the creation and re-creation of Docker containers. Please setup one per line. +> +> For a list of popular community-supported repositories, visit here: http://lime-technology.com/forum/index.php?topic=37958.0 + +  +: +
diff --git a/plugins/dynamix.docker.manager/DockerSettings.page b/plugins/dynamix.docker.manager/DockerSettings.page new file mode 100644 index 000000000..2ba7bbbbe --- /dev/null +++ b/plugins/dynamix.docker.manager/DockerSettings.page @@ -0,0 +1,233 @@ +Menu="OtherSettings" +Title="Docker" +Icon="dynamix.docker.manager.png" +--- + +Array must be Started to manage Docker.

"; + return; +} + +// Add the Docker JSON client +require_once('/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerClient.php'); +$docker = new DockerClient(); +$DockerUpdate = new DockerUpdate(); +$DockerTemplates = new DockerTemplates(); + +// Docker configuration file +$cfgfile = "/boot/config/docker.cfg"; + +if (!file_exists($cfgfile)) { + echo "

Missing docker.cfg file!

"; + return; +} +$dockercfg = parse_ini_file($cfgfile); +if (!array_key_exists('DOCKER_ENABLED', $dockercfg)) { + $dockercfg['DOCKER_ENABLED'] = 'no'; +} + +// Check for nodatacow flag on Docker file; display warning +$realfile = $dockercfg['DOCKER_IMAGE_FILE']; +if (file_exists($realfile)) { + if (strpos($realfile, '/mnt/user/') === 0) { + $tmp = parse_ini_string(shell_exec("getfattr --absolute-names -n user.LOCATION " . escapeshellarg($dockercfg['DOCKER_IMAGE_FILE']) . " | grep user.LOCATION")); + $realfile = str_replace('user', $tmp['user.LOCATION'], $realfile); // replace 'user' with say 'cache' or 'disk1' etc + } + + if (exec("stat -c %T -f " . escapeshellarg($realfile)) == "btrfs") { + if (shell_exec("lsattr " . escapeshellarg($realfile) . " | grep \"\\-C\"") == "") { + echo '

Your existing Docker image file needs to be recreated due to an issue from an earlier beta of unRAID 6. Failure to do so may result in your docker image suffering corruption at a later time. Please do this NOW!

'; + } + } +} +?> + + + +
+ + + +Enable Docker: +: + +> Before you can start the Docker service for the first time, please specify an image +> file for Docker to install to. Once started, Docker will always automatically start +> after the array has been started. + +Default image size: +: GB + +> If the system needs to create a new docker image file, this is the default size to use +> specified in GB. +> +> To resize an existing image file, specify the new size here. Next time the Docker service is +> started the file (and file system) will increased to the new size (but never decreased). + +Docker image: +: + +> You must specify an image file for Docker. The system will automatically +> create this file when the Docker service is first started. If you do not want Docker +> to run at all, set this field blank and click **Start**. + +  +: + +
+ +
+ + + +Enable Docker: +: + +> Stopping the Docker service will first stop all the running containers. + +Docker version: +: getInfo(); echo $arrInfo['Version']; ?> + +> This is the docker version. + +Docker image: +: + +> This is the docker volume. + +  +: + +
+
Docker volume info
+ +btrfs filesystem show: +: ".shell_exec("btrfs filesystem show /var/lib/docker")."";?> + +
+ + +btrfs scrub status: +: " . implode("\n", $scrub_status) . "";?> + + + + + + +  +: + +> **Scrub** runs the *btrfs scrub* program to check file system integrity. +> +> If repair is needed you should uncheck the *Don't fix file system errors* option and +> run a second Scrub pass; this will permit *btrfs scrub* to fix the file system. + + + + + + +  +: *Running* + +> **Cancel** will cancel the Scrub operation in progress. + +
+ + + + diff --git a/plugins/dynamix.docker.manager/LICENSE b/plugins/dynamix.docker.manager/LICENSE new file mode 100644 index 000000000..e7c8957ab --- /dev/null +++ b/plugins/dynamix.docker.manager/LICENSE @@ -0,0 +1,12 @@ +EXTENDED DOCKER CONFIGURATION PAGE +Copyright (C) 2014 Guilherme Jardim + + +FORKED FROM: +**Copyright 2005-2014 Lime Technology.** + +This Software is licensed under [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html). + +unRAID is a registered trademark of [Lime Technology](http://lime-technology.com). + +This file shall be included in all copies or substantial portions of the Software. \ No newline at end of file diff --git a/plugins/dynamix.docker.manager/UpdateContainer.page b/plugins/dynamix.docker.manager/UpdateContainer.page new file mode 100644 index 000000000..70955f4c1 --- /dev/null +++ b/plugins/dynamix.docker.manager/UpdateContainer.page @@ -0,0 +1,22 @@ +Title="Update Container" +Cond="(pgrep('docker')!==false)" +Markdown="false" +--- + +
+ Advanced View +
+ \ No newline at end of file diff --git a/plugins/dynamix.docker.manager/event/started b/plugins/dynamix.docker.manager/event/started new file mode 100755 index 000000000..12cae489a --- /dev/null +++ b/plugins/dynamix.docker.manager/event/started @@ -0,0 +1,11 @@ +#!/bin/sh + +# Only start if array has started in Normal operation mode +if grep -q 'fsState="Started"' /var/local/emhttp/var.ini && grep -q 'startMode="Normal"' /var/local/emhttp/var.ini; then + # Start Docker.io + if [ -x /etc/rc.d/rc.docker ]; then + echo "Starting Docker..." + /etc/rc.d/rc.docker start | logger + /usr/local/emhttp/plugins/dynamix.docker.manager/scripts/dockerupdate.php | logger + fi +fi diff --git a/plugins/dynamix.docker.manager/event/stopping_svcs b/plugins/dynamix.docker.manager/event/stopping_svcs new file mode 100755 index 000000000..4a8c75e23 --- /dev/null +++ b/plugins/dynamix.docker.manager/event/stopping_svcs @@ -0,0 +1,7 @@ +#!/bin/sh + +# Shutdown Docker.io +if [ -x /etc/rc.d/rc.docker ]; then + echo "Stopping Docker..." + /etc/rc.d/rc.docker stop | logger +fi diff --git a/plugins/dynamix.docker.manager/icons/addcontainer.png b/plugins/dynamix.docker.manager/icons/addcontainer.png new file mode 100644 index 000000000..9c8a9da4a Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/addcontainer.png differ diff --git a/plugins/dynamix.docker.manager/icons/default.png b/plugins/dynamix.docker.manager/icons/default.png new file mode 100644 index 000000000..50b991af3 Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/default.png differ diff --git a/plugins/dynamix.docker.manager/icons/docker.png b/plugins/dynamix.docker.manager/icons/docker.png new file mode 100644 index 000000000..50b991af3 Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/docker.png differ diff --git a/plugins/dynamix.docker.manager/icons/dockercontainers.png b/plugins/dynamix.docker.manager/icons/dockercontainers.png new file mode 100644 index 000000000..da3c2a2d7 Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/dockercontainers.png differ diff --git a/plugins/dynamix.docker.manager/icons/dockerrepositories.png b/plugins/dynamix.docker.manager/icons/dockerrepositories.png new file mode 100644 index 000000000..44084add7 Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/dockerrepositories.png differ diff --git a/plugins/dynamix.docker.manager/icons/extraparams.png b/plugins/dynamix.docker.manager/icons/extraparams.png new file mode 100644 index 000000000..6332fefea Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/extraparams.png differ diff --git a/plugins/dynamix.docker.manager/icons/network.png b/plugins/dynamix.docker.manager/icons/network.png new file mode 100644 index 000000000..c32d25c16 Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/network.png differ diff --git a/plugins/dynamix.docker.manager/icons/paths.png b/plugins/dynamix.docker.manager/icons/paths.png new file mode 100644 index 000000000..b9b75f6c3 Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/paths.png differ diff --git a/plugins/dynamix.docker.manager/icons/preferences.png b/plugins/dynamix.docker.manager/icons/preferences.png new file mode 100644 index 000000000..f67fcb0ae Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/preferences.png differ diff --git a/plugins/dynamix.docker.manager/icons/updatecontainer.png b/plugins/dynamix.docker.manager/icons/updatecontainer.png new file mode 100644 index 000000000..25b28bb6a Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/updatecontainer.png differ diff --git a/plugins/dynamix.docker.manager/icons/vcard.png b/plugins/dynamix.docker.manager/icons/vcard.png new file mode 100644 index 000000000..c02f315d2 Binary files /dev/null and b/plugins/dynamix.docker.manager/icons/vcard.png differ diff --git a/plugins/dynamix.docker.manager/images/dynamix.docker.manager.png b/plugins/dynamix.docker.manager/images/dynamix.docker.manager.png new file mode 100644 index 000000000..34506d41b Binary files /dev/null and b/plugins/dynamix.docker.manager/images/dynamix.docker.manager.png differ diff --git a/plugins/dynamix.docker.manager/images/plus.png b/plugins/dynamix.docker.manager/images/plus.png new file mode 100644 index 000000000..21ac0c1f9 Binary files /dev/null and b/plugins/dynamix.docker.manager/images/plus.png differ diff --git a/plugins/dynamix.docker.manager/images/question.png b/plugins/dynamix.docker.manager/images/question.png new file mode 100644 index 000000000..b894b6f24 Binary files /dev/null and b/plugins/dynamix.docker.manager/images/question.png differ diff --git a/plugins/dynamix.docker.manager/images/reload.png b/plugins/dynamix.docker.manager/images/reload.png new file mode 100644 index 000000000..79f278736 Binary files /dev/null and b/plugins/dynamix.docker.manager/images/reload.png differ diff --git a/plugins/dynamix.docker.manager/images/remove.png b/plugins/dynamix.docker.manager/images/remove.png new file mode 100644 index 000000000..accd3f0b6 Binary files /dev/null and b/plugins/dynamix.docker.manager/images/remove.png differ diff --git a/plugins/dynamix.docker.manager/images/spacer.png b/plugins/dynamix.docker.manager/images/spacer.png new file mode 100644 index 000000000..dd3f7d41d Binary files /dev/null and b/plugins/dynamix.docker.manager/images/spacer.png differ diff --git a/plugins/dynamix.docker.manager/include/CreateDocker.php b/plugins/dynamix.docker.manager/include/CreateDocker.php new file mode 100644 index 000000000..b697ea724 --- /dev/null +++ b/plugins/dynamix.docker.manager/include/CreateDocker.php @@ -0,0 +1,940 @@ + +getDockerContainers(); + if ( ! $all_containers) { return FALSE; } + foreach ($all_containers as $ct) { + if ($ct['Name'] == $container){ + return True; + break; + } + } + return False; +} + +function trimLine($text){ + return preg_replace("/([\n^])[\s]+/", '$1', $text); +} + +function pullImage($image) { + if (! preg_match("/:[\w]*$/i", $image)) $image .= ":latest"; + readfile("/usr/local/emhttp/plugins/dynamix.docker.manager/log.htm"); + echo ''; + echo ""; + @flush(); + + $fp = stream_socket_client('unix:///var/run/docker.sock', $errno, $errstr); + if ($fp === false) { + echo "Couldn't create socket: [$errno] $errstr"; + return NULL; + } + $out="POST /images/create?fromImage=$image HTTP/1.1\r\nConnection: Close\r\n\r\n"; + fwrite($fp, $out); + $cid = ""; + $cstatus=""; + $alltotals = []; + while (!feof($fp)) { + $cnt = json_decode( fgets($fp, 5000), TRUE ); + $id = ( isset( $cnt['id'] )) ? $cnt['id'] : ""; + if ($id != $cid && strlen($id)) { + $cid = $id; + $cstatus = ""; + echo ""; + @flush(); + } + $status = ( isset( $cnt['status'] )) ? $cnt['status'] : ""; + if ($status != $cstatus && strlen($status)) { + if ( isset($cnt['progressDetail']['total']) && $cnt['progressDetail']['total'] > 0 ) { + $alltotals[$cnt['id']] = $cnt['progressDetail']['total']; + } + $cstatus = $status; + echo ""; + @flush(); + } + if ($status == "Downloading") { + $total = $cnt['progressDetail']['total']; + $current = $cnt['progressDetail']['current']; + $alltotals[$cnt['id']] = $cnt['progressDetail']['current']; + if ($total > 0) { + $percentage = round(($current/$total) * 100); + echo "\n"; + } else { + // Docker must not know the total download size (http-chunked or something?) + // just show the current download progress without the percentage + echo "\n"; + } + @flush(); + } + } + echo "\n"; +} + +function sizeToHuman($size) { + $units = ['B','KB','MB','GB']; + $unitsIndex = 0; + while ($size > 1024 && (($unitsIndex+1) < count($units))) { + $size /= 1024; + $unitsIndex++; + } + return ceil($size) . " " . $units[$unitsIndex]; +} + +function xmlToCommand($xmlFile){ + global $var; + $doc = new DOMDocument(); + $doc->loadXML($xmlFile); + + $Name = $doc->getElementsByTagName( "Name" )->item(0)->nodeValue; + $cmdName = (strlen($Name)) ? '--name="' . $Name . '"' : ""; + $Privileged = $doc->getElementsByTagName( "Privileged" )->item(0)->nodeValue; + $cmdPrivileged = (strtolower($Privileged) == 'true') ? '--privileged="true"' : ""; + $Repository = $doc->getElementsByTagName( "Repository" )->item(0)->nodeValue; + $Mode = $doc->getElementsByTagName( "Mode" )->item(0)->nodeValue; + $cmdMode = '--net="'.strtolower($Mode).'"'; + $BindTime = $doc->getElementsByTagName( "BindTime" )->item(0)->nodeValue; + // $cmdBindTime = (strtolower($BindTime) == "true") ? '"/etc/localtime":"/etc/localtime":ro' : ''; + $cmdBindTime = (strtolower($BindTime) == "true") ? 'TZ="' . $var['timeZone'] . '"' : ''; + + $Ports = array(''); + foreach($doc->getElementsByTagName('Port') as $port){ + $ContainerPort = $port->getElementsByTagName( "ContainerPort" )->item(0)->nodeValue; + if (! strlen($ContainerPort)){ continue; } + $HostPort = $port->getElementsByTagName( "HostPort" )->item(0)->nodeValue; + $Protocol = $port->getElementsByTagName( "Protocol" )->item(0)->nodeValue; + $Ports[] = sprintf("%s:%s/%s", $HostPort, $ContainerPort, $Protocol); + } + + $Volumes = array(''); + foreach($doc->getElementsByTagName('Volume') as $volume){ + $ContainerDir = $volume->getElementsByTagName( "ContainerDir" )->item(0)->nodeValue; + if (! strlen($ContainerDir)){ continue; } + $HostDir = $volume->getElementsByTagName( "HostDir" )->item(0)->nodeValue; + $DirMode = $volume->getElementsByTagName( "Mode" )->item(0)->nodeValue; + $Volumes[] = sprintf( '"%s":"%s":%s', $HostDir, $ContainerDir, $DirMode); + } + + // if (strlen($cmdBindTime)) { + // $Volumes[] = $cmdBindTime; + // } + + $Variables = array(''); + foreach($doc->getElementsByTagName('Variable') as $variable){ + $VariableName = $variable->getElementsByTagName( "Name" )->item(0)->nodeValue; + if (! strlen($VariableName)){ continue; } + $VariableValue = $variable->getElementsByTagName( "Value" )->item(0)->nodeValue; + $Variables[] = sprintf('%s="%s"', $VariableName, $VariableValue); + } + + if (strlen($cmdBindTime)) { + $Variables[] = $cmdBindTime; + } + + $templateExtraParams = ''; + if ( $doc->getElementsByTagName( "ExtraParams" )->length > 0 ) { + $templateExtraParams = $doc->getElementsByTagName( "ExtraParams" )->item(0)->nodeValue; + } + + $cmd = sprintf('/plugins/dynamix.docker.manager/scripts/docker run -d %s %s %s %s %s %s %s %s', $cmdName, $cmdMode, $cmdPrivileged, implode(' -e ', $Variables), + implode(' -p ', $Ports), implode(' -v ', $Volumes), $templateExtraParams, $Repository); + $cmd = preg_replace('/\s+/', ' ', $cmd); + + return array($cmd, $Name, $Repository); +} + +function addElement($doc, $el, $elName, $elVal){ + $node = $el->appendChild($doc->createElement($elName)); + $node->appendChild($doc->createTextNode($elVal)); + return $node; +} + +function postToXML($post, $setOwnership = FALSE){ + global $DockerUpdate; + $doc = new DOMDocument('1.0', 'utf-8'); + $doc->preserveWhiteSpace = false; + $doc->formatOutput = true; + $root = $doc->createElement('Container'); + $root = $doc->appendChild($root); + + $docName = $root->appendChild($doc->createElement('Name')); + if ( isset( $post[ 'Description' ] )) addElement($doc, $root, 'Description', $post[ 'Description' ]); + if ( isset( $post[ 'Registry' ] )) addElement($doc, $root, 'Registry', $post[ 'Registry' ]); + $docRepository = $root->appendChild($doc->createElement('Repository')); + $BindTime = $root->appendChild($doc->createElement('BindTime')); + $Privileged = $root->appendChild($doc->createElement('Privileged')); + $Environment = $root->appendChild($doc->createElement('Environment')); + $docNetworking = $root->appendChild($doc->createElement('Networking')); + $Data = $root->appendChild($doc->createElement('Data')); + $Version = $root->appendChild($doc->createElement('Version')); + $Mode = $docNetworking->appendChild($doc->createElement('Mode')); + $Publish = $docNetworking->appendChild($doc->createElement('Publish')); + $Name = preg_replace('/\s+/', '', $post["containerName"]); + + // Editor Values + if ( isset( $post[ 'WebUI' ] )) addElement($doc, $root, 'WebUI', $post[ 'WebUI' ]); + if ( isset( $post[ 'Banner' ] )) addElement($doc, $root, 'Banner', $post[ 'Banner' ]); + if ( isset( $post[ 'Icon' ] )) addElement($doc, $root, 'Icon', $post[ 'Icon' ]); + + if ( isset( $post[ 'ExtraParams' ] )) addElement($doc, $root, 'ExtraParams', $post[ 'ExtraParams' ]); + + $docName->appendChild($doc->createTextNode($Name)); + $docRepository->appendChild($doc->createTextNode($post["Repository"])); + $BindTime->appendChild($doc->createTextNode((strtolower($post["BindTime"]) == 'on') ? 'true' : 'false')); + $Privileged->appendChild($doc->createTextNode((strtolower($post["Privileged"]) == 'on') ? 'true' : 'false')); + $Mode->appendChild($doc->createTextNode(strtolower($post["NetworkType"]))); + + for ($i = 0; $i < count($post["hostPort"]); $i++){ + if (! strlen($post["containerPort"][$i])) { continue; } + $protocol = $post["portProtocol"][$i]; + $Port = $Publish->appendChild($doc->createElement('Port')); + $HostPort = $Port->appendChild($doc->createElement('HostPort')); + $ContainerPort = $Port->appendChild($doc->createElement('ContainerPort')); + $Protocol = $Port->appendChild($doc->createElement('Protocol')); + $HostPort->appendChild($doc->createTextNode(trim($post["hostPort"][$i]))); + $ContainerPort->appendChild($doc->createTextNode($post["containerPort"][$i])); + $Protocol->appendChild($doc->createTextNode($protocol)); + } + + for ($i = 0; $i < count($post["VariableName"]); $i++){ + if (! strlen($post["VariableName"][$i])) { continue; } + $Variable = $Environment->appendChild($doc->createElement('Variable')); + $VariableName = $Variable->appendChild($doc->createElement('Name')); + $VariableValue = $Variable->appendChild($doc->createElement('Value')); + $VariableName->appendChild($doc->createTextNode(trim($post["VariableName"][$i]))); + $VariableValue->appendChild($doc->createTextNode(trim($post["VariableValue"][$i]))); + } + + for ($i = 0; $i < count($post["hostPath"]); $i++){ + if (! strlen($post["hostPath"][$i])) { continue; } + if (! strlen($post["containerPath"][$i])) { continue; } + $tmpMode = $post["hostWritable"][$i]; + if ($setOwnership){ + prepareDir($post["hostPath"][$i]); + } + $Volume = $Data->appendChild($doc->createElement('Volume')); + $HostDir = $Volume->appendChild($doc->createElement('HostDir')); + $ContainerDir = $Volume->appendChild($doc->createElement('ContainerDir')); + $DirMode = $Volume->appendChild($doc->createElement('Mode')); + $HostDir->appendChild($doc->createTextNode($post["hostPath"][$i])); + $ContainerDir->appendChild($doc->createTextNode($post["containerPath"][$i])); + $DirMode->appendChild($doc->createTextNode($tmpMode)); + } + + $currentVersion = $DockerUpdate->getRemoteVersion($post["Registry"], $post["Repository"]); + $Version->appendChild($doc->createTextNode($currentVersion)); + + return $doc->saveXML(); +} + +if ($_POST){ + + $postXML = postToXML($_POST, TRUE); + + // Get the command line + list($cmd, $Name, $Repository) = xmlToCommand($postXML); + + // Saving the generated configuration file. + $userTmplDir = $dockerManPaths['templates-user']; + if(is_dir($userTmplDir) === FALSE){ + mkdir($userTmplDir, 0777, true); + } + + if(strlen($Name)) { + $filename = sprintf('%s/my-%s.xml', $userTmplDir, $Name); + file_put_contents($filename, $postXML); + } + + // Pull image + pullImage($Repository); + + // Remove existing container + if (ContainerExist($Name)){ + $_GET['cmd'] = "/plugins/dynamix.docker.manager/scripts/docker rm -f $Name"; + include($dockerManPaths['plugin'] . "/include/Exec.php"); + } + + // Remove old container if renamed + $existing = isset($_POST['existingContainer']) ? $_POST['existingContainer'] : FALSE; + if ($existing && ContainerExist($existing)){ + $_GET['cmd'] = "/plugins/dynamix.docker.manager/scripts/docker rm -f $existing"; + include($dockerManPaths['plugin'] . "/include/Exec.php"); + } + + // Injecting the command in $_GET variable and executing. + $_GET['cmd'] = $cmd; + include($dockerManPaths['plugin'] . "/include/Exec.php"); + + $DockerTemplates->removeInfo($Name); + $DockerUpdate->syncVersions($Name); + + echo '

'; + die(); +} + + +if ($_GET['updateContainer']){ + foreach ($_GET['ct'] as $value) { + $Name = urldecode($value); + $tmpl = $DockerTemplates->getUserTemplate($Name); + + if (! $tmpl){ + echo 'Configuration not found. Was this container created using this plugin?'; + continue; + } + + $doc = new DOMDocument('1.0', 'utf-8'); + $doc->preserveWhiteSpace = false; + $doc->load( $tmpl ); + $doc->formatOutput = TRUE; + + $Repository = $doc->getElementsByTagName( "Repository" )->item(0)->nodeValue; + $Registry = $doc->getElementsByTagName( "Registry" )->item(0)->nodeValue; + + readfile("/usr/local/emhttp/plugins/dynamix.docker.manager/log.htm"); + echo ""; + @flush(); + + $CurrentVersion = $DockerUpdate->getRemoteVersion($Registry, $Repository); + + if ($CurrentVersion){ + if ( $doc->getElementsByTagName( "Version" )->length == 0 ) { + $root = $doc->getElementsByTagName( "Container" )->item(0); + $Version = $root->appendChild($doc->createElement('Version')); + } else { + $Version = $doc->getElementsByTagName( "Version" )->item(0); + } + $Version->nodeValue = $CurrentVersion; + + file_put_contents($tmpl, $doc->saveXML()); + } + + $oldContainerID = $DockerClient->getImageID($Repository); + list($cmd, $Name, $Repository) = xmlToCommand($doc->saveXML()); + + // Pull image + flush(); + pullImage($Repository); + + $_GET['cmd'] = "/plugins/dynamix.docker.manager/scripts/docker rm -f $Name"; + include($dockerManPaths['plugin'] . "/include/Exec.php"); + + $_GET['cmd'] = $cmd; + include($dockerManPaths['plugin'] . "/include/Exec.php"); + + $DockerTemplates->removeInfo($Name); + $newContainerID = $DockerClient->getImageID($Repository); + if ( $oldContainerID and $oldContainerID != $newContainerID){ + $_GET['cmd'] = sprintf("/plugins/dynamix.docker.manager/scripts/docker rmi %s", $oldContainerID); + include($dockerManPaths['plugin'] . "/include/Exec.php"); + } + + $DockerTemplates->removeInfo($Name); + $DockerUpdate->syncVersions($Name); + } + + echo '

'; + die(); +} + + +if($_GET['rmTemplate']){ + unlink($_GET['rmTemplate']); +} + +if($_GET['xmlTemplate']){ + list($xmlType, $xmlTemplate) = split(':', urldecode($_GET['xmlTemplate'])); + if(is_file($xmlTemplate)){ + $doc = new DOMDocument(); + $doc->load($xmlTemplate); + + $templateRepository = $doc->getElementsByTagName( "Repository" )->item(0)->nodeValue; + $templateName = $doc->getElementsByTagName( "Name" )->item(0)->nodeValue; + $Registry = $doc->getElementsByTagName( "Registry" )->item(0)->nodeValue; + $templatePrivileged = (strtolower($doc->getElementsByTagName( "Privileged" )->item(0)->nodeValue) == 'true') ? 'checked' : ""; + $templateMode = $doc->getElementsByTagName( "Mode" )->item(0)->nodeValue;; + $readonly = ($xmlType == 'default') ? 'readonly="readonly"' : ''; + $required = ($xmlType == 'default') ? 'required' : ''; + $disabled = ($xmlType == 'default') ? 'disabled="disabled"' : ''; + + if ( $doc->getElementsByTagName( "Description" )->length > 0 ) { + $templateDescription = $doc->getElementsByTagName( "Description" )->item(0)->nodeValue; + } else { + $templateDescription = $DockerTemplates->getTemplateValue($templateRepository, "Description", "default"); + } + + if ( $doc->getElementsByTagName( "Registry" )->length > 0 ) { + $templateRegistry = $doc->getElementsByTagName( "Registry" )->item(0)->nodeValue; + } else { + $templateRegistry = $DockerTemplates->getTemplateValue($templateRepository, "Registry", "default"); + } + + if ( $doc->getElementsByTagName( "WebUI" )->length > 0 ) { + $templateWebUI = $doc->getElementsByTagName( "WebUI" )->item(0)->nodeValue; + } else { + $templateWebUI = $DockerTemplates->getTemplateValue($templateRepository, "WebUI", "default"); + } + + if ( $doc->getElementsByTagName( "Banner" )->length > 0 ) { + $templateBanner = $doc->getElementsByTagName( "Banner" )->item(0)->nodeValue; + } else { + $templateBanner = $DockerTemplates->getTemplateValue($templateRepository, "Banner", "default"); + } + + if ( $doc->getElementsByTagName( "Icon" )->length > 0 ) { + $templateIcon = $doc->getElementsByTagName( "Icon" )->item(0)->nodeValue; + } else { + $templateIcon = $DockerTemplates->getTemplateValue($templateRepository, "Icon", "default"); + } + + if ( $doc->getElementsByTagName( "ExtraParams" )->length > 0 ) { + $templateExtraParams = $doc->getElementsByTagName( "ExtraParams" )->item(0)->nodeValue; + } else { + $templateExtraParams = $DockerTemplates->getTemplateValue($templateRepository, "ExtraParams", "default"); + } + + $templateDescription = stripslashes($templateDescription); + $templateRegistry = stripslashes($templateRegistry); + $templateWebUI = stripslashes($templateWebUI); + $templateBanner = stripslashes($templateBanner); + $templateIcon = stripslashes($templateIcon); + $templateExtraParams = stripslashes($templateExtraParams); + + $templateDescBox = preg_replace('/\[/', '<', $templateDescription); + $templateDescBox = preg_replace('/\]/', '>', $templateDescBox); + + $templatePorts = ''; + $row = ' + + + + + + + + + + + + + + '; + + $i = 1; + foreach($doc->getElementsByTagName('Port') as $port){ + $j = $i + 100; + $ContainerPort = $port->getElementsByTagName( "ContainerPort" )->item(0)->nodeValue; + if (! strlen($ContainerPort)){ continue; } + $HostPort = $port->getElementsByTagName( "HostPort" )->item(0)->nodeValue; + $Protocol = $port->getElementsByTagName( "Protocol" )->item(0)->nodeValue; + $select = ($Protocol == 'udp') ? 'selected' : ''; + $templatePorts .= sprintf($row, $j, htmlspecialchars($ContainerPort), $readonly, htmlspecialchars($HostPort), $required, $select, $j, $disabled); + $i++; + } + + $templateVolumes = ''; + $row = ' + + + + + + +
+ + + + + + + + '; + + $i = 1; + foreach($doc->getElementsByTagName('Volume') as $volume){ + $j = $i + 100; + $ContainerDir = $volume->getElementsByTagName( "ContainerDir" )->item(0)->nodeValue; + if (! strlen($ContainerDir)){ continue; } + $HostDir = $volume->getElementsByTagName( "HostDir" )->item(0)->nodeValue; + $Mode = $volume->getElementsByTagName( "Mode" )->item(0)->nodeValue; + $Mode = ($Mode == "ro") ? "selected" : ''; + $templateVolumes .= sprintf($row, $j, htmlspecialchars($ContainerDir), $j, $readonly, $j, htmlspecialchars($HostDir), $j, $required, $j, $Mode, $j, $disabled); + $i++; + } + + $templateVariables = ''; + $row = ' + + + + + + + + + '; + + $i = 1; + foreach($doc->getElementsByTagName('Variable') as $variable){ + $j = $i + 100; + $VariableName = $variable->getElementsByTagName( "Name" )->item(0)->nodeValue; + if (! strlen($VariableName)){ continue; } + $VariableValue = $variable->getElementsByTagName( "Value" )->item(0)->nodeValue; + $templateVariables .= sprintf($row, $j, htmlspecialchars($VariableName), $readonly, htmlspecialchars($VariableValue), $required, $j, $disabled); + $i++; + } + } +} + +$showAdditionalInfo = true; +?> + + + + +
+ + +
+ +
+
+ + \n"; endif; + else:?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Template: + + "; + }?> + +
Description: +
+
Container Page: " . htmlspecialchars($Registry) . ""; + } + ?> +
+
Name:
Network type:
+ +
+ Volume Mappings +
+ + + + + + + + + + + + + + + + + + + +
Container volume:Host path:Access:
+ + + +
+
+ + +
+ +
+

Applications can be given read and write access to your data by mapping a directory path from the container to a directory path on the host. When looking at the volume mappings section, the Container volume represents the path from the container that will be mapped. The Host path represents the path the Container volume will map to on your unRAID system. All applications should require at least one volume mapping to store application metadata (e.g., media libraries, application settings, user profile data, etc.). Clicking inside these fields provides a "picker" that will let you navigate to where the mapping should point. Additional mappings can be manually created by clicking the Add Path button. Most applications will need you to specify additional mappings in order for the application to interact with other data on the system (e.g., with Plex Media Server, you should specify an additional mapping to give it access to your media files). It is important that when naming Container volumes that you specify a path that won’t conflict with already existing folders present in the container. If unfamiliar with Linux, using a prefix such as "unraid_" for the volume name is a safe bet (e.g., "/unraid_media" is a valid Container volume name).

+
+ +
+
+ Port Mappings +
+ + + + + + + + + + + + + + + + + +
Container port:Host port:Protocol:
+ + + + + + + +
+ +
+

When the network type is set to Bridge, you will be given the option of customizing what ports the container will use. While applications may be configured to talk to a specific port by default, we can remap those to different ports on our host with Docker. This means that while three different apps may all want to use port 8000, we can map each app to a unique port on the host (e.g., 8000, 8001, and 8002). When the network type is set to Host, the container will be allowed to use any available port on your system. Additional port mappings can be created, similar to Volumes, although this is not typically necessary when working with templates as port mappings should already be specified.

+
+
+ + + +
> +
+ Extra Parameters +
+ + + +
+

If you wish to append additional commands to your Docker container at run-time, you can specify them here. For example, if you wish to pin an application to live on a specific CPU core, you can enter "--cpuset=0" in this field. Change 0 to the core # on your system (starting with 0). You can pin multiple cores by separation with a comma or a range of cores by separation with a dash. For all possible Docker run-time commands, see here: https://docs.docker.com/reference/run/

+
+
+ +
style="display:none"> +
+ Additional Fields +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Docker Hub URL: + +
WebUI: + +
Banner: + +
Icon: + +
Description: + +
+
+
+
+ + +
+
+
+ + + + diff --git a/plugins/dynamix.docker.manager/include/DockerClient.php b/plugins/dynamix.docker.manager/include/DockerClient.php new file mode 100644 index 000000000..17bd90d25 --- /dev/null +++ b/plugins/dynamix.docker.manager/include/DockerClient.php @@ -0,0 +1,666 @@ + + '/usr/local/emhttp/plugins/dynamix.docker.manager', + 'autostart-file' => '/var/lib/docker/unraid-autostart', + 'template-repos' => '/boot/config/plugins/dockerMan/template-repos', + 'templates-user' => '/boot/config/plugins/dockerMan/templates-user', + 'templates-storage' => '/boot/config/plugins/dockerMan/templates', + 'images-ram' => '/usr/local/emhttp/state/plugins/dynamix.docker.manager/images', + 'images-storage' => '/boot/config/plugins/dockerMan/images', + 'webui-info' => '/usr/local/emhttp/state/plugins/dynamix.docker.manager/docker.json', + 'update-status' => '/var/lib/docker/unraid-update-status.json', + ); + +//## BETA 9 +// $dockerManPaths = array( +// 'plugin' => '/usr/local/emhttp/plugins/dockerMan', +// 'autostart-file' => '/var/lib/docker/unraid-autostart', +// 'template-repos' => '/boot/config/plugins/dockerMan/template-repos', +// 'templates-user' => '/boot/config/plugins/dockerMan/templates-user', +// 'templates-storage' => '/boot/config/plugins/dockerMan/templates', +// 'images-ram' => '/usr/local/emhttp/state/plugins/dockerMan/images', +// 'images-storage' => '/boot/config/plugins/dockerMan/images', +// 'webui-info' => '/usr/local/emhttp/state/plugins/dockerMan/docker.json', +// ); + +//## BETA 8 +// $dockerManPaths = array( +// 'plugin' => '/usr/local/emhttp/plugins/dockerMan', +// 'autostart-file' => '/var/lib/docker/unraid-autostart', +// 'template-repos' => '/boot/config/plugins/dockerMan/template-repos', +// 'templates-user' => '/var/lib/docker/unraid-templates', +// 'templates-storage' => '/boot/config/plugins/dockerMan/templates', +// 'images-ram' => '/usr/local/emhttp/state/plugins/dockerMan/images', +// 'images-storage' => '/boot/config/plugins/dockerMan/images', +// 'webui-info' => '/usr/local/emhttp/state/plugins/dockerMan/docker.json', +// ); + +#load emhttp variables if needed. +if (! isset($var)){ + if (! is_file("/usr/local/emhttp/state/var.ini")) shell_exec("wget -qO /dev/null localhost:$(lsof -nPc emhttp | grep -Po 'TCP[^\d]*\K\d+')"); + $var = @parse_ini_file("/usr/local/emhttp/state/var.ini"); +} + +###################################### +## DOCKERTEMPLATES CLASS ## +###################################### + +class DockerTemplates { + + public $verbose = FALSE; + + private function debug($m) { + if($this->verbose) echo $m."\n"; + } + + public function download_url($url, $path = "", $bg = FALSE){ + exec("curl --max-time 60 --silent --insecure --location --fail ".($path ? " -o " . escapeshellarg($path) : "")." " . escapeshellarg($url) . " ".($bg ? ">/dev/null 2>&1 &" : "2>/dev/null"), $out, $exit_code ); + return ($exit_code === 0 ) ? implode("\n", $out) : FALSE; + } + + public function listDir($root, $ext=NULL) { + $iter = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($root, + RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST, + RecursiveIteratorIterator::CATCH_GET_CHILD); + $paths = array(); + foreach ($iter as $path => $fileinfo) { + $fext = $fileinfo->getExtension(); + if ($ext && ( $ext != $fext )) continue; + if ( $fileinfo->isFile()) $paths[] = array('path' => $path, 'prefix' => basename(dirname($path)), 'name' => $fileinfo->getBasename(".$fext")); + } + return $paths; + } + + + public function getTemplates($type) { + global $dockerManPaths; + $tmpls = array(); + $dirs = array(); + if ($type == "all"){ + $dirs[] = $dockerManPaths['templates-user']; + $dirs[] = $dockerManPaths['templates-storage']; + + } else if ($type == "user"){ + $dirs[] = $dockerManPaths['templates-user']; + + } else if ($type == "default"){ + $dirs[] = $dockerManPaths['templates-storage']; + } else { + $dirs[] = $type; + } + foreach ($dirs as $dir) { + if (! is_dir( $dir)) @mkdir( $dir, 0770, true); + $tmpls = array_merge($tmpls, $this->listDir($dir, "xml")); + } + return $tmpls; + } + + + private function removeDir($path){ + if (is_dir($path) === true) { + $files = array_diff(scandir($path), array('.', '..')); + foreach ($files as $file) { + $this->removeDir(realpath($path) . '/' . $file); + } + return rmdir($path); + } else if (is_file($path) === true) { + return unlink($path); + } + return false; + } + + + public function downloadTemplates($Dest=NULL, $Urls=NULL){ + global $dockerManPaths; + $Dest = ($Dest) ? $Dest : $dockerManPaths['templates-storage']; + $Urls = ($Urls) ? $Urls : $dockerManPaths['template-repos']; + $repotemplates = array(); + $output = ""; + $tmp_dir = "/tmp/tmp-".mt_rand(); + if (!file_exists($dockerManPaths['template-repos'])) { + @mkdir(dirname($dockerManPaths['template-repos']), 0777, true); + @file_put_contents($dockerManPaths['template-repos'], "https://github.com/limetech/docker-templates"); + } + $urls = @file($Urls, FILE_IGNORE_NEW_LINES); + if ( ! is_array($urls)) return false; + $this->debug("\nURLs:\n " . implode("\n ", $urls)); + foreach ($urls as $url) { + $api_regexes = array( + 0 => '%/.*github.com/([^/]*)/([^/]*)/tree/([^/]*)/(.*)$%i', + 1 => '%/.*github.com/([^/]*)/([^/]*)/tree/([^/]*)$%i', + 2 => '%/.*github.com/([^/]*)/(.*).git%i', + 3 => '%/.*github.com/([^/]*)/(.*)%i', + ); + for ($i=0; $i < count($api_regexes); $i++) { + if ( preg_match($api_regexes[$i], $url, $matches) ){ + $github_api['user'] = ( isset( $matches[1] )) ? $matches[1] : ""; + $github_api['repo'] = ( isset( $matches[2] )) ? $matches[2] : ""; + $github_api['branch'] = ( isset( $matches[3] )) ? $matches[3] : "master"; + $github_api['path'] = ( isset( $matches[4] )) ? $matches[4] : ""; + $github_api['url'] = sprintf("https://github.com/%s/%s/archive/%s.tar.gz", $github_api['user'], $github_api['repo'], $github_api['branch']); + break; + } + } + if ( $this->download_url($github_api['url'], "$tmp_dir.tar.gz") === FALSE) { + $this->debug("\n Download ". $github_api['url'] ." has failed."); + return NULL; + } else { + @mkdir($tmp_dir, 0777, TRUE); + shell_exec("tar -zxf $tmp_dir.tar.gz --strip=1 -C $tmp_dir/ 2>&1"); + unlink("$tmp_dir.tar.gz"); + } + $tmplsStor = array(); + $templates = $this->getTemplates($tmp_dir); + $this->debug("\n Templates found in ". $github_api['url']); + foreach ($templates as $template) { + $storPath = sprintf("%s/%s", $Dest, str_replace($tmp_dir."/", "", $template['path']) ); + $tmplsStor[] = $storPath; + if (! is_dir( dirname( $storPath ))) @mkdir( dirname( $storPath ), 0777, true); + if ( is_file($storPath) ){ + if ( sha1_file( $template['path'] ) === sha1_file( $storPath )) { + $this->debug(" Skipped: ".$template['prefix'].'/'.$template['name']); + continue; + } else { + @copy($template['path'], $storPath); + $this->debug(" Updated: ".$template['prefix'].'/'.$template['name']); + } + } else { + @copy($template['path'], $storPath); + $this->debug(" Added: ".$template['prefix'].'/'.$template['name']); + } + } + $repotemplates = array_merge($repotemplates, $tmplsStor); + $output[$url] = $tmplsStor; + $this->removeDir($tmp_dir); + } + // Delete any templates not in the repos + foreach ($this->listDir($Dest, "xml") as $arrLocalTemplate) { + if (!in_array($arrLocalTemplate['path'], $repotemplates)) { + unlink($arrLocalTemplate['path']); + $this->debug(" Removed: ".$arrLocalTemplate['prefix'].'/'.$arrLocalTemplate['name']."\n"); + // Any other files left in this template folder? if not delete the folder too + $files = array_diff(scandir(dirname($arrLocalTemplate['path'])), array('.', '..')); + if (empty($files)) { + rmdir(dirname($arrLocalTemplate['path'])); + $this->debug(" Removed: ".$arrLocalTemplate['prefix']); + } + } + } + return $output; + } + + + public function getTemplateValue($Repository, $field, $scope = "all"){ + $tmpls = $this->getTemplates($scope); + + foreach ($tmpls as $file) { + $doc = new DOMDocument(); + $doc->load($file['path']); + $TemplateRepository = $doc->getElementsByTagName( "Repository" )->item(0)->nodeValue; + if (! preg_match("/:[\w]*$/i", $TemplateRepository)) { + $Repo = preg_replace("/:[\w]*$/i", "", $Repository); + }else{ + $Repo = $Repository; + } + + if ( $Repo == $TemplateRepository ) { + $TemplateField = $doc->getElementsByTagName( $field )->item(0)->nodeValue; + return trim($TemplateField); + break; + } + } + return NULL; + } + + + public function getUserTemplate($Container){ + foreach ($this->getTemplates("user") as $file) { + $doc = new DOMDocument('1.0', 'utf-8'); + $doc->load( $file['path'] ); + $Name = $doc->getElementsByTagName( "Name" )->item(0)->nodeValue; + if ($Name == $Container){ + return $file['path']; + } + } + return FALSE; + } + + + public function getControlURL($name){ + global $var; + $DockerClient = new DockerClient(); + $IP = $var["IPADDR"]; + $Repository = ""; + + foreach ($DockerClient->getDockerContainers() as $ct) { + if ($ct['Name'] == $name) { + $Repository = preg_replace("/:[\w]*$/i", "", $ct['Image']); + $Ports = $ct["Ports"]; + break; + } + } + + $WebUI = $this->getTemplateValue($Repository, "WebUI"); + + if (preg_match("%\[IP\]%", $WebUI)) { + $WebUI = preg_replace("%\[IP\]%", $IP, $WebUI); + preg_match("%\[PORT:(\d+)\]%", $WebUI, $matches); + $ConfigPort = $matches[1]; + if ($ct["NetworkMode"] == "bridge"){ + foreach ($Ports as $key){ + if ($key["PrivatePort"] == $ConfigPort){ + $ConfigPort = $key["PublicPort"]; + } + } + } + $WebUI = preg_replace("%\[PORT:\d+\]%", $ConfigPort, $WebUI); + } + return $WebUI; + } + + + public function removeInfo($container){ + global $dockerManPaths; + $dockerIni = $dockerManPaths['webui-info']; + if (! is_dir( dirname( $dockerIni ))) @mkdir( dirname( $dockerIni ), 0770, true); + $info = (is_file($dockerIni)) ? json_decode(file_get_contents($dockerIni), TRUE) : array(); + if (! count($info) ) $info = array(); + + if (isset($info[$container])) unset($info[$container]); + file_put_contents($dockerIni, json_encode($info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + $update_file = $dockerManPaths['update-status']; + $updateStatus = (is_file($update_file)) ? json_decode(file_get_contents($update_file), TRUE) : array(); + if (isset($updateStatus[$container])) unset($updateStatus[$container]); + file_put_contents($update_file, json_encode($updateStatus, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + + + public function getAllInfo($reload = FALSE){ + global $dockerManPaths; + $DockerClient = new DockerClient(); + $DockerUpdate = new DockerUpdate(); + $new_info = array(); + + $dockerIni = $dockerManPaths['webui-info']; + if (! is_dir( dirname( $dockerIni ))) @mkdir( dirname( $dockerIni ), 0770, true); + $info = (is_file($dockerIni)) ? json_decode(file_get_contents($dockerIni), TRUE) : array(); + if (! count($info) ) $info = array(); + + $containers = $DockerClient->getDockerContainers(); + if (! count($containers) ) $containers = array(); + + $autostart_file = $dockerManPaths['autostart-file']; + $allAutoStart = @file($autostart_file, FILE_IGNORE_NEW_LINES); + if ($allAutoStart===FALSE) $allAutoStart = array(); + + $update_file = $dockerManPaths['update-status']; + $updateStatus = (is_file($update_file)) ? json_decode(file_get_contents($update_file), TRUE) : array(); + + foreach ($containers as $ct) { + $name = $ct['Name']; + $image = $ct['Image']; + $tmp = ( count($info[$name]) ) ? $info[$name] : array() ; + + $tmp['running'] = $ct['Running']; + $tmp['autostart'] = in_array($name, $allAutoStart); + + $img = $this->getBannerIcon( $image ); + $tmp['banner'] = ( $img['banner'] ) ? $img['banner'] : "#"; + $tmp['icon'] = ( $img['icon'] ) ? $img['icon'] : "#"; + + $WebUI = $this->getControlURL($name); + $tmp['url'] = ($WebUI) ? $WebUI : "#"; + + $Registry = $this->getTemplateValue($image, "Registry"); + $tmp['registry'] = ( $Registry ) ? $Registry : "#"; + + if ($reload) { + $nv = $DockerUpdate->getUpdateStatus($name, $image); + if ($nv != 'undef'){ + $updateStatus[$name] = $nv; + } + } + $tmp['updated'] = (array_key_exists($name, $updateStatus)) ? $updateStatus[$name] : 'undef'; + + $tmp['template'] = $this->getUserTemplate($name); + + $this->debug("\n$name");foreach ($tmp as $c => $d) $this->debug(sprintf(" %-10s: %s", $c, $d)); + $new_info[$name] = $tmp; + } + file_put_contents($dockerIni, json_encode($new_info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + if($reload) { + foreach ($updateStatus as $ct => $update) if (!isset($new_info[$ct])) unset($updateStatus[$ct]); + file_put_contents($update_file, json_encode($updateStatus, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + return $new_info; + } + + + public function getBannerIcon($Repository){ + global $dockerManPaths; + $out = array(); + $Images = array(); + + $Images = array('banner' => $this->getTemplateValue($Repository, "Banner"), + 'icon' => $this->getTemplateValue($Repository, "Icon") ); + + $defaultImages = array('banner' => '/plugins/dynamix.docker.manager/images/spacer.png', + 'icon' => '/plugins/dynamix.docker.manager/images/question.png'); + + foreach ($Images as $type => $imgUrl) { + preg_match_all("/(.*?):([\w]*$)/i", $Repository, $matches); + $tempPath = sprintf("%s/%s-%s-%s.png", $dockerManPaths[ 'images-ram' ], preg_replace('%\/|\\\%', '-', $matches[1][0]), $matches[2][0], $type); + $storagePath = sprintf("%s/%s-%s-%s.png", $dockerManPaths[ 'images-storage' ], preg_replace('%\/|\\\%', '-', $matches[1][0]), $matches[2][0], $type); + if (! is_dir( dirname( $tempPath ))) @mkdir( dirname( $tempPath ), 0770, true); + if (! is_dir( dirname( $storagePath ))) @mkdir( dirname( $storagePath ), 0770, true); + if (! is_file( $tempPath )) { + if ( is_file( $storagePath )){ + @copy($storagePath, $tempPath); + } else { + $this->download_url($imgUrl, $storagePath); + @copy($storagePath, $tempPath); + } + } + $out[ $type ] = ( is_file( $tempPath ) ) ? str_replace('/usr/local/emhttp', '', $tempPath) : ""; + } + return $out; + } +} + + +###################################### +## DOCKERUPDATE CLASS ## +###################################### +class DockerUpdate{ + + public function download_url($url, $path = "", $bg = FALSE){ + exec("curl --max-time 30 --silent --insecure --location --fail ".($path ? " -o " . escapeshellarg($path) : "")." " . escapeshellarg($url) . " ".($bg ? ">/dev/null 2>&1 &" : "2>/dev/null"), $out, $exit_code ); + return ($exit_code === 0 ) ? implode("\n", $out) : FALSE; + } + + + public function getRemoteVersion($RegistryUrl, $image){ + preg_match_all("/:([\w]*$)/i", $image, $matches); + $tag = isset($matches[1][0]) ? $matches[1][0] : "latest"; + preg_match("#/u/([^/]*)/([^/]*)#", $RegistryUrl, $matches); + $apiUrl = sprintf("http://index.docker.io/v1/repositories/%s/%s/tags/%s", $matches[1], $matches[2], $tag); + $apiContent = $this->download_url($apiUrl); + return ( $apiContent === FALSE ) ? NULL : substr(json_decode($apiContent, TRUE)[0]['id'],0,16); + } + + + public function getLocalVersion($file){ + if(is_file($file)){ + $doc = new DOMDocument(); + $doc->load($file); + if ( ! $doc->getElementsByTagName( "Version" )->length == 0 ) { + return $doc->getElementsByTagName( "Version" )->item(0)->nodeValue; + } else { + return NULL; + } + } + } + + + public function getUpdateStatus($container, $image) { + $DockerTemplates = new DockerTemplates(); + $RegistryUrl = $DockerTemplates->getTemplateValue($image, "Registry"); + $userFile = $DockerTemplates->getUserTemplate($container); + $localVersion = $this->getLocalVersion($userFile); + $remoteVersion = $this->getRemoteVersion($RegistryUrl, $image); + // echo "\n $localVersion => $remoteVersion"; + return ($localVersion && $remoteVersion) ? (($remoteVersion == $localVersion) ? "true" : "false") : "undef" ; + } + + + public function syncVersions($container) { + global $dockerManPaths; + $update_file = $dockerManPaths['update-status']; + $updateStatus = (is_file($update_file)) ? json_decode(file_get_contents($update_file), TRUE) : array(); + $updateStatus[$container] = 'true'; + file_put_contents($update_file, json_encode($updateStatus, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } +} + + +###################################### +## DOCKERCLIENT CLASS ## +###################################### +class DockerClient { + + private function build_sorter($key) { + return function ($a, $b) use ($key) { + return strnatcmp(strtolower($a[$key]), strtolower($b[$key])); + }; + } + + + private function humanTiming ($time){ + $time = time() - $time; // to get the time since that moment + $tokens = array (31536000 => 'year', + 2592000 => 'month', + 604800 => 'week', + 86400 => 'day', + 3600 => 'hour', + 60 => 'minute', + 1 => 'second' + ); + foreach ($tokens as $unit => $text) { + if ($time < $unit) continue; + $numberOfUnits = floor($time / $unit); + return $numberOfUnits.' '.$text.(($numberOfUnits>1)?'s':'')." ago"; + } + } + + + private function unchunk($result) { + return preg_replace_callback( + '/(?:(?:\r\n|\n)|^)([0-9A-F]+)(?:\r\n|\n){1,2}(.*?)' + .'((?:\r\n|\n)(?:[0-9A-F]+(?:\r\n|\n))|$)/si', + create_function('$matches','return hexdec($matches[1]) == strlen($matches[2]) ? $matches[2] :$matches[0];'), $result); + } + + + private function formatBytes($size){ + if ($size == 0){ return "0 B";} + $base = log($size) / log(1024); + $suffix = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'); + return round(pow(1024, $base - floor($base)), 1) ." ". $suffix[floor($base)]; + } + + + private function getDockerJSON($url, $method = "GET"){ + $fp = stream_socket_client('unix:///var/run/docker.sock', $errno, $errstr); + + if ($fp === false) { + echo "Couldn't create socket: [$errno] $errstr"; + return NULL; + } + $out="$method {$url} HTTP/1.1\r\nConnection: Close\r\n\r\n"; + fwrite($fp, $out); + // Strip headers out + while (($line = fgets($fp)) !== false) { + if (rtrim($line) == '') { + break; + } + } + $data = ''; + while (($line = fgets($fp)) !== false) { + $data .= $line; + } + fclose($fp); + $data = $this->unchunk($data); + $json = json_decode( $data, true ); + if ($json === null) { + $json = array(); + } else if (!array_key_exists(0, $json) && !empty($json)) { + $json = [ $json ]; + } + return $json; + } + + + public function getInfo(){ + $info = $this->getDockerJSON("/info"); + $version = $this->getDockerJSON("/version"); + return array_merge($info[0], $version[0]); + } + + + private function getContainerDetails($id){ + $json = $this->getDockerJSON("/containers/{$id}/json"); + return $json; + } + + + public function startContainer($id){ + $json = $this->getDockerJSON("/containers/${id}/start", "POST"); + return $json; + } + + + public function removeImage($id){ + $json = $this->getDockerJSON("/images/{$id}", "DELETE"); + return $json; + } + + + public function stopContainer($id){ + $json = $this->getDockerJSON("/containers/${id}/stop", "POST"); + return $json; + } + + + private function getImageDetails($id){ + $json = $this->getDockerJSON("/images/$id/json"); + return $json; + } + + + public function getDockerContainers(){ + $containers = array(); + $json = $this->getDockerJSON("/containers/json?all=1"); + + if (! $json ){ return $containers; } + + foreach($json as $obj){ + $c = array(); + $status = $obj['Status'] ? $obj['Status'] : "None"; + preg_match("/\b^Up\b/", $status, $matches); + $running = $matches ? TRUE : FALSE; + $details = $this->getContainerDetails($obj['Id']); + + // echo "
".print_r($details,TRUE)."
"; + + // Docker 1.7 doesn't automatically append the tag 'latest', so we do that now if there's no tag + preg_match_all("/:([\w]*$)/i", $obj['Image'], $matches2); + + $c["Image"] = $obj['Image'] . (isset($matches2[1][0]) ? "" : ":latest"); + $c["ImageId"] = substr($details[0]["Image"],0,12); + $c["Name"] = substr($details[0]['Name'], 1); + $c["Status"] = $status; + $c["Running"] = $running; + $c["Cmd"] = $obj['Command']; + $c["Id"] = substr($obj['Id'],0,12); + $c['Volumes'] = $details[0]["HostConfig"]['Binds']; + $c["Created"] = $this->humanTiming($obj['Created']); + $c["NetworkMode"] = $details[0]['HostConfig']['NetworkMode']; + + $Ports = $details[0]['HostConfig']['PortBindings']; + $Ports = (count ( $Ports )) ? $Ports : array(); + $c["Ports"] = array(); + if ($c["NetworkMode"] != 'host'){ + foreach ($Ports as $port => $value) { + list($PrivatePort, $Type) = explode("/", $port); + $PublicPort = $value[0]['HostPort']; + $c["Ports"][] = array('PrivatePort' => $PrivatePort, + 'PublicPort' => $PublicPort, + 'Type' => $Type ); + } + } + + $containers[] = $c; + } + usort($containers, $this->build_sorter('Name')); + return $containers; + } + + + public function getImageID($Image){ + $allImages = $this->getDockerImages(); + foreach ($allImages as $img) { + preg_match("%" . preg_quote($Image, "%") ."%", $img["Tags"][0], $matches); + if( $matches){ + return $img["Id"]; + } + } + return NULL; + } + + + private function usedBy($imageId){ + $out = array(); + $Containers = $this->getDockerContainers(); + $Containers = ( count( $Containers )) ? $Containers : array(); + foreach ($Containers as $ct) { + if ($ct["ImageId"] == $imageId){ + $out[] = $ct["Name"]; + } + } + return $out; + } + + + public function getDockerImages(){ + + $images = array(); + $c = array(); + $json = $this->getDockerJSON("/images/json?all=0"); + + if (! $json){ return $images; } + + foreach($json as $obj){ + $c = array(); + $tags = array(); + foreach($obj['RepoTags'] as $t){ + $tags[] = htmlentities($t); + } + + $c["Created"] = $this->humanTiming($obj['Created']);//date('Y-m-d H:i:s', $obj['Created']); + $c["Id"] = substr($obj['Id'],0,12); + $c["ParentId"] = substr($obj['ParentId'],0,12); + $c["Size"] = $this->formatBytes($obj['Size']); + $c["VirtualSize"] = $this->formatBytes($obj['VirtualSize']); + $c["Tags"] = $tags; + $c["usedBy"] = $this->usedBy($c["Id"]); + + $imgDetails = $this->getImageDetails($obj['Id']); + // echo "
".print_r($imgDetails,TRUE)."
"; + $a = $imgDetails[0]['Config']['Volumes']; + $b = $imgDetails[0]['Config']['ExposedPorts']; + $c['ImageType'] = (! count($a) && ! count($b)) ? 'base' : 'user'; + + $images[] = $c; + + } + return $images; + } +} +?> diff --git a/plugins/dynamix.docker.manager/include/Exec.php b/plugins/dynamix.docker.manager/include/Exec.php new file mode 100644 index 000000000..bd306ef9b --- /dev/null +++ b/plugins/dynamix.docker.manager/include/Exec.php @@ -0,0 +1,50 @@ + + array("pipe", "r"), // stdin is a pipe that the child will read from + 1 => array("pipe", "w"), // stdout is a pipe that the child will write to + 2 => array("pipe", "w") // stderr is a pipe that the child will write to + ); + + foreach (explode(';', $commands) as $command){ + $parts = explode(" ", $command); + $command = escapeshellcmd(realpath($_SERVER['DOCUMENT_ROOT'].array_shift($parts))); + if (!$command) continue; + $command .= " ".implode(" ", $parts); // should add 'escapeshellarg' here, but this requires changes in all the original arguments + $id = mt_rand(); + $output = array(); + echo "

"; + echo ""; + echo ""; + $proc = proc_open($command." 2>&1", $descriptorspec, $pipes, '/', array()); + while ($out = fgets( $pipes[1] )) { + $out = preg_replace("%[\t\n\x0B\f\r]+%", '', $out ); + @flush(); + echo "\n"; + @flush(); + } + $retval = proc_close($proc); + echo "\n"; + $out = $retval ? "The command failed." : "The command finished successfully!"; + echo ""; + } +} +?> diff --git a/plugins/dynamix.docker.manager/include/UpdateConfig.php b/plugins/dynamix.docker.manager/include/UpdateConfig.php new file mode 100644 index 000000000..d7326aac6 --- /dev/null +++ b/plugins/dynamix.docker.manager/include/UpdateConfig.php @@ -0,0 +1,55 @@ + + true )); + } + else { + unset($allAutoStart[$key]); + if ($json) echo json_encode(array( 'autostart' => false )); + } + file_put_contents($autostart_file, implode(PHP_EOL, $allAutoStart).(count($allAutoStart)? PHP_EOL : "")); +} + +if ($_POST['#action'] == "templates" ){ + readfile("/usr/local/emhttp/update.htm"); + $repos = $_POST['template_repos']; + file_put_contents($template_repos, $repos); + $DockerTemplates = new DockerTemplates(); + $DockerTemplates->downloadTemplates(); +} + +if ( isset($_GET['is_dir'] )) { + echo json_encode( array( 'is_dir' => is_dir( $_GET['is_dir'] ))); +} +?> diff --git a/plugins/dynamix.docker.manager/javascript/addDocker.js b/plugins/dynamix.docker.manager/javascript/addDocker.js new file mode 100644 index 000000000..2ac731a16 --- /dev/null +++ b/plugins/dynamix.docker.manager/javascript/addDocker.js @@ -0,0 +1,155 @@ +var pathNum = 2; +var portNum = 0; +var varNum = 0; +var currentPath = "/mnt/"; + +if (!String.prototype.format) { + String.prototype.format = function() { + var args = arguments; + return this.replace(/{(\d+)}/g, function(match, number) { + return typeof args[number] != 'undefined' ? args[number] : match; + }); + }; +} + +function rmTemplate(tmpl) { + var name = tmpl.split(/[\/]+/).pop(); + swal({title:"Are you sure?",text:"Remove template: "+name,type:"warning",showCancelButton:true},function(){$("#rmTemplate").val(tmpl);$("#formTemplate").submit();}); +} + +function toggleBrowser(N) { + var el = $('#fileTree' + N); + if (el.is(':visible')) { + hideBrowser(N); + } else { + $( el ).fileTree({ + root: currentPath, + filter: 'HIDE_FILES_FILTER' + }, + function(file) {}, + function(folder) { + $("#hostPath" + N).val(folder); + }); + $( el ).slideDown('fast'); + } +} + +function hideBrowser(N) { + $("#fileTree" + N).slideUp('fast', function () { + $(this).html(""); + }); +} + +function addPort(frm) { + portNum++; + var hostPort = $("#hostPort1"); + var containerPort = $("#containerPort1"); + var portProtocol = $("#portProtocol1"); + + var select = ""; + if (portProtocol.val() == "udp"){ + select = "selected"; + } + + var row = [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ].join('').format(portNum, hostPort.val(), containerPort.val(), select); + + $(row).appendTo('#portRows').fadeIn("fast"); + hostPort.val(''); + containerPort.val(''); + portProtocol.val('tcp'); +} + +function removePort(rnum) { + $('#portNum' + rnum).fadeOut("fast", function() { $(this).remove(); }); +} + +function addPath(frm) { + pathNum++; + var hostPath = $("#hostPath1"); + var containerPath = $("#containerPath1"); + var hostWritable = $("#hostWritable1"); + + var select = ""; + if (hostWritable.val() == "ro"){ + select = "selected"; + } + + var row = [ + '', + '', + '', + '', + '', + '', + '
', + '', + '', + '', + '', + '', + '', + '', + '' + ].join('').format(pathNum, hostPath.val(), containerPath.val(), select); + + $(row).appendTo('#pathRows tbody').fadeIn("fast"); + hostPath.val(''); + containerPath.val(''); + hostWritable.val('rw'); +} + +function removePath(rnum) { + $('#pathNum' + rnum).fadeOut("fast", function() { $(this).remove(); }); +} + +function addEnv(frm) { + varNum++; + var VariableName = $("#VariableName1"); + var VariableValue = $("#VariableValue1"); + + var row = [ + '', + '', + '', + '', + '', + '', + '', + '', + '' + ].join('').format(varNum, VariableName.val(), VariableValue.val()); + + $(row).appendTo('#envRows tbody').fadeIn("fast"); + VariableName.val(''); + VariableValue.val(''); +} + +function removeEnv(rnum) { + $('#varNum' + rnum).fadeOut("fast", function() { $(this).remove(); }); +} + +function toggleMode(){ + $("#toggleMode").toggleClass("fa-toggle-off fa-toggle-on"); + $(".additionalFields").slideToggle(); +} diff --git a/plugins/dynamix.docker.manager/javascript/docker.js b/plugins/dynamix.docker.manager/javascript/docker.js new file mode 100644 index 000000000..ca1cd25d4 --- /dev/null +++ b/plugins/dynamix.docker.manager/javascript/docker.js @@ -0,0 +1,242 @@ +function addDockerContainerContext(container, image, template, started, update, autostart, webui){ + var opts = [{header: container, image: "/plugins/dynamix.docker.manager/images/dynamix.docker.manager.png"}]; + if (started && (webui != "#")) { + opts.push({text: 'WebUI', icon:'fa-globe', href: webui, target: '_blank' }); + opts.push({divider: true}); + } + if (! update){ + opts.push({text: 'Update', icon:'fa-arrow-down', action: function(e){ e.preventDefault(); execUpContainer(container); }}); + opts.push({divider: true}); + } + if (started){ + opts.push({text: 'Stop', icon:'fa-stop', action: function(e){ e.preventDefault(); containerControl(container, 'stop'); }}); + opts.push({text: 'Restart', icon:'fa-refresh', action: function(e){ e.preventDefault(); containerControl(container, 'restart'); }}); + } else { + opts.push({text: 'Start', icon:'fa-play', action: function(e){ e.preventDefault(); containerControl(container, 'start'); }}); + } + opts.push({divider: true}); + if (location.pathname.indexOf("/Dashboard") === 0) { + opts.push({text: 'Logs', icon:'fa-navicon', action: function(e){ e.preventDefault(); containerLogs(container); }}); + } + if (template) { + opts.push({text: 'Edit', icon:'fa-wrench', action: function(e){ e.preventDefault(); editContainer(container, template); }}); + } + opts.push({divider: true}); + opts.push({text: 'Remove', icon:'fa-trash', action: function(e){ e.preventDefault(); rmContainer(container, image); }}); + context.attach('#context-'+container, opts); +} + +function addDockerImageContext(image, imageTag){ + var opts = [{header: '(orphan image)'}]; + opts.push({text: 'Remove', icon:'fa-trash', action: function(e){ e.preventDefault(); rmImage(image, imageTag); }}); + context.attach('#context-'+image, opts); +} + +function execUpContainer(container){ + var title = 'Updating the container: ' + container; + var address = "/plugins/dynamix.docker.manager/include/CreateDocker.php?updateContainer=true&ct[]=" + encodeURIComponent(container); + popupWithIframe(title, address, true); +} + +function popupWithIframe(title, cmd, reload) { + pauseEvents(); + + $( "#iframe-popup" ).html(''); + $("#iframe-popup").dialog({ + autoOpen: true, + title: title, + draggable: true, + width : 800, + height : ((screen.height/5)*4)||0, + resizable : true, + modal : true, + show : {effect: 'fade' , duration: 250}, + hide : {effect: 'fade' , duration: 250}, + open: function(ev, ui){ + $('#myIframe').attr('src', cmd); + }, + close: function( event, ui ) { + if (reload && !$('#myIframe').contents().find('#canvas').length){ + location = window.location.href; + } else { + resumeEvents(); + } + } + }); + $(".ui-dialog .ui-dialog-titlebar").addClass('menu'); + $(".ui-dialog .ui-dialog-content").css('padding','0'); + $(".ui-dialog .ui-dialog-title").css('text-align','center'); + $(".ui-dialog .ui-dialog-title").css('width', "100%"); + //$('.ui-widget-overlay').click(function() { $("#iframe-popup").dialog("close"); }); +} + +function addContainer() { + var path = location.pathname; + var x = path.indexOf("?"); + if (x!=-1) path = path.substring(0,x); + + location = path + '/AddContainer'; +} + +function editContainer(container, template) { + var path = location.pathname; + var x = path.indexOf("?"); + if (x!=-1) path = path.substring(0,x); + + location = path + '/UpdateContainer?xmlTemplate=edit:' + template; +} + +function rmContainer(containers, images){ + var ctCmd = "/plugins/dynamix.docker.manager/scripts/docker rm -f"; + var imgCmd = "/plugins/dynamix.docker.manager/scripts/docker rmi"; + var ctTitle = ""; + if (typeof containers === "object") { + for (var i = 0; i < containers.length; i++) { + ctCmd += " " + containers[i]; + imgCmd += " " + images[i]; + ctTitle += containers[i] + "
"; + } + } else { + ctCmd += " " + containers; + imgCmd += " " + images; + ctTitle += containers + "
"; + } + var title = 'Removing container'; + $( "#dialog-confirm" ).html(ctTitle); + $( "#dialog-confirm" ).append( "
Are you sure?" ); + $( "#dialog-confirm" ).dialog({ + title: title, + resizable: false, + width: 500, + modal: true, + show : {effect: 'fade' , duration: 250}, + hide : {effect: 'fade' , duration: 250}, + buttons: { + "Just the container": function() { + $( this ).dialog( "close" ); + var cmd = '/plugins/dynamix.docker.manager/include/Exec.php?cmd=' + encodeURIComponent(ctCmd); + popupWithIframe(title, cmd, true); + }, + "Container and image": function() { + $( this ).dialog( "close" ); + var cmd = '/plugins/dynamix.docker.manager/include/Exec.php?cmd=' + encodeURIComponent(ctCmd + ";" + imgCmd); + popupWithIframe(title, cmd, true); + }, + Cancel: function() { + $( this ).dialog( "close" ); + $( this ).html(""); + } + } + }); + $(".ui-dialog .ui-dialog-titlebar").addClass('menu'); + $(".ui-dialog .ui-dialog-title").css('text-align','center').css( 'width', "100%"); + $(".ui-dialog .ui-dialog-content").css('padding-top','15px').css('font-weight','bold'); + $(".ui-button-text").css('padding','0px 5px'); +} + +function updateContainer(containers){ + var ctCmd =""; + var ctTitle = ""; + if (typeof containers === "object") { + for (var i = 0; i < containers.length; i++) { + ctCmd += "&ct[]=" + encodeURIComponent(containers[i]); + ctTitle += containers[i] + "
"; + } + } else { + ctCmd += "&ct[]=" + encodeURIComponent(containers); + ctTitle += containers + "
"; + } + var title = 'Updating container'; + $( "#dialog-confirm" ).html(ctTitle); + $( "#dialog-confirm" ).append( "
Are you sure?" ); + $( "#dialog-confirm" ).dialog({ + title: title, + resizable: false, + width: 500, + modal: true, + show : {effect: 'fade' , duration: 250}, + hide : {effect: 'fade' , duration: 250}, + buttons: { + "Just do it!": function() { + $( this ).dialog( "close" ); + var cmd = "/plugins/dynamix.docker.manager/include/CreateDocker.php?updateContainer=true" + ctCmd; + popupWithIframe(title, cmd, true); + }, + Cancel: function() { + $( this ).dialog( "close" ); + } + } + }); + $(".ui-dialog .ui-dialog-titlebar").addClass('menu'); + $(".ui-dialog .ui-dialog-title").css('text-align','center'); + $(".ui-dialog .ui-dialog-content").css('padding-top','15px'); + $(".ui-dialog .ui-dialog-content").css('font-weight','bold'); + $(".ui-button-text").css('padding','0px 5px'); + $( ".ui-dialog .ui-dialog-title" ).css( 'width', "100%"); +} + +function rmImage(images, imageName){ + var imgCmd = "/plugins/dynamix.docker.manager/scripts/docker rmi"; + var imgTitle = ""; + if (typeof images === "object") { + for (var i = 0; i < images.length; i++) { + imgCmd += " " + images[i]; + imgTitle += imageName[i] + "
"; + } + } else { + imgCmd += " " + images; + imgTitle += imageName + "
"; + } + var title = "Removing image"; + $( "#dialog-confirm" ).html(imgTitle); + $( "#dialog-confirm" ).append( "
Are you sure?" ); + $( "#dialog-confirm" ).dialog({ + title: title, + dialogClass: "alert", + resizable: false, + width: 500, + modal: true, + show : {effect: 'fade' , duration: 250}, + hide : {effect: 'fade' , duration: 250}, + buttons: { + "Just do it!": function() { + $( this ).dialog( "close" ); + var cmd = '/plugins/dynamix.docker.manager/include/Exec.php?cmd=' + encodeURIComponent(imgCmd); + popupWithIframe(title, cmd, true); + }, + Cancel: function() { + $( this ).dialog( "close" ); + } + } + }); + $(".ui-dialog .ui-dialog-titlebar").addClass('menu'); + $(".ui-dialog .ui-dialog-title").css('text-align','center'); + $(".ui-dialog .ui-dialog-content").css('padding-top','15px'); + $(".ui-dialog .ui-dialog-content").css('font-weight','bold'); + $(".ui-button-text").css('padding','0px 5px'); + $( ".ui-dialog .ui-dialog-title" ).css( 'width', "100%"); +} + +function containerControl(container, action){ + $("#cmdStartStop").val("/plugins/dynamix.docker.manager/scripts/docker"); + $("#cmdArg1").val(action); + $("#cmdArg2").val(container); + $("#formStartStop").submit(); +} + +function reloadUpdate(){ + $(".updatecolumn").html(" checking..."); + $("#cmdStartStop").val("/plugins/dynamix.docker.manager/scripts/dockerupdate.php"); + $("#cmdArg1").remove(); + $("#cmdArg2").remove(); + $("#formStartStop").submit(); +} + +function autoStart(container, event){ + document.getElementsByName("container")[0].value = container; + $("#formStartStop").submit(); +} + +function containerLogs(container){ + openWindow('/plugins/dynamix.docker.manager/scripts/docker&arg1=logs&arg2=--tail=350&arg3=-f&arg4=' + container, 'Log for: ' + container, 600, 900); +} diff --git a/plugins/dynamix.docker.manager/log.htm b/plugins/dynamix.docker.manager/log.htm new file mode 100644 index 000000000..46baa5872 --- /dev/null +++ b/plugins/dynamix.docker.manager/log.htm @@ -0,0 +1,42 @@ + + + + + + + + + + + + +

+ + diff --git a/plugins/dynamix.docker.manager/scripts/docker b/plugins/dynamix.docker.manager/scripts/docker new file mode 100755 index 000000000..7f9e17f43 --- /dev/null +++ b/plugins/dynamix.docker.manager/scripts/docker @@ -0,0 +1,3 @@ +#!/bin/bash +# Just a wrapper. +exec /usr/bin/docker "$@" diff --git a/plugins/dynamix.docker.manager/scripts/docker_rm b/plugins/dynamix.docker.manager/scripts/docker_rm new file mode 100755 index 000000000..613b7e416 --- /dev/null +++ b/plugins/dynamix.docker.manager/scripts/docker_rm @@ -0,0 +1,10 @@ +#!/bin/bash + +# delete the docker image file +if [ -f /boot/config/docker.cfg ]; then + . /boot/config/docker.cfg + if [ "${DOCKER_IMAGE_FILE}" != "" -a -f "${DOCKER_IMAGE_FILE}" ]; then + echo "Deleting ${DOCKER_IMAGE_FILE} ..." + rm "${DOCKER_IMAGE_FILE}" + fi +fi diff --git a/plugins/dynamix.docker.manager/scripts/dockerupdate.php b/plugins/dynamix.docker.manager/scripts/dockerupdate.php new file mode 100755 index 000000000..2cb42649b --- /dev/null +++ b/plugins/dynamix.docker.manager/scripts/dockerupdate.php @@ -0,0 +1,52 @@ +#!/usr/bin/php + +verbose = true; break; + case 'check': $check = true; break;} +} + +if (!isset($check)) { + echo " Updating templates... "; + $docker->downloadTemplates(); + echo " Updating info... "; + $docker->getAllInfo(true); + echo " Done."; +} else { + require_once("/usr/local/emhttp/webGui/include/Wrappers.php"); + $client = new DockerClient(); + $update = new DockerUpdate(); + $notify = "/usr/local/emhttp/webGui/scripts/notify"; + $unraid = parse_plugin_cfg("dynamix",true); + $server = strtoupper($var['NAME']); + $output = $unraid['notify']['docker_notify']; + + $list = $client->getDockerContainers(); + $info = $docker->getAllInfo(); + foreach ($list as $ct) { + $name = $ct['Name']; + $image = $ct['Image']; + if ($info[$name]['updated'] == "false") { + $new = $update->getRemoteVersion($docker->getTemplateValue($image, "Registry"), $image); + exec("$notify -e 'Docker - $name [$new]' -s 'Notice [$server] - Docker update $new' -d 'A new version of $name is available' -i 'normal $output' -x"); + } + } +} +exit(0); +?> diff --git a/plugins/dynamix.docker.manager/styles/gh-buttons.css b/plugins/dynamix.docker.manager/styles/gh-buttons.css new file mode 100644 index 000000000..661167971 --- /dev/null +++ b/plugins/dynamix.docker.manager/styles/gh-buttons.css @@ -0,0 +1,440 @@ +/* ------------------------------------------ + * CSS3 GITHUB BUTTONS (Nicolas Gallagher) + * Licensed under Unlicense + * http://github.com/necolas/css3-github-buttons + * --------------------------------------- */ + + +/* ============================================================================= + Base Button + ========================================================================== */ + +.button { + position: relative; + overflow: visible; + display: inline-block; + padding: 0.5em 1em; + border: 1px solid #d4d4d4; + margin: 0; + text-decoration: none; + text-align: center; + text-shadow: 1px 1px 0 #fff; + font:11px/normal sans-serif; + color: #333; + white-space: nowrap; + cursor: pointer; + outline: none; + background-color: #ececec; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f4f4f4), to(#ececec)); + background-image: -moz-linear-gradient(#f4f4f4, #ececec); + background-image: -ms-linear-gradient(#f4f4f4, #ececec); + background-image: -o-linear-gradient(#f4f4f4, #ececec); + background-image: linear-gradient(#f4f4f4, #ececec); + -moz-background-clip: padding; /* for Firefox 3.6 */ + background-clip: padding-box; + border-radius: 0.2em; + /* IE hacks */ + zoom: 1; + *display: inline; +} + +.button:hover, +.button:focus, +.button:active, +.button.active { + border-color: #3072b3; + border-bottom-color: #2a65a0; + text-decoration: none; + text-shadow: -1px -1px 0 rgba(0,0,0,0.3); + color: #fff; + background-color: #3c8dde; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#599bdc), to(#3072b3)); + background-image: -moz-linear-gradient(#599bdc, #3072b3); + background-image: -o-linear-gradient(#599bdc, #3072b3); + background-image: linear-gradient(#599bdc, #3072b3); +} + +.button:active, +.button.active { + border-color: #2a65a0; + border-bottom-color: #3884cd; + background-color: #3072b3; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#3072b3), to(#599bdc)); + background-image: -moz-linear-gradient(#3072b3, #599bdc); + background-image: -ms-linear-gradient(#3072b3, #599bdc); + background-image: -o-linear-gradient(#3072b3, #599bdc); + background-image: linear-gradient(#3072b3, #599bdc); +} + +/* overrides extra padding on button elements in Firefox */ +.button::-moz-focus-inner { + padding: 0; + border: 0; +} + + +/* ============================================================================= + Button icons + ========================================================================== */ + +.button.icon:before { + content: ""; + position: relative; + top: 1px; + float:left; + width: 12px; + height: 12px; + margin: 0 0.75em 0 -0.25em; + background: url(gh-icons.png) 0 99px no-repeat; +} + +.button.arrowup.icon:before { background-position: 0 0; } +.button.arrowup.icon:hover:before, +.button.arrowup.icon:focus:before, +.button.arrowup.icon:active:before { background-position: -12px 0; } + +.button.arrowdown.icon:before { background-position: 0 -12px; } +.button.arrowdown.icon:hover:before, +.button.arrowdown.icon:focus:before, +.button.arrowdown.icon:active:before { background-position: -12px -12px; } + +.button.arrowleft.icon:before { background-position: 0 -24px; } +.button.arrowleft.icon:hover:before, +.button.arrowleft.icon:focus:before, +.button.arrowleft.icon:active:before { background-position: -12px -24px; } + +.button.arrowright.icon:before { float:right; margin: 0 -0.25em 0 0.5em; background-position: 0 -36px; } +.button.arrowright.icon:hover:before, +.button.arrowright.icon:focus:before, +.button.arrowright.icon:active:before { background-position: -12px -36px; } + +.button.approve.icon:before { background-position: 0 -48px; } +.button.approve.icon:hover:before, +.button.approve.icon:focus:before, +.button.approve.icon:active:before { background-position: -12px -48px; } + +.button.add.icon:before { background-position: 0 -288px; } +.button.add.icon:hover:before, +.button.add.icon:focus:before, +.button.add.icon:active:before { background-position: -12px -288px; } + +.button.remove.icon:before { background-position: 0 -60px; } +.button.remove.icon:hover:before, +.button.remove.icon:focus:before, +.button.remove.icon:active:before { background-position: -12px -60px; } + +.button.log.icon:before { background-position: 0 -72px; } +.button.log.icon:hover:before, +.button.log.icon:focus:before, +.button.log.icon:active:before { background-position: -12px -72px; } + +.button.calendar.icon:before { background-position: 0 -84px; } +.button.calendar.icon:hover:before, +.button.calendar.icon:focus:before, +.button.calendar.icon:active:before { background-position: -12px -84px; } + +.button.chat.icon:before { background-position: 0 -96px; } +.button.chat.icon:hover:before, +.button.chat.icon:focus:before, +.button.chat.icon:active:before { background-position: -12px -96px; } + +.button.clock.icon:before { background-position: 0 -108px; } +.button.clock.icon:hover:before, +.button.clock.icon:focus:before, +.button.clock.icon:active:before { background-position: -12px -108px; } + +.button.settings.icon:before { background-position: 0 -120px; } +.button.settings.icon:hover:before, +.button.settings.icon:focus:before, +.button.settings.icon:active:before { background-position: -12px -120px; } + +.button.comment.icon:before { background-position: 0 -132px; } +.button.comment.icon:hover:before, +.button.comment.icon:focus:before, +.button.comment.icon:active:before { background-position: -12px -132px; } + +.button.fork.icon:before { background-position: 0 -144px; } +.button.fork.icon:hover:before, +.button.fork.icon:focus:before, +.button.fork.icon:active:before { background-position: -12px -144px; } + +.button.like.icon:before { background-position: 0 -156px; } +.button.like.icon:hover:before, +.button.like.icon:focus:before, +.button.like.icon:active:before { background-position: -12px -156px; } + +.button.favorite.icon:before { background-position: 0 -348px; } +.button.favorite.icon:hover:before, +.button.favorite.icon:focus:before, +.button.favorite.icon:active:before { background-position: -12px -348px; } + +.button.home.icon:before { background-position: 0 -168px; } +.button.home.icon:hover:before, +.button.home.icon:focus:before, +.button.home.icon:active:before { background-position: -12px -168px; } + +.button.key.icon:before { background-position: 0 -180px; } +.button.key.icon:hover:before, +.button.key.icon:focus:before, +.button.key.icon:active:before { background-position: -12px -180px; } + +.button.lock.icon:before { background-position: 0 -192px; } +.button.lock.icon:hover:before, +.button.lock.icon:focus:before, +.button.lock.icon:active:before { background-position: -12px -192px; } + +.button.unlock.icon:before { background-position: 0 -204px; } +.button.unlock.icon:hover:before, +.button.unlock.icon:focus:before, +.button.unlock.icon:active:before { background-position: -12px -204px; } + +.button.loop.icon:before { background-position: 0 -216px; } +.button.loop.icon:hover:before, +.button.loop.icon:focus:before, +.button.loop.icon:active:before { background-position: -12px -216px; } + +.button.search.icon:before { background-position: 0 -228px; } +.button.search.icon:hover:before, +.button.search.icon:focus:before, +.button.search.icon:active:before { background-position: -12px -228px; } + +.button.mail.icon:before { background-position: 0 -240px; } +.button.mail.icon:hover:before, +.button.mail.icon:focus:before, +.button.mail.icon:active:before { background-position: -12px -240px; } + +.button.move.icon:before { background-position: 0 -252px; } +.button.move.icon:hover:before, +.button.move.icon:focus:before, +.button.move.icon:active:before { background-position: -12px -252px; } + +.button.edit.icon:before { background-position: 0 -264px; } +.button.edit.icon:hover:before, +.button.edit.icon:focus:before, +.button.edit.icon:active:before { background-position: -12px -264px; } + +.button.pin.icon:before { background-position: 0 -276px; } +.button.pin.icon:hover:before, +.button.pin.icon:focus:before, +.button.pin.icon:active:before { background-position: -12px -276px; } + +.button.reload.icon:before { background-position: 0 -300px; } +.button.reload.icon:hover:before, +.button.reload.icon:focus:before, +.button.reload.icon:active:before { background-position: -12px -300px; } + +.button.rss.icon:before { background-position: 0 -312px; } +.button.rss.icon:hover:before, +.button.rss.icon:focus:before, +.button.rss.icon:active:before { background-position: -12px -312px; } + +.button.tag.icon:before { background-position: 0 -324px; } +.button.tag.icon:hover:before, +.button.tag.icon:focus:before, +.button.tag.icon:active:before { background-position: -12px -324px; } + +.button.trash.icon:before { background-position: 0 -336px; } +.button.trash.icon:hover:before, +.button.trash.icon:focus:before, +.button.trash.icon:active:before { background-position: -12px -336px; } + +.button.user.icon:before { background-position: 0 -360px; } +.button.user.icon:hover:before, +.button.user.icon:focus:before, +.button.user.icon:active:before { background-position: -12px -360px; } + + +/* ============================================================================= + Button extensions + ========================================================================== */ + +/* Primary button + ========================================================================== */ + +.button.primary { + font-weight: bold; +} + +/* Danger button + ========================================================================== */ + +.button.danger { + color: #900; +} + +.button.danger:hover, +.button.danger:focus, +.button.danger:active { + border-color: #b53f3a; + border-bottom-color: #a0302a; + color: #fff; + background-color: #dc5f59; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#dc5f59), to(#b33630)); + background-image: -moz-linear-gradient(#dc5f59, #b33630); + background-image: -ms-linear-gradient(#dc5f59, #b33630); + background-image: -o-linear-gradient(#dc5f59, #b33630); + background-image: linear-gradient(#dc5f59, #b33630); +} + +.button.danger:active, +.button.danger.active { + border-color: #a0302a; + border-bottom-color: #bf4843; + background-color: #b33630; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b33630), to(#dc5f59)); + background-image: -moz-linear-gradient(#b33630, #dc5f59); + background-image: -ms-linear-gradient(#b33630, #dc5f59); + background-image: -o-linear-gradient(#b33630, #dc5f59); + background-image: linear-gradient(#b33630, #dc5f59); +} + +/* Green button + ========================================================================== */ + +.button.green { + color: #009900; +} + +.button.blue { + color: #0519FF; +} + +.button.blue:hover, +.button.blue:focus, +.button.blue:active { + color: #fff; +} + +.button.green:hover, +.button.green:focus, +.button.green:active { + color: #FFF; + text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.25); + background-color: #60B044; + background-image: linear-gradient(#8ADD6D, #60B044); + background-repeat: repeat-x; + border-color: #5CA941; +} + +.button.green:active, +.button.green.active { + color: #FFF; + text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.25); + background-color: #60B044; + background-image: linear-gradient(#60B044, #8ADD6D); + background-repeat: repeat-x; + border-color: #5CA941; +} + +/* Pill button + ========================================================================== */ + +.button.pill { + border-radius: 50em; +} + +/* Disabled button + ========================================================================== */ + +.button.disable { + opacity: 0.5; +} + +/* Big button + ========================================================================== */ + +.button.big { + font-size: 14px; +} + +.button.big.icon:before { + top: 0; +} + + +/* ============================================================================= + Button groups + ========================================================================== */ + +/* Standard group + ========================================================================== */ + +.button-group { + display: inline-block; + list-style: none; + padding: 0; + margin: 0; + /* IE hacks */ + zoom: 1; + *display: inline; +} + +.button + .button, +.button + .button-group, +.button-group + .button, +.button-group + .button-group { + margin-left: 15px; +} + +.button-group li { + float: left; + padding: 0; + margin: 0; +} + +.button-group .button { + float: left; + margin-left: -1px; +} + +.button-group > .button:not(:first-child):not(:last-child), +.button-group li:not(:first-child):not(:last-child) .button { + border-radius: 0; +} + +.button-group > .button:first-child, +.button-group li:first-child .button { + margin-left: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.button-group > .button:last-child, +.button-group li:last-child > .button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +/* Minor group + ========================================================================== */ + +.button-group.minor-group .button { + border: 1px solid #d4d4d4; + text-shadow: none; + background-image: none; + background-color: #fff; +} + +.button-group.minor-group .button:hover, +.button-group.minor-group .button:focus { + background-color: #599bdc; +} + +.button-group.minor-group .button:active, +.button-group.minor-group .button.active { + background-color: #3072b3; +} + +.button-group.minor-group .button.icon:before { + opacity: 0.8; +} + +/* ============================================================================= + Button container (mixing buttons and groups, e.g., nav bar) + ========================================================================== */ + +.button-container .button, +.button-container .button-group { + vertical-align: top; +} + diff --git a/plugins/dynamix.plugin.manager/PluginInstall.page b/plugins/dynamix.plugin.manager/PluginInstall.page new file mode 100644 index 000000000..fe5fb64eb --- /dev/null +++ b/plugins/dynamix.plugin.manager/PluginInstall.page @@ -0,0 +1,34 @@ +Menu="Plugins" +Title="Install Plugin" +--- + + +**Enter URL of remote plugin file or local plugin file** + +
+ + +
+ +> To download and install a plugin, enter the plg file URL and click **Install**. A window will open +> that displays install progress. Do not close this window until install has completed. You may also specify +> the local file name of an extension. + +**Select local plugin file** +
diff --git a/plugins/dynamix.plugin.manager/Plugins.page b/plugins/dynamix.plugin.manager/Plugins.page new file mode 100644 index 000000000..7e15c05da --- /dev/null +++ b/plugins/dynamix.plugin.manager/Plugins.page @@ -0,0 +1,49 @@ +Menu="Tasks:50" +Type="xmenu" +Title="Installed Plugins" +Tabs="true" +--- + + + + + + + + + +
+Click check for updates to check all plugins. This page might take some time to load depending on your internet connection and how many plugins need to be checked. +
+ + + + +
PluginAuthorVersionStatus

Please wait, retrieving plugin information ...
diff --git a/plugins/dynamix.plugin.manager/PluginsAvailable.page- b/plugins/dynamix.plugin.manager/PluginsAvailable.page- new file mode 100644 index 000000000..ce5f857a4 --- /dev/null +++ b/plugins/dynamix.plugin.manager/PluginsAvailable.page- @@ -0,0 +1,11 @@ +Menu="Plugins" +Title="Available Plugins" +--- +Here should come an online repository of available plugins, which can be downloaded from the web +and instantly installed. + +Problably this should be hosted on a wiki page of Limetech, with control over which plugins can be +displayed. This may be combined with a validation procedure to ensure that published plugins are +fully compatible with unRAID. + +For the time being only manual installation of plugins exist. \ No newline at end of file diff --git a/plugins/dynamix.plugin.manager/PluginsError.page b/plugins/dynamix.plugin.manager/PluginsError.page new file mode 100644 index 000000000..7cb934e38 --- /dev/null +++ b/plugins/dynamix.plugin.manager/PluginsError.page @@ -0,0 +1,39 @@ +Menu="Plugins" +Title="Plugin File Install Errors" +Cond="glob('/boot/config/plugins-error/*.plg')" +--- + + +"; +echo "Plugin FileStatus"; +echo ""; + +foreach (glob("/boot/config/plugins-error/*.plg", GLOB_NOSORT) as $plugin_file) { + // status info + $status_info = "ERROR
" . make_link("delete", $plugin_file); + + echo ""; + echo "{$plugin_file}"; + echo "{$status_info}"; + echo ""; +} + +echo ""; +?> + +> These plugins were not installed because of some kind of installation error. You should delete these +> plugins and then **reboot** your server.* \ No newline at end of file diff --git a/plugins/dynamix.plugin.manager/PluginsStale.page b/plugins/dynamix.plugin.manager/PluginsStale.page new file mode 100644 index 000000000..a0427c6dd --- /dev/null +++ b/plugins/dynamix.plugin.manager/PluginsStale.page @@ -0,0 +1,70 @@ +Menu="Plugins" +Title="Plugin History" +Cond="glob('/boot/config/plugins-stale/*.plg')" +--- + + +"; +echo "PluginAuthorVersionStatus"; +echo ""; + +foreach (glob("/boot/config/plugins-stale/*.plg", GLOB_NOSORT) as $plugin_file) { + // plugin name + $name = plugin("name", $plugin_file); + if ($name === false) $name = basename($plugin_file, ".plg"); + + // icon + $icon = icon($name); + + // desc + $readme = "plugins/{$name}/README.md"; + if (file_exists($readme)) + $desc = Markdown(file_get_contents($readme)); + else + $desc = Markdown("**{$name}**"); + + // author + $author = plugin("author", $plugin_file); + if ($author === false) $author = "anonymous"; + + // version + $version = plugin("version", $plugin_file); + if ($version === false) $version = "unknown"; + + // version info + $version_info = $version; + + // status info + $status_info = "STALE"; + + // action + $action = make_link("delete", $plugin_file); + + // echo our plugin information + echo ""; + echo ""; + echo "{$desc}"; + echo "{$author}"; + echo "{$version_info}"; + echo "{$status_info}"; + echo "{$action}"; + echo ""; +} + +echo ""; +?> +> These plugins were not installed because newer code already exists. It is safe to simply delete these. \ No newline at end of file diff --git a/plugins/dynamix.plugin.manager/icons/availableplugins.png b/plugins/dynamix.plugin.manager/icons/availableplugins.png new file mode 100644 index 000000000..8ca75fe52 Binary files /dev/null and b/plugins/dynamix.plugin.manager/icons/availableplugins.png differ diff --git a/plugins/dynamix.plugin.manager/icons/installedplugins.png b/plugins/dynamix.plugin.manager/icons/installedplugins.png new file mode 100644 index 000000000..445c18868 Binary files /dev/null and b/plugins/dynamix.plugin.manager/icons/installedplugins.png differ diff --git a/plugins/dynamix.plugin.manager/icons/installplugin.png b/plugins/dynamix.plugin.manager/icons/installplugin.png new file mode 100644 index 000000000..ac50aa453 Binary files /dev/null and b/plugins/dynamix.plugin.manager/icons/installplugin.png differ diff --git a/plugins/dynamix.plugin.manager/icons/pluginhistory.png b/plugins/dynamix.plugin.manager/icons/pluginhistory.png new file mode 100644 index 000000000..b6cb0ecf7 Binary files /dev/null and b/plugins/dynamix.plugin.manager/icons/pluginhistory.png differ diff --git a/plugins/dynamix.plugin.manager/images/dynamix.plugin.manager.png b/plugins/dynamix.plugin.manager/images/dynamix.plugin.manager.png new file mode 100644 index 000000000..92ad62bb4 Binary files /dev/null and b/plugins/dynamix.plugin.manager/images/dynamix.plugin.manager.png differ diff --git a/plugins/dynamix.plugin.manager/include/PluginHelpers.php b/plugins/dynamix.plugin.manager/include/PluginHelpers.php new file mode 100644 index 000000000..86368aa0a --- /dev/null +++ b/plugins/dynamix.plugin.manager/include/PluginHelpers.php @@ -0,0 +1,47 @@ + +"; + $disabled = $check ? " disabled" : ""; + $cmd = $method == "delete" ? "/plugins/dynamix.plugin.manager/scripts/plugin_rm&arg1=$arg" : "/plugins/dynamix.plugin.manager/scripts/plugin&arg1=$method&arg2=$arg"; + return "{$check}"; +} + +// trying our best to find an icon +function icon($name) { +// this should be the default location and name + $icon = "plugins/{$name}/images/{$name}.png"; + if (file_exists($icon)) return $icon; +// try alternatives if default is not present + $plugin = strtok($name, '.'); + $icon = "plugins/{$plugin}/images/{$plugin}.png"; + if (file_exists($icon)) return $icon; + $icon = "plugins/{$plugin}/images/{$name}.png"; + if (file_exists($icon)) return $icon; + $icon = "plugins/{$plugin}/{$plugin}.png"; + if (file_exists($icon)) return $icon; + $icon = "plugins/{$plugin}/{$name}.png"; + if (file_exists($icon)) return $icon; +// last resort - plugin manager icon + return "plugins/dynamix.plugin.manager/images/dynamix.plugin.manager.png"; +} +?> diff --git a/plugins/dynamix.plugin.manager/include/ShowChanges.php b/plugins/dynamix.plugin.manager/include/ShowChanges.php new file mode 100644 index 000000000..e45fab14f --- /dev/null +++ b/plugins/dynamix.plugin.manager/include/ShowChanges.php @@ -0,0 +1,27 @@ + + + + + + + + + +
+ + diff --git a/plugins/dynamix.plugin.manager/include/ShowPlugins.php b/plugins/dynamix.plugin.manager/include/ShowPlugins.php new file mode 100644 index 000000000..3bfeaee91 --- /dev/null +++ b/plugins/dynamix.plugin.manager/include/ShowPlugins.php @@ -0,0 +1,81 @@ + +"; + else + $link = ""; +// desc + $readme = "plugins/{$name}/README.md"; + if (file_exists($readme)) + $desc = Markdown(file_get_contents($readme)); + else + $desc = Markdown("**{$name}**"); +// author + $author = plugin("author", $plugin_file); + if ($author === false) $author = "anonymous"; +// version + $version = plugin("version", $plugin_file); + if ($version === false) $version = "unknown"; +// version info + $version_info = $version; +// status info + $status_info = "no update"; + $changes_file = $plugin_file; + $URL = plugin("pluginURL", $plugin_file); + if ($URL !== false) { + $filename = "/tmp/plugins/".basename($URL); + if (file_exists($filename)) { + $latest = plugin("version", $filename); + if ($latest && strcmp($latest, $version) > 0) { + $version_info .= "
{$latest}"; + $status_info = make_link("update", basename($plugin_file)); + $changes_file = $filename; + } else { + $status_info = "up-to-date"; + } + } else { + if ($_GET['stale']) $status_info = "unknown"; + } + } + $changes = plugin("changes", $changes_file); + if ($changes !== false) { + $txtfile = "/tmp/plugins/".basename($plugin_file,'.plg').".txt"; + file_put_contents($txtfile, $changes); + $version_info .= " "; + } +// action + $action = strpos($plugin_file, "/usr/local/emhttp/plugins") !== 0 ? make_link("remove", basename($plugin_file)) : "built-in"; +// write plugin information + echo ""; + echo "

{$link}

"; + echo "{$desc}"; + echo "{$author}"; + echo "{$version_info}"; + echo "{$status_info}"; + echo "{$action}"; + echo ""; +} +?> diff --git a/plugins/dynamix.plugin.manager/scripts/plugin b/plugins/dynamix.plugin.manager/scripts/plugin new file mode 100755 index 000000000..0e51462de --- /dev/null +++ b/plugins/dynamix.plugin.manager/scripts/plugin @@ -0,0 +1,540 @@ +#!/usr/bin/php + tag within PLUGIN-FILE. If the attribute exists, its value (a string) is output and the command +exit status is 0. If the attribute does not exist, command exit status is 1. + +The plugin manager recognizes this set of attributes for the tag: + +name - MANDATORY plugin name, e.g., "myplugin" or "my-plugin" etc. + This tag defines the name of the plugin. The name should omit embedded information such as architecture, + version, author, etc. + + The plugin should create a directory under `/usr/local/emhttp/plugins` named after + the plugin, e.g., `/usr/local/emhttp/plugins/myplugin`. Any webGui pages, icons, README files, etc, should + be created inside this directory. + + The plugin should also create a directory under `/boot/config/plugins` named after the plugin, e.g., + `/boot/config/plugins/myplugin`. Here is where you store plugin-specific files such as a configuration + file and icon file. Note that this directory exists on the users USB Flash device and writes should be + minimized. + + Upon successful installation, the plugin manager will copy the input plugin file to `/boot/config/plugins` + on the users USB Flash device, and create a symlink in `/var/log/plugins` also named after the plugin, + e.g., `/var/log/plugins/myplugin`. Each time the unRaid server is re-booted, all plugins stored + in `/boot/config/plugins` are automatically installed; plugin authors should be aware of this behavior. + +author - OPTIONAL + Whatever you put here will show up under the **Author** column in the Plugin List. If this attribute + is omitted we display "anonymous". + +version - MANDATORY + Use a string suitable for comparison to determine if one version is older/same/newer than another version. + Any format is acceptable but LimeTech uses "YYYY.MM.DD", e.g., "2014.02.18" (if multiple versions happen + to get posted on the same day we add a letter suffix, e.g., "2014.02.18a"). + +pluginURL - OPTIONAL but MANDATORY if you want "check for updates" to work with your plugin + This is the URL of the plugin file to download and extract the **version** attribute from to determine if + this is a new version. + +system - OPTIONAL + If present the plugin is considered a system plugin and is installed in '/boot/plugins'. + User plugins get installed in '/boot/config/plugins', which is the default. + +More attributes may be defined in the future. + +Here is the set of directories and files used by the plugin system: + +/boot/config/plugins/ + This directory contains the plugin files for plugins to be (re)installed at boot-time. Upon + successful `plugin install`, the plugin file is copied here (if not here already). Upon successful + `plugin remove`, the plugin file is deleted from here. + +/boot/config/plugins-error/ + This directory contains plugin files that failed to install. + +/boot/config/plugins-removed/ + This directory contains plugin files that have been removed. + +/boot/config/plugins-stale/ + This directory contains plugin files that failed to install because a newer version of the same plugin is + already installed. + +/tmp/plugins/ + This directory is used as a target for downloaded plugin files. The `plugin check` operation + downloads the plugin file here and the `plugin update` operation looks for the plugin to update here. + +/var/log/plugins/ + This directory contains a symlink named after the plugin name (not the plugin file name) which points to + the actual plugin file used to install the plugin. The existence of this file indicates successful + install of the plugin. + +EOF; + +// Download a file from a URL. +// Returns TRUE if success else FALSE and fills in error. +// +function download($URL, $name, &$error) { + if ($file = popen("wget --progress=dot -O $name $URL 2>&1", 'r')) { + echo "plugin: downloading: $URL ...\r"; + $level = -1; + while (!feof($file)) { + if (preg_match("/\d+%/", fgets($file), $matches)) { + $percentage = substr($matches[0],0,-1); + if ($percentage > $level) { + echo "plugin: downloading: $URL ... $percentage%\r"; + $level = $percentage; + } + } + } + if (pclose($file) != -1) { + echo "plugin: downloading: $URL ... done\n"; + return true; + } else { + echo "plugin: downloading: $URL ... failed\n"; + $error = "wget: $URL download failure"; + return false; + } + } else { + $error = "wget: $URL failed to open"; + return false; + } +} + +// Deal with logging message. +// +function logger($message) { +// echo "$message\n"; + shell_exec( "logger $message"); +} + +// Interpret a plugin file +// Returns TRUE if success, else FALSE and fills in error string. +// +// If a FILE element does not have a Method attribute, we treat as though Method is "install". +// A FILE Method attribute can list multiple methods separated by spaces in which case that file +// is processed for any of those methods. +// +function plugin($method, $plugin_file, &$error) { + $methods = array("install", "remove"); + + // parse plugin definition XML file + $xml = simplexml_load_file($plugin_file, NULL, LIBXML_NOCDATA); + if ($xml === false) { + $error = "xml parse error"; + return false; + } + + // dump + if ($method == "dump") { + // echo $xml->asXML(); + echo print_r($xml); + return true; + } + + // release notes + if ($method == 'changes') { + if (!$xml->CHANGES) return false; + return trim($xml->CHANGES); + } + + // check if $method is an attribute + if (!in_array($method, $methods)) { + foreach ($xml->attributes() as $key => $value) { + if ($method == $key) return $value; + } + $error = "$method attribute not present"; + return false; + } + + // Process FILE elements in order + // + foreach ($xml->FILE as $file) { + // skip if not our $method + if (isset($file->attributes()->Method)) { + if (!in_array($method, explode(" ", $file->attributes()->Method))) continue; + } else if ($method != "install") continue; + + // Name can be missing but only makes sense if Run attribute is present + $name = $file->attributes()->Name; + if ($name) { + // Ensure parent directory exists + // + if (!file_exists(dirname($name))) { + if (!mkdir(dirname($name), 0770, true)) { + $error = "plugin: error: unable to create parent directory for $name"; + return false; + } + } + // If file already exists, do not overwrite + // + if (file_exists($name)) { + logger("plugin: skipping: $name already exists"); + } elseif ($file->LOCAL) { + // Create the file + // + // for local file, just copy it + logger("plugin: creating: $name - copying LOCAL file $file->LOCAL"); + if (!copy($file->LOCAL, $name)) { + $error = "unable to copy LOCAL file: $name"; + @unlink($name); + return false; + } + } elseif ($file->INLINE) { + // for inline file, create with inline contents + logger("plugin: creating: $name - from INLINE content"); + $contents = trim($file->INLINE).PHP_EOL; + if ($file->attributes()->Type == 'base64') { + logger("plugin: decoding: $name as base64"); + $contents = base64_decode($contents); + if ($contents === false) { + $error = "unable to decode inline base64: $name"; + return false; + } + } + if (!file_put_contents($name, $contents)) { + $error = "unable to create file: $name"; + @unlink($name); + return false; + } + } elseif ($file->URL) { + // for download file, download and maybe verify the file MD5 + logger("plugin: creating: $name - downloading from URL $file->URL"); + if (download($file->URL, $name, $error) === false) { + @unlink($name); + return false; + } + if ($file->MD5) { + logger("plugin: checking: $name - MD5"); + if (md5_file($name) != $file->MD5) { + $error = "bad file MD5: $name"; + unlink($name); + return false; + } + } + } + // Maybe change the file mode + // + if ($file->attributes()->Mode) { + // if file has 'Mode' attribute, apply it + $mode = $file->attributes()->Mode; + logger("plugin: setting: $name - mode to $mode"); + if (!chmod($name, octdec($mode))) { + $error = "chmod failure: $name"; + return false; + } + } + } + // Maybe "run" the file now + // + if ($file->attributes()->Run) { + $command = $file->attributes()->Run; + if ($name) { + logger("plugin: running: $name"); + system("$command $name", $retval); + } elseif ($file->LOCAL) { + logger("plugin: running: $file->LOCAL"); + system("$command $file->LOCAL", $retval); + } elseif ($file->INLINE) { + logger("plugin: running: 'anonymous'"); + $inline = escapeshellarg($file->INLINE); + passthru("echo $inline | $command", $retval); + } + if ($retval) { + $error = "run failed: $command retval: $retval"; + return false; + } + } + } + return true; +} + +function move($src_file, $tar_dir) { + @mkdir($tar_dir); + return rename($src_file, $tar_dir."/".basename($src_file)); +} + +// In following code, +// $plugin - is a basename of a plugin, eg, "myplugin.plg" +// $plugin_file - is an absolute path, eg, "/boot/config/plugins/myplugin.plg" +// +if ($argc < 2) { + echo $usage; + exit(1); +} +$method = $argv[1]; + +// plugin checkall +// check all installed plugins +// +if ($method == "checkall") { + foreach (glob("/var/log/plugins/*", GLOB_NOSORT) as $link) { + // only consider symlinks + $installed_plugin_file = @readlink($link); + if ($installed_plugin_file === false) continue; + if (plugin("pluginURL", $installed_plugin_file, $error) === false) continue; + $plugin = basename($installed_plugin_file); + echo "plugin: checking $plugin ...\n"; + exec(realpath($argv[0]) . " check $plugin", $output, $retval); + } + exit(0); +} + +if ($argc < 3) { + echo $usage; + exit(1); +} + +// plugin install [plugin_file] +// cases: +// a) dirname of [plugin_file] is /boot/config/plugins (system startup) +// b) [plugin_file] is a URL +// c) dirname of [plugin_file] is not /boot/config/plugins +// +if ($method == "install") { + echo "plugin: installing: $argv[2]\n"; + // check for URL + if ((strpos($argv[2], "http://") === 0) || (strpos($argv[2], "https://") === 0)) { + $pluginURL = $argv[2]; + echo "plugin: downloading $pluginURL\n"; + $plugin_file = "/tmp/plugins/".basename($pluginURL); + if (!download($pluginURL, $plugin_file, $error)) { + echo "plugin: $error\n"; + @unlink($plugin_file); + exit(1); + } + } else + $plugin_file = realpath($argv[2]); + + $plugin = basename($plugin_file); + + // check for re-install + $installed_plugin_file = @readlink("/var/log/plugins/$plugin"); + if ($installed_plugin_file !== false) { + if ($plugin_file == $installed_plugin_file) { + echo "plugin: not re-installing same plugin\n"; + exit(1); + } + // must have version attributes for re-install + $version = plugin("version", $plugin_file, $error); + if ($version === false) { + echo "plugin: $error\n"; + exit(1); + } + $installed_version = plugin("version", $installed_plugin_file, $error); + if ($installed_version === false) { + echo "plugin: $error\n"; + exit(1); + } + // do not re-install if same plugin already installed or has higher version + if (strcmp($version, $installed_version) <= 0) { + if (strcmp($version, $installed_version) < 0) { + echo "plugin: not installing older version\n"; + } + if (strcmp($plugin_version, $installed_version) == 0) { + echo "plugin: not reinstalling same version\n"; + } + exit(1); + } + if (plugin("install", $plugin_file, $error) === false) { + echo "plugin: $error\n"; + if (dirname($plugin_file) == "/boot/config/plugins") { + move($plugin_file, "/boot/config/plugins-error"); + } + exit(1); + } + unlink("/var/log/plugins/$plugin"); + } else { + // fresh install + if (plugin("install", $plugin_file, $error) === false) { + echo "plugin: $error\n"; + if (dirname($plugin_file) == "/boot/config/plugins") { + move($plugin_file, "/boot/config/plugins-error"); + } + exit(1); + } + } + + // register successful install + // Bergware change: add user or system plugin selection + $plugintype = plugin("plugintype", $plugin_file, $error); + $target = $plugintype != "system" ? "/boot/config/plugins/$plugin" : "/boot/plugins/$plugin"; + if ($target != $plugin_file) copy($plugin_file, $target); + symlink($target, "/var/log/plugins/$plugin"); + echo "plugin: installed\n"; + exit(0); +} + +// plugin remove [plugin] +// only .plg files should have a remove method +// +if ($method == "remove") { + echo "plugin: removing: {$argv[2]}\n"; + $plugin = $argv[2]; + $installed_plugin_file = @readlink("/var/log/plugins/$plugin"); + if ($installed_plugin_file !== false) { + // remove the symlink + unlink("/var/log/plugins/$plugin"); + if (plugin("remove", $installed_plugin_file, $error) === false) { + // but if can't remove, restore the symlink + symlink($installed_plugin_file, "/var/log/plugins/$plugin"); + echo "plugin: $error\n"; + exit(1); + } + } + // remove the plugin file + move($installed_plugin_file, "/boot/config/plugins-removed"); + echo "plugin: removed\n"; + exit(0); +} + +// plugin check [plugin] +// We use the pluginURL attribute to download the latest plg file into the "/tmp/plugins/" +// directory. +// +if ($method == "check") { + echo "plugin: checking: {$argv[2]}\n"; + $plugin = $argv[2]; + $installed_plugin_file = @readlink("/var/log/plugins/$plugin"); + if ($installed_plugin_file === false) { + echo "plugin: not installed\n"; + exit(1); + } + $installed_pluginURL = plugin("pluginURL", $installed_plugin_file, $error); + if ($installed_pluginURL === false) { + echo "plugin: $error\n"; + exit(1); + } + $plugin_file = "/tmp/plugins/$plugin"; + if (!download($installed_pluginURL, $plugin_file, $error)) { + echo "plugin: $error\n"; + @unlink($plugin_file); + exit(1); + } + $version = plugin("version", $plugin_file, $error); + if ($version === false) { + echo "plugin: $error\n"; + exit(1); + } + echo "$version\n"; + exit(0); +} + +// plugin update [plugin] +// [plugin] is the plg file we are going to be replacing, eg, "old.plg". +// We assume a "check" has already been done, ie, "/tmp/plugins/new.plg" already exists. +// We execute the "install" method of new.plg. If this fails, then we mark old.plg "not installed"; +// the plugin manager will recognize this as an install error. +// If install new.plg succeeds, then we remove old.plg and copy new.plg in place. +// Finally we mark the new.plg "installed". +// +if ($method == "update") { + echo "plugin: updating: {$argv[2]}\n"; + $plugin = $argv[2]; + $installed_plugin_file = @readlink("/var/log/plugins/$plugin"); + if ($installed_plugin_file === false) { + echo "plugin: not installed\n"; + exit(1); + } + // verify previous check has been done + $plugin_file = "/tmp/plugins/$plugin"; + if (!file_exists($plugin_file)) { + echo "plugin: $plugin_file does not exist, check first\n"; + exit (1); + } + // install the updated plugin + if (plugin("install", $plugin_file, $error) === false) { + echo "plugin: $error\n"; + exit(1); + } + // install was successful, save the updated plugin so it installs again next boot + unlink("/var/log/plugins/$plugin"); + copy($plugin_file, "/boot/config/plugins/$plugin"); + symlink("/boot/config/plugins/$plugin", "/var/log/plugins/$plugin"); + echo "plugin: updated\n"; + exit(0); +} + +// +// +$plugin_file = $argv[2]; +$value = plugin($method, $plugin_file, $error); +if ($value === false) { + echo "plugin: $error\n"; + exit(1); +} +echo "$value\n"; +exit(0); +?> diff --git a/plugins/dynamix.plugin.manager/scripts/plugin_rm b/plugins/dynamix.plugin.manager/scripts/plugin_rm new file mode 100755 index 000000000..b7f63e8bc --- /dev/null +++ b/plugins/dynamix.plugin.manager/scripts/plugin_rm @@ -0,0 +1,6 @@ +#!/bin/bash + +# put some restrictions on 'rm' +echo "Deleting $1 ..." +[[ $1 == /boot/config/plugins-error/* ]] && rm $1 +[[ $1 == /boot/config/plugins-stale/* ]] && rm $1 diff --git a/plugins/dynamix.plugin.manager/scripts/plugincheck b/plugins/dynamix.plugin.manager/scripts/plugincheck new file mode 100755 index 000000000..2dae311ce --- /dev/null +++ b/plugins/dynamix.plugin.manager/scripts/plugincheck @@ -0,0 +1,42 @@ +#!/usr/bin/php + +/dev/null"); +foreach (glob("/tmp/plugins/*.plg", GLOB_NOSORT) as $file) { + $plg = basename($file); + $old = exec("$plugin version '/var/log/plugins/$plg'"); + unset($new); + exec("$plugin version '$file'", $new, $error); + // Silently suppress bad download of PLG file + if ($error) continue; + $new = $new[0]; + if (strcmp($new, $old) > 0) { + $name = basename($file, '.plg'); + exec("$notify -e 'Plugin - $name [$new]' -s 'Notice [$server] - Version update $new' -d 'A new version of $name is available' -i 'normal $output' -x"); + } +} +exit(0); +?> diff --git a/plugins/dynamix.vm.manager/AddVM.page b/plugins/dynamix.vm.manager/AddVM.page new file mode 100644 index 000000000..04cc15418 --- /dev/null +++ b/plugins/dynamix.vm.manager/AddVM.page @@ -0,0 +1,20 @@ +Title="Add VM" +Cond="(pgrep('libvirtd')!==false)" +Markdown="false" +--- + + + \ No newline at end of file diff --git a/plugins/dynamix.vm.manager/UpdateVM.page b/plugins/dynamix.vm.manager/UpdateVM.page new file mode 100644 index 000000000..05dd9ee18 --- /dev/null +++ b/plugins/dynamix.vm.manager/UpdateVM.page @@ -0,0 +1,20 @@ +Title="Update VM" +Cond="(pgrep('libvirtd')!==false)" +Markdown="false" +--- + + + \ No newline at end of file diff --git a/plugins/dynamix.vm.manager/VMMachines.page b/plugins/dynamix.vm.manager/VMMachines.page new file mode 100644 index 000000000..6a740e603 --- /dev/null +++ b/plugins/dynamix.vm.manager/VMMachines.page @@ -0,0 +1,643 @@ +Menu="VMs" +Title="Virtual Machines" +Cond="(pgrep('libvirtd')!==false)" +--- + + +domain_is_active($name)){ + echo ""; + $msg = "Waiting for $name to shutdown..."; + }else{ + echo ""; + $msg = "$name has been shutdown"; + } +} +if ($action) { + +}else{ + if ($subaction) { + $domName = $lv->domain_get_name_by_uuid($uuid); + if ($subaction == 'domain-diskdev') { + $msg = $lv->domain_set_disk_dev($domName, $_GET['olddev'], $_POST['diskdev']) ? + $domName.' disk dev has been changed from '.$_GET['olddev'].' to '.$_POST['diskdev']: + 'Error: '.$lv->get_last_error(); + } + elseif ($subaction == 'disk-resize') { + $capacity = str_replace(array("KB","MB","GB","TB","PB", " ", ","), array("K","M","G","T","P", "", ""), strtoupper($_POST['cap'])); + $oldcap = str_replace(array("KB","MB","GB","TB","PB", " ", ","), array("K","M","G","T","P", "", ""), strtoupper($_GET['oldcap'])); + if (substr($oldcap,0,-1) < substr($capacity,0,-1)){ + shell_exec("qemu-img resize -q " . escapeshellarg($_GET['disk']) . " $capacity"); + $msg = $domName." disk capacity has been changed to $capacity"; + }else { + $msg = "Error: disk capacity has to be greater than {$_GET['oldcap']}"; + } + } + elseif ($subaction == 'cdrom-change') { + $msg = $lv->domain_change_cdrom($domName, $_POST['cdrom'], $_GET['dev'], $_GET['bus']) ? + "$domName cdrom has been changed" : + "Error: ".$lv->get_last_error(); + } + elseif ($subaction == 'memory-change') { + $msg = $lv->domain_set_memory($domName, $_GET['memory']*1024) ? + "$domName memory has been changed to ".$_GET['memory']." MiB" : + "Error: ".$lv->get_last_error(); + } + elseif ($subaction == 'vcpu-change') { + $vcpu = $_GET['vcpu']; + $msg = $lv->domain_set_vcpu($domName, $vcpu) ? + "$domName vcpu number has been changed to $vcpu" : + "Error: ".$lv->get_last_error(); + } + elseif ($subaction == 'bootdev-change') { + $bootdev = $_GET['bootdev']; + $msg = $lv->domain_set_boot_device($domName, $bootdev) ? + "$domName boot device has been changed to $bootdev" : + "Error: ".$lv->get_last_error(); + } + elseif ($subaction == 'disk-remove') { + $msg = $lv->domain_disk_remove($domName, $_GET['dev']) ? + "$domName disk has been removed" : + 'Error: '.$lv->get_last_error(); + } + elseif ($subaction == 'snap-create') { + $msg = $lv->domain_snapshot_create($domName) ? + "Snapshot for $domName has been created" : + 'Error: '.$lv->get_last_error(); + } + elseif ($subaction == 'snap-delete') { + $msg = $lv->domain_snapshot_delete($domName, $_GET['snap']) ? + "Snapshot for $domName has been deleted" : + 'Error: '.$lv->get_last_error(); + } + elseif ($subaction == 'snap-revert') { + $msg = $lv->domain_snapshot_revert($domName, $_GET['snap']) ? + "$domName has been reverted" : + 'Error: '.$lv->get_last_error(); + } + elseif ($subaction == 'snap-desc') { + $msg = $lv->snapshot_set_metadata($domName, $_GET['snap'], $_POST['snapdesc']) ? + "Snapshot description for $domName has been saved": + 'Error: '.$lv->get_last_error(); + } + echo ""; + } + echo " + + + + + + + + + + + + + "; + //Get domain variables for each domain + if ($libvirt_running == "yes") + $doms = $lv->get_domains(); + if(!is_array($doms)){ + if ($libvirt_running == "yes") { + echo ""; + $msg = "No VMs defined. Create from template or add XML."; + } else { + $msg = "Libvirt is not running. Goto Settings tab then click Start."; + } + } else { + sort($doms); + $contextMenus = array(); + + for ($i = 0; $i < sizeof($doms); $i++) { + $name = $doms[$i]; + $res = $lv->get_domain_by_name($name); + $uuid = $lv->domain_get_uuid($res); + $dom = $lv->domain_get_info($res); + $id = $lv->domain_get_id($res); + $is_autostart = $lv->domain_get_autostart($res); + $state = $lv->domain_state_translate($dom['state']); + switch ($state) { + case 'running': + $shape = 'play'; + $status = 'started'; + break; + case 'paused': //no break on purpose + case 'pmsuspended': + $shape = 'pause'; + $status = 'paused'; + break; + default: + $shape = 'square'; + $status = 'stopped'; + break; + } + if ($state == 'running') { + $mem = $dom['memory'] / 1024; + }else{ + $mem = $lv->domain_get_memory($res)/1024; + } + $mem = number_format($mem, 0); + $vcpu = $dom['nrVirtCpu']; + $auto = $is_autostart ? 'checked="checked"':""; + $template = $lv->_get_single_xpath_result($res, '//domain/metadata/vmtemplate/@name'); + if (empty($template)) { + $template = 'Custom'; + } + $log = (is_file('/var/log/libvirt/qemu/' . $name . '.log') ? 'libvirt/qemu/' . $name . '.log' : ''); + if (($diskcnt = $lv->get_disk_count($res)) > 0) { + $disks = $diskcnt.' / '.$lv->get_disk_capacity($res); + $diskdesc = 'Current physical size: '.$lv->get_disk_capacity($res, true); + }else{ + $disks = '-'; + $diskdesc = ''; + } + + $vncport = $lv->domain_get_vnc_port($res); + + $vnc = ''; + if ($vncport > 0) { + $wsport = $lv->domain_get_ws_port($res); + $vnc = '/plugins/dynamix.vm.manager/vnc.html?autoconnect=true&host=' . $var['IPADDR'] . '&port=' . $wsport; + } else { + $vncport = ($vncport < 0) ? "auto" : ""; + } + + $bootdev = $lv->domain_get_boot_devices($res)[0]; + + unset($tmp); + if (!$id) + $id = '-'; + unset($dom); + + $contextMenus[] = sprintf("addVMContext('%s', '%s', '%s', '%s', '%s', '%s');", addslashes($name), addslashes($uuid), addslashes($template), $state, addslashes($vnc), addslashes($log)); + + // fallback icon for users that created VMs before metadata support was added + $vmicon = '/plugins/dynamix.vm.manager/templates/images/' . ($lv->domain_get_clock_offset($res) == 'localtime' ? 'windows.png' : 'linux.png'); + + $vmtemplateicon = $lv->_get_single_xpath_result($res, '//domain/metadata/vmtemplate/@icon'); + if (!empty($vmtemplateicon)) { + if (file_exists($vmtemplateicon)) { + $vmicon = $vmtemplateicon; + } else if (file_exists('/usr/local/emhttp/plugins/dynamix.vm.manager/templates/images/' . $vmtemplateicon)) { + $vmicon = '/plugins/dynamix.vm.manager/templates/images/' . $vmtemplateicon; + } + } + + //Domain information + echo " + + + + + + + "; + + // Log file + if (!empty($log)) { + echo ""; + } else { + echo ""; + } + + echo " + "; + /* Disk device information */ + echo "'; + } + } + echo '
NameCPUsMemoryHard DrivesVNC PortAutostartLog
No Virtual Machines Installed
+
+
+ + +
+
+
$name$vcpu$mem$disks$vncport
'; +} +/* End Snapshot information */ +unset($name, $val); +if($msg){ + if(strpos($msg, "rror:")) + $color = 'red'; + else + $color = 'green'; + echo ""; + } +?> + + + + + diff --git a/plugins/dynamix.vm.manager/VMSettings.page b/plugins/dynamix.vm.manager/VMSettings.page new file mode 100644 index 000000000..76cd8fb38 --- /dev/null +++ b/plugins/dynamix.vm.manager/VMSettings.page @@ -0,0 +1,183 @@ +Menu="OtherSettings" +Title="VM Manager" +Icon="dynamix.vm.manager.png" +--- + +Array must be Started to manage Virtual Machines.

"; + return; +} + +require_once('/usr/local/emhttp/plugins/dynamix.vm.manager/classes/libvirt.php'); +require_once('/usr/local/emhttp/plugins/dynamix.vm.manager/classes/libvirt_helpers.php'); + + +// Check for Intel VT-x (vmx) or AMD-V (svm) cpu virtualzation support +// Attempt to load either of the kvm modules to see if virtualzation hw is supported +exec('modprobe -a kvm_intel kvm_amd 2>/dev/null'); + +// If either kvm_intel or kvm_amd are loaded then Intel VT-x (vmx) or AMD-V (svm) cpu virtualzation support was found +$strLoadedModules = shell_exec("lsmod | grep '^kvm_\(amd\|intel\)'"); + +if (empty($strLoadedModules)) { + ?>

Your hardware does not have Intel VT-x or AMD-V capability. This is required to create VMs in KVM. Click here to see the unRAID Wiki for more information

You must reboot for changes to take effect

+ + + + + + +
+ + + +Enable VMs: +: + +> Stopping the VM Manager will first attempt to shutdown all running VMs. After 40 seconds, any remaining VM instances will be terminated. + +get_connect_information(); ?> +Libvirt Version: +: + +QEMU Version: +: + + +ISO Library Share (optional): +: + +> Specify a user share that contains all your installation media for operating systems + +VirtIO Windows Drivers ISO (optional): +: + +> Specify the virtual CD-ROM (ISO) that contains the VirtIO Windows drivers as provided by the Fedora Project. +> Download the latest ISO from here: https://fedoraproject.org/wiki/Windows_Virtio_Drivers#Direct_download +> +> When installing Windows, you will reach a step where no disk devices will be found. +> There is an option to browse for drivers on that screen. Click browse and locate the additional CD-ROM in the menu. +> Inside there will be various folders for the different versions of Windows. Open the folder for the version of Windows +> you are installing and then select the AMD64 subfolder inside (even if you are on an Intel system, select AMD64). +> Three drivers will be found. Select them all, click next, and the vDisks you have assigned will appear. + + + +Default Network Bridge: +: + +> Enter the name of the network bridge you wish to use for your VMs here, otherwise leave the field blank and +> libvirt will create a bridge that will utilize NAT (network address translation) and act as a DHCP server to hand out +> IP addresses to virtual machines directly. +> +> NOTE: You can also specify an network bridge on a per-VM basis. + +PCIe ACS Override: +: + +> Warning: Use of this setting could cause possible data corruption with certain hardware configurations. Please visit the Lime Technology forums for more information. +> +> A reboot will be required for changes to this setting to take affect. + +  +: +
+ +> View the log for libvirt: /var/log/libvirt/libvirtd.log + + + + + + diff --git a/plugins/dynamix.vm.manager/VMajax.php b/plugins/dynamix.vm.manager/VMajax.php new file mode 100644 index 000000000..52f629b18 --- /dev/null +++ b/plugins/dynamix.vm.manager/VMajax.php @@ -0,0 +1,372 @@ + + '', + 1 => 'K', + 2 => 'M', + 3 => 'G', + 4 => 'T', + 5 => 'P' +]; + +$_REQUEST = array_merge($_GET, $_POST); + +$action = array_key_exists('action', $_REQUEST) ? $_REQUEST['action'] : ''; +$uuid = array_key_exists('uuid', $_REQUEST) ? $_REQUEST['uuid'] : ''; + +// Make sure libvirt is connected to qemu +if (!isset($lv) || !$lv->enabled()) { + header('Content-Type: application/json'); + die(json_encode(['error' => 'failed to connect to the hypervisor'])); +} + +if ($uuid) { + $domName = $lv->domain_get_name_by_uuid($uuid); + if (!$domName) { + header('Content-Type: application/json'); + die(json_encode(['error' => $lv->get_last_error()])); + } +} + +$arrResponse = []; + + +switch ($action) { + + case 'domain-autostart': + $arrResponse = $lv->domain_set_autostart($domName, ($_REQUEST['autostart'] != "false")) ? + ['success' => true, 'autostart' => (bool)$lv->domain_get_autostart($domName)] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-start': + $arrResponse = $lv->domain_start($domName) ? + ['success' => true, 'state' => $lv->domain_get_state($domName)] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-pause': + $arrResponse = $lv->domain_suspend($domName) ? + ['success' => true, 'state' => $lv->domain_get_state($domName)] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-resume': + $arrResponse = $lv->domain_resume($domName) ? + ['success' => true, 'state' => $lv->domain_get_state($domName)] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-pmwakeup': + // No support in libvirt-php to do a dompmwakeup, use virsh tool instead + exec("virsh dompmwakeup " . escapeshellarg($uuid) . " 2>&1", $arrOutput, $intReturnCode); + $arrResponse = ($intReturnCode == 0) ? + ['success' => true, 'state' => $lv->domain_get_state($domName)] : + ['error' => str_replace('error: ', '', implode('. ', $arrOutput))]; + break; + + case 'domain-restart': + $arrResponse = $lv->domain_reboot($domName) ? + ['success' => true, 'state' => $lv->domain_get_state($domName)] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-save': + $arrResponse = $lv->domain_save($domName) ? + ['success' => true, 'state' => $lv->domain_get_state($domName)] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-stop': + $arrResponse = $lv->domain_shutdown($domName) ? + ['success' => true, 'state' => $lv->domain_get_state($domName)] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-destroy': + $arrResponse = $lv->domain_destroy($domName) ? + ['success' => true, 'state' => $lv->domain_get_state($domName)] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-delete': + $arrResponse = $lv->domain_delete($domName) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-undefine': + $arrResponse = $lv->domain_undefine($domName) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-define': + $domName = $lv->domain_define($_REQUEST['xml']); + $arrResponse = $domName ? + ['success' => true, 'state' => $lv->domain_get_state($domName)] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-state': + $state = $lv->domain_get_state($domName); + $arrResponse = ($state) ? + ['success' => true, 'state' => $state] : + ['error' => $lv->get_last_error()]; + break; + + case 'domain-diskdev': + $arrResponse = ($lv->domain_set_disk_dev($domName, $_REQUEST['olddev'], $_REQUEST['diskdev'])) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'cdrom-change': + $arrResponse = ($lv->domain_change_cdrom($domName, $_REQUEST['cdrom'], $_REQUEST['dev'], $_REQUEST['bus'])) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'memory-change': + $arrResponse = ($lv->domain_set_memory($domName, $_REQUEST['memory']*1024)) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'vcpu-change': + $arrResponse = ($lv->domain_set_vcpu($domName, $_REQUEST['vcpu'])) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'bootdev-change': + $arrResponse = ($lv->domain_set_boot_device($domName, $_REQUEST['bootdev'])) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'disk-remove': + $arrResponse = ($lv->domain_disk_remove($domName, $_REQUEST['dev'])) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'snap-create': + $arrResponse = ($lv->domain_snapshot_create($domName)) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'snap-delete': + $arrResponse = ($lv->domain_snapshot_delete($domName, $_REQUEST['snap'])) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'snap-revert': + $arrResponse = ($lv->domain_snapshot_revert($domName, $_REQUEST['snap'])) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'snap-desc': + $arrResponse = ($lv->snapshot_set_metadata($domName, $_REQUEST['snap'], $_REQUEST['snapdesc'])) ? + ['success' => true] : + ['error' => $lv->get_last_error()]; + break; + + case 'disk-create': + $disk = $_REQUEST['disk']; + $driver = $_REQUEST['driver']; + $size = str_replace(array("KB","MB","GB","TB","PB", " ", ","), array("K","M","G","T","P", "", ""), strtoupper($_REQUEST['size'])); + + $dir = dirname($disk); + + if (!is_dir($dir)) + mkdir($dir); + + // determine the actual disk if user share is being used + if (strpos($dir, '/mnt/user/') === 0) { + $tmp = parse_ini_string(shell_exec("getfattr -n user.LOCATION " . escapeshellarg($dir) . " | grep user.LOCATION")); + $dir = str_replace('/mnt/user', '/mnt/' . $tmp['user.LOCATION'], $dir); // replace 'user' with say 'cache' or 'disk1' etc + } + + @exec("chattr +C -R " . escapeshellarg($dir) . " >/dev/null"); + + $strLastLine = exec("qemu-img create -q -f " . escapeshellarg($driver) . " " . escapeshellarg($disk) . " " . escapeshellarg($size) . " 2>&1", $out, $status); + + if (empty($status)) { + $arrResponse = ['success' => true]; + } else { + $arrResponse = ['error' => $strLastLine]; + } + + break; + + case 'disk-resize': + $disk = $_REQUEST['disk']; + $capacity = str_replace(array("KB","MB","GB","TB","PB", " ", ","), array("K","M","G","T","P", "", ""), strtoupper($_REQUEST['cap'])); + $old_capacity = str_replace(array("KB","MB","GB","TB","PB", " ", ","), array("K","M","G","T","P", "", ""), strtoupper($_REQUEST['oldcap'])); + + if (substr($old_capacity,0,-1) < substr($capacity,0,-1)){ + $strLastLine = exec("qemu-img resize -q " . escapeshellarg($disk) . " " . escapeshellarg($capacity) . " 2>&1", $out, $status); + if (empty($status)) { + $arrResponse = ['success' => true]; + } else { + $arrResponse = ['error' => $strLastLine]; + } + } else { + $arrResponse = ['error' => "Disk capacity has to be greater than " . $old_capacity]; + } + break; + + case 'file-info': + $file = $_REQUEST['file']; + + $arrResponse = [ + 'isfile' => (!empty($file) ? is_file($file) : false), + 'isdir' => (!empty($file) ? is_dir($file) : false), + 'isblock' => (!empty($file) ? is_block($file) : false), + 'resizable' => false + ]; + + // if file, get size and format info + if (is_file($file)) { + $json_info = json_decode(shell_exec("qemu-img info --output json " . escapeshellarg($file)), true); + if (!empty($json_info)) { + $intDisplaySize = (int)$json_info['virtual-size']; + $intShifts = 0; + while (!empty($intDisplaySize) && + (floor($intDisplaySize) == $intDisplaySize) && + isset($arrSizePrefix[$intShifts])) { + + $arrResponse['display-size'] = $intDisplaySize . $arrSizePrefix[$intShifts]; + + $intDisplaySize /= 1024; + $intShifts++; + } + + $arrResponse['virtual-size'] = $json_info['virtual-size']; + $arrResponse['actual-size'] = $json_info['actual-size']; + $arrResponse['format'] = $json_info['format']; + $arrResponse['dirty-flag'] = $json_info['dirty-flag']; + $arrResponse['resizable'] = true; + } + } else if (is_block($file)) { + $strDevSize = trim(shell_exec("blockdev --getsize64 " . escapeshellarg($file))); + if (!empty($strDevSize) && is_numeric($strDevSize)) { + $arrResponse['actual-size'] = (int)$strDevSize; + $arrResponse['format'] = 'raw'; + + $intDisplaySize = (int)$strDevSize; + $intShifts = 0; + while (!empty($intDisplaySize) && + ($intDisplaySize >= 2) && + isset($arrSizePrefix[$intShifts])) { + + $arrResponse['display-size'] = round($intDisplaySize, 0) . $arrSizePrefix[$intShifts]; + + $intDisplaySize /= 1000; // 1000 looks better than 1024 for block devs + $intShifts++; + } + } + } + break; + + case 'generate-mac': + $arrResponse = [ + 'mac' => $lv->generate_random_mac_addr() + ]; + break; + + case 'acs-override-enable': + // Check the /boot/syslinux/syslinux.cfg for the existance of pcie_acs_override=downstream, add it in if not found + $arrSyslinuxCfg = file('/boot/syslinux/syslinux.cfg'); + $strCurrentLabel = ''; + $boolModded = false; + foreach ($arrSyslinuxCfg as &$strSyslinuxCfg) { + if (stripos(trim($strSyslinuxCfg), 'label ') === 0) { + $strCurrentLabel = trim(str_ireplace('label ', '', $strSyslinuxCfg)); + } + if (stripos($strSyslinuxCfg, 'append ') !== false) { + if (stripos($strSyslinuxCfg, 'pcie_acs_override=') === false) { + // pcie_acs_override=downstream was not found so append it in + $strSyslinuxCfg = str_ireplace('append ', 'append pcie_acs_override=downstream ', $strSyslinuxCfg); + $boolModded = true; + } + + // We just modify the first append line, other boot menu items are untouched + break; + } + } + + if ($boolModded) { + // Backup syslinux.cfg + copy('/boot/syslinux/syslinux.cfg', '/boot/syslinux/syslinux.cfg-'); + + // Write Changes to syslinux.cfg + file_put_contents('/boot/syslinux/syslinux.cfg', implode('', $arrSyslinuxCfg)); + } + + $arrResponse = ['success' => true, 'label' => $strCurrentLabel]; + break; + + case 'acs-override-disable': + // Check the /boot/syslinux/syslinux.cfg for the existance of pcie_acs_override=, remove it if found + $arrSyslinuxCfg = file('/boot/syslinux/syslinux.cfg'); + $strCurrentLabel = ''; + $boolModded = false; + foreach ($arrSyslinuxCfg as &$strSyslinuxCfg) { + if (stripos(trim($strSyslinuxCfg), 'label ') === 0) { + $strCurrentLabel = trim(str_ireplace('label ', '', $strSyslinuxCfg)); + } + if (stripos($strSyslinuxCfg, 'append ') !== false) { + if (stripos($strSyslinuxCfg, 'pcie_acs_override=') !== false) { + // pcie_acs_override= was found so remove the two variations + $strSyslinuxCfg = str_ireplace('pcie_acs_override=downstream ', '', $strSyslinuxCfg); + $strSyslinuxCfg = str_ireplace('pcie_acs_override=multifunction ', '', $strSyslinuxCfg); + $boolModded = true; + } + + // We just modify the first append line, other boot menu items are untouched + break; + } + } + + if ($boolModded) { + // Backup syslinux.cfg + copy('/boot/syslinux/syslinux.cfg', '/boot/syslinux/syslinux.cfg-'); + + // Write Changes to syslinux.cfg + file_put_contents('/boot/syslinux/syslinux.cfg', implode('', $arrSyslinuxCfg)); + } + + $arrResponse = ['success' => true, 'label' => $strCurrentLabel]; + break; + + + default: + $arrResponse = ['error' => 'Unknown action \'' . $action . '\'']; + break; + +} + +header('Content-Type: application/json'); +die(json_encode($arrResponse)); + diff --git a/plugins/dynamix.vm.manager/VMedit.php b/plugins/dynamix.vm.manager/VMedit.php new file mode 100644 index 000000000..836c24b3d --- /dev/null +++ b/plugins/dynamix.vm.manager/VMedit.php @@ -0,0 +1,329 @@ + + '', + 'icon' => 'default.png', + 'desc' => '', + 'autostart' => false +]; + +$strSelectedTemplate = 'Custom'; + +if (!empty($_GET['uuid'])) { + // Edit VM mode + $res = $lv->domain_get_domain_by_uuid($_GET['uuid']); + + $strIcon = $lv->_get_single_xpath_result($res, '//domain/metadata/vmtemplate/@icon'); + + if (!empty($strIcon)) { + if (file_exists($strIcon)) { + $strIconURL = $strIcon; + } else if (file_exists('/usr/local/emhttp/plugins/dynamix.vm.manager/templates/images/' . $strIcon)) { + $strIconURL = '/plugins/dynamix.vm.manager/templates/images/' . $strIcon; + } + } else { + $strIcon = ($lv->domain_get_clock_offset($res) == 'localtime' ? 'windows.png' : 'linux.png'); + $strIconURL = '/plugins/dynamix.vm.manager/templates/images/' . $strIcon; + } + + $arrLoad = [ + 'name' => $lv->domain_get_name($res), + 'icon' => $strIcon, + 'desc' => $lv->domain_get_description($res), + 'autostart' => $lv->domain_get_autostart($res) + ]; + + if (!empty($_GET['template'])) { + $strSelectedTemplate = $_GET['template']; + } else { + $strTemplate = $lv->_get_single_xpath_result($res, '//domain/metadata/vmtemplate/@name'); + if (!empty($strTemplate)) { + $strSelectedTemplate = $strTemplate; + } + } +} else { + // New VM mode + $strIcon = 'windows.png'; + $strIconURL = '/plugins/dynamix.vm.manager/templates/images/windows.png'; + + $arrLoad['icon'] = $strIcon; + + if (!empty($_GET['template'])) { + $strSelectedTemplate = $_GET['template']; + } +} + +$arrTemplates = array(); + +// Read files from the templates folder +foreach (glob('plugins/dynamix.vm.manager/templates/*.form.php') as $template) { + $arrTemplates[] = basename($template, '.form.php'); +} + +if (!empty($arrTemplates) && !in_array($strSelectedTemplate, $arrTemplates)) { + $strSelectedTemplate = $arrTemplates[0]; +} + +?> + + + + + +
+
+ + + + > + + + +
Template: + +
+
> +
+

Choose a preconfigured template or select Custom to create your own from scratch.

+
+
+ + + + + + +
Icon:
+ + + + + + +
Name:
+
+
+

Give the VM a name (e.g. Work, Gaming, Media Player, Firewall, Bitcoin Miner)

+
+
+ + + + + + +
Description:
+
+
+

Give the VM a brief description (optional field).

+
+
+ + + + + + +
Autostart:
>
+
+

If you want this VM to start with the array, set this to yes.

+
+ +
+ Template Settings + +
+ +
+ +
+
+ + + + + diff --git a/plugins/dynamix.vm.manager/VMs.page b/plugins/dynamix.vm.manager/VMs.page new file mode 100644 index 000000000..7ef5ce548 --- /dev/null +++ b/plugins/dynamix.vm.manager/VMs.page @@ -0,0 +1,40 @@ +Menu="Tasks:70" +Type="xmenu" +Cond="(pgrep('libvirtd')!==false)" +--- + + + + + + + + + + + +Array must be Started to manage Virtual Machines.

"; + return; +} + +require_once('/usr/local/emhttp/plugins/dynamix.vm.manager/classes/libvirt.php'); +require_once('/usr/local/emhttp/plugins/dynamix.vm.manager/classes/libvirt_helpers.php'); + +if (count($pages)==2) $tabbed = false; +?> \ No newline at end of file diff --git a/plugins/dynamix.vm.manager/classes/libvirt.php b/plugins/dynamix.vm.manager/classes/libvirt.php new file mode 100644 index 000000000..42a934cd1 --- /dev/null +++ b/plugins/dynamix.vm.manager/classes/libvirt.php @@ -0,0 +1,2232 @@ + +set_logfile($debug); + if ($uri != false) { + $this->enabled = $this->connect($uri, $login, $pwd); + } + } + + function _set_last_error() { + $this->last_error = libvirt_get_last_error(); + return false; + } + + function enabled() { + return $this->enabled; + } + + function set_logfile($filename) { + if (!libvirt_logfile_set($filename,'10M')) + return $this->_set_last_error(); + + return true; + } + + function get_capabilities() { + $tmp = libvirt_connect_get_capabilities($this->conn); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function get_machine_types($arch = 'x86_64' /* or 'i686' */) { + $tmp = libvirt_connect_get_machine_types($this->conn); + + if (!$tmp) + return $this->_set_last_error(); + + if (empty($tmp[$arch])) + return []; + + return $tmp[$arch]; + } + + function get_default_emulator() { + $tmp = libvirt_connect_get_capabilities($this->conn, '//capabilities/guest/arch/domain/emulator'); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function set_folder_nodatacow($folder) { + if (!is_dir($folder)) { + return false; + } + + // determine the actual disk if user share is being used + if (strpos($folder, '/mnt/user/') === 0) { + $tmp = parse_ini_string(shell_exec("getfattr -n user.LOCATION " . escapeshellarg($folder) . " | grep user.LOCATION")); + $folder = str_replace('/mnt/user', '/mnt/' . $tmp['user.LOCATION'], $folder); // replace 'user' with say 'cache' or 'disk1' etc + } + + @shell_exec("chattr +C -R " . escapeshellarg($folder) . " &>/dev/null"); + + return true; + } + + function create_disk_image($disk, $vmname = '', $diskid = 1) { + $arrReturn = []; + + if (!empty($disk['size'])) { + $disk['size'] = str_replace(array("KB","MB","GB","TB","PB"), array("K","M","G","T","P"), strtoupper($disk['size'])); + } + if (empty($disk['driver'])) { + $disk['driver'] = 'raw'; + } + + // if new is a folder then + // if existing then + // create folder 'new/vmname' + // create image file as new/vmname/vdisk[1-x].xxx + // if doesn't exist then + // create folder 'new' + // create image file as new/vdisk[1-x].xxx + + // if new is a file then + // if existing then + // nothing to do + // if doesn't exist then + // create folder dirname('new') if needed + // create image file as new --> if size is specified + + if (!empty($disk['new'])) { + if (is_file($disk['new']) || is_block($disk['new'])) { + $disk['image'] = $disk['new']; + } + } + + if (!empty($disk['image'])) { + // Use existing disk image + + if (is_block($disk['image'])) { + // Valid block device, return as-is + return $disk; + } + + if (is_file($disk['image'])) { + $json_info = json_decode(shell_exec("qemu-img info --output json " . escapeshellarg($disk['image'])), true); + $disk['driver'] = $json_info['format']; + + if (!empty($disk['size'])) { + //TODO: expand disk image if size param is larger + } + + return $disk; + } + + $disk['new'] = $disk['image']; + } + + if (!empty($disk['new'])) { + // Create new disk image + $strImgFolder = $disk['new']; + $strImgPath = ''; + + if (strpos($strImgFolder, '/dev/') === 0) { + // ERROR invalid block device + $arrReturn = [ + 'error' => "Not a valid block device location '" . $strImgFolder . "'" + ]; + + return $arrReturn; + } + + if (empty($disk['size'])) { + // ERROR invalid disk size + $arrReturn = [ + 'error' => "Please specify a disk size for '" . $strImgFolder . "'" + ]; + + return $arrReturn; + } + + $path_parts = pathinfo($strImgFolder); + if (empty($path_parts['extension'])) { + // 'new' is a folder + + if (substr($strImgFolder, -1) != '/') { + $strImgFolder .= '/'; + } + + if (is_dir($strImgFolder)) { + // 'new' is a folder and already exists, append vmname folder + $strImgFolder .= preg_replace('((^\.)|\/|(\.$))', '_', $vmname) . '/'; + } + + // create folder if needed + if (!is_dir($strImgFolder)) { + mkdir($strImgFolder, 0777, true); + chown($strImgFolder, 'nobody'); + chgrp($strImgFolder, 'users'); + } + + $this->set_folder_nodatacow($strImgFolder); + + $strExt = ($disk['driver'] == 'raw') ? 'img' : $disk['driver']; + + $strImgPath = $strImgFolder . 'vdisk' . $diskid . '.' . $strExt; + + } else { + // 'new' is a file + + // create parent folder if needed + if (!is_dir($path_parts['dirname'])) { + mkdir($path_parts['dirname'], 0777, true); + chown($path_parts['dirname'], 'nobody'); + chgrp($path_parts['dirname'], 'users'); + } + + $this->set_folder_nodatacow($path_parts['dirname']); + + $strImgPath = $strImgFolder; + } + + + if (is_file($strImgPath)) { + $json_info = json_decode(shell_exec("qemu-img info --output json " . escapeshellarg($strImgPath)), true); + $disk['driver'] = $json_info['format']; + $return_value = 0; + } else { + $strLastLine = exec("qemu-img create -q -f " . escapeshellarg($disk['driver']) . " " . escapeshellarg($strImgPath) . " " . escapeshellarg($disk['size']), $output, $return_value); + chmod($strImgPath, 0777); + chown($strImgPath, 'nobody'); + chgrp($strImgPath, 'users'); + } + + if ($return_value != 0) { + + // ERROR during image creation, return message to user + $arrReturn = [ + 'error' => "Error creating disk image '" . $strImgPath . "': " . $strLastLine, + 'error_output' => $output + ]; + + } else { + + // Success! + $arrReturn = [ + 'image' => $strImgPath, + 'driver' => $disk['driver'] + ]; + if (!empty($disk['dev'])) { + $arrReturn['dev'] = $disk['dev']; + } + if (!empty($disk['bus'])) { + $arrReturn['bus'] = $disk['bus']; + } + + } + } + + return $arrReturn; + } + + + function config_to_xml($config) { + $domain = $config['domain']; + $media = $config['media']; + $nics = $config['nic']; + $disks = $config['disk']; + $usb = $config['usb']; + $shares = $config['shares']; + $gpus = $config['gpu']; + $audios = $config['audio']; + $template = $config['template']; + + + $type = $domain['type']; + $name = $domain['name']; + $mem = $domain['mem']; + $maxmem = $mem; + if (!empty($domain['maxmem'])) { + $maxmem = $domain['maxmem']; + } + $uuid = (!empty($domain['uuid']) ? $domain['uuid'] : $this->domain_generate_uuid()); + $machine = $domain['machine']; + $machine_type = (stripos($machine, 'q35') !== false ? 'q35' : 'pc'); + $os_type = ((empty($template['os']) || stripos($template['os'], 'windows') === false) ? 'other' : 'windows'); + $emulator = $this->get_default_emulator(); + $arch = $domain['arch']; + $pae = ''; + if ($arch == 'i686'){ + $pae = ''; + } + + $loader = ''; + if (!empty($domain['ovmf'])) { + $loader = "/usr/share/qemu/ovmf-x64/OVMF-pure-efi.fd"; + } + + $metadata = ''; + if (!empty($template)) { + $template_options = ''; + foreach ($template as $key => $value) { + $template_options .= $key . "='" . htmlspecialchars($value, ENT_QUOTES | ENT_XML1) . "' "; + } + $metadata = ""; + } + + $vcpus = 1; + $vcpupinstr = ''; + + if (!empty($domain['vcpu']) && is_array($domain['vcpu'])) { + $vcpus = count($domain['vcpu']); + foreach($domain['vcpu'] as $i => $vcpu) { + $vcpupinstr .= ""; + } + } else if (!empty($domain['vcpus'])) { + $vcpus = $domain['vcpus']; + for ($i=0; $i < $vcpus; $i++) { + $vcpupinstr .= ""; + } + } + + $cpumode = ''; + if (!empty($domain['cpumode']) && $domain['cpumode'] == 'host-passthrough') { + $cpumode .= "mode='host-passthrough'"; + } + + $cpustr = " + + + {$vcpus} + + $vcpupinstr + "; + + $bus = "ide"; + $ctrl = ''; + if ($machine_type == 'q35'){ + $bus = "sata"; + $ctrl = " + "; + if (!empty($domain['ovmf'])) { + // OVMF + Q35 needs the bus set to usb for cdroms + $bus = "usb"; + } + } + + $clock = " + + + + "; + + $hyperv = ''; + if (!empty($domain['hyperv']) && $os_type == "windows") { + $hyperv = " + + + + "; + + $clock = " + + + "; + } + + $usbstr = ''; + if (!empty($usb)) { + foreach($usb as $i => $v){ + $usbx = explode(':', $v); + $usbstr .= " + + + + + "; + } + } + + $arrAvailableDevs = []; + foreach (range('a', 'z') as $letter) { + $arrAvailableDevs['hd' . $letter] = 'hd' . $letter; + } + $arrUsedBootOrders = []; + + //media settings + $mediastr = ''; + if (!empty($media['cdrom'])) { + unset($arrAvailableDevs['hda']); + $arrUsedBootOrders[] = 2; + $mediastr = " + + + + + + "; + } + + $driverstr = ''; + if (!empty($media['drivers']) && $os_type == "windows") { + unset($arrAvailableDevs['hdb']); + $driverstr = " + + + + + "; + } + + //disk settings + $diskstr = ''; + $diskcount = 0; + if (!empty($disks)) { + foreach ($disks as $i => $disk) { + if (!empty($disk['image']) | !empty($disk['new']) ) { + //TODO: check if image/new is a block device + $diskcount++; + + if (!empty($disk['new'])) { + if (is_file($disk['new']) || is_block($disk['new'])) { + $disk['image'] = $disk['new']; + } + } + + if (!empty($disk['image'])) { + if (empty($disk['driver'])) { + $disk['driver'] = 'raw'; + + if (is_file($disk['image'])) { + $json_info = json_decode(shell_exec("qemu-img info --output json " . escapeshellarg($disk['image'])), true); + $disk['driver'] = $json_info['format']; + } + } + } else { + if (empty($disk['driver'])) { + $disk['driver'] = 'raw'; + } + + $strImgFolder = $disk['new']; + $strImgPath = ''; + + $path_parts = pathinfo($strImgFolder); + if (empty($path_parts['extension'])) { + // 'new' is a folder + + if (substr($strImgFolder, -1) != '/') { + $strImgFolder .= '/'; + } + + if (is_dir($strImgFolder)) { + // 'new' is a folder and already exists, append domain name as child folder + $strImgFolder .= preg_replace('((^\.)|\/|(\.$))', '_', $domain['name']) . '/'; + } + + $strExt = ($disk['driver'] == 'raw') ? 'img' : $disk['driver']; + + $strImgPath = $strImgFolder . 'vdisk' . $diskcount . '.' . $strExt; + + } else { + // 'new' is a file + $strImgPath = $strImgFolder; + } + + if (is_file($strImgPath)) { + $json_info = json_decode(shell_exec("qemu-img info --output json " . escapeshellarg($strImgPath)), true); + $disk['driver'] = $json_info['format']; + } + + $arrReturn = [ + 'image' => $strImgPath, + 'driver' => $disk['driver'] + ]; + if (!empty($disk['dev'])) { + $arrReturn['dev'] = $disk['dev']; + } + if (!empty($disk['bus'])) { + $arrReturn['bus'] = $disk['bus']; + } + + $disk = $arrReturn; + } + + if (empty($disk['bus'])) { + $disk['bus'] = 'virtio'; + } + + if (empty($disk['dev']) || !in_array($disk['dev'], $arrAvailableDevs)) { + $disk['dev'] = array_shift($arrAvailableDevs); + } + unset($arrAvailableDevs[$disk['dev']]); + + $bootorder = ''; + if (!in_array(1, $arrUsedBootOrders)) { + $bootorder = ""; + $arrUsedBootOrders[] = 1; + } + + $readonly = ''; + if (!empty($disk['readonly'])) { + $readonly = ''; + } + + $strDevType = @filetype(realpath($disk['image'])); + + if ($strDevType == 'file' || $strDevType == 'block') { + $strSourceType = ($strDevType == 'file' ? 'file' : 'dev'); + + $diskstr .= " + + + + $bootorder + $readonly + "; + } + } + } + } + + $netstr = ''; + if (!empty($nics)) { + foreach ($nics as $i => $nic) { + if (empty($nic['mac']) || empty($nic['network'])) { + continue; + } + + $netstr .= " + + + + "; + } + } + + $sharestr = ''; + if (!empty($shares) && $os_type != "windows") { + foreach ($shares as $i => $share) { + if (empty($share['source']) || empty($share['target'])) { + continue; + } + + $sharestr .= " + + + "; + } + } + + $passwdstr = ''; + if (!empty($domain['password'])){ + $passwdstr = "passwd='" . htmlspecialchars($domain['password'], ENT_QUOTES | ENT_XML1) . "'"; + } + + $pcidevs=''; + $gpuargs=''; + $gpudevs=[]; + $gpuargsdevs=[]; + $gpuincr=0; + $vnc=''; + if (!empty($gpus)) { + foreach ($gpus as $i => $gpu) { + if (empty($gpu['id'])) { + continue; + } + + // Skip duplicate video devices + if (in_array($gpu['id'], $gpudevs)) { + continue; + } + + if ($gpu['id'] == 'vnc') { + $strKeyMap = ''; + if (!empty($gpu['keymap'])) { + $strKeyMap = "keymap='" . $gpu['keymap'] . "'"; + } + + $vnc = " + + + + + "; + + if (!empty($domain['ovmf'])) { + // OVMF doesn't work with vmvga + $vnc .= ""; + } else { + // SeaBIOS is cool with vmvga + $vnc .= ""; + } + continue; + } + + list($gpu_bus, $gpu_slot, $gpu_function) = explode(":", str_replace('.', ':', $gpu['id'])); + + if (!empty($domain['ovmf'])) { + + // OVMF passthrough uses the normal hostdev and libvirt can fully manage the device + $pcidevs .= " + + +
+ + "; + + } else { + + // VGA BIOS passthrough uses qemu args and we have to manage the device (libvirt wont attach/detach the device driver to vfio-pci) + switch ($machine_type) { + + case 'q35': + $gpuargs .= " + "; + $gpuargsdevs[$gpu['id']] = ""; // no address needed + break; + + case 'pc': + $gpuargs .= " + "; + $gpuargsdevs[$gpu['id']] = "0{$gpuincr}.0"; + break; + } + + } + + $gpudevs[] = $gpu['id']; + $gpuincr++; + } + } + + $audioargs=''; + $audiodevs=[]; + if (!empty($audios)) { + foreach ($audios as $i => $audio) { + if (empty($audio['id'])) { + continue; + } + + // Skip duplicate audio devices + if (in_array($audio['id'], $audiodevs)) { + continue; + } + + list($audio_bus, $audio_slot, $audio_function) = explode(":", str_replace('.', ':', $audio['id'])); + + if (!empty($domain['ovmf']) || empty($gpuargsdevs)) { + + $pcidevs .= " + + +
+ + "; + + } else { + + // VGA BIOS passthrough uses qemu args and we have to manage the device (libvirt wont attach/detach the device driver to vfio-pci) + switch ($machine_type) { + + case 'q35': + $audioargs .= " + "; + break; + + case 'pc': + // Look for video device and see if this sound device is a function of that video card + $addr = ''; + if (isset($gpuargsdevs[$audio_bus . ':' . $audio_slot . '.0'])) { + $addr = str_replace('.0', '.' . $audio_function, $gpuargsdevs[$audio_bus . ':' . $audio_slot . '.0']); + } else { + $addr = "0" . $gpuincr++ . ".0"; + } + + $audioargs .= " + "; + break; + } + + } + + $audiodevs[] = $audio['id']; + } + } + + $cmdargs=''; + if (!empty($gpuargs) || !empty($audioargs)) { + switch ($machine_type) { + + case 'q35': + $cmdargs .= " + + + $gpuargs + $audioargs + "; + break; + + case 'pc': + $cmdargs .= " + + + $gpuargs + $audioargs + "; + break; + + } + } + + + return " + $uuid + $name + " . htmlspecialchars($domain['desc'], ENT_QUOTES | ENT_XML1) . " + $metadata + $mem + $maxmem + + + + + $cpustr + + $loader + hvm + + + + + $hyperv + $pae + + $clock + destroy + restart + restart + + $emulator + $diskstr + $mediastr + $driverstr + $ctrl + $sharestr + $netstr + $vnc + + $pcidevs + $usbstr + + + + + + + + $cmdargs + "; + + } + + function domain_new($config) { + + // attempt to create all disk images if needed + $diskcount = 0; + if (!empty($config['disk'])) { + foreach ($config['disk'] as $i => $disk) { + if (!empty($disk['image']) | !empty($disk['new']) ) { + $diskcount++; + + $disk = $this->create_disk_image($disk, $config['domain']['name'], $diskcount); + + if (!empty($disk['error'])) { + $this->last_error = $disk['error']; + return false; + } + + $config['disk'][$i] = $disk; + } + } + } + + // generate xml for this domain + $strXML = $this->config_to_xml($config); + + + // Start the VM now if requested + if (!empty($config['domain']['startnow'])) { + $tmp = libvirt_domain_create_xml($this->conn, $strXML); + if (!$tmp) + return $this->_set_last_error(); + } + + // Define the VM to persist + if ($config['domain']['persistent']) { + $tmp = libvirt_domain_define_xml($this->conn, $strXML); + if (!$tmp) + return $this->_set_last_error(); + + $this->domain_set_autostart($tmp, $config['domain']['autostart'] == 1); + return $tmp; + } + else + return $tmp; + + } + + function vfio_bind($strPassthruDevice) { + // Ensure we have leading 0000: + $strPassthruDeviceShort = str_replace('0000:', '', $strPassthruDevice); + $strPassthruDeviceLong = '0000:' . $strPassthruDeviceShort; + + // Determine the driver currently assigned to the device + $strDriverSymlink = @readlink('/sys/bus/pci/devices/' . $strPassthruDeviceLong . '/driver'); + + if ($strDriverSymlink !== false) { + // Device is bound to a Driver already + + if (strpos($strDriverSymlink, 'vfio-pci') !== false) { + // Driver bound to vfio-pci already - nothing left to do for this device now regarding vfio + return true; + } + + // Driver bound to some other driver - attempt to unbind driver + if (file_put_contents('/sys/bus/pci/devices/' . $strPassthruDeviceLong . '/driver/unbind', $strPassthruDeviceLong) === false) { + $this->last_error = 'Failed to unbind device ' . $strPassthruDeviceShort . ' from current driver'; + return false; + } + } + + // Get Vendor and Device IDs for the passthru device + $strVendor = file_get_contents('/sys/bus/pci/devices/' . $strPassthruDeviceLong . '/vendor'); + $strDevice = file_get_contents('/sys/bus/pci/devices/' . $strPassthruDeviceLong . '/device'); + + // Attempt to bind driver to vfio-pci + if (file_put_contents('/sys/bus/pci/drivers/vfio-pci/new_id', $strVendor . ' ' . $strDevice) === false) { + $this->last_error = 'Failed to bind device ' . $strPassthruDeviceShort . ' to vfio-pci driver'; + return false; + } + + return true; + } + + function connect($uri = 'null', $login = false, $password = false) { + if ($login !== false && $password !== false) { + $this->conn=libvirt_connect($uri, false, array(VIR_CRED_AUTHNAME => $login, VIR_CRED_PASSPHRASE => $password)); + } else { + $this->conn=libvirt_connect($uri, false); + } + if ($this->conn==false) + return $this->_set_last_error(); + + return true; + } + + function domain_change_boot_devices($domain, $first, $second) { + $domain = $this->get_domain_object($domain); + + $tmp = libvirt_domain_change_boot_devices($domain, $first, $second); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_get_screen_dimensions($domain) { + $dom = $this->get_domain_object($domain); + + $tmp = libvirt_domain_get_screen_dimensions($dom, $this->get_hostname() ); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_send_keys($domain, $keys) { + $dom = $this->get_domain_object($domain); + + $tmp = libvirt_domain_send_keys($dom, $this->get_hostname(), $keys); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_send_pointer_event($domain, $x, $y, $clicked = 1, $release = false) { + $dom = $this->get_domain_object($domain); + + $tmp = libvirt_domain_send_pointer_event($dom, $this->get_hostname(), $x, $y, $clicked, $release); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_disk_remove($domain, $dev) { + $dom = $this->get_domain_object($domain); + + $tmp = libvirt_domain_disk_remove($dom, $dev); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function supports($name) { + return libvirt_has_feature($name); + } + + function macbyte($val) { + if ($val < 16) + return '0'.dechex($val); + + return dechex($val); + } + + function generate_random_mac_addr($seed=false) { + if (!$seed) + $seed = 1; + + if ($this->get_hypervisor_name() == 'qemu') + $prefix = '52:54:00'; + else + $prefix = $this->macbyte(($seed * rand()) % 256).':'. + $this->macbyte(($seed * rand()) % 256).':'. + $this->macbyte(($seed * rand()) % 256); + + return $prefix.':'. + $this->macbyte(($seed * rand()) % 256).':'. + $this->macbyte(($seed * rand()) % 256).':'. + $this->macbyte(($seed * rand()) % 256); + } + + function get_connection() { + return $this->conn; + } + + function get_hostname() { + return libvirt_connect_get_hostname($this->conn); + } + + function get_domain_object($nameRes) { + if (is_resource($nameRes)) + return $nameRes; + + $dom=libvirt_domain_lookup_by_name($this->conn, $nameRes); + if (!$dom) { + $dom=libvirt_domain_lookup_by_uuid_string($this->conn, $nameRes); + if (!$dom) + return $this->_set_last_error(); + } + + return $dom; + } + + function get_xpath($domain, $xpath, $inactive = false) { + $dom = $this->get_domain_object($domain); + $flags = 0; + if ($inactive) + $flags = VIR_DOMAIN_XML_INACTIVE; + + $tmp = libvirt_domain_xml_xpath($dom, $xpath, $flags); + if (!$tmp) + return $this->_set_last_error(); + + return $tmp; + } + + function get_cdrom_stats($domain, $sort=true) { + $dom = $this->get_domain_object($domain); + + $buses = $this->get_xpath($dom, '//domain/devices/disk[@device="cdrom"]/target/@bus', false); + $disks = $this->get_xpath($dom, '//domain/devices/disk[@device="cdrom"]/target/@dev', false); + $files = $this->get_xpath($dom, '//domain/devices/disk[@device="cdrom"]/source/@file', false); + + $ret = array(); + for ($i = 0; $i < $disks['num']; $i++) { + $tmp = libvirt_domain_get_block_info($dom, $disks[$i]); + if ($tmp) { + $tmp['bus'] = $buses[$i]; + $ret[] = $tmp; + } + else { + $this->_set_last_error(); + + $ret[] = array( + 'device' => $disks[$i], + 'file' => $files[$i], + 'type' => '-', + 'capacity' => '-', + 'allocation' => '-', + 'physical' => '-', + 'bus' => $buses[$i] + ); + } + } + + if ($sort) { + for ($i = 0; $i < sizeof($ret); $i++) { + for ($ii = 0; $ii < sizeof($ret); $ii++) { + if (strcmp($ret[$i]['device'], $ret[$ii]['device']) < 0) { + $tmp = $ret[$i]; + $ret[$i] = $ret[$ii]; + $ret[$ii] = $tmp; + } + } + } + } + + unset($buses); + unset($disks); + unset($files); + + return $ret; + } + + function get_disk_stats($domain, $sort=true) { + $dom = $this->get_domain_object($domain); + + $buses = $this->get_xpath($dom, '//domain/devices/disk[@device="disk"]/target/@bus', false); + $disks = $this->get_xpath($dom, '//domain/devices/disk[@device="disk"]/target/@dev', false); + $files = $this->get_xpath($dom, '//domain/devices/disk[@device="disk"]/source/@file', false); + + $ret = array(); + for ($i = 0; $i < $disks['num']; $i++) { + $tmp = libvirt_domain_get_block_info($dom, $disks[$i]); + if ($tmp) { + $tmp['bus'] = $buses[$i]; + + // Libvirt reports 0 bytes for raw disk images that haven't been + // written to yet so we just report the raw disk size for now + if ( !empty($tmp['file']) && + $tmp['type'] == 'raw' && + empty($tmp['physical']) && + is_file($tmp['file']) ) { + + $intSize = filesize($tmp['file']); + $tmp['physical'] = $intSize; + $tmp['capacity'] = $intSize; + } + + $ret[] = $tmp; + } + else { + $this->_set_last_error(); + + $ret[] = array( + 'device' => $disks[$i], + 'file' => $files[$i], + 'type' => '-', + 'capacity' => '-', + 'allocation' => '-', + 'physical' => '-', + 'bus' => $buses[$i] + ); + } + } + + if ($sort) { + for ($i = 0; $i < sizeof($ret); $i++) { + for ($ii = 0; $ii < sizeof($ret); $ii++) { + if (strcmp($ret[$i]['device'], $ret[$ii]['device']) < 0) { + $tmp = $ret[$i]; + $ret[$i] = $ret[$ii]; + $ret[$ii] = $tmp; + } + } + } + } + + unset($buses); + unset($disks); + unset($files); + + return $ret; + } + + function get_domain_type($domain) { + $dom = $this->get_domain_object($domain); + + $tmp = $this->get_xpath($dom, '//domain/@type', false); + if ($tmp['num'] == 0) + return $this->_set_last_error(); + + $ret = $tmp[0]; + unset($tmp); + + return $ret; + } + + function get_domain_emulator($domain) { + $dom = $this->get_domain_object($domain); + + $tmp = $this->get_xpath($dom, '//domain/devices/emulator', false); + if ($tmp['num'] == 0) + return $this->_set_last_error(); + + $ret = $tmp[0]; + unset($tmp); + + return $ret; + } + + function get_disk_capacity($domain, $physical=false, $disk='*', $unit='?') { + $dom = $this->get_domain_object($domain); + $tmp = $this->get_disk_stats($dom); + + $ret = 0; + for ($i = 0; $i < sizeof($tmp); $i++) { + if (($disk == '*') || ($tmp[$i]['device'] == $disk)) + if ($physical) + $ret += $tmp[$i]['physical']; + else + $ret += $tmp[$i]['capacity']; + } + unset($tmp); + + return $this->format_size($ret, 2, $unit); + } + + function get_disk_count($domain) { + $dom = $this->get_domain_object($domain); + $tmp = $this->get_disk_stats($dom); + $ret = sizeof($tmp); + unset($tmp); + + return $ret; + } + + function format_size($value, $decimals, $unit='?') { + if ($value == '-') + return 'unknown'; + + /* Autodetect unit that's appropriate */ + if ($unit == '?') { + /* (1 << 40) is not working correctly on i386 systems */ + if ($value >= 1099511627776) + $unit = 'T'; + else + if ($value >= (1 << 30)) + $unit = 'G'; + else + if ($value >= (1 << 20)) + $unit = 'M'; + else + if ($value >= (1 << 10)) + $unit = 'K'; + else + $unit = 'B'; + } + + $unit = strtoupper($unit); + + switch ($unit) { + case 'T': return number_format($value / (float)1099511627776, $decimals).' TB'; + case 'G': return number_format($value / (float)(1 << 30), $decimals).' GB'; + case 'M': return number_format($value / (float)(1 << 20), $decimals).' MB'; + case 'K': return number_format($value / (float)(1 << 10), $decimals).' KB'; + case 'B': return $value.' B'; + } + + return false; + } + + function get_uri() { + $tmp = libvirt_connect_get_uri($this->conn); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function get_domain_count() { + $tmp = libvirt_domain_get_counts($this->conn); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function translate_volume_type($type) { + if ($type == 1) + return 'Block device'; + + return 'File image'; + } + + function translate_perms($mode) { + $mode = (string)((int)$mode); + + $tmp = '---------'; + + for ($i = 0; $i < 3; $i++) { + $bits = (int)$mode[$i]; + if ($bits & 4) + $tmp[ ($i * 3) ] = 'r'; + if ($bits & 2) + $tmp[ ($i * 3) + 1 ] = 'w'; + if ($bits & 1) + $tmp[ ($i * 3) + 2 ] = 'x'; + } + + + return $tmp; + } + + function parse_size($size) { + $unit = $size[ strlen($size) - 1 ]; + + $size = (int)$size; + switch (strtoupper($unit)) { + case 'T': $size *= 1099511627776; + break; + case 'G': $size *= 1073741824; + break; + case 'M': $size *= 1048576; + break; + case 'K': $size *= 1024; + break; + } + + return $size; + } + + //create a storage volume and add file extension + function volume_create($name, $capacity, $allocation, $format) { + $capacity = $this->parse_size($capacity); + $allocation = $this->parse_size($allocation); + ($format != 'raw' ) ? $ext = $format : $ext = 'img'; + ($ext == pathinfo($name, PATHINFO_EXTENSION)) ? $ext = '': $name .= '.'; + + $xml = "\n". + " $name$ext\n". + " $capacity\n". + " $allocation\n". + " \n". + " \n". + " \n". + ""; + + $tmp = libvirt_storagevolume_create_xml($pool, $xml); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function get_hypervisor_name() { + $tmp = libvirt_connect_get_information($this->conn); + $hv = $tmp['hypervisor']; + unset($tmp); + + switch (strtoupper($hv)) { + case 'QEMU': $type = 'qemu'; + break; + + default: + $type = $hv; + } + + return $type; + } + + function get_connect_information() { + $tmp = libvirt_connect_get_information($this->conn); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_change_xml($domain, $xml) { + $dom = $this->get_domain_object($domain); + + if (!($old_xml = domain_get_xml($dom))) + return $this->_set_last_error(); + if (!libvirt_domain_undefine($dom)) + return $this->_set_last_error(); + if (!libvirt_domain_define_xml($this->conn, $xml)) { + $this->last_error = libvirt_get_last_error(); + libvirt_domain_define_xml($this->conn, $old_xml); + return false; + } + + return true; + } + + function get_domains() { + $tmp = libvirt_list_domains($this->conn); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function get_active_domain_ids() { + $tmp = libvirt_list_active_domain_ids($this->conn); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function get_domain_by_name($name) { + $tmp = libvirt_domain_lookup_by_name($this->conn, $name); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function get_node_devices($dev = false) { + $tmp = ($dev == false) ? libvirt_list_nodedevs($this->conn) : libvirt_list_nodedevs($this->conn, $dev); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function get_node_device_res($res) { + if ($res == false) + return false; + if (is_resource($res)) + return $res; + + $tmp = libvirt_nodedev_get($this->conn, $res); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function get_node_device_caps($dev) { + $dev = $this->get_node_device_res($dev); + + $tmp = libvirt_nodedev_capabilities($dev); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function get_node_device_cap_options() { + $all = $this->get_node_devices(); + + $ret = array(); + for ($i = 0; $i < sizeof($all); $i++) { + $tmp = $this->get_node_device_caps($all[$i]); + + for ($ii = 0; $ii < sizeof($tmp); $ii++) + if (!in_array($tmp[$ii], $ret)) + $ret[] = $tmp[$ii]; + } + + return $ret; + } + + function get_node_device_xml($dev) { + $dev = $this->get_node_device_res($dev); + + $tmp = libvirt_nodedev_get_xml_desc($dev, NULL); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function get_node_device_information($dev) { + $dev = $this->get_node_device_res($dev); + + $tmp = libvirt_nodedev_get_information($dev); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_get_name($res) { + return libvirt_domain_get_name($res); + } + + function domain_get_info_call($name = false, $name_override = false) { + $ret = array(); + + if ($name != false) { + $dom = $this->get_domain_object($name); + if (!$dom) + return false; + + if ($name_override) + $name = $name_override; + + $ret[$name] = libvirt_domain_get_info($dom); + return $ret; + } + else { + $doms = libvirt_list_domains($this->conn); + foreach ($doms as $dom) { + $tmp = $this->domain_get_name($dom); + $ret[$tmp] = libvirt_domain_get_info($dom); + } + } + + ksort($ret); + return $ret; + } + + function domain_get_info($name = false, $name_override = false) { + if (!$name) + return false; + + if (!$this->allow_cached) + return $this->domain_get_info_call($name, $name_override); + + $domname = $name_override ? $name_override : $name; + $dom = $this->get_domain_object($domname); + $domkey = $name_override ? $name_override : $this->domain_get_name($dom); + if (!array_key_exists($domkey, $this->dominfos)) { + $tmp = $this->domain_get_info_call($name, $name_override); + $this->dominfos[$domkey] = $tmp[$domname]; + } + + return $this->dominfos[$domkey]; + } + + function get_last_error() { + return $this->last_error; + } + + function domain_get_xml($domain, $xpath = NULL) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_get_xml_desc($dom, $xpath); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_get_id($domain, $name = false) { + $dom = $this->get_domain_object($domain); + if ((!$dom) || (!$this->domain_is_running($dom, $name))) + return false; + + $tmp = libvirt_domain_get_id($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_get_interface_stats($domain, $iface) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_interface_stats($dom, $iface); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_get_interface_devices($res) { + $tmp = libvirt_domain_get_interface_devices($res); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_get_memory_stats($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_memory_stats($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_start($dom) { + $dom=$this->get_domain_object($dom); + if ($dom) { + $ret = libvirt_domain_create($dom); + $this->last_error = libvirt_get_last_error(); + return $ret; + } + + $ret = libvirt_domain_create_xml($this->conn, $dom); + $this->last_error = libvirt_get_last_error(); + return $ret; + } + + function domain_define($xml) { + if (strpos($xml,'')) { + $tmp = explode("\n", $xml); + for ($i = 0; $i < sizeof($tmp); $i++) + if (strpos('.'.$tmp[$i], "conn, $xml); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_destroy($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_destroy($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_reboot($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_reboot($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_suspend($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_suspend($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_save($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_managedsave($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_resume($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_resume($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_get_uuid($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_get_uuid_string($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_get_domain_by_uuid($uuid) { + $dom = libvirt_domain_lookup_by_uuid_string($this->conn, $uuid); + return ($dom) ? $dom : $this->_set_last_error(); + } + + function domain_get_name_by_uuid($uuid) { + $dom = $this->domain_get_domain_by_uuid($uuid); + if (!$dom) + return false; + $tmp = libvirt_domain_get_name($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_is_active($domain) { + $domain = $this->get_domain_object($domain); + $tmp = libvirt_domain_is_active($domain); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function generate_uuid($seed=false) { + if (!$seed) + $seed = time(); + srand($seed); + + $ret = array(); + for ($i = 0; $i < 16; $i++) + $ret[] = $this->macbyte(rand() % 256); + + $a = $ret[0].$ret[1].$ret[2].$ret[3]; + $b = $ret[4].$ret[5]; + $c = $ret[6].$ret[7]; + $d = $ret[8].$ret[9]; + $e = $ret[10].$ret[11].$ret[12].$ret[13].$ret[14].$ret[15]; + + return $a.'-'.$b.'-'.$c.'-'.$d.'-'.$e; + } + + function domain_generate_uuid() { + $uuid = $this->generate_uuid(); + + //while ($this->domain_get_name_by_uuid($uuid)) + //$uuid = $this->generate_uuid(); + + return $uuid; + } + + function domain_shutdown($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_shutdown($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_undefine($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = libvirt_domain_undefine($dom); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + + function domain_delete($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + $disks = $this->get_disk_stats($dom); + $tmp = libvirt_domain_undefine($dom); + if (!$tmp) + return $this->_set_last_error(); + + // remove the first disk only + if (array_key_exists('file', $disks[0])) { + $disk = $disks[0]['file']; + $pathinfo = pathinfo($disk); + $dir = $pathinfo['dirname']; + + // remove the vm config + $cfg_vm = $dir.'/'.$domain.'.cfg'; + if (file_exists($cfg_vm)); + unlink($cfg_vm); + + $cfg = $dir.'/'.$pathinfo['filename'].'.cfg'; + $xml = $dir.'/'.$pathinfo['filename'].'.xml'; + if (file_exists($disk)); + unlink($disk); + if (file_exists($cfg)); + unlink($cfg); + if (file_exists($xml)); + unlink($xml); + if (is_dir($dir) && $this->is_dir_empty($dir)) + rmdir($dir); + } + + return true; + } + + function is_dir_empty($dir) { + if (!is_readable($dir)) return NULL; + $handle = opendir($dir); + while (false !== ($entry = readdir($handle))) { + if ($entry != "." && $entry != "..") { + return FALSE; + } + } + return TRUE; + } + + function domain_is_running($domain, $name = false) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $tmp = $this->domain_get_info( $domain, $name ); + if (!$tmp) + return $this->_set_last_error(); + $ret = ( ($tmp['state'] == VIR_DOMAIN_RUNNING) || ($tmp['state'] == VIR_DOMAIN_BLOCKED) ); + unset($tmp); + return $ret; + } + + function domain_get_state($domain) { + $dom = $this->get_domain_object($domain); + if (!$dom) + return false; + + $info = libvirt_domain_get_info($dom); + if (!$info) + return $this->_set_last_error(); + + return $this->domain_state_translate($info['state']); + } + + function domain_state_translate($state) { + switch ($state) { + case VIR_DOMAIN_RUNNING: return 'running'; + case VIR_DOMAIN_NOSTATE: return 'nostate'; + case VIR_DOMAIN_BLOCKED: return 'blocked'; + case VIR_DOMAIN_PAUSED: return 'paused'; + case VIR_DOMAIN_SHUTDOWN: return 'shutdown'; + case VIR_DOMAIN_SHUTOFF: return 'shutoff'; + case VIR_DOMAIN_CRASHED: return 'crashed'; + //VIR_DOMAIN_PMSUSPENDED is 7 (not defined in libvirt-php yet) + case 7: return 'pmsuspended'; + } + + return 'unknown'; + } + + function domain_get_vnc_port($domain) { + $tmp = $this->get_xpath($domain, '//domain/devices/graphics/@port', false); + $var = (int)$tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_vnc_keymap($domain) { + $tmp = $this->get_xpath($domain, '//domain/devices/graphics/@keymap', false); + if (!$tmp) + return 'en-us'; + + $var = $tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_ws_port($domain) { + $tmp = $this->get_xpath($domain, '//domain/devices/graphics/@websocket', false); + $var = (int)$tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_arch($domain) { + $tmp = $this->get_xpath($domain, '//domain/os/type/@arch', false); + $var = $tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_machine($domain) { + $tmp = $this->get_xpath($domain, '//domain/os/type/@machine', false); + $var = $tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_description($domain) { + $tmp = $this->get_xpath($domain, '//domain/description', false); + $var = $tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_clock_offset($domain) { + $tmp = $this->get_xpath($domain, '//domain/clock/@offset', false); + $var = $tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_cpu_type($domain) { + $tmp = $this->get_xpath($domain, '//domain/cpu/@mode', false); + if (!$tmp) + return 'emulated'; + + $var = $tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_vcpu($domain) { + $tmp = $this->get_xpath($domain, '//domain/vcpu', false); + $var = $tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_vcpu_pins($domain) { + $tmp = $this->get_xpath($domain, '//domain/cputune/vcpupin/@cpuset', false); + if (!$tmp) + return false; + + $devs = array(); + for ($i = 0; $i < $tmp['num']; $i++) + $devs[] = $tmp[$i]; + + return $devs; + } + + function domain_get_memory($domain) { + $tmp = $this->get_xpath($domain, '//domain/memory', false); + $var = $tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_current_memory($domain) { + $tmp = $this->get_xpath($domain, '//domain/currentMemory', false); + $var = $tmp[0]; + unset($tmp); + + return $var; + } + + function domain_get_feature($domain, $feature) { + $tmp = $this->get_xpath($domain, '//domain/features/'.$feature.'/..', false); + $ret = ($tmp != false); + unset($tmp); + + return $ret; + } + + function domain_get_boot_devices($domain) { + $tmp = $this->get_xpath($domain, '//domain/os/boot/@dev', false); + if (!$tmp) + return false; + + $devs = array(); + for ($i = 0; $i < $tmp['num']; $i++) + $devs[] = $tmp[$i]; + + return $devs; + } + + function domain_get_mount_filesystems($domain) { + $xpath = '//domain/devices/filesystem[@type="mount"]'; + + $sources = $this->get_xpath($domain, $xpath.'/source/@dir', false); + $targets = $this->get_xpath($domain, $xpath.'/target/@dir', false); + + $ret = array(); + if (!empty($sources)) { + for ($i = 0; $i < $sources['num']; $i++) { + $ret[] = array( + 'source' => $sources[$i], + 'target' => $targets[$i] + ); + } + } + + return $ret; + } + + function _get_single_xpath_result($domain, $xpath) { + $tmp = $this->get_xpath($domain, $xpath, false); + if (!$tmp) + return false; + + if ($tmp['num'] == 0) + return false; + + return $tmp[0]; + } + + function domain_get_ovmf($domain) { + return $this->_get_single_xpath_result($domain, '//domain/os/loader'); + } + + function domain_get_multimedia_device($domain, $type, $display=false) { + $domain = $this->get_domain_object($domain); + + if ($type == 'console') { + $type = $this->_get_single_xpath_result($domain, '//domain/devices/console/@type'); + $targetType = $this->_get_single_xpath_result($domain, '//domain/devices/console/target/@type'); + $targetPort = $this->_get_single_xpath_result($domain, '//domain/devices/console/target/@port'); + + if ($display) + return $type.' ('.$targetType.' on port '.$targetPort.')'; + else + return array('type' => $type, 'targetType' => $targetType, 'targetPort' => $targetPort); + } + else + if ($type == 'input') { + $type = $this->_get_single_xpath_result($domain, '//domain/devices/input/@type'); + $bus = $this->_get_single_xpath_result($domain, '//domain/devices/input/@bus'); + + if ($display) + return $type.' on '.$bus; + else + return array('type' => $type, 'bus' => $bus); + } + else + if ($type == 'graphics') { + $type = $this->_get_single_xpath_result($domain, '//domain/devices/graphics/@type'); + $port = $this->_get_single_xpath_result($domain, '//domain/devices/graphics/@port'); + $autoport = $this->_get_single_xpath_result($domain, '//domain/devices/graphics/@autoport'); + + if ($display) + return $type.' on port '.$port.' with'.($autoport ? '' : 'out').' autoport enabled'; + else + return array('type' => $type, 'port' => $port, 'autoport' => $autoport); + } + else + if ($type == 'video') { + $type = $this->_get_single_xpath_result($domain, '//domain/devices/video/model/@type'); + $vram = $this->_get_single_xpath_result($domain, '//domain/devices/video/model/@vram'); + $heads = $this->_get_single_xpath_result($domain, '//domain/devices/video/model/@heads'); + + if ($display) + return $type.' with '.($vram / 1024).' MB VRAM, '.$heads.' head(s)'; + else + return array('type' => $type, 'vram' => $vram, 'heads' => $heads); + } + else + return false; + } + + function domain_get_host_devices_pci($domain) { + $xpath = '//domain/devices/hostdev[@type="pci"]/source/address/@'; + + $dom = $this->get_xpath($domain, $xpath.'domain', false); + $bus = $this->get_xpath($domain, $xpath.'bus', false); + $slot = $this->get_xpath($domain, $xpath.'slot', false); + $func = $this->get_xpath($domain, $xpath.'function', false); + + $devs = array(); + for ($i = 0; $i < $bus['num']; $i++) { + $devid = str_replace('0x', '', 'pci_'.$dom[$i].'_'.$bus[$i].'_'.$slot[$i].'_'.$func[$i]); + $tmp2 = $this->get_node_device_information($devid); + $devs[] = array( + 'domain' => $dom[$i], + 'bus' => $bus[$i], + 'slot' => $slot[$i], + 'func' => $func[$i], + 'id' => str_replace('0x', '', $bus[$i].':'.$slot[$i].'.'.$func[$i]), + 'vendor' => $tmp2['vendor_name'], + 'vendor_id' => $tmp2['vendor_id'], + 'product' => $tmp2['product_name'], + 'product_id' => $tmp2['product_id'] + ); + } + + // Get any pci devices contained in the qemu args + $args = $this->get_xpath($domain, '//domain/*[name()=\'qemu:commandline\']/*[name()=\'qemu:arg\']/@value', false); + + for ($i = 0; $i < $args['num']; $i++) { + if (strpos($args[$i], 'vfio-pci') !== 0) { + continue; + } + + $arg_list = explode(',', $args[$i]); + + foreach ($arg_list as $arg) { + $keypair = explode('=', $arg); + + if ($keypair[0] == 'host' && !empty($keypair[1])) { + $devid = 'pci_0000_' . str_replace(array(':', '.'), '_', $keypair[1]); + $tmp2 = $this->get_node_device_information($devid); + list($bus, $slot, $func) = explode(":", str_replace('.', ':', $keypair[1])); + $devs[] = array( + 'domain' => '0x0000', + 'bus' => '0x' . $bus, + 'slot' => '0x' . $slot, + 'func' => '0x' . $func, + 'id' => $keypair[1], + 'vendor' => $tmp2['vendor_name'], + 'vendor_id' => $tmp2['vendor_id'], + 'product' => $tmp2['product_name'], + 'product_id' => $tmp2['product_id'] + ); + break; + } + } + } + + return $devs; + } + + function _lookup_device_usb($vendor_id, $product_id) { + $tmp = $this->get_node_devices(false); + for ($i = 0; $i < sizeof($tmp); $i++) { + $tmp2 = $this->get_node_device_information($tmp[$i]); + if (array_key_exists('product_id', $tmp2)) { + if (($tmp2['product_id'] == $product_id) + && ($tmp2['vendor_id'] == $vendor_id)) + return $tmp2; + } + } + + return false; + } + + function domain_get_host_devices_usb($domain) { + $xpath = '//domain/devices/hostdev[@type="usb"]/source/'; + + $vid = $this->get_xpath($domain, $xpath.'vendor/@id', false); + $pid = $this->get_xpath($domain, $xpath.'product/@id', false); + + $devs = array(); + for ($i = 0; $i < $vid['num']; $i++) { + $dev = $this->_lookup_device_usb($vid[$i], $pid[$i]); + $devs[] = array( + 'id' => str_replace('0x', '', $vid[$i] . ':' . $pid[$i]), + 'vendor_id' => $vid[$i], + 'product_id' => $pid[$i], + 'product' => $dev['product_name'], + 'vendor' => $dev['vendor_name'] + ); + } + + return $devs; + } + + function domain_get_host_devices($domain) { + $domain = $this->get_domain_object($domain); + + $devs_pci = $this->domain_get_host_devices_pci($domain); + $devs_usb = $this->domain_get_host_devices_usb($domain); + + return array('pci' => $devs_pci, 'usb' => $devs_usb); + } + + function get_nic_info($domain) { + $macs = $this->get_xpath($domain, "//domain/devices/interface/mac/@address", false); + $net = $this->get_xpath($domain, "//domain/devices/interface/@type", false); + $bridge = $this->get_xpath($domain, "//domain/devices/interface/source/@bridge", false); + if (!$macs) + return $this->_set_last_error(); + $ret = array(); + for ($i = 0; $i < $macs['num']; $i++) { + if ($net[$i] != 'bridge') + $tmp = libvirt_domain_get_network_info($domain, $macs[$i]); + if ($tmp) + $ret[] = $tmp; + else { + $this->_set_last_error(); + $ret[] = array( + 'mac' => $macs[$i], + 'network' => $bridge[$i], + 'nic_type' => 'virtio' + ); + } + } + + return $ret; + } + + function domain_set_feature($domain, $feature, $val) { + $domain = $this->get_domain_object($domain); + + if ($this->domain_get_feature($domain, $feature) == $val) + return true; + + $xml = $this->domain_get_xml($domain); + if ($val) { + if (strpos('features', $xml)) + $xml = str_replace('', "\n<$feature/>", $xml); + else + $xml = str_replace('', "\n<$feature/>", $xml); + } + else + $xml = str_replace("<$feature/>\n", '', $xml); + + return $this->domain_define($xml); + } + + function domain_set_clock_offset($domain, $offset) { + $domain = $this->get_domain_object($domain); + + if (($old_offset = $this->domain_get_clock_offset($domain)) == $offset) + return true; + + $xml = $this->domain_get_xml($domain); + $xml = str_replace("", "", $xml); + + return $this->domain_define($xml); + } + +//change vpus for domain + function domain_set_vcpu($domain, $vcpu) { + $domain = $this->get_domain_object($domain); + + if (($old_vcpu = $this->domain_get_vcpu($domain)) == $vcpu) + return true; + + $xml = $this->domain_get_xml($domain); + $xml = str_replace("$old_vcpu", "$vcpu", $xml); + + return $this->domain_define($xml); + } + +//change memory for domain + function domain_set_memory($domain, $memory) { + $domain = $this->get_domain_object($domain); + if (($old_memory = $this->domain_get_memory($domain)) == $memory) + return true; + + $xml = $this->domain_get_xml($domain); + $xml = str_replace("$old_memory", "$memory", $xml); + + return $this->domain_define($xml); + } + +//change memory for domain + function domain_set_current_memory($domain, $memory) { + $domain = $this->get_domain_object($domain); + if (($old_memory = $this->domain_get_current_memory($domain)) == $memory) + return true; + + $xml = $this->domain_get_xml($domain); + $xml = str_replace("$old_memory", "$memory", $xml); + + return $this->domain_define($xml); + } + +//change domain disk dev name + function domain_set_disk_dev($domain, $olddev, $dev) { + $domain = $this->get_domain_object($domain); + + $xml = $this->domain_get_xml($domain); + $tmp = explode("\n", $xml); + for ($i = 0; $i < sizeof($tmp); $i++) + if (strpos('.'.$tmp[$i], "domain_define($xml); + } + +//create metadata node for domain + function domain_set_metadata($domain) { + $domain = $this->get_domain_object($domain); + + $xml = $this->domain_get_xml($domain); + $metadata = $this->get_xpath($domain, '//domain/metadata', false); + if (empty($metadata)){ + $description = $this->domain_get_description($domain); + if(!$description) + $node = ""; + else + $node = ""; + $desc = "$node\n\n\n"; + $xml = str_replace($node, $desc, $xml); + } + return $this->domain_define($xml); + } + +//set description for snapshot + function snapshot_set_metadata($domain, $name, $desc) { + $this->domain_set_metadata($domain); + $domain = $this->get_domain_object($domain); + + $xml = $this->domain_get_xml($domain); + $metadata = $this->get_xpath($domain, '//domain/metadata/snapshot'.$name, false); + if (empty($metadata)){ + $desc = "\n$desc\n"; + $xml = str_replace('', $desc, $xml); + } else { + $tmp = explode("\n", $xml); + for ($i = 0; $i < sizeof($tmp); $i++) + if (strpos('.'.$tmp[$i], 'domain_define($xml); + } + +//get host node info + function host_get_node_info() { + $tmp = libvirt_node_get_info($this->conn); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + +//get domain autostart status true or false + function domain_get_autostart($domain) { + $domain = $this->get_domain_object($domain); + $tmp = libvirt_domain_get_autostart($domain); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + +//set domain to start with libvirt + function domain_set_autostart($domain,$flags) { + $domain = $this->get_domain_object($domain); + $tmp = libvirt_domain_set_autostart($domain,$flags); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + +//list all snapshots for domain + function domain_snapshots_list($domain) { + $tmp = libvirt_list_domain_snapshots($domain); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + +// create a snapshot and metadata node for description + function domain_snapshot_create($domain) { + $this->domain_set_metadata($domain); + $domain = $this->get_domain_object($domain); + $tmp = libvirt_domain_snapshot_create($domain); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + +//delete snapshot and metadata + function domain_snapshot_delete($domain, $name) { + $this->snapshot_remove_metadata($domain, $name); + $name = $this->domain_snapshot_lookup_by_name($domain, $name); + $tmp = libvirt_domain_snapshot_delete($name); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + +//get resource number of snapshot + function domain_snapshot_lookup_by_name($domain, $name) { + $domain = $this->get_domain_object($domain); + $tmp = libvirt_domain_snapshot_lookup_by_name($domain, $name); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + +//revert domain to snapshot state + function domain_snapshot_revert($domain, $name) { + $name = $this->domain_snapshot_lookup_by_name($domain, $name); + $tmp = libvirt_domain_snapshot_revert($name); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + +//get snapshot description + function domain_snapshot_get_info($domain, $name) { + $domain = $this->get_domain_object($domain); + $tmp = $this->get_xpath($domain, '//domain/metadata/snapshot'.$name, false); + $var = $tmp[0]; + unset($tmp); + + return $var; + } + +//remove snapshot metadata + function snapshot_remove_metadata($domain, $name) { + $domain = $this->get_domain_object($domain); + + $xml = $this->domain_get_xml($domain); + $tmp = explode("\n", $xml); + for ($i = 0; $i < sizeof($tmp); $i++) + if (strpos('.'.$tmp[$i], 'domain_define($xml); + } + +//change cdrom media + function domain_change_cdrom($domain, $iso, $dev, $bus) { + $domain = $this->get_domain_object($domain); + $tmp = libvirt_domain_update_device($domain, "", VIR_DOMAIN_DEVICE_MODIFY_CONFIG); + if ($this->domain_is_active($domain)) + libvirt_domain_update_device($domain, "", VIR_DOMAIN_DEVICE_MODIFY_LIVE); + return ($tmp) ? $tmp : $this->_set_last_error(); + } + +//change disk capacity + function disk_set_cap($disk, $cap) { + $xml = $this->domain_get_xml($domain); + $tmp = explode("\n", $xml); + for ($i = 0; $i < sizeof($tmp); $i++) + if (strpos('.'.$tmp[$i], ""; + + $xml = join("\n", $tmp); + + return $this->domain_define($xml); + } + } +?> \ No newline at end of file diff --git a/plugins/dynamix.vm.manager/classes/libvirt_helpers.php b/plugins/dynamix.vm.manager/classes/libvirt_helpers.php new file mode 100644 index 000000000..0d268aaa8 --- /dev/null +++ b/plugins/dynamix.vm.manager/classes/libvirt_helpers.php @@ -0,0 +1,542 @@ + + /dev/null`/exe ] && echo 'yes' || echo 'no' 2> /dev/null" )); + + // Create domain config if needed + $domain_cfgfile = "/boot/config/domain.cfg"; + if (!file_exists($domain_cfgfile)) { + file_put_contents($domain_cfgfile, 'SERVICE="disable"'."\n".'DEBUG="no"'."\n".'MEDIADIR="/mnt/"'."\n".'VIRTIOISO=""'."\n".'DISKDIR="/mnt/"'."\n".'BRNAME=""'."\n"); + } else { + // This will clean any ^M characters (\r) caused by windows from the config file + shell_exec("sed -i 's!\r!!g' '$domain_cfgfile'"); + } + + $domain_cfg = parse_ini_file($domain_cfgfile); + + if (!isset($domain_cfg['VIRTIOISO'])) { + $domain_cfg['VIRTIOISO'] = ""; + } + + $domain_debug = isset($domain_cfg['DEBUG']) ? $domain_cfg['DEBUG'] : "no"; + if ($domain_debug != "yes") { + error_reporting(0); + } + + $domain_bridge = (!($domain_cfg['BRNAME'])) ? 'virbr0' : $domain_cfg['BRNAME']; + $msg = (empty($domain_bridge)) ? "Error: Setup Bridge in Settings/Network Settings" : false; + $libvirt_service = isset($domain_cfg['SERVICE']) ? $domain_cfg['SERVICE'] : "disable"; + + if ($libvirt_running == "yes"){ + $lv = new Libvirt('qemu:///system', null, null, false); + $arrHostInfo = $lv->host_get_node_info(); + $maxcpu = (int)$arrHostInfo['cpus']; + $maxmem = number_format(($arrHostInfo['memory'] / 1048576), 1, '.', ' '); + } + + $theme = $display['theme']; + //set color on even rows for white or black theme + function bcolor($row, $color) { + if ($color == "white") + $color = ($row % 2 == 0) ? "transparent" : "#F8F8F8"; + else + $color = ($row % 2 == 0) ? "transparent" : "#0C0C0C"; + return $color; + } + + //create checkboxes for usb devices + function usb_checkbox($usb, $key) { + $deviceid = substr(strstr($usb, 'ID'),3,9); + echo ''; + echo "
"; + } + + //create memory drop down option based on max memory + function memOption($maxmem) { + for ($i = 1; $i <= ($maxmem*2); $i++) { + $mem = ($i*512); + echo ""; + } + } + + //create drop down options from arrays + function arrayOptions($ValueArray, $DisplayArray, $value) { + for ($i = 0; $i < sizeof($ValueArray); $i++) { + echo ""; + else + echo ">$DisplayArray[$i]"; + } + } + + //create memory drop down options + function memOptions($maxmem, $mem) { + for ($i = 1; $i <= ($maxmem*2); $i++) { + $mem2 = ($i*512); + echo ""; + else + echo ">$mem2"; + } + } + + + function mk_dropdown_options($arrOptions, $strSelected) { + foreach ($arrOptions as $key => $label) { + echo mk_option($strSelected, $key, $label); + } + } + + function appendOrdinalSuffix($number) { + $ends = array('th','st','nd','rd','th','th','th','th','th','th'); + + if (($number % 100) >= 11 && ($number % 100) <= 13) { + $abbreviation = $number . 'th'; + } else { + $abbreviation = $number . $ends[$number % 10]; + } + + return $abbreviation; + } + + + $cacheValidPCIDevices = null; + function getValidPCIDevices() { + global $cacheValidPCIDevices; + + if (!is_null($cacheValidPCIDevices)) { + return $cacheValidPCIDevices; + } + + $strOSUSBController = trim(shell_exec("udevadm info -q path -n /dev/disk/by-label/UNRAID 2>/dev/null | grep -Po '0000:\K\w{2}:\w{2}\.\w{1}'")); + $strOSNetworkDevice = trim(shell_exec("udevadm info -q path -p /sys/class/net/eth0 2>/dev/null | grep -Po '0000:\K\w{2}:\w{2}\.\w{1}'")); + + //TODO: add any drive controllers currently being used by unraid to the blacklist + + $arrBlacklistIDs = array($strOSUSBController, $strOSNetworkDevice); + $arrBlacklistClassIDregex = '/^(05|06|08|0a|0b|0c05)/'; + // Got Class IDs at the bottom of /usr/share/hwdata/pci.ids + $arrWhitelistGPUClassIDregex = '/^(0001|03)/'; + $arrWhitelistAudioClassIDregex = '/^(0403)/'; + + $arrValidPCIDevices = array(); + + exec("lspci -m -nn 2>/dev/null", $arrAllPCIDevices); + + foreach ($arrAllPCIDevices as $strPCIDevice) { + // Example: 00:1f.0 "ISA bridge [0601]" "Intel Corporation [8086]" "Z77 Express Chipset LPC Controller [1e44]" -r04 "Micro-Star International Co., Ltd. [MSI] [1462]" "Device [7759]" + if (preg_match('/^(?P\S+) \"(?P[^"]+) \[(?P[a-f0-9]{4})\]\" \"(?P[^"]+) \[(?P[a-f0-9]{4})\]\" \"(?P[^"]+) \[(?P[a-f0-9]{4})\]\"/', $strPCIDevice, $arrMatch)) { + if (in_array($arrMatch['id'], $arrBlacklistIDs) || preg_match($arrBlacklistClassIDregex, $arrMatch['typeid'])) { + // Device blacklisted, skip device + continue; + } + + $strClass = 'other'; + if (preg_match($arrWhitelistGPUClassIDregex, $arrMatch['typeid'])) { + $strClass = 'vga'; + // Specialized product name cleanup for GPU + // GF116 [GeForce GTX 550 Ti] --> GeForce GTX 550 Ti + if (preg_match('/.+\[(?P.+)\]/', $arrMatch['productname'], $arrGPUMatch)) { + $arrMatch['productname'] = $arrGPUMatch['gpuname']; + } + } else if (preg_match($arrWhitelistAudioClassIDregex, $arrMatch['typeid'])) { + $strClass = 'audio'; + } + + if ($strClass == 'vga' && + strpos($arrMatch['id'], '00:') === 0 && + (stripos($arrMatch['productname'], 'integrated') !== false || strpos($arrMatch['vendorname'], 'Intel ') !== false)) { + // Our sorry attempt to detect a integrated gpu + // Integrated gpus dont work for passthrough, skip device + continue; + } + + if (!file_exists('/sys/bus/pci/devices/0000:' . $arrMatch['id'] . '/iommu_group/')) { + // No IOMMU support for device, skip device + continue; + } + + // Specialized vendor name cleanup + // e.g.: Advanced Micro Devices, Inc. [AMD/ATI] --> Advanced Micro Devices, Inc. + if (preg_match('/(?P.+) \[.+\]/', $arrMatch['vendorname'], $arrGPUMatch)) { + $arrMatch['vendorname'] = $arrGPUMatch['gpuvendor']; + } + + // Clean up the vendor and product name + $arrMatch['vendorname'] = str_replace(['Advanced Micro Devices, Inc.'], 'AMD', $arrMatch['vendorname']); + $arrMatch['vendorname'] = str_replace([' Corporation', ' Semiconductor Co., Ltd.', ' Technology Group Ltd.', ' Electronics Systems Ltd.', ' Systems, Inc.'], '', $arrMatch['vendorname']); + $arrMatch['productname'] = str_replace([' PCI Express'], [' PCIe'], $arrMatch['productname']); + + $arrValidPCIDevices[] = array( + 'id' => $arrMatch['id'], + 'type' => $arrMatch['type'], + 'typeid' => $arrMatch['typeid'], + 'vendorid' => $arrMatch['vendorid'], + 'vendorname' => $arrMatch['vendorname'], + 'productid' => $arrMatch['productid'], + 'productname' => $arrMatch['productname'], + 'class' => $strClass, + 'name' => $arrMatch['vendorname'] . ' ' . $arrMatch['productname'] + ); + } + } + + $cacheValidPCIDevices = $arrValidPCIDevices; + + return $arrValidPCIDevices; + } + + + function getValidGPUDevices() { + $arrValidPCIDevices = getValidPCIDevices(); + + $arrValidGPUDevices = array_filter($arrValidPCIDevices, function($arrDev) { + return ($arrDev['class'] == 'vga'); + }); + + return $arrValidGPUDevices; + } + + + function getValidAudioDevices() { + $arrValidPCIDevices = getValidPCIDevices(); + + $arrValidAudioDevices = array_filter($arrValidPCIDevices, function($arrDev) { + return ($arrDev['class'] == 'audio'); + }); + + return $arrValidAudioDevices; + } + + + function getValidOtherDevices() { + $arrValidPCIDevices = getValidPCIDevices(); + + $arrValidOtherDevices = array_filter($arrValidPCIDevices, function($arrDev) { + return ($arrDev['class'] == 'other'); + }); + + return $arrValidOtherDevices; + } + + + $cacheValidUSBDevices = null; + function getValidUSBDevices() { + global $cacheValidUSBDevices; + + if (!is_null($cacheValidUSBDevices)) { + return $cacheValidUSBDevices; + } + + $arrValidUSBDevices = array(); + + // Get a list of all usb hubs so we can blacklist them + exec("cat /sys/bus/usb/drivers/hub/*/modalias | grep -Po 'usb:v\K\w{9}' | tr 'p' ':'", $arrAllUSBHubs); + + exec("lsusb 2>/dev/null", $arrAllUSBDevices); + + foreach ($arrAllUSBDevices as $strUSBDevice) { + if (preg_match('/^.+ID (?P\S+) (?P.+)$/', $strUSBDevice, $arrMatch)) { + $arrMatch['name'] = trim($arrMatch['name']); + + if (empty($arrMatch['name'])) { + // Device name is blank, replace using fallback default + $arrMatch['name'] = 'unnamed device ('.$arrMatch['id'].')'; + } + + if (stripos($GLOBALS['var']['flashGUID'], str_replace(':', '-', $arrMatch['id'])) === 0) { + // Device id matches the unraid boot device, skip device + continue; + } + + if (in_array(strtoupper($arrMatch['id']), $arrAllUSBHubs)) { + // Device class is a Hub, skip device + continue; + } + + $arrValidUSBDevices[] = array( + 'id' => $arrMatch['id'], + 'name' => $arrMatch['name'], + ); + } + } + + $cacheValidUSBDevices = $arrValidUSBDevices; + + return $arrValidUSBDevices; + } + + + function getValidMachineTypes() { + global $lv; + + $arrValidMachineTypes = []; + + $arrQEMUInfo = $lv->get_connect_information(); + $arrMachineTypes = $lv->get_machine_types('x86_64'); + + $strQEMUVersion = $arrQEMUInfo['hypervisor_major'] . '.' . $arrQEMUInfo['hypervisor_minor']; + + foreach ($arrMachineTypes as $arrMachine) { + if ($arrMachine['name'] == 'q35') { + // Latest Q35 + $arrValidMachineTypes['pc-q35-' . $strQEMUVersion] = 'Q35-' . $strQEMUVersion; + } + if (strpos($arrMachine['name'], 'q35-') !== false) { + // Prior releases of Q35 + $arrValidMachineTypes[$arrMachine['name']] = str_replace(['q35', 'pc-'], ['Q35', ''], $arrMachine['name']); + } + if ($arrMachine['name'] == 'pc') { + // Latest i440fx + $arrValidMachineTypes['pc-i440fx-' . $strQEMUVersion] = 'i440fx-' . $strQEMUVersion; + } + if (strpos($arrMachine['name'], 'i440fx-') !== false) { + // Prior releases of i440fx + $arrValidMachineTypes[$arrMachine['name']] = str_replace('pc-', '', $arrMachine['name']); + } + } + + arsort($arrValidMachineTypes); + + return $arrValidMachineTypes; + } + + + function getLatestMachineType($strType = 'i440fx') { + $arrMachineTypes = getValidMachineTypes(); + + foreach ($arrMachineTypes as $key => $value) { + if (stripos($key, $strType) !== false) { + return $key; + } + } + + return array_shift(array_keys($arrMachineTypes)); + } + + + function getValidDiskDrivers() { + $arrValidDiskDrivers = [ + 'raw' => 'raw', + 'qcow2' => 'qcow2' + ]; + + return $arrValidDiskDrivers; + } + + + function getValidKeyMaps() { + $arrValidKeyMaps = [ + 'ar' => 'Arabic (ar)', + 'hr' => 'Croatian (hr)', + 'cz' => 'Czech (cz)', + 'da' => 'Danish (da)', + 'nl' => 'Dutch (nl)', + 'nl-be' => 'Dutch-Belgium (nl-be)', + 'en-gb' => 'English-United Kingdom (en-gb)', + 'en-us' => 'English-United States (en-us)', + 'es' => 'Español (es)', + 'et' => 'Estonian (et)', + 'fo' => 'Faroese (fo)', + 'fi' => 'Finnish (fi)', + 'fr' => 'French (fr)', + 'bepo' => 'French-Bépo (bepo)', + 'fr-be' => 'French-Belgium (fr-be)', + 'fr-ca' => 'French-Canadian (fr-ca)', + 'fr-ch' => 'French-Switzerland (fr-ch)', + 'de-ch' => 'German-Switzerland (de-ch)', + 'hu' => 'Hungarian (hu)', + 'is' => 'Icelandic (is)', + 'it' => 'Italian (it)', + 'ja' => 'Japanese (ja)', + 'lv' => 'Latvian (lv)', + 'lt' => 'Lithuanian (lt)', + 'mk' => 'Macedonian (mk)', + 'no' => 'Norwegian (no)', + 'pl' => 'Polish (pl)', + 'pt-br' => 'Portuguese-Brazil (pt-br)', + 'ru' => 'Russian (ru)', + 'sl' => 'Slovene (sl)', + 'sv' => 'Swedish (sv)', + 'th' => 'Thailand (th)', + 'tr' => 'Turkish (tr)' + ]; + + return $arrValidKeyMaps; + } + + + function getHostCPUModel() { + $cpu = explode('#', exec("dmidecode -q -t 4|awk -F: '{if(/Version:/) v=$2; else if(/Current Speed:/) s=$2} END{print v\"#\"s}'")); + list($strCPUModel) = explode('@', str_replace(array("Processor","CPU","(C)","(R)","(TM)"), array("","","©","®","™"), $cpu[0]) . '@'); + return trim($strCPUModel); + } + + + function getNetworkBridges() { + exec("brctl show | awk -F'\t' 'FNR > 1 {print \$1}' | awk 'NF > 0'", $arrValidBridges); + + if (!is_array($arrValidBridges)) { + $arrValidBridges = []; + } + + // Make sure the default libvirt bridge is first in the list + if (($key = array_search('virbr0', $arrValidBridges)) !== false) { + unset($arrValidBridges[$key]); + } + // We always list virbr0 because libvirt might not be started yet (thus the bridge doesn't exists) + array_unshift($arrValidBridges, 'virbr0'); + + return array_values($arrValidBridges); + } + + + function domain_to_config($uuid) { + global $lv; + + $arrValidGPUDevices = getValidGPUDevices(); + $arrValidAudioDevices = getValidAudioDevices(); + $arrValidOtherDevices = getValidOtherDevices(); + $arrValidUSBDevices = getValidUSBDevices(); + $arrValidDiskDrivers = getValidDiskDrivers(); + + $res = $lv->domain_get_domain_by_uuid($uuid); + $dom = $lv->domain_get_info($res); + $medias = $lv->get_cdrom_stats($res); + $disks = $lv->get_disk_stats($res, false); + $arrNICs = $lv->get_nic_info($res); + $arrHostDevs = $lv->domain_get_host_devices_pci($res); + $arrUSBDevs = $lv->domain_get_host_devices_usb($res); + + + // Metadata Parsing + // libvirt xpath parser sucks, use php's xpath parser instead + $strDOMXML = $lv->domain_get_xml($res); + $xmldoc = new DOMDocument(); + $xmldoc->loadXML($strDOMXML); + $xpath = new DOMXPath($xmldoc); + $objNodes = $xpath->query('//domain/metadata/vmtemplate/@*'); + + $arrTemplateValues = []; + if ($objNodes->length > 0) { + foreach ($objNodes as $objNode) { + $arrTemplateValues[$objNode->nodeName] = $objNode->nodeValue; + } + } + + if (empty($arrTemplateValues['name'])) { + $arrTemplateValues['name'] = 'Custom'; + } + + + $arrGPUDevices = []; + $arrAudioDevices = []; + $arrOtherDevices = []; + + // check for vnc; add to arrGPUDevices + $intVNCPort = $lv->domain_get_vnc_port($res); + if (!empty($intVNCPort)) { + $arrGPUDevices[] = [ + 'id' => 'vnc', + 'keymap' => $lv->domain_get_vnc_keymap($res) + ]; + } + + foreach ($arrHostDevs as $arrHostDev) { + $arrFoundGPUDevices = array_filter($arrValidGPUDevices, function($arrDev) use ($arrHostDev) { return ($arrDev['id'] == $arrHostDev['id']); }); + if (!empty($arrFoundGPUDevices)) { + $arrGPUDevices[] = ['id' => $arrHostDev['id']]; + continue; + } + + $arrFoundAudioDevices = array_filter($arrValidAudioDevices, function($arrDev) use ($arrHostDev) { return ($arrDev['id'] == $arrHostDev['id']); }); + if (!empty($arrFoundAudioDevices)) { + $arrAudioDevices[] = ['id' => $arrHostDev['id']]; + continue; + } + + $arrFoundOtherDevices = array_filter($arrValidOtherDevices, function($arrDev) use ($arrHostDev) { return ($arrDev['id'] == $arrHostDev['id']); }); + if (!empty($arrFoundOtherDevices)) { + $arrOtherDevices[] = ['id' => $arrHostDev['id']]; + continue; + } + } + + // Add claimed USB devices by this VM to the available USB devices + /* + foreach($arrUSBDevs as $arrUSB) { + $arrValidUSBDevices[] = array( + 'id' => $arrUSB['id'], + 'name' => $arrUSB['product'], + ); + } + */ + + $arrDisks = []; + foreach ($disks as $disk) { + $arrDisks[] = [ + 'new' => (empty($disk['file']) ? $disk['partition'] : $disk['file']), + 'size' => '', + 'driver' => 'raw', + 'dev' => $disk['device'], + 'bus' => $disk['bus'] + ]; + } + + return [ + 'template' => $arrTemplateValues, + 'domain' => [ + 'name' => $lv->domain_get_name($res), + 'desc' => $lv->domain_get_description($res), + 'persistent' => 1, + 'uuid' => $lv->domain_get_uuid($res), + 'clock' => $lv->domain_get_clock_offset($res), + 'arch' => $lv->domain_get_arch($res), + 'machine' => $lv->domain_get_machine($res), + 'mem' => $lv->domain_get_current_memory($res), + 'maxmem' => $lv->domain_get_memory($res), + 'password' => '', //TODO? + 'cpumode' => $lv->domain_get_cpu_type($res), + 'vcpus' => $dom['nrVirtCpu'], + 'vcpu' => $lv->domain_get_vcpu_pins($res), + 'hyperv' => ($lv->domain_get_feature($res, 'hyperv') ? 1 : 0), + 'autostart' => ($lv->domain_get_autostart($res) ? 1 : 0), + 'state' => $lv->domain_state_translate($dom['state']), + 'ovmf' => ($lv->domain_get_ovmf($res) ? 1 : 0) + ], + 'media' => [ + 'cdrom' => (!empty($medias) && !empty($medias[0]) && array_key_exists('file', $medias[0])) ? $medias[0]['file'] : '', + 'drivers' => (!empty($medias) && !empty($medias[1]) && array_key_exists('file', $medias[1])) ? $medias[1]['file'] : '' + ], + 'disk' => $arrDisks, + 'gpu' => $arrGPUDevices, + 'audio' => $arrAudioDevices, + 'pci' => $arrOtherDevices, + 'nic' => $arrNICs, + 'usb' => $arrUSBDevs, + 'shares' => $lv->domain_get_mount_filesystems($res) + ]; + } + +?> \ No newline at end of file diff --git a/plugins/dynamix.vm.manager/dynamix.kvm.manager/domain.tar.xz b/plugins/dynamix.vm.manager/dynamix.kvm.manager/domain.tar.xz new file mode 100644 index 000000000..4ed31250a Binary files /dev/null and b/plugins/dynamix.vm.manager/dynamix.kvm.manager/domain.tar.xz differ diff --git a/plugins/dynamix.vm.manager/dynamix.kvm.manager/qemu b/plugins/dynamix.vm.manager/dynamix.kvm.manager/qemu new file mode 100644 index 000000000..0b454cf26 --- /dev/null +++ b/plugins/dynamix.vm.manager/dynamix.kvm.manager/qemu @@ -0,0 +1,74 @@ +#!/usr/bin/env php + +loadXML($strXML); + +$xpath = new DOMXpath($doc); + +$args = $xpath->evaluate("//domain/*[name()='qemu:commandline']/*[name()='qemu:arg']/@value"); + +for ($i = 0; $i < $args->length; $i++){ + $arg_list = explode(',', $args->item($i)->nodeValue); + + if ($arg_list[0] !== 'vfio-pci') { + continue; + } + + foreach ($arg_list as $arg) { + $keypair = explode('=', $arg); + + if ($keypair[0] == 'host' && !empty($keypair[1])) { + vfio_bind($keypair[1]); + break; + } + } +} + +exit(0); // end of script + + + +function vfio_bind($strPassthruDevice) { + // Ensure we have leading 0000: + $strPassthruDeviceShort = str_replace('0000:', '', $strPassthruDevice); + $strPassthruDeviceLong = '0000:' . $strPassthruDeviceShort; + + // Determine the driver currently assigned to the device + $strDriverSymlink = @readlink('/sys/bus/pci/devices/' . $strPassthruDeviceLong . '/driver'); + + if ($strDriverSymlink !== false) { + // Device is bound to a Driver already + + if (strpos($strDriverSymlink, 'vfio-pci') !== false) { + // Driver bound to vfio-pci already - nothing left to do for this device now regarding vfio + return true; + } + + // Driver bound to some other driver - attempt to unbind driver + if (file_put_contents('/sys/bus/pci/devices/' . $strPassthruDeviceLong . '/driver/unbind', $strPassthruDeviceLong) === false) { + file_put_contents('php://stderr', 'Failed to unbind device ' . $strPassthruDeviceShort . ' from current driver'); + exit(1); + return false; + } + } + + // Get Vendor and Device IDs for the passthru device + $strVendor = file_get_contents('/sys/bus/pci/devices/' . $strPassthruDeviceLong . '/vendor'); + $strDevice = file_get_contents('/sys/bus/pci/devices/' . $strPassthruDeviceLong . '/device'); + + // Attempt to bind driver to vfio-pci + if (file_put_contents('/sys/bus/pci/drivers/vfio-pci/new_id', $strVendor . ' ' . $strDevice) === false) { + file_put_contents('php://stderr', 'Failed to bind device ' . $strPassthruDeviceShort . ' to vfio-pci driver'); + exit(1); + return false; + } + + return true; +} diff --git a/plugins/dynamix.vm.manager/event/started b/plugins/dynamix.vm.manager/event/started new file mode 100755 index 000000000..9dd522e20 --- /dev/null +++ b/plugins/dynamix.vm.manager/event/started @@ -0,0 +1,43 @@ +#!/bin/sh + +# Only start if array has started in Normal operation mode +if grep -q 'fsState="Started"' /var/local/emhttp/var.ini && grep -q 'startMode="Normal"' /var/local/emhttp/var.ini; then + + SERVICE="disable" + if [ -f /boot/config/domain.cfg ]; then + source /boot/config/domain.cfg + fi + + #copy old image to new + if [ -f /boot/config/plugins/virtMan/virtMan.img ]; then + if [ "$(mount | grep virtMan.img)" ]; then + umount /etc/libvirt + fi + if [ ! -f /boot/config/plugins/dynamix.kvm.manager/domain.img ]; then + mkdir -p /boot/config/plugins/dynamix.kvm.manager + cp /boot/config/plugins/virtMan/virtMan.img /boot/config/plugins/dynamix.kvm.manager/domain.img + fi + fi + + #copy seed loopback image and qemu hook, if needed, to flash drive + if [ -d /usr/local/emhttp/plugins/dynamix.vm.manager/dynamix.kvm.manager ]; then + mkdir -p /boot/config/plugins/dynamix.kvm.manager + tar --no-same-owner -xkf /usr/local/emhttp/plugins/dynamix.vm.manager/dynamix.kvm.manager/domain.tar.xz -C /boot/config/plugins/dynamix.kvm.manager/ + cp -n /usr/local/emhttp/plugins/dynamix.vm.manager/dynamix.kvm.manager/qemu /boot/config/plugins/dynamix.kvm.manager/ + fi + + if [ "$SERVICE" = "enable" ]; then + # mount xml/conf image if not already mounted + if [ ! "$(mount | grep domain.img)" ]; then + mount -t ext4 /boot/config/plugins/dynamix.kvm.manager/domain.img /etc/libvirt + mkdir -p /etc/libvirt/hooks + cp /boot/config/plugins/dynamix.kvm.manager/qemu /etc/libvirt/hooks/ + fi + + # Start libvirt + if [ -x /etc/rc.d/rc.libvirt ]; then + echo "Starting libvirt..." + /etc/rc.d/rc.libvirt start |& logger + fi + fi +fi diff --git a/plugins/dynamix.vm.manager/event/stopping_svcs b/plugins/dynamix.vm.manager/event/stopping_svcs new file mode 100755 index 000000000..75bf0c66b --- /dev/null +++ b/plugins/dynamix.vm.manager/event/stopping_svcs @@ -0,0 +1,11 @@ +#!/bin/sh + +# Shutdown libvirt +if [ -x /etc/rc.d/rc.libvirt ]; then + echo "Stopping libvirt..." + /etc/rc.d/rc.libvirt stop |& logger +fi + +if [ "$(mount | grep domain.img)" ]; then + umount /etc/libvirt +fi diff --git a/plugins/dynamix.vm.manager/icons/addvm.png b/plugins/dynamix.vm.manager/icons/addvm.png new file mode 100644 index 000000000..dec741630 Binary files /dev/null and b/plugins/dynamix.vm.manager/icons/addvm.png differ diff --git a/plugins/dynamix.vm.manager/icons/addxml.png b/plugins/dynamix.vm.manager/icons/addxml.png new file mode 100644 index 000000000..e9747fa43 Binary files /dev/null and b/plugins/dynamix.vm.manager/icons/addxml.png differ diff --git a/plugins/dynamix.vm.manager/icons/devices.png b/plugins/dynamix.vm.manager/icons/devices.png new file mode 100644 index 000000000..c2d68cc39 Binary files /dev/null and b/plugins/dynamix.vm.manager/icons/devices.png differ diff --git a/plugins/dynamix.vm.manager/icons/storage.png b/plugins/dynamix.vm.manager/icons/storage.png new file mode 100644 index 000000000..bce0e7bd8 Binary files /dev/null and b/plugins/dynamix.vm.manager/icons/storage.png differ diff --git a/plugins/dynamix.vm.manager/icons/updatevm.png b/plugins/dynamix.vm.manager/icons/updatevm.png new file mode 100644 index 000000000..049fb14c0 Binary files /dev/null and b/plugins/dynamix.vm.manager/icons/updatevm.png differ diff --git a/plugins/dynamix.vm.manager/icons/virtualmachines.png b/plugins/dynamix.vm.manager/icons/virtualmachines.png new file mode 100644 index 000000000..d4cfd34f9 Binary files /dev/null and b/plugins/dynamix.vm.manager/icons/virtualmachines.png differ diff --git a/plugins/dynamix.vm.manager/icons/vmmanager.png b/plugins/dynamix.vm.manager/icons/vmmanager.png new file mode 100644 index 000000000..715bef649 Binary files /dev/null and b/plugins/dynamix.vm.manager/icons/vmmanager.png differ diff --git a/plugins/dynamix.vm.manager/images/alt.png b/plugins/dynamix.vm.manager/images/alt.png new file mode 100644 index 000000000..d42af7b42 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/alt.png differ diff --git a/plugins/dynamix.vm.manager/images/cdrom.png b/plugins/dynamix.vm.manager/images/cdrom.png new file mode 100644 index 000000000..3b2df0bfc Binary files /dev/null and b/plugins/dynamix.vm.manager/images/cdrom.png differ diff --git a/plugins/dynamix.vm.manager/images/clipboard.png b/plugins/dynamix.vm.manager/images/clipboard.png new file mode 100644 index 000000000..24df33c1c Binary files /dev/null and b/plugins/dynamix.vm.manager/images/clipboard.png differ diff --git a/plugins/dynamix.vm.manager/images/connect.png b/plugins/dynamix.vm.manager/images/connect.png new file mode 100644 index 000000000..79e71adb8 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/connect.png differ diff --git a/plugins/dynamix.vm.manager/images/ctrl.png b/plugins/dynamix.vm.manager/images/ctrl.png new file mode 100644 index 000000000..a63b601f1 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/ctrl.png differ diff --git a/plugins/dynamix.vm.manager/images/ctrlaltdel.png b/plugins/dynamix.vm.manager/images/ctrlaltdel.png new file mode 100644 index 000000000..31922e532 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/ctrlaltdel.png differ diff --git a/plugins/dynamix.vm.manager/images/db.png b/plugins/dynamix.vm.manager/images/db.png new file mode 100644 index 000000000..bddba1f98 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/db.png differ diff --git a/plugins/dynamix.vm.manager/images/disconnect.png b/plugins/dynamix.vm.manager/images/disconnect.png new file mode 100644 index 000000000..8832f5ea7 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/disconnect.png differ diff --git a/plugins/dynamix.vm.manager/images/drag.png b/plugins/dynamix.vm.manager/images/drag.png new file mode 100644 index 000000000..433f896d6 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/drag.png differ diff --git a/plugins/dynamix.vm.manager/images/dynamix.vm.manager.png b/plugins/dynamix.vm.manager/images/dynamix.vm.manager.png new file mode 100644 index 000000000..0e23b804b Binary files /dev/null and b/plugins/dynamix.vm.manager/images/dynamix.vm.manager.png differ diff --git a/plugins/dynamix.vm.manager/images/esc.png b/plugins/dynamix.vm.manager/images/esc.png new file mode 100644 index 000000000..ece5f7cbe Binary files /dev/null and b/plugins/dynamix.vm.manager/images/esc.png differ diff --git a/plugins/dynamix.vm.manager/images/favicon.ico b/plugins/dynamix.vm.manager/images/favicon.ico new file mode 100644 index 000000000..c999634f0 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/favicon.ico differ diff --git a/plugins/dynamix.vm.manager/images/favicon.png b/plugins/dynamix.vm.manager/images/favicon.png new file mode 100644 index 000000000..e2bdb1943 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/favicon.png differ diff --git a/plugins/dynamix.vm.manager/images/fullscreen.png b/plugins/dynamix.vm.manager/images/fullscreen.png new file mode 100644 index 000000000..f4fa0ce83 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/fullscreen.png differ diff --git a/plugins/dynamix.vm.manager/images/green-on.png b/plugins/dynamix.vm.manager/images/green-on.png new file mode 100644 index 000000000..e326b04d9 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/green-on.png differ diff --git a/plugins/dynamix.vm.manager/images/keyboard.png b/plugins/dynamix.vm.manager/images/keyboard.png new file mode 100644 index 000000000..f79795251 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/keyboard.png differ diff --git a/plugins/dynamix.vm.manager/images/mouse_left.png b/plugins/dynamix.vm.manager/images/mouse_left.png new file mode 100644 index 000000000..1de7a486c Binary files /dev/null and b/plugins/dynamix.vm.manager/images/mouse_left.png differ diff --git a/plugins/dynamix.vm.manager/images/mouse_middle.png b/plugins/dynamix.vm.manager/images/mouse_middle.png new file mode 100644 index 000000000..81fbd9bd3 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/mouse_middle.png differ diff --git a/plugins/dynamix.vm.manager/images/mouse_none.png b/plugins/dynamix.vm.manager/images/mouse_none.png new file mode 100644 index 000000000..93dbf5780 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/mouse_none.png differ diff --git a/plugins/dynamix.vm.manager/images/mouse_right.png b/plugins/dynamix.vm.manager/images/mouse_right.png new file mode 100644 index 000000000..355b25dc9 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/mouse_right.png differ diff --git a/plugins/dynamix.vm.manager/images/power.png b/plugins/dynamix.vm.manager/images/power.png new file mode 100644 index 000000000..f68fd0813 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/power.png differ diff --git a/plugins/dynamix.vm.manager/images/red-on.png b/plugins/dynamix.vm.manager/images/red-on.png new file mode 100644 index 000000000..1c1ef8e9a Binary files /dev/null and b/plugins/dynamix.vm.manager/images/red-on.png differ diff --git a/plugins/dynamix.vm.manager/images/screen_320x460.png b/plugins/dynamix.vm.manager/images/screen_320x460.png new file mode 100644 index 000000000..172ec555c Binary files /dev/null and b/plugins/dynamix.vm.manager/images/screen_320x460.png differ diff --git a/plugins/dynamix.vm.manager/images/screen_57x57.png b/plugins/dynamix.vm.manager/images/screen_57x57.png new file mode 100644 index 000000000..e2085f29f Binary files /dev/null and b/plugins/dynamix.vm.manager/images/screen_57x57.png differ diff --git a/plugins/dynamix.vm.manager/images/screen_700x700.png b/plugins/dynamix.vm.manager/images/screen_700x700.png new file mode 100644 index 000000000..ae6776853 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/screen_700x700.png differ diff --git a/plugins/dynamix.vm.manager/images/settings.png b/plugins/dynamix.vm.manager/images/settings.png new file mode 100644 index 000000000..a43f5e100 Binary files /dev/null and b/plugins/dynamix.vm.manager/images/settings.png differ diff --git a/plugins/dynamix.vm.manager/images/showextrakeys.png b/plugins/dynamix.vm.manager/images/showextrakeys.png new file mode 100644 index 000000000..ad8e0a70d Binary files /dev/null and b/plugins/dynamix.vm.manager/images/showextrakeys.png differ diff --git a/plugins/dynamix.vm.manager/images/spinner.gif b/plugins/dynamix.vm.manager/images/spinner.gif new file mode 100644 index 000000000..85b99d46b Binary files /dev/null and b/plugins/dynamix.vm.manager/images/spinner.gif differ diff --git a/plugins/dynamix.vm.manager/images/tab.png b/plugins/dynamix.vm.manager/images/tab.png new file mode 100644 index 000000000..84134872a Binary files /dev/null and b/plugins/dynamix.vm.manager/images/tab.png differ diff --git a/plugins/dynamix.vm.manager/images/yellow-on.png b/plugins/dynamix.vm.manager/images/yellow-on.png new file mode 100644 index 000000000..1f5235b7a Binary files /dev/null and b/plugins/dynamix.vm.manager/images/yellow-on.png differ diff --git a/plugins/dynamix.vm.manager/include/Orbitron700.ttf b/plugins/dynamix.vm.manager/include/Orbitron700.ttf new file mode 100644 index 000000000..e28729dc5 Binary files /dev/null and b/plugins/dynamix.vm.manager/include/Orbitron700.ttf differ diff --git a/plugins/dynamix.vm.manager/include/Orbitron700.woff b/plugins/dynamix.vm.manager/include/Orbitron700.woff new file mode 100644 index 000000000..61db630cc Binary files /dev/null and b/plugins/dynamix.vm.manager/include/Orbitron700.woff differ diff --git a/plugins/dynamix.vm.manager/include/base.css b/plugins/dynamix.vm.manager/include/base.css new file mode 100644 index 000000000..2769357e2 --- /dev/null +++ b/plugins/dynamix.vm.manager/include/base.css @@ -0,0 +1,526 @@ +/* + * noVNC base CSS + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +body { + margin:0; + padding:0; + font-family: Helvetica; + /*Background image with light grey curve.*/ + background-color:#494949; + background-repeat:no-repeat; + background-position:right bottom; + height:100%; +} + +html { + height:100%; +} + +#noVNC_controls ul { + list-style: none; + margin: 0px; + padding: 0px; +} +#noVNC_controls li { + padding-bottom:8px; +} + +#noVNC_host { + width:150px; +} +#noVNC_port { + width: 80px; +} +#noVNC_password { + width: 150px; +} +#noVNC_encrypt { +} +#noVNC_path { + width: 100px; +} +#noVNC_connect_button { + width: 110px; + float:right; +} + +#noVNC_buttons { + white-space: nowrap; +} + +#noVNC_view_drag_button { + display: none; +} +#sendCtrlAltDelButton { + display: none; +} +#fullscreenButton { + display: none; +} +#noVNC_xvp_buttons { + display: none; +} +#noVNC_mobile_buttons { + display: none; +} + +#noVNC_extra_keys { + display: inline; + list-style-type: none; + padding: 0px; + margin: 0px; + position: relative; +} + +.noVNC-buttons-left { + float: left; + z-index: 1; + position: relative; +} + +.noVNC-buttons-right { + float:right; + right: 0px; + z-index: 2; + position: absolute; +} + +#noVNC_status { + font-size: 12px; + padding-top: 4px; + height:32px; + text-align: center; + font-weight: bold; + color: #fff; +} + +#noVNC_settings_menu { + margin: 3px; + text-align: left; +} +#noVNC_settings_menu ul { + list-style: none; + margin: 0px; + padding: 0px; +} + +#noVNC_apply { + float:right; +} + +/* Do not set width/height for VNC_screen or VNC_canvas or incorrect + * scaling will occur. Canvas resizes to remote VNC settings */ +#noVNC_screen { + display: table; + width:100%; + height:100%; + background-color:#313131; + border-bottom-right-radius: 800px 600px; + /*border-top-left-radius: 800px 600px;*/ +} + +#noVNC_container { + display: none; + position: absolute; + margin: 0px; + padding: 0px; + bottom: 0px; + top: 36px; /* the height of the control bar */ + left: 0px; + right: 0px; + width: auto; + height: auto; +} + +#noVNC_canvas { + position: absolute; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; +} + +#VNC_clipboard_clear_button { + float:right; +} +#VNC_clipboard_text { + font-size: 11px; +} + +#noVNC_clipboard_clear_button { + float:right; +} + +/*Bubble contents divs*/ +#noVNC_settings { + display:none; + margin-top:73px; + right:20px; + position:fixed; +} + +#noVNC_controls { + display:none; + margin-top:73px; + right:12px; + position:fixed; +} +#noVNC_controls.top:after { + right:15px; +} + +#noVNC_description { + display:none; + position:fixed; + + margin-top:73px; + right:20px; + left:20px; + padding:15px; + color:#000; + background:#eee; /* default background for browsers without gradient support */ + + border:2px solid #E0E0E0; + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; +} + +#noVNC_popup_status { + display:none; + position: fixed; + z-index: 1; + + margin:15px; + margin-top:60px; + padding:15px; + width:auto; + + text-align:center; + font-weight:bold; + word-wrap:break-word; + color:#fff; + background:rgba(0,0,0,0.65); + + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; +} + +#noVNC_xvp { + display:none; + margin-top:73px; + right:30px; + position:fixed; +} +#noVNC_xvp.top:after { + right:125px; +} + +#noVNC_clipboard { + display:none; + margin-top:73px; + right:30px; + position:fixed; +} +#noVNC_clipboard.top:after { + right:85px; +} + +#keyboardinput { + width:1px; + height:1px; + background-color:#fff; + color:#fff; + border:0; + position: relative; + left: -40px; + z-index: -1; + ime-mode: disabled; +} + +/* + * Advanced Styling + */ + +.noVNC_status_normal { + background: #b2bdcd; /* Old browsers */ + background: -moz-linear-gradient(top, #b2bdcd 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2bdcd), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} +.noVNC_status_error { + background: #f04040; /* Old browsers */ + background: -moz-linear-gradient(top, #f04040 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f04040), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} +.noVNC_status_warn { + background: #f0f040; /* Old browsers */ + background: -moz-linear-gradient(top, #f0f040 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f040), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} + +/* Control bar */ +#noVNC-control-bar { + position:fixed; + + display:block; + height:36px; + left:0; + top:0; + width:100%; + z-index:200; +} + +.noVNC_status_button { + padding: 4px 4px; + vertical-align: middle; + border:1px solid #869dbc; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + background: #b2bdcd; /* Old browsers */ + background: -moz-linear-gradient(top, #b2bdcd 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2bdcd), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#b2bdcd', endColorstr='#6e84a3',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ + /*box-shadow:inset 0.4px 0.4px 0.4px #000000;*/ +} + +.noVNC_status_button_selected { + padding: 4px 4px; + vertical-align: middle; + border:1px solid #4366a9; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + background: #779ced; /* Old browsers */ + background: -moz-linear-gradient(top, #779ced 0%, #3970e0 49%, #2160dd 51%, #2463df 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#779ced), color-stop(49%,#3970e0), color-stop(51%,#2160dd), color-stop(100%,#2463df)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#779ced', endColorstr='#2463df',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* W3C */ + /*box-shadow:inset 0.4px 0.4px 0.4px #000000;*/ +} + +.noVNC_status_button:disabled { + opacity: 0.4; +} + + +/*Settings Bubble*/ +.triangle-right { + position:relative; + padding:15px; + margin:1em 0 3em; + color:#fff; + background:#fff; /* default background for browsers without gradient support */ + /* css3 */ + /*background:-webkit-gradient(linear, 0 0, 0 100%, from(#2e88c4), to(#075698)); + background:-moz-linear-gradient(#2e88c4, #075698); + background:-o-linear-gradient(#2e88c4, #075698); + background:linear-gradient(#2e88c4, #075698);*/ + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; + color:#000; + border:2px solid #E0E0E0; +} + +.triangle-right.top:after { + border-color: transparent #E0E0E0; + border-width: 20px 20px 0 0; + bottom: auto; + left: auto; + right: 50px; + top: -20px; +} + +.triangle-right:after { + content:""; + position:absolute; + bottom:-20px; /* value = - border-top-width - border-bottom-width */ + left:50px; /* controls horizontal position */ + border-width:20px 0 0 20px; /* vary these values to change the angle of the vertex */ + border-style:solid; + border-color:#E0E0E0 transparent; + /* reduce the damage in FF3.0 */ + display:block; + width:0; +} + +.triangle-right.top:after { + top:-40px; /* value = - border-top-width - border-bottom-width */ + right:50px; /* controls horizontal position */ + bottom:auto; + left:auto; + border-width:40px 40px 0 0; /* vary these values to change the angle of the vertex */ + border-color:transparent #E0E0E0; +} + +/*Default noVNC logo.*/ +/* From: http://fonts.googleapis.com/css?family=Orbitron:700 */ +@font-face { + font-family: 'Orbitron'; + font-style: normal; + font-weight: 700; + src: local('?'), url('Orbitron700.woff') format('woff'), + url('Orbitron700.ttf') format('truetype'); +} + +#noVNC_logo { + margin-top: 170px; + margin-left: 10px; + color:yellow; + text-align:left; + font-family: 'Orbitron', 'OrbitronTTF', sans-serif; + line-height:90%; + text-shadow: + 5px 5px 0 #000, + -1px -1px 0 #000, + 1px -1px 0 #000, + -1px 1px 0 #000, + 1px 1px 0 #000; +} + + +#noVNC_logo span{ + color:green; +} + +/* ---------------------------------------- + * Media sizing + * ---------------------------------------- + */ + + +.noVNC_status_button { + font-size: 12px; +} + +#noVNC_clipboard_text { + width: 500px; +} + +#noVNC_logo { + font-size: 180px; +} + +.noVNC-buttons-left { + padding-left: 10px; +} + +.noVNC-buttons-right { + padding-right: 10px; +} + +#noVNC_status { + z-index: 0; + position: absolute; + width: 100%; + margin-left: 0px; +} + +#showExtraKeysButton { display: none; } +#toggleCtrlButton { display: inline; } +#toggleAltButton { display: inline; } +#sendTabButton { display: inline; } +#sendEscButton { display: inline; } + +/* left-align the status text on lower resolutions */ +@media screen and (max-width: 800px){ + #noVNC_status { + z-index: 1; + position: relative; + width: auto; + float: left; + margin-left: 4px; + } +} + +@media screen and (max-width: 640px){ + #noVNC_clipboard_text { + width: 410px; + } + #noVNC_logo { + font-size: 150px; + } + .noVNC_status_button { + font-size: 10px; + } + .noVNC-buttons-left { + padding-left: 0px; + } + .noVNC-buttons-right { + padding-right: 0px; + } + /* collapse the extra keys on lower resolutions */ + #showExtraKeysButton { + display: inline; + } + #toggleCtrlButton { + display: none; + position: absolute; + top: 30px; + left: 0px; + } + #toggleAltButton { + display: none; + position: absolute; + top: 65px; + left: 0px; + } + #sendTabButton { + display: none; + position: absolute; + top: 100px; + left: 0px; + } + #sendEscButton { + display: none; + position: absolute; + top: 135px; + left: 0px; + } +} + +@media screen and (min-width: 321px) and (max-width: 480px) { + #noVNC_clipboard_text { + width: 250px; + } + #noVNC_logo { + font-size: 110px; + } +} + +@media screen and (max-width: 320px) { + .noVNC_status_button { + font-size: 9px; + } + #noVNC_clipboard_text { + width: 220px; + } + #noVNC_logo { + font-size: 90px; + } +} diff --git a/plugins/dynamix.vm.manager/include/base64.js b/plugins/dynamix.vm.manager/include/base64.js new file mode 100644 index 000000000..651fbadc9 --- /dev/null +++ b/plugins/dynamix.vm.manager/include/base64.js @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js + +/*jslint white: false */ +/*global console */ + +var Base64 = { + /* Convert data (an array of integers) to a Base64 string. */ + toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), + base64Pad : '=', + + encode: function (data) { + "use strict"; + var result = ''; + var toBase64Table = Base64.toBase64Table; + var length = data.length; + var lengthpad = (length % 3); + // Convert every three bytes to 4 ascii characters. + + for (var i = 0; i < (length - 2); i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += toBase64Table[data[i + 2] & 0x3f]; + } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + var j = 0; + if (lengthpad === 2) { + j = length - lengthpad; + result += toBase64Table[data[j] >> 2]; + result += toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)]; + result += toBase64Table[(data[j + 1] & 0x0f) << 2]; + result += toBase64Table[64]; + } else if (lengthpad === 1) { + j = length - lengthpad; + result += toBase64Table[data[j] >> 2]; + result += toBase64Table[(data[j] & 0x03) << 4]; + result += toBase64Table[64]; + result += toBase64Table[64]; + } + + return result; + }, + + /* Convert Base64 data to a string */ + /* jshint -W013 */ + toBinaryTable : [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 + ], + /* jshint +W013 */ + + decode: function (data, offset) { + "use strict"; + offset = typeof(offset) !== 'undefined' ? offset : 0; + var toBinaryTable = Base64.toBinaryTable; + var base64Pad = Base64.base64Pad; + var result, result_length; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + var data_length = data.indexOf('=') - offset; + + if (data_length < 0) { data_length = data.length - offset; } + + /* Every four characters is 3 resulting numbers */ + result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5); + result = new Array(result_length); + + // Convert one by one. + for (var idx = 0, i = offset; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data.charAt(i) === base64Pad); + // Skip illegal characters and whitespace + if (c === -1) { + console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i); + continue; + } + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) { + result[idx++] = (leftdata >> leftbits) & 0xff; + } + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) { + err = new Error('Corrupted base64 string'); + err.name = 'Base64-Error'; + throw err; + } + + return result; + } +}; /* End of Base64 namespace */ diff --git a/plugins/dynamix.vm.manager/include/black.css b/plugins/dynamix.vm.manager/include/black.css new file mode 100644 index 000000000..7d940c5af --- /dev/null +++ b/plugins/dynamix.vm.manager/include/black.css @@ -0,0 +1,71 @@ +/* + * noVNC black CSS + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +#keyboardinput { + background-color:#000; +} + +.noVNC_status_normal { + background: #4c4c4c; /* Old browsers */ + background: -moz-linear-gradient(top, #4c4c4c 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} +.noVNC_status_error { + background: #f04040; /* Old browsers */ + background: -moz-linear-gradient(top, #f04040 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f04040), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} +.noVNC_status_warn { + background: #f0f040; /* Old browsers */ + background: -moz-linear-gradient(top, #f0f040 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f040), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} + +.triangle-right { + border:2px solid #fff; + background:#000; + color:#fff; +} + +.noVNC_status_button { + font-size: 12px; + vertical-align: middle; + border:1px solid #4c4c4c; + + background: #4c4c4c; /* Old browsers */ + background: -moz-linear-gradient(top, #4c4c4c 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4c4c4c', endColorstr='#131313',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} + +.noVNC_status_button_selected { + background: #9dd53a; /* Old browsers */ + background: -moz-linear-gradient(top, #9dd53a 0%, #a1d54f 50%, #80c217 51%, #7cbc0a 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#9dd53a), color-stop(50%,#a1d54f), color-stop(51%,#80c217), color-stop(100%,#7cbc0a)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#9dd53a', endColorstr='#7cbc0a',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* W3C */ +} diff --git a/plugins/dynamix.vm.manager/include/blue.css b/plugins/dynamix.vm.manager/include/blue.css new file mode 100644 index 000000000..b2a0adcc9 --- /dev/null +++ b/plugins/dynamix.vm.manager/include/blue.css @@ -0,0 +1,64 @@ +/* + * noVNC blue CSS + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +.noVNC_status_normal { + background-color:#04073d; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(10,15,79)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(10,15,79) 54%, + rgb(4,7,61) 50% + ); +} +.noVNC_status_error { + background-color:#f04040; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(240,64,64)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(4,7,61) 54%, + rgb(249,64,64) 50% + ); +} +.noVNC_status_warn { + background-color:#f0f040; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(240,240,64)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(4,7,61) 54%, + rgb(240,240,64) 50% + ); +} + +.triangle-right { + border:2px solid #fff; + background:#04073d; + color:#fff; +} + +#keyboardinput { + background-color:#04073d; +} + diff --git a/plugins/dynamix.vm.manager/include/chrome-app/tcp-client.js b/plugins/dynamix.vm.manager/include/chrome-app/tcp-client.js new file mode 100644 index 000000000..b8c125f5c --- /dev/null +++ b/plugins/dynamix.vm.manager/include/chrome-app/tcp-client.js @@ -0,0 +1,321 @@ +/* +Copyright 2012 Google Inc. + +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. + +Author: Boris Smus (smus@chromium.org) +*/ + +(function(exports) { + + // Define some local variables here. + var socket = chrome.socket || chrome.experimental.socket; + var dns = chrome.experimental.dns; + + /** + * Creates an instance of the client + * + * @param {String} host The remote host to connect to + * @param {Number} port The port to connect to at the remote host + */ + function TcpClient(host, port, pollInterval) { + this.host = host; + this.port = port; + this.pollInterval = pollInterval || 15; + + // Callback functions. + this.callbacks = { + connect: null, // Called when socket is connected. + disconnect: null, // Called when socket is disconnected. + recvBuffer: null, // Called (as ArrayBuffer) when client receives data from server. + recvString: null, // Called (as string) when client receives data from server. + sent: null // Called when client sends data to server. + }; + + // Socket. + this.socketId = null; + this.isConnected = false; + + log('initialized tcp client'); + } + + /** + * Connects to the TCP socket, and creates an open socket. + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-create + * @param {Function} callback The function to call on connection + */ + TcpClient.prototype.connect = function(callback) { + // First resolve the hostname to an IP. + dns.resolve(this.host, function(result) { + this.addr = result.address; + socket.create('tcp', {}, this._onCreate.bind(this)); + + // Register connect callback. + this.callbacks.connect = callback; + }.bind(this)); + }; + + /** + * Sends an arraybuffer/view down the wire to the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-write + * @param {String} msg The arraybuffer/view to send + * @param {Function} callback The function to call when the message has sent + */ + TcpClient.prototype.sendBuffer = function(buf, callback) { + if (buf.buffer) { + buf = buf.buffer; + } + + /* + // Debug + var bytes = [], u8 = new Uint8Array(buf); + for (var i = 0; i < u8.length; i++) { + bytes.push(u8[i]); + } + log("sending bytes: " + (bytes.join(','))); + */ + + socket.write(this.socketId, buf, this._onWriteComplete.bind(this)); + + // Register sent callback. + this.callbacks.sent = callback; + }; + + /** + * Sends a string down the wire to the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-write + * @param {String} msg The string to send + * @param {Function} callback The function to call when the message has sent + */ + TcpClient.prototype.sendString = function(msg, callback) { + /* + // Debug + log("sending string: " + msg); + */ + + this._stringToArrayBuffer(msg, function(arrayBuffer) { + socket.write(this.socketId, arrayBuffer, this._onWriteComplete.bind(this)); + }.bind(this)); + + // Register sent callback. + this.callbacks.sent = callback; + }; + + /** + * Sets the callback for when a message is received + * + * @param {Function} callback The function to call when a message has arrived + * @param {String} type The callback argument type: "arraybuffer" or "string" + */ + TcpClient.prototype.addResponseListener = function(callback, type) { + if (typeof type === "undefined") { + type = "arraybuffer"; + } + // Register received callback. + if (type === "string") { + this.callbacks.recvString = callback; + } else { + this.callbacks.recvBuffer = callback; + } + }; + + /** + * Sets the callback for when the socket disconnects + * + * @param {Function} callback The function to call when the socket disconnects + * @param {String} type The callback argument type: "arraybuffer" or "string" + */ + TcpClient.prototype.addDisconnectListener = function(callback) { + // Register disconnect callback. + this.callbacks.disconnect = callback; + }; + + /** + * Disconnects from the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-disconnect + */ + TcpClient.prototype.disconnect = function() { + if (this.isConnected) { + this.isConnected = false; + socket.disconnect(this.socketId); + if (this.callbacks.disconnect) { + this.callbacks.disconnect(); + } + log('socket disconnected'); + } + }; + + /** + * The callback function used for when we attempt to have Chrome + * create a socket. If the socket is successfully created + * we go ahead and connect to the remote side. + * + * @private + * @see http://developer.chrome.com/trunk/apps/socket.html#method-connect + * @param {Object} createInfo The socket details + */ + TcpClient.prototype._onCreate = function(createInfo) { + this.socketId = createInfo.socketId; + if (this.socketId > 0) { + socket.connect(this.socketId, this.addr, this.port, this._onConnectComplete.bind(this)); + } else { + error('Unable to create socket'); + } + }; + + /** + * The callback function used for when we attempt to have Chrome + * connect to the remote side. If a successful connection is + * made then polling starts to check for data to read + * + * @private + * @param {Number} resultCode Indicates whether the connection was successful + */ + TcpClient.prototype._onConnectComplete = function(resultCode) { + // Start polling for reads. + this.isConnected = true; + setTimeout(this._periodicallyRead.bind(this), this.pollInterval); + + if (this.callbacks.connect) { + log('connect complete'); + this.callbacks.connect(); + } + log('onConnectComplete'); + }; + + /** + * Checks for new data to read from the socket + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-read + */ + TcpClient.prototype._periodicallyRead = function() { + var that = this; + socket.getInfo(this.socketId, function (info) { + if (info.connected) { + setTimeout(that._periodicallyRead.bind(that), that.pollInterval); + socket.read(that.socketId, null, that._onDataRead.bind(that)); + } else if (that.isConnected) { + log('socket disconnect detected'); + that.disconnect(); + } + }); + }; + + /** + * Callback function for when data has been read from the socket. + * Converts the array buffer that is read in to a string + * and sends it on for further processing by passing it to + * the previously assigned callback function. + * + * @private + * @see TcpClient.prototype.addResponseListener + * @param {Object} readInfo The incoming message + */ + TcpClient.prototype._onDataRead = function(readInfo) { + // Call received callback if there's data in the response. + if (readInfo.resultCode > 0) { + log('onDataRead'); + + /* + // Debug + var bytes = [], u8 = new Uint8Array(readInfo.data); + for (var i = 0; i < u8.length; i++) { + bytes.push(u8[i]); + } + log("received bytes: " + (bytes.join(','))); + */ + + if (this.callbacks.recvBuffer) { + // Return raw ArrayBuffer directly. + this.callbacks.recvBuffer(readInfo.data); + } + if (this.callbacks.recvString) { + // Convert ArrayBuffer to string. + this._arrayBufferToString(readInfo.data, function(str) { + this.callbacks.recvString(str); + }.bind(this)); + } + + // Trigger another read right away + setTimeout(this._periodicallyRead.bind(this), 0); + } + }; + + /** + * Callback for when data has been successfully + * written to the socket. + * + * @private + * @param {Object} writeInfo The outgoing message + */ + TcpClient.prototype._onWriteComplete = function(writeInfo) { + log('onWriteComplete'); + // Call sent callback. + if (this.callbacks.sent) { + this.callbacks.sent(writeInfo); + } + }; + + /** + * Converts an array buffer to a string + * + * @private + * @param {ArrayBuffer} buf The buffer to convert + * @param {Function} callback The function to call when conversion is complete + */ + TcpClient.prototype._arrayBufferToString = function(buf, callback) { + var bb = new Blob([new Uint8Array(buf)]); + var f = new FileReader(); + f.onload = function(e) { + callback(e.target.result); + }; + f.readAsText(bb); + }; + + /** + * Converts a string to an array buffer + * + * @private + * @param {String} str The string to convert + * @param {Function} callback The function to call when conversion is complete + */ + TcpClient.prototype._stringToArrayBuffer = function(str, callback) { + var bb = new Blob([str]); + var f = new FileReader(); + f.onload = function(e) { + callback(e.target.result); + }; + f.readAsArrayBuffer(bb); + }; + + /** + * Wrapper function for logging + */ + function log(msg) { + console.log(msg); + } + + /** + * Wrapper function for error logging + */ + function error(msg) { + console.error(msg); + } + + exports.TcpClient = TcpClient; + +})(window); diff --git a/plugins/dynamix.vm.manager/include/des.js b/plugins/dynamix.vm.manager/include/des.js new file mode 100644 index 000000000..ecbc819ec --- /dev/null +++ b/plugins/dynamix.vm.manager/include/des.js @@ -0,0 +1,276 @@ +/* + * Ported from Flashlight VNC ActionScript implementation: + * http://www.wizhelp.com/flashlight-vnc/ + * + * Full attribution follows: + * + * ------------------------------------------------------------------------- + * + * This DES class has been extracted from package Acme.Crypto for use in VNC. + * The unnecessary odd parity code has been removed. + * + * These changes are: + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + + * DesCipher - the DES encryption method + * + * The meat of this code is by Dave Zimmerman , and is: + * + * Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved. + * + * Permission to use, copy, modify, and distribute this software + * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and + * without fee is hereby granted, provided that this copyright notice is kept + * intact. + * + * WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY + * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE + * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. + * + * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE + * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE + * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT + * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE + * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE + * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE + * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP + * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR + * HIGH RISK ACTIVITIES. + * + * + * The rest is: + * + * Copyright (C) 1996 by Jef Poskanzer . All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * Visit the ACME Labs Java page for up-to-date versions of this and other + * fine Java utilities: http://www.acme.com/java/ + */ + +/* jslint white: false */ + +function DES(passwd) { + "use strict"; + + // Tables, permutations, S-boxes, etc. + // jshint -W013 + var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, + 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, + 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], + totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28], + z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8, + keys = []; + + // jshint -W015 + a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; + SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, + z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, + a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, + c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; + a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; + SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, + a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, + z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, + z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; + a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; + SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, + b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, + c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, + b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; + a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; + SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, + z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, + b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, + c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; + a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; + SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, + a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, + z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, + c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; + a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; + SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, + z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, + b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, + a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; + a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; + SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, + b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, + b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, + z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; + a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; + SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, + c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, + a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, + z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; + // jshint +W013,+W015 + + // Set the key. + function setKeys(keyBlock) { + var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [], + raw0, raw1, rawi, KnLi; + + for (j = 0, l = 56; j < 56; ++j, l -= 8) { + l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1 + m = l & 0x7; + pc1m[j] = ((keyBlock[l >>> 3] & (1<>> 10; + keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; + ++KnLi; + keys[KnLi] = (raw0 & 0x0003f000) << 12; + keys[KnLi] |= (raw0 & 0x0000003f) << 16; + keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; + keys[KnLi] |= (raw1 & 0x0000003f); + ++KnLi; + } + } + + // Encrypt 8 bytes of text + function enc8(text) { + var i = 0, b = text.slice(), fval, keysi = 0, + l, r, x; // left, right, accumulator + + // Squash 8 bytes to 2 ints + l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + + x = ((l >>> 4) ^ r) & 0x0f0f0f0f; + r ^= x; + l ^= (x << 4); + x = ((l >>> 16) ^ r) & 0x0000ffff; + r ^= x; + l ^= (x << 16); + x = ((r >>> 2) ^ l) & 0x33333333; + l ^= x; + r ^= (x << 2); + x = ((r >>> 8) ^ l) & 0x00ff00ff; + l ^= x; + r ^= (x << 8); + r = (r << 1) | ((r >>> 31) & 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 1) | ((l >>> 31) & 1); + + for (i = 0; i < 8; ++i) { + x = (r << 28) | (r >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = r ^ keys[keysi++]; + fval |= SP8[x & 0x3f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + l ^= fval; + x = (l << 28) | (l >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = l ^ keys[keysi++]; + fval |= SP8[x & 0x0000003f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + r ^= fval; + } + + r = (r << 31) | (r >>> 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 31) | (l >>> 1); + x = ((l >>> 8) ^ r) & 0x00ff00ff; + r ^= x; + l ^= (x << 8); + x = ((l >>> 2) ^ r) & 0x33333333; + r ^= x; + l ^= (x << 2); + x = ((r >>> 16) ^ l) & 0x0000ffff; + l ^= x; + r ^= (x << 16); + x = ((r >>> 4) ^ l) & 0x0f0f0f0f; + l ^= x; + r ^= (x << 4); + + // Spread ints to bytes + x = [r, l]; + for (i = 0; i < 8; i++) { + b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256; + if (b[i] < 0) { b[i] += 256; } // unsigned + } + return b; + } + + // Encrypt 16 bytes of text using passwd as key + function encrypt(t) { + return enc8(t.slice(0, 8)).concat(enc8(t.slice(8, 16))); + } + + setKeys(passwd); // Setup keys + return {'encrypt': encrypt}; // Public interface + +} // function DES diff --git a/plugins/dynamix.vm.manager/include/display.js b/plugins/dynamix.vm.manager/include/display.js new file mode 100644 index 000000000..6bf89bd60 --- /dev/null +++ b/plugins/dynamix.vm.manager/include/display.js @@ -0,0 +1,898 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2015 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/*jslint browser: true, white: false */ +/*global Util, Base64, changeCursor */ + +var Display; + +(function () { + "use strict"; + + var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false; + try { + new ImageData(new Uint8ClampedArray(1), 1, 1); + SUPPORTS_IMAGEDATA_CONSTRUCTOR = true; + } catch (ex) { + // ignore failure + } + + Display = function (defaults) { + this._drawCtx = null; + this._c_forceCanvas = false; + + this._renderQ = []; // queue drawing actions for in-oder rendering + + // the full frame buffer (logical canvas) size + this._fb_width = 0; + this._fb_height = 0; + + // the size limit of the viewport (start disabled) + this._maxWidth = 0; + this._maxHeight = 0; + + // the visible "physical canvas" viewport + this._viewportLoc = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 }; + this._cleanRect = { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 }; + + this._prevDrawStyle = ""; + this._tile = null; + this._tile16x16 = null; + this._tile_x = 0; + this._tile_y = 0; + + Util.set_defaults(this, defaults, { + 'true_color': true, + 'colourMap': [], + 'scale': 1.0, + 'viewport': false, + 'render_mode': '' + }); + + Util.Debug(">> Display.constructor"); + + if (!this._target) { + throw new Error("Target must be set"); + } + + if (typeof this._target === 'string') { + throw new Error('target must be a DOM element'); + } + + if (!this._target.getContext) { + throw new Error("no getContext method"); + } + + if (!this._drawCtx) { + this._drawCtx = this._target.getContext('2d'); + } + + Util.Debug("User Agent: " + navigator.userAgent); + if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); } + if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); } + if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); } + if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); } + + this.clear(); + + // Check canvas features + if ('createImageData' in this._drawCtx) { + this._render_mode = 'canvas rendering'; + } else { + throw new Error("Canvas does not support createImageData"); + } + + if (this._prefer_js === null) { + Util.Info("Prefering javascript operations"); + this._prefer_js = true; + } + + // Determine browser support for setting the cursor via data URI scheme + if (this._cursor_uri || this._cursor_uri === null || + this._cursor_uri === undefined) { + this._cursor_uri = Util.browserSupportsCursorURIs(); + } + + Util.Debug("<< Display.constructor"); + }; + + Display.prototype = { + // Public methods + viewportChangePos: function (deltaX, deltaY) { + var vp = this._viewportLoc; + deltaX = Math.floor(deltaX); + deltaY = Math.floor(deltaY); + + if (!this._viewport) { + deltaX = -vp.w; // clamped later of out of bounds + deltaY = -vp.h; + } + + var vx2 = vp.x + vp.w - 1; + var vy2 = vp.y + vp.h - 1; + + // Position change + + if (deltaX < 0 && vp.x + deltaX < 0) { + deltaX = -vp.x; + } + if (vx2 + deltaX >= this._fb_width) { + deltaX -= vx2 + deltaX - this._fb_width + 1; + } + + if (vp.y + deltaY < 0) { + deltaY = -vp.y; + } + if (vy2 + deltaY >= this._fb_height) { + deltaY -= (vy2 + deltaY - this._fb_height + 1); + } + + if (deltaX === 0 && deltaY === 0) { + return; + } + Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); + + vp.x += deltaX; + vx2 += deltaX; + vp.y += deltaY; + vy2 += deltaY; + + // Update the clean rectangle + var cr = this._cleanRect; + if (vp.x > cr.x1) { + cr.x1 = vp.x; + } + if (vx2 < cr.x2) { + cr.x2 = vx2; + } + if (vp.y > cr.y1) { + cr.y1 = vp.y; + } + if (vy2 < cr.y2) { + cr.y2 = vy2; + } + + var x1, w; + if (deltaX < 0) { + // Shift viewport left, redraw left section + x1 = 0; + w = -deltaX; + } else { + // Shift viewport right, redraw right section + x1 = vp.w - deltaX; + w = deltaX; + } + + var y1, h; + if (deltaY < 0) { + // Shift viewport up, redraw top section + y1 = 0; + h = -deltaY; + } else { + // Shift viewport down, redraw bottom section + y1 = vp.h - deltaY; + h = deltaY; + } + + var saveStyle = this._drawCtx.fillStyle; + var canvas = this._target; + this._drawCtx.fillStyle = "rgb(255,255,255)"; + + // Due to this bug among others [1] we need to disable the image-smoothing to + // avoid getting a blur effect when panning. + // + // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719 + // + // We need to set these every time since all properties are reset + // when the the size is changed + if (this._drawCtx.mozImageSmoothingEnabled) { + this._drawCtx.mozImageSmoothingEnabled = false; + } else if (this._drawCtx.webkitImageSmoothingEnabled) { + this._drawCtx.webkitImageSmoothingEnabled = false; + } else if (this._drawCtx.msImageSmoothingEnabled) { + this._drawCtx.msImageSmoothingEnabled = false; + } else if (this._drawCtx.imageSmoothingEnabled) { + this._drawCtx.imageSmoothingEnabled = false; + } + + // Copy the valid part of the viewport to the shifted location + this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, -deltaY, vp.w, vp.h); + + if (deltaX !== 0) { + this._drawCtx.fillRect(x1, 0, w, vp.h); + } + if (deltaY !== 0) { + this._drawCtx.fillRect(0, y1, vp.w, h); + } + this._drawCtx.fillStyle = saveStyle; + }, + + viewportChangeSize: function(width, height) { + + if (typeof(width) === "undefined" || typeof(height) === "undefined") { + + Util.Debug("Setting viewport to full display region"); + width = this._fb_width; + height = this._fb_height; + } + + var vp = this._viewportLoc; + if (vp.w !== width || vp.h !== height) { + + if (this._viewport) { + if (this._maxWidth !== 0 && width > this._maxWidth) { + width = this._maxWidth; + } + if (this._maxHeight !== 0 && height > this._maxHeight) { + height = this._maxHeight; + } + } + + var cr = this._cleanRect; + + if (width < vp.w && cr.x2 > vp.x + width - 1) { + cr.x2 = vp.x + width - 1; + } + if (height < vp.h && cr.y2 > vp.y + height - 1) { + cr.y2 = vp.y + height - 1; + } + + vp.w = width; + vp.h = height; + + var canvas = this._target; + if (canvas.width !== width || canvas.height !== height) { + + // We have to save the canvas data since changing the size will clear it + var saveImg = null; + if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { + var img_width = canvas.width < vp.w ? canvas.width : vp.w; + var img_height = canvas.height < vp.h ? canvas.height : vp.h; + saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); + } + + if (canvas.width !== width) { + canvas.width = width; + canvas.style.width = width + 'px'; + } + if (canvas.height !== height) { + canvas.height = height; + canvas.style.height = height + 'px'; + } + + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); + } + } + } + }, + + // Return a map of clean and dirty areas of the viewport and reset the + // tracking of clean and dirty areas + // + // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h}, + // 'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] } + getCleanDirtyReset: function () { + var vp = this._viewportLoc; + var cr = this._cleanRect; + + var cleanBox = { 'x': cr.x1, 'y': cr.y1, + 'w': cr.x2 - cr.x1 + 1, 'h': cr.y2 - cr.y1 + 1 }; + + var dirtyBoxes = []; + if (cr.x1 >= cr.x2 || cr.y1 >= cr.y2) { + // Whole viewport is dirty + dirtyBoxes.push({ 'x': vp.x, 'y': vp.y, 'w': vp.w, 'h': vp.h }); + } else { + // Redraw dirty regions + var vx2 = vp.x + vp.w - 1; + var vy2 = vp.y + vp.h - 1; + + if (vp.x < cr.x1) { + // left side dirty region + dirtyBoxes.push({'x': vp.x, 'y': vp.y, + 'w': cr.x1 - vp.x + 1, 'h': vp.h}); + } + if (vx2 > cr.x2) { + // right side dirty region + dirtyBoxes.push({'x': cr.x2 + 1, 'y': vp.y, + 'w': vx2 - cr.x2, 'h': vp.h}); + } + if(vp.y < cr.y1) { + // top/middle dirty region + dirtyBoxes.push({'x': cr.x1, 'y': vp.y, + 'w': cr.x2 - cr.x1 + 1, 'h': cr.y1 - vp.y}); + } + if (vy2 > cr.y2) { + // bottom/middle dirty region + dirtyBoxes.push({'x': cr.x1, 'y': cr.y2 + 1, + 'w': cr.x2 - cr.x1 + 1, 'h': vy2 - cr.y2}); + } + } + + this._cleanRect = {'x1': vp.x, 'y1': vp.y, + 'x2': vp.x + vp.w - 1, 'y2': vp.y + vp.h - 1}; + + return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes}; + }, + + absX: function (x) { + return x + this._viewportLoc.x; + }, + + absY: function (y) { + return y + this._viewportLoc.y; + }, + + resize: function (width, height) { + this._prevDrawStyle = ""; + + this._fb_width = width; + this._fb_height = height; + + this._rescale(this._scale); + + this.viewportChangeSize(); + }, + + clear: function () { + if (this._logo) { + this.resize(this._logo.width, this._logo.height); + this.blitStringImage(this._logo.data, 0, 0); + } else { + if (Util.Engine.trident === 6) { + // NB(directxman12): there's a bug in IE10 where we can fail to actually + // clear the canvas here because of the resize. + // Clearing the current viewport first fixes the issue + this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); + } + this.resize(240, 20); + this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); + } + + this._renderQ = []; + }, + + fillRect: function (x, y, width, height, color, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this.renderQ_push({ + 'type': 'fill', + 'x': x, + 'y': y, + 'width': width, + 'height': height, + 'color': color + }); + } else { + this._setFillColor(color); + this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height); + } + }, + + copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this.renderQ_push({ + 'type': 'copy', + 'old_x': old_x, + 'old_y': old_y, + 'x': new_x, + 'y': new_y, + 'width': w, + 'height': h, + }); + } else { + var x1 = old_x - this._viewportLoc.x; + var y1 = old_y - this._viewportLoc.y; + var x2 = new_x - this._viewportLoc.x; + var y2 = new_y - this._viewportLoc.y; + + this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h); + } + }, + + // start updating a tile + startTile: function (x, y, width, height, color) { + this._tile_x = x; + this._tile_y = y; + if (width === 16 && height === 16) { + this._tile = this._tile16x16; + } else { + this._tile = this._drawCtx.createImageData(width, height); + } + + if (this._prefer_js) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color[0]]; + } + var red = bgr[2]; + var green = bgr[1]; + var blue = bgr[0]; + + var data = this._tile.data; + for (var i = 0; i < width * height * 4; i += 4) { + data[i] = red; + data[i + 1] = green; + data[i + 2] = blue; + data[i + 3] = 255; + } + } else { + this.fillRect(x, y, width, height, color, true); + } + }, + + // update sub-rectangle of the current tile + subTile: function (x, y, w, h, color) { + if (this._prefer_js) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color[0]]; + } + var red = bgr[2]; + var green = bgr[1]; + var blue = bgr[0]; + var xend = x + w; + var yend = y + h; + + var data = this._tile.data; + var width = this._tile.width; + for (var j = y; j < yend; j++) { + for (var i = x; i < xend; i++) { + var p = (i + (j * width)) * 4; + data[p] = red; + data[p + 1] = green; + data[p + 2] = blue; + data[p + 3] = 255; + } + } + } else { + this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color, true); + } + }, + + // draw the current tile to the screen + finishTile: function () { + if (this._prefer_js) { + this._drawCtx.putImageData(this._tile, this._tile_x - this._viewportLoc.x, + this._tile_y - this._viewportLoc.y); + } + // else: No-op -- already done by setSubTile + }, + + blitImage: function (x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this.renderQ_push({ + 'type': 'blit', + 'data': arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else if (this._true_color) { + this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } else { + this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } + }, + + blitRgbImage: function (x, y , width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this.renderQ_push({ + 'type': 'blitRgb', + 'data': arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else if (this._true_color) { + this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } else { + // probably wrong? + this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } + }, + + blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, but it + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + var new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this.renderQ_push({ + 'type': 'blitRgbx', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else { + this._rgbxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } + }, + + blitStringImage: function (str, x, y) { + var img = new Image(); + img.onload = function () { + this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); + }.bind(this); + img.src = str; + return img; // for debugging purposes + }, + + // wrap ctx.drawImage but relative to viewport + drawImage: function (img, x, y) { + this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); + }, + + renderQ_push: function (action) { + this._renderQ.push(action); + if (this._renderQ.length === 1) { + // If this can be rendered immediately it will be, otherwise + // the scanner will start polling the queue (every + // requestAnimationFrame interval) + this._scan_renderQ(); + } + }, + + changeCursor: function (pixels, mask, hotx, hoty, w, h) { + if (this._cursor_uri === false) { + Util.Warn("changeCursor called but no cursor data URI support"); + return; + } + + if (this._true_color) { + Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h); + } else { + Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap); + } + }, + + defaultCursor: function () { + this._target.style.cursor = "default"; + }, + + disableLocalCursor: function () { + this._target.style.cursor = "none"; + }, + + clippingDisplay: function () { + var vp = this._viewportLoc; + + var fbClip = this._fb_width > vp.w || this._fb_height > vp.h; + var limitedVp = this._maxWidth !== 0 && this._maxHeight !== 0; + var clipping = false; + + if (limitedVp) { + clipping = vp.w > this._maxWidth || vp.h > this._maxHeight; + } + + return fbClip || (limitedVp && clipping); + }, + + // Overridden getters/setters + get_context: function () { + return this._drawCtx; + }, + + set_scale: function (scale) { + this._rescale(scale); + }, + + set_width: function (w) { + this._fb_width = w; + }, + get_width: function () { + return this._fb_width; + }, + + set_height: function (h) { + this._fb_height = h; + }, + get_height: function () { + return this._fb_height; + }, + + autoscale: function (containerWidth, containerHeight, downscaleOnly) { + var targetAspectRatio = containerWidth / containerHeight; + var fbAspectRatio = this._fb_width / this._fb_height; + + var scaleRatio; + if (fbAspectRatio >= targetAspectRatio) { + scaleRatio = containerWidth / this._fb_width; + } else { + scaleRatio = containerHeight / this._fb_height; + } + + var targetW, targetH; + if (scaleRatio > 1.0 && downscaleOnly) { + targetW = this._fb_width; + targetH = this._fb_height; + scaleRatio = 1.0; + } else if (fbAspectRatio >= targetAspectRatio) { + targetW = containerWidth; + targetH = Math.round(containerWidth / fbAspectRatio); + } else { + targetW = Math.round(containerHeight * fbAspectRatio); + targetH = containerHeight; + } + + // NB(directxman12): If you set the width directly, or set the + // style width to a number, the canvas is cleared. + // However, if you set the style width to a string + // ('NNNpx'), the canvas is scaled without clearing. + this._target.style.width = targetW + 'px'; + this._target.style.height = targetH + 'px'; + + this._scale = scaleRatio; + + return scaleRatio; // so that the mouse, etc scale can be set + }, + + // Private Methods + _rescale: function (factor) { + this._scale = factor; + + var w; + var h; + + if (this._viewport && + this._maxWidth !== 0 && this._maxHeight !== 0) { + w = Math.min(this._fb_width, this._maxWidth); + h = Math.min(this._fb_height, this._maxHeight); + } else { + w = this._fb_width; + h = this._fb_height; + } + + this._target.style.width = Math.round(factor * w) + 'px'; + this._target.style.height = Math.round(factor * h) + 'px'; + }, + + _setFillColor: function (color) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color]; + } + + var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')'; + if (newStyle !== this._prevDrawStyle) { + this._drawCtx.fillStyle = newStyle; + this._prevDrawStyle = newStyle; + } + }, + + _rgbImageData: function (x, y, vx, vy, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { + data[i] = arr[j]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j + 2]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _bgrxImageData: function (x, y, vx, vy, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) { + data[i] = arr[j + 2]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _rgbxImageData: function (x, y, vx, vy, width, height, arr, offset) { + // NB(directxman12): arr must be an Type Array view + var img; + if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { + img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); + } else { + img = this._drawCtx.createImageData(width, height); + img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _cmapImageData: function (x, y, vx, vy, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + var cmap = this._colourMap; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) { + var bgr = cmap[arr[j]]; + data[i] = bgr[2]; + data[i + 1] = bgr[1]; + data[i + 2] = bgr[0]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _scan_renderQ: function () { + var ready = true; + while (ready && this._renderQ.length > 0) { + var a = this._renderQ[0]; + switch (a.type) { + case 'copy': + this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true); + break; + case 'fill': + this.fillRect(a.x, a.y, a.width, a.height, a.color, true); + break; + case 'blit': + this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'blitRgb': + this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'blitRgbx': + this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'img': + if (a.img.complete) { + this.drawImage(a.img, a.x, a.y); + } else { + // We need to wait for this image to 'load' + // to keep things in-order + ready = false; + } + break; + } + + if (ready) { + this._renderQ.shift(); + } + } + + if (this._renderQ.length > 0) { + requestAnimFrame(this._scan_renderQ.bind(this)); + } + }, + }; + + Util.make_properties(Display, [ + ['target', 'wo', 'dom'], // Canvas element for rendering + ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only) + ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "data": data} + ['true_color', 'rw', 'bool'], // Use true-color pixel data + ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color) + ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0 + ['viewport', 'rw', 'bool'], // Use viewport clipping + ['width', 'rw', 'int'], // Display area width + ['height', 'rw', 'int'], // Display area height + ['maxWidth', 'rw', 'int'], // Viewport max width (0 if disabled) + ['maxHeight', 'rw', 'int'], // Viewport max height (0 if disabled) + + ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only) + + ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods + ['cursor_uri', 'rw', 'raw'] // Can we render cursor using data URI + ]); + + // Class Methods + Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) { + var w = w0; + var h = h0; + if (h < w) { + h = w; // increase h to make it square + } else { + w = h; // increase w to make it square + } + + var cur = []; + + // Push multi-byte little-endian values + cur.push16le = function (num) { + this.push(num & 0xFF, (num >> 8) & 0xFF); + }; + cur.push32le = function (num) { + this.push(num & 0xFF, + (num >> 8) & 0xFF, + (num >> 16) & 0xFF, + (num >> 24) & 0xFF); + }; + + var IHDRsz = 40; + var RGBsz = w * h * 4; + var XORsz = Math.ceil((w * h) / 8.0); + var ANDsz = Math.ceil((w * h) / 8.0); + + cur.push16le(0); // 0: Reserved + cur.push16le(2); // 2: .CUR type + cur.push16le(1); // 4: Number of images, 1 for non-animated ico + + // Cursor #1 header (ICONDIRENTRY) + cur.push(w); // 6: width + cur.push(h); // 7: height + cur.push(0); // 8: colors, 0 -> true-color + cur.push(0); // 9: reserved + cur.push16le(hotx); // 10: hotspot x coordinate + cur.push16le(hoty); // 12: hotspot y coordinate + cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); + // 14: cursor data byte size + cur.push32le(22); // 18: offset of cursor data in the file + + // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) + cur.push32le(IHDRsz); // 22: InfoHeader size + cur.push32le(w); // 26: Cursor width + cur.push32le(h * 2); // 30: XOR+AND height + cur.push16le(1); // 34: number of planes + cur.push16le(32); // 36: bits per pixel + cur.push32le(0); // 38: Type of compression + + cur.push32le(XORsz + ANDsz); + // 42: Size of Image + cur.push32le(0); // 46: reserved + cur.push32le(0); // 50: reserved + cur.push32le(0); // 54: reserved + cur.push32le(0); // 58: reserved + + // 62: color data (RGBQUAD icColors[]) + var y, x; + for (y = h - 1; y >= 0; y--) { + for (x = 0; x < w; x++) { + if (x >= w0 || y >= h0) { + cur.push(0); // blue + cur.push(0); // green + cur.push(0); // red + cur.push(0); // alpha + } else { + var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8); + var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; + if (cmap) { + idx = (w0 * y) + x; + var rgb = cmap[pixels[idx]]; + cur.push(rgb[2]); // blue + cur.push(rgb[1]); // green + cur.push(rgb[0]); // red + cur.push(alpha); // alpha + } else { + idx = ((w0 * y) + x) * 4; + cur.push(pixels[idx + 2]); // blue + cur.push(pixels[idx + 1]); // green + cur.push(pixels[idx]); // red + cur.push(alpha); // alpha + } + } + } + } + + // XOR/bitmask data (BYTE icXOR[]) + // (ignored, just needs to be the right size) + for (y = 0; y < h; y++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0); + } + } + + // AND/bitmask data (BYTE icAND[]) + // (ignored, just needs to be the right size) + for (y = 0; y < h; y++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0); + } + } + + var url = 'data:image/x-icon;base64,' + Base64.encode(cur); + target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; + }; +})(); diff --git a/plugins/dynamix.vm.manager/include/inflator.js b/plugins/dynamix.vm.manager/include/inflator.js new file mode 100644 index 000000000..3a708dd2f --- /dev/null +++ b/plugins/dynamix.vm.manager/include/inflator.js @@ -0,0 +1,2409 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.inflator = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o>> 16) & 0xffff) |0, + n = 0; + + while (len !== 0) { + // Set limit ~ twice less than 5552, to keep + // s2 in 31-bits, because we force signed ints. + // in other case %= will fail. + n = len > 2000 ? 2000 : len; + len -= n; + + do { + s1 = (s1 + buf[pos++]) |0; + s2 = (s2 + s1) |0; + } while (--n); + + s1 %= 65521; + s2 %= 65521; + } + + return (s1 | (s2 << 16)) |0; +} + + +module.exports = adler32; + +},{}],3:[function(require,module,exports){ +'use strict'; + +// Note: we can't get significant speed boost here. +// So write code to minimize size - no pregenerated tables +// and array tools dependencies. + + +// Use ordinary array, since untyped makes no boost here +function makeTable() { + var c, table = []; + + for (var n =0; n < 256; n++) { + c = n; + for (var k =0; k < 8; k++) { + c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); + } + table[n] = c; + } + + return table; +} + +// Create table on load. Just 255 signed longs. Not a problem. +var crcTable = makeTable(); + + +function crc32(crc, buf, len, pos) { + var t = crcTable, + end = pos + len; + + crc = crc ^ (-1); + + for (var i = pos; i < end; i++) { + crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF]; + } + + return (crc ^ (-1)); // >>> 0; +} + + +module.exports = crc32; + +},{}],4:[function(require,module,exports){ +'use strict'; + +// See state defs from inflate.js +var BAD = 30; /* got a data error -- remain here until reset */ +var TYPE = 12; /* i: waiting for type bits, including last-flag bit */ + +/* + Decode literal, length, and distance codes and write out the resulting + literal and match bytes until either not enough input or output is + available, an end-of-block is encountered, or a data error is encountered. + When large enough input and output buffers are supplied to inflate(), for + example, a 16K input buffer and a 64K output buffer, more than 95% of the + inflate execution time is spent in this routine. + + Entry assumptions: + + state.mode === LEN + strm.avail_in >= 6 + strm.avail_out >= 258 + start >= strm.avail_out + state.bits < 8 + + On return, state.mode is one of: + + LEN -- ran out of enough output space or enough available input + TYPE -- reached end of block code, inflate() to interpret next block + BAD -- error in block data + + Notes: + + - The maximum input bits used by a length/distance pair is 15 bits for the + length code, 5 bits for the length extra, 15 bits for the distance code, + and 13 bits for the distance extra. This totals 48 bits, or six bytes. + Therefore if strm.avail_in >= 6, then there is enough input to avoid + checking for available input while decoding. + + - The maximum bytes that a single length/distance pair can output is 258 + bytes, which is the maximum length that can be coded. inflate_fast() + requires strm.avail_out >= 258 for each loop to avoid checking for + output space. + */ +module.exports = function inflate_fast(strm, start) { + var state; + var _in; /* local strm.input */ + var last; /* have enough input while in < last */ + var _out; /* local strm.output */ + var beg; /* inflate()'s initial strm.output */ + var end; /* while out < end, enough space available */ +//#ifdef INFLATE_STRICT + var dmax; /* maximum distance from zlib header */ +//#endif + var wsize; /* window size or zero if not using window */ + var whave; /* valid bytes in the window */ + var wnext; /* window write index */ + var window; /* allocated sliding window, if wsize != 0 */ + var hold; /* local strm.hold */ + var bits; /* local strm.bits */ + var lcode; /* local strm.lencode */ + var dcode; /* local strm.distcode */ + var lmask; /* mask for first level of length codes */ + var dmask; /* mask for first level of distance codes */ + var here; /* retrieved table entry */ + var op; /* code bits, operation, extra bits, or */ + /* window position, window bytes to copy */ + var len; /* match length, unused bytes */ + var dist; /* match distance */ + var from; /* where to copy match from */ + var from_source; + + + var input, output; // JS specific, because we have no pointers + + /* copy state to local variables */ + state = strm.state; + //here = state.here; + _in = strm.next_in; + input = strm.input; + last = _in + (strm.avail_in - 5); + _out = strm.next_out; + output = strm.output; + beg = _out - (start - strm.avail_out); + end = _out + (strm.avail_out - 257); +//#ifdef INFLATE_STRICT + dmax = state.dmax; +//#endif + wsize = state.wsize; + whave = state.whave; + wnext = state.wnext; + window = state.window; + hold = state.hold; + bits = state.bits; + lcode = state.lencode; + dcode = state.distcode; + lmask = (1 << state.lenbits) - 1; + dmask = (1 << state.distbits) - 1; + + + /* decode literals and length/distances until end-of-block or not enough + input data or output space */ + + top: + do { + if (bits < 15) { + hold += input[_in++] << bits; + bits += 8; + hold += input[_in++] << bits; + bits += 8; + } + + here = lcode[hold & lmask]; + + dolen: + for (;;) { // Goto emulation + op = here >>> 24/*here.bits*/; + hold >>>= op; + bits -= op; + op = (here >>> 16) & 0xff/*here.op*/; + if (op === 0) { /* literal */ + //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ? + // "inflate: literal '%c'\n" : + // "inflate: literal 0x%02x\n", here.val)); + output[_out++] = here & 0xffff/*here.val*/; + } + else if (op & 16) { /* length base */ + len = here & 0xffff/*here.val*/; + op &= 15; /* number of extra bits */ + if (op) { + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + } + len += hold & ((1 << op) - 1); + hold >>>= op; + bits -= op; + } + //Tracevv((stderr, "inflate: length %u\n", len)); + if (bits < 15) { + hold += input[_in++] << bits; + bits += 8; + hold += input[_in++] << bits; + bits += 8; + } + here = dcode[hold & dmask]; + + dodist: + for (;;) { // goto emulation + op = here >>> 24/*here.bits*/; + hold >>>= op; + bits -= op; + op = (here >>> 16) & 0xff/*here.op*/; + + if (op & 16) { /* distance base */ + dist = here & 0xffff/*here.val*/; + op &= 15; /* number of extra bits */ + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + } + } + dist += hold & ((1 << op) - 1); +//#ifdef INFLATE_STRICT + if (dist > dmax) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break top; + } +//#endif + hold >>>= op; + bits -= op; + //Tracevv((stderr, "inflate: distance %u\n", dist)); + op = _out - beg; /* max distance in output */ + if (dist > op) { /* see if copy from window */ + op = dist - op; /* distance back in window */ + if (op > whave) { + if (state.sane) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break top; + } + +// (!) This block is disabled in zlib defailts, +// don't enable it for binary compatibility +//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR +// if (len <= op - whave) { +// do { +// output[_out++] = 0; +// } while (--len); +// continue top; +// } +// len -= op - whave; +// do { +// output[_out++] = 0; +// } while (--op > whave); +// if (op === 0) { +// from = _out - dist; +// do { +// output[_out++] = output[from++]; +// } while (--len); +// continue top; +// } +//#endif + } + from = 0; // window index + from_source = window; + if (wnext === 0) { /* very common case */ + from += wsize - op; + if (op < len) { /* some from window */ + len -= op; + do { + output[_out++] = window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + else if (wnext < op) { /* wrap around window */ + from += wsize + wnext - op; + op -= wnext; + if (op < len) { /* some from end of window */ + len -= op; + do { + output[_out++] = window[from++]; + } while (--op); + from = 0; + if (wnext < len) { /* some from start of window */ + op = wnext; + len -= op; + do { + output[_out++] = window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + } + else { /* contiguous in window */ + from += wnext - op; + if (op < len) { /* some from window */ + len -= op; + do { + output[_out++] = window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + while (len > 2) { + output[_out++] = from_source[from++]; + output[_out++] = from_source[from++]; + output[_out++] = from_source[from++]; + len -= 3; + } + if (len) { + output[_out++] = from_source[from++]; + if (len > 1) { + output[_out++] = from_source[from++]; + } + } + } + else { + from = _out - dist; /* copy direct from output */ + do { /* minimum length is three */ + output[_out++] = output[from++]; + output[_out++] = output[from++]; + output[_out++] = output[from++]; + len -= 3; + } while (len > 2); + if (len) { + output[_out++] = output[from++]; + if (len > 1) { + output[_out++] = output[from++]; + } + } + } + } + else if ((op & 64) === 0) { /* 2nd level distance code */ + here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))]; + continue dodist; + } + else { + strm.msg = 'invalid distance code'; + state.mode = BAD; + break top; + } + + break; // need to emulate goto via "continue" + } + } + else if ((op & 64) === 0) { /* 2nd level length code */ + here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))]; + continue dolen; + } + else if (op & 32) { /* end-of-block */ + //Tracevv((stderr, "inflate: end of block\n")); + state.mode = TYPE; + break top; + } + else { + strm.msg = 'invalid literal/length code'; + state.mode = BAD; + break top; + } + + break; // need to emulate goto via "continue" + } + } while (_in < last && _out < end); + + /* return unused bytes (on entry, bits < 8, so in won't go too far back) */ + len = bits >> 3; + _in -= len; + bits -= len << 3; + hold &= (1 << bits) - 1; + + /* update state and return */ + strm.next_in = _in; + strm.next_out = _out; + strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last)); + strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end)); + state.hold = hold; + state.bits = bits; + return; +}; + +},{}],5:[function(require,module,exports){ +'use strict'; + + +var utils = require('../utils/common'); +var adler32 = require('./adler32'); +var crc32 = require('./crc32'); +var inflate_fast = require('./inffast'); +var inflate_table = require('./inftrees'); + +var CODES = 0; +var LENS = 1; +var DISTS = 2; + +/* Public constants ==========================================================*/ +/* ===========================================================================*/ + + +/* Allowed flush values; see deflate() and inflate() below for details */ +//var Z_NO_FLUSH = 0; +//var Z_PARTIAL_FLUSH = 1; +//var Z_SYNC_FLUSH = 2; +//var Z_FULL_FLUSH = 3; +var Z_FINISH = 4; +var Z_BLOCK = 5; +var Z_TREES = 6; + + +/* Return codes for the compression/decompression functions. Negative values + * are errors, positive values are used for special but normal events. + */ +var Z_OK = 0; +var Z_STREAM_END = 1; +var Z_NEED_DICT = 2; +//var Z_ERRNO = -1; +var Z_STREAM_ERROR = -2; +var Z_DATA_ERROR = -3; +var Z_MEM_ERROR = -4; +var Z_BUF_ERROR = -5; +//var Z_VERSION_ERROR = -6; + +/* The deflate compression method */ +var Z_DEFLATED = 8; + + +/* STATES ====================================================================*/ +/* ===========================================================================*/ + + +var HEAD = 1; /* i: waiting for magic header */ +var FLAGS = 2; /* i: waiting for method and flags (gzip) */ +var TIME = 3; /* i: waiting for modification time (gzip) */ +var OS = 4; /* i: waiting for extra flags and operating system (gzip) */ +var EXLEN = 5; /* i: waiting for extra length (gzip) */ +var EXTRA = 6; /* i: waiting for extra bytes (gzip) */ +var NAME = 7; /* i: waiting for end of file name (gzip) */ +var COMMENT = 8; /* i: waiting for end of comment (gzip) */ +var HCRC = 9; /* i: waiting for header crc (gzip) */ +var DICTID = 10; /* i: waiting for dictionary check value */ +var DICT = 11; /* waiting for inflateSetDictionary() call */ +var TYPE = 12; /* i: waiting for type bits, including last-flag bit */ +var TYPEDO = 13; /* i: same, but skip check to exit inflate on new block */ +var STORED = 14; /* i: waiting for stored size (length and complement) */ +var COPY_ = 15; /* i/o: same as COPY below, but only first time in */ +var COPY = 16; /* i/o: waiting for input or output to copy stored block */ +var TABLE = 17; /* i: waiting for dynamic block table lengths */ +var LENLENS = 18; /* i: waiting for code length code lengths */ +var CODELENS = 19; /* i: waiting for length/lit and distance code lengths */ +var LEN_ = 20; /* i: same as LEN below, but only first time in */ +var LEN = 21; /* i: waiting for length/lit/eob code */ +var LENEXT = 22; /* i: waiting for length extra bits */ +var DIST = 23; /* i: waiting for distance code */ +var DISTEXT = 24; /* i: waiting for distance extra bits */ +var MATCH = 25; /* o: waiting for output space to copy string */ +var LIT = 26; /* o: waiting for output space to write literal */ +var CHECK = 27; /* i: waiting for 32-bit check value */ +var LENGTH = 28; /* i: waiting for 32-bit length (gzip) */ +var DONE = 29; /* finished check, done -- remain here until reset */ +var BAD = 30; /* got a data error -- remain here until reset */ +var MEM = 31; /* got an inflate() memory error -- remain here until reset */ +var SYNC = 32; /* looking for synchronization bytes to restart inflate() */ + +/* ===========================================================================*/ + + + +var ENOUGH_LENS = 852; +var ENOUGH_DISTS = 592; +//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS); + +var MAX_WBITS = 15; +/* 32K LZ77 window */ +var DEF_WBITS = MAX_WBITS; + + +function ZSWAP32(q) { + return (((q >>> 24) & 0xff) + + ((q >>> 8) & 0xff00) + + ((q & 0xff00) << 8) + + ((q & 0xff) << 24)); +} + + +function InflateState() { + this.mode = 0; /* current inflate mode */ + this.last = false; /* true if processing last block */ + this.wrap = 0; /* bit 0 true for zlib, bit 1 true for gzip */ + this.havedict = false; /* true if dictionary provided */ + this.flags = 0; /* gzip header method and flags (0 if zlib) */ + this.dmax = 0; /* zlib header max distance (INFLATE_STRICT) */ + this.check = 0; /* protected copy of check value */ + this.total = 0; /* protected copy of output count */ + // TODO: may be {} + this.head = null; /* where to save gzip header information */ + + /* sliding window */ + this.wbits = 0; /* log base 2 of requested window size */ + this.wsize = 0; /* window size or zero if not using window */ + this.whave = 0; /* valid bytes in the window */ + this.wnext = 0; /* window write index */ + this.window = null; /* allocated sliding window, if needed */ + + /* bit accumulator */ + this.hold = 0; /* input bit accumulator */ + this.bits = 0; /* number of bits in "in" */ + + /* for string and stored block copying */ + this.length = 0; /* literal or length of data to copy */ + this.offset = 0; /* distance back to copy string from */ + + /* for table and code decoding */ + this.extra = 0; /* extra bits needed */ + + /* fixed and dynamic code tables */ + this.lencode = null; /* starting table for length/literal codes */ + this.distcode = null; /* starting table for distance codes */ + this.lenbits = 0; /* index bits for lencode */ + this.distbits = 0; /* index bits for distcode */ + + /* dynamic table building */ + this.ncode = 0; /* number of code length code lengths */ + this.nlen = 0; /* number of length code lengths */ + this.ndist = 0; /* number of distance code lengths */ + this.have = 0; /* number of code lengths in lens[] */ + this.next = null; /* next available space in codes[] */ + + this.lens = new utils.Buf16(320); /* temporary storage for code lengths */ + this.work = new utils.Buf16(288); /* work area for code table building */ + + /* + because we don't have pointers in js, we use lencode and distcode directly + as buffers so we don't need codes + */ + //this.codes = new utils.Buf32(ENOUGH); /* space for code tables */ + this.lendyn = null; /* dynamic table for length/literal codes (JS specific) */ + this.distdyn = null; /* dynamic table for distance codes (JS specific) */ + this.sane = 0; /* if false, allow invalid distance too far */ + this.back = 0; /* bits back of last unprocessed length/lit */ + this.was = 0; /* initial length of match */ +} + +function inflateResetKeep(strm) { + var state; + + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + strm.total_in = strm.total_out = state.total = 0; + strm.msg = ''; /*Z_NULL*/ + if (state.wrap) { /* to support ill-conceived Java test suite */ + strm.adler = state.wrap & 1; + } + state.mode = HEAD; + state.last = 0; + state.havedict = 0; + state.dmax = 32768; + state.head = null/*Z_NULL*/; + state.hold = 0; + state.bits = 0; + //state.lencode = state.distcode = state.next = state.codes; + state.lencode = state.lendyn = new utils.Buf32(ENOUGH_LENS); + state.distcode = state.distdyn = new utils.Buf32(ENOUGH_DISTS); + + state.sane = 1; + state.back = -1; + //Tracev((stderr, "inflate: reset\n")); + return Z_OK; +} + +function inflateReset(strm) { + var state; + + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + state.wsize = 0; + state.whave = 0; + state.wnext = 0; + return inflateResetKeep(strm); + +} + +function inflateReset2(strm, windowBits) { + var wrap; + var state; + + /* get the state */ + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + + /* extract wrap request from windowBits parameter */ + if (windowBits < 0) { + wrap = 0; + windowBits = -windowBits; + } + else { + wrap = (windowBits >> 4) + 1; + if (windowBits < 48) { + windowBits &= 15; + } + } + + /* set number of window bits, free window if different */ + if (windowBits && (windowBits < 8 || windowBits > 15)) { + return Z_STREAM_ERROR; + } + if (state.window !== null && state.wbits !== windowBits) { + state.window = null; + } + + /* update state and reset the rest of it */ + state.wrap = wrap; + state.wbits = windowBits; + return inflateReset(strm); +} + +function inflateInit2(strm, windowBits) { + var ret; + var state; + + if (!strm) { return Z_STREAM_ERROR; } + //strm.msg = Z_NULL; /* in case we return an error */ + + state = new InflateState(); + + //if (state === Z_NULL) return Z_MEM_ERROR; + //Tracev((stderr, "inflate: allocated\n")); + strm.state = state; + state.window = null/*Z_NULL*/; + ret = inflateReset2(strm, windowBits); + if (ret !== Z_OK) { + strm.state = null/*Z_NULL*/; + } + return ret; +} + +function inflateInit(strm) { + return inflateInit2(strm, DEF_WBITS); +} + + +/* + Return state with length and distance decoding tables and index sizes set to + fixed code decoding. Normally this returns fixed tables from inffixed.h. + If BUILDFIXED is defined, then instead this routine builds the tables the + first time it's called, and returns those tables the first time and + thereafter. This reduces the size of the code by about 2K bytes, in + exchange for a little execution time. However, BUILDFIXED should not be + used for threaded applications, since the rewriting of the tables and virgin + may not be thread-safe. + */ +var virgin = true; + +var lenfix, distfix; // We have no pointers in JS, so keep tables separate + +function fixedtables(state) { + /* build fixed huffman tables if first call (may not be thread safe) */ + if (virgin) { + var sym; + + lenfix = new utils.Buf32(512); + distfix = new utils.Buf32(32); + + /* literal/length table */ + sym = 0; + while (sym < 144) { state.lens[sym++] = 8; } + while (sym < 256) { state.lens[sym++] = 9; } + while (sym < 280) { state.lens[sym++] = 7; } + while (sym < 288) { state.lens[sym++] = 8; } + + inflate_table(LENS, state.lens, 0, 288, lenfix, 0, state.work, {bits: 9}); + + /* distance table */ + sym = 0; + while (sym < 32) { state.lens[sym++] = 5; } + + inflate_table(DISTS, state.lens, 0, 32, distfix, 0, state.work, {bits: 5}); + + /* do this just once */ + virgin = false; + } + + state.lencode = lenfix; + state.lenbits = 9; + state.distcode = distfix; + state.distbits = 5; +} + + +/* + Update the window with the last wsize (normally 32K) bytes written before + returning. If window does not exist yet, create it. This is only called + when a window is already in use, or when output has been written during this + inflate call, but the end of the deflate stream has not been reached yet. + It is also called to create a window for dictionary data when a dictionary + is loaded. + + Providing output buffers larger than 32K to inflate() should provide a speed + advantage, since only the last 32K of output is copied to the sliding window + upon return from inflate(), and since all distances after the first 32K of + output will fall in the output data, making match copies simpler and faster. + The advantage may be dependent on the size of the processor's data caches. + */ +function updatewindow(strm, src, end, copy) { + var dist; + var state = strm.state; + + /* if it hasn't been done already, allocate space for the window */ + if (state.window === null) { + state.wsize = 1 << state.wbits; + state.wnext = 0; + state.whave = 0; + + state.window = new utils.Buf8(state.wsize); + } + + /* copy state->wsize or less output bytes into the circular window */ + if (copy >= state.wsize) { + utils.arraySet(state.window,src, end - state.wsize, state.wsize, 0); + state.wnext = 0; + state.whave = state.wsize; + } + else { + dist = state.wsize - state.wnext; + if (dist > copy) { + dist = copy; + } + //zmemcpy(state->window + state->wnext, end - copy, dist); + utils.arraySet(state.window,src, end - copy, dist, state.wnext); + copy -= dist; + if (copy) { + //zmemcpy(state->window, end - copy, copy); + utils.arraySet(state.window,src, end - copy, copy, 0); + state.wnext = copy; + state.whave = state.wsize; + } + else { + state.wnext += dist; + if (state.wnext === state.wsize) { state.wnext = 0; } + if (state.whave < state.wsize) { state.whave += dist; } + } + } + return 0; +} + +function inflate(strm, flush) { + var state; + var input, output; // input/output buffers + var next; /* next input INDEX */ + var put; /* next output INDEX */ + var have, left; /* available input and output */ + var hold; /* bit buffer */ + var bits; /* bits in bit buffer */ + var _in, _out; /* save starting available input and output */ + var copy; /* number of stored or match bytes to copy */ + var from; /* where to copy match bytes from */ + var from_source; + var here = 0; /* current decoding table entry */ + var here_bits, here_op, here_val; // paked "here" denormalized (JS specific) + //var last; /* parent table entry */ + var last_bits, last_op, last_val; // paked "last" denormalized (JS specific) + var len; /* length to copy for repeats, bits to drop */ + var ret; /* return code */ + var hbuf = new utils.Buf8(4); /* buffer for gzip header crc calculation */ + var opts; + + var n; // temporary var for NEED_BITS + + var order = /* permutation of code lengths */ + [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; + + + if (!strm || !strm.state || !strm.output || + (!strm.input && strm.avail_in !== 0)) { + return Z_STREAM_ERROR; + } + + state = strm.state; + if (state.mode === TYPE) { state.mode = TYPEDO; } /* skip check */ + + + //--- LOAD() --- + put = strm.next_out; + output = strm.output; + left = strm.avail_out; + next = strm.next_in; + input = strm.input; + have = strm.avail_in; + hold = state.hold; + bits = state.bits; + //--- + + _in = have; + _out = left; + ret = Z_OK; + + inf_leave: // goto emulation + for (;;) { + switch (state.mode) { + case HEAD: + if (state.wrap === 0) { + state.mode = TYPEDO; + break; + } + //=== NEEDBITS(16); + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if ((state.wrap & 2) && hold === 0x8b1f) { /* gzip header */ + state.check = 0/*crc32(0L, Z_NULL, 0)*/; + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = FLAGS; + break; + } + state.flags = 0; /* expect zlib header */ + if (state.head) { + state.head.done = false; + } + if (!(state.wrap & 1) || /* check if zlib header allowed */ + (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) { + strm.msg = 'incorrect header check'; + state.mode = BAD; + break; + } + if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED) { + strm.msg = 'unknown compression method'; + state.mode = BAD; + break; + } + //--- DROPBITS(4) ---// + hold >>>= 4; + bits -= 4; + //---// + len = (hold & 0x0f)/*BITS(4)*/ + 8; + if (state.wbits === 0) { + state.wbits = len; + } + else if (len > state.wbits) { + strm.msg = 'invalid window size'; + state.mode = BAD; + break; + } + state.dmax = 1 << len; + //Tracev((stderr, "inflate: zlib header ok\n")); + strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/; + state.mode = hold & 0x200 ? DICTID : TYPE; + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + break; + case FLAGS: + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.flags = hold; + if ((state.flags & 0xff) !== Z_DEFLATED) { + strm.msg = 'unknown compression method'; + state.mode = BAD; + break; + } + if (state.flags & 0xe000) { + strm.msg = 'unknown header flags set'; + state.mode = BAD; + break; + } + if (state.head) { + state.head.text = ((hold >> 8) & 1); + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = TIME; + /* falls through */ + case TIME: + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (state.head) { + state.head.time = hold; + } + if (state.flags & 0x0200) { + //=== CRC4(state.check, hold) + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + hbuf[2] = (hold >>> 16) & 0xff; + hbuf[3] = (hold >>> 24) & 0xff; + state.check = crc32(state.check, hbuf, 4, 0); + //=== + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = OS; + /* falls through */ + case OS: + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (state.head) { + state.head.xflags = (hold & 0xff); + state.head.os = (hold >> 8); + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = EXLEN; + /* falls through */ + case EXLEN: + if (state.flags & 0x0400) { + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.length = hold; + if (state.head) { + state.head.extra_len = hold; + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + } + else if (state.head) { + state.head.extra = null/*Z_NULL*/; + } + state.mode = EXTRA; + /* falls through */ + case EXTRA: + if (state.flags & 0x0400) { + copy = state.length; + if (copy > have) { copy = have; } + if (copy) { + if (state.head) { + len = state.head.extra_len - state.length; + if (!state.head.extra) { + // Use untyped array for more conveniend processing later + state.head.extra = new Array(state.head.extra_len); + } + utils.arraySet( + state.head.extra, + input, + next, + // extra field is limited to 65536 bytes + // - no need for additional size check + copy, + /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/ + len + ); + //zmemcpy(state.head.extra + len, next, + // len + copy > state.head.extra_max ? + // state.head.extra_max - len : copy); + } + if (state.flags & 0x0200) { + state.check = crc32(state.check, input, copy, next); + } + have -= copy; + next += copy; + state.length -= copy; + } + if (state.length) { break inf_leave; } + } + state.length = 0; + state.mode = NAME; + /* falls through */ + case NAME: + if (state.flags & 0x0800) { + if (have === 0) { break inf_leave; } + copy = 0; + do { + // TODO: 2 or 1 bytes? + len = input[next + copy++]; + /* use constant limit because in js we should not preallocate memory */ + if (state.head && len && + (state.length < 65536 /*state.head.name_max*/)) { + state.head.name += String.fromCharCode(len); + } + } while (len && copy < have); + + if (state.flags & 0x0200) { + state.check = crc32(state.check, input, copy, next); + } + have -= copy; + next += copy; + if (len) { break inf_leave; } + } + else if (state.head) { + state.head.name = null; + } + state.length = 0; + state.mode = COMMENT; + /* falls through */ + case COMMENT: + if (state.flags & 0x1000) { + if (have === 0) { break inf_leave; } + copy = 0; + do { + len = input[next + copy++]; + /* use constant limit because in js we should not preallocate memory */ + if (state.head && len && + (state.length < 65536 /*state.head.comm_max*/)) { + state.head.comment += String.fromCharCode(len); + } + } while (len && copy < have); + if (state.flags & 0x0200) { + state.check = crc32(state.check, input, copy, next); + } + have -= copy; + next += copy; + if (len) { break inf_leave; } + } + else if (state.head) { + state.head.comment = null; + } + state.mode = HCRC; + /* falls through */ + case HCRC: + if (state.flags & 0x0200) { + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (hold !== (state.check & 0xffff)) { + strm.msg = 'header crc mismatch'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + } + if (state.head) { + state.head.hcrc = ((state.flags >> 9) & 1); + state.head.done = true; + } + strm.adler = state.check = 0 /*crc32(0L, Z_NULL, 0)*/; + state.mode = TYPE; + break; + case DICTID: + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + strm.adler = state.check = ZSWAP32(hold); + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = DICT; + /* falls through */ + case DICT: + if (state.havedict === 0) { + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + return Z_NEED_DICT; + } + strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/; + state.mode = TYPE; + /* falls through */ + case TYPE: + if (flush === Z_BLOCK || flush === Z_TREES) { break inf_leave; } + /* falls through */ + case TYPEDO: + if (state.last) { + //--- BYTEBITS() ---// + hold >>>= bits & 7; + bits -= bits & 7; + //---// + state.mode = CHECK; + break; + } + //=== NEEDBITS(3); */ + while (bits < 3) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.last = (hold & 0x01)/*BITS(1)*/; + //--- DROPBITS(1) ---// + hold >>>= 1; + bits -= 1; + //---// + + switch ((hold & 0x03)/*BITS(2)*/) { + case 0: /* stored block */ + //Tracev((stderr, "inflate: stored block%s\n", + // state.last ? " (last)" : "")); + state.mode = STORED; + break; + case 1: /* fixed block */ + fixedtables(state); + //Tracev((stderr, "inflate: fixed codes block%s\n", + // state.last ? " (last)" : "")); + state.mode = LEN_; /* decode codes */ + if (flush === Z_TREES) { + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + break inf_leave; + } + break; + case 2: /* dynamic block */ + //Tracev((stderr, "inflate: dynamic codes block%s\n", + // state.last ? " (last)" : "")); + state.mode = TABLE; + break; + case 3: + strm.msg = 'invalid block type'; + state.mode = BAD; + } + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + break; + case STORED: + //--- BYTEBITS() ---// /* go to byte boundary */ + hold >>>= bits & 7; + bits -= bits & 7; + //---// + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) { + strm.msg = 'invalid stored block lengths'; + state.mode = BAD; + break; + } + state.length = hold & 0xffff; + //Tracev((stderr, "inflate: stored length %u\n", + // state.length)); + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = COPY_; + if (flush === Z_TREES) { break inf_leave; } + /* falls through */ + case COPY_: + state.mode = COPY; + /* falls through */ + case COPY: + copy = state.length; + if (copy) { + if (copy > have) { copy = have; } + if (copy > left) { copy = left; } + if (copy === 0) { break inf_leave; } + //--- zmemcpy(put, next, copy); --- + utils.arraySet(output, input, next, copy, put); + //---// + have -= copy; + next += copy; + left -= copy; + put += copy; + state.length -= copy; + break; + } + //Tracev((stderr, "inflate: stored end\n")); + state.mode = TYPE; + break; + case TABLE: + //=== NEEDBITS(14); */ + while (bits < 14) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257; + //--- DROPBITS(5) ---// + hold >>>= 5; + bits -= 5; + //---// + state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1; + //--- DROPBITS(5) ---// + hold >>>= 5; + bits -= 5; + //---// + state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4; + //--- DROPBITS(4) ---// + hold >>>= 4; + bits -= 4; + //---// +//#ifndef PKZIP_BUG_WORKAROUND + if (state.nlen > 286 || state.ndist > 30) { + strm.msg = 'too many length or distance symbols'; + state.mode = BAD; + break; + } +//#endif + //Tracev((stderr, "inflate: table sizes ok\n")); + state.have = 0; + state.mode = LENLENS; + /* falls through */ + case LENLENS: + while (state.have < state.ncode) { + //=== NEEDBITS(3); + while (bits < 3) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.lens[order[state.have++]] = (hold & 0x07);//BITS(3); + //--- DROPBITS(3) ---// + hold >>>= 3; + bits -= 3; + //---// + } + while (state.have < 19) { + state.lens[order[state.have++]] = 0; + } + // We have separate tables & no pointers. 2 commented lines below not needed. + //state.next = state.codes; + //state.lencode = state.next; + // Switch to use dynamic table + state.lencode = state.lendyn; + state.lenbits = 7; + + opts = {bits: state.lenbits}; + ret = inflate_table(CODES, state.lens, 0, 19, state.lencode, 0, state.work, opts); + state.lenbits = opts.bits; + + if (ret) { + strm.msg = 'invalid code lengths set'; + state.mode = BAD; + break; + } + //Tracev((stderr, "inflate: code lengths ok\n")); + state.have = 0; + state.mode = CODELENS; + /* falls through */ + case CODELENS: + while (state.have < state.nlen + state.ndist) { + for (;;) { + here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if (here_val < 16) { + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.lens[state.have++] = here_val; + } + else { + if (here_val === 16) { + //=== NEEDBITS(here.bits + 2); + n = here_bits + 2; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + if (state.have === 0) { + strm.msg = 'invalid bit length repeat'; + state.mode = BAD; + break; + } + len = state.lens[state.have - 1]; + copy = 3 + (hold & 0x03);//BITS(2); + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + } + else if (here_val === 17) { + //=== NEEDBITS(here.bits + 3); + n = here_bits + 3; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + len = 0; + copy = 3 + (hold & 0x07);//BITS(3); + //--- DROPBITS(3) ---// + hold >>>= 3; + bits -= 3; + //---// + } + else { + //=== NEEDBITS(here.bits + 7); + n = here_bits + 7; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + len = 0; + copy = 11 + (hold & 0x7f);//BITS(7); + //--- DROPBITS(7) ---// + hold >>>= 7; + bits -= 7; + //---// + } + if (state.have + copy > state.nlen + state.ndist) { + strm.msg = 'invalid bit length repeat'; + state.mode = BAD; + break; + } + while (copy--) { + state.lens[state.have++] = len; + } + } + } + + /* handle error breaks in while */ + if (state.mode === BAD) { break; } + + /* check for end-of-block code (better have one) */ + if (state.lens[256] === 0) { + strm.msg = 'invalid code -- missing end-of-block'; + state.mode = BAD; + break; + } + + /* build code tables -- note: do not change the lenbits or distbits + values here (9 and 6) without reading the comments in inftrees.h + concerning the ENOUGH constants, which depend on those values */ + state.lenbits = 9; + + opts = {bits: state.lenbits}; + ret = inflate_table(LENS, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts); + // We have separate tables & no pointers. 2 commented lines below not needed. + // state.next_index = opts.table_index; + state.lenbits = opts.bits; + // state.lencode = state.next; + + if (ret) { + strm.msg = 'invalid literal/lengths set'; + state.mode = BAD; + break; + } + + state.distbits = 6; + //state.distcode.copy(state.codes); + // Switch to use dynamic table + state.distcode = state.distdyn; + opts = {bits: state.distbits}; + ret = inflate_table(DISTS, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts); + // We have separate tables & no pointers. 2 commented lines below not needed. + // state.next_index = opts.table_index; + state.distbits = opts.bits; + // state.distcode = state.next; + + if (ret) { + strm.msg = 'invalid distances set'; + state.mode = BAD; + break; + } + //Tracev((stderr, 'inflate: codes ok\n')); + state.mode = LEN_; + if (flush === Z_TREES) { break inf_leave; } + /* falls through */ + case LEN_: + state.mode = LEN; + /* falls through */ + case LEN: + if (have >= 6 && left >= 258) { + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + inflate_fast(strm, _out); + //--- LOAD() --- + put = strm.next_out; + output = strm.output; + left = strm.avail_out; + next = strm.next_in; + input = strm.input; + have = strm.avail_in; + hold = state.hold; + bits = state.bits; + //--- + + if (state.mode === TYPE) { + state.back = -1; + } + break; + } + state.back = 0; + for (;;) { + here = state.lencode[hold & ((1 << state.lenbits) -1)]; /*BITS(state.lenbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if (here_bits <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if (here_op && (here_op & 0xf0) === 0) { + last_bits = here_bits; + last_op = here_op; + last_val = here_val; + for (;;) { + here = state.lencode[last_val + + ((hold & ((1 << (last_bits + last_op)) -1))/*BITS(last.bits + last.op)*/ >> last_bits)]; + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((last_bits + here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + //--- DROPBITS(last.bits) ---// + hold >>>= last_bits; + bits -= last_bits; + //---// + state.back += last_bits; + } + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.back += here_bits; + state.length = here_val; + if (here_op === 0) { + //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ? + // "inflate: literal '%c'\n" : + // "inflate: literal 0x%02x\n", here.val)); + state.mode = LIT; + break; + } + if (here_op & 32) { + //Tracevv((stderr, "inflate: end of block\n")); + state.back = -1; + state.mode = TYPE; + break; + } + if (here_op & 64) { + strm.msg = 'invalid literal/length code'; + state.mode = BAD; + break; + } + state.extra = here_op & 15; + state.mode = LENEXT; + /* falls through */ + case LENEXT: + if (state.extra) { + //=== NEEDBITS(state.extra); + n = state.extra; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.length += hold & ((1 << state.extra) -1)/*BITS(state.extra)*/; + //--- DROPBITS(state.extra) ---// + hold >>>= state.extra; + bits -= state.extra; + //---// + state.back += state.extra; + } + //Tracevv((stderr, "inflate: length %u\n", state.length)); + state.was = state.length; + state.mode = DIST; + /* falls through */ + case DIST: + for (;;) { + here = state.distcode[hold & ((1 << state.distbits) -1)];/*BITS(state.distbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if ((here_op & 0xf0) === 0) { + last_bits = here_bits; + last_op = here_op; + last_val = here_val; + for (;;) { + here = state.distcode[last_val + + ((hold & ((1 << (last_bits + last_op)) -1))/*BITS(last.bits + last.op)*/ >> last_bits)]; + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((last_bits + here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + //--- DROPBITS(last.bits) ---// + hold >>>= last_bits; + bits -= last_bits; + //---// + state.back += last_bits; + } + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.back += here_bits; + if (here_op & 64) { + strm.msg = 'invalid distance code'; + state.mode = BAD; + break; + } + state.offset = here_val; + state.extra = (here_op) & 15; + state.mode = DISTEXT; + /* falls through */ + case DISTEXT: + if (state.extra) { + //=== NEEDBITS(state.extra); + n = state.extra; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.offset += hold & ((1 << state.extra) -1)/*BITS(state.extra)*/; + //--- DROPBITS(state.extra) ---// + hold >>>= state.extra; + bits -= state.extra; + //---// + state.back += state.extra; + } +//#ifdef INFLATE_STRICT + if (state.offset > state.dmax) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break; + } +//#endif + //Tracevv((stderr, "inflate: distance %u\n", state.offset)); + state.mode = MATCH; + /* falls through */ + case MATCH: + if (left === 0) { break inf_leave; } + copy = _out - left; + if (state.offset > copy) { /* copy from window */ + copy = state.offset - copy; + if (copy > state.whave) { + if (state.sane) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break; + } +// (!) This block is disabled in zlib defailts, +// don't enable it for binary compatibility +//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR +// Trace((stderr, "inflate.c too far\n")); +// copy -= state.whave; +// if (copy > state.length) { copy = state.length; } +// if (copy > left) { copy = left; } +// left -= copy; +// state.length -= copy; +// do { +// output[put++] = 0; +// } while (--copy); +// if (state.length === 0) { state.mode = LEN; } +// break; +//#endif + } + if (copy > state.wnext) { + copy -= state.wnext; + from = state.wsize - copy; + } + else { + from = state.wnext - copy; + } + if (copy > state.length) { copy = state.length; } + from_source = state.window; + } + else { /* copy from output */ + from_source = output; + from = put - state.offset; + copy = state.length; + } + if (copy > left) { copy = left; } + left -= copy; + state.length -= copy; + do { + output[put++] = from_source[from++]; + } while (--copy); + if (state.length === 0) { state.mode = LEN; } + break; + case LIT: + if (left === 0) { break inf_leave; } + output[put++] = state.length; + left--; + state.mode = LEN; + break; + case CHECK: + if (state.wrap) { + //=== NEEDBITS(32); + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + // Use '|' insdead of '+' to make sure that result is signed + hold |= input[next++] << bits; + bits += 8; + } + //===// + _out -= left; + strm.total_out += _out; + state.total += _out; + if (_out) { + strm.adler = state.check = + /*UPDATE(state.check, put - _out, _out);*/ + (state.flags ? crc32(state.check, output, _out, put - _out) : adler32(state.check, output, _out, put - _out)); + + } + _out = left; + // NB: crc32 stored as signed 32-bit int, ZSWAP32 returns signed too + if ((state.flags ? hold : ZSWAP32(hold)) !== state.check) { + strm.msg = 'incorrect data check'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + //Tracev((stderr, "inflate: check matches trailer\n")); + } + state.mode = LENGTH; + /* falls through */ + case LENGTH: + if (state.wrap && state.flags) { + //=== NEEDBITS(32); + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (hold !== (state.total & 0xffffffff)) { + strm.msg = 'incorrect length check'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + //Tracev((stderr, "inflate: length matches trailer\n")); + } + state.mode = DONE; + /* falls through */ + case DONE: + ret = Z_STREAM_END; + break inf_leave; + case BAD: + ret = Z_DATA_ERROR; + break inf_leave; + case MEM: + return Z_MEM_ERROR; + case SYNC: + /* falls through */ + default: + return Z_STREAM_ERROR; + } + } + + // inf_leave <- here is real place for "goto inf_leave", emulated via "break inf_leave" + + /* + Return from inflate(), updating the total counts and the check value. + If there was no progress during the inflate() call, return a buffer + error. Call updatewindow() to create and/or update the window state. + Note: a memory error from inflate() is non-recoverable. + */ + + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + + if (state.wsize || (_out !== strm.avail_out && state.mode < BAD && + (state.mode < CHECK || flush !== Z_FINISH))) { + if (updatewindow(strm, strm.output, strm.next_out, _out - strm.avail_out)) { + state.mode = MEM; + return Z_MEM_ERROR; + } + } + _in -= strm.avail_in; + _out -= strm.avail_out; + strm.total_in += _in; + strm.total_out += _out; + state.total += _out; + if (state.wrap && _out) { + strm.adler = state.check = /*UPDATE(state.check, strm.next_out - _out, _out);*/ + (state.flags ? crc32(state.check, output, _out, strm.next_out - _out) : adler32(state.check, output, _out, strm.next_out - _out)); + } + strm.data_type = state.bits + (state.last ? 64 : 0) + + (state.mode === TYPE ? 128 : 0) + + (state.mode === LEN_ || state.mode === COPY_ ? 256 : 0); + if (((_in === 0 && _out === 0) || flush === Z_FINISH) && ret === Z_OK) { + ret = Z_BUF_ERROR; + } + return ret; +} + +function inflateEnd(strm) { + + if (!strm || !strm.state /*|| strm->zfree == (free_func)0*/) { + return Z_STREAM_ERROR; + } + + var state = strm.state; + if (state.window) { + state.window = null; + } + strm.state = null; + return Z_OK; +} + +function inflateGetHeader(strm, head) { + var state; + + /* check state */ + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR; } + + /* save header structure */ + state.head = head; + head.done = false; + return Z_OK; +} + + +exports.inflateReset = inflateReset; +exports.inflateReset2 = inflateReset2; +exports.inflateResetKeep = inflateResetKeep; +exports.inflateInit = inflateInit; +exports.inflateInit2 = inflateInit2; +exports.inflate = inflate; +exports.inflateEnd = inflateEnd; +exports.inflateGetHeader = inflateGetHeader; +exports.inflateInfo = 'pako inflate (from Nodeca project)'; + +/* Not implemented +exports.inflateCopy = inflateCopy; +exports.inflateGetDictionary = inflateGetDictionary; +exports.inflateMark = inflateMark; +exports.inflatePrime = inflatePrime; +exports.inflateSetDictionary = inflateSetDictionary; +exports.inflateSync = inflateSync; +exports.inflateSyncPoint = inflateSyncPoint; +exports.inflateUndermine = inflateUndermine; +*/ + +},{"../utils/common":1,"./adler32":2,"./crc32":3,"./inffast":4,"./inftrees":6}],6:[function(require,module,exports){ +'use strict'; + + +var utils = require('../utils/common'); + +var MAXBITS = 15; +var ENOUGH_LENS = 852; +var ENOUGH_DISTS = 592; +//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS); + +var CODES = 0; +var LENS = 1; +var DISTS = 2; + +var lbase = [ /* Length codes 257..285 base */ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 +]; + +var lext = [ /* Length codes 257..285 extra */ + 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, + 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78 +]; + +var dbase = [ /* Distance codes 0..29 base */ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577, 0, 0 +]; + +var dext = [ /* Distance codes 0..29 extra */ + 16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, + 28, 28, 29, 29, 64, 64 +]; + +module.exports = function inflate_table(type, lens, lens_index, codes, table, table_index, work, opts) +{ + var bits = opts.bits; + //here = opts.here; /* table entry for duplication */ + + var len = 0; /* a code's length in bits */ + var sym = 0; /* index of code symbols */ + var min = 0, max = 0; /* minimum and maximum code lengths */ + var root = 0; /* number of index bits for root table */ + var curr = 0; /* number of index bits for current table */ + var drop = 0; /* code bits to drop for sub-table */ + var left = 0; /* number of prefix codes available */ + var used = 0; /* code entries in table used */ + var huff = 0; /* Huffman code */ + var incr; /* for incrementing code, index */ + var fill; /* index for replicating entries */ + var low; /* low bits for current root entry */ + var mask; /* mask for low root bits */ + var next; /* next available space in table */ + var base = null; /* base value table to use */ + var base_index = 0; +// var shoextra; /* extra bits table to use */ + var end; /* use base and extra for symbol > end */ + var count = new utils.Buf16(MAXBITS+1); //[MAXBITS+1]; /* number of codes of each length */ + var offs = new utils.Buf16(MAXBITS+1); //[MAXBITS+1]; /* offsets in table for each length */ + var extra = null; + var extra_index = 0; + + var here_bits, here_op, here_val; + + /* + Process a set of code lengths to create a canonical Huffman code. The + code lengths are lens[0..codes-1]. Each length corresponds to the + symbols 0..codes-1. The Huffman code is generated by first sorting the + symbols by length from short to long, and retaining the symbol order + for codes with equal lengths. Then the code starts with all zero bits + for the first code of the shortest length, and the codes are integer + increments for the same length, and zeros are appended as the length + increases. For the deflate format, these bits are stored backwards + from their more natural integer increment ordering, and so when the + decoding tables are built in the large loop below, the integer codes + are incremented backwards. + + This routine assumes, but does not check, that all of the entries in + lens[] are in the range 0..MAXBITS. The caller must assure this. + 1..MAXBITS is interpreted as that code length. zero means that that + symbol does not occur in this code. + + The codes are sorted by computing a count of codes for each length, + creating from that a table of starting indices for each length in the + sorted table, and then entering the symbols in order in the sorted + table. The sorted table is work[], with that space being provided by + the caller. + + The length counts are used for other purposes as well, i.e. finding + the minimum and maximum length codes, determining if there are any + codes at all, checking for a valid set of lengths, and looking ahead + at length counts to determine sub-table sizes when building the + decoding tables. + */ + + /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */ + for (len = 0; len <= MAXBITS; len++) { + count[len] = 0; + } + for (sym = 0; sym < codes; sym++) { + count[lens[lens_index + sym]]++; + } + + /* bound code lengths, force root to be within code lengths */ + root = bits; + for (max = MAXBITS; max >= 1; max--) { + if (count[max] !== 0) { break; } + } + if (root > max) { + root = max; + } + if (max === 0) { /* no symbols to code at all */ + //table.op[opts.table_index] = 64; //here.op = (var char)64; /* invalid code marker */ + //table.bits[opts.table_index] = 1; //here.bits = (var char)1; + //table.val[opts.table_index++] = 0; //here.val = (var short)0; + table[table_index++] = (1 << 24) | (64 << 16) | 0; + + + //table.op[opts.table_index] = 64; + //table.bits[opts.table_index] = 1; + //table.val[opts.table_index++] = 0; + table[table_index++] = (1 << 24) | (64 << 16) | 0; + + opts.bits = 1; + return 0; /* no symbols, but wait for decoding to report error */ + } + for (min = 1; min < max; min++) { + if (count[min] !== 0) { break; } + } + if (root < min) { + root = min; + } + + /* check for an over-subscribed or incomplete set of lengths */ + left = 1; + for (len = 1; len <= MAXBITS; len++) { + left <<= 1; + left -= count[len]; + if (left < 0) { + return -1; + } /* over-subscribed */ + } + if (left > 0 && (type === CODES || max !== 1)) { + return -1; /* incomplete set */ + } + + /* generate offsets into symbol table for each length for sorting */ + offs[1] = 0; + for (len = 1; len < MAXBITS; len++) { + offs[len + 1] = offs[len] + count[len]; + } + + /* sort symbols by length, by symbol order within each length */ + for (sym = 0; sym < codes; sym++) { + if (lens[lens_index + sym] !== 0) { + work[offs[lens[lens_index + sym]]++] = sym; + } + } + + /* + Create and fill in decoding tables. In this loop, the table being + filled is at next and has curr index bits. The code being used is huff + with length len. That code is converted to an index by dropping drop + bits off of the bottom. For codes where len is less than drop + curr, + those top drop + curr - len bits are incremented through all values to + fill the table with replicated entries. + + root is the number of index bits for the root table. When len exceeds + root, sub-tables are created pointed to by the root entry with an index + of the low root bits of huff. This is saved in low to check for when a + new sub-table should be started. drop is zero when the root table is + being filled, and drop is root when sub-tables are being filled. + + When a new sub-table is needed, it is necessary to look ahead in the + code lengths to determine what size sub-table is needed. The length + counts are used for this, and so count[] is decremented as codes are + entered in the tables. + + used keeps track of how many table entries have been allocated from the + provided *table space. It is checked for LENS and DIST tables against + the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in + the initial root table size constants. See the comments in inftrees.h + for more information. + + sym increments through all symbols, and the loop terminates when + all codes of length max, i.e. all codes, have been processed. This + routine permits incomplete codes, so another loop after this one fills + in the rest of the decoding tables with invalid code markers. + */ + + /* set up for code type */ + // poor man optimization - use if-else instead of switch, + // to avoid deopts in old v8 + if (type === CODES) { + base = extra = work; /* dummy value--not used */ + end = 19; + + } else if (type === LENS) { + base = lbase; + base_index -= 257; + extra = lext; + extra_index -= 257; + end = 256; + + } else { /* DISTS */ + base = dbase; + extra = dext; + end = -1; + } + + /* initialize opts for loop */ + huff = 0; /* starting code */ + sym = 0; /* starting code symbol */ + len = min; /* starting code length */ + next = table_index; /* current table to fill in */ + curr = root; /* current table index bits */ + drop = 0; /* current bits to drop from code for index */ + low = -1; /* trigger new sub-table when len > root */ + used = 1 << root; /* use root table entries */ + mask = used - 1; /* mask for comparing low */ + + /* check available table space */ + if ((type === LENS && used > ENOUGH_LENS) || + (type === DISTS && used > ENOUGH_DISTS)) { + return 1; + } + + var i=0; + /* process all codes and make table entries */ + for (;;) { + i++; + /* create table entry */ + here_bits = len - drop; + if (work[sym] < end) { + here_op = 0; + here_val = work[sym]; + } + else if (work[sym] > end) { + here_op = extra[extra_index + work[sym]]; + here_val = base[base_index + work[sym]]; + } + else { + here_op = 32 + 64; /* end of block */ + here_val = 0; + } + + /* replicate for those indices with low len bits equal to huff */ + incr = 1 << (len - drop); + fill = 1 << curr; + min = fill; /* save offset to next table */ + do { + fill -= incr; + table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0; + } while (fill !== 0); + + /* backwards increment the len-bit code huff */ + incr = 1 << (len - 1); + while (huff & incr) { + incr >>= 1; + } + if (incr !== 0) { + huff &= incr - 1; + huff += incr; + } else { + huff = 0; + } + + /* go to next symbol, update count, len */ + sym++; + if (--count[len] === 0) { + if (len === max) { break; } + len = lens[lens_index + work[sym]]; + } + + /* create new sub-table if needed */ + if (len > root && (huff & mask) !== low) { + /* if first time, transition to sub-tables */ + if (drop === 0) { + drop = root; + } + + /* increment past last table */ + next += min; /* here min is 1 << curr */ + + /* determine length of next table */ + curr = len - drop; + left = 1 << curr; + while (curr + drop < max) { + left -= count[curr + drop]; + if (left <= 0) { break; } + curr++; + left <<= 1; + } + + /* check for enough space */ + used += 1 << curr; + if ((type === LENS && used > ENOUGH_LENS) || + (type === DISTS && used > ENOUGH_DISTS)) { + return 1; + } + + /* point entry in root table to sub-table */ + low = huff & mask; + /*table.op[low] = curr; + table.bits[low] = root; + table.val[low] = next - opts.table_index;*/ + table[low] = (root << 24) | (curr << 16) | (next - table_index) |0; + } + } + + /* fill in remaining table entry if code is incomplete (guaranteed to have + at most one remaining entry, since if the code is incomplete, the + maximum code length that was allowed to get this far is one bit) */ + if (huff !== 0) { + //table.op[next + huff] = 64; /* invalid code marker */ + //table.bits[next + huff] = len - drop; + //table.val[next + huff] = 0; + table[next + huff] = ((len - drop) << 24) | (64 << 16) |0; + } + + /* set return parameters */ + //opts.table_index += used; + opts.bits = root; + return 0; +}; + +},{"../utils/common":1}],7:[function(require,module,exports){ +'use strict'; + + +function ZStream() { + /* next input byte */ + this.input = null; // JS specific, because we have no pointers + this.next_in = 0; + /* number of bytes available at input */ + this.avail_in = 0; + /* total number of input bytes read so far */ + this.total_in = 0; + /* next output byte should be put there */ + this.output = null; // JS specific, because we have no pointers + this.next_out = 0; + /* remaining free space at output */ + this.avail_out = 0; + /* total number of bytes output so far */ + this.total_out = 0; + /* last error message, NULL if no error */ + this.msg = ''/*Z_NULL*/; + /* not visible by applications */ + this.state = null; + /* best guess about the data type: binary or text */ + this.data_type = 2/*Z_UNKNOWN*/; + /* adler32 value of the uncompressed data */ + this.adler = 0; +} + +module.exports = ZStream; + +},{}],"/partial_inflator.js":[function(require,module,exports){ +var zlib = require('./lib/zlib/inflate.js'); +var ZStream = require('./lib/zlib/zstream.js'); + +var Inflate = function () { + this.strm = new ZStream(); + this.chunkSize = 1024 * 10 * 10 * 10; + this.strm.output = new Uint8Array(this.chunkSize); + this.windowBits = 5; + + zlib.inflateInit(this.strm, this.windowBits); +}; + +Inflate.prototype = { + inflate: function (data, flush) { + this.strm.input = data; + this.strm.avail_in = this.strm.input.length; + this.strm.next_in = 0; + this.strm.next_out = 0; + + this.strm.avail_out = this.chunkSize; + + zlib.inflate(this.strm, flush); + + return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); + }, + + reset: function () { + zlib.inflateReset(this.strm); + } +}; + +module.exports = {Inflate: Inflate}; + +},{"./lib/zlib/inflate.js":5,"./lib/zlib/zstream.js":7}]},{},[])("/partial_inflator.js") +}); diff --git a/plugins/dynamix.vm.manager/include/input.js b/plugins/dynamix.vm.manager/include/input.js new file mode 100644 index 000000000..5d9e209e6 --- /dev/null +++ b/plugins/dynamix.vm.manager/include/input.js @@ -0,0 +1,388 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +/*jslint browser: true, white: false */ +/*global window, Util */ + +var Keyboard, Mouse; + +(function () { + "use strict"; + + // + // Keyboard event handler + // + + Keyboard = function (defaults) { + this._keyDownList = []; // List of depressed keys + // (even if they are happy) + + Util.set_defaults(this, defaults, { + 'target': document, + 'focused': true + }); + + // create the keyboard handler + this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(), + VerifyCharModifier( /* jshint newcap: false */ + TrackKeyState( + EscapeModifiers(this._handleRfbEvent.bind(this)) + ) + ) + ); /* jshint newcap: true */ + + // keep these here so we can refer to them later + this._eventHandlers = { + 'keyup': this._handleKeyUp.bind(this), + 'keydown': this._handleKeyDown.bind(this), + 'keypress': this._handleKeyPress.bind(this), + 'blur': this._allKeysUp.bind(this) + }; + }; + + Keyboard.prototype = { + // private methods + + _handleRfbEvent: function (e) { + if (this._onKeyPress) { + Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") + + ", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")"); + this._onKeyPress(e.keysym.keysym, e.type == 'keydown'); + } + }, + + _handleKeyDown: function (e) { + if (!this._focused) { return true; } + + if (this._handler.keydown(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, + + _handleKeyPress: function (e) { + if (!this._focused) { return true; } + + if (this._handler.keypress(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, + + _handleKeyUp: function (e) { + if (!this._focused) { return true; } + + if (this._handler.keyup(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, + + _allKeysUp: function () { + Util.Debug(">> Keyboard.allKeysUp"); + this._handler.releaseAll(); + Util.Debug("<< Keyboard.allKeysUp"); + }, + + // Public methods + + grab: function () { + //Util.Debug(">> Keyboard.grab"); + var c = this._target; + + Util.addEvent(c, 'keydown', this._eventHandlers.keydown); + Util.addEvent(c, 'keyup', this._eventHandlers.keyup); + Util.addEvent(c, 'keypress', this._eventHandlers.keypress); + + // Release (key up) if window loses focus + Util.addEvent(window, 'blur', this._eventHandlers.blur); + + //Util.Debug("<< Keyboard.grab"); + }, + + ungrab: function () { + //Util.Debug(">> Keyboard.ungrab"); + var c = this._target; + + Util.removeEvent(c, 'keydown', this._eventHandlers.keydown); + Util.removeEvent(c, 'keyup', this._eventHandlers.keyup); + Util.removeEvent(c, 'keypress', this._eventHandlers.keypress); + Util.removeEvent(window, 'blur', this._eventHandlers.blur); + + // Release (key up) all keys that are in a down state + this._allKeysUp(); + + //Util.Debug(">> Keyboard.ungrab"); + }, + + sync: function (e) { + this._handler.syncModifiers(e); + } + }; + + Util.make_properties(Keyboard, [ + ['target', 'wo', 'dom'], // DOM element that captures keyboard input + ['focused', 'rw', 'bool'], // Capture and send key events + + ['onKeyPress', 'rw', 'func'] // Handler for key press/release + ]); + + // + // Mouse event handler + // + + Mouse = function (defaults) { + this._mouseCaptured = false; + + this._doubleClickTimer = null; + this._lastTouchPos = null; + + // Configuration attributes + Util.set_defaults(this, defaults, { + 'target': document, + 'focused': true, + 'scale': 1.0, + 'touchButton': 1 + }); + + this._eventHandlers = { + 'mousedown': this._handleMouseDown.bind(this), + 'mouseup': this._handleMouseUp.bind(this), + 'mousemove': this._handleMouseMove.bind(this), + 'mousewheel': this._handleMouseWheel.bind(this), + 'mousedisable': this._handleMouseDisable.bind(this) + }; + }; + + Mouse.prototype = { + // private methods + _captureMouse: function () { + // capturing the mouse ensures we get the mouseup event + if (this._target.setCapture) { + this._target.setCapture(); + } + + // some browsers give us mouseup events regardless, + // so if we never captured the mouse, we can disregard the event + this._mouseCaptured = true; + }, + + _releaseMouse: function () { + if (this._target.releaseCapture) { + this._target.releaseCapture(); + } + this._mouseCaptured = false; + }, + + _resetDoubleClickTimer: function () { + this._doubleClickTimer = null; + }, + + _handleMouseButton: function (e, down) { + if (!this._focused) { return true; } + + if (this._notify) { + this._notify(e); + } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + + var bmask; + if (e.touches || e.changedTouches) { + // Touch device + + // When two touches occur within 500 ms of each other and are + // closer than 20 pixels together a double click is triggered. + if (down == 1) { + if (this._doubleClickTimer === null) { + this._lastTouchPos = pos; + } else { + clearTimeout(this._doubleClickTimer); + + // When the distance between the two touches is small enough + // force the position of the latter touch to the position of + // the first. + + var xs = this._lastTouchPos.x - pos.x; + var ys = this._lastTouchPos.y - pos.y; + var d = Math.sqrt((xs * xs) + (ys * ys)); + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + if (d < 20 * window.devicePixelRatio) { + pos = this._lastTouchPos; + } + } + this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); + } + bmask = this._touchButton; + // If bmask is set + } else if (evt.which) { + /* everything except IE */ + bmask = 1 << evt.button; + } else { + /* IE including 9 */ + bmask = (evt.button & 0x1) + // Left + (evt.button & 0x2) * 2 + // Right + (evt.button & 0x4) / 2; // Middle + } + + if (this._onMouseButton) { + Util.Debug("onMouseButton " + (down ? "down" : "up") + + ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); + this._onMouseButton(pos.x, pos.y, down, bmask); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseDown: function (e) { + this._captureMouse(); + this._handleMouseButton(e, 1); + }, + + _handleMouseUp: function (e) { + if (!this._mouseCaptured) { return; } + + this._handleMouseButton(e, 0); + this._releaseMouse(); + }, + + _handleMouseWheel: function (e) { + if (!this._focused) { return true; } + + if (this._notify) { + this._notify(e); + } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + var wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; + var bmask; + if (wheelData > 0) { + bmask = 1 << 3; + } else { + bmask = 1 << 4; + } + + if (this._onMouseButton) { + this._onMouseButton(pos.x, pos.y, 1, bmask); + this._onMouseButton(pos.x, pos.y, 0, bmask); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseMove: function (e) { + if (! this._focused) { return true; } + + if (this._notify) { + this._notify(e); + } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + if (this._onMouseMove) { + this._onMouseMove(pos.x, pos.y); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseDisable: function (e) { + if (!this._focused) { return true; } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + + /* Stop propagation if inside canvas area */ + if ((pos.realx >= 0) && (pos.realy >= 0) && + (pos.realx < this._target.offsetWidth) && + (pos.realy < this._target.offsetHeight)) { + //Util.Debug("mouse event disabled"); + Util.stopEvent(e); + return false; + } + + return true; + }, + + + // Public methods + grab: function () { + var c = this._target; + + if ('ontouchstart' in document.documentElement) { + Util.addEvent(c, 'touchstart', this._eventHandlers.mousedown); + Util.addEvent(window, 'touchend', this._eventHandlers.mouseup); + Util.addEvent(c, 'touchend', this._eventHandlers.mouseup); + Util.addEvent(c, 'touchmove', this._eventHandlers.mousemove); + } else { + Util.addEvent(c, 'mousedown', this._eventHandlers.mousedown); + Util.addEvent(window, 'mouseup', this._eventHandlers.mouseup); + Util.addEvent(c, 'mouseup', this._eventHandlers.mouseup); + Util.addEvent(c, 'mousemove', this._eventHandlers.mousemove); + Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + this._eventHandlers.mousewheel); + } + + /* Work around right and middle click browser behaviors */ + Util.addEvent(document, 'click', this._eventHandlers.mousedisable); + Util.addEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable); + }, + + ungrab: function () { + var c = this._target; + + if ('ontouchstart' in document.documentElement) { + Util.removeEvent(c, 'touchstart', this._eventHandlers.mousedown); + Util.removeEvent(window, 'touchend', this._eventHandlers.mouseup); + Util.removeEvent(c, 'touchend', this._eventHandlers.mouseup); + Util.removeEvent(c, 'touchmove', this._eventHandlers.mousemove); + } else { + Util.removeEvent(c, 'mousedown', this._eventHandlers.mousedown); + Util.removeEvent(window, 'mouseup', this._eventHandlers.mouseup); + Util.removeEvent(c, 'mouseup', this._eventHandlers.mouseup); + Util.removeEvent(c, 'mousemove', this._eventHandlers.mousemove); + Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + this._eventHandlers.mousewheel); + } + + /* Work around right and middle click browser behaviors */ + Util.removeEvent(document, 'click', this._eventHandlers.mousedisable); + Util.removeEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable); + + } + }; + + Util.make_properties(Mouse, [ + ['target', 'ro', 'dom'], // DOM element that captures mouse input + ['notify', 'ro', 'func'], // Function to call to notify whenever a mouse event is received + ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement + ['scale', 'rw', 'float'], // Viewport scale factor 0.0 - 1.0 + + ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release + ['onMouseMove', 'rw', 'func'], // Handler for mouse movement + ['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) + ]); +})(); diff --git a/plugins/dynamix.vm.manager/include/jsunzip.js b/plugins/dynamix.vm.manager/include/jsunzip.js new file mode 100644 index 000000000..8968f866a --- /dev/null +++ b/plugins/dynamix.vm.manager/include/jsunzip.js @@ -0,0 +1,676 @@ +/* + * JSUnzip + * + * Copyright (c) 2011 by Erik Moller + * All Rights Reserved + * + * This software is provided 'as-is', without any express + * or implied warranty. In no event will the authors be + * held liable for any damages arising from the use of + * this software. + * + * Permission is granted to anyone to use this software + * for any purpose, including commercial applications, + * and to alter it and redistribute it freely, subject to + * the following restrictions: + * + * 1. The origin of this software must not be + * misrepresented; you must not claim that you + * wrote the original software. If you use this + * software in a product, an acknowledgment in + * the product documentation would be appreciated + * but is not required. + * + * 2. Altered source versions must be plainly marked + * as such, and must not be misrepresented as + * being the original software. + * + * 3. This notice may not be removed or altered from + * any source distribution. + */ + +var tinf; + +function JSUnzip() { + + this.getInt = function(offset, size) { + switch (size) { + case 4: + return (this.data.charCodeAt(offset + 3) & 0xff) << 24 | + (this.data.charCodeAt(offset + 2) & 0xff) << 16 | + (this.data.charCodeAt(offset + 1) & 0xff) << 8 | + (this.data.charCodeAt(offset + 0) & 0xff); + break; + case 2: + return (this.data.charCodeAt(offset + 1) & 0xff) << 8 | + (this.data.charCodeAt(offset + 0) & 0xff); + break; + default: + return this.data.charCodeAt(offset) & 0xff; + break; + } + }; + + this.getDOSDate = function(dosdate, dostime) { + var day = dosdate & 0x1f; + var month = ((dosdate >> 5) & 0xf) - 1; + var year = 1980 + ((dosdate >> 9) & 0x7f) + var second = (dostime & 0x1f) * 2; + var minute = (dostime >> 5) & 0x3f; + hour = (dostime >> 11) & 0x1f; + return new Date(year, month, day, hour, minute, second); + } + + this.open = function(data) { + this.data = data; + this.files = []; + + if (this.data.length < 22) + return { 'status' : false, 'error' : 'Invalid data' }; + var endOfCentralDirectory = this.data.length - 22; + while (endOfCentralDirectory >= 0 && this.getInt(endOfCentralDirectory, 4) != 0x06054b50) + --endOfCentralDirectory; + if (endOfCentralDirectory < 0) + return { 'status' : false, 'error' : 'Invalid data' }; + if (this.getInt(endOfCentralDirectory + 4, 2) != 0 || this.getInt(endOfCentralDirectory + 6, 2) != 0) + return { 'status' : false, 'error' : 'No multidisk support' }; + + var entriesInThisDisk = this.getInt(endOfCentralDirectory + 8, 2); + var centralDirectoryOffset = this.getInt(endOfCentralDirectory + 16, 4); + var globalCommentLength = this.getInt(endOfCentralDirectory + 20, 2); + this.comment = this.data.slice(endOfCentralDirectory + 22, endOfCentralDirectory + 22 + globalCommentLength); + + var fileOffset = centralDirectoryOffset; + + for (var i = 0; i < entriesInThisDisk; ++i) { + if (this.getInt(fileOffset + 0, 4) != 0x02014b50) + return { 'status' : false, 'error' : 'Invalid data' }; + if (this.getInt(fileOffset + 6, 2) > 20) + return { 'status' : false, 'error' : 'Unsupported version' }; + if (this.getInt(fileOffset + 8, 2) & 1) + return { 'status' : false, 'error' : 'Encryption not implemented' }; + + var compressionMethod = this.getInt(fileOffset + 10, 2); + if (compressionMethod != 0 && compressionMethod != 8) + return { 'status' : false, 'error' : 'Unsupported compression method' }; + + var lastModFileTime = this.getInt(fileOffset + 12, 2); + var lastModFileDate = this.getInt(fileOffset + 14, 2); + var lastModifiedDate = this.getDOSDate(lastModFileDate, lastModFileTime); + + var crc = this.getInt(fileOffset + 16, 4); + // TODO: crc + + var compressedSize = this.getInt(fileOffset + 20, 4); + var uncompressedSize = this.getInt(fileOffset + 24, 4); + + var fileNameLength = this.getInt(fileOffset + 28, 2); + var extraFieldLength = this.getInt(fileOffset + 30, 2); + var fileCommentLength = this.getInt(fileOffset + 32, 2); + + var relativeOffsetOfLocalHeader = this.getInt(fileOffset + 42, 4); + + var fileName = this.data.slice(fileOffset + 46, fileOffset + 46 + fileNameLength); + var fileComment = this.data.slice(fileOffset + 46 + fileNameLength + extraFieldLength, fileOffset + 46 + fileNameLength + extraFieldLength + fileCommentLength); + + if (this.getInt(relativeOffsetOfLocalHeader + 0, 4) != 0x04034b50) + return { 'status' : false, 'error' : 'Invalid data' }; + var localFileNameLength = this.getInt(relativeOffsetOfLocalHeader + 26, 2); + var localExtraFieldLength = this.getInt(relativeOffsetOfLocalHeader + 28, 2); + var localFileContent = relativeOffsetOfLocalHeader + 30 + localFileNameLength + localExtraFieldLength; + + this.files[fileName] = + { + 'fileComment' : fileComment, + 'compressionMethod' : compressionMethod, + 'compressedSize' : compressedSize, + 'uncompressedSize' : uncompressedSize, + 'localFileContent' : localFileContent, + 'lastModifiedDate' : lastModifiedDate + }; + + fileOffset += 46 + fileNameLength + extraFieldLength + fileCommentLength; + } + return { 'status' : true } + }; + + + this.read = function(fileName) { + var fileInfo = this.files[fileName]; + if (fileInfo) { + if (fileInfo.compressionMethod == 8) { + if (!tinf) { + tinf = new TINF(); + tinf.init(); + } + var result = tinf.uncompress(this.data, fileInfo.localFileContent); + if (result.status == tinf.OK) + return { 'status' : true, 'data' : result.data }; + else + return { 'status' : false, 'error' : result.error }; + } else { + return { 'status' : true, 'data' : this.data.slice(fileInfo.localFileContent, fileInfo.localFileContent + fileInfo.uncompressedSize) }; + } + } + return { 'status' : false, 'error' : "File '" + fileName + "' doesn't exist in zip" }; + }; + +}; + + + +/* + * tinflate - tiny inflate + * + * Copyright (c) 2003 by Joergen Ibsen / Jibz + * All Rights Reserved + * + * http://www.ibsensoftware.com/ + * + * This software is provided 'as-is', without any express + * or implied warranty. In no event will the authors be + * held liable for any damages arising from the use of + * this software. + * + * Permission is granted to anyone to use this software + * for any purpose, including commercial applications, + * and to alter it and redistribute it freely, subject to + * the following restrictions: + * + * 1. The origin of this software must not be + * misrepresented; you must not claim that you + * wrote the original software. If you use this + * software in a product, an acknowledgment in + * the product documentation would be appreciated + * but is not required. + * + * 2. Altered source versions must be plainly marked + * as such, and must not be misrepresented as + * being the original software. + * + * 3. This notice may not be removed or altered from + * any source distribution. + */ + +/* + * tinflate javascript port by Erik Moller in May 2011. + * emoller@opera.com + * + * read_bits() patched by mike@imidio.com to allow + * reading more then 8 bits (needed in some zlib streams) + */ + +"use strict"; + +function TINF() { + +this.OK = 0; +this.DATA_ERROR = (-3); +this.WINDOW_SIZE = 32768; + +/* ------------------------------ * + * -- internal data structures -- * + * ------------------------------ */ + +this.TREE = function() { + this.table = new Array(16); /* table of code length counts */ + this.trans = new Array(288); /* code -> symbol translation table */ +}; + +this.DATA = function(that) { + this.source = ''; + this.sourceIndex = 0; + this.tag = 0; + this.bitcount = 0; + + this.dest = []; + + this.history = []; + + this.ltree = new that.TREE(); /* dynamic length/symbol tree */ + this.dtree = new that.TREE(); /* dynamic distance tree */ +}; + +/* --------------------------------------------------- * + * -- uninitialized global data (static structures) -- * + * --------------------------------------------------- */ + +this.sltree = new this.TREE(); /* fixed length/symbol tree */ +this.sdtree = new this.TREE(); /* fixed distance tree */ + +/* extra bits and base tables for length codes */ +this.length_bits = new Array(30); +this.length_base = new Array(30); + +/* extra bits and base tables for distance codes */ +this.dist_bits = new Array(30); +this.dist_base = new Array(30); + +/* special ordering of code length codes */ +this.clcidx = [ + 16, 17, 18, 0, 8, 7, 9, 6, + 10, 5, 11, 4, 12, 3, 13, 2, + 14, 1, 15 +]; + +/* ----------------------- * + * -- utility functions -- * + * ----------------------- */ + +/* build extra bits and base tables */ +this.build_bits_base = function(bits, base, delta, first) +{ + var i, sum; + + /* build bits table */ + for (i = 0; i < delta; ++i) bits[i] = 0; + for (i = 0; i < 30 - delta; ++i) bits[i + delta] = Math.floor(i / delta); + + /* build base table */ + for (sum = first, i = 0; i < 30; ++i) + { + base[i] = sum; + sum += 1 << bits[i]; + } +} + +/* build the fixed huffman trees */ +this.build_fixed_trees = function(lt, dt) +{ + var i; + + /* build fixed length tree */ + for (i = 0; i < 7; ++i) lt.table[i] = 0; + + lt.table[7] = 24; + lt.table[8] = 152; + lt.table[9] = 112; + + for (i = 0; i < 24; ++i) lt.trans[i] = 256 + i; + for (i = 0; i < 144; ++i) lt.trans[24 + i] = i; + for (i = 0; i < 8; ++i) lt.trans[24 + 144 + i] = 280 + i; + for (i = 0; i < 112; ++i) lt.trans[24 + 144 + 8 + i] = 144 + i; + + /* build fixed distance tree */ + for (i = 0; i < 5; ++i) dt.table[i] = 0; + + dt.table[5] = 32; + + for (i = 0; i < 32; ++i) dt.trans[i] = i; +} + +/* given an array of code lengths, build a tree */ +this.build_tree = function(t, lengths, loffset, num) +{ + var offs = new Array(16); + var i, sum; + + /* clear code length count table */ + for (i = 0; i < 16; ++i) t.table[i] = 0; + + /* scan symbol lengths, and sum code length counts */ + for (i = 0; i < num; ++i) t.table[lengths[loffset + i]]++; + + t.table[0] = 0; + + /* compute offset table for distribution sort */ + for (sum = 0, i = 0; i < 16; ++i) + { + offs[i] = sum; + sum += t.table[i]; + } + + /* create code->symbol translation table (symbols sorted by code) */ + for (i = 0; i < num; ++i) + { + if (lengths[loffset + i]) t.trans[offs[lengths[loffset + i]]++] = i; + } +} + +/* ---------------------- * + * -- decode functions -- * + * ---------------------- */ + +/* get one bit from source stream */ +this.getbit = function(d) +{ + var bit; + + /* check if tag is empty */ + if (!d.bitcount--) + { + /* load next tag */ + d.tag = d.source[d.sourceIndex++] & 0xff; + d.bitcount = 7; + } + + /* shift bit out of tag */ + bit = d.tag & 0x01; + d.tag >>= 1; + + return bit; +} + +/* read a num bit value from a stream and add base */ +function read_bits_direct(source, bitcount, tag, idx, num) +{ + var val = 0; + while (bitcount < 24) { + tag = tag | (source[idx++] & 0xff) << bitcount; + bitcount += 8; + } + val = tag & (0xffff >> (16 - num)); + tag >>= num; + bitcount -= num; + return [bitcount, tag, idx, val]; +} +this.read_bits = function(d, num, base) +{ + if (!num) + return base; + + var ret = read_bits_direct(d.source, d.bitcount, d.tag, d.sourceIndex, num); + d.bitcount = ret[0]; + d.tag = ret[1]; + d.sourceIndex = ret[2]; + return ret[3] + base; +} + +/* given a data stream and a tree, decode a symbol */ +this.decode_symbol = function(d, t) +{ + while (d.bitcount < 16) { + d.tag = d.tag | (d.source[d.sourceIndex++] & 0xff) << d.bitcount; + d.bitcount += 8; + } + + var sum = 0, cur = 0, len = 0; + do { + cur = 2 * cur + ((d.tag & (1 << len)) >> len); + + ++len; + + sum += t.table[len]; + cur -= t.table[len]; + + } while (cur >= 0); + + d.tag >>= len; + d.bitcount -= len; + + return t.trans[sum + cur]; +} + +/* given a data stream, decode dynamic trees from it */ +this.decode_trees = function(d, lt, dt) +{ + var code_tree = new this.TREE(); + var lengths = new Array(288+32); + var hlit, hdist, hclen; + var i, num, length; + + /* get 5 bits HLIT (257-286) */ + hlit = this.read_bits(d, 5, 257); + + /* get 5 bits HDIST (1-32) */ + hdist = this.read_bits(d, 5, 1); + + /* get 4 bits HCLEN (4-19) */ + hclen = this.read_bits(d, 4, 4); + + for (i = 0; i < 19; ++i) lengths[i] = 0; + + /* read code lengths for code length alphabet */ + for (i = 0; i < hclen; ++i) + { + /* get 3 bits code length (0-7) */ + var clen = this.read_bits(d, 3, 0); + + lengths[this.clcidx[i]] = clen; + } + + /* build code length tree */ + this.build_tree(code_tree, lengths, 0, 19); + + /* decode code lengths for the dynamic trees */ + for (num = 0; num < hlit + hdist; ) + { + var sym = this.decode_symbol(d, code_tree); + + switch (sym) + { + case 16: + /* copy previous code length 3-6 times (read 2 bits) */ + { + var prev = lengths[num - 1]; + for (length = this.read_bits(d, 2, 3); length; --length) + { + lengths[num++] = prev; + } + } + break; + case 17: + /* repeat code length 0 for 3-10 times (read 3 bits) */ + for (length = this.read_bits(d, 3, 3); length; --length) + { + lengths[num++] = 0; + } + break; + case 18: + /* repeat code length 0 for 11-138 times (read 7 bits) */ + for (length = this.read_bits(d, 7, 11); length; --length) + { + lengths[num++] = 0; + } + break; + default: + /* values 0-15 represent the actual code lengths */ + lengths[num++] = sym; + break; + } + } + + /* build dynamic trees */ + this.build_tree(lt, lengths, 0, hlit); + this.build_tree(dt, lengths, hlit, hdist); +} + +/* ----------------------------- * + * -- block inflate functions -- * + * ----------------------------- */ + +/* given a stream and two trees, inflate a block of data */ +this.inflate_block_data = function(d, lt, dt) +{ + // js optimization. + var ddest = d.dest; + var ddestlength = ddest.length; + + while (1) + { + var sym = this.decode_symbol(d, lt); + + /* check for end of block */ + if (sym == 256) + { + return this.OK; + } + + if (sym < 256) + { + ddest[ddestlength++] = sym; // ? String.fromCharCode(sym); + d.history.push(sym); + } else { + + var length, dist, offs; + var i; + + sym -= 257; + + /* possibly get more bits from length code */ + length = this.read_bits(d, this.length_bits[sym], this.length_base[sym]); + + dist = this.decode_symbol(d, dt); + + /* possibly get more bits from distance code */ + offs = d.history.length - this.read_bits(d, this.dist_bits[dist], this.dist_base[dist]); + + if (offs < 0) + throw ("Invalid zlib offset " + offs); + + /* copy match */ + for (i = offs; i < offs + length; ++i) { + //ddest[ddestlength++] = ddest[i]; + ddest[ddestlength++] = d.history[i]; + d.history.push(d.history[i]); + } + } + } +} + +/* inflate an uncompressed block of data */ +this.inflate_uncompressed_block = function(d) +{ + var length, invlength; + var i; + + if (d.bitcount > 7) { + var overflow = Math.floor(d.bitcount / 8); + d.sourceIndex -= overflow; + d.bitcount = 0; + d.tag = 0; + } + + /* get length */ + length = d.source[d.sourceIndex+1]; + length = 256*length + d.source[d.sourceIndex]; + + /* get one's complement of length */ + invlength = d.source[d.sourceIndex+3]; + invlength = 256*invlength + d.source[d.sourceIndex+2]; + + /* check length */ + if (length != (~invlength & 0x0000ffff)) return this.DATA_ERROR; + + d.sourceIndex += 4; + + /* copy block */ + for (i = length; i; --i) { + d.history.push(d.source[d.sourceIndex]); + d.dest[d.dest.length] = d.source[d.sourceIndex++]; + } + + /* make sure we start next block on a byte boundary */ + d.bitcount = 0; + + return this.OK; +} + +/* inflate a block of data compressed with fixed huffman trees */ +this.inflate_fixed_block = function(d) +{ + /* decode block using fixed trees */ + return this.inflate_block_data(d, this.sltree, this.sdtree); +} + +/* inflate a block of data compressed with dynamic huffman trees */ +this.inflate_dynamic_block = function(d) +{ + /* decode trees from stream */ + this.decode_trees(d, d.ltree, d.dtree); + + /* decode block using decoded trees */ + return this.inflate_block_data(d, d.ltree, d.dtree); +} + +/* ---------------------- * + * -- public functions -- * + * ---------------------- */ + +/* initialize global (static) data */ +this.init = function() +{ + /* build fixed huffman trees */ + this.build_fixed_trees(this.sltree, this.sdtree); + + /* build extra bits and base tables */ + this.build_bits_base(this.length_bits, this.length_base, 4, 3); + this.build_bits_base(this.dist_bits, this.dist_base, 2, 1); + + /* fix a special case */ + this.length_bits[28] = 0; + this.length_base[28] = 258; + + this.reset(); +} + +this.reset = function() +{ + this.d = new this.DATA(this); + delete this.header; +} + +/* inflate stream from source to dest */ +this.uncompress = function(source, offset) +{ + + var d = this.d; + var bfinal; + + /* initialise data */ + d.source = source; + d.sourceIndex = offset; + d.bitcount = 0; + + d.dest = []; + + // Skip zlib header at start of stream + if (typeof this.header == 'undefined') { + this.header = this.read_bits(d, 16, 0); + /* byte 0: 0x78, 7 = 32k window size, 8 = deflate */ + /* byte 1: check bits for header and other flags */ + } + + var blocks = 0; + + do { + + var btype; + var res; + + /* read final block flag */ + bfinal = this.getbit(d); + + /* read block type (2 bits) */ + btype = this.read_bits(d, 2, 0); + + /* decompress block */ + switch (btype) + { + case 0: + /* decompress uncompressed block */ + res = this.inflate_uncompressed_block(d); + break; + case 1: + /* decompress block with fixed huffman trees */ + res = this.inflate_fixed_block(d); + break; + case 2: + /* decompress block with dynamic huffman trees */ + res = this.inflate_dynamic_block(d); + break; + default: + return { 'status' : this.DATA_ERROR }; + } + + if (res != this.OK) return { 'status' : this.DATA_ERROR }; + blocks++; + + } while (!bfinal && d.sourceIndex < d.source.length); + + d.history = d.history.slice(-this.WINDOW_SIZE); + + return { 'status' : this.OK, 'data' : d.dest }; +} + +}; diff --git a/plugins/dynamix.vm.manager/include/keyboard.js b/plugins/dynamix.vm.manager/include/keyboard.js new file mode 100644 index 000000000..86670312a --- /dev/null +++ b/plugins/dynamix.vm.manager/include/keyboard.js @@ -0,0 +1,543 @@ +var kbdUtil = (function() { + "use strict"; + + function substituteCodepoint(cp) { + // Any Unicode code points which do not have corresponding keysym entries + // can be swapped out for another code point by adding them to this table + var substitutions = { + // {S,s} with comma below -> {S,s} with cedilla + 0x218 : 0x15e, + 0x219 : 0x15f, + // {T,t} with comma below -> {T,t} with cedilla + 0x21a : 0x162, + 0x21b : 0x163 + }; + + var sub = substitutions[cp]; + return sub ? sub : cp; + } + + function isMac() { + return navigator && !!(/mac/i).exec(navigator.platform); + } + function isWindows() { + return navigator && !!(/win/i).exec(navigator.platform); + } + function isLinux() { + return navigator && !!(/linux/i).exec(navigator.platform); + } + + // Return true if a modifier which is not the specified char modifier (and is not shift) is down + function hasShortcutModifier(charModifier, currentModifiers) { + var mods = {}; + for (var key in currentModifiers) { + if (parseInt(key) !== XK_Shift_L) { + mods[key] = currentModifiers[key]; + } + } + + var sum = 0; + for (var k in currentModifiers) { + if (mods[k]) { + ++sum; + } + } + if (hasCharModifier(charModifier, mods)) { + return sum > charModifier.length; + } + else { + return sum > 0; + } + } + + // Return true if the specified char modifier is currently down + function hasCharModifier(charModifier, currentModifiers) { + if (charModifier.length === 0) { return false; } + + for (var i = 0; i < charModifier.length; ++i) { + if (!currentModifiers[charModifier[i]]) { + return false; + } + } + return true; + } + + // Helper object tracking modifier key state + // and generates fake key events to compensate if it gets out of sync + function ModifierSync(charModifier) { + if (!charModifier) { + if (isMac()) { + // on Mac, Option (AKA Alt) is used as a char modifier + charModifier = [XK_Alt_L]; + } + else if (isWindows()) { + // on Windows, Ctrl+Alt is used as a char modifier + charModifier = [XK_Alt_L, XK_Control_L]; + } + else if (isLinux()) { + // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier + charModifier = [XK_ISO_Level3_Shift]; + } + else { + charModifier = []; + } + } + + var state = {}; + state[XK_Control_L] = false; + state[XK_Alt_L] = false; + state[XK_ISO_Level3_Shift] = false; + state[XK_Shift_L] = false; + state[XK_Meta_L] = false; + + function sync(evt, keysym) { + var result = []; + function syncKey(keysym) { + return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; + } + + if (evt.ctrlKey !== undefined && + evt.ctrlKey !== state[XK_Control_L] && keysym !== XK_Control_L) { + state[XK_Control_L] = evt.ctrlKey; + result.push(syncKey(XK_Control_L)); + } + if (evt.altKey !== undefined && + evt.altKey !== state[XK_Alt_L] && keysym !== XK_Alt_L) { + state[XK_Alt_L] = evt.altKey; + result.push(syncKey(XK_Alt_L)); + } + if (evt.altGraphKey !== undefined && + evt.altGraphKey !== state[XK_ISO_Level3_Shift] && keysym !== XK_ISO_Level3_Shift) { + state[XK_ISO_Level3_Shift] = evt.altGraphKey; + result.push(syncKey(XK_ISO_Level3_Shift)); + } + if (evt.shiftKey !== undefined && + evt.shiftKey !== state[XK_Shift_L] && keysym !== XK_Shift_L) { + state[XK_Shift_L] = evt.shiftKey; + result.push(syncKey(XK_Shift_L)); + } + if (evt.metaKey !== undefined && + evt.metaKey !== state[XK_Meta_L] && keysym !== XK_Meta_L) { + state[XK_Meta_L] = evt.metaKey; + result.push(syncKey(XK_Meta_L)); + } + return result; + } + function syncKeyEvent(evt, down) { + var obj = getKeysym(evt); + var keysym = obj ? obj.keysym : null; + + // first, apply the event itself, if relevant + if (keysym !== null && state[keysym] !== undefined) { + state[keysym] = down; + } + return sync(evt, keysym); + } + + return { + // sync on the appropriate keyboard event + keydown: function(evt) { return syncKeyEvent(evt, true);}, + keyup: function(evt) { return syncKeyEvent(evt, false);}, + // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway + syncAny: function(evt) { return sync(evt);}, + + // is a shortcut modifier down? + hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); }, + // if a char modifier is down, return the keys it consists of, otherwise return null + activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; } + }; + } + + // Get a key ID from a keyboard event + // May be a string or an integer depending on the available properties + function getKey(evt){ + if ('keyCode' in evt && 'key' in evt) { + return evt.key + ':' + evt.keyCode; + } + else if ('keyCode' in evt) { + return evt.keyCode; + } + else { + return evt.key; + } + } + + // Get the most reliable keysym value we can get from a key event + // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which + function getKeysym(evt){ + var codepoint; + if (evt.char && evt.char.length === 1) { + codepoint = evt.char.charCodeAt(); + } + else if (evt.charCode) { + codepoint = evt.charCode; + } + else if (evt.keyCode && evt.type === 'keypress') { + // IE10 stores the char code as keyCode, and has no other useful properties + codepoint = evt.keyCode; + } + if (codepoint) { + var res = keysyms.fromUnicode(substituteCodepoint(codepoint)); + if (res) { + return res; + } + } + // we could check evt.key here. + // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list, + // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key + // so we don't *need* it yet + if (evt.keyCode) { + return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey)); + } + if (evt.which) { + return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey)); + } + return null; + } + + // Given a keycode, try to predict which keysym it might be. + // If the keycode is unknown, null is returned. + function keysymFromKeyCode(keycode, shiftPressed) { + if (typeof(keycode) !== 'number') { + return null; + } + // won't be accurate for azerty + if (keycode >= 0x30 && keycode <= 0x39) { + return keycode; // digit + } + if (keycode >= 0x41 && keycode <= 0x5a) { + // remap to lowercase unless shift is down + return shiftPressed ? keycode : keycode + 32; // A-Z + } + if (keycode >= 0x60 && keycode <= 0x69) { + return XK_KP_0 + (keycode - 0x60); // numpad 0-9 + } + + switch(keycode) { + case 0x20: return XK_space; + case 0x6a: return XK_KP_Multiply; + case 0x6b: return XK_KP_Add; + case 0x6c: return XK_KP_Separator; + case 0x6d: return XK_KP_Subtract; + case 0x6e: return XK_KP_Decimal; + case 0x6f: return XK_KP_Divide; + case 0xbb: return XK_plus; + case 0xbc: return XK_comma; + case 0xbd: return XK_minus; + case 0xbe: return XK_period; + } + + return nonCharacterKey({keyCode: keycode}); + } + + // if the key is a known non-character key (any key which doesn't generate character data) + // return its keysym value. Otherwise return null + function nonCharacterKey(evt) { + // evt.key not implemented yet + if (!evt.keyCode) { return null; } + var keycode = evt.keyCode; + + if (keycode >= 0x70 && keycode <= 0x87) { + return XK_F1 + keycode - 0x70; // F1-F24 + } + switch (keycode) { + + case 8 : return XK_BackSpace; + case 13 : return XK_Return; + + case 9 : return XK_Tab; + + case 27 : return XK_Escape; + case 46 : return XK_Delete; + + case 36 : return XK_Home; + case 35 : return XK_End; + case 33 : return XK_Page_Up; + case 34 : return XK_Page_Down; + case 45 : return XK_Insert; + + case 37 : return XK_Left; + case 38 : return XK_Up; + case 39 : return XK_Right; + case 40 : return XK_Down; + + case 16 : return XK_Shift_L; + case 17 : return XK_Control_L; + case 18 : return XK_Alt_L; // also: Option-key on Mac + + case 224 : return XK_Meta_L; + case 225 : return XK_ISO_Level3_Shift; // AltGr + case 91 : return XK_Super_L; // also: Windows-key + case 92 : return XK_Super_R; // also: Windows-key + case 93 : return XK_Menu; // also: Windows-Menu, Command on Mac + default: return null; + } + } + return { + hasShortcutModifier : hasShortcutModifier, + hasCharModifier : hasCharModifier, + ModifierSync : ModifierSync, + getKey : getKey, + getKeysym : getKeysym, + keysymFromKeyCode : keysymFromKeyCode, + nonCharacterKey : nonCharacterKey, + substituteCodepoint : substituteCodepoint + }; +})(); + +// Takes a DOM keyboard event and: +// - determines which keysym it represents +// - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event) +// - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down +// - marks each event with an 'escape' property if a modifier was down which should be "escaped" +// - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown +// This information is collected into an object which is passed to the next() function. (one call per event) +function KeyEventDecoder(modifierState, next) { + "use strict"; + function sendAll(evts) { + for (var i = 0; i < evts.length; ++i) { + next(evts[i]); + } + } + function process(evt, type) { + var result = {type: type}; + var keyId = kbdUtil.getKey(evt); + if (keyId) { + result.keyId = keyId; + } + + var keysym = kbdUtil.getKeysym(evt); + + var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); + // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress? + // "special" keys like enter, tab or backspace don't send keypress events, + // and some browsers don't send keypresses at all if a modifier is down + if (keysym && (type !== 'keydown' || kbdUtil.nonCharacterKey(evt) || hasModifier)) { + result.keysym = keysym; + } + + var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; + + // Should we prevent the browser from handling the event? + // Doing so on a keydown (in most browsers) prevents keypress from being generated + // so only do that if we have to. + var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!kbdUtil.nonCharacterKey(evt)); + + // If a char modifier is down on a keydown, we need to insert a stall, + // so VerifyCharModifier knows to wait and see if a keypress is comnig + var stall = type === 'keydown' && modifierState.activeCharModifier() && !kbdUtil.nonCharacterKey(evt); + + // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt) + var active = modifierState.activeCharModifier(); + + // If we have a char modifier down, and we're able to determine a keysym reliably + // then (a) we know to treat the modifier as a char modifier, + // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char. + if (active && keysym) { + var isCharModifier = false; + for (var i = 0; i < active.length; ++i) { + if (active[i] === keysym.keysym) { + isCharModifier = true; + } + } + if (type === 'keypress' && !isCharModifier) { + result.escape = modifierState.activeCharModifier(); + } + } + + if (stall) { + // insert a fake "stall" event + next({type: 'stall'}); + } + next(result); + + return suppress; + } + + return { + keydown: function(evt) { + sendAll(modifierState.keydown(evt)); + return process(evt, 'keydown'); + }, + keypress: function(evt) { + return process(evt, 'keypress'); + }, + keyup: function(evt) { + sendAll(modifierState.keyup(evt)); + return process(evt, 'keyup'); + }, + syncModifiers: function(evt) { + sendAll(modifierState.syncAny(evt)); + }, + releaseAll: function() { next({type: 'releaseall'}); } + }; +} + +// Combines keydown and keypress events where necessary to handle char modifiers. +// On some OS'es, a char modifier is sometimes used as a shortcut modifier. +// For example, on Windows, AltGr is synonymous with Ctrl-Alt. On a Danish keyboard layout, AltGr-2 yields a @, but Ctrl-Alt-D does nothing +// so when used with the '2' key, Ctrl-Alt counts as a char modifier (and should be escaped), but when used with 'D', it does not. +// The only way we can distinguish these cases is to wait and see if a keypress event arrives +// When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two +function VerifyCharModifier(next) { + "use strict"; + var queue = []; + var timer = null; + function process() { + if (timer) { + return; + } + + var delayProcess = function () { + clearTimeout(timer); + timer = null; + process(); + }; + + while (queue.length !== 0) { + var cur = queue[0]; + queue = queue.splice(1); + switch (cur.type) { + case 'stall': + // insert a delay before processing available events. + /* jshint loopfunc: true */ + timer = setTimeout(delayProcess, 5); + /* jshint loopfunc: false */ + return; + case 'keydown': + // is the next element a keypress? Then we should merge the two + if (queue.length !== 0 && queue[0].type === 'keypress') { + // Firefox sends keypress even when no char is generated. + // so, if keypress keysym is the same as we'd have guessed from keydown, + // the modifier didn't have any effect, and should not be escaped + if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) { + cur.escape = queue[0].escape; + } + cur.keysym = queue[0].keysym; + queue = queue.splice(1); + } + break; + } + + // swallow stall events, and pass all others to the next stage + if (cur.type !== 'stall') { + next(cur); + } + } + } + return function(evt) { + queue.push(evt); + process(); + }; +} + +// Keeps track of which keys we (and the server) believe are down +// When a keyup is received, match it against this list, to determine the corresponding keysym(s) +// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars +// key repeat events should be merged into a single entry. +// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess +function TrackKeyState(next) { + "use strict"; + var state = []; + + return function (evt) { + var last = state.length !== 0 ? state[state.length-1] : null; + + switch (evt.type) { + case 'keydown': + // insert a new entry if last seen key was different. + if (!last || !evt.keyId || last.keyId !== evt.keyId) { + last = {keyId: evt.keyId, keysyms: {}}; + state.push(last); + } + if (evt.keysym) { + // make sure last event contains this keysym (a single "logical" keyevent + // can cause multiple key events to be sent to the VNC server) + last.keysyms[evt.keysym.keysym] = evt.keysym; + last.ignoreKeyPress = true; + next(evt); + } + break; + case 'keypress': + if (!last) { + last = {keyId: evt.keyId, keysyms: {}}; + state.push(last); + } + if (!evt.keysym) { + console.log('keypress with no keysym:', evt); + } + + // If we didn't expect a keypress, and already sent a keydown to the VNC server + // based on the keydown, make sure to skip this event. + if (evt.keysym && !last.ignoreKeyPress) { + last.keysyms[evt.keysym.keysym] = evt.keysym; + evt.type = 'keydown'; + next(evt); + } + break; + case 'keyup': + if (state.length === 0) { + return; + } + var idx = null; + // do we have a matching key tracked as being down? + for (var i = 0; i !== state.length; ++i) { + if (state[i].keyId === evt.keyId) { + idx = i; + break; + } + } + // if we couldn't find a match (it happens), assume it was the last key pressed + if (idx === null) { + idx = state.length - 1; + } + + var item = state.splice(idx, 1)[0]; + // for each keysym tracked by this key entry, clone the current event and override the keysym + var clone = (function(){ + function Clone(){} + return function (obj) { Clone.prototype=obj; return new Clone(); }; + }()); + for (var key in item.keysyms) { + var out = clone(evt); + out.keysym = item.keysyms[key]; + next(out); + } + break; + case 'releaseall': + /* jshint shadow: true */ + for (var i = 0; i < state.length; ++i) { + for (var key in state[i].keysyms) { + var keysym = state[i].keysyms[key]; + next({keyId: 0, keysym: keysym, type: 'keyup'}); + } + } + /* jshint shadow: false */ + state = []; + } + }; +} + +// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @), +// then the modifier must be "undone" before sending the @, and "redone" afterwards. +function EscapeModifiers(next) { + "use strict"; + return function(evt) { + if (evt.type !== 'keydown' || evt.escape === undefined) { + next(evt); + return; + } + // undo modifiers + for (var i = 0; i < evt.escape.length; ++i) { + next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); + } + // send the character event + next(evt); + // redo modifiers + /* jshint shadow: true */ + for (var i = 0; i < evt.escape.length; ++i) { + next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); + } + /* jshint shadow: false */ + }; +} diff --git a/plugins/dynamix.vm.manager/include/keysym.js b/plugins/dynamix.vm.manager/include/keysym.js new file mode 100644 index 000000000..58b107c05 --- /dev/null +++ b/plugins/dynamix.vm.manager/include/keysym.js @@ -0,0 +1,378 @@ +var XK_VoidSymbol = 0xffffff, /* Void symbol */ + +XK_BackSpace = 0xff08, /* Back space, back char */ +XK_Tab = 0xff09, +XK_Linefeed = 0xff0a, /* Linefeed, LF */ +XK_Clear = 0xff0b, +XK_Return = 0xff0d, /* Return, enter */ +XK_Pause = 0xff13, /* Pause, hold */ +XK_Scroll_Lock = 0xff14, +XK_Sys_Req = 0xff15, +XK_Escape = 0xff1b, +XK_Delete = 0xffff, /* Delete, rubout */ + +/* Cursor control & motion */ + +XK_Home = 0xff50, +XK_Left = 0xff51, /* Move left, left arrow */ +XK_Up = 0xff52, /* Move up, up arrow */ +XK_Right = 0xff53, /* Move right, right arrow */ +XK_Down = 0xff54, /* Move down, down arrow */ +XK_Prior = 0xff55, /* Prior, previous */ +XK_Page_Up = 0xff55, +XK_Next = 0xff56, /* Next */ +XK_Page_Down = 0xff56, +XK_End = 0xff57, /* EOL */ +XK_Begin = 0xff58, /* BOL */ + + +/* Misc functions */ + +XK_Select = 0xff60, /* Select, mark */ +XK_Print = 0xff61, +XK_Execute = 0xff62, /* Execute, run, do */ +XK_Insert = 0xff63, /* Insert, insert here */ +XK_Undo = 0xff65, +XK_Redo = 0xff66, /* Redo, again */ +XK_Menu = 0xff67, +XK_Find = 0xff68, /* Find, search */ +XK_Cancel = 0xff69, /* Cancel, stop, abort, exit */ +XK_Help = 0xff6a, /* Help */ +XK_Break = 0xff6b, +XK_Mode_switch = 0xff7e, /* Character set switch */ +XK_script_switch = 0xff7e, /* Alias for mode_switch */ +XK_Num_Lock = 0xff7f, + +/* Keypad functions, keypad numbers cleverly chosen to map to ASCII */ + +XK_KP_Space = 0xff80, /* Space */ +XK_KP_Tab = 0xff89, +XK_KP_Enter = 0xff8d, /* Enter */ +XK_KP_F1 = 0xff91, /* PF1, KP_A, ... */ +XK_KP_F2 = 0xff92, +XK_KP_F3 = 0xff93, +XK_KP_F4 = 0xff94, +XK_KP_Home = 0xff95, +XK_KP_Left = 0xff96, +XK_KP_Up = 0xff97, +XK_KP_Right = 0xff98, +XK_KP_Down = 0xff99, +XK_KP_Prior = 0xff9a, +XK_KP_Page_Up = 0xff9a, +XK_KP_Next = 0xff9b, +XK_KP_Page_Down = 0xff9b, +XK_KP_End = 0xff9c, +XK_KP_Begin = 0xff9d, +XK_KP_Insert = 0xff9e, +XK_KP_Delete = 0xff9f, +XK_KP_Equal = 0xffbd, /* Equals */ +XK_KP_Multiply = 0xffaa, +XK_KP_Add = 0xffab, +XK_KP_Separator = 0xffac, /* Separator, often comma */ +XK_KP_Subtract = 0xffad, +XK_KP_Decimal = 0xffae, +XK_KP_Divide = 0xffaf, + +XK_KP_0 = 0xffb0, +XK_KP_1 = 0xffb1, +XK_KP_2 = 0xffb2, +XK_KP_3 = 0xffb3, +XK_KP_4 = 0xffb4, +XK_KP_5 = 0xffb5, +XK_KP_6 = 0xffb6, +XK_KP_7 = 0xffb7, +XK_KP_8 = 0xffb8, +XK_KP_9 = 0xffb9, + +/* + * Auxiliary functions; note the duplicate definitions for left and right + * function keys; Sun keyboards and a few other manufacturers have such + * function key groups on the left and/or right sides of the keyboard. + * We've not found a keyboard with more than 35 function keys total. + */ + +XK_F1 = 0xffbe, +XK_F2 = 0xffbf, +XK_F3 = 0xffc0, +XK_F4 = 0xffc1, +XK_F5 = 0xffc2, +XK_F6 = 0xffc3, +XK_F7 = 0xffc4, +XK_F8 = 0xffc5, +XK_F9 = 0xffc6, +XK_F10 = 0xffc7, +XK_F11 = 0xffc8, +XK_L1 = 0xffc8, +XK_F12 = 0xffc9, +XK_L2 = 0xffc9, +XK_F13 = 0xffca, +XK_L3 = 0xffca, +XK_F14 = 0xffcb, +XK_L4 = 0xffcb, +XK_F15 = 0xffcc, +XK_L5 = 0xffcc, +XK_F16 = 0xffcd, +XK_L6 = 0xffcd, +XK_F17 = 0xffce, +XK_L7 = 0xffce, +XK_F18 = 0xffcf, +XK_L8 = 0xffcf, +XK_F19 = 0xffd0, +XK_L9 = 0xffd0, +XK_F20 = 0xffd1, +XK_L10 = 0xffd1, +XK_F21 = 0xffd2, +XK_R1 = 0xffd2, +XK_F22 = 0xffd3, +XK_R2 = 0xffd3, +XK_F23 = 0xffd4, +XK_R3 = 0xffd4, +XK_F24 = 0xffd5, +XK_R4 = 0xffd5, +XK_F25 = 0xffd6, +XK_R5 = 0xffd6, +XK_F26 = 0xffd7, +XK_R6 = 0xffd7, +XK_F27 = 0xffd8, +XK_R7 = 0xffd8, +XK_F28 = 0xffd9, +XK_R8 = 0xffd9, +XK_F29 = 0xffda, +XK_R9 = 0xffda, +XK_F30 = 0xffdb, +XK_R10 = 0xffdb, +XK_F31 = 0xffdc, +XK_R11 = 0xffdc, +XK_F32 = 0xffdd, +XK_R12 = 0xffdd, +XK_F33 = 0xffde, +XK_R13 = 0xffde, +XK_F34 = 0xffdf, +XK_R14 = 0xffdf, +XK_F35 = 0xffe0, +XK_R15 = 0xffe0, + +/* Modifiers */ + +XK_Shift_L = 0xffe1, /* Left shift */ +XK_Shift_R = 0xffe2, /* Right shift */ +XK_Control_L = 0xffe3, /* Left control */ +XK_Control_R = 0xffe4, /* Right control */ +XK_Caps_Lock = 0xffe5, /* Caps lock */ +XK_Shift_Lock = 0xffe6, /* Shift lock */ + +XK_Meta_L = 0xffe7, /* Left meta */ +XK_Meta_R = 0xffe8, /* Right meta */ +XK_Alt_L = 0xffe9, /* Left alt */ +XK_Alt_R = 0xffea, /* Right alt */ +XK_Super_L = 0xffeb, /* Left super */ +XK_Super_R = 0xffec, /* Right super */ +XK_Hyper_L = 0xffed, /* Left hyper */ +XK_Hyper_R = 0xffee, /* Right hyper */ + +XK_ISO_Level3_Shift = 0xfe03, /* AltGr */ + +/* + * Latin 1 + * (ISO/IEC 8859-1 = Unicode U+0020..U+00FF) + * Byte 3 = 0 + */ + +XK_space = 0x0020, /* U+0020 SPACE */ +XK_exclam = 0x0021, /* U+0021 EXCLAMATION MARK */ +XK_quotedbl = 0x0022, /* U+0022 QUOTATION MARK */ +XK_numbersign = 0x0023, /* U+0023 NUMBER SIGN */ +XK_dollar = 0x0024, /* U+0024 DOLLAR SIGN */ +XK_percent = 0x0025, /* U+0025 PERCENT SIGN */ +XK_ampersand = 0x0026, /* U+0026 AMPERSAND */ +XK_apostrophe = 0x0027, /* U+0027 APOSTROPHE */ +XK_quoteright = 0x0027, /* deprecated */ +XK_parenleft = 0x0028, /* U+0028 LEFT PARENTHESIS */ +XK_parenright = 0x0029, /* U+0029 RIGHT PARENTHESIS */ +XK_asterisk = 0x002a, /* U+002A ASTERISK */ +XK_plus = 0x002b, /* U+002B PLUS SIGN */ +XK_comma = 0x002c, /* U+002C COMMA */ +XK_minus = 0x002d, /* U+002D HYPHEN-MINUS */ +XK_period = 0x002e, /* U+002E FULL STOP */ +XK_slash = 0x002f, /* U+002F SOLIDUS */ +XK_0 = 0x0030, /* U+0030 DIGIT ZERO */ +XK_1 = 0x0031, /* U+0031 DIGIT ONE */ +XK_2 = 0x0032, /* U+0032 DIGIT TWO */ +XK_3 = 0x0033, /* U+0033 DIGIT THREE */ +XK_4 = 0x0034, /* U+0034 DIGIT FOUR */ +XK_5 = 0x0035, /* U+0035 DIGIT FIVE */ +XK_6 = 0x0036, /* U+0036 DIGIT SIX */ +XK_7 = 0x0037, /* U+0037 DIGIT SEVEN */ +XK_8 = 0x0038, /* U+0038 DIGIT EIGHT */ +XK_9 = 0x0039, /* U+0039 DIGIT NINE */ +XK_colon = 0x003a, /* U+003A COLON */ +XK_semicolon = 0x003b, /* U+003B SEMICOLON */ +XK_less = 0x003c, /* U+003C LESS-THAN SIGN */ +XK_equal = 0x003d, /* U+003D EQUALS SIGN */ +XK_greater = 0x003e, /* U+003E GREATER-THAN SIGN */ +XK_question = 0x003f, /* U+003F QUESTION MARK */ +XK_at = 0x0040, /* U+0040 COMMERCIAL AT */ +XK_A = 0x0041, /* U+0041 LATIN CAPITAL LETTER A */ +XK_B = 0x0042, /* U+0042 LATIN CAPITAL LETTER B */ +XK_C = 0x0043, /* U+0043 LATIN CAPITAL LETTER C */ +XK_D = 0x0044, /* U+0044 LATIN CAPITAL LETTER D */ +XK_E = 0x0045, /* U+0045 LATIN CAPITAL LETTER E */ +XK_F = 0x0046, /* U+0046 LATIN CAPITAL LETTER F */ +XK_G = 0x0047, /* U+0047 LATIN CAPITAL LETTER G */ +XK_H = 0x0048, /* U+0048 LATIN CAPITAL LETTER H */ +XK_I = 0x0049, /* U+0049 LATIN CAPITAL LETTER I */ +XK_J = 0x004a, /* U+004A LATIN CAPITAL LETTER J */ +XK_K = 0x004b, /* U+004B LATIN CAPITAL LETTER K */ +XK_L = 0x004c, /* U+004C LATIN CAPITAL LETTER L */ +XK_M = 0x004d, /* U+004D LATIN CAPITAL LETTER M */ +XK_N = 0x004e, /* U+004E LATIN CAPITAL LETTER N */ +XK_O = 0x004f, /* U+004F LATIN CAPITAL LETTER O */ +XK_P = 0x0050, /* U+0050 LATIN CAPITAL LETTER P */ +XK_Q = 0x0051, /* U+0051 LATIN CAPITAL LETTER Q */ +XK_R = 0x0052, /* U+0052 LATIN CAPITAL LETTER R */ +XK_S = 0x0053, /* U+0053 LATIN CAPITAL LETTER S */ +XK_T = 0x0054, /* U+0054 LATIN CAPITAL LETTER T */ +XK_U = 0x0055, /* U+0055 LATIN CAPITAL LETTER U */ +XK_V = 0x0056, /* U+0056 LATIN CAPITAL LETTER V */ +XK_W = 0x0057, /* U+0057 LATIN CAPITAL LETTER W */ +XK_X = 0x0058, /* U+0058 LATIN CAPITAL LETTER X */ +XK_Y = 0x0059, /* U+0059 LATIN CAPITAL LETTER Y */ +XK_Z = 0x005a, /* U+005A LATIN CAPITAL LETTER Z */ +XK_bracketleft = 0x005b, /* U+005B LEFT SQUARE BRACKET */ +XK_backslash = 0x005c, /* U+005C REVERSE SOLIDUS */ +XK_bracketright = 0x005d, /* U+005D RIGHT SQUARE BRACKET */ +XK_asciicircum = 0x005e, /* U+005E CIRCUMFLEX ACCENT */ +XK_underscore = 0x005f, /* U+005F LOW LINE */ +XK_grave = 0x0060, /* U+0060 GRAVE ACCENT */ +XK_quoteleft = 0x0060, /* deprecated */ +XK_a = 0x0061, /* U+0061 LATIN SMALL LETTER A */ +XK_b = 0x0062, /* U+0062 LATIN SMALL LETTER B */ +XK_c = 0x0063, /* U+0063 LATIN SMALL LETTER C */ +XK_d = 0x0064, /* U+0064 LATIN SMALL LETTER D */ +XK_e = 0x0065, /* U+0065 LATIN SMALL LETTER E */ +XK_f = 0x0066, /* U+0066 LATIN SMALL LETTER F */ +XK_g = 0x0067, /* U+0067 LATIN SMALL LETTER G */ +XK_h = 0x0068, /* U+0068 LATIN SMALL LETTER H */ +XK_i = 0x0069, /* U+0069 LATIN SMALL LETTER I */ +XK_j = 0x006a, /* U+006A LATIN SMALL LETTER J */ +XK_k = 0x006b, /* U+006B LATIN SMALL LETTER K */ +XK_l = 0x006c, /* U+006C LATIN SMALL LETTER L */ +XK_m = 0x006d, /* U+006D LATIN SMALL LETTER M */ +XK_n = 0x006e, /* U+006E LATIN SMALL LETTER N */ +XK_o = 0x006f, /* U+006F LATIN SMALL LETTER O */ +XK_p = 0x0070, /* U+0070 LATIN SMALL LETTER P */ +XK_q = 0x0071, /* U+0071 LATIN SMALL LETTER Q */ +XK_r = 0x0072, /* U+0072 LATIN SMALL LETTER R */ +XK_s = 0x0073, /* U+0073 LATIN SMALL LETTER S */ +XK_t = 0x0074, /* U+0074 LATIN SMALL LETTER T */ +XK_u = 0x0075, /* U+0075 LATIN SMALL LETTER U */ +XK_v = 0x0076, /* U+0076 LATIN SMALL LETTER V */ +XK_w = 0x0077, /* U+0077 LATIN SMALL LETTER W */ +XK_x = 0x0078, /* U+0078 LATIN SMALL LETTER X */ +XK_y = 0x0079, /* U+0079 LATIN SMALL LETTER Y */ +XK_z = 0x007a, /* U+007A LATIN SMALL LETTER Z */ +XK_braceleft = 0x007b, /* U+007B LEFT CURLY BRACKET */ +XK_bar = 0x007c, /* U+007C VERTICAL LINE */ +XK_braceright = 0x007d, /* U+007D RIGHT CURLY BRACKET */ +XK_asciitilde = 0x007e, /* U+007E TILDE */ + +XK_nobreakspace = 0x00a0, /* U+00A0 NO-BREAK SPACE */ +XK_exclamdown = 0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */ +XK_cent = 0x00a2, /* U+00A2 CENT SIGN */ +XK_sterling = 0x00a3, /* U+00A3 POUND SIGN */ +XK_currency = 0x00a4, /* U+00A4 CURRENCY SIGN */ +XK_yen = 0x00a5, /* U+00A5 YEN SIGN */ +XK_brokenbar = 0x00a6, /* U+00A6 BROKEN BAR */ +XK_section = 0x00a7, /* U+00A7 SECTION SIGN */ +XK_diaeresis = 0x00a8, /* U+00A8 DIAERESIS */ +XK_copyright = 0x00a9, /* U+00A9 COPYRIGHT SIGN */ +XK_ordfeminine = 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */ +XK_guillemotleft = 0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */ +XK_notsign = 0x00ac, /* U+00AC NOT SIGN */ +XK_hyphen = 0x00ad, /* U+00AD SOFT HYPHEN */ +XK_registered = 0x00ae, /* U+00AE REGISTERED SIGN */ +XK_macron = 0x00af, /* U+00AF MACRON */ +XK_degree = 0x00b0, /* U+00B0 DEGREE SIGN */ +XK_plusminus = 0x00b1, /* U+00B1 PLUS-MINUS SIGN */ +XK_twosuperior = 0x00b2, /* U+00B2 SUPERSCRIPT TWO */ +XK_threesuperior = 0x00b3, /* U+00B3 SUPERSCRIPT THREE */ +XK_acute = 0x00b4, /* U+00B4 ACUTE ACCENT */ +XK_mu = 0x00b5, /* U+00B5 MICRO SIGN */ +XK_paragraph = 0x00b6, /* U+00B6 PILCROW SIGN */ +XK_periodcentered = 0x00b7, /* U+00B7 MIDDLE DOT */ +XK_cedilla = 0x00b8, /* U+00B8 CEDILLA */ +XK_onesuperior = 0x00b9, /* U+00B9 SUPERSCRIPT ONE */ +XK_masculine = 0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */ +XK_guillemotright = 0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */ +XK_onequarter = 0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */ +XK_onehalf = 0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */ +XK_threequarters = 0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */ +XK_questiondown = 0x00bf, /* U+00BF INVERTED QUESTION MARK */ +XK_Agrave = 0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */ +XK_Aacute = 0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */ +XK_Acircumflex = 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */ +XK_Atilde = 0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */ +XK_Adiaeresis = 0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */ +XK_Aring = 0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */ +XK_AE = 0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */ +XK_Ccedilla = 0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */ +XK_Egrave = 0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */ +XK_Eacute = 0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */ +XK_Ecircumflex = 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */ +XK_Ediaeresis = 0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */ +XK_Igrave = 0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */ +XK_Iacute = 0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */ +XK_Icircumflex = 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */ +XK_Idiaeresis = 0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */ +XK_ETH = 0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */ +XK_Eth = 0x00d0, /* deprecated */ +XK_Ntilde = 0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */ +XK_Ograve = 0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */ +XK_Oacute = 0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */ +XK_Ocircumflex = 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */ +XK_Otilde = 0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */ +XK_Odiaeresis = 0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */ +XK_multiply = 0x00d7, /* U+00D7 MULTIPLICATION SIGN */ +XK_Oslash = 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ +XK_Ooblique = 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ +XK_Ugrave = 0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */ +XK_Uacute = 0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */ +XK_Ucircumflex = 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */ +XK_Udiaeresis = 0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */ +XK_Yacute = 0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */ +XK_THORN = 0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */ +XK_Thorn = 0x00de, /* deprecated */ +XK_ssharp = 0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */ +XK_agrave = 0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */ +XK_aacute = 0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */ +XK_acircumflex = 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */ +XK_atilde = 0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */ +XK_adiaeresis = 0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */ +XK_aring = 0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */ +XK_ae = 0x00e6, /* U+00E6 LATIN SMALL LETTER AE */ +XK_ccedilla = 0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */ +XK_egrave = 0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */ +XK_eacute = 0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */ +XK_ecircumflex = 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */ +XK_ediaeresis = 0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */ +XK_igrave = 0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */ +XK_iacute = 0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */ +XK_icircumflex = 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */ +XK_idiaeresis = 0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */ +XK_eth = 0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */ +XK_ntilde = 0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */ +XK_ograve = 0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */ +XK_oacute = 0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */ +XK_ocircumflex = 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */ +XK_otilde = 0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */ +XK_odiaeresis = 0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */ +XK_division = 0x00f7, /* U+00F7 DIVISION SIGN */ +XK_oslash = 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ +XK_ooblique = 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ +XK_ugrave = 0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */ +XK_uacute = 0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */ +XK_ucircumflex = 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */ +XK_udiaeresis = 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */ +XK_yacute = 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */ +XK_thorn = 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ +XK_ydiaeresis = 0x00ff; /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ diff --git a/plugins/dynamix.vm.manager/include/keysymdef.js b/plugins/dynamix.vm.manager/include/keysymdef.js new file mode 100644 index 000000000..f94445cf3 --- /dev/null +++ b/plugins/dynamix.vm.manager/include/keysymdef.js @@ -0,0 +1,15 @@ +// This file describes mappings from Unicode codepoints to the keysym values +// (and optionally, key names) expected by the RFB protocol +// How this file was generated: +// node /Users/jalf/dev/mi/novnc/utils/parse.js /opt/X11/include/X11/keysymdef.h +var keysyms = (function(){ + "use strict"; + var keynames = null; + var codepoints = {"32":32,"33":33,"34":34,"35":35,"36":36,"37":37,"38":38,"39":39,"40":40,"41":41,"42":42,"43":43,"44":44,"45":45,"46":46,"47":47,"48":48,"49":49,"50":50,"51":51,"52":52,"53":53,"54":54,"55":55,"56":56,"57":57,"58":58,"59":59,"60":60,"61":61,"62":62,"63":63,"64":64,"65":65,"66":66,"67":67,"68":68,"69":69,"70":70,"71":71,"72":72,"73":73,"74":74,"75":75,"76":76,"77":77,"78":78,"79":79,"80":80,"81":81,"82":82,"83":83,"84":84,"85":85,"86":86,"87":87,"88":88,"89":89,"90":90,"91":91,"92":92,"93":93,"94":94,"95":95,"96":96,"97":97,"98":98,"99":99,"100":100,"101":101,"102":102,"103":103,"104":104,"105":105,"106":106,"107":107,"108":108,"109":109,"110":110,"111":111,"112":112,"113":113,"114":114,"115":115,"116":116,"117":117,"118":118,"119":119,"120":120,"121":121,"122":122,"123":123,"124":124,"125":125,"126":126,"160":160,"161":161,"162":162,"163":163,"164":164,"165":165,"166":166,"167":167,"168":168,"169":169,"170":170,"171":171,"172":172,"173":173,"174":174,"175":175,"176":176,"177":177,"178":178,"179":179,"180":180,"181":181,"182":182,"183":183,"184":184,"185":185,"186":186,"187":187,"188":188,"189":189,"190":190,"191":191,"192":192,"193":193,"194":194,"195":195,"196":196,"197":197,"198":198,"199":199,"200":200,"201":201,"202":202,"203":203,"204":204,"205":205,"206":206,"207":207,"208":208,"209":209,"210":210,"211":211,"212":212,"213":213,"214":214,"215":215,"216":216,"217":217,"218":218,"219":219,"220":220,"221":221,"222":222,"223":223,"224":224,"225":225,"226":226,"227":227,"228":228,"229":229,"230":230,"231":231,"232":232,"233":233,"234":234,"235":235,"236":236,"237":237,"238":238,"239":239,"240":240,"241":241,"242":242,"243":243,"244":244,"245":245,"246":246,"247":247,"248":248,"249":249,"250":250,"251":251,"252":252,"253":253,"254":254,"255":255,"256":960,"257":992,"258":451,"259":483,"260":417,"261":433,"262":454,"263":486,"264":710,"265":742,"266":709,"267":741,"268":456,"269":488,"270":463,"271":495,"272":464,"273":496,"274":938,"275":954,"278":972,"279":1004,"280":458,"281":490,"282":460,"283":492,"284":728,"285":760,"286":683,"287":699,"288":725,"289":757,"290":939,"291":955,"292":678,"293":694,"294":673,"295":689,"296":933,"297":949,"298":975,"299":1007,"300":16777516,"301":16777517,"302":967,"303":999,"304":681,"305":697,"308":684,"309":700,"310":979,"311":1011,"312":930,"313":453,"314":485,"315":934,"316":950,"317":421,"318":437,"321":419,"322":435,"323":465,"324":497,"325":977,"326":1009,"327":466,"328":498,"330":957,"331":959,"332":978,"333":1010,"336":469,"337":501,"338":5052,"339":5053,"340":448,"341":480,"342":931,"343":947,"344":472,"345":504,"346":422,"347":438,"348":734,"349":766,"350":426,"351":442,"352":425,"353":441,"354":478,"355":510,"356":427,"357":443,"358":940,"359":956,"360":989,"361":1021,"362":990,"363":1022,"364":733,"365":765,"366":473,"367":505,"368":475,"369":507,"370":985,"371":1017,"372":16777588,"373":16777589,"374":16777590,"375":16777591,"376":5054,"377":428,"378":444,"379":431,"380":447,"381":430,"382":446,"399":16777615,"402":2294,"415":16777631,"416":16777632,"417":16777633,"431":16777647,"432":16777648,"437":16777653,"438":16777654,"439":16777655,"466":16777681,"486":16777702,"487":16777703,"601":16777817,"629":16777845,"658":16777874,"711":439,"728":418,"729":511,"731":434,"733":445,"901":1966,"902":1953,"904":1954,"905":1955,"906":1956,"908":1959,"910":1960,"911":1963,"912":1974,"913":1985,"914":1986,"915":1987,"916":1988,"917":1989,"918":1990,"919":1991,"920":1992,"921":1993,"922":1994,"923":1995,"924":1996,"925":1997,"926":1998,"927":1999,"928":2000,"929":2001,"931":2002,"932":2004,"933":2005,"934":2006,"935":2007,"936":2008,"937":2009,"938":1957,"939":1961,"940":1969,"941":1970,"942":1971,"943":1972,"944":1978,"945":2017,"946":2018,"947":2019,"948":2020,"949":2021,"950":2022,"951":2023,"952":2024,"953":2025,"954":2026,"955":2027,"956":2028,"957":2029,"958":2030,"959":2031,"960":2032,"961":2033,"962":2035,"963":2034,"964":2036,"965":2037,"966":2038,"967":2039,"968":2040,"969":2041,"970":1973,"971":1977,"972":1975,"973":1976,"974":1979,"1025":1715,"1026":1713,"1027":1714,"1028":1716,"1029":1717,"1030":1718,"1031":1719,"1032":1720,"1033":1721,"1034":1722,"1035":1723,"1036":1724,"1038":1726,"1039":1727,"1040":1761,"1041":1762,"1042":1783,"1043":1767,"1044":1764,"1045":1765,"1046":1782,"1047":1786,"1048":1769,"1049":1770,"1050":1771,"1051":1772,"1052":1773,"1053":1774,"1054":1775,"1055":1776,"1056":1778,"1057":1779,"1058":1780,"1059":1781,"1060":1766,"1061":1768,"1062":1763,"1063":1790,"1064":1787,"1065":1789,"1066":1791,"1067":1785,"1068":1784,"1069":1788,"1070":1760,"1071":1777,"1072":1729,"1073":1730,"1074":1751,"1075":1735,"1076":1732,"1077":1733,"1078":1750,"1079":1754,"1080":1737,"1081":1738,"1082":1739,"1083":1740,"1084":1741,"1085":1742,"1086":1743,"1087":1744,"1088":1746,"1089":1747,"1090":1748,"1091":1749,"1092":1734,"1093":1736,"1094":1731,"1095":1758,"1096":1755,"1097":1757,"1098":1759,"1099":1753,"1100":1752,"1101":1756,"1102":1728,"1103":1745,"1105":1699,"1106":1697,"1107":1698,"1108":1700,"1109":1701,"1110":1702,"1111":1703,"1112":1704,"1113":1705,"1114":1706,"1115":1707,"1116":1708,"1118":1710,"1119":1711,"1168":1725,"1169":1709,"1170":16778386,"1171":16778387,"1174":16778390,"1175":16778391,"1178":16778394,"1179":16778395,"1180":16778396,"1181":16778397,"1186":16778402,"1187":16778403,"1198":16778414,"1199":16778415,"1200":16778416,"1201":16778417,"1202":16778418,"1203":16778419,"1206":16778422,"1207":16778423,"1208":16778424,"1209":16778425,"1210":16778426,"1211":16778427,"1240":16778456,"1241":16778457,"1250":16778466,"1251":16778467,"1256":16778472,"1257":16778473,"1262":16778478,"1263":16778479,"1329":16778545,"1330":16778546,"1331":16778547,"1332":16778548,"1333":16778549,"1334":16778550,"1335":16778551,"1336":16778552,"1337":16778553,"1338":16778554,"1339":16778555,"1340":16778556,"1341":16778557,"1342":16778558,"1343":16778559,"1344":16778560,"1345":16778561,"1346":16778562,"1347":16778563,"1348":16778564,"1349":16778565,"1350":16778566,"1351":16778567,"1352":16778568,"1353":16778569,"1354":16778570,"1355":16778571,"1356":16778572,"1357":16778573,"1358":16778574,"1359":16778575,"1360":16778576,"1361":16778577,"1362":16778578,"1363":16778579,"1364":16778580,"1365":16778581,"1366":16778582,"1370":16778586,"1371":16778587,"1372":16778588,"1373":16778589,"1374":16778590,"1377":16778593,"1378":16778594,"1379":16778595,"1380":16778596,"1381":16778597,"1382":16778598,"1383":16778599,"1384":16778600,"1385":16778601,"1386":16778602,"1387":16778603,"1388":16778604,"1389":16778605,"1390":16778606,"1391":16778607,"1392":16778608,"1393":16778609,"1394":16778610,"1395":16778611,"1396":16778612,"1397":16778613,"1398":16778614,"1399":16778615,"1400":16778616,"1401":16778617,"1402":16778618,"1403":16778619,"1404":16778620,"1405":16778621,"1406":16778622,"1407":16778623,"1408":16778624,"1409":16778625,"1410":16778626,"1411":16778627,"1412":16778628,"1413":16778629,"1414":16778630,"1415":16778631,"1417":16778633,"1418":16778634,"1488":3296,"1489":3297,"1490":3298,"1491":3299,"1492":3300,"1493":3301,"1494":3302,"1495":3303,"1496":3304,"1497":3305,"1498":3306,"1499":3307,"1500":3308,"1501":3309,"1502":3310,"1503":3311,"1504":3312,"1505":3313,"1506":3314,"1507":3315,"1508":3316,"1509":3317,"1510":3318,"1511":3319,"1512":3320,"1513":3321,"1514":3322,"1548":1452,"1563":1467,"1567":1471,"1569":1473,"1570":1474,"1571":1475,"1572":1476,"1573":1477,"1574":1478,"1575":1479,"1576":1480,"1577":1481,"1578":1482,"1579":1483,"1580":1484,"1581":1485,"1582":1486,"1583":1487,"1584":1488,"1585":1489,"1586":1490,"1587":1491,"1588":1492,"1589":1493,"1590":1494,"1591":1495,"1592":1496,"1593":1497,"1594":1498,"1600":1504,"1601":1505,"1602":1506,"1603":1507,"1604":1508,"1605":1509,"1606":1510,"1607":1511,"1608":1512,"1609":1513,"1610":1514,"1611":1515,"1612":1516,"1613":1517,"1614":1518,"1615":1519,"1616":1520,"1617":1521,"1618":1522,"1619":16778835,"1620":16778836,"1621":16778837,"1632":16778848,"1633":16778849,"1634":16778850,"1635":16778851,"1636":16778852,"1637":16778853,"1638":16778854,"1639":16778855,"1640":16778856,"1641":16778857,"1642":16778858,"1648":16778864,"1657":16778873,"1662":16778878,"1670":16778886,"1672":16778888,"1681":16778897,"1688":16778904,"1700":16778916,"1705":16778921,"1711":16778927,"1722":16778938,"1726":16778942,"1729":16778945,"1740":16778956,"1746":16778962,"1748":16778964,"1776":16778992,"1777":16778993,"1778":16778994,"1779":16778995,"1780":16778996,"1781":16778997,"1782":16778998,"1783":16778999,"1784":16779000,"1785":16779001,"3458":16780674,"3459":16780675,"3461":16780677,"3462":16780678,"3463":16780679,"3464":16780680,"3465":16780681,"3466":16780682,"3467":16780683,"3468":16780684,"3469":16780685,"3470":16780686,"3471":16780687,"3472":16780688,"3473":16780689,"3474":16780690,"3475":16780691,"3476":16780692,"3477":16780693,"3478":16780694,"3482":16780698,"3483":16780699,"3484":16780700,"3485":16780701,"3486":16780702,"3487":16780703,"3488":16780704,"3489":16780705,"3490":16780706,"3491":16780707,"3492":16780708,"3493":16780709,"3494":16780710,"3495":16780711,"3496":16780712,"3497":16780713,"3498":16780714,"3499":16780715,"3500":16780716,"3501":16780717,"3502":16780718,"3503":16780719,"3504":16780720,"3505":16780721,"3507":16780723,"3508":16780724,"3509":16780725,"3510":16780726,"3511":16780727,"3512":16780728,"3513":16780729,"3514":16780730,"3515":16780731,"3517":16780733,"3520":16780736,"3521":16780737,"3522":16780738,"3523":16780739,"3524":16780740,"3525":16780741,"3526":16780742,"3530":16780746,"3535":16780751,"3536":16780752,"3537":16780753,"3538":16780754,"3539":16780755,"3540":16780756,"3542":16780758,"3544":16780760,"3545":16780761,"3546":16780762,"3547":16780763,"3548":16780764,"3549":16780765,"3550":16780766,"3551":16780767,"3570":16780786,"3571":16780787,"3572":16780788,"3585":3489,"3586":3490,"3587":3491,"3588":3492,"3589":3493,"3590":3494,"3591":3495,"3592":3496,"3593":3497,"3594":3498,"3595":3499,"3596":3500,"3597":3501,"3598":3502,"3599":3503,"3600":3504,"3601":3505,"3602":3506,"3603":3507,"3604":3508,"3605":3509,"3606":3510,"3607":3511,"3608":3512,"3609":3513,"3610":3514,"3611":3515,"3612":3516,"3613":3517,"3614":3518,"3615":3519,"3616":3520,"3617":3521,"3618":3522,"3619":3523,"3620":3524,"3621":3525,"3622":3526,"3623":3527,"3624":3528,"3625":3529,"3626":3530,"3627":3531,"3628":3532,"3629":3533,"3630":3534,"3631":3535,"3632":3536,"3633":3537,"3634":3538,"3635":3539,"3636":3540,"3637":3541,"3638":3542,"3639":3543,"3640":3544,"3641":3545,"3642":3546,"3647":3551,"3648":3552,"3649":3553,"3650":3554,"3651":3555,"3652":3556,"3653":3557,"3654":3558,"3655":3559,"3656":3560,"3657":3561,"3658":3562,"3659":3563,"3660":3564,"3661":3565,"3664":3568,"3665":3569,"3666":3570,"3667":3571,"3668":3572,"3669":3573,"3670":3574,"3671":3575,"3672":3576,"3673":3577,"4304":16781520,"4305":16781521,"4306":16781522,"4307":16781523,"4308":16781524,"4309":16781525,"4310":16781526,"4311":16781527,"4312":16781528,"4313":16781529,"4314":16781530,"4315":16781531,"4316":16781532,"4317":16781533,"4318":16781534,"4319":16781535,"4320":16781536,"4321":16781537,"4322":16781538,"4323":16781539,"4324":16781540,"4325":16781541,"4326":16781542,"4327":16781543,"4328":16781544,"4329":16781545,"4330":16781546,"4331":16781547,"4332":16781548,"4333":16781549,"4334":16781550,"4335":16781551,"4336":16781552,"4337":16781553,"4338":16781554,"4339":16781555,"4340":16781556,"4341":16781557,"4342":16781558,"7682":16784898,"7683":16784899,"7690":16784906,"7691":16784907,"7710":16784926,"7711":16784927,"7734":16784950,"7735":16784951,"7744":16784960,"7745":16784961,"7766":16784982,"7767":16784983,"7776":16784992,"7777":16784993,"7786":16785002,"7787":16785003,"7808":16785024,"7809":16785025,"7810":16785026,"7811":16785027,"7812":16785028,"7813":16785029,"7818":16785034,"7819":16785035,"7840":16785056,"7841":16785057,"7842":16785058,"7843":16785059,"7844":16785060,"7845":16785061,"7846":16785062,"7847":16785063,"7848":16785064,"7849":16785065,"7850":16785066,"7851":16785067,"7852":16785068,"7853":16785069,"7854":16785070,"7855":16785071,"7856":16785072,"7857":16785073,"7858":16785074,"7859":16785075,"7860":16785076,"7861":16785077,"7862":16785078,"7863":16785079,"7864":16785080,"7865":16785081,"7866":16785082,"7867":16785083,"7868":16785084,"7869":16785085,"7870":16785086,"7871":16785087,"7872":16785088,"7873":16785089,"7874":16785090,"7875":16785091,"7876":16785092,"7877":16785093,"7878":16785094,"7879":16785095,"7880":16785096,"7881":16785097,"7882":16785098,"7883":16785099,"7884":16785100,"7885":16785101,"7886":16785102,"7887":16785103,"7888":16785104,"7889":16785105,"7890":16785106,"7891":16785107,"7892":16785108,"7893":16785109,"7894":16785110,"7895":16785111,"7896":16785112,"7897":16785113,"7898":16785114,"7899":16785115,"7900":16785116,"7901":16785117,"7902":16785118,"7903":16785119,"7904":16785120,"7905":16785121,"7906":16785122,"7907":16785123,"7908":16785124,"7909":16785125,"7910":16785126,"7911":16785127,"7912":16785128,"7913":16785129,"7914":16785130,"7915":16785131,"7916":16785132,"7917":16785133,"7918":16785134,"7919":16785135,"7920":16785136,"7921":16785137,"7922":16785138,"7923":16785139,"7924":16785140,"7925":16785141,"7926":16785142,"7927":16785143,"7928":16785144,"7929":16785145,"8194":2722,"8195":2721,"8196":2723,"8197":2724,"8199":2725,"8200":2726,"8201":2727,"8202":2728,"8210":2747,"8211":2730,"8212":2729,"8213":1967,"8215":3295,"8216":2768,"8217":2769,"8218":2813,"8220":2770,"8221":2771,"8222":2814,"8224":2801,"8225":2802,"8226":2790,"8229":2735,"8230":2734,"8240":2773,"8242":2774,"8243":2775,"8248":2812,"8254":1150,"8304":16785520,"8308":16785524,"8309":16785525,"8310":16785526,"8311":16785527,"8312":16785528,"8313":16785529,"8320":16785536,"8321":16785537,"8322":16785538,"8323":16785539,"8324":16785540,"8325":16785541,"8326":16785542,"8327":16785543,"8328":16785544,"8329":16785545,"8352":16785568,"8353":16785569,"8354":16785570,"8355":16785571,"8356":16785572,"8357":16785573,"8358":16785574,"8359":16785575,"8360":16785576,"8361":3839,"8362":16785578,"8363":16785579,"8364":8364,"8453":2744,"8470":1712,"8471":2811,"8478":2772,"8482":2761,"8531":2736,"8532":2737,"8533":2738,"8534":2739,"8535":2740,"8536":2741,"8537":2742,"8538":2743,"8539":2755,"8540":2756,"8541":2757,"8542":2758,"8592":2299,"8593":2300,"8594":2301,"8595":2302,"8658":2254,"8660":2253,"8706":2287,"8709":16785925,"8711":2245,"8712":16785928,"8713":16785929,"8715":16785931,"8728":3018,"8730":2262,"8731":16785947,"8732":16785948,"8733":2241,"8734":2242,"8743":2270,"8744":2271,"8745":2268,"8746":2269,"8747":2239,"8748":16785964,"8749":16785965,"8756":2240,"8757":16785973,"8764":2248,"8771":2249,"8773":16785992,"8775":16785991,"8800":2237,"8801":2255,"8802":16786018,"8803":16786019,"8804":2236,"8805":2238,"8834":2266,"8835":2267,"8866":3068,"8867":3036,"8868":3010,"8869":3022,"8968":3027,"8970":3012,"8981":2810,"8992":2212,"8993":2213,"9109":3020,"9115":2219,"9117":2220,"9118":2221,"9120":2222,"9121":2215,"9123":2216,"9124":2217,"9126":2218,"9128":2223,"9132":2224,"9143":2209,"9146":2543,"9147":2544,"9148":2546,"9149":2547,"9225":2530,"9226":2533,"9227":2537,"9228":2531,"9229":2532,"9251":2732,"9252":2536,"9472":2211,"9474":2214,"9484":2210,"9488":2539,"9492":2541,"9496":2538,"9500":2548,"9508":2549,"9516":2551,"9524":2550,"9532":2542,"9618":2529,"9642":2791,"9643":2785,"9644":2779,"9645":2786,"9646":2783,"9647":2767,"9650":2792,"9651":2787,"9654":2781,"9655":2765,"9660":2793,"9661":2788,"9664":2780,"9665":2764,"9670":2528,"9675":2766,"9679":2782,"9702":2784,"9734":2789,"9742":2809,"9747":2762,"9756":2794,"9758":2795,"9792":2808,"9794":2807,"9827":2796,"9829":2798,"9830":2797,"9837":2806,"9839":2805,"10003":2803,"10007":2804,"10013":2777,"10016":2800,"10216":2748,"10217":2750,"10240":16787456,"10241":16787457,"10242":16787458,"10243":16787459,"10244":16787460,"10245":16787461,"10246":16787462,"10247":16787463,"10248":16787464,"10249":16787465,"10250":16787466,"10251":16787467,"10252":16787468,"10253":16787469,"10254":16787470,"10255":16787471,"10256":16787472,"10257":16787473,"10258":16787474,"10259":16787475,"10260":16787476,"10261":16787477,"10262":16787478,"10263":16787479,"10264":16787480,"10265":16787481,"10266":16787482,"10267":16787483,"10268":16787484,"10269":16787485,"10270":16787486,"10271":16787487,"10272":16787488,"10273":16787489,"10274":16787490,"10275":16787491,"10276":16787492,"10277":16787493,"10278":16787494,"10279":16787495,"10280":16787496,"10281":16787497,"10282":16787498,"10283":16787499,"10284":16787500,"10285":16787501,"10286":16787502,"10287":16787503,"10288":16787504,"10289":16787505,"10290":16787506,"10291":16787507,"10292":16787508,"10293":16787509,"10294":16787510,"10295":16787511,"10296":16787512,"10297":16787513,"10298":16787514,"10299":16787515,"10300":16787516,"10301":16787517,"10302":16787518,"10303":16787519,"10304":16787520,"10305":16787521,"10306":16787522,"10307":16787523,"10308":16787524,"10309":16787525,"10310":16787526,"10311":16787527,"10312":16787528,"10313":16787529,"10314":16787530,"10315":16787531,"10316":16787532,"10317":16787533,"10318":16787534,"10319":16787535,"10320":16787536,"10321":16787537,"10322":16787538,"10323":16787539,"10324":16787540,"10325":16787541,"10326":16787542,"10327":16787543,"10328":16787544,"10329":16787545,"10330":16787546,"10331":16787547,"10332":16787548,"10333":16787549,"10334":16787550,"10335":16787551,"10336":16787552,"10337":16787553,"10338":16787554,"10339":16787555,"10340":16787556,"10341":16787557,"10342":16787558,"10343":16787559,"10344":16787560,"10345":16787561,"10346":16787562,"10347":16787563,"10348":16787564,"10349":16787565,"10350":16787566,"10351":16787567,"10352":16787568,"10353":16787569,"10354":16787570,"10355":16787571,"10356":16787572,"10357":16787573,"10358":16787574,"10359":16787575,"10360":16787576,"10361":16787577,"10362":16787578,"10363":16787579,"10364":16787580,"10365":16787581,"10366":16787582,"10367":16787583,"10368":16787584,"10369":16787585,"10370":16787586,"10371":16787587,"10372":16787588,"10373":16787589,"10374":16787590,"10375":16787591,"10376":16787592,"10377":16787593,"10378":16787594,"10379":16787595,"10380":16787596,"10381":16787597,"10382":16787598,"10383":16787599,"10384":16787600,"10385":16787601,"10386":16787602,"10387":16787603,"10388":16787604,"10389":16787605,"10390":16787606,"10391":16787607,"10392":16787608,"10393":16787609,"10394":16787610,"10395":16787611,"10396":16787612,"10397":16787613,"10398":16787614,"10399":16787615,"10400":16787616,"10401":16787617,"10402":16787618,"10403":16787619,"10404":16787620,"10405":16787621,"10406":16787622,"10407":16787623,"10408":16787624,"10409":16787625,"10410":16787626,"10411":16787627,"10412":16787628,"10413":16787629,"10414":16787630,"10415":16787631,"10416":16787632,"10417":16787633,"10418":16787634,"10419":16787635,"10420":16787636,"10421":16787637,"10422":16787638,"10423":16787639,"10424":16787640,"10425":16787641,"10426":16787642,"10427":16787643,"10428":16787644,"10429":16787645,"10430":16787646,"10431":16787647,"10432":16787648,"10433":16787649,"10434":16787650,"10435":16787651,"10436":16787652,"10437":16787653,"10438":16787654,"10439":16787655,"10440":16787656,"10441":16787657,"10442":16787658,"10443":16787659,"10444":16787660,"10445":16787661,"10446":16787662,"10447":16787663,"10448":16787664,"10449":16787665,"10450":16787666,"10451":16787667,"10452":16787668,"10453":16787669,"10454":16787670,"10455":16787671,"10456":16787672,"10457":16787673,"10458":16787674,"10459":16787675,"10460":16787676,"10461":16787677,"10462":16787678,"10463":16787679,"10464":16787680,"10465":16787681,"10466":16787682,"10467":16787683,"10468":16787684,"10469":16787685,"10470":16787686,"10471":16787687,"10472":16787688,"10473":16787689,"10474":16787690,"10475":16787691,"10476":16787692,"10477":16787693,"10478":16787694,"10479":16787695,"10480":16787696,"10481":16787697,"10482":16787698,"10483":16787699,"10484":16787700,"10485":16787701,"10486":16787702,"10487":16787703,"10488":16787704,"10489":16787705,"10490":16787706,"10491":16787707,"10492":16787708,"10493":16787709,"10494":16787710,"10495":16787711,"12289":1188,"12290":1185,"12300":1186,"12301":1187,"12443":1246,"12444":1247,"12449":1191,"12450":1201,"12451":1192,"12452":1202,"12453":1193,"12454":1203,"12455":1194,"12456":1204,"12457":1195,"12458":1205,"12459":1206,"12461":1207,"12463":1208,"12465":1209,"12467":1210,"12469":1211,"12471":1212,"12473":1213,"12475":1214,"12477":1215,"12479":1216,"12481":1217,"12483":1199,"12484":1218,"12486":1219,"12488":1220,"12490":1221,"12491":1222,"12492":1223,"12493":1224,"12494":1225,"12495":1226,"12498":1227,"12501":1228,"12504":1229,"12507":1230,"12510":1231,"12511":1232,"12512":1233,"12513":1234,"12514":1235,"12515":1196,"12516":1236,"12517":1197,"12518":1237,"12519":1198,"12520":1238,"12521":1239,"12522":1240,"12523":1241,"12524":1242,"12525":1243,"12527":1244,"12530":1190,"12531":1245,"12539":1189,"12540":1200}; + + function lookup(k) { return k ? {keysym: k, keyname: keynames ? keynames[k] : k} : undefined; } + return { + fromUnicode : function(u) { return lookup(codepoints[u]); }, + lookup : lookup + }; +})(); diff --git a/plugins/dynamix.vm.manager/include/logo.js b/plugins/dynamix.vm.manager/include/logo.js new file mode 100644 index 000000000..befa598c1 --- /dev/null +++ b/plugins/dynamix.vm.manager/include/logo.js @@ -0,0 +1 @@ +noVNC_logo = {"width": 640, "height": 435, "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAGzCAYAAAC/y6a9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAStAAAErQBBHTWggAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7N13fBvlwQfw3522ZMm2vPdIGCFkA4GyoYyGsCmjk+7dQksHL2/H2/dtC4W2tLTlfelu2VA2lEILFCgQIHEGJCQkdjzkLdmWZGvfvX8oOkmJEy/pNO73/Xz44DtLzz2RT7qfnnXC8uXLZUxDlqfdnUYQhIP+bjbPn+5xhypzrmUf6rGzOc5cjzVduXN9/nTPyfRrMt/jzOcY05U5n3L2f95s/34LPW4m/p6FbLp/73xe+5nKnWuZs/07ZOOcnusx5nucbJU727LneuxslDmdTBxn/2NmusyEuZS7kHMxG/XP5Gf3TOVmQzY+u/PhPMnkMcSsH5WIiIiI8goDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMADSrMmynOsqEBERUQboc10Bym+SJMHv92NiYgJ+vx+CIECv18NgMCj/N5lMcDgcua4qERERzRIDIE0rEfp8Ph8kSVL2y7KMcDiMcDic9niPx4O6ujqYTCa1q1qU/H4/fD4fBEFQ9iV+NplMKC0tTfsdERHRXDAA0gFcLhcmJibS9pkrAJ1ZgBxD/D9JhhQBwt747wOBALq6uuB0OlFZWQlR5OiCuZJlGV6vF263+4CAvT+3242KigoGQSIimhcGQErT19cHr9erbFuqBSy6SETN8QKmyxnurTJ23iNhalCGLMtwu93wer2oq6uDzWZTseaFS5IkjI+Pw+PxIBqNHvB7vQWwVArwu2TI+xpjI5EIBgcH4Xa74XQ6UVZWxiBIRESzJixfvnzakf2zGfB/qAvObCcM7P+42VzE5jIZ4WCPne3Fcq4TH/Yvdz4TJ7L9mkz3HFmW0d/fr4Q/UxnQdoGIhlNECLoZyokBPc9K6HpMQjSQrHN9fT1KS0szUtf9nzefsDOf42bi7zmT3t5eTE5OKtuliwWULRbgaBVgbwWs1QIgABE/MNIhYXijDM/2eAtsgtlsRktLS8ZD4HT/3kwcYz7n+KGefzAzlZuJv2e23p/ZKne2Zc/12NkoczrZOMez9eVpLuUu5FzMRv2nK1Ot90smZOOzOx/Ok0wegwEwg8eartxCCID7h7+K5QJWfEEH0Ti38sJe4J0/xzC8MVmXhoaGA0IgA2DS0NAQxsbG9h0MWHypiNbzZu4+j4WAgX9L2HmXpLQKOhwO1NfXZ7R+DIALP8Z8j8MAOD0GwIUdZyFl5nOImuk4+Vz3XAVAdgETxsbGlPCntwBHXT338AcARgew7As6bP9dDAP/jr/ZXC4XAEzbEqh14+PjSvjTGYGln9ahes3sPgh0JqDxDBE6k4C3fxcDZMDr9cJisaC8vDyb1Z6TcDiMQCCAQCCAYDDIpYRySKfTQa/XQ6/Xo7S0lBO2iDSOAZDSJnwcdrkI0wLygyAAR31CB0GU0P9SvGnK5XJBlmWUlZUttKpFY2pqCkNDQwDi3e0rvqKDo3Xu3wLrThQQDYrYeWf8tR4eHobZbIbFYslofefC6/XC6/UiEAggFovlrB50cB6PByUlJXA6nbBarbmuDhHlAAOgxoXDYQSDQQBA+RECGk5d+OxdQQCO+lh87KDrhXgw6e/vhyzLedU6lSuSJCmhGABWf10HW/38uwCazhQRCwG7H5AgyzJcLhfa2tqg080weDPDZFnG0NAQxsfHD/idIALWOgGiulWiBBmYGpERCyZ3+f1++P1+mM1mVFRUwG63565+RKQ6BkCNS7T+CSKw5GMikKmhCAKw5CMiBBHoey4eAgcGBgBA8y2BU1NTSstY6WJhQeEvoXWdCPdbMsZ2yIhGo/D7/ap2u0ejUbhcLgQCAWWfrUFAxVECyo8SUH6EAH3uGiUJ8cla47tluLfKcG+T4euNfwEJBoNwuVwoLy9HTU1NjmtJRGphANS4RAC01gqw1mR4IKoAHPnheEtg77PJEChJEpxOZ2aPVUCmpqaUn+tPzNxrXrFUwNiO+EU9EAioFgCnpqbQ39+vLGFjbxGw4ss6mLX7J85Lgi7eyl9+hIDF7wdC48DuB5PjdcfGxmAymTT/BY1IKxgANSwQCCASia8lYsvs5NE0R3wg3hLY8/d4CEyMfdNqCEwEQNEA1ByXuQWzy49MhsnUlrhsCgaD6O3tVbqzyw4TsPJaHVv7CoCpDFi633jdoaEhGI1Gjgsk0gDerkHDUu82UdKQ3Wnoh18pomVd8nQbGhqC2+3O6jHzUSwWU8ZcVq0SoM/gddbRJkBnjv8cCoVUmYDhdruV8FdxtIBV1zH8FZR943XrT46/NxNjSGe6Ew0RFT4GQA1LDQhGFXoLD3u/iLb1yVNueHgYo6Oj2T9wHkltmXMuzWzoFkSg/PBkmYmgmS2hUAg+nw8AULVSwIqv6KCbx/JBlGP7hcBYLIa+vr4cV4qIso0BkABg2tu8ZcOiS0W0X5g87UZGRjQVAlNDmd6U+Rfd3qJeN3Dq323RJSJEDigpXAKw5GpRab0Nh8MIhUK5rRMRZRUDIKmu/SIRiy5JD4EjIyM5rFGOZCF0p962L5uLLqe2/lmqBJQ08T7EhU4Q01ulU29PSETFhwGQcqLtfBGLL0uefqOjoxgeHs5hjWguPB6P8vNs715C+a9yRfJvmTpbnYiKDwMg5UzreSIOuyJ5CrrdbobAApHaPVi1mgGwWFQcnVwLVK2Z5ESUGwyAlFMt54o4/Kr0EJhYJobyV2L5IKM9vpg1FQdTGWBvjv89U2esE1HxYQCknGs+W8QRH0qeih6PhyEwj8myrMwgN1cJqk0gInUkAiCQ/ZnkRJQ7DICUF5rOFHHkR5LdTx6PB4ODg7mtFE1LkiTlZ4a/4pM6mzv1b01ExYUBkPJG4+killydDIFjY2PK/YOJSB2iIflzNmeSE1FuMQBSXmk4RcRRHxeVlqXx8XGGQCIVMQASaQMDIOWd+pNEHPVJHYR9Z+f4+Dj6+/tzWykijUjtAmYAJCpeDICUl+reI2Dpp5MhcGJigiGQSAVsASTSBgZAylu1awUc/dlkCPR6vXC5XLwoEWURAyCRNjAAUl6rOVbAss/rlFuc+Xw+9Pf388JElCXsAibSBgZAynvVawQs/4JOuTD5fD62BBJlCZeBIdIGBkAqCFWrBCz/YnoI7OvrYwgkyjDBkFzcke8vouLFAEgFo3KFgBVf1iljlPx+P0MgUYaxC5hIG/QzP4Qof1QsE7DyKzps/kUMUjgeAnt7e9HU1ASBt6UoCtEA8MLno7muRkFoO1/Eoksy+z2ek0CItIEtgFRwnEsFrLxGB50xvj05OYne3l6OVyLNkbKQk9kCSKQNDIBUkJxLBKz8qg46U3ybIZC0SIpkvky2ABJpA7uAqWCVHyFg1dd06PhpDLEgMDU1pXQHiyK/2xQDQRDQ0tKS62rklXA4rCyKzhZAIpovBkAqaGWHCVi9LwRGA/EQ2NPTg+bmZobAImE2mw/YN9tgMtO40EwEnNmMPZ3PcQ5Wbup+KZL5gMYWQCJt4BWSCl7pYgGrr9NBb41vBwIB9PT0sDuYilJaAMxyCyDfQ0TFiwGQioKjXcDqr+tgsMW3A4EAuru7EYvFclsxogxLbwHMfPlsASTSBgZAKhqOVgGrv6GDoSS+HQwG0dPTwxBIRSX7LYBcCJpICxgAKWv8fTJklXuQ7M0C1nxDB6M9vs0QSMUmNQDKbAEkonliAKSs8eyQsfVXMcgqZ6+SJgFrvqmD0RHfDgaD7A6mopH1FkAGQCJNYACkrBrZJGPrL2NZuVAdiq1hXwgsjW+HQiH09PQgGuUdJqiwpc5uz8oYQC4DQ6QJDICUdSObZWy9LQchsF7AMd/SwVQW32YIpGKTjQAo6ACkrEDDEEhUnBgASRWjW2Vs+XksKxesQ7HWClhzvQ6m8vh2OBxmCKSCl2gFzNaXKrYCEhU/BkBSjfstGZtvjSEWVve41moBx1yvg7kivh0Oh9Hd3Y1IROU0SpQhiXGAUjQ74YzjAImKHwMgqcqzXcbmn8UQC6l7XEuVgGOu18NSFb9wRiIR9PT0MARSQVICYJZOXy4GTVT8GABJFTqdTvl57B05fv9elUOguQJY8y0dLNUMgVTYsh4A2QJIVPQYAEkVDocD1dXVyvb4Lhmbbonfv1dNZidwzLd0sNYkQ2B3dzfCYZX7pYkWINkFnJ3yuRg0UfFjACTVVFZWpoXAid0yOnIQAk3l8ZZAa238IheNRtHT08MQSAUj6wGQLYBERY8BkFR1QAjslLHp5hgik+rWw1QGHHO9DrZ6hkAqPMpi0DKystA6ZwETFT8GQFJdRUUFampqlG1vV25CoNEBrPmmDiUNyRDY3d2NUEjlwYlEc5R2NxDeDo6I5oEBkHLC6XSitrZW2fZ1y9h0UwwRv7r1MDri3cH2pvgFNRaLoaenhyGQ8lra3UCycTs4tgASFT0GQMqZ8vLy9BDYK2PjTTGEverWw1ACrP6mDvaW9BAYDAbVrQjRLGW7BVBIaQHkMjBExYkBkHKqvLwcdXV1yra/L0ch0Aas/roOjtZkCOzt7WUIpLyUFgCzsBi0jl3AREWPAZByrqysLC0ETvbL2HhjDKFxdethsAGrv6FDaXt6S2AgoPI0ZaIZZL0FkF3AREWPAZDyQllZGerr65XtyYF9IXBM3XroLcCq63QoXcwQSPkr65NAGACJih4DIOWN0tLStBA4NSTjzRtjCHrUrYfeAqy+Toeyw/ettSZJ6OnpwdTUlLoVITqI9C7gzJcvGrgQNFGxYwCkvFJaWoqGhgblAhcYjrcEBt3q1kNnAlZ9VYfyI5MhsLe3lyGQ8kL2A2DyZwZAouLEAEh5x+FwoL6+PhkCR2S8+aMoAiPqXoh0JmDltTo4j0qGwL6+PoZAyjl2ARPRQjEAUl5yOBxpLYFBN/Dmj2KYGlY5BBqBldfoUHF0ekvg5KTKq1YTpVAzAHIZGKLixABIectut6eFwNAYsPFHMUwNqhsCRQOw4is6VC6P10OWZfT19TEEUs5kfSFodgETFT0GQMprdrsdjY2NyRA4Drx5YwyT/SqHQD2w/Es6VK1MD4F+v8q3LiGCCmMA2QVMVPQYACnvlZSUoKmpSbnohSeAjTfFMOnKQQj8og5Vq5Mh0OVyMQSS6tK7gDP/PmALIFHxYwCkgmCz2dJaAsPeeAj096p7cRJ0wPIv6FB9THoI9Pl8qtaDtI2TQIhooRgAqWDYbDY0NTUp45/CPmDjj2Pw9agcAkVg2ed0qDkuGQL7+/sZAkk1qQFQ5jqARDQPDIBUUKxWKxobG5UQGPEDm34cg3ev+iHw6M/oUHtCekug16vyTYxJkzgGkIgWigGQCo7Vak1rCYxMAptujsHbqX4IXPopHepOTF6M+/v7GQIp67LeBcwxgERFjwGQCpLVakVzc7MSAqNTwKZbYpjYrXIIFICjPqFD/cnJt1J/fz8mJiZUrQdpS9oyMBwDSETzwABIBctisaSHwACw6ScxjL+bgxD4MRENpyXfTgMDAxgfH1e1HqQd2e4CFlJaALkQNFFxYgCkgmaxWNDS0gKdTgcAiAWBjp/EMLZT5VYLAVjyERGNZyTfUoODgwyBlBWcBUxEC8UASAXPbDajubk5GQJDwOafxjC2Q/0QeOSHRTSdlR4Cx8bG1K0HFT2OASSihWIApKJwQAgMAx23xuB5W/2L1xEfENF8TvKtNTQ0xBBIGZXeBcyFoIlo7hgAqWiYzWa0tLRAr4/3X0lhYPPPY3BvU/8CdviVIlrWpYdAj8ejej2oOHEZGCJaKAZAKiomkwnNzc3JEBgBtvwihtEt6l/EDnu/iLb1ybfY8PAwl4ihjMj+GEAuBE1U7BgAqeiYTKb0lsAosPWXMYx0qH8hW3SpiPYLk2+zcDiseh2o+GS9BZBdwERFjwGQipLRaDwwBP4qhuGN6l/M2i8SsegSvtUoc9ScBMJlYIiKE69KVLQSIdBgiF/N5Biw7dcxDL2hfghsO1/E4sv4dqPMSFsImmMAiWgeeEWionZACJSAt/43hsEN6l/UWs8TcdgVfMvRwqW2AMpZXgcQYAgkKka8GlHRMxgMB4TAt++IYeAV9S9qLeeKOPwqvu1o4RIhMBstgBAAQZfcZAAkKj76mR9CVPgMBgOam5vR09ODSCQCWQK2/zYGWRJRf5K6gaz5bDHt4ko0H4IgQJblrIwBBOKtgLFY/GcGQKLiw6YI0oxES6DRaAQAyDKw/fcSXC+qP8i96UwRVauEmR9IdBDJFsDshDPOBCYqbgyApCl6vR7Nzc1KCIQM7PijhL7n1Q+BqRdYorlSAmC2WgAZAImKGgMgaU4iBJpMpvgOGXjnLxJ6/8nlLqhwZHUMILgYNFGxYwAkTZouBO68U0LPMwyBVBjUbAHkWoBExYcBkDRLp9Olh0AAu+6R0P00L3aU/xIBUJbi/2Uau4CJihsDIGlaIgSazWZl37v3Sdj7JEMg5bes3w6Oi0ETFTUGQNI8nU6HpqamtBC4+0EJXY8zBFL+SrsbSJYXg2YAJCo+DIBEmD4E7nlIQucjDIGUn9S8HzADIFHxYQAk2ifRHWyxWJR9nY9K2PNXhkDKP+ldwJkPaAyARMWNAZAohSiKaGpqSguBXU9IePcBhkDKL2n3A+YYQCKaIwZAov0kQqDValX2dT8lYde9DIGUP7LeBZwSALkMDFHxYQAkmoYoimhsbEwLgT1/l7Dzbl4IKT9kOwAKBi4ETVTMGACJDkIURTQ3N8Nmsyn7ep+V8M5fJIDXQ8oxLgNDRAvBAEh0CIIgoKmpKS0E9j0nYcefGQIpt7IeADkJhKioMQASzSARAktKSpR9rhckbP+DBF4XKVe4DiARLQQDINEsCIKAxsbGtBDY/5KE7b+LMQRSTnAdQCJaCP3MDyEiIH7BbWhoQH9/P3w+HwBg4N8yZCmGpZ/UQeDXqayIRA5MN7MNJKkhaTqZCDYzHWO+x5mp3NSZuRwDSERzxQBINAeCIKC+vj4tBA6+KkOOxXD0ZxgCM02WZezZsyfX1ch78YWgZw6ic8EWQKLixssV0RwlQqDD4VD2Db0uY9vtMchcJYZygF3ARDRXbAEkmodECAQAr9cLABh+U8bWX8Ww/PM6CLpc1q6wiQag/SJ+N50Le0tmW/8ALgRNVOwYAIkWoL6+HoIgYGJiAgAwsknG1l/GsOwLurQLKM2eqAfaL2QAzDVRz4WgiYoZP2WJFqiurg6lpaXK9shmGVtvi2VlYD6RWtgFTFTcGACJMqCurg5lZWXK9uhWGVt+HsvK2CwiNTAAEhU3BkCiDKmtrU0Lge63ZGy+NYZYOIeVIponLgNDVNwYAIkyqLa2FuXl5cq2Z7uMzT+LIRbKYaWI5kFgACQqagyAGpZ6K6mgJ/Ply7Hkz7NZLLdY1NTUwOl0Kttj78jo+Kk6ITD1NSdaCHYBExU3BkANs1gsys+eHZn/gA+MJH82m80ZLz+fVVdXp4XA8V0yNt0SQzSQ3eNODSf/jloK3ZR5qQGQy8AQFR8GQA0zmUzQ6eIL1nm75Iy2UElRYGhD8qJhtVozV3iBqK6uRkVFhbI9sVtGR5ZD4NRA8meDwXDwBxLNgGMAiYobA6DGJVoB5Rgw/m7mPuRHOmREJuM/GwwGGI3GjJVdSKqqqtJDYKeMTT+OKa9NJskyMDUY/xsKgoCSkpLMH4Q0gwGQqLgxAGpctrqB+19Otv7ZbLaMlVuIqqqqUFlZqWx798rYdHPmQ2Dfc5Iy49hmsymtu0TzIRq4EDRRMWMA1LjUrtnBVyWExhZeZmgM8LyVvGBosft3f5WVlaiqqlK2fd0yNt0UQ8SfmfKDbmD3A8nQnbowdaalTh4KjTEYFK2UP23q35yIigPf1RpnsViUCRqhMWDjj2MIe+dfXiwE7PhjDPK+LCKKIrsi96moqEgPgb0yNt64sNc7YfsfkrOMs/2aC4KgnDNBT/rEEyoenneSf1e9nvc1JCo2DIAaJwgCGhsble7CqcH4bNX5dE+GxoA3fxjD6NbkhaOxsZEXjxQVFRWorq5Wtv0uGRtvWlgI7H9Jguft5GvucDiyPgM4dejAWBZmkFPujWxKtijzPUxUfBgACQaDAfX19cq2v3ffunXB2Zfh65bx+vej8PUkw0BNTQ1b/6bhdDrTQuBkv4wN34ti8FU5rdttJrIMDLwqY9c9yQu10WhMG2+YLdleQohyS4oA7m3Jv6tWJ3ERFTNdTU3N9+b75Gy0Mqi1dlm2jlOor4nRaIQoipicjDf9hcaAgX9LCIzE1wMzOwUI+31dkCVg0iVj6HUZb90hpbUalpWVobq6uqBeZzWPY7FYoNPplNc7FgSGN8pwvy3D3iTAVH7o4450yNj2KwmuFyRI0fg+o9GI5ubmrLfWCIIAnU4Hjye+enjEB7S8j98li8noNhkD/07OKK+vr59xHGAhvyf5OaVOmdksV43jFPJrMt0x2K5PisrKSgQCAfh8PgBAaDw+s7TvOUBvBapWCCg9TMDUYHzdQF+3PO19bq1WK2pra1WufeEpLy+HKIoYGhpSFtqd2C3j9f+Oof5EETVrBYg6QEj5L+wFuh6TMLEnvdVNrfCXoNfrYTAYEIlEEPbGZ33Xn8QQWCxS1/AsLS1lFzBRERKWL18+bf/NbKb9Hyq1znbZgP0fN5skPJclCQ722Nkm7rkuf7B/ufNZPiHbr8lMx/H5fBgZGUEwOIc+YMS7kp1OJ8rKypTWgunqPt8lJVKfN59vTPM5bib+njOJRqMYHh6G1zu/gYDZDH/T/XsTr4nb7cbISPx2L4IILPucDtXH8O4jhW7PwxK6HosHQEEQ0N7ePu2i4vP5nJrJQq878z1mPrTsLOSzRa3Wrkx8/uWqBTCf684WQMobdrsddrsdPp8Po6OjCAQOfesKk8mEiooKVSYfFCO9Xo/6+nqUlpZicHAQkUhkVs8zGo1wOBwoLy/PyZp/FRUVCAaD8Pl8kCXgrf+LYYVRh4rlPAcK1a57JPQ8k976xzvKEBUntgBm8FjTlVuILYD78/v9CAQCkGVZeU7i/zab7ZATPdgCODeyLMPj8SAYDCISiSAajSIajSq/NxgMsNvtcDgcqtxf+VAtgED8HrF79+5FOBwfCyAagVXX6lB+JENgIZFl4J0/SXD9Kxn+DtX6F38OWwBnwhbA2ZWbDWwBnPkYDIAZPNZ05RZDAFzIMRgAMyMajSIWi8FkMql63JkCIACEw2Hs3btXGcco6gHnUgHVawRUrhRhtKtSVZqHoBvw7JAwtEGGO2Xx9kSr9KEWcWcAnBkD4OzKzQYGwJmPwS5gogKg1+vzdiC+0WhEXV0dXC4XAECKAqNbZIxukSEIEsoOF1C1SoClSgDYMJhz0SlgfJcMzw4ZgZEDL4oWiwUNDQ15e74RUWbwHU5EC2a329He3g63242JiQllvywDYztljO3kWoGFoLy8PKvLNxFR/mAAJKKMSLQEVlRUHBAEKX+JogiLxYLS0lI4HI5cV4eIVMIASEQZlRoEJyYmEA6HD5jMQrkjCAIsFovyn9rjSokoPzAAElFWGI1GVFVVTfu7hU4gmO2A7mxMbJrrMeZ7nGyVO9uyiai4cel+IiIiIo1hACQiIiLSGAZAIiIiIo1hACQiIiLSGAZAIiIiIo1hACQiIiLSGAZAIiIiIo1hACQiIiLSGAZAIiIiIo1hACQiIiLSGAZAIiIiIo3hvYBp3iRJQigUUu4rmnp/UaPRyPuN0rQkSYLX64XP50MkEoFer1f+MxgM0Ov1sNlsEEV+PyUiyhYGQJqzWCwGj8eDsbExxGKxaR9jMBhQUVGB8vJyBsEFkGUZExMTymudGrYFQYAoiigvL4fD4chxTQ9NkiT4/X54vV5MTk5ClmXld+Fw+IDH6/V61NTUwG63q1nNopUI3YlzKPU8EkURdru9oAJ3NBpFIBBAIBBAJBJJO5/yyVw++xbyb8jlZ6zZbIbVaoXFYuFnfYERli9fPu1ZN5uT8VB/7NmezPs/bjYn0FzeKAd77GxP1Lm+Kfcvdz5v6my/JvM9Tjgchtvtxvj4+KyPZzAYUFlZibKyMqX8+X7QpT5vPh808zluJv6e8yFJEsbHx+HxeBCNRmd8vNFoREVFBRwOR0Y/hKf79861/HA4jJ6engP+HSUlOsSiMiJRGdHo9K9rSUkJampqYDAYZqzXdGaqayb+ntl6f2ai3FgshrGxMYyPjx/0yxoA6HQ6lJeXw+l0zjoIzudzaq5lppqYmMDU1JQS+ii/CIIAi8UCm80Gm80Gi8Vy0MepVZ9Uar3X86ncmY7BAJjBY01Xnw4y4QAAIABJREFUbrEEwOHhYYyOjqbtq6o0YsXyEqxaYcfKFXZUVBjwzD88ePzJEby7eyrtsXq9HnV1dbDb7QyAMxgdHZ22dbWkRIeVy+1YtdKOvr4QnvmnG5OT6Y9JBO7S0tKM1GWhATAUCqG3tzct/J1+mhPfu6Edq1amt+5FozLe3T2F//jObvzzeY+yXxRFVFZWwul0HrJe09FqAIxEIhgbG8PExAQkSTrg9xaLDtGohEgk/fmJFmWn0wmdTjenY2crAMZiMfT392NqamqaZwBmU+G0XBabmCQfcA4lVFVVobKy8oD9DIDqlTvTMRgAM3is6cothgA4Pj6O/v5+Zfvs91bg1psPR2Oj+aBl73p3Co8/OYI77xnAns4AgPjFpbW1FSaTac513b++xRoAR0ZG4Ha7le2zzqzA+y+pxupVDixeZIEoJusTCkt44V9jePzJEfzt726MjCa7Uqurq9MC03wtJACGQiH09PQoQfbYYxz47g3tOOWk8hmf+9TTo7j+27vRtTeg7HM4HKivrz9ovaajxQAYDAbR09Oj/M5gELDsaDvWrIp/eVi10oEjD7fC74/h6WfcePypETz7Tw8CgeSXCVEU0djYCKvVOutjZyMABoNBuFyutC8QTU1mnHZyOU47pRynnFyOmmrjgo9L8+f1RvHI4yO48+4BvPb6RNrvEu/Z1HODAVC9cmc6BgNgBo81XbmFHgCnpqbQ3d2tPPYzn2zEjf+zGDrd7F4/tyeC913QgXd2TgKIt1C1tbXN2LowU32LMQBOTExgYGBA2f7yF5rx/e+0p4W+g4nFZHzmiztw/4NDAOL1bmpqOuQFfDbmGwBTw58gAL/59VG4/LKaOR07HJbwy9t78T83dildxHa7XQmBs6G1ABiNRtHd3a0EptoaI+79yzKsXnXoMaKBoITnnvfg+m/vxt7ueOjW6/VobW2FXj/9UPFsB8CJiQkMDQ0p+654fw3+4xttaGudvmuRcm/3nincde8g7rpnAIND8S+kZrMZTU1NynnEAKheuTMdgwEwg8eartxCDoCRSASdnZ2IxWLQ6QTc+D+L8ZlPNs75OINDYZyzfpPSmmOz2dDc3Lyg+hZbAAwEAkqrjV4v4Kc/PhxXf3j2QQeId6F++ONv4cm/xbvqdTod2traDnoBn435BEBZlrFnzx4lhFz94Xr84qdHzLsOjz0xgo99+m2lq8lut6Ourm5W54CWAqAsy+jp6UEwGAQArFhWgnvvXI6G+tm3uPf0BnHO+k1w9YcAABaLBc3NzdPWJZsB0OPxYGRkRNn/6U804OYfHQ7OMSgMA4MhnHHORuU80uv1aGlpUXV1CAbAmY/BwRM0LUmS0rrv7vnzsnmFPyDeCvHEQyvR2BC/EE1OTmJ4eDhjdS10kUgELpcLsizD4dDjr/eumHP4AwC9XsAff7sUp58a72KNxWJKuWqamppSwl9drQn/871FCyrvgvVV+Mvvj4bRGP+48vl86O/vz9uZn7kyODiohL/zz6vC359YPafwBwDNTWY89teVqKqMd6sGAgHV36uSJKUNg7jumhbcciPDXyGpqzXhvruWw2aL9/REo9EDxpBT7jEA0rT8fj9Cofi3t+OPK8W5Z1csqLymfReWxIBtt9vNmXz7DA8PK4HpG19tUQLcfJiMIu758zIce0y8yy8QCKj+wTsxkRwH9LObD4fDsfDVptadW4m7/ng0TPtCoN/vz0m4zVdjY2Pwer0AgHPPrsCdfzgaVuvch1kAwGGLrXjkwRUoK9MrZaf+TbNtbGxMmbjyX99ehO/c0K7asSlzlh9dgj/csVQZwuL1evmZn2cYAGlak5OTys8fuLI2I2UuXmTF8WuTs1MPNqtPaxKvtV4v4MrLF/5aW606XP/1NmXb7/cvuMzZSqz3BwAXX1CNdeceOAtwvs45qwL3/GWZ8iVicnKSIXCfRPgDgE9+rGHBrWXLlpbgFz89Utn2eDyHeHTmSJKEsbExAMDSJTZc++W5DxWh/HHu2RX40X8vBhDvglXrPKLZYQCkaSVCidkk4pILqzNW7skpM0ADgcAhHqkNwWBQae04+70VqK7KzIzG9xxfqnSZhkKhQ64Bl0l+v1/591xycebOm4T3nuHEfXcug8XMEJggSZLS9VtTbcSZpy989jcAnHlauTLZKxQKTbucTKalLn+0fl1V1o9H2ffZTzUqvQAzrUdJ6mIApANEo1Hl7gzr11VmpAsv4dSTypSfGQDTW0E/dFVdxsq1WnU4dk1y5mciIGRbakvUkYfbsnKM009z4v67l8NiiXdxTk5Ooq+vT5WAko9S30eXX1Yz6xn6M7Hb9WlrNWb7/SrLstL6BwDrz2MALAaCAKxcHj+PJEliK2AeYQCkA6R2/1568dyW7pjJ6lUOZWBwauuXViVe68oKA845a2HjLPd36inJ1la1utsTQdNoFLGoPXvLdZx6cjn+eu9yZZzb1NQUXC6XJs+n1L/tB67M3JcIAGlrNmY7AEYiEaV1qKnJjBXLSrJ6PFLP6lXJLxIc+pM/GADpAKlv0LnOIpyJXi/g+OOS4wC13Aooy7Ly719ypA0GQ2anOZ52ivrd7YkAVl1thF6f3WmbJ72nDA/fvwIlJckQqMWWwMT7tbHRjKVLMtvqqmaLfeoEgfXvy9zYUcq9/e/6Q/mBAZAOkDpGQ5eFi3h5WbJLeTb3uS1WsVhMCSuJ8XqZlLpgbmJGd7YlxuLNYu3qjDhhbSkeuX8F7Pb4ORUIBDQVAmVZVlpdyzI4VCPhmJRhBGoGwCOyNHyAcmNNykLkaq0DSDNjACTKA9n4TEwts5gnSRx3bCkee3AFSkuTIbC3t1czITBBzMKnuTHlPrvZfj219vfSktSPNwbA/MEASEQFb81qBx7/60pl7bpgMIje3l7OOCTKA08+nVyLlAEwfzAAElFRWLnCjscfWgWn0wCAIZAoXzz+5MjMDyLVMQASUdFYsawETzy8EhUpITD1loZEpC6PJ4JXXkveScZm4/jOfMEASERF5eijSvDkI6uU+9mGQiGGQKIc+dszbsRi8THIBoMBZWVlMzyD1MIASERF56glNjz16CrUVKeHQC3POidS2+BQGD/7RbeyXVFRwTGAeYQBkIiK0hGHW/HUo6tQVxtfy5IhkEg9Pb1BnLN+E3a9G1+nUq/Xs/UvzzAAElHROmxxPAQmFjQPh8MMgURZtuvdKZyzfhO69ibXjmTrX/5hACSiorao3YK/PbYKjY1mAPEQ2N3dnbbwMBFlxqsbJnDu+Zvg6k8uPm+1Wtn6l4cyv3Q8EVGeaW2x4OnHVmHdhR3o6Q0iEomgp6cHTU1NMBgMua4eUcHq7Q3ihZfG8MKLY3jxpTEMDYfTfu90OlFTk9l7ylNmMAASkSY0N5njIfCizdjbHUAkEkFvby9DoEbcc98gfvLz7pkfSLM2NRVDn2v620yKooi6ujo4HI5pf0+5xwBIRJrR2GjG3x5bhfMu6kBnV0BpCWxubmYILHLjE1FlQgJll9FoRGNjI0wmU66rQofAMYBEpCkN9Sb87bFVWLzICgCIRqPo6elBOBye4ZlENB29Xg+73Y6amhq0traivb2d4a8AsAWQiDSnrjYeAtdf3IGdu6YQjUaV7mCj0Zjr6lGW2Ww2lJaWZv04c5n1KsuyKsdZSJn711EQBJjNZraeFygGQCLSpJpqI556dBXWX7wZO96ZZAjUEIPBoIxNk2U5a8uTFHsApMLGLmAi0qyqSiOefGQVjj6qBECyOzgUmn5gOxFRsWAAJCJNq6ww4ImHV2L50fEQGIvF0NvbyxBIREWNAZCINM/pNOCJh1dh5Qo7gGQIDAaDOa4ZEVF2MAASEQEoK9Pj8b+uxJrV8bFhsVgMfX19DIFEVJQYAImI9ikt1eOxB1fg2GOSIZAtgURUjBgAiYhS2O16PPrAShx/XHyZEEmS0Nvbi0AgMMMziYgKBwMgEdF+Skp0ePj+FTjxhPgN7CVJQl9fH0MgERUNBkAiomnYbDo8dN9ynHJSOYBkCJya4u3EiKjwMQASUcHo6VV3LJ7FosMD9yzH6acmQ6DL5WIIJKKCxwBIRAVj/cWb8e5udcOXxSzivruW48zTnQCSLYGTk5Oq1oOIKJMYAImoYAwOhrDuwvj9e9VkNom4985lOPu9FQDit8RyuVwMgURUsBgAiaigDA2Hse7CDux4R93wZTKKuPtPR2PduZUAkiHQ7/erWg8iokxgACSigjMyGsZ5F3Xgre3qhi+jUcRffn80zj+vCgBDIBEVLgZAyqnJyUlIkpTralABGnVHsP7izdiyTd3wZTAI+NNvl+Ki89NDoM/nU7UeREQLwQBIqjOakqfdxMQEOjs74fV6c1gjKlQeTwTnX9KBzVvUDV96vYDf37EUl11cDSAeAvv7+xkCiahgMACS6m695Qhc/402mPcFwUgkApfLhZ6eHoRCoRzXjgqFKMbPn/HxKM6/dDM2blL3S4ReL+A3tx+Fyy+rAZBsCeSXGSIqBAyApDqzScT1X2/Fm6+uxfp1lcr+yclJdHV1YWhoiN3CNKP6+nolBE5MRHHBZVvw+hsTqtZBpxNwx6+W4ANX1ir7+vv7MTGhbj2IiOaKAZByprnJjLv/tAwP378CixdZAcRbUTweD/bs2cOLKB2SxWJBU1MTdDodAMDni+Kiy7fg1Q3qnjeiKODXPz8SH/lgnbJvYGCA5y8R5TUGQMq5M0934rUXj8V/fXsRrNb4xTwajaK/vx/d3d0IBtW9+wMVDrPZnBYC/f4YLr58C15+ZVzVeoiigNt+diQ+9pF6Zd/AwADGx9WtBxHRbDEAUl4wGkVc++VmbHptrTKwHgCmpqbQ1dWFwcFBxGKxHNaQ8pXJZEoLgVNTMVx65Vb866UxVeshCPHxrZ/6eIOyb3BwkCGQiPISAyAdktpD8errTPj9HUvx5COrcNQSm7J/bGwMnZ2dvJjStBIhUK/XAwACgRgu/8BWPP+CR9V6CALwk5sOx2c/1ajsGxwcxNiYumGUiGgmDIB0gMRFFAB27MjNArcnn1iGl587Fjf94DA4HPH6xGIxDA4OYu/evQgEAjmpF+WvA0JgUMIVH9qGfzynbggEgB//8DB88XNNyvbw8DBDIBHlFQZAOoDValV+fvHl3LW46fUCPvfpRnRsWIsPXlkLQYjvDwaD2Lt3LwYGBtgtTGmMRmNaCAyGJFz14W14+hm36nX54fcX45ovNSvbw8PD8HjUD6NERNNhAKQDWCwW5ed/vZz7VouqSiNuv20J/vHUGqxYblf2j4+PY8+ePWxZoTT7h8BQWMKHPvYWnvzbqOp1+f53FuG6a1qU7ZGREYZAIsoLDIB0AIPBAIPBAADo7Q2iuyc/ZuEee4wD/3p2DW695Qg4nfH6JbqFu7q62C1MCqPRiObmZuU8DoclfOQTb+GxJ0ZUr8t3bmjHt65rVbZHRkbgdqvfIklElIoBkKaV1gqo8mzKQxFFAR//aD06XluLT1zdAFGM9wsnuoX7+/sRjUZzXEvKBwaDIS0ERiIyrv7U23jo0WHV6/If32zDDd9qU7ZHR0cxOqp+iyQRUQIDIE0rdRzgD27sQtfe/GpdKy834Gc3H45/PbsGxx1bquyfmJjAnj174PF4IMtyDmtI+WD/EBiNyvjEZ7bjwYeGVK/LN7/Wiu/e0K5su91uhkAiyhkGQJpWWVkZTCYTAGBgMITzL9kMV3/+3ad3xXI7nn1yNf73tiWorjICACRJwtDQELq6ujA5OZnjGlKuGQwGtLS0wGiMnx+xmIxPfX4H7r1/UPW6fO2aFvz3dxcp2263GyMj6ndLExExANK0RFFEU1OTcq/Vnt4gzr9kM0ZGwzmu2YEEAfjAlbXo2LAWn/9ME/T6eLdwKBRCT08PXC4XIpFIjmtJuaTX69Hc3JwWAj/7pXdw5z0DqtflK19sxo/+e7Gy7fF4GAKJSHUMgHRQRqMRDQ3Juxrs3jOFCy/dglF3foYpu12PG/9nMf79/LE45aRyZb/X60VnZyfcbje7hTVs/xAoSTK+8JV38Me/9Ktely98tgk3/+gwZdvj8WB4WP2xiUSkXQyAdEh2ux2VlZXK9lvb/Vhzwgb87o8uSFJ+hqklR9rwxMMr8YffLEVDfbwbW5IkjIyMsFtY4xIhMDG8QZaBr3xtJ373R5fqdfnMJxvx0x8frqxvOTY2xhBIRKphAKQZVVdXw+FwKNtjYxFc+/VdOPWsjXj9jYkc1uzQLr2oGhtfXYuvfqUFRmP8VA+Hw+jt7UVfXx+7hTVquhB47dd34Y7f9qlel09+rAE//8kRaSFwaEj9CSpEpD0MgDQrjY2NqK+vh06nU/Zt2erDWedtwue+tAPDI/k3NhAArFYdvvef7djw0nF47xlOZb/f70dnZydGR0fZLaxBOp0uLQQCwHXXv4tf/1+v6nW5+sP1+NXPj1SWNBofH8fgoPoTVIhIWxgAadZKS0uxaNEilJWVKftkGbjr3kGsPn4Dbr+jD9FofoapRe0WPHTfCtzz52VoaTYDAGRZxujoKDo7O+Hz+XJcQ1KbTqdDU1MTzGazsu9b/7kbv/hVj+p1+dBVdbj9F8kQODExgYEB9SeoEJF2MADSnOh0OtTV1aGtrS1tsWivN4pv3vAuTjrjDbz8Su7uHzyT895XiTdeWYvrv9EGizl++kciEbhcLvT29iIczs+WTMqO6ULgf35vD35ya7fqdbnqilr85tdLoNMxBBJR9jEA0ryYzWa0trairq5OuecqAGzfMYl1F3bg459+G/0D+bduIACYTSKu/3or3nhlLdavS05wmZycRFdXF0ZGRiBJUg5rSGpKLHmUGgL/6weduOkne1Wvy/svrcFv//coZSmjiYkJ9PerP0uZiIofAyAtSFlZGRYtWgSn05m2/8GHh7HmhA249bYehMP5Gaaam8y4+0/L8PD9K7B4UfzOJ7Isw+12o7OzE16vN8c1JLUkQmBqq/YPbuzCD2/qUr0ul15UjT/8ZikMhngI9Hq9cLlcHKtKRBnFAEgLJooiampq0N7ennYLucnJGL7z/T044dQ38NzznhzW8NDOPN2J1148Fv/17UWw2eKTXKLRKPr7+9Hd3Y1QKD9bMimzRFFEY2NjWgi88Za9+P4POlWvy4Xrq/Cn3x6thECfz4f+/n6GQCLKGAZAyhiTyYSWlhY0NDSkdQu/u3sKF12+BR+8+i309gZzWMODMxpFXPvlZmx8dS0uu7ha2T81NYWuri4MDQ2xW1gDEiEw9YvMLbd24zvf36N6Xdavq8SdfzhaWcLI5/OxJZCIMoYBkDLO4XBg0aJFqKiogJBY4AzA40+O4Jj3bMCNt+xFMJSfYaq+zoTf37EUTz26CkctsQGIdwt7PB7s2bMHExP5u+4hZYYoimhoaEgLgbfe1oPrv71b9bq875xK3PPno2HaFwL9fj9DIBFlBAMgZYUoiqiurkZ7eztsNpuyPxCU8MObunDcia/jqadHc1jDQzvpPWV4+bljcdMPDoPDEW/NjEajGBgYQHd3N4LB/GzJpMxItASmnru/+t9efP36d1Wvy1lnVuC+O5fBbEqGwL6+PoZAIloQBkDKqkS3cGNjIwwGg7J/b3cAV354Gy69cis6uwI5rOHB6fUCPvfpRnRsWIsPXlmr3K0hEAhg7969GBwcRCwWy20lKWsEQUBDQ0NaCPy/3/bhq9/YBbWz1xmnO3H/3cthscTHqE5OTqKvr4/DEoho3hgASRWJbuHKysq0buFn/+nGcSe9ju//oBOBQH6GqapKI26/bQn+8dQarFxhV/aPj4+js7MT4+P5u+4hLUwiBJaUlCj7fvsHF665bqfqIfC0U8rx4D3LYbUmQ6DLpf49jImoODAAkmpSu4VTL6jhsIRbbu3GmhM24OHHhnNYw0M79hgHXnhmDW695Qg4nfHWzFgshsHBQezduxeBQH62ZNLCCIKA+vr6tHP2D3/uxxeveQeSpG4KPPnEMjx033JltjrPOSKaLwZAUp3RaERTUxOamppgNBqV/X2uED76ibdx/iWb8c7OyRzW8OBEUcDHP1qPjtfW4hNXNyi37goGg+ju7sbAwAC7hYtQIgTa7ckW4L/cPYDPfVn9EPie48vwyAMrUFKim/nBREQHwQBIOVNSUoL29nZUVVVBFJOn4r9eGsN7TnsD//Gd3fD78zNMlZcb8LObD8e//nEM1h5bquyfmJjAnj17MDY2lsPaUTYIgoC6urq0EHjPfYP41Od3IBZTNwSuPbYUjz24UpmgREQ0VwyAlFOCIKCyshLt7e1pF9ZoVMYvb+/FqrWv4b4HhnJYw0NbsawEzzy5Gv/3yyWoroq3ZkqShKGhIXR1dWFqairHNaRMSrQEOhwOZd8Dfx3CJz+7HdGouiHwmDUOPPbgCpSWMgQS0dwxAFJeMBgMaGxsRHNzc1q38NBwGJ/6/Hacs34Ttr3tz2END04QgKuuqEXHhrX4wmeblPu4hkIh9PT0oL+/H9FoNMe1pEyqq6tLC4F/fWQYH/vU24hE1A2Bq1c58MRDK1Febpj5wUREKRgAKa/YbDa0t7ejuro6rVv41Q0TOOXMN3Hdt3ZhfDw/w5TdrseP/nsx/v38sTjlpHJlv9frRWdnJ9xuN9duKyJ1dXUoLU12/z/6xAg++sm3VA+BK5bb8cTDK1FRwRBIRLPHAEh5RxAEVFRUYNGiRWmtLLGYjDt+58Kqta/hT3cOqL4Mx2wtOdKGJx5eiT/+dika6k0A4t3CIyMj6OrqwuRkfk5wobmrra1FWVmZsv3EU6P44NXbEA6ruz7fsqUl+N3/LVX1mERU2BgAKW/p9Xo0NDSgpaUFJpNJ2e/2RPCla9/B6edsxMZN3hzW8NAuubAaG19di69d06LczzUcDqO3txculwuRSCTHNaRM2D8EPv2MG1d95C3Vb3dYyRZAIpoDBkDKe1arFe3t7aipqYFOl1z6YlOHF2ecuxFfvOYdjLrzM0xZrTp894Z2bHjpOJx1ZoWy3+fzoaenJ4c1o0yqra1FeXmy2//Zf7px5Ye3IRDknTqIKD8xAFLBKC8vR3t7e9q4K1kG/nzXAFYf/xru+J1L9eU4ZmtRuwV/vXc57v3LMrQ0mwGA4wGLTE1NTVoIfO55D6744Na8vcMNEWkbAyAVFJ1Oh7q6OrS2tsJsNiv7x8ejuO5bu3DKmW/i1Q0TOazhoa07txJvvLIW//HNNljMfPsVm5qaGjidTmX7hRfHcNlVWzE1xRBIRPmFVyAqSBaLBW1tbairq0vrFt72th/nrN+ET31+OwaHwjms4cGZTSK+dV0r3nhlLc4/ryrX1aEMq66uRkVFsrv/pX+P45IrtmJykiGQiPIHAyAVtLKyMixatCit6w0A7ntgCKuPfw23/bpX9WU5Zqu5yYy7/ng0Hrl/BQ5bbM11dSiDqqqq0kLgK6+N46L3b4HPl59LGBGR9jAAUsHT6XSora1FW1sbLBaLst/vj+GG7+7Ge057HS+8mL+3ZjvjdCdu+sFhua4GZVhVVRUqKyuV7Q1vTODC92+B18sQSES5xwBIRcNsNqO1tRX19fXQ65O3x9q5awoXXLoZH/3E2+hzhXJYQ9KaysrKtBD45kYvzr90c94uZk5E2sEASEWntLQUixYtgtPphCAIyv6HHxvGmhM24JZbuxFSeaFe0q79Q2DHZh/Ov6QDY2P5uXQREWkDAyAVJVEUUVNTg7a2NlityfF1gUAM3/9BJ9ae9Dqe+Yc7hzUkLamoqEBVVXLCz5Ztfqy/eDPcHoZAIsoNBkAqaiaTCS0tLWhoaEjrFu7sCuCyq7biig9tw97uQA5rSFrhdDpRXV2tbG9724/zLurAyGh+zlYnouKmn/khRIXP4XDAZrPB7XbD4/EoizD/7e+jeP4FD77yxWZ89ZoWrs1HWZWYrT48PAwA2L5jEuddtBmPP7QSNdXGXFZNU8bHxzE+Pp7rahQVURRhtVphtVphs9nS1mml/MSrHWmGKIqoqqpCW1sbbDabsj8YknDTT/bimPdswONPjuSwhqQF5eXlqKmpUbbf2TmJdRd2YGCQE5SocEmSBL/fj+HhYXR1dWHnzp3o7e3F1NRUrqtGB8EWQNIco9GIpqYm+Hw+DA8PIxKJj8Pq7Q3ig1e/hTNOd+LmHx7Gtfkoa8rKygAAQ0NDAIB3d09h3YUdeOLhVWioN+WyakVr8SILLru4euYH0pzJMhAKS9iyzY/e3iCAZCCcnJw84DaJlB8YAEmz7HY7SkpK4Ha74Xa7lW7h55734PhTXscXPtuEb36tFTabboaSiOaurKwMgiBgcHAQALCnM4D3XdCBpx5ZicZGdp9l2llnVuCsMytmfiDNmywDL78yjrvvHcAjj49gcjIGWZYxODiIUCiEmpqatJUZKHdEUWQXMGmbIAiorKxEe3s7SkpKlP2RiIxbb+vBmhM24MGHhnJYQypmpaWlqK2tVbb3dgdw7gUd6NnXikJUSAQBOPnEMtx+2xLs2X4i7vjVEmVs69jYGHp7exGL8ZaIuSTLMkRRhCAIDIBEAGAwGNDY2IjGxkYYjcnB+P0DIXz8M9ux7sIOvL1jMoc1pGJVWlqKuro6ZbunN4hzL+jg7HQqaFarDldeXov77lymTK6bnJxEd3d3jmumXbIsQ6fTKa2wDIBEKUpKStDW1oaqqiqIYvLt8fIr4zj5jDfwzRve5a28KOMcDkdaCOzrC+J9F3Sgs4shkArb6lUO/Ob2o5Do+Q0Gg/D5fLmtlEbp9fq0LngGQKL9CIKAiooKtLW1weFwKPujURm339GHVWs34K57B7FvyCBRRjgcDtTX1ysf0K7+EN53QQfe3c1ZlFTYLlhfhf/69iJle3R0NIe10R5BEKDTHTiWnQGQ6CAMBgMaGhrQ3NwMkyk5M3NkNIzPfWkH3rtuI7Zs5TdZyhy73Y7vreEdAAAQDUlEQVS6ujolBA4MhrDuwg7s3MUQSIXtmi814+IL4rOwA4EAl4dRiSiK04Y/gAGQaEY2mw1tbW2oqalJ6xZ+400vTj1rI665bic8vKUXZYjdbk9rCRwaDmPdhR3YzjGoVODOPy95T2y3m7fizCZBEOIzfcWDxzwGQKJZEAQBTqcT7e3tad3CkiTj93/qx6rjN+B3f3RBktgvTAtXUlKChoYGJQSOjIax/uIOvLXdn+OaEc3fyhV25Wefz4dwmLdBzIbZhD+AAZBoTvR6Perr69HS0pLWLTw2FsG1X9+FU8/aiNffmMhhDalYlJSUoLGxUQmBo+4I1l+8GVu2MQRSYVrUbkVJSbI7MhrlhLpMS4S/2ay3yABINA8Wi0XpFk4dX7Flqw9nnbcJn/vSDgyP8NstLYzNZksLgR5PBOdf0oHNWzj2lAqPIAArltlnfiDNS2Kyx2wX22YAJFqA8vJytLe3K7f2AuKr4d917yBWH78Bt9/Rh2g0N93CqbOU1Vp9P9HlMDYezUp3eEyDXeyJEJh4bcfHozj/0s3YuMmb9WNLMfVe79TuKjfH1BatZUcnF9znXUEy52AzfQ+FAZBogXQ6HWpra9Ha2gqzOXkLL683im/e8C5OOuMNvPzKuOr1GhtLXkT1enXu+pj49/t8UWzNcFel2xNBJBIPJHq9fsbxLcVk/xA4MRHFBZdtyfpwg+6e5B1Jsn2xTn3vvPzvsawei3JnF5c1yrhDzfQ95POyUBciTTKbzWhtbUVtbW3am3H7jkmsu7ADH//02+gfCKlWnyf/llxrK/XuJtlksViUn//1UmYv4vc9MKj8bLVaM1p2IbBarWkh0OeL4qLLt+DVDdkLgbveTV6ss/0lwmQyKSHztQ0TCIelrB6P1DcxEcVLLyc/F9gCuDCJVr/5fhlmACTKsLKyMrS3t6O8vDxt/4MPD2PNCRtw6209qlzcHki5h3HqhJVsymYAvPNubQdAIP7vbmpqUj7w/f4YLr58S9ZamFNba1LvlZ0NgiAorYCBoIQ3VejiJnX9/Vm30ooPMAAuxFzH+02HAZAoC3Q6HWpqatDa2poWiiYnY/jO9/fghFPfwHPPe7J2/O07JtPWjctFAHx1w0TGxj9u2epLWwJFqwEQiL/GqSFwaiqGS6/cmvHAHQpLuPf+ZOguLS3NaPnTST1/XnxZ/WETlF1PPDWi/Gy1WlX7XCo28xnvNx0GQKIsMpvNaGlpQV1dXVoX2ru7p3DR5VvwwavfQm9v8BAlzE9q658oimkX1mwSRVFpxZmcjOG+B4dmeMbs/OXuAeVng8EAg8GQkXILlcViQXNzsxICA4EYLv/AVjz/Qua+VNx4816lC9hoNKaN0cuW1PP0oUeG4ffHsn5MUkcgEMOzzyXPz6qqqhzWpnDNd7zftGVlpBQiOqTS0lK0t7fD6XSmNdk//uQIjnnPBtx4y14EQ5npFh51R9JabqqqqlSbBAIgbaHsL137Dp56emH3/XzjTS/uuT8ZJLPdFVkozGYzmpqalItBICjhig9twz+eW3gI3LLVh5//skfZVqP1D4i3CiXO1Xd2TuLyD25FIMixgIVuaiqGKz60DZOT8UBvtVphs9lyXKvCstDxftNhACRSiSiKqK6uRmtra1oXZiAo4Yc3deG4E19fcFjq2hvAe9+3Ea7++GQTs9l8wFjEbHM6nbDb42t9RaMyPvqJt/H8v+bXPfnQo8M476IO+HzxBWPNZjNbDlLsHwKDIQlXfXgbnn5m/rfZikRkfP7L76R136eG+mwSRTHtDigvvzKOD350GyeEFDCvN4oLL9uCF15MfgZUVlYe4hm0v0yM95uOrqam5nvzfXI2BnCqNSg0W8fha5L9MrNZrhrH0ev1KC0thclkQiAQgCTFL27jE1E8+PAw3tzkw7FrHCgvn1s356YOL9ZfvFkJfwDQ2NiY8da/2bwmJSUl8Pl8iMViiMVkPPr4CE4+sQyNDbPvRrzl1m589Ru7lCCi1+vR3Nw8p+4PNc6TXJ/jer0eNpsNPp8PsizHX+8nRhCNyjhmjQMGw+y/53u9UXzrP3fjmX8kA6TT6ZxVAMzU66DX66HT6TA5GR/D2tkVwDu7pnDh+VUQRU4aKCRuTwQXXLIZGzuSC5dbLBbU1NRk/djFcj3LZJfv/hgAC6DcQn5NCul1Vvs4JpNJWUA6GEyOA+zsCuD3f+pHICjhsMVWOOwzB7hn/uHGZR/Yhglv8tZKZWVlaQtUZ8psXhNBEGC1WuH1eiHLMqJRGQ89Oozu7gD0egGNjWbodQeWI0ky3t0dwA3f241f3t6bVl5zc/Ocl7PRQgAEkiHQ7/dDkiTEYjJefmUc994/iJpqI45acuhu80BQwq9u78VHPvk2XktZVsbpdKK6ujrj9Z2J2WxGNBpFKBT/MrNz1xSee8GDUFBCba1pVu8Jyp3BoTCeenoUn//yDrydMhnNYrGgsbExa4EmVTFczzLd5XvAcZcvXz7tND1Znnn23qFejNk8f7rHzeYFnm3Zh3rsbP+QcznWdOXO9fnTPSfTr8l8jzOfY0xX5nzK2f9583kjZqL+8637TMLhMIaGhpRWj1RLl9hw9lkVOPu9FVh7bCn0egG790zh9Te8eP3NCWx4fQI7dk6l3XmjpKQE9fX1C/7wmO7fO5fX3ufzweVyHbC/pESHs8+swPp1lRBEAR2bfejY7EXHFt+0A//r6+vTWqFm+3fIxjk912PM9zjzKTccDsPlciEcTr8N4fHHleK7/9mO+joT9DoBeoMAvV6AThTw8GPD+PFPujEwmL5G5Uzhbz6fUzNJLVOWZfT09KR9OYofB1i10oHz11XitFPKYTRxJFOuyXL8i+uLL43hxZfH0taPTCgrK0Ntba1qC7hn47NbrQA4l/v5Lui4DICZO9Z05TIAMgDOhd/vx9DQECKR6W+F5XDoYdALB71VVmL5mUyN2VpoAASAQCAAt9sNv3/udwaxWCyoqKg4YOIHA+DBy5VlGR6PB263e97/vtm0/GU7AAKAJEkYHx+Hx+NBLMYZwYVIEATU1tYqvRG56r0plACYqSVeZnVcBsDMHWu6chkAGQDnKnEB93q9ShfYbDgcDtTU1GT0wyMTATAhGAxidHR0VkGwpKQETqfzoOv9MQDOXG44HMbw8PC0rcoHYzab/7+9e1mO4gYDMKq+mAIvhuL9H9IuXCzMDDSbaCLL3fZc+q5zNoCTjG2STL78aknh+/fvF20cmiMAoxiCT09P4XQ69f45rEtVVefn/dIjhATg8OvWdT3rFZcCcMTP1fe6AlAA3uP379/h5eUlvLy8vFsKa9v2fCbe4XCY5HiUMQMwen19DU9PT+fNL+nnaJom/Pjx49MDYgXg5a/78+fP8Pz8HE6nUzidTu/+mi9fvoTD4RAOh8NVz1jOGYDpH39+fg6/fv0Kx+MxHI/H2f+dpF/TNOHx8TF8+/YtPD4+hq9fv/b+MyEA34vhN/fNKAJwxM/V97oCUACO5Xg8htfX13P0zfF/ilMEYN/rXvuaAvD21z2dTuHPnz/heDyGh4eHmw94XiIA+z7PvX8Pu65bxUaee76PuTY8fPQ1Xvo1CMC3ptzl+xlbqWAj3IDBGOLkeI6bPeYwxn+U1xCAa/s8c0+jShOf9Vvy91kAAgDMZMmpX0oAAgBMLB7vMudGj48IQACACa1hyTcnAAEAJrKWJd+cAAQAGNkap34pAQgAMKK1Tv1SAhAAYCRN06xmo8dHBCAAwJ3WvuSbE4AAAHfYytQvJQABAG6wtalfSgACAFxpi1O/lAAEALjQlqd+KQEIAHCBLRzvcikBCADwgRh+W5/6pQQgAECPuNy75Wf9hghAAIBM0zS7We7tIwABAP6z56lfSgACAIT9T/1SAhAAKFpVVaFt211t8viMAAQAitW27e6Xe/sIQACgOHs82uUaAhAAKEo+9YsR2HXdUl/S7AQgAFCEfJNHqdO/EAQgALBz+XJvyeEXCUAAYLfSM/2E3/8EIACwK13XvVnuFX7vCUAAYDfS5V7hN0wAAgCbF69wK+Umj3sJQABgs9K7e038LicAAYBNiuFX4k0e9xKAAMCmxLt7hd/tBCAAsAnCbzwCEABYvaZpQtvKlrH4nQQAVivu7LXBY1wCEABYnbquQ9u2wm8iAhAAWA3hNw8BCAAszgaPeQlAAGAxVVWFh4cHE7+ZCUAAYHYmfssSgADAbITfOghAAGBydV2fj3RheQIQAJhMvKvXWX7rIgABgNEJv3UTgADAaKqqcnvHBghAAOBucdpX17Xw2wABCADcTPhtkwAEAK5SVdX5OJf4c7ZFAAIAF0nDzzl+2yYAAYAPpeEXz/Hrum7hr4p7CEAAoFcMPwc4748ABADesNS7fwIQADhv5KjrWvgVQAACQMFi+MVlXuFXBgEIAAXKn+9zlEtZBCAAFCJd5nV4c9kEIADsXJz22dFLJAABYKdi9KUTPwhBAALArqRXs8XoE37kBCAA7EC6zBsnfp7vY4gABIANS8PP+X1cSgACwMbk0762bU37uIoABICN6Jv2CT9uIQABYMX6NnU4xoV7CUAAWKG+s/tM+xiLAASAlcif7TPtYyoCEAAWlC7xptFn2seUBCAALCCd9qVn98EcBCAAzCQNvhCCaR+LEYAAMKG+6HM9G0sTgAAwsrikm0720nP7uq5b8KsDAQgAo4nhl/48nt0HayIAAeAOQ8/1uaWDNROAAHClPPriYc2e7WMrBCAAXGAo+uziZYsEIAAMMOljrwQgACTy6AshnKPPQc3shQAEoHh90VfXdWjb9t3HYQ8EIABFSq9gGzqvD/ZKAAJQjDz4uq47P89nMwclEYAA7FrflC+NPiiRAARgdz6LvvTjrmWjRAIQgM1LN3Hk0edWDnhPAAKwSekmjvRj6ZRP9EE/AQjAZsRJXh52zumD6whAAFYrX9qN4Ref53MjB9xGAAKwKukGjnTSV1WVM/pgJAIQgEXld+3mP3dGH4xPAAIwu74pX3oo89CzfsA4BCAAkxta1g0hvIs+YHoCEIDRDd2zG8LwBg4HMsN8BCAAd0t36fbtyo3HtJjywToIQABuEmNu6CiWpmls3oCVEoAAXOSj5/jix9zAAdsgAAHolQZfPuFLl3xN+WB7BCAAIYT3E778EOY0BgUfbJsABChUumnjs926wL4IQICCpLdqpGGX37qRcjwL7I8ABNixvp26+a8t6UJ5BCDATgw9p5du1kgjECiXAATYqDz2YtSlv47RZxkXSLXpG0PXdaGqKm8UACuTX60W37vzjRyWc4FLtCGEN8sE8ceu696EIQDzyXfnpu/PYg+41+AScN9J7qIQYBpp1KVhF3fkOmwZGNNVzwDmUSgGAa7XN8WLP6ZXqcX3W++xwNju2gSSLx2HIAoBUvlze0PRBzCn0XcB58sW0d+/f0MIwhDYv/xQ5aE7deNjNafTacGvtl9J79WlfK+lfJ8hlPW93uofobzfbYnRxloAAAAASUVORK5CYII="}; diff --git a/plugins/dynamix.vm.manager/include/playback.js b/plugins/dynamix.vm.manager/include/playback.js new file mode 100644 index 000000000..203576f61 --- /dev/null +++ b/plugins/dynamix.vm.manager/include/playback.js @@ -0,0 +1,120 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + */ + +"use strict"; +/*jslint browser: true, white: false */ +/*global Util, VNC_frame_data, finish */ + +var rfb, mode, test_state, frame_idx, frame_length, + iteration, iterations, istart_time, + + // Pre-declarations for jslint + send_array, next_iteration, queue_next_packet, do_packet, enable_test_mode; + +// Override send_array +send_array = function (arr) { + // Stub out send_array +}; + +enable_test_mode = function () { + rfb._sock._mode = VNC_frame_encoding; + rfb._sock.send = send_array; + rfb._sock.close = function () {}; + rfb._sock.flush = function () {}; + rfb._checkEvents = function () {}; + rfb.connect = function (host, port, password, path) { + this._rfb_host = host; + this._rfb_port = port; + this._rfb_password = (password !== undefined) ? password : ""; + this._rfb_path = (path !== undefined) ? path : ""; + this._sock.init('binary', 'ws'); + this._updateState('ProtocolVersion', "Starting VNC handshake"); + }; +}; + +next_iteration = function () { + rfb = new RFB({'target': $D('VNC_canvas'), + 'onUpdateState': updateState}); + enable_test_mode(); + + if (iteration === 0) { + frame_length = VNC_frame_data.length; + test_state = 'running'; + } + + if (test_state !== 'running') { return; } + + iteration += 1; + if (iteration > iterations) { + finish(); + return; + } + + frame_idx = 0; + istart_time = (new Date()).getTime(); + rfb.connect('test', 0, "bogus"); + + queue_next_packet(); + +}; + +queue_next_packet = function () { + var frame, foffset, toffset, delay; + if (test_state !== 'running') { return; } + + frame = VNC_frame_data[frame_idx]; + while ((frame_idx < frame_length) && (frame.charAt(0) === "}")) { + //Util.Debug("Send frame " + frame_idx); + frame_idx += 1; + frame = VNC_frame_data[frame_idx]; + } + + if (frame === 'EOF') { + Util.Debug("Finished, found EOF"); + next_iteration(); + return; + } + if (frame_idx >= frame_length) { + Util.Debug("Finished, no more frames"); + next_iteration(); + return; + } + + if (mode === 'realtime') { + foffset = frame.slice(1, frame.indexOf('{', 1)); + toffset = (new Date()).getTime() - istart_time; + delay = foffset - toffset; + if (delay < 1) { + delay = 1; + } + + setTimeout(do_packet, delay); + } else { + setTimeout(do_packet, 1); + } +}; + +var bytes_processed = 0; + +do_packet = function () { + //Util.Debug("Processing frame: " + frame_idx); + var frame = VNC_frame_data[frame_idx], + start = frame.indexOf('{', 1) + 1; + bytes_processed += frame.length - start; + if (VNC_frame_encoding === 'binary') { + var u8 = new Uint8Array(frame.length - start); + for (var i = 0; i < frame.length - start; i++) { + u8[i] = frame.charCodeAt(start + i); + } + rfb._sock._recv_message({'data' : u8}); + } else { + rfb._sock._recv_message({'data' : frame.slice(start)}); + } + frame_idx += 1; + + queue_next_packet(); +}; + diff --git a/plugins/dynamix.vm.manager/include/rfb.js b/plugins/dynamix.vm.manager/include/rfb.js new file mode 100644 index 000000000..b45537c7b --- /dev/null +++ b/plugins/dynamix.vm.manager/include/rfb.js @@ -0,0 +1,2111 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + * TIGHT decoder portion: + * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) + */ + +/*jslint white: false, browser: true */ +/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */ + +var RFB; + +(function () { + "use strict"; + RFB = function (defaults) { + if (!defaults) { + defaults = {}; + } + + this._rfb_host = ''; + this._rfb_port = 5900; + this._rfb_password = ''; + this._rfb_path = ''; + + this._rfb_state = 'disconnected'; + this._rfb_version = 0; + this._rfb_max_version = 3.8; + this._rfb_auth_scheme = ''; + + this._rfb_tightvnc = false; + this._rfb_xvp_ver = 0; + + // In preference order + this._encodings = [ + ['COPYRECT', 0x01 ], + ['TIGHT', 0x07 ], + ['TIGHT_PNG', -260 ], + ['HEXTILE', 0x05 ], + ['RRE', 0x02 ], + ['RAW', 0x00 ], + ['DesktopSize', -223 ], + ['Cursor', -239 ], + + // Psuedo-encoding settings + //['JPEG_quality_lo', -32 ], + ['JPEG_quality_med', -26 ], + //['JPEG_quality_hi', -23 ], + //['compress_lo', -255 ], + ['compress_hi', -247 ], + ['last_rect', -224 ], + ['xvp', -309 ], + ['ExtendedDesktopSize', -308 ] + ]; + + this._encHandlers = {}; + this._encNames = {}; + this._encStats = {}; + + this._sock = null; // Websock object + this._display = null; // Display object + this._keyboard = null; // Keyboard input handler object + this._mouse = null; // Mouse input handler object + this._sendTimer = null; // Send Queue check timer + this._disconnTimer = null; // disconnection timer + this._msgTimer = null; // queued handle_msg timer + + // Frame buffer update state + this._FBU = { + rects: 0, + subrects: 0, // RRE + lines: 0, // RAW + tiles: 0, // HEXTILE + bytes: 0, + x: 0, + y: 0, + width: 0, + height: 0, + encoding: 0, + subencoding: -1, + background: null, + zlib: [] // TIGHT zlib streams + }; + + this._fb_Bpp = 4; + this._fb_depth = 3; + this._fb_width = 0; + this._fb_height = 0; + this._fb_name = ""; + + this._destBuff = null; + this._paletteBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) + + this._rre_chunk_sz = 100; + + this._timing = { + last_fbu: 0, + fbu_total: 0, + fbu_total_cnt: 0, + full_fbu_total: 0, + full_fbu_cnt: 0, + + fbu_rt_start: 0, + fbu_rt_total: 0, + fbu_rt_cnt: 0, + pixels: 0 + }; + + this._supportsSetDesktopSize = false; + this._screen_id = 0; + this._screen_flags = 0; + + // Mouse state + this._mouse_buttonMask = 0; + this._mouse_arr = []; + this._viewportDragging = false; + this._viewportDragPos = {}; + + // set the default value on user-facing properties + Util.set_defaults(this, defaults, { + 'target': 'null', // VNC display rendering Canvas object + 'focusContainer': document, // DOM element that captures keyboard input + 'encrypt': false, // Use TLS/SSL/wss encryption + 'true_color': true, // Request true color pixel data + 'local_cursor': false, // Request locally rendered cursor + 'shared': true, // Request shared mode + 'view_only': false, // Disable client mouse/keyboard + 'xvp_password_sep': '@', // Separator for XVP password fields + 'disconnectTimeout': 3, // Time (s) to wait for disconnection + 'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection + 'repeaterID': '', // [UltraVNC] RepeaterID to connect to + 'viewportDrag': false, // Move the viewport on mouse drags + + // Callback functions + 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change + 'onPasswordRequired': function () { }, // onPasswordRequired(rfb): VNC password is required + 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received + 'onBell': function () { }, // onBell(rfb): RFB Bell message received + 'onFBUReceive': function () { }, // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed + 'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed + 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized + 'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received + 'onXvpInit': function () { }, // onXvpInit(version): XVP extensions active for this connection + }); + + // main setup + Util.Debug(">> RFB.constructor"); + + // populate encHandlers with bound versions + Object.keys(RFB.encodingHandlers).forEach(function (encName) { + this._encHandlers[encName] = RFB.encodingHandlers[encName].bind(this); + }.bind(this)); + + // Create lookup tables based on encoding number + for (var i = 0; i < this._encodings.length; i++) { + this._encHandlers[this._encodings[i][1]] = this._encHandlers[this._encodings[i][0]]; + this._encNames[this._encodings[i][1]] = this._encodings[i][0]; + this._encStats[this._encodings[i][1]] = [0, 0]; + } + + // NB: nothing that needs explicit teardown should be done + // before this point, since this can throw an exception + try { + this._display = new Display({target: this._target}); + } catch (exc) { + Util.Error("Display exception: " + exc); + throw exc; + } + + this._keyboard = new Keyboard({target: this._focusContainer, + onKeyPress: this._handleKeyPress.bind(this)}); + + this._mouse = new Mouse({target: this._target, + onMouseButton: this._handleMouseButton.bind(this), + onMouseMove: this._handleMouseMove.bind(this), + notify: this._keyboard.sync.bind(this._keyboard)}); + + this._sock = new Websock(); + this._sock.on('message', this._handle_message.bind(this)); + this._sock.on('open', function () { + if (this._rfb_state === 'connect') { + this._updateState('ProtocolVersion', "Starting VNC handshake"); + } else { + this._fail("Got unexpected WebSocket connection"); + } + }.bind(this)); + this._sock.on('close', function (e) { + Util.Warn("WebSocket on-close event"); + var msg = ""; + if (e.code) { + msg = " (code: " + e.code; + if (e.reason) { + msg += ", reason: " + e.reason; + } + msg += ")"; + } + if (this._rfb_state === 'disconnect') { + this._updateState('disconnected', 'VNC disconnected' + msg); + } else if (this._rfb_state === 'ProtocolVersion') { + this._fail('Failed to connect to server' + msg); + } else if (this._rfb_state in {'failed': 1, 'disconnected': 1}) { + Util.Error("Received onclose while disconnected" + msg); + } else { + this._fail("Server disconnected" + msg); + } + this._sock.off('close'); + }.bind(this)); + this._sock.on('error', function (e) { + Util.Warn("WebSocket on-error event"); + }); + + this._init_vars(); + + var rmode = this._display.get_render_mode(); + if (Websock_native) { + Util.Info("Using native WebSockets"); + this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); + } else { + this._cleanupSocket('fatal'); + throw new Error("WebSocket support is required to use noVNC"); + } + + Util.Debug("<< RFB.constructor"); + }; + + RFB.prototype = { + // Public methods + connect: function (host, port, password, path) { + this._rfb_host = host; + this._rfb_port = port; + this._rfb_password = (password !== undefined) ? password : ""; + this._rfb_path = (path !== undefined) ? path : ""; + + if (!this._rfb_host || !this._rfb_port) { + return this._fail("Must set host and port"); + } + + this._updateState('connect'); + }, + + disconnect: function () { + this._updateState('disconnect', 'Disconnecting'); + this._sock.off('error'); + this._sock.off('message'); + this._sock.off('open'); + }, + + sendPassword: function (passwd) { + this._rfb_password = passwd; + this._rfb_state = 'Authentication'; + setTimeout(this._init_msg.bind(this), 1); + }, + + sendCtrlAltDel: function () { + if (this._rfb_state !== 'normal' || this._view_only) { return false; } + Util.Info("Sending Ctrl-Alt-Del"); + + RFB.messages.keyEvent(this._sock, XK_Control_L, 1); + RFB.messages.keyEvent(this._sock, XK_Alt_L, 1); + RFB.messages.keyEvent(this._sock, XK_Delete, 1); + RFB.messages.keyEvent(this._sock, XK_Delete, 0); + RFB.messages.keyEvent(this._sock, XK_Alt_L, 0); + RFB.messages.keyEvent(this._sock, XK_Control_L, 0); + + this._sock.flush(); + }, + + xvpOp: function (ver, op) { + if (this._rfb_xvp_ver < ver) { return false; } + Util.Info("Sending XVP operation " + op + " (version " + ver + ")"); + this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op)); + return true; + }, + + xvpShutdown: function () { + return this.xvpOp(1, 2); + }, + + xvpReboot: function () { + return this.xvpOp(1, 3); + }, + + xvpReset: function () { + return this.xvpOp(1, 4); + }, + + // Send a key press. If 'down' is not specified then send a down key + // followed by an up key. + sendKey: function (code, down) { + if (this._rfb_state !== "normal" || this._view_only) { return false; } + if (typeof down !== 'undefined') { + Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); + RFB.messages.keyEvent(this._sock, code, down ? 1 : 0); + } else { + Util.Info("Sending key code (down + up): " + code); + RFB.messages.keyEvent(this._sock, code, 1); + RFB.messages.keyEvent(this._sock, code, 0); + } + + this._sock.flush(); + }, + + clipboardPasteFrom: function (text) { + if (this._rfb_state !== 'normal') { return; } + RFB.messages.clientCutText(this._sock, text); + this._sock.flush(); + }, + + setDesktopSize: function (width, height) { + if (this._rfb_state !== "normal") { return; } + + if (this._supportsSetDesktopSize) { + + var arr = [251]; // msg-type + arr.push8(0); // padding + arr.push16(width); // width + arr.push16(height); // height + + arr.push8(1); // number-of-screens + arr.push8(0); // padding + + // screen array + arr.push32(this._screen_id); // id + arr.push16(0); // x-position + arr.push16(0); // y-position + arr.push16(width); // width + arr.push16(height); // height + arr.push32(this._screen_flags); // flags + + this._sock.send(arr); + } + }, + + + // Private methods + + _connect: function () { + Util.Debug(">> RFB.connect"); + + var uri; + if (typeof UsingSocketIO !== 'undefined') { + uri = 'http'; + } else { + uri = this._encrypt ? 'wss' : 'ws'; + } + + uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path; + Util.Info("connecting to " + uri); + + this._sock.open(uri, this._wsProtocols); + + Util.Debug("<< RFB.connect"); + }, + + _init_vars: function () { + // reset state + this._FBU.rects = 0; + this._FBU.subrects = 0; // RRE and HEXTILE + this._FBU.lines = 0; // RAW + this._FBU.tiles = 0; // HEXTILE + this._FBU.zlibs = []; // TIGHT zlib encoders + this._mouse_buttonMask = 0; + this._mouse_arr = []; + this._rfb_tightvnc = false; + + // Clear the per connection encoding stats + var i; + for (i = 0; i < this._encodings.length; i++) { + this._encStats[this._encodings[i][1]][0] = 0; + } + + for (i = 0; i < 4; i++) { + //this._FBU.zlibs[i] = new TINF(); + //this._FBU.zlibs[i].init(); + this._FBU.zlibs[i] = new inflator.Inflate(); + } + }, + + _print_stats: function () { + Util.Info("Encoding stats for this connection:"); + var i, s; + for (i = 0; i < this._encodings.length; i++) { + s = this._encStats[this._encodings[i][1]]; + if (s[0] + s[1] > 0) { + Util.Info(" " + this._encodings[i][0] + ": " + s[0] + " rects"); + } + } + + Util.Info("Encoding stats since page load:"); + for (i = 0; i < this._encodings.length; i++) { + s = this._encStats[this._encodings[i][1]]; + Util.Info(" " + this._encodings[i][0] + ": " + s[1] + " rects"); + } + }, + + _cleanupSocket: function (state) { + if (this._sendTimer) { + clearInterval(this._sendTimer); + this._sendTimer = null; + } + + if (this._msgTimer) { + clearInterval(this._msgTimer); + this._msgTimer = null; + } + + if (this._display && this._display.get_context()) { + this._keyboard.ungrab(); + this._mouse.ungrab(); + if (state !== 'connect' && state !== 'loaded') { + this._display.defaultCursor(); + } + if (Util.get_logging() !== 'debug' || state === 'loaded') { + // Show noVNC logo on load and when disconnected, unless in + // debug mode + this._display.clear(); + } + } + + this._sock.close(); + }, + + /* + * Page states: + * loaded - page load, equivalent to disconnected + * disconnected - idle state + * connect - starting to connect (to ProtocolVersion) + * normal - connected + * disconnect - starting to disconnect + * failed - abnormal disconnect + * fatal - failed to load page, or fatal error + * + * RFB protocol initialization states: + * ProtocolVersion + * Security + * Authentication + * password - waiting for password, not part of RFB + * SecurityResult + * ClientInitialization - not triggered by server message + * ServerInitialization (to normal) + */ + _updateState: function (state, statusMsg) { + var oldstate = this._rfb_state; + + if (state === oldstate) { + // Already here, ignore + Util.Debug("Already in state '" + state + "', ignoring"); + } + + /* + * These are disconnected states. A previous connect may + * asynchronously cause a connection so make sure we are closed. + */ + if (state in {'disconnected': 1, 'loaded': 1, 'connect': 1, + 'disconnect': 1, 'failed': 1, 'fatal': 1}) { + this._cleanupSocket(state); + } + + if (oldstate === 'fatal') { + Util.Error('Fatal error, cannot continue'); + } + + var cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; + var fullmsg = "New state '" + state + "', was '" + oldstate + "'." + cmsg; + if (state === 'failed' || state === 'fatal') { + Util.Error(cmsg); + } else { + Util.Warn(cmsg); + } + + if (oldstate === 'failed' && state === 'disconnected') { + // do disconnect action, but stay in failed state + this._rfb_state = 'failed'; + } else { + this._rfb_state = state; + } + + if (this._disconnTimer && this._rfb_state !== 'disconnect') { + Util.Debug("Clearing disconnect timer"); + clearTimeout(this._disconnTimer); + this._disconnTimer = null; + this._sock.off('close'); // make sure we don't get a double event + } + + switch (state) { + case 'normal': + if (oldstate === 'disconnected' || oldstate === 'failed') { + Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); + } + break; + + case 'connect': + this._init_vars(); + this._connect(); + // WebSocket.onopen transitions to 'ProtocolVersion' + break; + + case 'disconnect': + this._disconnTimer = setTimeout(function () { + this._fail("Disconnect timeout"); + }.bind(this), this._disconnectTimeout * 1000); + + this._print_stats(); + + // WebSocket.onclose transitions to 'disconnected' + break; + + case 'failed': + if (oldstate === 'disconnected') { + Util.Error("Invalid transition from 'disconnected' to 'failed'"); + } else if (oldstate === 'normal') { + Util.Error("Error while connected."); + } else if (oldstate === 'init') { + Util.Error("Error while initializing."); + } + + // Make sure we transition to disconnected + setTimeout(function () { + this._updateState('disconnected'); + }.bind(this), 50); + + break; + + default: + // No state change action to take + } + + if (oldstate === 'failed' && state === 'disconnected') { + this._onUpdateState(this, state, oldstate); + } else { + this._onUpdateState(this, state, oldstate, statusMsg); + } + }, + + _fail: function (msg) { + this._updateState('failed', msg); + return false; + }, + + _handle_message: function () { + if (this._sock.rQlen() === 0) { + Util.Warn("handle_message called on an empty receive queue"); + return; + } + + switch (this._rfb_state) { + case 'disconnected': + case 'failed': + Util.Error("Got data while disconnected"); + break; + case 'normal': + if (this._normal_msg() && this._sock.rQlen() > 0) { + // true means we can continue processing + // Give other events a chance to run + if (this._msgTimer === null) { + Util.Debug("More data to process, creating timer"); + this._msgTimer = setTimeout(function () { + this._msgTimer = null; + this._handle_message(); + }.bind(this), 10); + } else { + Util.Debug("More data to process, existing timer"); + } + } + break; + default: + this._init_msg(); + break; + } + }, + + _handleKeyPress: function (keysym, down) { + if (this._view_only) { return; } // View only, skip keyboard, events + RFB.messages.keyEvent(this._sock, keysym, down); + this._sock.flush(); + }, + + _handleMouseButton: function (x, y, down, bmask) { + if (down) { + this._mouse_buttonMask |= bmask; + } else { + this._mouse_buttonMask ^= bmask; + } + + if (this._viewportDrag) { + if (down && !this._viewportDragging) { + this._viewportDragging = true; + this._viewportDragPos = {'x': x, 'y': y}; + + // Skip sending mouse events + return; + } else { + this._viewportDragging = false; + } + } + + if (this._view_only) { return; } // View only, skip mouse events + + if (this._rfb_state !== "normal") { return; } + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + }, + + _handleMouseMove: function (x, y) { + if (this._viewportDragging) { + var deltaX = this._viewportDragPos.x - x; + var deltaY = this._viewportDragPos.y - y; + this._viewportDragPos = {'x': x, 'y': y}; + + this._display.viewportChangePos(deltaX, deltaY); + + // Skip sending mouse events + return; + } + + if (this._view_only) { return; } // View only, skip mouse events + + if (this._rfb_state !== "normal") { return; } + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + }, + + // Message Handlers + + _negotiate_protocol_version: function () { + if (this._sock.rQlen() < 12) { + return this._fail("Incomplete protocol version"); + } + + var sversion = this._sock.rQshiftStr(12).substr(4, 7); + Util.Info("Server ProtocolVersion: " + sversion); + var is_repeater = 0; + switch (sversion) { + case "000.000": // UltraVNC repeater + is_repeater = 1; + break; + case "003.003": + case "003.006": // UltraVNC + case "003.889": // Apple Remote Desktop + this._rfb_version = 3.3; + break; + case "003.007": + this._rfb_version = 3.7; + break; + case "003.008": + case "004.000": // Intel AMT KVM + case "004.001": // RealVNC 4.6 + this._rfb_version = 3.8; + break; + default: + return this._fail("Invalid server version " + sversion); + } + + if (is_repeater) { + var repeaterID = this._repeaterID; + while (repeaterID.length < 250) { + repeaterID += "\0"; + } + this._sock.send_string(repeaterID); + return true; + } + + if (this._rfb_version > this._rfb_max_version) { + this._rfb_version = this._rfb_max_version; + } + + // Send updates either at a rate of 1 update per 50ms, or + // whatever slower rate the network can handle + this._sendTimer = setInterval(this._sock.flush.bind(this._sock), 50); + + var cversion = "00" + parseInt(this._rfb_version, 10) + + ".00" + ((this._rfb_version * 10) % 10); + this._sock.send_string("RFB " + cversion + "\n"); + this._updateState('Security', 'Sent ProtocolVersion: ' + cversion); + }, + + _negotiate_security: function () { + if (this._rfb_version >= 3.7) { + // Server sends supported list, client decides + var num_types = this._sock.rQshift8(); + if (this._sock.rQwait("security type", num_types, 1)) { return false; } + + if (num_types === 0) { + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Security failure: " + reason); + } + + this._rfb_auth_scheme = 0; + var types = this._sock.rQshiftBytes(num_types); + Util.Debug("Server security types: " + types); + for (var i = 0; i < types.length; i++) { + if (types[i] > this._rfb_auth_scheme && (types[i] <= 16 || types[i] == 22)) { + this._rfb_auth_scheme = types[i]; + } + } + + if (this._rfb_auth_scheme === 0) { + return this._fail("Unsupported security types: " + types); + } + + this._sock.send([this._rfb_auth_scheme]); + } else { + // Server decides + if (this._sock.rQwait("security scheme", 4)) { return false; } + this._rfb_auth_scheme = this._sock.rQshift32(); + } + + this._updateState('Authentication', 'Authenticating using scheme: ' + this._rfb_auth_scheme); + return this._init_msg(); // jump to authentication + }, + + // authentication + _negotiate_xvp_auth: function () { + var xvp_sep = this._xvp_password_sep; + var xvp_auth = this._rfb_password.split(xvp_sep); + if (xvp_auth.length < 3) { + this._updateState('password', 'XVP credentials required (user' + xvp_sep + + 'target' + xvp_sep + 'password) -- got only ' + this._rfb_password); + this._onPasswordRequired(this); + return false; + } + + var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + + String.fromCharCode(xvp_auth[1].length) + + xvp_auth[0] + + xvp_auth[1]; + this._sock.send_string(xvp_auth_str); + this._rfb_password = xvp_auth.slice(2).join(xvp_sep); + this._rfb_auth_scheme = 2; + return this._negotiate_authentication(); + }, + + _negotiate_std_vnc_auth: function () { + if (this._rfb_password.length === 0) { + // Notify via both callbacks since it's kind of + // an RFB state change and a UI interface issue + this._updateState('password', "Password Required"); + this._onPasswordRequired(this); + } + + if (this._sock.rQwait("auth challenge", 16)) { return false; } + + // TODO(directxman12): make genDES not require an Array + var challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); + var response = RFB.genDES(this._rfb_password, challenge); + this._sock.send(response); + this._updateState("SecurityResult"); + return true; + }, + + _negotiate_tight_tunnels: function (numTunnels) { + var clientSupportedTunnelTypes = { + 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } + }; + var serverSupportedTunnelTypes = {}; + // receive tunnel capabilities + for (var i = 0; i < numTunnels; i++) { + var cap_code = this._sock.rQshift32(); + var cap_vendor = this._sock.rQshiftStr(4); + var cap_signature = this._sock.rQshiftStr(8); + serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; + } + + // choose the notunnel type + if (serverSupportedTunnelTypes[0]) { + if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || + serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { + return this._fail("Client's tunnel type had the incorrect vendor or signature"); + } + this._sock.send([0, 0, 0, 0]); // use NOTUNNEL + return false; // wait until we receive the sub auth count to continue + } else { + return this._fail("Server wanted tunnels, but doesn't support the notunnel type"); + } + }, + + _negotiate_tight_auth: function () { + if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation + if (this._sock.rQwait("num tunnels", 4)) { return false; } + var numTunnels = this._sock.rQshift32(); + if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } + + this._rfb_tightvnc = true; + + if (numTunnels > 0) { + this._negotiate_tight_tunnels(numTunnels); + return false; // wait until we receive the sub auth to continue + } + } + + // second pass, do the sub-auth negotiation + if (this._sock.rQwait("sub auth count", 4)) { return false; } + var subAuthCount = this._sock.rQshift32(); + if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } + + var clientSupportedTypes = { + 'STDVNOAUTH__': 1, + 'STDVVNCAUTH_': 2 + }; + + var serverSupportedTypes = []; + + for (var i = 0; i < subAuthCount; i++) { + var capNum = this._sock.rQshift32(); + var capabilities = this._sock.rQshiftStr(12); + serverSupportedTypes.push(capabilities); + } + + for (var authType in clientSupportedTypes) { + if (serverSupportedTypes.indexOf(authType) != -1) { + this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); + + switch (authType) { + case 'STDVNOAUTH__': // no auth + this._updateState('SecurityResult'); + return true; + case 'STDVVNCAUTH_': // VNC auth + this._rfb_auth_scheme = 2; + return this._init_msg(); + default: + return this._fail("Unsupported tiny auth scheme: " + authType); + } + } + } + + this._fail("No supported sub-auth types!"); + }, + + _negotiate_authentication: function () { + switch (this._rfb_auth_scheme) { + case 0: // connection failed + if (this._sock.rQwait("auth reason", 4)) { return false; } + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Auth failure: " + reason); + + case 1: // no auth + if (this._rfb_version >= 3.8) { + this._updateState('SecurityResult'); + return true; + } + this._updateState('ClientInitialisation', "No auth required"); + return this._init_msg(); + + case 22: // XVP auth + return this._negotiate_xvp_auth(); + + case 2: // VNC authentication + return this._negotiate_std_vnc_auth(); + + case 16: // TightVNC Security Type + return this._negotiate_tight_auth(); + + default: + return this._fail("Unsupported auth scheme: " + this._rfb_auth_scheme); + } + }, + + _handle_security_result: function () { + if (this._sock.rQwait('VNC auth response ', 4)) { return false; } + switch (this._sock.rQshift32()) { + case 0: // OK + this._updateState('ClientInitialisation', 'Authentication OK'); + return this._init_msg(); + case 1: // failed + if (this._rfb_version >= 3.8) { + var length = this._sock.rQshift32(); + if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; } + var reason = this._sock.rQshiftStr(length); + return this._fail(reason); + } else { + return this._fail("Authentication failure"); + } + return false; + case 2: + return this._fail("Too many auth attempts"); + } + }, + + _negotiate_server_init: function () { + if (this._sock.rQwait("server initialization", 24)) { return false; } + + /* Screen size */ + this._fb_width = this._sock.rQshift16(); + this._fb_height = this._sock.rQshift16(); + this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); + + /* PIXEL_FORMAT */ + var bpp = this._sock.rQshift8(); + var depth = this._sock.rQshift8(); + var big_endian = this._sock.rQshift8(); + var true_color = this._sock.rQshift8(); + + var red_max = this._sock.rQshift16(); + var green_max = this._sock.rQshift16(); + var blue_max = this._sock.rQshift16(); + var red_shift = this._sock.rQshift8(); + var green_shift = this._sock.rQshift8(); + var blue_shift = this._sock.rQshift8(); + this._sock.rQskipBytes(3); // padding + + // NB(directxman12): we don't want to call any callbacks or print messages until + // *after* we're past the point where we could backtrack + + /* Connection name/title */ + var name_length = this._sock.rQshift32(); + if (this._sock.rQwait('server init name', name_length, 24)) { return false; } + this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length)); + + if (this._rfb_tightvnc) { + if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } + // In TightVNC mode, ServerInit message is extended + var numServerMessages = this._sock.rQshift16(); + var numClientMessages = this._sock.rQshift16(); + var numEncodings = this._sock.rQshift16(); + this._sock.rQskipBytes(2); // padding + + var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; + if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } + + var i; + for (i = 0; i < numServerMessages; i++) { + var srvMsg = this._sock.rQshiftStr(16); + } + + for (i = 0; i < numClientMessages; i++) { + var clientMsg = this._sock.rQshiftStr(16); + } + + for (i = 0; i < numEncodings; i++) { + var encoding = this._sock.rQshiftStr(16); + } + } + + // NB(directxman12): these are down here so that we don't run them multiple times + // if we backtrack + Util.Info("Screen: " + this._fb_width + "x" + this._fb_height + + ", bpp: " + bpp + ", depth: " + depth + + ", big_endian: " + big_endian + + ", true_color: " + true_color + + ", red_max: " + red_max + + ", green_max: " + green_max + + ", blue_max: " + blue_max + + ", red_shift: " + red_shift + + ", green_shift: " + green_shift + + ", blue_shift: " + blue_shift); + + if (big_endian !== 0) { + Util.Warn("Server native endian is not little endian"); + } + + if (red_shift !== 16) { + Util.Warn("Server native red-shift is not 16"); + } + + if (blue_shift !== 0) { + Util.Warn("Server native blue-shift is not 0"); + } + + // we're past the point where we could backtrack, so it's safe to call this + this._onDesktopName(this, this._fb_name); + + if (this._true_color && this._fb_name === "Intel(r) AMT KVM") { + Util.Warn("Intel AMT KVM only supports 8/16 bit depths. Disabling true color"); + this._true_color = false; + } + + this._display.set_true_color(this._true_color); + this._display.resize(this._fb_width, this._fb_height); + this._onFBResize(this, this._fb_width, this._fb_height); + this._keyboard.grab(); + this._mouse.grab(); + + if (this._true_color) { + this._fb_Bpp = 4; + this._fb_depth = 3; + } else { + this._fb_Bpp = 1; + this._fb_depth = 1; + } + + RFB.messages.pixelFormat(this._sock, this._fb_Bpp, this._fb_depth, this._true_color); + RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._true_color); + RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); + + this._timing.fbu_rt_start = (new Date()).getTime(); + this._timing.pixels = 0; + this._sock.flush(); + + if (this._encrypt) { + this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name); + } else { + this._updateState('normal', 'Connected (unencrypted) to: ' + this._fb_name); + } + }, + + _init_msg: function () { + switch (this._rfb_state) { + case 'ProtocolVersion': + return this._negotiate_protocol_version(); + + case 'Security': + return this._negotiate_security(); + + case 'Authentication': + return this._negotiate_authentication(); + + case 'SecurityResult': + return this._handle_security_result(); + + case 'ClientInitialisation': + this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._updateState('ServerInitialisation', "Authentication OK"); + return true; + + case 'ServerInitialisation': + return this._negotiate_server_init(); + } + }, + + _handle_set_colour_map_msg: function () { + Util.Debug("SetColorMapEntries"); + this._sock.rQskip8(); // Padding + + var first_colour = this._sock.rQshift16(); + var num_colours = this._sock.rQshift16(); + if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; } + + for (var c = 0; c < num_colours; c++) { + var red = parseInt(this._sock.rQshift16() / 256, 10); + var green = parseInt(this._sock.rQshift16() / 256, 10); + var blue = parseInt(this._sock.rQshift16() / 256, 10); + this._display.set_colourMap([blue, green, red], first_colour + c); + } + Util.Debug("colourMap: " + this._display.get_colourMap()); + Util.Info("Registered " + num_colours + " colourMap entries"); + + return true; + }, + + _handle_server_cut_text: function () { + Util.Debug("ServerCutText"); + if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + var length = this._sock.rQshift32(); + if (this._sock.rQwait("ServerCutText", length, 8)) { return false; } + + var text = this._sock.rQshiftStr(length); + this._onClipboard(this, text); + + return true; + }, + + _handle_xvp_msg: function () { + if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + var xvp_ver = this._sock.rQshift8(); + var xvp_msg = this._sock.rQshift8(); + + switch (xvp_msg) { + case 0: // XVP_FAIL + this._updateState(this._rfb_state, "Operation Failed"); + break; + case 1: // XVP_INIT + this._rfb_xvp_ver = xvp_ver; + Util.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")"); + this._onXvpInit(this._rfb_xvp_ver); + break; + default: + this._fail("Disconnected: illegal server XVP message " + xvp_msg); + break; + } + + return true; + }, + + _normal_msg: function () { + var msg_type; + + if (this._FBU.rects > 0) { + msg_type = 0; + } else { + msg_type = this._sock.rQshift8(); + } + + switch (msg_type) { + case 0: // FramebufferUpdate + var ret = this._framebufferUpdate(); + if (ret) { + RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); + this._sock.flush(); + } + return ret; + + case 1: // SetColorMapEntries + return this._handle_set_colour_map_msg(); + + case 2: // Bell + Util.Debug("Bell"); + this._onBell(this); + return true; + + case 3: // ServerCutText + return this._handle_server_cut_text(); + + case 250: // XVP + return this._handle_xvp_msg(); + + default: + this._fail("Disconnected: illegal server message type " + msg_type); + Util.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); + return true; + } + }, + + _framebufferUpdate: function () { + var ret = true; + var now; + + if (this._FBU.rects === 0) { + if (this._sock.rQwait("FBU header", 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + this._FBU.rects = this._sock.rQshift16(); + this._FBU.bytes = 0; + this._timing.cur_fbu = 0; + if (this._timing.fbu_rt_start > 0) { + now = (new Date()).getTime(); + Util.Info("First FBU latency: " + (now - this._timing.fbu_rt_start)); + } + } + + while (this._FBU.rects > 0) { + if (this._rfb_state !== "normal") { return false; } + + if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; } + if (this._FBU.bytes === 0) { + if (this._sock.rQwait("rect header", 12)) { return false; } + /* New FramebufferUpdate */ + + var hdr = this._sock.rQshiftBytes(12); + this._FBU.x = (hdr[0] << 8) + hdr[1]; + this._FBU.y = (hdr[2] << 8) + hdr[3]; + this._FBU.width = (hdr[4] << 8) + hdr[5]; + this._FBU.height = (hdr[6] << 8) + hdr[7]; + this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + + (hdr[10] << 8) + hdr[11], 10); + + this._onFBUReceive(this, + {'x': this._FBU.x, 'y': this._FBU.y, + 'width': this._FBU.width, 'height': this._FBU.height, + 'encoding': this._FBU.encoding, + 'encodingName': this._encNames[this._FBU.encoding]}); + + if (!this._encNames[this._FBU.encoding]) { + this._fail("Disconnected: unsupported encoding " + + this._FBU.encoding); + return false; + } + } + + this._timing.last_fbu = (new Date()).getTime(); + + var handler = this._encHandlers[this._FBU.encoding]; + try { + //ret = this._encHandlers[this._FBU.encoding](); + ret = handler(); + } catch (ex) { + console.log("missed " + this._FBU.encoding + ": " + handler); + ret = this._encHandlers[this._FBU.encoding](); + } + + now = (new Date()).getTime(); + this._timing.cur_fbu += (now - this._timing.last_fbu); + + if (ret) { + this._encStats[this._FBU.encoding][0]++; + this._encStats[this._FBU.encoding][1]++; + this._timing.pixels += this._FBU.width * this._FBU.height; + } + + if (this._timing.pixels >= (this._fb_width * this._fb_height)) { + if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) || + this._timing.fbu_rt_start > 0) { + this._timing.full_fbu_total += this._timing.cur_fbu; + this._timing.full_fbu_cnt++; + Util.Info("Timing of full FBU, curr: " + + this._timing.cur_fbu + ", total: " + + this._timing.full_fbu_total + ", cnt: " + + this._timing.full_fbu_cnt + ", avg: " + + (this._timing.full_fbu_total / this._timing.full_fbu_cnt)); + } + + if (this._timing.fbu_rt_start > 0) { + var fbu_rt_diff = now - this._timing.fbu_rt_start; + this._timing.fbu_rt_total += fbu_rt_diff; + this._timing.fbu_rt_cnt++; + Util.Info("full FBU round-trip, cur: " + + fbu_rt_diff + ", total: " + + this._timing.fbu_rt_total + ", cnt: " + + this._timing.fbu_rt_cnt + ", avg: " + + (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt)); + this._timing.fbu_rt_start = 0; + } + } + + if (!ret) { return ret; } // need more data + } + + this._onFBUComplete(this, + {'x': this._FBU.x, 'y': this._FBU.y, + 'width': this._FBU.width, 'height': this._FBU.height, + 'encoding': this._FBU.encoding, + 'encodingName': this._encNames[this._FBU.encoding]}); + + return true; // We finished this FBU + }, + }; + + Util.make_properties(RFB, [ + ['target', 'wo', 'dom'], // VNC display rendering Canvas object + ['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input + ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption + ['true_color', 'rw', 'bool'], // Request true color pixel data + ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor + ['shared', 'rw', 'bool'], // Request shared mode + ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard + ['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields + ['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection + ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection + ['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to + ['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags + + // Callback functions + ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change + ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb): VNC password is required + ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received + ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received + ['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed + ['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed + ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized + ['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received + ['onXvpInit', 'rw', 'func'], // onXvpInit(version): XVP extensions active for this connection + ]); + + RFB.prototype.set_local_cursor = function (cursor) { + if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) { + this._local_cursor = false; + this._display.disableLocalCursor(); //Only show server-side cursor + } else { + if (this._display.get_cursor_uri()) { + this._local_cursor = true; + } else { + Util.Warn("Browser does not support local cursor"); + this._display.disableLocalCursor(); + } + } + }; + + RFB.prototype.get_display = function () { return this._display; }; + RFB.prototype.get_keyboard = function () { return this._keyboard; }; + RFB.prototype.get_mouse = function () { return this._mouse; }; + + // Class Methods + RFB.messages = { + keyEvent: function (sock, keysym, down) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 4; // msg-type + buff[offset + 1] = down; + + buff[offset + 2] = 0; + buff[offset + 3] = 0; + + buff[offset + 4] = (keysym >> 24); + buff[offset + 5] = (keysym >> 16); + buff[offset + 6] = (keysym >> 8); + buff[offset + 7] = keysym; + + sock._sQlen += 8; + }, + + pointerEvent: function (sock, x, y, mask) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 5; // msg-type + + buff[offset + 1] = mask; + + buff[offset + 2] = x >> 8; + buff[offset + 3] = x; + + buff[offset + 4] = y >> 8; + buff[offset + 5] = y; + + sock._sQlen += 6; + }, + + // TODO(directxman12): make this unicode compatible? + clientCutText: function (sock, text) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 6; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + var n = text.length; + + buff[offset + 4] = n >> 24; + buff[offset + 5] = n >> 16; + buff[offset + 6] = n >> 8; + buff[offset + 7] = n; + + for (var i = 0; i < n; i++) { + buff[offset + 8 + i] = text.charCodeAt(i); + } + + sock._sQlen += 8 + n; + }, + + pixelFormat: function (sock, bpp, depth, true_color) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 0; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = bpp * 8; // bits-per-pixel + buff[offset + 5] = depth * 8; // depth + buff[offset + 6] = 0; // little-endian + buff[offset + 7] = true_color ? 1 : 0; // true-color + + buff[offset + 8] = 0; // red-max + buff[offset + 9] = 255; // red-max + + buff[offset + 10] = 0; // green-max + buff[offset + 11] = 255; // green-max + + buff[offset + 12] = 0; // blue-max + buff[offset + 13] = 255; // blue-max + + buff[offset + 14] = 16; // red-shift + buff[offset + 15] = 8; // green-shift + buff[offset + 16] = 0; // blue-shift + + buff[offset + 17] = 0; // padding + buff[offset + 18] = 0; // padding + buff[offset + 19] = 0; // padding + + sock._sQlen += 20; + }, + + clientEncodings: function (sock, encodings, local_cursor, true_color) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 2; // msg-type + buff[offset + 1] = 0; // padding + + // offset + 2 and offset + 3 are encoding count + + var i, j = offset + 4, cnt = 0; + for (i = 0; i < encodings.length; i++) { + if (encodings[i][0] === "Cursor" && !local_cursor) { + Util.Debug("Skipping Cursor pseudo-encoding"); + } else if (encodings[i][0] === "TIGHT" && !true_color) { + // TODO: remove this when we have tight+non-true-color + Util.Warn("Skipping tight as it is only supported with true color"); + } else { + var enc = encodings[i][1]; + buff[j] = enc >> 24; + buff[j + 1] = enc >> 16; + buff[j + 2] = enc >> 8; + buff[j + 3] = enc; + + j += 4; + cnt++; + } + } + + buff[offset + 2] = cnt >> 8; + buff[offset + 3] = cnt; + + sock._sQlen += j - offset; + }, + + fbUpdateRequests: function (sock, cleanDirty, fb_width, fb_height) { + var offsetIncrement = 0; + + var cb = cleanDirty.cleanBox; + var w, h; + if (cb.w > 0 && cb.h > 0) { + w = typeof cb.w === "undefined" ? fb_width : cb.w; + h = typeof cb.h === "undefined" ? fb_height : cb.h; + // Request incremental for clean box + RFB.messages.fbUpdateRequest(sock, 1, cb.x, cb.y, w, h); + } + + for (var i = 0; i < cleanDirty.dirtyBoxes.length; i++) { + var db = cleanDirty.dirtyBoxes[i]; + // Force all (non-incremental) for dirty box + w = typeof db.w === "undefined" ? fb_width : db.w; + h = typeof db.h === "undefined" ? fb_height : db.h; + RFB.messages.fbUpdateRequest(sock, 0, db.x, db.y, w, h); + } + }, + + fbUpdateRequest: function (sock, incremental, x, y, w, h) { + var buff = sock._sQ; + var offset = sock._sQlen; + + if (typeof(x) === "undefined") { x = 0; } + if (typeof(y) === "undefined") { y = 0; } + + buff[offset] = 3; // msg-type + buff[offset + 1] = incremental; + + buff[offset + 2] = (x >> 8) & 0xFF; + buff[offset + 3] = x & 0xFF; + + buff[offset + 4] = (y >> 8) & 0xFF; + buff[offset + 5] = y & 0xFF; + + buff[offset + 6] = (w >> 8) & 0xFF; + buff[offset + 7] = w & 0xFF; + + buff[offset + 8] = (h >> 8) & 0xFF; + buff[offset + 9] = h & 0xFF; + + sock._sQlen += 10; + } + }; + + RFB.genDES = function (password, challenge) { + var passwd = []; + for (var i = 0; i < password.length; i++) { + passwd.push(password.charCodeAt(i)); + } + return (new DES(passwd)).encrypt(challenge); + }; + + RFB.extract_data_uri = function (arr) { + return ";base64," + Base64.encode(arr); + }; + + RFB.encodingHandlers = { + RAW: function () { + if (this._FBU.lines === 0) { + this._FBU.lines = this._FBU.height; + } + + this._FBU.bytes = this._FBU.width * this._fb_Bpp; // at least a line + if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; } + var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); + var curr_height = Math.min(this._FBU.lines, + Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp))); + this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, + curr_height, this._sock.get_rQ(), + this._sock.get_rQi()); + this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp); + this._FBU.lines -= curr_height; + + if (this._FBU.lines > 0) { + this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } + + return true; + }, + + COPYRECT: function () { + this._FBU.bytes = 4; + if (this._sock.rQwait("COPYRECT", 4)) { return false; } + this._display.copyImage(this._sock.rQshift16(), this._sock.rQshift16(), + this._FBU.x, this._FBU.y, this._FBU.width, + this._FBU.height); + + this._FBU.rects--; + this._FBU.bytes = 0; + return true; + }, + + RRE: function () { + var color; + if (this._FBU.subrects === 0) { + this._FBU.bytes = 4 + this._fb_Bpp; + if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; } + this._FBU.subrects = this._sock.rQshift32(); + color = this._sock.rQshiftBytes(this._fb_Bpp); // Background + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); + } + + while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) { + color = this._sock.rQshiftBytes(this._fb_Bpp); + var x = this._sock.rQshift16(); + var y = this._sock.rQshift16(); + var width = this._sock.rQshift16(); + var height = this._sock.rQshift16(); + this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color); + this._FBU.subrects--; + } + + if (this._FBU.subrects > 0) { + var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); + this._FBU.bytes = (this._fb_Bpp + 8) * chunk; + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } + + return true; + }, + + HEXTILE: function () { + var rQ = this._sock.get_rQ(); + var rQi = this._sock.get_rQi(); + + if (this._FBU.tiles === 0) { + this._FBU.tiles_x = Math.ceil(this._FBU.width / 16); + this._FBU.tiles_y = Math.ceil(this._FBU.height / 16); + this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y; + this._FBU.tiles = this._FBU.total_tiles; + } + + while (this._FBU.tiles > 0) { + this._FBU.bytes = 1; + if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; } + var subencoding = rQ[rQi]; // Peek + if (subencoding > 30) { // Raw + this._fail("Disconnected: illegal hextile subencoding " + subencoding); + return false; + } + + var subrects = 0; + var curr_tile = this._FBU.total_tiles - this._FBU.tiles; + var tile_x = curr_tile % this._FBU.tiles_x; + var tile_y = Math.floor(curr_tile / this._FBU.tiles_x); + var x = this._FBU.x + tile_x * 16; + var y = this._FBU.y + tile_y * 16; + var w = Math.min(16, (this._FBU.x + this._FBU.width) - x); + var h = Math.min(16, (this._FBU.y + this._FBU.height) - y); + + // Figure out how much we are expecting + if (subencoding & 0x01) { // Raw + this._FBU.bytes += w * h * this._fb_Bpp; + } else { + if (subencoding & 0x02) { // Background + this._FBU.bytes += this._fb_Bpp; + } + if (subencoding & 0x04) { // Foreground + this._FBU.bytes += this._fb_Bpp; + } + if (subencoding & 0x08) { // AnySubrects + this._FBU.bytes++; // Since we aren't shifting it off + if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; } + subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek + if (subencoding & 0x10) { // SubrectsColoured + this._FBU.bytes += subrects * (this._fb_Bpp + 2); + } else { + this._FBU.bytes += subrects * 2; + } + } + } + + if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; } + + // We know the encoding and have a whole tile + this._FBU.subencoding = rQ[rQi]; + rQi++; + if (this._FBU.subencoding === 0) { + if (this._FBU.lastsubencoding & 0x01) { + // Weird: ignore blanks are RAW + Util.Debug(" Ignoring blank after RAW"); + } else { + this._display.fillRect(x, y, w, h, this._FBU.background); + } + } else if (this._FBU.subencoding & 0x01) { // Raw + this._display.blitImage(x, y, w, h, rQ, rQi); + rQi += this._FBU.bytes - 1; + } else { + if (this._FBU.subencoding & 0x02) { // Background + if (this._fb_Bpp == 1) { + this._FBU.background = rQ[rQi]; + } else { + // fb_Bpp is 4 + this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + } + rQi += this._fb_Bpp; + } + if (this._FBU.subencoding & 0x04) { // Foreground + if (this._fb_Bpp == 1) { + this._FBU.foreground = rQ[rQi]; + } else { + // this._fb_Bpp is 4 + this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + } + rQi += this._fb_Bpp; + } + + this._display.startTile(x, y, w, h, this._FBU.background); + if (this._FBU.subencoding & 0x08) { // AnySubrects + subrects = rQ[rQi]; + rQi++; + + for (var s = 0; s < subrects; s++) { + var color; + if (this._FBU.subencoding & 0x10) { // SubrectsColoured + if (this._fb_Bpp === 1) { + color = rQ[rQi]; + } else { + // _fb_Bpp is 4 + color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + } + rQi += this._fb_Bpp; + } else { + color = this._FBU.foreground; + } + var xy = rQ[rQi]; + rQi++; + var sx = (xy >> 4); + var sy = (xy & 0x0f); + + var wh = rQ[rQi]; + rQi++; + var sw = (wh >> 4) + 1; + var sh = (wh & 0x0f) + 1; + + this._display.subTile(sx, sy, sw, sh, color); + } + } + this._display.finishTile(); + } + this._sock.set_rQi(rQi); + this._FBU.lastsubencoding = this._FBU.subencoding; + this._FBU.bytes = 0; + this._FBU.tiles--; + } + + if (this._FBU.tiles === 0) { + this._FBU.rects--; + } + + return true; + }, + + getTightCLength: function (arr) { + var header = 1, data = 0; + data += arr[0] & 0x7f; + if (arr[0] & 0x80) { + header++; + data += (arr[1] & 0x7f) << 7; + if (arr[1] & 0x80) { + header++; + data += arr[2] << 14; + } + } + return [header, data]; + }, + + display_tight: function (isTightPNG) { + if (this._fb_depth === 1) { + this._fail("Tight protocol handler only implements true color mode"); + } + + this._FBU.bytes = 1; // compression-control byte + if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; } + + var checksum = function (data) { + var sum = 0; + for (var i = 0; i < data.length; i++) { + sum += data[i]; + if (sum > 65536) sum -= 65536; + } + return sum; + }; + + var resetStreams = 0; + var streamId = -1; + var decompress = function (data) { + for (var i = 0; i < 4; i++) { + if ((resetStreams >> i) & 1) { + this._FBU.zlibs[i].reset(); + Util.Info("Reset zlib stream " + i); + } + } + + //var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); + var uncompressed = this._FBU.zlibs[streamId].inflate(data, true); + /*if (uncompressed.status !== 0) { + Util.Error("Invalid data in zlib stream"); + }*/ + + //return uncompressed.data; + return uncompressed; + }.bind(this); + + var indexedToRGBX2Color = function (data, palette, width, height) { + // Convert indexed (palette based) image data to RGB + // TODO: reduce number of calculations inside loop + var dest = this._destBuff; + var w = Math.floor((width + 7) / 8); + var w1 = Math.floor(width / 8); + + /*for (var y = 0; y < height; y++) { + var b, x, dp, sp; + var yoffset = y * width; + var ybitoffset = y * w; + var xoffset, targetbyte; + for (x = 0; x < w1; x++) { + xoffset = yoffset + x * 8; + targetbyte = data[ybitoffset + x]; + for (b = 7; b >= 0; b--) { + dp = (xoffset + 7 - b) * 3; + sp = (targetbyte >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + } + + xoffset = yoffset + x * 8; + targetbyte = data[ybitoffset + x]; + for (b = 7; b >= 8 - width % 8; b--) { + dp = (xoffset + 7 - b) * 3; + sp = (targetbyte >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + }*/ + + for (var y = 0; y < height; y++) { + var b, x, dp, sp; + for (x = 0; x < w1; x++) { + for (b = 7; b >= 0; b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } + } + + for (b = 7; b >= 8 - width % 8; b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } + } + + return dest; + }.bind(this); + + var indexedToRGBX = function (data, palette, width, height) { + // Convert indexed (palette based) image data to RGB + var dest = this._destBuff; + var total = width * height * 4; + for (var i = 0, j = 0; i < total; i += 4, j++) { + var sp = data[j] * 3; + dest[i] = palette[sp]; + dest[i + 1] = palette[sp + 1]; + dest[i + 2] = palette[sp + 2]; + dest[i + 3] = 255; + } + + return dest; + }.bind(this); + + var rQi = this._sock.get_rQi(); + var rQ = this._sock.rQwhole(); + var cmode, data; + var cl_header, cl_data; + + var handlePalette = function () { + var numColors = rQ[rQi + 2] + 1; + var paletteSize = numColors * this._fb_depth; + this._FBU.bytes += paletteSize; + if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } + + var bpp = (numColors <= 2) ? 1 : 8; + var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8); + var raw = false; + if (rowSize * this._FBU.height < 12) { + raw = true; + cl_header = 0; + cl_data = rowSize * this._FBU.height; + //clength = [0, rowSize * this._FBU.height]; + } else { + // begin inline getTightCLength (returning two-item arrays is bad for performance with GC) + var cl_offset = rQi + 3 + paletteSize; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength + } + + this._FBU.bytes += cl_header + cl_data; + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Shift ctl, filter id, num colors, palette entries, and clength off + this._sock.rQskipBytes(3); + //var palette = this._sock.rQshiftBytes(paletteSize); + this._sock.rQshiftTo(this._paletteBuff, paletteSize); + this._sock.rQskipBytes(cl_header); + + if (raw) { + data = this._sock.rQshiftBytes(cl_data); + } else { + data = decompress(this._sock.rQshiftBytes(cl_data)); + } + + // Convert indexed (palette based) image data to RGB + var rgbx; + if (numColors == 2) { + rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height); + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); + } else { + rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); + } + + + return true; + }.bind(this); + + var handleCopy = function () { + var raw = false; + var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; + if (uncompressedSize < 12) { + raw = true; + cl_header = 0; + cl_data = uncompressedSize; + } else { + // begin inline getTightCLength (returning two-item arrays is for peformance with GC) + var cl_offset = rQi + 1; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength + } + this._FBU.bytes = 1 + cl_header + cl_data; + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Shift ctl, clength off + this._sock.rQshiftBytes(1 + cl_header); + + if (raw) { + data = this._sock.rQshiftBytes(cl_data); + } else { + data = decompress(this._sock.rQshiftBytes(cl_data)); + } + + this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); + + return true; + }.bind(this); + + var ctl = this._sock.rQpeek8(); + + // Keep tight reset bits + resetStreams = ctl & 0xF; + + // Figure out filter + ctl = ctl >> 4; + streamId = ctl & 0x3; + + if (ctl === 0x08) cmode = "fill"; + else if (ctl === 0x09) cmode = "jpeg"; + else if (ctl === 0x0A) cmode = "png"; + else if (ctl & 0x04) cmode = "filter"; + else if (ctl < 0x04) cmode = "copy"; + else return this._fail("Illegal tight compression received, ctl: " + ctl); + + if (isTightPNG && (cmode === "filter" || cmode === "copy")) { + return this._fail("filter/copy received in tightPNG mode"); + } + + switch (cmode) { + // fill use fb_depth because TPIXELs drop the padding byte + case "fill": // TPIXEL + this._FBU.bytes += this._fb_depth; + break; + case "jpeg": // max clength + this._FBU.bytes += 3; + break; + case "png": // max clength + this._FBU.bytes += 3; + break; + case "filter": // filter id + num colors if palette + this._FBU.bytes += 2; + break; + case "copy": + break; + } + + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Determine FBU.bytes + switch (cmode) { + case "fill": + // skip ctl byte + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false); + this._sock.rQskipBytes(4); + break; + case "png": + case "jpeg": + // begin inline getTightCLength (returning two-item arrays is for peformance with GC) + var cl_offset = rQi + 1; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength + this._FBU.bytes = 1 + cl_header + cl_data; // ctl + clength size + jpeg-data + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // We have everything, render it + this._sock.rQskipBytes(1 + cl_header); // shift off clt + compact length + var img = new Image(); + img.src = "data: image/" + cmode + + RFB.extract_data_uri(this._sock.rQshiftBytes(cl_data)); + this._display.renderQ_push({ + 'type': 'img', + 'img': img, + 'x': this._FBU.x, + 'y': this._FBU.y + }); + img = null; + break; + case "filter": + var filterId = rQ[rQi + 1]; + if (filterId === 1) { + if (!handlePalette()) { return false; } + } else { + // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter + // Filter 2, Gradient is valid but not use if jpeg is enabled + // TODO(directxman12): why aren't we just calling '_fail' here + throw new Error("Unsupported tight subencoding received, filter: " + filterId); + } + break; + case "copy": + if (!handleCopy()) { return false; } + break; + } + + + this._FBU.bytes = 0; + this._FBU.rects--; + + return true; + }, + + TIGHT: function () { return this._encHandlers.display_tight(false); }, + TIGHT_PNG: function () { return this._encHandlers.display_tight(true); }, + + last_rect: function () { + this._FBU.rects = 0; + return true; + }, + + handle_FB_resize: function () { + this._fb_width = this._FBU.width; + this._fb_height = this._FBU.height; + this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); + this._display.resize(this._fb_width, this._fb_height); + this._onFBResize(this, this._fb_width, this._fb_height); + this._timing.fbu_rt_start = (new Date()).getTime(); + + this._FBU.bytes = 0; + this._FBU.rects -= 1; + return true; + }, + + ExtendedDesktopSize: function () { + this._FBU.bytes = 1; + if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } + + this._supportsSetDesktopSize = true; + var number_of_screens = this._sock.rQpeek8(); + + this._FBU.bytes = 4 + (number_of_screens * 16); + if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } + + this._sock.rQskipBytes(1); // number-of-screens + this._sock.rQskipBytes(3); // padding + + for (var i = 0; i < number_of_screens; i += 1) { + // Save the id and flags of the first screen + if (i === 0) { + this._screen_id = this._sock.rQshiftBytes(4); // id + this._sock.rQskipBytes(2); // x-position + this._sock.rQskipBytes(2); // y-position + this._sock.rQskipBytes(2); // width + this._sock.rQskipBytes(2); // height + this._screen_flags = this._sock.rQshiftBytes(4); // flags + } else { + this._sock.rQskipBytes(16); + } + } + + /* + * The x-position indicates the reason for the change: + * + * 0 - server resized on its own + * 1 - this client requested the resize + * 2 - another client requested the resize + */ + + // We need to handle errors when we requested the resize. + if (this._FBU.x === 1 && this._FBU.y !== 0) { + var msg = ""; + // The y-position indicates the status code from the server + switch (this._FBU.y) { + case 1: + msg = "Resize is administratively prohibited"; + break; + case 2: + msg = "Out of resources"; + break; + case 3: + msg = "Invalid screen layout"; + break; + default: + msg = "Unknown reason"; + break; + } + Util.Info("Server did not accept the resize request: " + msg); + return true; + } + + this._encHandlers.handle_FB_resize(); + return true; + }, + + DesktopSize: function () { + this._encHandlers.handle_FB_resize(); + return true; + }, + + Cursor: function () { + Util.Debug(">> set_cursor"); + var x = this._FBU.x; // hotspot-x + var y = this._FBU.y; // hotspot-y + var w = this._FBU.width; + var h = this._FBU.height; + + var pixelslength = w * h * this._fb_Bpp; + var masklength = Math.floor((w + 7) / 8) * h; + + this._FBU.bytes = pixelslength + masklength; + if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } + + this._display.changeCursor(this._sock.rQshiftBytes(pixelslength), + this._sock.rQshiftBytes(masklength), + x, y, w, h); + + this._FBU.bytes = 0; + this._FBU.rects--; + + Util.Debug("<< set_cursor"); + return true; + }, + + JPEG_quality_lo: function () { + Util.Error("Server sent jpeg_quality pseudo-encoding"); + }, + + compress_lo: function () { + Util.Error("Server sent compress level pseudo-encoding"); + } + }; +})(); diff --git a/plugins/dynamix.vm.manager/include/ui.js b/plugins/dynamix.vm.manager/include/ui.js new file mode 100644 index 000000000..f3b5fe416 --- /dev/null +++ b/plugins/dynamix.vm.manager/include/ui.js @@ -0,0 +1,1208 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2015 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* jslint white: false, browser: true */ +/* global window, $D, Util, WebUtil, RFB, Display */ + +var UI; + +(function () { + "use strict"; + + var resizeTimeout; + + // Load supporting scripts + window.onscriptsload = function () { UI.load(); }; + Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", + "keysymdef.js", "keyboard.js", "input.js", "display.js", + "rfb.js", "keysym.js", "inflator.js"]); + + UI = { + + rfb_state : 'loaded', + settingsOpen : false, + connSettingsOpen : false, + popupStatusTimeout: null, + clipboardOpen: false, + keyboardVisible: false, + hideKeyboardTimeout: null, + lastKeyboardinput: null, + defaultKeyboardinputLen: 100, + extraKeysVisible: false, + ctrlOn: false, + altOn: false, + isTouchDevice: false, + rememberedClipSetting: null, + + // Setup rfb object, load settings from browser storage, then call + // UI.init to setup the UI/menus + load: function (callback) { + WebUtil.initSettings(UI.start, callback); + }, + + // Render default UI and initialize settings menu + start: function(callback) { + UI.isTouchDevice = 'ontouchstart' in document.documentElement; + + // Stylesheet selection dropdown + var sheet = WebUtil.selectStylesheet(); + var sheets = WebUtil.getStylesheets(); + var i; + for (i = 0; i < sheets.length; i += 1) { + UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title); + } + + // Logging selection dropdown + var llevels = ['error', 'warn', 'info', 'debug']; + for (i = 0; i < llevels.length; i += 1) { + UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]); + } + + // Settings with immediate effects + UI.initSetting('logging', 'warn'); + WebUtil.init_logging(UI.getSetting('logging')); + + UI.initSetting('stylesheet', 'default'); + WebUtil.selectStylesheet(null); + // call twice to get around webkit bug + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); + + // if port == 80 (or 443) then it won't be present and should be + // set manually + var port = window.location.port; + if (!port) { + if (window.location.protocol.substring(0,5) == 'https') { + port = 443; + } + else if (window.location.protocol.substring(0,4) == 'http') { + port = 80; + } + } + + /* Populate the controls if defaults are provided in the URL */ + UI.initSetting('host', window.location.hostname); + UI.initSetting('port', port); + UI.initSetting('password', ''); + UI.initSetting('encrypt', (window.location.protocol === "https:")); + UI.initSetting('true_color', true); + UI.initSetting('cursor', !UI.isTouchDevice); + UI.initSetting('resize', 'scale'); // LT: default was 'off' before + UI.initSetting('shared', true); + UI.initSetting('view_only', false); + UI.initSetting('path', 'websockify'); + UI.initSetting('repeaterID', ''); + + var autoconnect = WebUtil.getQueryVar('autoconnect', false); + if (autoconnect === 'true' || autoconnect == '1') { + autoconnect = true; + UI.connect(); + } else { + autoconnect = false; + } + + UI.updateVisualState(); + + $D('noVNC_host').focus(); + + // Show mouse selector buttons on touch screen devices + if (UI.isTouchDevice) { + // Show mobile buttons + $D('noVNC_mobile_buttons').style.display = "inline"; + UI.setMouseButton(); + // Remove the address bar + setTimeout(function() { window.scrollTo(0, 1); }, 100); + UI.forceSetting('clip', true); + } else { + UI.initSetting('clip', false); + } + + UI.setViewClip(); + UI.setBarPosition(); + + Util.addEvent(window, 'resize', function () { + UI.onresize(); + UI.setViewClip(); + UI.updateViewDrag(); + UI.setBarPosition(); + } ); + + var isSafari = (navigator.userAgent.indexOf('Safari') != -1 && + navigator.userAgent.indexOf('Chrome') == -1); + + // Only show the button if fullscreen is properly supported + // * Safari doesn't support alphanumerical input while in fullscreen + if (!isSafari && + (document.documentElement.requestFullscreen || + document.documentElement.mozRequestFullScreen || + document.documentElement.webkitRequestFullscreen || + document.body.msRequestFullscreen)) { + $D('fullscreenButton').style.display = "inline"; + Util.addEvent(window, 'fullscreenchange', UI.updateFullscreenButton); + Util.addEvent(window, 'mozfullscreenchange', UI.updateFullscreenButton); + Util.addEvent(window, 'webkitfullscreenchange', UI.updateFullscreenButton); + Util.addEvent(window, 'msfullscreenchange', UI.updateFullscreenButton); + } + + Util.addEvent(window, 'load', UI.keyboardinputReset); + + Util.addEvent(window, 'beforeunload', function () { + if (UI.rfb && UI.rfb_state === 'normal') { + return "You are currently connected."; + } + } ); + + // Show description by default when hosted at for kanaka.github.com + if (location.host === "kanaka.github.io") { + // Open the description dialog + $D('noVNC_description').style.display = "block"; + } else { + // Show the connect panel on first load unless autoconnecting + if (autoconnect === UI.connSettingsOpen) { + UI.toggleConnectPanel(); + } + } + + // Add mouse event click/focus/blur event handlers to the UI + UI.addMouseHandlers(); + + if (typeof callback === "function") { + callback(UI.rfb); + } + }, + + initRFB: function () { + try { + UI.rfb = new RFB({'target': $D('noVNC_canvas'), + 'onUpdateState': UI.updateState, + 'onXvpInit': UI.updateXvpVisualState, + 'onClipboard': UI.clipReceive, + 'onFBUComplete': UI.FBUComplete, + 'onFBResize': UI.updateViewDrag, + 'onDesktopName': UI.updateDocumentTitle}); + return true; + } catch (exc) { + UI.updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc); + return false; + } + }, + + addMouseHandlers: function() { + // Setup interface handlers that can't be inline + $D("noVNC_view_drag_button").onclick = UI.toggleViewDrag; + $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); }; + $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); }; + $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); }; + $D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); }; + $D("showKeyboard").onclick = UI.showKeyboard; + + $D("keyboardinput").oninput = UI.keyInput; + $D("keyboardinput").onblur = UI.keyInputBlur; + $D("keyboardinput").onsubmit = function () { return false; }; + + $D("showExtraKeysButton").onclick = UI.showExtraKeys; + $D("toggleCtrlButton").onclick = UI.toggleCtrl; + $D("toggleAltButton").onclick = UI.toggleAlt; + $D("sendTabButton").onclick = UI.sendTab; + $D("sendEscButton").onclick = UI.sendEsc; + + $D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel; + $D("xvpShutdownButton").onclick = UI.xvpShutdown; + $D("xvpRebootButton").onclick = UI.xvpReboot; + $D("xvpResetButton").onclick = UI.xvpReset; + $D("noVNC_status").onclick = UI.togglePopupStatus; + $D("noVNC_popup_status").onclick = UI.togglePopupStatus; + $D("xvpButton").onclick = UI.toggleXvpPanel; + $D("clipboardButton").onclick = UI.toggleClipboardPanel; + $D("fullscreenButton").onclick = UI.toggleFullscreen; + $D("settingsButton").onclick = UI.toggleSettingsPanel; + $D("connectButton").onclick = UI.toggleConnectPanel; + $D("disconnectButton").onclick = UI.disconnect; + $D("descriptionButton").onclick = UI.toggleConnectPanel; + + $D("noVNC_clipboard_text").onfocus = UI.displayBlur; + $D("noVNC_clipboard_text").onblur = UI.displayFocus; + $D("noVNC_clipboard_text").onchange = UI.clipSend; + $D("noVNC_clipboard_clear_button").onclick = UI.clipClear; + + $D("noVNC_settings_menu").onmouseover = UI.displayBlur; + $D("noVNC_settings_menu").onmouseover = UI.displayFocus; + $D("noVNC_apply").onclick = UI.settingsApply; + + $D("noVNC_connect_button").onclick = UI.connect; + + $D("noVNC_resize").onchange = UI.enableDisableViewClip; + }, + + onresize: function (callback) { + if (!UI.rfb) return; + + var size = UI.getCanvasLimit(); + + if (size && UI.rfb_state === 'normal' && UI.rfb.get_display()) { + var display = UI.rfb.get_display(); + var scaleType = UI.getSetting('resize'); + if (scaleType === 'remote') { + // use remote resizing + + // When the local window has been resized, wait until the size remains + // the same for 0.5 seconds before sending the request for changing + // the resolution of the session + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(function(){ + display.set_maxWidth(size.w); + display.set_maxHeight(size.h); + Util.Debug('Attempting setDesktopSize(' + + size.w + ', ' + size.h + ')'); + UI.rfb.setDesktopSize(size.w, size.h); + }, 500); + } else if (scaleType === 'scale' || scaleType === 'downscale') { + // use local scaling + + var downscaleOnly = scaleType === 'downscale'; + var scaleRatio = display.autoscale(size.w, size.h, downscaleOnly); + UI.rfb.get_mouse().set_scale(scaleRatio); + Util.Debug('Scaling by ' + UI.rfb.get_mouse().get_scale()); + } else if (scaleType === 'off') { + display.set_scale(1.0); + } + } + }, + + getCanvasLimit: function () { + var container = $D('noVNC_container'); + + // Hide the scrollbars until the size is calculated + container.style.overflow = "hidden"; + + var pos = Util.getPosition(container); + var w = pos.width; + var h = pos.height; + + container.style.overflow = "visible"; + + if (isNaN(w) || isNaN(h)) { + return false; + } else { + return {w: w, h: h}; + } + }, + + // Read form control compatible setting from cookie + getSetting: function(name) { + var ctrl = $D('noVNC_' + name); + var val = WebUtil.readSetting(name); + if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { + if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { + val = false; + } else { + val = true; + } + } + return val; + }, + + // Update cookie and form control setting. If value is not set, then + // updates from control to current cookie setting. + updateSetting: function(name, value) { + + // Save the cookie for this session + if (typeof value !== 'undefined') { + WebUtil.writeSetting(name, value); + } + + // Update the settings control + value = UI.getSetting(name); + + var ctrl = $D('noVNC_' + name); + if (ctrl.type === 'checkbox') { + ctrl.checked = value; + + } else if (typeof ctrl.options !== 'undefined') { + for (var i = 0; i < ctrl.options.length; i += 1) { + if (ctrl.options[i].value === value) { + ctrl.selectedIndex = i; + break; + } + } + } else { + /*Weird IE9 error leads to 'null' appearring + in textboxes instead of ''.*/ + if (value === null) { + value = ""; + } + ctrl.value = value; + } + }, + + // Save control setting to cookie + saveSetting: function(name) { + var val, ctrl = $D('noVNC_' + name); + if (ctrl.type === 'checkbox') { + val = ctrl.checked; + } else if (typeof ctrl.options !== 'undefined') { + val = ctrl.options[ctrl.selectedIndex].value; + } else { + val = ctrl.value; + } + WebUtil.writeSetting(name, val); + //Util.Debug("Setting saved '" + name + "=" + val + "'"); + return val; + }, + + // Initial page load read/initialization of settings + initSetting: function(name, defVal) { + // Check Query string followed by cookie + var val = WebUtil.getQueryVar(name); + if (val === null) { + val = WebUtil.readSetting(name, defVal); + } + UI.updateSetting(name, val); + return val; + }, + + // Force a setting to be a certain value + forceSetting: function(name, val) { + UI.updateSetting(name, val); + return val; + }, + + + // Show the popup status + togglePopupStatus: function(text) { + var psp = $D('noVNC_popup_status'); + + var closePopup = function() { psp.style.display = "none"; }; + + if (window.getComputedStyle(psp).display === 'none') { + if (typeof text === 'string') { + psp.innerHTML = text; + } else { + psp.innerHTML = $D('noVNC_status').innerHTML; + } + psp.style.display = "block"; + psp.style.left = window.innerWidth/2 - + parseInt(window.getComputedStyle(psp).width)/2 -30 + "px"; + + // Show the popup for a maximum of 1.5 seconds + UI.popupStatusTimeout = setTimeout(function() { closePopup(); }, 1500); + } else { + clearTimeout(UI.popupStatusTimeout); + closePopup(); + } + }, + + // Show the XVP panel + toggleXvpPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Toggle XVP panel + if (UI.xvpOpen === true) { + $D('noVNC_xvp').style.display = "none"; + $D('xvpButton').className = "noVNC_status_button"; + UI.xvpOpen = false; + } else { + $D('noVNC_xvp').style.display = "block"; + $D('xvpButton').className = "noVNC_status_button_selected"; + UI.xvpOpen = true; + } + }, + + // Show the clipboard panel + toggleClipboardPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + // Toggle Clipboard Panel + if (UI.clipboardOpen === true) { + $D('noVNC_clipboard').style.display = "none"; + $D('clipboardButton').className = "noVNC_status_button"; + UI.clipboardOpen = false; + } else { + $D('noVNC_clipboard').style.display = "block"; + $D('clipboardButton').className = "noVNC_status_button_selected"; + UI.clipboardOpen = true; + } + }, + + // Toggle fullscreen mode + toggleFullscreen: function() { + if (document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.documentElement.mozRequestFullScreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (document.body.msRequestFullscreen) { + document.body.msRequestFullscreen(); + } + } + UI.enableDisableViewClip(); + UI.updateFullscreenButton(); + }, + + updateFullscreenButton: function() { + if (document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement ) { + $D('fullscreenButton').className = "noVNC_status_button_selected"; + } else { + $D('fullscreenButton').className = "noVNC_status_button"; + } + }, + + // Show the connection settings panel/menu + toggleConnectPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close connection settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + $D('connectButton').className = "noVNC_status_button"; + } + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + + // Toggle Connection Panel + if (UI.connSettingsOpen === true) { + $D('noVNC_controls').style.display = "none"; + $D('connectButton').className = "noVNC_status_button"; + UI.connSettingsOpen = false; + UI.saveSetting('host'); + UI.saveSetting('port'); + //UI.saveSetting('password'); + } else { + $D('noVNC_controls').style.display = "block"; + $D('connectButton').className = "noVNC_status_button_selected"; + UI.connSettingsOpen = true; + $D('noVNC_host').focus(); + } + }, + + // Toggle the settings menu: + // On open, settings are refreshed from saved cookies. + // On close, settings are applied + toggleSettingsPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + if (UI.settingsOpen) { + UI.settingsApply(); + } else { + UI.updateSetting('encrypt'); + UI.updateSetting('true_color'); + if (Util.browserSupportsCursorURIs()) { + UI.updateSetting('cursor'); + } else { + UI.updateSetting('cursor', !UI.isTouchDevice); + $D('noVNC_cursor').disabled = true; + } + UI.updateSetting('clip'); + UI.updateSetting('resize'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('stylesheet'); + UI.updateSetting('logging'); + + UI.openSettingsMenu(); + } + }, + + // Open menu + openSettingsMenu: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + $D('noVNC_settings').style.display = "block"; + $D('settingsButton').className = "noVNC_status_button_selected"; + UI.settingsOpen = true; + }, + + // Close menu (without applying settings) + closeSettingsMenu: function() { + $D('noVNC_settings').style.display = "none"; + $D('settingsButton').className = "noVNC_status_button"; + UI.settingsOpen = false; + }, + + // Save/apply settings when 'Apply' button is pressed + settingsApply: function() { + //Util.Debug(">> settingsApply"); + UI.saveSetting('encrypt'); + UI.saveSetting('true_color'); + if (Util.browserSupportsCursorURIs()) { + UI.saveSetting('cursor'); + } + + UI.saveSetting('resize'); + + if (UI.getSetting('resize') === 'downscale' || UI.getSetting('resize') === 'scale') { + UI.forceSetting('clip', false); + } + + UI.saveSetting('clip'); + UI.saveSetting('shared'); + UI.saveSetting('view_only'); + UI.saveSetting('path'); + UI.saveSetting('repeaterID'); + UI.saveSetting('stylesheet'); + UI.saveSetting('logging'); + + // Settings with immediate (non-connected related) effect + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); + WebUtil.init_logging(UI.getSetting('logging')); + UI.onresize(); + UI.setViewClip(); + UI.updateViewDrag(); + //Util.Debug("<< settingsApply"); + + UI.closeSettingsMenu(); + }, + + + + setPassword: function() { + UI.rfb.sendPassword($D('noVNC_password').value); + //Reset connect button. + $D('noVNC_connect_button').value = "Connect"; + $D('noVNC_connect_button').onclick = UI.connect; + //Hide connection panel. + UI.toggleConnectPanel(); + return false; + }, + + sendCtrlAltDel: function() { + UI.rfb.sendCtrlAltDel(); + }, + + xvpShutdown: function() { + UI.rfb.xvpShutdown(); + }, + + xvpReboot: function() { + UI.rfb.xvpReboot(); + }, + + xvpReset: function() { + UI.rfb.xvpReset(); + }, + + setMouseButton: function(num) { + if (typeof num === 'undefined') { + // Disable mouse buttons + num = -1; + } + if (UI.rfb) { + UI.rfb.get_mouse().set_touchButton(num); + } + + var blist = [0, 1,2,4]; + for (var b = 0; b < blist.length; b++) { + var button = $D('noVNC_mouse_button' + blist[b]); + if (blist[b] === num) { + button.style.display = ""; + } else { + button.style.display = "none"; + } + } + }, + + updateState: function(rfb, state, oldstate, msg) { + UI.rfb_state = state; + var klass; + switch (state) { + case 'failed': + case 'fatal': + klass = "noVNC_status_error"; + break; + case 'normal': + klass = "noVNC_status_normal"; + break; + case 'disconnected': + $D('noVNC_logo').style.display = "block"; + $D('noVNC_container').style.display = "none"; + /* falls through */ + case 'loaded': + klass = "noVNC_status_normal"; + break; + case 'password': + UI.toggleConnectPanel(); + + $D('noVNC_connect_button').value = "Send Password"; + $D('noVNC_connect_button').onclick = UI.setPassword; + $D('noVNC_password').focus(); + + klass = "noVNC_status_warn"; + break; + default: + klass = "noVNC_status_warn"; + break; + } + + if (typeof(msg) !== 'undefined') { + $D('noVNC-control-bar').setAttribute("class", klass); + $D('noVNC_status').innerHTML = msg; + } + + UI.updateVisualState(); + }, + + // Disable/enable controls depending on connection state + updateVisualState: function() { + var connected = UI.rfb && UI.rfb_state === 'normal'; + + //Util.Debug(">> updateVisualState"); + $D('noVNC_encrypt').disabled = connected; + $D('noVNC_true_color').disabled = connected; + if (Util.browserSupportsCursorURIs()) { + $D('noVNC_cursor').disabled = connected; + } else { + UI.updateSetting('cursor', !UI.isTouchDevice); + $D('noVNC_cursor').disabled = true; + } + + UI.enableDisableViewClip(); + //$D('noVNC_resize').disabled = connected; + $D('noVNC_shared').disabled = connected; + $D('noVNC_view_only').disabled = connected; + $D('noVNC_path').disabled = connected; + $D('noVNC_repeaterID').disabled = connected; + + if (connected) { + UI.setViewClip(); + UI.setMouseButton(1); + $D('clipboardButton').style.display = "inline"; + $D('showKeyboard').style.display = "inline"; + $D('noVNC_extra_keys').style.display = ""; + $D('sendCtrlAltDelButton').style.display = "inline"; + } else { + UI.setMouseButton(); + $D('clipboardButton').style.display = "none"; + $D('showKeyboard').style.display = "none"; + $D('noVNC_extra_keys').style.display = "none"; + $D('sendCtrlAltDelButton').style.display = "none"; + UI.updateXvpVisualState(0); + } + + // State change disables viewport dragging. + // It is enabled (toggled) by direct click on the button + UI.updateViewDrag(false); + + switch (UI.rfb_state) { + case 'fatal': + case 'failed': + case 'disconnected': + $D('connectButton').style.display = ""; + $D('disconnectButton').style.display = "none"; + UI.connSettingsOpen = false; + UI.toggleConnectPanel(); + break; + case 'loaded': + $D('connectButton').style.display = ""; + $D('disconnectButton').style.display = "none"; + break; + default: + $D('connectButton').style.display = "none"; + $D('disconnectButton').style.display = ""; + break; + } + + //Util.Debug("<< updateVisualState"); + }, + + // Disable/enable XVP button + updateXvpVisualState: function(ver) { + if (ver >= 1) { + $D('xvpButton').style.display = 'inline'; + } else { + $D('xvpButton').style.display = 'none'; + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + } + }, + + // This resize can not be done until we know from the first Frame Buffer Update + // if it is supported or not. + // The resize is needed to make sure the server desktop size is updated to the + // corresponding size of the current local window when reconnecting to an + // existing session. + FBUComplete: function(rfb, fbu) { + UI.onresize(); + UI.rfb.set_onFBUComplete(function() { }); + }, + + // Display the desktop name in the document title + updateDocumentTitle: function(rfb, name) { + document.title = name + " - noVNC"; + }, + + clipReceive: function(rfb, text) { + Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); + $D('noVNC_clipboard_text').value = text; + Util.Debug("<< UI.clipReceive"); + }, + + connect: function() { + UI.closeSettingsMenu(); + UI.toggleConnectPanel(); + + var host = $D('noVNC_host').value; + var port = $D('noVNC_port').value; + var password = $D('noVNC_password').value; + var path = $D('noVNC_path').value; + if ((!host) || (!port)) { + throw new Error("Must set host and port"); + } + + if (!UI.initRFB()) return; + + UI.rfb.set_encrypt(UI.getSetting('encrypt')); + UI.rfb.set_true_color(UI.getSetting('true_color')); + UI.rfb.set_local_cursor(UI.getSetting('cursor')); + UI.rfb.set_shared(UI.getSetting('shared')); + UI.rfb.set_view_only(UI.getSetting('view_only')); + UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); + + UI.rfb.connect(host, port, password, path); + + //Close dialog. + setTimeout(UI.setBarPosition, 100); + $D('noVNC_logo').style.display = "none"; + $D('noVNC_container').style.display = "inline"; + }, + + disconnect: function() { + UI.closeSettingsMenu(); + UI.rfb.disconnect(); + + // Restore the callback used for initial resize + UI.rfb.set_onFBUComplete(UI.FBUComplete); + + $D('noVNC_logo').style.display = "block"; + $D('noVNC_container').style.display = "none"; + + // Don't display the connection settings until we're actually disconnected + }, + + displayBlur: function() { + if (!UI.rfb) return; + + UI.rfb.get_keyboard().set_focused(false); + UI.rfb.get_mouse().set_focused(false); + }, + + displayFocus: function() { + if (!UI.rfb) return; + + UI.rfb.get_keyboard().set_focused(true); + UI.rfb.get_mouse().set_focused(true); + }, + + clipClear: function() { + $D('noVNC_clipboard_text').value = ""; + UI.rfb.clipboardPasteFrom(""); + }, + + clipSend: function() { + var text = $D('noVNC_clipboard_text').value; + Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "..."); + UI.rfb.clipboardPasteFrom(text); + Util.Debug("<< UI.clipSend"); + }, + + // Set and configure viewport clipping + setViewClip: function(clip) { + var display; + if (UI.rfb) { + display = UI.rfb.get_display(); + } else { + UI.forceSetting('clip', clip); + return; + } + + var cur_clip = display.get_viewport(); + + if (typeof(clip) !== 'boolean') { + // Use current setting + clip = UI.getSetting('clip'); + } + + if (clip && !cur_clip) { + // Turn clipping on + UI.updateSetting('clip', true); + } else if (!clip && cur_clip) { + // Turn clipping off + UI.updateSetting('clip', false); + display.set_viewport(false); + // Disable max dimensions + display.set_maxWidth(0); + display.set_maxHeight(0); + display.viewportChangeSize(); + } + if (UI.getSetting('clip')) { + // If clipping, update clipping settings + display.set_viewport(true); + + var size = UI.getCanvasLimit(); + if (size) { + display.set_maxWidth(size.w); + display.set_maxHeight(size.h); + + // Hide potential scrollbars that can skew the position + $D('noVNC_container').style.overflow = "hidden"; + + // The x position marks the left margin of the canvas, + // remove the margin from both sides to keep it centered + var new_w = size.w - (2 * Util.getPosition($D('noVNC_canvas')).x); + + $D('noVNC_container').style.overflow = "visible"; + + display.viewportChangeSize(new_w, size.h); + } + } + }, + + // Handle special cases where clipping is forced on/off or locked + enableDisableViewClip: function () { + var resizeElem = $D('noVNC_resize'); + var connected = UI.rfb && UI.rfb_state === 'normal'; + + if (resizeElem.value === 'downscale' || resizeElem.value === 'scale') { + // Disable clipping if we are scaling + UI.setViewClip(false); + $D('noVNC_clip').disabled = true; + } else if (document.msFullscreenElement) { + // The browser is IE and we are in fullscreen mode. + // - We need to force clipping while in fullscreen since + // scrollbars doesn't work. + UI.togglePopupStatus("Forcing clipping mode since scrollbars aren't supported by IE in fullscreen"); + UI.rememberedClipSetting = UI.getSetting('clip'); + UI.setViewClip(true); + $D('noVNC_clip').disabled = true; + } else if (document.body.msRequestFullscreen && UI.rememberedClip !== null) { + // Restore view clip to what it was before fullscreen on IE + UI.setViewClip(UI.rememberedClipSetting); + $D('noVNC_clip').disabled = connected || UI.isTouchDevice; + } else { + $D('noVNC_clip').disabled = connected || UI.isTouchDevice; + if (UI.isTouchDevice) { + UI.setViewClip(true); + } + } + }, + + // Update the viewport drag/move button + updateViewDrag: function(drag) { + if (!UI.rfb) return; + + var vmb = $D('noVNC_view_drag_button'); + + // Check if viewport drag is possible + if (UI.rfb_state === 'normal' && + UI.rfb.get_display().get_viewport() && + UI.rfb.get_display().clippingDisplay()) { + + // Show and enable the drag button + vmb.style.display = "inline"; + vmb.disabled = false; + + } else { + // The VNC content is the same size as + // or smaller than the display + + if (UI.rfb.get_viewportDrag) { + // Turn off viewport drag when it's + // active since it can't be used here + vmb.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } + + // Disable or hide the drag button + if (UI.rfb_state === 'normal' && UI.isTouchDevice) { + vmb.style.display = "inline"; + vmb.disabled = true; + } else { + vmb.style.display = "none"; + } + return; + } + + if (typeof(drag) !== "undefined" && + typeof(drag) !== "object") { + if (drag) { + vmb.className = "noVNC_status_button_selected"; + UI.rfb.set_viewportDrag(true); + } else { + vmb.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } + } + }, + + toggleViewDrag: function() { + if (!UI.rfb) return; + + var vmb = $D('noVNC_view_drag_button'); + if (UI.rfb.get_viewportDrag()) { + vmb.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } else { + vmb.className = "noVNC_status_button_selected"; + UI.rfb.set_viewportDrag(true); + } + }, + + // On touch devices, show the OS keyboard + showKeyboard: function() { + var kbi = $D('keyboardinput'); + var skb = $D('showKeyboard'); + var l = kbi.value.length; + if(UI.keyboardVisible === false) { + kbi.focus(); + try { kbi.setSelectionRange(l, l); } // Move the caret to the end + catch (err) {} // setSelectionRange is undefined in Google Chrome + UI.keyboardVisible = true; + skb.className = "noVNC_status_button_selected"; + } else if(UI.keyboardVisible === true) { + kbi.blur(); + skb.className = "noVNC_status_button"; + UI.keyboardVisible = false; + } + }, + + keepKeyboard: function() { + clearTimeout(UI.hideKeyboardTimeout); + if(UI.keyboardVisible === true) { + $D('keyboardinput').focus(); + $D('showKeyboard').className = "noVNC_status_button_selected"; + } else if(UI.keyboardVisible === false) { + $D('keyboardinput').blur(); + $D('showKeyboard').className = "noVNC_status_button"; + } + }, + + keyboardinputReset: function() { + var kbi = $D('keyboardinput'); + kbi.value = new Array(UI.defaultKeyboardinputLen).join("_"); + UI.lastKeyboardinput = kbi.value; + }, + + // When normal keyboard events are left uncought, use the input events from + // the keyboardinput element instead and generate the corresponding key events. + // This code is required since some browsers on Android are inconsistent in + // sending keyCodes in the normal keyboard events when using on screen keyboards. + keyInput: function(event) { + + if (!UI.rfb) return; + + var newValue = event.target.value; + + if (!UI.lastKeyboardinput) { + UI.keyboardinputReset(); + } + var oldValue = UI.lastKeyboardinput; + + var newLen; + try { + // Try to check caret position since whitespace at the end + // will not be considered by value.length in some browsers + newLen = Math.max(event.target.selectionStart, newValue.length); + } catch (err) { + // selectionStart is undefined in Google Chrome + newLen = newValue.length; + } + var oldLen = oldValue.length; + + var backspaces; + var inputs = newLen - oldLen; + if (inputs < 0) { + backspaces = -inputs; + } else { + backspaces = 0; + } + + // Compare the old string with the new to account for + // text-corrections or other input that modify existing text + var i; + for (i = 0; i < Math.min(oldLen, newLen); i++) { + if (newValue.charAt(i) != oldValue.charAt(i)) { + inputs = newLen - i; + backspaces = oldLen - i; + break; + } + } + + // Send the key events + for (i = 0; i < backspaces; i++) { + UI.rfb.sendKey(XK_BackSpace); + } + for (i = newLen - inputs; i < newLen; i++) { + UI.rfb.sendKey(newValue.charCodeAt(i)); + } + + // Control the text content length in the keyboardinput element + if (newLen > 2 * UI.defaultKeyboardinputLen) { + UI.keyboardinputReset(); + } else if (newLen < 1) { + // There always have to be some text in the keyboardinput + // element with which backspace can interact. + UI.keyboardinputReset(); + // This sometimes causes the keyboard to disappear for a second + // but it is required for the android keyboard to recognize that + // text has been added to the field + event.target.blur(); + // This has to be ran outside of the input handler in order to work + setTimeout(function() { UI.keepKeyboard(); }, 0); + } else { + UI.lastKeyboardinput = newValue; + } + }, + + keyInputBlur: function() { + $D('showKeyboard').className = "noVNC_status_button"; + //Weird bug in iOS if you change keyboardVisible + //here it does not actually occur so next time + //you click keyboard icon it doesnt work. + UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100); + }, + + showExtraKeys: function() { + UI.keepKeyboard(); + if(UI.extraKeysVisible === false) { + $D('toggleCtrlButton').style.display = "inline"; + $D('toggleAltButton').style.display = "inline"; + $D('sendTabButton').style.display = "inline"; + $D('sendEscButton').style.display = "inline"; + $D('showExtraKeysButton').className = "noVNC_status_button_selected"; + UI.extraKeysVisible = true; + } else if(UI.extraKeysVisible === true) { + $D('toggleCtrlButton').style.display = ""; + $D('toggleAltButton').style.display = ""; + $D('sendTabButton').style.display = ""; + $D('sendEscButton').style.display = ""; + $D('showExtraKeysButton').className = "noVNC_status_button"; + UI.extraKeysVisible = false; + } + }, + + toggleCtrl: function() { + UI.keepKeyboard(); + if(UI.ctrlOn === false) { + UI.rfb.sendKey(XK_Control_L, true); + $D('toggleCtrlButton').className = "noVNC_status_button_selected"; + UI.ctrlOn = true; + } else if(UI.ctrlOn === true) { + UI.rfb.sendKey(XK_Control_L, false); + $D('toggleCtrlButton').className = "noVNC_status_button"; + UI.ctrlOn = false; + } + }, + + toggleAlt: function() { + UI.keepKeyboard(); + if(UI.altOn === false) { + UI.rfb.sendKey(XK_Alt_L, true); + $D('toggleAltButton').className = "noVNC_status_button_selected"; + UI.altOn = true; + } else if(UI.altOn === true) { + UI.rfb.sendKey(XK_Alt_L, false); + $D('toggleAltButton').className = "noVNC_status_button"; + UI.altOn = false; + } + }, + + sendTab: function() { + UI.keepKeyboard(); + UI.rfb.sendKey(XK_Tab); + }, + + sendEsc: function() { + UI.keepKeyboard(); + UI.rfb.sendKey(XK_Escape); + }, + + setKeyboard: function() { + UI.keyboardVisible = false; + }, + + //Helper to add options to dropdown. + addOption: function(selectbox, text, value) { + var optn = document.createElement("OPTION"); + optn.text = text; + optn.value = value; + selectbox.options.add(optn); + }, + + setBarPosition: function() { + $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; + $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; + + var vncwidth = $D('noVNC_screen').style.offsetWidth; + $D('noVNC-control-bar').style.width = vncwidth + 'px'; + } + + }; +})(); diff --git a/plugins/dynamix.vm.manager/include/util.js b/plugins/dynamix.vm.manager/include/util.js new file mode 100644 index 000000000..ed0e3cdea --- /dev/null +++ b/plugins/dynamix.vm.manager/include/util.js @@ -0,0 +1,622 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* jshint white: false, nonstandard: true */ +/*global window, console, document, navigator, ActiveXObject, INCLUDE_URI */ + +// Globals defined here +var Util = {}; + + +/* + * Make arrays quack + */ + +var addFunc = function (cl, name, func) { + if (!cl.prototype[name]) { + Object.defineProperty(cl.prototype, name, { enumerable: false, value: func }); + } +}; + +addFunc(Array, 'push8', function (num) { + "use strict"; + this.push(num & 0xFF); +}); + +addFunc(Array, 'push16', function (num) { + "use strict"; + this.push((num >> 8) & 0xFF, + num & 0xFF); +}); + +addFunc(Array, 'push32', function (num) { + "use strict"; + this.push((num >> 24) & 0xFF, + (num >> 16) & 0xFF, + (num >> 8) & 0xFF, + num & 0xFF); +}); + +// IE does not support map (even in IE9) +//This prototype is provided by the Mozilla foundation and +//is distributed under the MIT license. +//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license +addFunc(Array, 'map', function (fun /*, thisp*/) { + "use strict"; + var len = this.length; + if (typeof fun != "function") { + throw new TypeError(); + } + + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in this) { + res[i] = fun.call(thisp, this[i], i, this); + } + } + + return res; +}); + +// IE <9 does not support indexOf +//This prototype is provided by the Mozilla foundation and +//is distributed under the MIT license. +//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license +addFunc(Array, 'indexOf', function (elt /*, from*/) { + "use strict"; + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) { + from += len; + } + + for (; from < len; from++) { + if (from in this && + this[from] === elt) { + return from; + } + } + return -1; +}); + +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys +if (!Object.keys) { + Object.keys = (function () { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function (obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + })(); +} + +// PhantomJS 1.x doesn't support bind, +// so leave this in until PhantomJS 2.0 is released +//This prototype is provided by the Mozilla foundation and +//is distributed under the MIT license. +//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license +addFunc(Function, 'bind', function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError("Function.prototype.bind - " + + "what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; +}); + +// +// requestAnimationFrame shim with setTimeout fallback +// + +window.requestAnimFrame = (function () { + "use strict"; + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60); + }; +})(); + +/* + * ------------------------------------------------------ + * Namespaced in Util + * ------------------------------------------------------ + */ + +/* + * Logging/debug routines + */ + +Util._log_level = 'warn'; +Util.init_logging = function (level) { + "use strict"; + if (typeof level === 'undefined') { + level = Util._log_level; + } else { + Util._log_level = level; + } + if (typeof window.console === "undefined") { + if (typeof window.opera !== "undefined") { + window.console = { + 'log' : window.opera.postError, + 'warn' : window.opera.postError, + 'error': window.opera.postError + }; + } else { + window.console = { + 'log' : function (m) {}, + 'warn' : function (m) {}, + 'error': function (m) {} + }; + } + } + + Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; + /* jshint -W086 */ + switch (level) { + case 'debug': + Util.Debug = function (msg) { console.log(msg); }; + case 'info': + Util.Info = function (msg) { console.log(msg); }; + case 'warn': + Util.Warn = function (msg) { console.warn(msg); }; + case 'error': + Util.Error = function (msg) { console.error(msg); }; + case 'none': + break; + default: + throw new Error("invalid logging type '" + level + "'"); + } + /* jshint +W086 */ +}; +Util.get_logging = function () { + return Util._log_level; +}; +// Initialize logging level +Util.init_logging(); + +Util.make_property = function (proto, name, mode, type) { + "use strict"; + + var getter; + if (type === 'arr') { + getter = function (idx) { + if (typeof idx !== 'undefined') { + return this['_' + name][idx]; + } else { + return this['_' + name]; + } + }; + } else { + getter = function () { + return this['_' + name]; + }; + } + + var make_setter = function (process_val) { + if (process_val) { + return function (val, idx) { + if (typeof idx !== 'undefined') { + this['_' + name][idx] = process_val(val); + } else { + this['_' + name] = process_val(val); + } + }; + } else { + return function (val, idx) { + if (typeof idx !== 'undefined') { + this['_' + name][idx] = val; + } else { + this['_' + name] = val; + } + }; + } + }; + + var setter; + if (type === 'bool') { + setter = make_setter(function (val) { + if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) { + return false; + } else { + return true; + } + }); + } else if (type === 'int') { + setter = make_setter(function (val) { return parseInt(val, 10); }); + } else if (type === 'float') { + setter = make_setter(parseFloat); + } else if (type === 'str') { + setter = make_setter(String); + } else if (type === 'func') { + setter = make_setter(function (val) { + if (!val) { + return function () {}; + } else { + return val; + } + }); + } else if (type === 'arr' || type === 'dom' || type == 'raw') { + setter = make_setter(); + } else { + throw new Error('Unknown property type ' + type); // some sanity checking + } + + // set the getter + if (typeof proto['get_' + name] === 'undefined') { + proto['get_' + name] = getter; + } + + // set the setter if needed + if (typeof proto['set_' + name] === 'undefined') { + if (mode === 'rw') { + proto['set_' + name] = setter; + } else if (mode === 'wo') { + proto['set_' + name] = function (val, idx) { + if (typeof this['_' + name] !== 'undefined') { + throw new Error(name + " can only be set once"); + } + setter.call(this, val, idx); + }; + } + } + + // make a special setter that we can use in set defaults + proto['_raw_set_' + name] = function (val, idx) { + setter.call(this, val, idx); + //delete this['_init_set_' + name]; // remove it after use + }; +}; + +Util.make_properties = function (constructor, arr) { + "use strict"; + for (var i = 0; i < arr.length; i++) { + Util.make_property(constructor.prototype, arr[i][0], arr[i][1], arr[i][2]); + } +}; + +Util.set_defaults = function (obj, conf, defaults) { + var defaults_keys = Object.keys(defaults); + var conf_keys = Object.keys(conf); + var keys_obj = {}; + var i; + for (i = 0; i < defaults_keys.length; i++) { keys_obj[defaults_keys[i]] = 1; } + for (i = 0; i < conf_keys.length; i++) { keys_obj[conf_keys[i]] = 1; } + var keys = Object.keys(keys_obj); + + for (i = 0; i < keys.length; i++) { + var setter = obj['_raw_set_' + keys[i]]; + if (!setter) { + Util.Warn('Invalid property ' + keys[i]); + continue; + } + + if (keys[i] in conf) { + setter.call(obj, conf[keys[i]]); + } else { + setter.call(obj, defaults[keys[i]]); + } + } +}; + +/* + * Decode from UTF-8 + */ +Util.decodeUTF8 = function (utf8string) { + "use strict"; + return decodeURIComponent(escape(utf8string)); +}; + + + +/* + * Cross-browser routines + */ + + +// Dynamically load scripts without using document.write() +// Reference: http://unixpapa.com/js/dyna.html +// +// Handles the case where load_scripts is invoked from a script that +// itself is loaded via load_scripts. Once all scripts are loaded the +// window.onscriptsloaded handler is called (if set). +Util.get_include_uri = function () { + return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/"; +}; +Util._loading_scripts = []; +Util._pending_scripts = []; +Util.load_scripts = function (files) { + "use strict"; + var head = document.getElementsByTagName('head')[0], script, + ls = Util._loading_scripts, ps = Util._pending_scripts; + + var loadFunc = function (e) { + while (ls.length > 0 && (ls[0].readyState === 'loaded' || + ls[0].readyState === 'complete')) { + // For IE, append the script to trigger execution + var s = ls.shift(); + //console.log("loaded script: " + s.src); + head.appendChild(s); + } + if (!this.readyState || + (Util.Engine.presto && this.readyState === 'loaded') || + this.readyState === 'complete') { + if (ps.indexOf(this) >= 0) { + this.onload = this.onreadystatechange = null; + //console.log("completed script: " + this.src); + ps.splice(ps.indexOf(this), 1); + + // Call window.onscriptsload after last script loads + if (ps.length === 0 && window.onscriptsload) { + window.onscriptsload(); + } + } + } + }; + + for (var f = 0; f < files.length; f++) { + script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = Util.get_include_uri() + files[f]; + //console.log("loading script: " + script.src); + script.onload = script.onreadystatechange = loadFunc; + // In-order script execution tricks + if (Util.Engine.trident) { + // For IE wait until readyState is 'loaded' before + // appending it which will trigger execution + // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order + ls.push(script); + } else { + // For webkit and firefox set async=false and append now + // https://developer.mozilla.org/en-US/docs/HTML/Element/script + script.async = false; + head.appendChild(script); + } + ps.push(script); + } +}; + + +Util.getPosition = function(obj) { + "use strict"; + // NB(sross): the Mozilla developer reference seems to indicate that + // getBoundingClientRect includes border and padding, so the canvas + // style should NOT include either. + var objPosition = obj.getBoundingClientRect(); + return {'x': objPosition.left + window.pageXOffset, 'y': objPosition.top + window.pageYOffset, + 'width': objPosition.width, 'height': objPosition.height}; +}; + + +// Get mouse event position in DOM element +Util.getEventPosition = function (e, obj, scale) { + "use strict"; + var evt, docX, docY, pos; + //if (!e) evt = window.event; + evt = (e ? e : window.event); + evt = (evt.changedTouches ? evt.changedTouches[0] : evt.touches ? evt.touches[0] : evt); + if (evt.pageX || evt.pageY) { + docX = evt.pageX; + docY = evt.pageY; + } else if (evt.clientX || evt.clientY) { + docX = evt.clientX + document.body.scrollLeft + + document.documentElement.scrollLeft; + docY = evt.clientY + document.body.scrollTop + + document.documentElement.scrollTop; + } + pos = Util.getPosition(obj); + if (typeof scale === "undefined") { + scale = 1; + } + var realx = docX - pos.x; + var realy = docY - pos.y; + var x = Math.max(Math.min(realx, pos.width - 1), 0); + var y = Math.max(Math.min(realy, pos.height - 1), 0); + return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale}; +}; + + +// Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events +Util.addEvent = function (obj, evType, fn) { + "use strict"; + if (obj.attachEvent) { + var r = obj.attachEvent("on" + evType, fn); + return r; + } else if (obj.addEventListener) { + obj.addEventListener(evType, fn, false); + return true; + } else { + throw new Error("Handler could not be attached"); + } +}; + +Util.removeEvent = function (obj, evType, fn) { + "use strict"; + if (obj.detachEvent) { + var r = obj.detachEvent("on" + evType, fn); + return r; + } else if (obj.removeEventListener) { + obj.removeEventListener(evType, fn, false); + return true; + } else { + throw new Error("Handler could not be removed"); + } +}; + +Util.stopEvent = function (e) { + "use strict"; + if (e.stopPropagation) { e.stopPropagation(); } + else { e.cancelBubble = true; } + + if (e.preventDefault) { e.preventDefault(); } + else { e.returnValue = false; } +}; + +Util._cursor_uris_supported = null; + +Util.browserSupportsCursorURIs = function () { + if (Util._cursor_uris_supported === null) { + try { + var target = document.createElement('canvas'); + target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; + + if (target.style.cursor) { + Util.Info("Data URI scheme cursor supported"); + Util._cursor_uris_supported = true; + } else { + Util.Warn("Data URI scheme cursor not supported"); + Util._cursor_uris_supported = false; + } + } catch (exc) { + Util.Error("Data URI scheme cursor test exception: " + exc); + Util._cursor_uris_supported = false; + } + } + + return Util._cursor_uris_supported; +}; + +// Set browser engine versions. Based on mootools. +Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)}; + +(function () { + "use strict"; + // 'presto': (function () { return (!window.opera) ? false : true; }()), + var detectPresto = function () { + return !!window.opera; + }; + + // 'trident': (function () { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); + var detectTrident = function () { + if (!window.ActiveXObject) { + return false; + } else { + if (window.XMLHttpRequest) { + return (document.querySelectorAll) ? 6 : 5; + } else { + return 4; + } + } + }; + + // 'webkit': (function () { try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), + var detectInitialWebkit = function () { + try { + if (navigator.taintEnabled) { + return false; + } else { + if (Util.Features.xpath) { + return (Util.Features.query) ? 525 : 420; + } else { + return 419; + } + } + } catch (e) { + return false; + } + }; + + var detectActualWebkit = function (initial_ver) { + var re = /WebKit\/([0-9\.]*) /; + var str_ver = (navigator.userAgent.match(re) || ['', initial_ver])[1]; + return parseFloat(str_ver, 10); + }; + + // 'gecko': (function () { return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19ssName) ? 19 : 18 : 18); }()) + var detectGecko = function () { + /* jshint -W041 */ + if (!document.getBoxObjectFor && window.mozInnerScreenX == null) { + return false; + } else { + return (document.getElementsByClassName) ? 19 : 18; + } + /* jshint +W041 */ + }; + + Util.Engine = { + // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) + //'presto': (function() { + // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), + 'presto': detectPresto(), + 'trident': detectTrident(), + 'webkit': detectInitialWebkit(), + 'gecko': detectGecko(), + }; + + if (Util.Engine.webkit) { + // Extract actual webkit version if available + Util.Engine.webkit = detectActualWebkit(Util.Engine.webkit); + } +})(); + +Util.Flash = (function () { + "use strict"; + var v, version; + try { + v = navigator.plugins['Shockwave Flash'].description; + } catch (err1) { + try { + v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); + } catch (err2) { + v = '0 r0'; + } + } + version = v.match(/\d+/g); + return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0}; +}()); diff --git a/plugins/dynamix.vm.manager/include/websock.js b/plugins/dynamix.vm.manager/include/websock.js new file mode 100644 index 000000000..892238b5e --- /dev/null +++ b/plugins/dynamix.vm.manager/include/websock.js @@ -0,0 +1,410 @@ +/* + * Websock: high-performance binary WebSockets + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * Websock is similar to the standard WebSocket object but Websock + * enables communication with raw TCP sockets (i.e. the binary stream) + * via websockify. This is accomplished by base64 encoding the data + * stream between Websock and websockify. + * + * Websock has built-in receive queue buffering; the message event + * does not contain actual data but is simply a notification that + * there is new data available. Several rQ* methods are available to + * read binary data off of the receive queue. + */ + +/*jslint browser: true, bitwise: true */ +/*global Util*/ + + +// Load Flash WebSocket emulator if needed + +// To force WebSocket emulator even when native WebSocket available +//window.WEB_SOCKET_FORCE_FLASH = true; +// To enable WebSocket emulator debug: +//window.WEB_SOCKET_DEBUG=1; + +if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) { + Websock_native = true; +} else if (window.MozWebSocket && !window.WEB_SOCKET_FORCE_FLASH) { + Websock_native = true; + window.WebSocket = window.MozWebSocket; +} else { + /* no builtin WebSocket so load web_socket.js */ + + Websock_native = false; +} + +function Websock() { + "use strict"; + + this._websocket = null; // WebSocket object + + this._rQi = 0; // Receive queue index + this._rQlen = 0; // Next write position in the receive queue + this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB) + this._rQmax = this._rQbufferSize / 8; + // called in init: this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ = null; // Receive queue + + this._sQbufferSize = 1024 * 10; // 10 KiB + // called in init: this._sQ = new Uint8Array(this._sQbufferSize); + this._sQlen = 0; + this._sQ = null; // Send queue + + this._mode = 'binary'; // Current WebSocket mode: 'binary', 'base64' + this.maxBufferedAmount = 200; + + this._eventHandlers = { + 'message': function () {}, + 'open': function () {}, + 'close': function () {}, + 'error': function () {} + }; +} + +(function () { + "use strict"; + + var typedArrayToString = (function () { + // This is only for PhantomJS, which doesn't like apply-ing + // with Typed Arrays + try { + var arr = new Uint8Array([1, 2, 3]); + String.fromCharCode.apply(null, arr); + return function (a) { return String.fromCharCode.apply(null, a); }; + } catch (ex) { + return function (a) { + return String.fromCharCode.apply( + null, Array.prototype.slice.call(a)); + }; + } + })(); + + Websock.prototype = { + // Getters and Setters + get_sQ: function () { + return this._sQ; + }, + + get_rQ: function () { + return this._rQ; + }, + + get_rQi: function () { + return this._rQi; + }, + + set_rQi: function (val) { + this._rQi = val; + }, + + // Receive Queue + rQlen: function () { + return this._rQlen - this._rQi; + }, + + rQpeek8: function () { + return this._rQ[this._rQi]; + }, + + rQshift8: function () { + return this._rQ[this._rQi++]; + }, + + rQskip8: function () { + this._rQi++; + }, + + rQskipBytes: function (num) { + this._rQi += num; + }, + + // TODO(directxman12): test performance with these vs a DataView + rQshift16: function () { + return (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + }, + + rQshift32: function () { + return (this._rQ[this._rQi++] << 24) + + (this._rQ[this._rQi++] << 16) + + (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + }, + + rQshiftStr: function (len) { + if (typeof(len) === 'undefined') { len = this.rQlen(); } + var arr = new Uint8Array(this._rQ.buffer, this._rQi, len); + this._rQi += len; + return typedArrayToString(arr); + }, + + rQshiftBytes: function (len) { + if (typeof(len) === 'undefined') { len = this.rQlen(); } + this._rQi += len; + return new Uint8Array(this._rQ.buffer, this._rQi - len, len); + }, + + rQshiftTo: function (target, len) { + if (len === undefined) { len = this.rQlen(); } + // TODO: make this just use set with views when using a ArrayBuffer to store the rQ + target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); + this._rQi += len; + }, + + rQwhole: function () { + return new Uint8Array(this._rQ.buffer, 0, this._rQlen); + }, + + rQslice: function (start, end) { + if (end) { + return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); + } else { + return new Uint8Array(this._rQ.buffer, this._rQi + start, this._rQlen - this._rQi - start); + } + }, + + // Check to see if we must wait for 'num' bytes (default to FBU.bytes) + // to be available in the receive queue. Return true if we need to + // wait (and possibly print a debug message), otherwise false. + rQwait: function (msg, num, goback) { + var rQlen = this._rQlen - this._rQi; // Skip rQlen() function call + if (rQlen < num) { + if (goback) { + if (this._rQi < goback) { + throw new Error("rQwait cannot backup " + goback + " bytes"); + } + this._rQi -= goback; + } + return true; // true means need more data + } + return false; + }, + + // Send Queue + + flush: function () { + if (this._websocket.bufferedAmount !== 0) { + Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount); + } + + if (this._websocket.bufferedAmount < this.maxBufferedAmount) { + if (this._sQlen > 0) { + this._websocket.send(this._encode_message()); + this._sQlen = 0; + } + + return true; + } else { + Util.Info("Delaying send, bufferedAmount: " + + this._websocket.bufferedAmount); + return false; + } + }, + + send: function (arr) { + this._sQ.set(arr, this._sQlen); + this._sQlen += arr.length; + return this.flush(); + }, + + send_string: function (str) { + this.send(str.split('').map(function (chr) { + return chr.charCodeAt(0); + })); + }, + + // Event Handlers + off: function (evt) { + this._eventHandlers[evt] = function () {}; + }, + + on: function (evt, handler) { + this._eventHandlers[evt] = handler; + }, + + _allocate_buffers: function () { + this._rQ = new Uint8Array(this._rQbufferSize); + this._sQ = new Uint8Array(this._sQbufferSize); + }, + + init: function (protocols, ws_schema) { + this._allocate_buffers(); + this._rQi = 0; + this._websocket = null; + + // Check for full typed array support + var bt = false; + if (('Uint8Array' in window) && + ('set' in Uint8Array.prototype)) { + bt = true; + } + + // Check for full binary type support in WebSockets + // Inspired by: + // https://github.com/Modernizr/Modernizr/issues/370 + // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js + var wsbt = false; + try { + if (bt && ('binaryType' in WebSocket.prototype || + !!(new WebSocket(ws_schema + '://.').binaryType))) { + Util.Info("Detected binaryType support in WebSockets"); + wsbt = true; + } + } catch (exc) { + // Just ignore failed test localhost connection + } + + // Default protocols if not specified + if (typeof(protocols) === "undefined") { + protocols = 'binary'; + } + + if (Array.isArray(protocols) && protocols.indexOf('binary') > -1) { + protocols = 'binary'; + } + + if (!wsbt) { + throw new Error("noVNC no longer supports base64 WebSockets. " + + "Please use a browser which supports binary WebSockets."); + } + + if (protocols != 'binary') { + throw new Error("noVNC no longer supports base64 WebSockets. Please " + + "use the binary subprotocol instead."); + } + + return protocols; + }, + + open: function (uri, protocols) { + var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; + protocols = this.init(protocols, ws_schema); + + this._websocket = new WebSocket(uri, protocols); + + if (protocols.indexOf('binary') >= 0) { + this._websocket.binaryType = 'arraybuffer'; + } + + this._websocket.onmessage = this._recv_message.bind(this); + this._websocket.onopen = (function () { + Util.Debug('>> WebSock.onopen'); + if (this._websocket.protocol) { + this._mode = this._websocket.protocol; + Util.Info("Server choose sub-protocol: " + this._websocket.protocol); + } else { + this._mode = 'binary'; + Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol); + } + + if (this._mode != 'binary') { + throw new Error("noVNC no longer supports base64 WebSockets. Please " + + "use the binary subprotocol instead."); + + } + + this._eventHandlers.open(); + Util.Debug("<< WebSock.onopen"); + }).bind(this); + this._websocket.onclose = (function (e) { + Util.Debug(">> WebSock.onclose"); + this._eventHandlers.close(e); + Util.Debug("<< WebSock.onclose"); + }).bind(this); + this._websocket.onerror = (function (e) { + Util.Debug(">> WebSock.onerror: " + e); + this._eventHandlers.error(e); + Util.Debug("<< WebSock.onerror: " + e); + }).bind(this); + }, + + close: function () { + if (this._websocket) { + if ((this._websocket.readyState === WebSocket.OPEN) || + (this._websocket.readyState === WebSocket.CONNECTING)) { + Util.Info("Closing WebSocket connection"); + this._websocket.close(); + } + + this._websocket.onmessage = function (e) { return; }; + } + }, + + // private methods + _encode_message: function () { + // Put in a binary arraybuffer + // according to the spec, you can send ArrayBufferViews with the send method + return new Uint8Array(this._sQ.buffer, 0, this._sQlen); + }, + + _decode_message: function (data) { + // push arraybuffer values onto the end + var u8 = new Uint8Array(data); + this._rQ.set(u8, this._rQlen); + this._rQlen += u8.length; + }, + + _recv_message: function (e) { + try { + this._decode_message(e.data); + if (this.rQlen() > 0) { + this._eventHandlers.message(); + // Compact the receive queue + if (this._rQlen == this._rQi) { + this._rQlen = 0; + this._rQi = 0; + } else if (this._rQlen > this._rQmax) { + if (this._rQlen - this._rQi > 0.5 * this._rQbufferSize) { + var old_rQbuffer = this._rQ.buffer; + this._rQbufferSize *= 2; + this._rQmax = this._rQbufferSize / 8; + this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); + } else { + if (this._rQ.copyWithin) { + // Firefox only, ATM + this._rQ.copyWithin(0, this._rQi); + } else { + this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); + } + } + + this._rQlen = this._rQlen - this._rQi; + this._rQi = 0; + } + } else { + Util.Debug("Ignoring empty message"); + } + } catch (exc) { + var exception_str = ""; + if (exc.name) { + exception_str += "\n name: " + exc.name + "\n"; + exception_str += " message: " + exc.message + "\n"; + } + + if (typeof exc.description !== 'undefined') { + exception_str += " description: " + exc.description + "\n"; + } + + if (typeof exc.stack !== 'undefined') { + exception_str += exc.stack; + } + + if (exception_str.length > 0) { + Util.Error("recv_message, caught exception: " + exception_str); + } else { + Util.Error("recv_message, caught exception: " + exc); + } + + if (typeof exc.name !== 'undefined') { + this._eventHandlers.error(exc.name + ": " + exc.message); + } else { + this._eventHandlers.error(exc); + } + } + } + }; +})(); diff --git a/plugins/dynamix.vm.manager/include/webutil.js b/plugins/dynamix.vm.manager/include/webutil.js new file mode 100644 index 000000000..e674bf94f --- /dev/null +++ b/plugins/dynamix.vm.manager/include/webutil.js @@ -0,0 +1,239 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 NTT corp. + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/*jslint bitwise: false, white: false, browser: true, devel: true */ +/*global Util, window, document */ + +// Globals defined here +var WebUtil = {}, $D; + +/* + * Simple DOM selector by ID + */ +if (!window.$D) { + window.$D = function (id) { + if (document.getElementById) { + return document.getElementById(id); + } else if (document.all) { + return document.all[id]; + } else if (document.layers) { + return document.layers[id]; + } + return undefined; + }; +} + + +/* + * ------------------------------------------------------ + * Namespaced in WebUtil + * ------------------------------------------------------ + */ + +// init log level reading the logging HTTP param +WebUtil.init_logging = function (level) { + "use strict"; + if (typeof level !== "undefined") { + Util._log_level = level; + } else { + var param = document.location.href.match(/logging=([A-Za-z0-9\._\-]*)/); + Util._log_level = (param || ['', Util._log_level])[1]; + } + Util.init_logging(); +}; + + +WebUtil.dirObj = function (obj, depth, parent) { + "use strict"; + if (! depth) { depth = 2; } + if (! parent) { parent = ""; } + + // Print the properties of the passed-in object + var msg = ""; + for (var i in obj) { + if ((depth > 1) && (typeof obj[i] === "object")) { + // Recurse attributes that are objects + msg += WebUtil.dirObj(obj[i], depth - 1, parent + "." + i); + } else { + //val = new String(obj[i]).replace("\n", " "); + var val = ""; + if (typeof(obj[i]) === "undefined") { + val = "undefined"; + } else { + val = obj[i].toString().replace("\n", " "); + } + if (val.length > 30) { + val = val.substr(0, 30) + "..."; + } + msg += parent + "." + i + ": " + val + "\n"; + } + } + return msg; +}; + +// Read a query string variable +WebUtil.getQueryVar = function (name, defVal) { + "use strict"; + var re = new RegExp('.*[?&]' + name + '=([^&#]*)'), + match = document.location.href.match(re); + if (typeof defVal === 'undefined') { defVal = null; } + if (match) { + return decodeURIComponent(match[1]); + } else { + return defVal; + } +}; + + +/* + * Cookie handling. Dervied from: http://www.quirksmode.org/js/cookies.html + */ + +// No days means only for this browser session +WebUtil.createCookie = function (name, value, days) { + "use strict"; + var date, expires; + if (days) { + date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toGMTString(); + } else { + expires = ""; + } + + var secure; + if (document.location.protocol === "https:") { + secure = "; secure"; + } else { + secure = ""; + } + document.cookie = name + "=" + value + expires + "; path=/" + secure; +}; + +WebUtil.readCookie = function (name, defaultValue) { + "use strict"; + var nameEQ = name + "=", + ca = document.cookie.split(';'); + + for (var i = 0; i < ca.length; i += 1) { + var c = ca[i]; + while (c.charAt(0) === ' ') { c = c.substring(1, c.length); } + if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length, c.length); } + } + return (typeof defaultValue !== 'undefined') ? defaultValue : null; +}; + +WebUtil.eraseCookie = function (name) { + "use strict"; + WebUtil.createCookie(name, "", -1); +}; + +/* + * Setting handling. + */ + +WebUtil.initSettings = function (callback /*, ...callbackArgs */) { + "use strict"; + var callbackArgs = Array.prototype.slice.call(arguments, 1); + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.get(function (cfg) { + WebUtil.settings = cfg; + console.log(WebUtil.settings); + if (callback) { + callback.apply(this, callbackArgs); + } + }); + } else { + // No-op + if (callback) { + callback.apply(this, callbackArgs); + } + } +}; + +// No days means only for this browser session +WebUtil.writeSetting = function (name, value) { + "use strict"; + if (window.chrome && window.chrome.storage) { + //console.log("writeSetting:", name, value); + if (WebUtil.settings[name] !== value) { + WebUtil.settings[name] = value; + window.chrome.storage.sync.set(WebUtil.settings); + } + } else { + localStorage.setItem(name, value); + } +}; + +WebUtil.readSetting = function (name, defaultValue) { + "use strict"; + var value; + if (window.chrome && window.chrome.storage) { + value = WebUtil.settings[name]; + } else { + value = localStorage.getItem(name); + } + if (typeof value === "undefined") { + value = null; + } + if (value === null && typeof defaultValue !== undefined) { + return defaultValue; + } else { + return value; + } +}; + +WebUtil.eraseSetting = function (name) { + "use strict"; + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.remove(name); + delete WebUtil.settings[name]; + } else { + localStorage.removeItem(name); + } +}; + +/* + * Alternate stylesheet selection + */ +WebUtil.getStylesheets = function () { + "use strict"; + var links = document.getElementsByTagName("link"); + var sheets = []; + + for (var i = 0; i < links.length; i += 1) { + if (links[i].title && + links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) { + sheets.push(links[i]); + } + } + return sheets; +}; + +// No sheet means try and use value from cookie, null sheet used to +// clear all alternates. +WebUtil.selectStylesheet = function (sheet) { + "use strict"; + if (typeof sheet === 'undefined') { + sheet = 'default'; + } + + var sheets = WebUtil.getStylesheets(); + for (var i = 0; i < sheets.length; i += 1) { + var link = sheets[i]; + if (link.title === sheet) { + Util.Debug("Using stylesheet " + sheet); + link.disabled = false; + } else { + //Util.Debug("Skipping stylesheet " + link.title); + link.disabled = true; + } + } + return sheet; +}; diff --git a/plugins/dynamix.vm.manager/scripts/codemirror/addon/display/placeholder.js b/plugins/dynamix.vm.manager/scripts/codemirror/addon/display/placeholder.js new file mode 100644 index 000000000..bb0c3931e --- /dev/null +++ b/plugins/dynamix.vm.manager/scripts/codemirror/addon/display/placeholder.js @@ -0,0 +1,58 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + CodeMirror.defineOption("placeholder", "", function(cm, val, old) { + var prev = old && old != CodeMirror.Init; + if (val && !prev) { + cm.on("blur", onBlur); + cm.on("change", onChange); + onChange(cm); + } else if (!val && prev) { + cm.off("blur", onBlur); + cm.off("change", onChange); + clearPlaceholder(cm); + var wrapper = cm.getWrapperElement(); + wrapper.className = wrapper.className.replace(" CodeMirror-empty", ""); + } + + if (val && !cm.hasFocus()) onBlur(cm); + }); + + function clearPlaceholder(cm) { + if (cm.state.placeholder) { + cm.state.placeholder.parentNode.removeChild(cm.state.placeholder); + cm.state.placeholder = null; + } + } + function setPlaceholder(cm) { + clearPlaceholder(cm); + var elt = cm.state.placeholder = document.createElement("pre"); + elt.style.cssText = "height: 0; overflow: visible"; + elt.className = "CodeMirror-placeholder"; + elt.appendChild(document.createTextNode(cm.getOption("placeholder"))); + cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild); + } + + function onBlur(cm) { + if (isEmpty(cm)) setPlaceholder(cm); + } + function onChange(cm) { + var wrapper = cm.getWrapperElement(), empty = isEmpty(cm); + wrapper.className = wrapper.className.replace(" CodeMirror-empty", "") + (empty ? " CodeMirror-empty" : ""); + + if (empty) setPlaceholder(cm); + else clearPlaceholder(cm); + } + + function isEmpty(cm) { + return (cm.lineCount() === 1) && (cm.getLine(0) === ""); + } +}); diff --git a/plugins/dynamix.vm.manager/scripts/codemirror/addon/fold/foldcode.js b/plugins/dynamix.vm.manager/scripts/codemirror/addon/fold/foldcode.js new file mode 100644 index 000000000..62911f935 --- /dev/null +++ b/plugins/dynamix.vm.manager/scripts/codemirror/addon/fold/foldcode.js @@ -0,0 +1,149 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function doFold(cm, pos, options, force) { + if (options && options.call) { + var finder = options; + options = null; + } else { + var finder = getOption(cm, options, "rangeFinder"); + } + if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0); + var minSize = getOption(cm, options, "minFoldSize"); + + function getRange(allowFolded) { + var range = finder(cm, pos); + if (!range || range.to.line - range.from.line < minSize) return null; + var marks = cm.findMarksAt(range.from); + for (var i = 0; i < marks.length; ++i) { + if (marks[i].__isFold && force !== "fold") { + if (!allowFolded) return null; + range.cleared = true; + marks[i].clear(); + } + } + return range; + } + + var range = getRange(true); + if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) { + pos = CodeMirror.Pos(pos.line - 1, 0); + range = getRange(false); + } + if (!range || range.cleared || force === "unfold") return; + + var myWidget = makeWidget(cm, options); + CodeMirror.on(myWidget, "mousedown", function(e) { + myRange.clear(); + CodeMirror.e_preventDefault(e); + }); + var myRange = cm.markText(range.from, range.to, { + replacedWith: myWidget, + clearOnEnter: true, + __isFold: true + }); + myRange.on("clear", function(from, to) { + CodeMirror.signal(cm, "unfold", cm, from, to); + }); + CodeMirror.signal(cm, "fold", cm, range.from, range.to); + } + + function makeWidget(cm, options) { + var widget = getOption(cm, options, "widget"); + if (typeof widget == "string") { + var text = document.createTextNode(widget); + widget = document.createElement("span"); + widget.appendChild(text); + widget.className = "CodeMirror-foldmarker"; + } + return widget; + } + + // Clumsy backwards-compatible interface + CodeMirror.newFoldFunction = function(rangeFinder, widget) { + return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; + }; + + // New-style interface + CodeMirror.defineExtension("foldCode", function(pos, options, force) { + doFold(this, pos, options, force); + }); + + CodeMirror.defineExtension("isFolded", function(pos) { + var marks = this.findMarksAt(pos); + for (var i = 0; i < marks.length; ++i) + if (marks[i].__isFold) return true; + }); + + CodeMirror.commands.toggleFold = function(cm) { + cm.foldCode(cm.getCursor()); + }; + CodeMirror.commands.fold = function(cm) { + cm.foldCode(cm.getCursor(), null, "fold"); + }; + CodeMirror.commands.unfold = function(cm) { + cm.foldCode(cm.getCursor(), null, "unfold"); + }; + CodeMirror.commands.foldAll = function(cm) { + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) + cm.foldCode(CodeMirror.Pos(i, 0), null, "fold"); + }); + }; + CodeMirror.commands.unfoldAll = function(cm) { + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) + cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold"); + }); + }; + + CodeMirror.registerHelper("fold", "combine", function() { + var funcs = Array.prototype.slice.call(arguments, 0); + return function(cm, start) { + for (var i = 0; i < funcs.length; ++i) { + var found = funcs[i](cm, start); + if (found) return found; + } + }; + }); + + CodeMirror.registerHelper("fold", "auto", function(cm, start) { + var helpers = cm.getHelpers(start, "fold"); + for (var i = 0; i < helpers.length; i++) { + var cur = helpers[i](cm, start); + if (cur) return cur; + } + }); + + var defaultOptions = { + rangeFinder: CodeMirror.fold.auto, + widget: "\u2194", + minFoldSize: 0, + scanUp: false + }; + + CodeMirror.defineOption("foldOptions", null); + + function getOption(cm, options, name) { + if (options && options[name] !== undefined) + return options[name]; + var editorOptions = cm.options.foldOptions; + if (editorOptions && editorOptions[name] !== undefined) + return editorOptions[name]; + return defaultOptions[name]; + } + + CodeMirror.defineExtension("foldOption", function(options, name) { + return getOption(this, options, name); + }); +}); diff --git a/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/libvirt-schema.js b/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/libvirt-schema.js new file mode 100644 index 000000000..5881c0c55 --- /dev/null +++ b/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/libvirt-schema.js @@ -0,0 +1,282 @@ +function getLibvirtSchema() { + + var root = {}; + + root.domain = { + "!attrs": { + type: ["kvm"], + "xmlns:qemu": ["http://libvirt.org/schemas/domain/qemu/1.0"] + } + }; + + root.domain.name = { + "!value": "" + }; + + root.domain.description = { + "!value": "" + }; + + root.domain.memory = { + "!attrs": { + unit: ["MiB", "KiB", "GiB"] + }, + "!value": 512 + }; + + root.domain.currentMemory = { + "!attrs": { + unit: ["MiB", "KiB", "GiB"] + }, + "!value": 512 + }; + + root.domain.memoryBacking = {}; + root.domain.memoryBacking.nosharepages = { + "!novalue": 1 + }; + root.domain.memoryBacking.locked = { + "!novalue": 1 + }; + + root.domain.vcpu = { + "!attrs": { + placement: ["static"] + }, + "!value": 1 + }; + + root.domain.cputune = {}; + root.domain.cputune.vcpupin = { + "!attrs": { + vcpu: null, + cpuset: null + } + }; + + root.domain.cpu = { + "!attrs": { + mode: ["host-passthrough"] + } + }; + root.domain.cpu.topology = { + "!attrs": { + sockets: null, + cores: null, + threads: null + } + }; + + root.domain.os = {}; + root.domain.os.type = { + "!attrs": { + arch: ["x86_64"], + machine: ["pc", "q35"] + }, + "!value": "hvm" + }; + root.domain.os.loader = { + "!attrs": { + type: ["pflash"] + }, + "!value": "/usr/share/qemu/ovmf-x64/OVMF-pure-efi.fd" + }; + + root.domain.features = {}; + root.domain.features.acpi = { + "!novalue": 1 + }; + root.domain.features.apic = { + "!novalue": 1 + }; + root.domain.features.hyperv = {}; + root.domain.features.hyperv.relaxed = { + "!attrs": { + state: ["on", "off"] + } + }; + root.domain.features.hyperv.vapic = { + "!attrs": { + state: ["on", "off"] + } + }; + root.domain.features.hyperv.spinlocks = { + "!attrs": { + state: ["on", "off"], + retries: null + } + }; + root.domain.features.pae = { + "!novalue": 1 + }; + + root.domain.clock = { + "!attrs": { + offset: ["localtime", "utc"] + } + }; + root.domain.clock.timer = { + "!attrs": { + name: ["hypervclock", "hpet", "rtc", "pit"], + tickpolicy: ["catchup", "delay"], + present: ["no", "yes"] + } + }; + + root.domain.on_poweroff = { + "!value": "destroy" + }; + + root.domain.on_reboot = { + "!value": "restart" + }; + + root.domain.on_crash = { + "!value": "destroy" + }; + + root.domain.devices = {}; + + root.domain.devices.emulator = { + "!value": "/usr/bin/qemu-system-x86_64" + }; + + root.domain.devices.disk = { + "!attrs": { + type: ["file"], + device: ["disk", "cdrom"] + } + }; + root.domain.devices.disk.driver = { + "!attrs": { + name: ["qemu"], + type: ["raw", "qcow2"], + cache: ["none"], + io: ["native"] + } + }; + root.domain.devices.disk.source = { + "!attrs": { + file: null + } + }; + root.domain.devices.disk.backingStore = { + "!novalue": 1 + }; + root.domain.devices.disk.target = { + "!attrs": { + dev: null, + bus: ["ide", "sata", "virtio"] + } + }; + root.domain.devices.disk.readonly = { + "!novalue": 1 + }; + root.domain.devices.disk.boot = { + "!attrs": { + order: null + } + }; + + root.domain.devices.interface = { + "!attrs": { + type: ["bridge"] + } + }; + root.domain.devices.interface.mac = { + "!attrs": { + address: null + } + }; + root.domain.devices.interface.source = { + "!attrs": { + bridge: null + } + }; + root.domain.devices.interface.model = { + "!attrs": { + type: ["virtio"] + } + }; + + root.domain.devices.input = { + "!attrs": { + type: ["tablet", "mouse", "keyboard"], + bus: ["usb", "ps2"] + } + }; + + root.domain.devices.graphics = { + "!attrs": { + type: ["vnc"], + port: ["-1"], + autoport: ["yes", "no"], + websocket: ["-1"], + listen: ["0.0.0.0"], + keymap: ["en-us", "en-gb", "ar", "hr", "cz", "da", "nl", "nl-be", "es", "et", "fo", + "fi", "fr", "bepo", "fr-be", "fr-ca", "fr-ch", "de-ch", "hu", "is", "it", + "ja", "lv", "lt", "mk", "no", "pl", "pt-br", "ru", "sl", "sv", "th", "tr"] + } + }; + + root.domain.devices.graphics.listen = { + "!attrs": { + type: ["address"], + address: ["0.0.0.0"] + } + }; + + root.domain.devices.hostdev = { + "!attrs": { + mode: ["subsystem"], + type: ["pci", "usb"], + managed: ["yes", "no"] + } + }; + root.domain.devices.hostdev.driver = { + "!attrs": { + name: ["vfio"] + } + }; + root.domain.devices.hostdev.source = {}; + root.domain.devices.hostdev.source.address = { + "!attrs": { + domain: null, + bus: null, + slot: null, + function: null + } + }; + root.domain.devices.hostdev.source.vendor = { + "!attrs": { + id: null + } + }; + root.domain.devices.hostdev.source.product = { + "!attrs": { + id: null + } + }; + + root.domain.devices.memballoon = { + "!attrs": { + model: ["virtio"] + } + }; + root.domain.devices.memballoon.alias = { + "!attrs": { + name: ["balloon0"] + } + }; + + root.domain['qemu:commandline'] = {}; + root.domain['qemu:commandline']['qemu:arg'] = { + "!attrs": { + value: null + } + }; + + + return root; + +} \ No newline at end of file diff --git a/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/show-hint.css b/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/show-hint.css new file mode 100644 index 000000000..924e638f7 --- /dev/null +++ b/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/show-hint.css @@ -0,0 +1,38 @@ +.CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + + margin: 0; + padding: 2px; + + -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + box-shadow: 2px 3px 5px rgba(0,0,0,.2); + border-radius: 3px; + border: 1px solid silver; + + background: white; + font-size: 90%; + font-family: monospace; + + max-height: 20em; + overflow-y: auto; +} + +.CodeMirror-hint { + margin: 0; + padding: 0 4px; + border-radius: 2px; + max-width: 19em; + overflow: hidden; + white-space: pre; + color: black; + cursor: pointer; +} + +li.CodeMirror-hint-active { + background: #08f; + color: white; +} diff --git a/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/show-hint.js b/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/show-hint.js new file mode 100644 index 000000000..fda5ffaa1 --- /dev/null +++ b/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/show-hint.js @@ -0,0 +1,389 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var HINT_ELEMENT_CLASS = "CodeMirror-hint"; + var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; + + // This is the old interface, kept around for now to stay + // backwards-compatible. + CodeMirror.showHint = function(cm, getHints, options) { + if (!getHints) return cm.showHint(options); + if (options && options.async) getHints.async = true; + var newOpts = {hint: getHints}; + if (options) for (var prop in options) newOpts[prop] = options[prop]; + return cm.showHint(newOpts); + }; + + CodeMirror.defineExtension("showHint", function(options) { + // We want a single cursor position. + if (this.listSelections().length > 1 || this.somethingSelected()) return; + + if (this.state.completionActive) this.state.completionActive.close(); + var completion = this.state.completionActive = new Completion(this, options); + var getHints = completion.options.hint; + if (!getHints) return; + + CodeMirror.signal(this, "startCompletion", this); + if (getHints.async) + getHints(this, function(hints) { completion.showHints(hints); }, completion.options); + else + return completion.showHints(getHints(this, completion.options)); + }); + + function Completion(cm, options) { + this.cm = cm; + this.options = this.buildOptions(options); + this.widget = this.onClose = null; + } + + Completion.prototype = { + close: function() { + if (!this.active()) return; + this.cm.state.completionActive = null; + + if (this.widget) this.widget.close(); + if (this.onClose) this.onClose(); + CodeMirror.signal(this.cm, "endCompletion", this.cm); + }, + + active: function() { + return this.cm.state.completionActive == this; + }, + + pick: function(data, i) { + var completion = data.list[i]; + if (completion.hint) completion.hint(this.cm, data, completion); + else this.cm.replaceRange(getText(completion), completion.from || data.from, + completion.to || data.to, "complete"); + CodeMirror.signal(data, "pick", completion); + this.close(); + }, + + showHints: function(data) { + if (!data || !data.list.length || !this.active()) return this.close(); + + if (this.options.completeSingle && data.list.length == 1) + this.pick(data, 0); + else + this.showWidget(data); + }, + + showWidget: function(data) { + this.widget = new Widget(this, data); + CodeMirror.signal(data, "shown"); + + var debounce = 0, completion = this, finished; + var closeOn = this.options.closeCharacters; + var startPos = this.cm.getCursor(), startLen = this.cm.getLine(startPos.line).length; + + var requestAnimationFrame = window.requestAnimationFrame || function(fn) { + return setTimeout(fn, 1000/60); + }; + var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; + + function done() { + if (finished) return; + finished = true; + completion.close(); + completion.cm.off("cursorActivity", activity); + if (data) CodeMirror.signal(data, "close"); + } + + function update() { + if (finished) return; + CodeMirror.signal(data, "update"); + var getHints = completion.options.hint; + if (getHints.async) + getHints(completion.cm, finishUpdate, completion.options); + else + finishUpdate(getHints(completion.cm, completion.options)); + } + function finishUpdate(data_) { + data = data_; + if (finished) return; + if (!data || !data.list.length) return done(); + if (completion.widget) completion.widget.close(); + completion.widget = new Widget(completion, data); + } + + function clearDebounce() { + if (debounce) { + cancelAnimationFrame(debounce); + debounce = 0; + } + } + + function activity() { + clearDebounce(); + var pos = completion.cm.getCursor(), line = completion.cm.getLine(pos.line); + if (pos.line != startPos.line || line.length - pos.ch != startLen - startPos.ch || + pos.ch < startPos.ch || completion.cm.somethingSelected() || + (pos.ch && closeOn.test(line.charAt(pos.ch - 1)))) { + completion.close(); + } else { + debounce = requestAnimationFrame(update); + if (completion.widget) completion.widget.close(); + } + } + this.cm.on("cursorActivity", activity); + this.onClose = done; + }, + + buildOptions: function(options) { + var editor = this.cm.options.hintOptions; + var out = {}; + for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; + if (editor) for (var prop in editor) + if (editor[prop] !== undefined) out[prop] = editor[prop]; + if (options) for (var prop in options) + if (options[prop] !== undefined) out[prop] = options[prop]; + return out; + } + }; + + function getText(completion) { + if (typeof completion == "string") return completion; + else return completion.text; + } + + function buildKeyMap(completion, handle) { + var baseMap = { + Up: function() {handle.moveFocus(-1);}, + Down: function() {handle.moveFocus(1);}, + PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);}, + PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);}, + Home: function() {handle.setFocus(0);}, + End: function() {handle.setFocus(handle.length - 1);}, + Enter: handle.pick, + Tab: handle.pick, + Esc: handle.close + }; + var custom = completion.options.customKeys; + var ourMap = custom ? {} : baseMap; + function addBinding(key, val) { + var bound; + if (typeof val != "string") + bound = function(cm) { return val(cm, handle); }; + // This mechanism is deprecated + else if (baseMap.hasOwnProperty(val)) + bound = baseMap[val]; + else + bound = val; + ourMap[key] = bound; + } + if (custom) + for (var key in custom) if (custom.hasOwnProperty(key)) + addBinding(key, custom[key]); + var extra = completion.options.extraKeys; + if (extra) + for (var key in extra) if (extra.hasOwnProperty(key)) + addBinding(key, extra[key]); + return ourMap; + } + + function getHintElement(hintsElement, el) { + while (el && el != hintsElement) { + if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; + el = el.parentNode; + } + } + + function Widget(completion, data) { + this.completion = completion; + this.data = data; + var widget = this, cm = completion.cm; + + var hints = this.hints = document.createElement("ul"); + hints.className = "CodeMirror-hints"; + this.selectedHint = data.selectedHint || 0; + + var completions = data.list; + for (var i = 0; i < completions.length; ++i) { + var elt = hints.appendChild(document.createElement("li")), cur = completions[i]; + var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); + if (cur.className != null) className = cur.className + " " + className; + elt.className = className; + if (cur.render) cur.render(elt, data, cur); + else elt.appendChild(document.createTextNode(cur.displayText || getText(cur))); + elt.hintId = i; + } + + var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); + var left = pos.left, top = pos.bottom, below = true; + hints.style.left = left + "px"; + hints.style.top = top + "px"; + // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. + var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); + var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); + (completion.options.container || document.body).appendChild(hints); + var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH; + if (overlapY > 0) { + var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); + if (curTop - height > 0) { // Fits above cursor + hints.style.top = (top = pos.top - height) + "px"; + below = false; + } else if (height > winH) { + hints.style.height = (winH - 5) + "px"; + hints.style.top = (top = pos.bottom - box.top) + "px"; + var cursor = cm.getCursor(); + if (data.from.ch != cursor.ch) { + pos = cm.cursorCoords(cursor); + hints.style.left = (left = pos.left) + "px"; + box = hints.getBoundingClientRect(); + } + } + } + var overlapX = box.right - winW; + if (overlapX > 0) { + if (box.right - box.left > winW) { + hints.style.width = (winW - 5) + "px"; + overlapX -= (box.right - box.left) - winW; + } + hints.style.left = (left = pos.left - overlapX) + "px"; + } + + cm.addKeyMap(this.keyMap = buildKeyMap(completion, { + moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); }, + setFocus: function(n) { widget.changeActive(n); }, + menuSize: function() { return widget.screenAmount(); }, + length: completions.length, + close: function() { completion.close(); }, + pick: function() { widget.pick(); }, + data: data + })); + + if (completion.options.closeOnUnfocus) { + var closingOnBlur; + cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); }); + cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); }); + } + + var startScroll = cm.getScrollInfo(); + cm.on("scroll", this.onScroll = function() { + var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); + var newTop = top + startScroll.top - curScroll.top; + var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop); + if (!below) point += hints.offsetHeight; + if (point <= editor.top || point >= editor.bottom) return completion.close(); + hints.style.top = newTop + "px"; + hints.style.left = (left + startScroll.left - curScroll.left) + "px"; + }); + + CodeMirror.on(hints, "dblclick", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();} + }); + + CodeMirror.on(hints, "click", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) { + widget.changeActive(t.hintId); + if (completion.options.completeOnSingleClick) widget.pick(); + } + }); + + CodeMirror.on(hints, "mousedown", function() { + setTimeout(function(){cm.focus();}, 20); + }); + + CodeMirror.signal(data, "select", completions[0], hints.firstChild); + return true; + } + + Widget.prototype = { + close: function() { + if (this.completion.widget != this) return; + this.completion.widget = null; + this.hints.parentNode.removeChild(this.hints); + this.completion.cm.removeKeyMap(this.keyMap); + + var cm = this.completion.cm; + if (this.completion.options.closeOnUnfocus) { + cm.off("blur", this.onBlur); + cm.off("focus", this.onFocus); + } + cm.off("scroll", this.onScroll); + }, + + pick: function() { + this.completion.pick(this.data, this.selectedHint); + }, + + changeActive: function(i, avoidWrap) { + if (i >= this.data.list.length) + i = avoidWrap ? this.data.list.length - 1 : 0; + else if (i < 0) + i = avoidWrap ? 0 : this.data.list.length - 1; + if (this.selectedHint == i) return; + var node = this.hints.childNodes[this.selectedHint]; + node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); + node = this.hints.childNodes[this.selectedHint = i]; + node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; + if (node.offsetTop < this.hints.scrollTop) + this.hints.scrollTop = node.offsetTop - 3; + else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) + this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3; + CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); + }, + + screenAmount: function() { + return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; + } + }; + + CodeMirror.registerHelper("hint", "auto", function(cm, options) { + var helpers = cm.getHelpers(cm.getCursor(), "hint"), words; + if (helpers.length) { + for (var i = 0; i < helpers.length; i++) { + var cur = helpers[i](cm, options); + if (cur && cur.list.length) return cur; + } + } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { + if (words) return CodeMirror.hint.fromList(cm, {words: words}); + } else if (CodeMirror.hint.anyword) { + return CodeMirror.hint.anyword(cm, options); + } + }); + + CodeMirror.registerHelper("hint", "fromList", function(cm, options) { + var cur = cm.getCursor(), token = cm.getTokenAt(cur); + var found = []; + for (var i = 0; i < options.words.length; i++) { + var word = options.words[i]; + if (word.slice(0, token.string.length) == token.string) + found.push(word); + } + + if (found.length) return { + list: found, + from: CodeMirror.Pos(cur.line, token.start), + to: CodeMirror.Pos(cur.line, token.end) + }; + }); + + CodeMirror.commands.autocomplete = CodeMirror.showHint; + + var defaultOptions = { + hint: CodeMirror.hint.auto, + completeSingle: true, + alignWithWord: true, + closeCharacters: /[\s()\[\]{};:>,]/, + closeOnUnfocus: true, + completeOnSingleClick: false, + container: null, + customKeys: null, + extraKeys: null + }; + + CodeMirror.defineOption("hintOptions", null); +}); diff --git a/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/xml-hint.js b/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/xml-hint.js new file mode 100644 index 000000000..d937dfa8a --- /dev/null +++ b/plugins/dynamix.vm.manager/scripts/codemirror/addon/hint/xml-hint.js @@ -0,0 +1,135 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var Pos = CodeMirror.Pos; + + function getHints(cm, options) { + var tags = options && options.schemaInfo; + var quote = (options && options.quoteChar) || '"'; + if (!tags) return; + var cur = cm.getCursor(), token = cm.getTokenAt(cur); + if (token.end > cur.ch) { + token.end = cur.ch; + token.string = token.string.slice(0, cur.ch - token.start); + } + var inner = CodeMirror.innerMode(cm.getMode(), token.state); + if (inner.mode.name != "xml") return; + var result = [], replaceToken = false, prefix; + var tag = /\btag\b/.test(token.type) && !/>$/.test(token.string); + var tagName = tag && /^\w/.test(token.string), tagStart; + var tagType = null; + + if (tagName) { + var before = cm.getLine(cur.line).slice(Math.max(0, token.start - 2), token.start); + tagType = /<\/$/.test(before) ? "close" : /<$/.test(before) ? "open" : null; + if (tagType) tagStart = token.start - (tagType == "close" ? 2 : 1); + } else if (tag && token.string == "<") { + tagType = "open"; + } else if (tag && token.string == "= 0; i--) { + if (! localtags[nodepath[i]]) { + break; + } + + if (localtags["!attrs"]) { + topattrs = localtags["!attrs"]; + } + + localtags = localtags[nodepath[i]]; + } + } + + if (!tag && !inner.state.tagName || tagType) { + if (tagName) + prefix = token.string; + replaceToken = tagType; + if (tagType != "close") { + for (var name in localtags) { + if (localtags.hasOwnProperty(name) && name != "!top" && name != "!attrs" && name != "!value" && (!prefix || name.lastIndexOf(prefix, 0) === 0)) { + if (localtags[name]["!attrs"]) { + result.push("<" + name); + } else { + if (Object.keys(localtags[name]).length === 1 && localtags[name].hasOwnProperty("!novalue")) { + result.push("<" + name + "/>"); + } else if (Object.keys(localtags[name]).length === 1 && localtags[name].hasOwnProperty("!value")) { + result.push("<" + name + ">"); + } else { + result.push("<" + name + ">"); + } + } + } + } + } + if (cx && (!prefix || tagType == "close" && cx.tagName.lastIndexOf(prefix, 0) === 0)) { + result.push(""); + } + } else { + // Attribute completion + var attrs = localtags && localtags[inner.state.tagName] && localtags[inner.state.tagName]["!attrs"]; + if (!attrs) return; + if (token.type == "string" || token.string == "=") { // A value + var before = cm.getRange(Pos(cur.line, Math.max(0, cur.ch - 60)), + Pos(cur.line, token.type == "string" ? token.start : token.end)); + var atName = before.match(/([^\s\u00a0=<>\"\']+)=$/), atValues; + if (!atName || !attrs.hasOwnProperty(atName[1]) || !(atValues = attrs[atName[1]])) return; + if (typeof atValues == 'function') atValues = atValues.call(this, cm); // Functions can be used to supply values for autocomplete widget + if (token.type == "string") { + prefix = token.string; + var n = 0; + if (/['"]/.test(token.string.charAt(0))) { + quote = token.string.charAt(0); + prefix = token.string.slice(1); + n++; + } + var len = token.string.length; + if (/['"]/.test(token.string.charAt(len - 1))) { + quote = token.string.charAt(len - 1); + prefix = token.string.substr(n, len - 2); + } + replaceToken = true; + } + for (var i = 0; i < atValues.length; ++i) if (!prefix || atValues[i].lastIndexOf(prefix, 0) === 0) + result.push(quote + atValues[i] + quote); + } else { // An attribute name + if (token.type == "attribute") { + prefix = token.string; + replaceToken = true; + } + for (var attr in attrs) if (attrs.hasOwnProperty(attr) && (!prefix || attr.lastIndexOf(prefix, 0) === 0)) + result.push(attr); + } + } + return { + list: result, + from: replaceToken ? Pos(cur.line, tagStart == null ? token.start : tagStart) : cur, + to: replaceToken ? Pos(cur.line, token.end) : cur + }; + } + + CodeMirror.registerHelper("hint", "xml", getHints); +}); diff --git a/plugins/dynamix.vm.manager/scripts/codemirror/lib/codemirror.css b/plugins/dynamix.vm.manager/scripts/codemirror/lib/codemirror.css new file mode 100644 index 000000000..c56510e99 --- /dev/null +++ b/plugins/dynamix.vm.manager/scripts/codemirror/lib/codemirror.css @@ -0,0 +1,309 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror div.CodeMirror-cursor { + border-left: 1px solid black; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} + +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +@-moz-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@-webkit-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} + +/* Can style cursor different in overwrite (non-insert) mode */ +div.CodeMirror-overwrite div.CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + line-height: 1; + position: relative; + overflow: hidden; + background: white; + color: black; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + -moz-box-sizing: content-box; + box-sizing: content-box; + display: inline-block; + margin-bottom: -30px; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + height: 100%; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget {} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} +.CodeMirror-measure pre { position: static; } + +.CodeMirror div.CodeMirror-cursor { + position: absolute; + border-right: none; + width: 0; +} + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } + +.cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } diff --git a/plugins/dynamix.vm.manager/scripts/codemirror/lib/codemirror.js b/plugins/dynamix.vm.manager/scripts/codemirror/lib/codemirror.js new file mode 100644 index 000000000..382260b2e --- /dev/null +++ b/plugins/dynamix.vm.manager/scripts/codemirror/lib/codemirror.js @@ -0,0 +1,8053 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +// This is CodeMirror (http://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + module.exports = mod(); + else if (typeof define == "function" && define.amd) // AMD + return define([], mod); + else // Plain browser env + this.CodeMirror = mod(); +})(function() { + "use strict"; + + // BROWSER SNIFFING + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + + var gecko = /gecko\/\d/i.test(navigator.userAgent); + // ie_uptoN means Internet Explorer version N or lower + var ie_upto10 = /MSIE \d/.test(navigator.userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent); + var ie = ie_upto10 || ie_11up; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); + var webkit = /WebKit\//.test(navigator.userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var presto = /Opera\//.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var khtml = /KHTML\//.test(navigator.userAgent); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); + var phantom = /PhantomJS/.test(navigator.userAgent); + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var windows = /win/i.test(navigator.platform); + + var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) presto_version = Number(presto_version[1]); + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + // EDITOR CONSTRUCTOR + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); + + this.options = options = options ? copyObj(options) : {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + setGuttersForLineNumbers(options); + + var doc = options.value; + if (typeof doc == "string") doc = new Doc(doc, options.mode); + this.doc = doc; + + var display = this.display = new Display(place, doc); + display.wrapper.CodeMirror = this; + updateGutters(this); + themeChanged(this); + if (options.lineWrapping) + this.display.wrapper.className += " CodeMirror-wrap"; + if (options.autofocus && !mobile) focusInput(this); + initScrollbars(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in readInput + draggingText: false, + highlight: new Delayed(), // stores highlight worker timeout + keySeq: null // Unfinished key sequence + }; + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) setTimeout(bind(resetInput, this, true), 20); + + registerEventHandlers(this); + ensureGlobalHandlers(); + + startOperation(this); + this.curOp.forceUpdate = true; + attachDoc(this, doc); + + if ((options.autofocus && !mobile) || activeElt() == display.input) + setTimeout(bind(onFocus, this), 20); + else + onBlur(this); + + for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) + optionHandlers[opt](this, options[opt], Init); + maybeUpdateLineNumberWidth(this); + if (options.finishInit) options.finishInit(this); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); + endOperation(this); + // Suppress optimizelegibility in Webkit, since it breaks text + // measuring on line wrapping boundaries. + if (webkit && options.lineWrapping && + getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") + display.lineDiv.style.textRendering = "auto"; + } + + // DISPLAY CONSTRUCTOR + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc) { + var d = this; + + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) input.style.width = "1000px"; + else input.setAttribute("wrap", "off"); + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) input.style.border = "1px solid black"; + input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); input.setAttribute("spellcheck", "false"); + + // Wraps and hides input textarea + d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("not-content", "true"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("not-content", "true"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = elt("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + d.sizerWidth = null; + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.inputDiv, d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + // Needed to hide big blue blinking cursor on Mobile Safari + if (ios) input.style.width = "0px"; + if (!webkit) d.scroller.draggable = true; + // Needed to handle Tab key in KHTML + if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; } + + if (place) { + if (place.appendChild) place.appendChild(d.wrapper); + else place(d.wrapper); + } + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + d.reportedViewFrom = d.reportedViewTo = doc.first; + // Information about the rendered lines. + d.view = []; + d.renderedView = null; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastWrapHeight = d.lastWrapWidth = 0; + d.updateLineNumbers = null; + + d.nativeBarWidth = d.barHeight = d.barWidth = 0; + d.scrollbarsClipped = false; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // See readInput and resetInput + d.prevInput = ""; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + d.pollingFast = false; + // Self-resetting timeout for the poller + d.poll = new Delayed(); + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks when resetInput has punted to just putting a short + // string into the textarea instead of the full selection. + d.inaccurateSelection = false; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + } + + // STATE UPDATES + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function(line) { + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + }); + cm.doc.frontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) regChange(cm); + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + cm.display.sizerWidth = null; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function(){updateScrollbars(cm);}, 100); + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function(line) { + if (lineIsHidden(cm.doc, line)) return 0; + + var widgetsHeight = 0; + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; + } + + if (wrapping) + return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; + else + return widgetsHeight + th; + }; + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function(line) { + var estHeight = est(line); + if (estHeight != line.height) updateLineHeight(line, estHeight); + }); + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + setTimeout(function(){alignHorizontally(cm);}, 20); + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + for (var i = 0; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + updateGutterSpace(cm); + } + + function updateGutterSpace(cm) { + var width = cm.display.gutters.offsetWidth; + cm.display.sizer.style.marginLeft = width + "px"; + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) return 0; + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(0, true); + len -= cur.text.length - found.from.ch; + cur = found.to.line; + len += cur.text.length - found.to.ch; + } + return len; + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function(line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 1); + } + } + + // SCROLLBARS + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); + return { + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW + }; + } + + function NativeScrollbars(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + place(vert); place(horiz); + + on(vert, "scroll", function() { + if (vert.clientHeight) scroll(vert.scrollTop, "vertical"); + }); + on(horiz, "scroll", function() { + if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); + }); + + this.checkedOverlay = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; + } + + NativeScrollbars.prototype = copyObj({ + update: function(measure) { + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + (measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedOverlay && measure.clientHeight > 0) { + if (sWidth == 0) this.overlayHack(); + this.checkedOverlay = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; + }, + setScrollLeft: function(pos) { + if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; + }, + setScrollTop: function(pos) { + if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; + }, + overlayHack: function() { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.minHeight = this.vert.style.minWidth = w; + var self = this; + var barMouseDown = function(e) { + if (e_target(e) != self.vert && e_target(e) != self.horiz) + operation(self.cm, onMouseDown)(e); + }; + on(this.vert, "mousedown", barMouseDown); + on(this.horiz, "mousedown", barMouseDown); + }, + clear: function() { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + } + }, NativeScrollbars.prototype); + + function NullScrollbars() {} + + NullScrollbars.prototype = copyObj({ + update: function() { return {bottom: 0, right: 0}; }, + setScrollLeft: function() {}, + setScrollTop: function() {}, + clear: function() {} + }, NullScrollbars.prototype); + + CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + on(node, "mousedown", function() { + if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); + }); + node.setAttribute("not-content", "true"); + }, function(pos, axis) { + if (axis == "horizontal") setScrollLeft(cm, pos); + else setScrollTop(cm, pos); + }, cm); + if (cm.display.scrollbars.addClass) + addClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + function updateScrollbars(cm, measure) { + if (!measure) measure = measureForScrollbars(cm); + var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; + updateScrollbarsInner(cm, measure); + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { + if (startWidth != cm.display.barWidth && cm.options.lineWrapping) + updateHeightsInViewport(cm); + updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; + } + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbarsInner(cm, measure) { + var d = cm.display; + var sizes = d.scrollbars.update(measure); + + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; + + if (sizes.right && sizes.bottom) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; + } else d.scrollbarFiller.style.display = ""; + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; + } else d.gutterFiller.style.display = ""; + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) { + from = ensureFrom; + to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); + } else if (Math.min(ensureTo, doc.lastLine()) >= to) { + from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); + to = ensureTo; + } + } + return {from: from, to: Math.max(to, from + 1)}; + } + + // LINE NUMBERS + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) if (!view[i].hidden) { + if (cm.options.fixedGutter && view[i].gutter) + view[i].gutter.style.left = left; + var align = view[i].alignable; + if (align) for (var j = 0; j < align.length; j++) + align[j].style.left = left; + } + if (cm.options.fixedGutter) + display.gutters.style.left = (comp + gutterW) + "px"; + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) return false; + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding); + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm); + return true; + } + return false; + } + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; + } + + // DISPLAY DRAWING + + function DisplayUpdate(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.wrapperWidth = display.wrapper.clientWidth; + this.oldDisplayWidth = displayWidth(cm); + this.force = force; + this.dims = getDimensions(cm); + } + + function maybeClipScrollbars(cm) { + var display = cm.display; + if (!display.scrollbarsClipped && display.scroller.offsetWidth) { + display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; + display.heightForcer.style.height = scrollGap(cm) + "px"; + display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; + display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; + display.scrollbarsClipped = true; + } + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + + if (update.editorIsHidden) { + resetView(cm); + return false; + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + display.renderedView == display.view && countDirtyView(cm) == 0) + return false; + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); + if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + return false; + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var focused = activeElt(); + if (toUpdate > 4) display.lineDiv.style.display = "none"; + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) display.lineDiv.style.display = ""; + display.renderedView = display.view; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); + + // Prevent selection and cursors from interfering with the scroll + // width and height. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + display.gutters.style.height = 0; + + if (different) { + display.lastWrapHeight = update.wrapperHeight; + display.lastWrapWidth = update.wrapperWidth; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true; + } + + function postUpdateDisplay(cm, update) { + var force = update.force, viewport = update.viewport; + for (var first = true;; first = false) { + if (first && cm.options.lineWrapping && update.oldDisplayWidth != displayWidth(cm)) { + force = true; + } else { + force = false; + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + break; + } + if (!updateDisplayIfNeeded(cm, update)) break; + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + } + + signalLater(cm, "update", cm); + if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { + signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; + } + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + } + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = measure.docHeight + "px"; + var total = measure.docHeight + cm.display.barHeight; + cm.display.heightForcer.style.top = total + "px"; + cm.display.gutters.style.height = Math.max(total + scrollGap(cm), measure.clientHeight) + "px"; + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], height; + if (cur.hidden) continue; + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + } + var diff = cur.line.height - height; + if (height < 2) height = textHeight(display); + if (diff > .001 || diff < -.001) { + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) for (var j = 0; j < cur.rest.length; j++) + updateWidgetHeight(cur.rest[j]); + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) + line.widgets[i].height = line.widgets[i].node.offsetHeight; + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + var gutterLeft = d.gutters.clientLeft; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft; + width[cm.options.gutters[i]] = n.clientWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth}; + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + node.style.display = "none"; + else + node.parentNode.removeChild(node); + return next; + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) { + } else if (!lineView.node) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) cur = rm(cur); + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) cur = rm(cur); + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") updateLineText(cm, lineView); + else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); + else if (type == "class") updateLineClasses(lineView); + else if (type == "widget") updateLineWidgets(lineView, dims); + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + lineView.text.parentNode.replaceChild(lineView.node, lineView.text); + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) lineView.node.style.zIndex = 2; + } + return lineView.node; + } + + function updateLineBackground(lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) cls += " CodeMirror-linebackground"; + if (lineView.background) { + if (cls) lineView.background.className = cls; + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built; + } + return buildLineContent(cm, lineView); + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) lineView.node = built.pre; + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(lineView) { + updateLineBackground(lineView); + if (lineView.line.wrapClass) + ensureLineWrapped(lineView).className = lineView.line.wrapClass; + else if (lineView.node != lineView.text) + lineView.node.className = ""; + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = + wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + + "px; width: " + dims.gutterTotalWidth + "px"), + lineView.text); + if (lineView.line.gutterClass) + gutterWrap.className += " " + lineView.line.gutterClass; + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + + cm.display.lineNumInnerWidth + "px")); + if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } + } + } + + function updateLineWidgets(lineView, dims) { + if (lineView.alignable) lineView.alignable = null; + for (var node = lineView.node.firstChild, next; node; node = next) { + var next = node.nextSibling; + if (node.className == "CodeMirror-linewidget") + lineView.node.removeChild(node); + } + insertLineWidgets(lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) lineView.bgClass = built.bgClass; + if (built.textClass) lineView.textClass = built.textClass; + + updateLineClasses(lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(lineView, dims); + return lineView.node; + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(lineView, dims) { + insertLineWidgetsFor(lineView.line, lineView, dims, true); + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + insertLineWidgetsFor(lineView.rest[i], lineView, dims, false); + } + + function insertLineWidgetsFor(line, lineView, dims, allowAbove) { + if (!line.widgets) return; + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true"); + positionLineWidget(widget, node, lineView, dims); + if (allowAbove && widget.above) + wrap.insertBefore(node, lineView.gutter || lineView.text); + else + wrap.appendChild(node); + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; + } + } + + // POSITION OBJECT + + // A Pos instance represents a position within the text. + var Pos = CodeMirror.Pos = function(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + }; + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; + + function copyPos(x) {return Pos(x.line, x.ch);} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } + + // SELECTION / CURSOR + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + function Selection(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + } + + Selection.prototype = { + primary: function() { return this.ranges[this.primIndex]; }, + equals: function(other) { + if (other == this) return true; + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; + } + return true; + }, + deepCopy: function() { + for (var out = [], i = 0; i < this.ranges.length; i++) + out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); + return new Selection(out, this.primIndex); + }, + somethingSelected: function() { + for (var i = 0; i < this.ranges.length; i++) + if (!this.ranges[i].empty()) return true; + return false; + }, + contains: function(pos, end) { + if (!end) end = pos; + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + return i; + } + return -1; + } + }; + + function Range(anchor, head) { + this.anchor = anchor; this.head = head; + } + + Range.prototype = { + from: function() { return minPos(this.anchor, this.head); }, + to: function() { return maxPos(this.anchor, this.head); }, + empty: function() { + return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; + } + }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(ranges, primIndex) { + var prim = ranges[primIndex]; + ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + if (cmp(prev.to(), cur.from()) >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) --primIndex; + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex); + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0); + } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + function clipPosArray(doc, array) { + for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); + return out; + } + + // SELECTION UPDATES + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(doc, range, head, other) { + if (doc.cm && doc.cm.display.shift || doc.extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head); + } else { + return new Range(other || head, head); + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options) { + setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + for (var out = [], i = 0; i < doc.sel.ranges.length; i++) + out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); + var newSel = normalizeSelection(out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); + } + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); + else return sel; + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + sel = filterSelectionChange(doc, sel); + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + ensureCursorVisible(doc.cm); + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) return; + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) out = sel.ranges.slice(0, i); + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(out, sel.primIndex) : sel; + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, bias, mayClear) { + var flipped = false, curPos = pos; + var dir = bias || 1; + doc.cantEdit = false; + search: for (;;) { + var line = getLine(doc, curPos.line); + if (line.markedSpans) { + for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; + var newPos = m.find(dir < 0 ? -1 : 1); + if (cmp(newPos, curPos) == 0) { + newPos.ch += dir; + if (newPos.ch < 0) { + if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); + else newPos = null; + } else if (newPos.ch > line.text.length) { + if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); + else newPos = null; + } + if (!newPos) { + if (flipped) { + // Driven in a corner -- no valid cursor position found at all + // -- try again *with* clearing, if we didn't already + if (!mayClear) return skipAtomic(doc, pos, bias, true); + // Otherwise, turn off editing until further notice, and return the start of the doc + doc.cantEdit = true; + return Pos(doc.first, 0); + } + flipped = true; newPos = pos; dir = -dir; + } + } + curPos = newPos; + continue search; + } + } + } + return curPos; + } + } + + // SELECTION DRAWING + + // Redraw the selection and/or cursor + function drawSelection(cm) { + var display = cm.display, doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + drawSelectionCursor(cm, range, curFragment); + if (!collapsed) + drawSelectionRange(cm, range, selFragment); + } + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result; + } + + function showSelection(cm, drawn) { + removeChildrenAndAdd(cm.display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(cm.display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + cm.display.inputDiv.style.top = drawn.teTop + "px"; + cm.display.inputDiv.style.left = drawn.teLeft + "px"; + } + } + + function updateSelection(cm) { + showSelection(cm, drawSelection(cm)); + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, range, output) { + var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left; + var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; + + function add(left, top, width, bottom) { + if (top < 0) top = 0; + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + + "px; height: " + (bottom - top) + "px")); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias); + } + + iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { + var leftPos = coords(from, "left"), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1, "right"); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (fromArg == null && from == 0) left = leftSide; + if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part + add(left, leftPos.top, null, leftPos.bottom); + left = leftSide; + if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); + } + if (toArg == null && to == lineLen) right = rightSide; + if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) + start = leftPos; + if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) + end = rightPos; + if (left < leftSide + 1) left = leftSide; + add(left, rightPos.top, right - left, rightPos.bottom); + }); + return {start: start, end: end}; + } + + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + add(leftSide, leftEnd.bottom, null, rightStart.top); + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) return; + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + display.blinker = setInterval(function() { + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); + else if (cm.options.cursorBlinkRate < 0) + display.cursorDiv.style.visibility = "hidden"; + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) + cm.state.highlight.set(time, bind(highlightWorker, cm)); + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.frontier < doc.first) doc.frontier = doc.first; + if (doc.frontier >= cm.display.viewTo) return; + var end = +new Date + cm.options.workTime; + var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); + var changedLines = []; + + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { + if (doc.frontier >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles; + var highlighted = highlightLine(cm, line, state, true); + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) line.styleClasses = newCls; + else if (oldCls) line.styleClasses = null; + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; + if (ischange) changedLines.push(doc.frontier); + line.stateAfter = copyState(doc.mode, state); + } else { + processLine(cm, line.text, state); + line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; + } + ++doc.frontier; + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true; + } + }); + if (changedLines.length) runInOp(cm, function() { + for (var i = 0; i < changedLines.length; i++) + regLineChange(cm, changedLines[i], "text"); + }); + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) return doc.first; + var line = getLine(doc, search - 1); + if (line.stateAfter && (!precise || search <= doc.frontier)) return search; + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + + function getStateBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) return true; + var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; + if (!state) state = startState(doc.mode); + else state = copyState(doc.mode, state); + doc.iter(pos, n, function(line) { + processLine(cm, line.text, state); + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; + line.stateAfter = save ? copyState(doc.mode, state) : null; + ++pos; + }); + if (precise) doc.frontier = pos; + return state; + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop;} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} + function paddingH(display) { + if (display.cachedPaddingH) return display.cachedPaddingH; + var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data; + return data; + } + + function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; } + function displayWidth(cm) { + return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth; + } + function displayHeight(cm) { + return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight; + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && displayWidth(cm); + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + heights.push((cur.bottom + next.top) / 2 - rect.top); + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + return {map: lineView.measure.map, cache: lineView.measure.cache}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineView.rest[i] == line) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineNo(lineView.rest[i]) > lineN) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view; + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + return cm.display.view[findViewIndex(cm, lineN)]; + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + return ext; + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) + view = null; + else if (view && view.changes) + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + if (!view) + view = updateExternalMeasurement(cm, line); + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + }; + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) ch = -1; + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + prepared.rect = prepared.view.text.getBoundingClientRect(); + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) prepared.cache[key] = found; + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom}; + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function measureCharInner(cm, prepared, ch, bias) { + var map = prepared.map; + + var node, start, end, collapse; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + var mStart = map[i], mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) collapse = "right"; + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + collapse = bias; + if (bias == "left" && start == 0) + while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } + if (bias == "right" && start == mEnd - mStart) + while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } + break; + } + } + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(mStart + start))) --start; + while (mStart + end < mEnd && isExtendingChar(prepared.line.text.charAt(mStart + end))) ++end; + if (ie && ie_version < 9 && start == 0 && end == mEnd - mStart) { + rect = node.parentNode.getBoundingClientRect(); + } else if (ie && cm.options.lineWrapping) { + var rects = range(node, start, end).getClientRects(); + if (rects.length) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = nullRect; + } else { + rect = range(node, start, end).getBoundingClientRect() || nullRect; + } + if (rect.left || rect.right || start == 0) break; + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect); + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) collapse = bias = "right"; + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = node.getBoundingClientRect(); + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; + else + rect = nullRect; + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + for (var i = 0; i < heights.length - 1; i++) + if (mid < heights[i]) break; + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) result.bogus = true; + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result; + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + return rect; + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY}; + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + lineView.measure.caches[i] = {}; + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + clearLineMeasurementCacheFor(cm.display.view[i]); + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; + cm.display.lineNumChars = null; + } + + function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } + function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"/null (editor), "window", + // or "page". + function intoCoordSystem(cm, lineObj, rect, context) { + if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { + var size = widgetHeight(lineObj.widgets[i]); + rect.top += size; rect.bottom += size; + } + if (context == "line") return rect; + if (!context) context = "local"; + var yOff = heightAtLine(lineObj); + if (context == "local") yOff += paddingTop(cm.display); + else yOff -= cm.display.viewOffset; + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect; + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"/null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") return coords; + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) lineObj = getLine(cm.doc, pos.line); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) m.left = m.right; else m.right = m.left; + return intoCoordSystem(cm, lineObj, m, context); + } + function getBidi(ch, partPos) { + var part = order[partPos], right = part.level % 2; + if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { + part = order[--partPos]; + ch = bidiRight(part) - (part.level % 2 ? 0 : 1); + right = true; + } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { + part = order[++partPos]; + ch = bidiLeft(part) - part.level % 2; + right = false; + } + if (right && ch == part.to && ch > part.from) return get(ch - 1); + return get(ch, right); + } + var order = getOrder(lineObj), ch = pos.ch; + if (!order) return get(ch); + var partPos = getBidiPartAt(order, ch); + var val = getBidi(ch, partPos); + if (bidiOther != null) val.other = getBidi(ch, bidiOther); + return val; + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0, pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height}; + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, outside, xRel) { + var pos = Pos(line, ch); + pos.xRel = xRel; + if (outside) pos.outside = true; + return pos; + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) return PosWithInfo(doc.first, 0, true, -1); + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); + if (x < 0) x = 0; + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var merged = collapsedSpanAtEnd(lineObj); + var mergedPos = merged && merged.find(0, true); + if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) + lineN = lineNo(lineObj = mergedPos.to.line); + else + return found; + } + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + var innerOff = y - heightAtLine(lineObj); + var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + + function getX(ch) { + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); + wrongLine = true; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; + else wrongLine = false; + return sp.left; + } + + var bidi = getOrder(lineObj), dist = lineObj.text.length; + var from = lineLeft(lineObj), to = lineRight(lineObj); + var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; + + if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); + // Do a binary search between these bounds. + for (;;) { + if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { + var ch = x < fromX || x - fromX <= toX - x ? from : to; + var xDiff = x - (ch == from ? fromX : toX); + while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; + var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, + xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); + return pos; + } + var step = Math.ceil(dist / 2), middle = from + step; + if (bidi) { + middle = from; + for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); + } + var middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} + } + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) return display.cachedTextHeight; + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) display.cachedTextHeight = height; + removeChildren(display.measure); + return height || 1; + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) return display.cachedCharWidth; + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) display.cachedCharWidth = width; + return width || 10; + } + + // OPERATIONS + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var operationGroup = null; + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: null, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + id: ++nextOpId // Unique ID + }; + if (operationGroup) { + operationGroup.ops.push(cm.curOp); + } else { + cm.curOp.ownsGroup = operationGroup = { + ops: [cm.curOp], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + callbacks[i](); + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm); + } + } while (i < callbacks.length); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp, group = op.ownsGroup; + if (!group) return; + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + for (var i = 0; i < group.ops.length; i++) + group.ops[i].cm.curOp = null; + endOperations(group); + } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R1(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W1(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R2(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W2(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_finish(ops[i]); + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + maybeClipScrollbars(cm); + if (op.updateMaxLine) findMaxLine(cm); + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) updateHeightsInViewport(cm); + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + cm.display.sizerWidth = op.adjustWidthTo; + op.barMeasure.scrollWidth = + Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); + } + + if (op.updatedDisplay || op.selectionChanged) + op.newSelectionNodes = drawSelection(cm); + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); + cm.display.maxLineChanged = false; + } + + if (op.newSelectionNodes) + showSelection(cm, op.newSelectionNodes); + if (op.updatedDisplay) + setDocumentHeight(cm, op.barMeasure); + if (op.updatedDisplay || op.startHeight != cm.doc.height) + updateScrollbars(cm, op.barMeasure); + + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + resetInput(cm, op.typing); + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.updatedDisplay) postUpdateDisplay(cm, op.update); + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + display.wheelStartX = display.wheelStartY = null; + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { + doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); + display.scrollbars.setScrollTop(doc.scrollTop); + display.scroller.scrollTop = doc.scrollTop; + } + if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { + doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft)); + display.scrollbars.setScrollLeft(doc.scrollLeft); + display.scroller.scrollLeft = doc.scrollLeft; + alignHorizontally(cm); + } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) for (var i = 0; i < hidden.length; ++i) + if (!hidden[i].lines.length) signal(hidden[i], "hide"); + if (unhidden) for (var i = 0; i < unhidden.length; ++i) + if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); + + if (display.wrapper.offsetHeight) + doc.scrollTop = cm.display.scroller.scrollTop; + + // Fire change events, and delayed event handlers + if (op.changeObjs) + signal(cm, "changes", cm, op.changeObjs); + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) return f(); + startOperation(cm); + try { return f(); } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) return f.apply(cm, arguments); + startOperation(cm); + try { return f.apply(cm, arguments); } + finally { endOperation(cm); } + }; + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) return f.apply(this, arguments); + startOperation(this); + try { return f.apply(this, arguments); } + finally { endOperation(this); } + }; + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) return f.apply(this, arguments); + startOperation(cm); + try { return f.apply(this, arguments); } + finally { endOperation(cm); } + }; + } + + // VIEW TRACKING + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array; + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) from = cm.doc.first; + if (to == null) to = cm.doc.first + cm.doc.size; + if (!lendiff) lendiff = 0; + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + display.updateLineNumbers = from; + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + resetView(cm); + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut = viewCuttingPoint(cm, from, from, -1); + if (cut) { + display.view = display.view.slice(0, cut.index); + display.viewTo = cut.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + ext.lineN += lendiff; + else if (from < ext.lineN + ext.size) + display.externalMeasured = null; + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + display.externalMeasured = null; + + if (line < display.viewFrom || line >= display.viewTo) return; + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) return; + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) arr.push(type); + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) return null; + n -= cm.display.viewFrom; + if (n < 0) return null; + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) return i; + } + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + return {index: index, lineN: newN}; + for (var i = 0, n = cm.display.viewFrom; i < index; i++) + n += view[i].size; + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) return null; + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) return null; + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN}; + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); + else if (display.viewFrom < from) + display.view = display.view.slice(findViewIndex(cm, from)); + display.viewFrom = from; + if (display.viewTo < to) + display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); + else if (display.viewTo > to) + display.view = display.view.slice(0, findViewIndex(cm, to)); + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; + } + return dirty; + } + + // INPUT HANDLING + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + function slowPoll(cm) { + if (cm.display.pollingFast) return; + cm.display.poll.set(cm.options.pollInterval, function() { + readInput(cm); + if (cm.state.focused) slowPoll(cm); + }); + } + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + function fastPoll(cm) { + var missed = false; + cm.display.pollingFast = true; + function p() { + var changed = readInput(cm); + if (!changed && !missed) {missed = true; cm.display.poll.set(60, p);} + else {cm.display.pollingFast = false; slowPoll(cm);} + } + cm.display.poll.set(20, p); + } + + // This will be set to an array of strings when copying, so that, + // when pasting, we know what kind of selections the copied text + // was made out of. + var lastCopied = null; + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + function readInput(cm) { + var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (!cm.state.focused || (hasSelection(input) && !prevInput) || isReadOnly(cm) || cm.options.disableInput || cm.state.keySeq) + return false; + // See paste handler for more on the fakedLastChar kludge + if (cm.state.pasteIncoming && cm.state.fakedLastChar) { + input.value = input.value.substring(0, input.value.length - 1); + cm.state.fakedLastChar = false; + } + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) return false; + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && cm.display.inputHasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + resetInput(cm); + return false; + } + + var withOp = !cm.curOp; + if (withOp) startOperation(cm); + cm.display.shift = false; + + if (text.charCodeAt(0) == 0x200b && doc.sel == cm.display.selForContextMenu && !prevInput) + prevInput = "\u200b"; + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; + var inserted = text.slice(same), textLines = splitLines(inserted); + + // When pasing N lines into N selections, insert one line per selection + var multiPaste = null; + if (cm.state.pasteIncoming && doc.sel.ranges.length > 1) { + if (lastCopied && lastCopied.join("\n") == inserted) + multiPaste = doc.sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); + else if (textLines.length == doc.sel.ranges.length) + multiPaste = map(textLines, function(l) { return [l]; }); + } + + // Normal behavior is to insert the new text into every selection + for (var i = doc.sel.ranges.length - 1; i >= 0; i--) { + var range = doc.sel.ranges[i]; + var from = range.from(), to = range.to(); + // Handle deletion + if (same < prevInput.length) + from = Pos(from.line, from.ch - (prevInput.length - same)); + // Handle overwrite + else if (cm.state.overwrite && range.empty() && !cm.state.pasteIncoming) + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, + origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + // When an 'electric' character is inserted, immediately trigger a reindent + if (inserted && !cm.state.pasteIncoming && cm.options.electricChars && + cm.options.smartIndent && range.head.ch < 100 && + (!i || doc.sel.ranges[i - 1].head.line != range.head.line)) { + var mode = cm.getModeAt(range.head); + var end = changeEnd(changeEvent); + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indentLine(cm, end.line, "smart"); + break; + } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(doc, end.line).text.slice(0, end.ch))) + indentLine(cm, end.line, "smart"); + } + } + } + ensureCursorVisible(cm); + cm.curOp.updateInput = updateInput; + cm.curOp.typing = true; + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = ""; + else cm.display.prevInput = text; + if (withOp) endOperation(cm); + cm.state.pasteIncoming = cm.state.cutIncoming = false; + return true; + } + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + function resetInput(cm, typing) { + if (cm.display.contextMenuPending) return; + var minimal, selected, doc = cm.doc; + if (cm.somethingSelected()) { + cm.display.prevInput = ""; + var range = doc.sel.primary(); + minimal = hasCopyEvent && + (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); + var content = minimal ? "-" : selected || cm.getSelection(); + cm.display.input.value = content; + if (cm.state.focused) selectInput(cm.display.input); + if (ie && ie_version >= 9) cm.display.inputHasSelection = content; + } else if (!typing) { + cm.display.prevInput = cm.display.input.value = ""; + if (ie && ie_version >= 9) cm.display.inputHasSelection = null; + } + cm.display.inaccurateSelection = minimal; + } + + function focusInput(cm) { + if (cm.options.readOnly != "nocursor" && (!mobile || activeElt() != cm.display.input)) { + try { cm.display.input.focus(); } + catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM + } + } + + function ensureFocus(cm) { + if (!cm.state.focused) { focusInput(cm); onFocus(cm); } + } + + function isReadOnly(cm) { + return cm.options.readOnly || cm.doc.cantEdit; + } + + // EVENT HANDLERS + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + on(d.scroller, "dblclick", operation(cm, function(e) { + if (signalDOMEvent(cm, e)) return; + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); + else + on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + // Prevent normal selection in the editor (we handle our own) + on(d.lineSpace, "selectstart", function(e) { + if (!eventInWidget(d, e)) e_preventDefault(e); + }); + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function() { + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); + on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + on(d.input, "keyup", function(e) { onKeyUp.call(cm, e); }); + on(d.input, "input", function() { + if (ie && ie_version >= 9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null; + readInput(cm); + }); + on(d.input, "keydown", operation(cm, onKeyDown)); + on(d.input, "keypress", operation(cm, onKeyPress)); + on(d.input, "focus", bind(onFocus, cm)); + on(d.input, "blur", bind(onBlur, cm)); + + function drag_(e) { + if (!signalDOMEvent(cm, e)) e_stop(e); + } + if (cm.options.dragDrop) { + on(d.scroller, "dragstart", function(e){onDragStart(cm, e);}); + on(d.scroller, "dragenter", drag_); + on(d.scroller, "dragover", drag_); + on(d.scroller, "drop", operation(cm, onDrop)); + } + on(d.scroller, "paste", function(e) { + if (eventInWidget(d, e)) return; + cm.state.pasteIncoming = true; + focusInput(cm); + fastPoll(cm); + }); + on(d.input, "paste", function() { + // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 + // Add a char to the end of textarea before paste occur so that + // selection doesn't span to the end of textarea. + if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { + var start = d.input.selectionStart, end = d.input.selectionEnd; + d.input.value += "$"; + // The selection end needs to be set before the start, otherwise there + // can be an intermediate non-empty selection between the two, which + // can override the middle-click paste buffer on linux and cause the + // wrong thing to get pasted. + d.input.selectionEnd = end; + d.input.selectionStart = start; + cm.state.fakedLastChar = true; + } + cm.state.pasteIncoming = true; + fastPoll(cm); + }); + + function prepareCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (d.inaccurateSelection) { + d.prevInput = ""; + d.inaccurateSelection = false; + d.input.value = lastCopied.join("\n"); + selectInput(d.input); + } + } else { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + if (e.type == "cut") { + cm.setSelections(ranges, null, sel_dontScroll); + } else { + d.prevInput = ""; + d.input.value = text.join("\n"); + selectInput(d.input); + } + lastCopied = text; + } + if (e.type == "cut") cm.state.cutIncoming = true; + } + on(d.input, "cut", prepareCopyCut); + on(d.input, "copy", prepareCopyCut); + + // Needed to handle Tab key in KHTML + if (khtml) on(d.sizer, "mouseup", function() { + if (activeElt() == d.input) d.input.blur(); + focusInput(cm); + }); + } + + // Called when the window resizes + function onResize(cm) { + var d = cm.display; + if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth) + return; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; + cm.setSize(); + } + + // MOUSE EVENTS + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || + (n.parentNode == display.sizer && n != display.mover)) + return true; + } + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal && e_target(e).getAttribute("not-content") == "true") return null; + + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e) { return null; } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords; + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + if (signalDOMEvent(this, e)) return; + var cm = this, display = cm.display; + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function(){display.scroller.draggable = true;}, 100); + } + return; + } + if (clickInGutter(cm, e)) return; + var start = posFromMouse(cm, e); + window.focus(); + + switch (e_button(e)) { + case 1: + if (start) + leftButtonDown(cm, e, start); + else if (e_target(e) == display.scroller) + e_preventDefault(e); + break; + case 2: + if (webkit) cm.state.lastMiddleDown = +new Date; + if (start) extendSelection(cm.doc, start); + setTimeout(bind(focusInput, cm), 20); + e_preventDefault(e); + break; + case 3: + if (captureRightClick) onContextMenu(cm, e); + break; + } + } + + var lastClick, lastDoubleClick; + function leftButtonDown(cm, e, start) { + setTimeout(bind(ensureFocus, cm), 0); + + var now = +new Date, type; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { + type = "triple"; + } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { + type = "double"; + lastDoubleClick = {time: now, pos: start}; + } else { + type = "single"; + lastClick = {time: now, pos: start}; + } + + var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained; + if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && + type == "single" && (contained = sel.contains(start)) > -1 && + !sel.ranges[contained].empty()) + leftButtonStartDrag(cm, e, start, modifier); + else + leftButtonSelect(cm, e, start, type, modifier); + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, e, start, modifier) { + var display = cm.display; + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + if (!modifier) + extendSelection(cm.doc, start); + focusInput(cm); + // Work around unexplainable focus problem in IE9 (#2127) + if (ie && ie_version == 9) + setTimeout(function() {document.body.focus(); focusInput(cm);}, 20); + } + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, e, start, type, addNew) { + var display = cm.display, doc = cm.doc; + e_preventDefault(e); + + var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; + if (addNew && !e.shiftKey) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + ourRange = ranges[ourIndex]; + else + ourRange = new Range(start, start); + } else { + ourRange = doc.sel.primary(); + } + + if (e.altKey) { + type = "rect"; + if (!addNew) ourRange = new Range(start, start); + start = posFromMouse(cm, e, true, true); + ourIndex = -1; + } else if (type == "double") { + var word = cm.findWordAt(start); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, word.anchor, word.head); + else + ourRange = word; + } else if (type == "triple") { + var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, line.anchor, line.head); + else + ourRange = line; + } else { + ourRange = extendRange(doc, ourRange, start); + } + + if (!addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex == -1) { + ourIndex = ranges.length; + setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single") { + setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0)); + startSel = doc.sel; + } else { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) return; + lastPos = pos; + + if (type == "rect") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); + else if (text.length > leftPos) + ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); + } + if (!ranges.length) ranges.push(new Range(start, start)); + setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var anchor = oldRange.anchor, head = pos; + if (type != "single") { + if (type == "double") + var range = cm.findWordAt(pos); + else + var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + } + var ranges = startSel.ranges.slice(0); + ranges[ourIndex] = new Range(clipPos(doc, anchor), head); + setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, type == "rect"); + if (!cur) return; + if (cmp(cur, lastPos) != 0) { + ensureFocus(cm); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) setTimeout(operation(cm, function() { + if (counter != curCount) return; + display.scroller.scrollTop += outside; + extend(e); + }), 50); + } + } + + function done(e) { + counter = Infinity; + e_preventDefault(e); + focusInput(cm); + off(document, "mousemove", move); + off(document, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function(e) { + if (!e_button(e)) done(e); + else extend(e); + }); + var up = operation(cm, done); + on(document, "mousemove", move); + on(document, "mouseup", up); + } + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent, signalfn) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; + if (prevent) e_preventDefault(e); + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signalfn(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e); + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true, signalLater); + } + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + return; + e_preventDefault(e); + if (ie) lastDrop = +new Date; + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || isReadOnly(cm)) return; + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function(file, i) { + var reader = new FileReader; + reader.onload = operation(cm, function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); + } + }); + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(bind(focusInput, cm), 20); + return; + } + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + if (cm.state.draggingText && !(mac ? e.metaKey : e.ctrlKey)) + var selected = cm.listSelections(); + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) for (var i = 0; i < selected.length; ++i) + replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); + cm.replaceSelection(text, "around", "paste"); + focusInput(cm); + } + } + catch(e){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; + + e.dataTransfer.setData("Text", cm.getSelection()); + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) img.parentNode.removeChild(img); + } + } + + // SCROLL EVENTS + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function setScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) return; + cm.doc.scrollTop = val; + if (!gecko) updateDisplaySimple(cm, {top: val}); + if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); + if (gecko) updateDisplaySimple(cm); + startWorker(cm, 100); + } + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller) { + if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; + cm.display.scrollbars.setScrollLeft(val); + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) wheelPixelsPerUnit = -.53; + else if (gecko) wheelPixelsPerUnit = 15; + else if (chrome) wheelPixelsPerUnit = -.7; + else if (safari) wheelPixelsPerUnit = -1/3; + + var wheelEventDelta = function(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; + else if (dy == null) dy = e.wheelDelta; + return {x: dx, y: dy}; + }; + CodeMirror.wheelEventPixels = function(e) { + var delta = wheelEventDelta(e); + delta.x *= wheelPixelsPerUnit; + delta.y *= wheelPixelsPerUnit; + return delta; + }; + + function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + if (!(dx && scroll.scrollWidth > scroll.clientWidth || + dy && scroll.scrollHeight > scroll.clientHeight)) return; + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer; + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy) + setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); + setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); + e_preventDefault(e); + display.wheelStartX = null; // Abort measurement, if in progress + return; + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) top = Math.max(0, top + pixels - 50); + else bot = Math.min(cm.doc.height, bot + pixels + 50); + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function() { + if (display.wheelStartX == null) return; + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) return; + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // KEY EVENTS + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + if (cm.display.pollingFast && readInput(cm)) cm.display.pollingFast = false; + var prevShift = cm.display.shift, done = false; + try { + if (isReadOnly(cm)) cm.state.suppressEdits = true; + if (dropShift) cm.display.shift = false; + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done; + } + + function lookupKeyForEditor(cm, name, handle) { + for (var i = 0; i < cm.state.keyMaps.length; i++) { + var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); + if (result) return result; + } + return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) + || lookupKey(name, cm.options.keyMap, handle, cm); + } + + var stopSeq = new Delayed; + function dispatchKey(cm, name, e, handle) { + var seq = cm.state.keySeq; + if (seq) { + if (isModifierKey(name)) return "handled"; + stopSeq.set(50, function() { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null; + resetInput(cm); + } + }); + name = seq + " " + name; + } + var result = lookupKeyForEditor(cm, name, handle); + + if (result == "multi") + cm.state.keySeq = name; + if (result == "handled") + signalLater(cm, "keyHandled", cm, name, e); + + if (result == "handled" || result == "multi") { + e_preventDefault(e); + restartBlink(cm); + } + + if (seq && !result && /\'$/.test(name)) { + e_preventDefault(e); + return true; + } + return !!result; + } + + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + var name = keyName(e, true); + if (!name) return false; + + if (e.shiftKey && !cm.state.keySeq) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);}) + || dispatchKey(cm, name, e, function(b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + return doHandleBinding(cm, b); + }); + } else { + return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); }); + } + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + return dispatchKey(cm, "'" + ch + "'", e, + function(b) { return doHandleBinding(cm, b, true); }); + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + ensureFocus(cm); + if (signalDOMEvent(cm, e)) return; + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + cm.replaceSelection("", null, "cut"); + } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + showCrossHair(cm); + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) this.doc.sel.shift = false; + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if (((presto && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (handleCharBinding(cm, e, ch)) return; + if (ie && ie_version >= 9) cm.display.inputHasSelection = null; + fastPoll(cm); + } + + // FOCUS/BLUR EVENTS + + function onFocus(cm) { + if (cm.options.readOnly == "nocursor") return; + if (!cm.state.focused) { + signal(cm, "focus", cm); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // The prevInput test prevents this from firing when a context + // menu is closed (since the resetInput would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + resetInput(cm); + if (webkit) setTimeout(bind(resetInput, cm, true), 0); // Issue #1730 + } + } + slowPoll(cm); + restartBlink(cm); + } + function onBlur(cm) { + if (cm.state.focused) { + signal(cm, "blur", cm); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (signalDOMEvent(cm, e, "contextmenu")) return; + var display = cm.display; + if (eventInWidget(display, e) || contextMenuInGutter(cm, e)) return; + + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) return; // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); + + var oldCSS = display.input.style.cssText; + display.inputDiv.style.position = "absolute"; + display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + + "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) + focusInput(cm); + if (webkit) window.scrollTo(null, oldScrollY); + resetInput(cm); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) display.input.value = display.prevInput = " "; + display.contextMenuPending = true; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (display.input.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = display.input.value = "\u200b" + (selected ? display.input.value : ""); + display.prevInput = selected ? "" : "\u200b"; + display.input.selectionStart = 1; display.input.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + display.contextMenuPending = false; + display.inputDiv.style.position = "relative"; + display.input.style.cssText = oldCSS; + if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); + slowPoll(cm); + + // Try to detect the user choosing select-all + if (display.input.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); + var i = 0, poll = function() { + if (display.selForContextMenu == cm.doc.sel && display.input.selectionStart == 0) + operation(cm, commands.selectAll)(cm); + else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); + else resetInput(cm); + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) prepareSelectAllHack(); + if (captureRightClick) { + e_stop(e); + var mouseup = function() { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + + // UPDATING + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + var changeEnd = CodeMirror.changeEnd = function(change) { + if (!change.text) return change.to; + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); + }; + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) return pos; + if (cmp(pos, change.to) <= 0) return changeEnd(change); + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; + return Pos(line, ch); + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(out, doc.sel.primIndex); + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + return Pos(nw.line, pos.ch - old.ch + nw.ch); + else + return Pos(nw.line + (pos.line - old.line), pos.ch); + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex); + } + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function() { this.canceled = true; } + }; + if (update) obj.update = function(from, to, text, origin) { + if (from) this.from = clipPos(doc, from); + if (to) this.to = clipPos(doc, to); + if (text) this.text = text; + if (origin !== undefined) this.origin = origin; + }; + signal(doc, "beforeChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); + + if (obj.canceled) return null; + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly); + if (doc.cm.state.suppressEdits) return; + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) return; + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + if (doc.cm && doc.cm.state.suppressEdits) return; + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + for (var i = 0; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + break; + } + if (i == source.length) return; + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return; + } + selAfter = event; + } + else break; + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + for (var i = event.changes.length - 1; i >= 0; --i) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return; + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) return; + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function(range) { + return new Range(Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch)); + }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + regLineChange(doc.cm, l, "gutter"); + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return; + } + if (change.from.line > doc.lastLine()) return; + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) selAfter = computeSelAfterChange(doc, change); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); + else updateDoc(doc, change, spans); + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function(line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true; + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + signalCursorActivity(cm); + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function(line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) cm.curOp.updateMaxLine = true; + } + + // Adjust frontier, schedule worker + doc.frontier = Math.min(doc.frontier, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (change.full) + regChange(cm); + else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + regLineChange(cm, from.line, "text"); + else + regChange(cm, from.line, to.line + 1, lendiff); + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) signalLater(cm, "change", cm, obj); + if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + if (!to) to = from; + if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } + if (typeof code == "string") code = splitLines(code); + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, coords) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) return; + + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (coords.top + box.top < 0) doScroll = true; + else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + + (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " + + (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " + + coords.left + "px; width: 2px;"); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) margin = 0; + for (var limit = 0; limit < 5; limit++) { + var changed = false, coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), + Math.min(coords.top, endCoords.top) - margin, + Math.max(coords.left, endCoords.left), + Math.max(coords.bottom, endCoords.bottom) + margin); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + setScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; + } + if (!changed) break; + } + return coords; + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, x1, y1, x2, y2) { + var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); + if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); + if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, x1, y1, x2, y2) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (y1 < 0) y1 = 0; + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = displayHeight(cm), result = {}; + if (y2 - y1 > screen) y2 = y1 + screen; + var docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; + if (y1 < screentop) { + result.scrollTop = atTop ? 0 : y1; + } else if (y2 > screentop + screen) { + var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); + if (newTop != screentop) result.scrollTop = newTop; + } + + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); + var tooWide = x2 - x1 > screenw; + if (tooWide) x2 = x1 + screenw; + if (x1 < 10) + result.scrollLeft = 0; + else if (x1 < screenleft) + result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)); + else if (x2 > screenw + screenleft - 3) + result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw; + return result; + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollPos(cm, left, top) { + if (left != null || top != null) resolveScrollToPos(cm); + if (left != null) + cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; + if (top != null) + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(), from = cur, to = cur; + if (!cm.options.lineWrapping) { + from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; + to = Pos(cur.line, cur.ch + 1); + } + cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), + Math.min(from.top, to.top) - range.margin, + Math.max(from.right, to.right), + Math.max(from.bottom, to.bottom) + range.margin); + cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + } + + // API UTILITIES + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) how = "add"; + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) how = "prev"; + else state = getStateBefore(cm, n); + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) line.stateAfter = null; + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) return; + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); + else indentation = 0; + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i, new Range(pos, pos)); + break; + } + } + } + line.stateAfter = null; + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType); + return line; + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break; + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function() { + for (var i = kill.length - 1; i >= 0; i--) + replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); + ensureCursorVisible(cm); + }); + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "char", "column" (like char, but doesn't + // cross line boundaries), "word" (across next word), or "group" (to + // the start of next group of word or non-word-non-whitespace + // chars). The visually param controls whether, in right-to-left + // text, direction 1 means to move towards the next index in the + // string, or towards the character to the right of the current + // position. The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var line = pos.line, ch = pos.ch, origDir = dir; + var lineObj = getLine(doc, line); + var possible = true; + function findNextLine() { + var l = line + dir; + if (l < doc.first || l >= doc.first + doc.size) return (possible = false); + line = l; + return lineObj = getLine(doc, l); + } + function moveOnce(boundToLine) { + var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); + if (next == null) { + if (!boundToLine && findNextLine()) { + if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); + else ch = dir < 0 ? lineObj.text.length : 0; + } else return (possible = false); + } else ch = next; + return true; + } + + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) break; + var cur = lineObj.text.charAt(ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) type = "s"; + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce();} + break; + } + + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; + } + } + var result = skipAtomic(doc, Pos(line, ch), origDir, true); + if (!possible) result.hitSide = true; + return result; + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + for (;;) { + var target = coordsChar(cm, x, y); + if (!target.outside) break; + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } + y += dir * 5; + } + return target; + } + + // EDITOR METHODS + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); focusInput(this); fastPoll(this);}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") return; + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + operation(this, optionHandlers[option])(this, value, old); + }, + + getOption: function(option) {return this.options[option];}, + getDoc: function() {return this.doc;}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + if (maps[i] == map || maps[i].name == map) { + maps.splice(i, 1); + return true; + } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) throw new Error("Overlays may not be stateful."); + this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return; + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; + } + if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); + }), + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var from = range.from(), to = range.to(); + var start = Math.max(end, from.line); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + indentLine(this, j, how); + var newRanges = this.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) ensureCursorVisible(this); + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + return takeToken(this, pos, precise); + }, + + getLineTokens: function(line, precise) { + return takeToken(this, Pos(line), precise, true); + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) type = styles[2]; + else for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; + else if (styles[mid * 2 + 1] < ch) before = mid + 1; + else { type = styles[mid * 2 + 2]; break; } + } + var cut = type ? type.indexOf("cm-overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1); + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) return mode; + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0]; + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) return helpers; + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) found.push(help[mode[type]]); + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) found.push(val); + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i = 0; i < help._global.length; i++) { + var cur = help._global[i]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + found.push(cur.val); + } + return found; + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getStateBefore(this, line + 1, precise); + }, + + cursorCoords: function(start, mode) { + var pos, range = this.doc.sel.primary(); + if (start == null) pos = range.head; + else if (typeof start == "object") pos = clipPos(this.doc, start); + else pos = start ? range.from() : range.to(); + return cursorCoords(this, pos, mode || "page"); + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page"); + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset); + }, + heightAtLine: function(line, mode) { + var end = false, last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) line = this.doc.first; + else if (line > last) { line = last; end = true; } + var lineObj = getLine(this.doc, line); + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + + (end ? this.doc.height - heightAtLine(lineObj) : 0); + }, + + defaultTextHeight: function() { return textHeight(this.display); }, + defaultCharWidth: function() { return charWidth(this.display); }, + + setGutterMarker: methodOp(function(line, gutterID, value) { + return changeLine(this.doc, line, "gutter", function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; + }); + }), + + clearGutter: methodOp(function(gutterID) { + var cm = this, doc = cm.doc, i = doc.first; + doc.iter(function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + regLineChange(cm, i, "gutter"); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; + } + ++i; + }); + }), + + addLineWidget: methodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options); + }), + + removeLineWidget: function(widget) { widget.clear(); }, + + lineInfo: function(line) { + if (typeof line == "number") { + if (!isLine(this.doc, line)) return null; + var n = line; + line = getLine(this.doc, line); + if (!line) return null; + } else { + var n = lineNo(line); + if (n == null) return null; + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets}; + }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + node.setAttribute("cm-ignore-events", "true"); + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + top = pos.top - node.offsetHeight; + else if (pos.bottom + node.offsetHeight <= vspace) + top = pos.bottom; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; + node.style.left = left + "px"; + } + if (scroll) + scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + return commands[cmd](this); + }, + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) break; + } + return cur; + }, + + moveH: methodOp(function(dir, unit) { + var cm = this; + cm.extendSelectionsBy(function(range) { + if (cm.display.shift || cm.doc.extend || range.empty()) + return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); + else + return dir < 0 ? range.from() : range.to(); + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + doc.replaceSelection("", null, "+delete"); + else + deleteNearSelection(this, function(range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; + }); + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) x = coords.left; + else coords.left = x; + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) break; + } + return cur; + }, + + moveV: methodOp(function(dir, unit) { + var cm = this, doc = this.doc, goals = []; + var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function(range) { + if (collapse) + return dir < 0 ? range.from() : range.to(); + var headPos = cursorCoords(cm, range.head, "div"); + if (range.goalColumn != null) headPos.left = range.goalColumn; + goals.push(headPos.left); + var pos = findPosV(cm, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); + return pos; + }, sel_move); + if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) + doc.sel.ranges[i].goalColumn = goals[i]; + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function(ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} + : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)); + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) return; + if (this.state.overwrite = !this.state.overwrite) + addClass(this.display.cursorDiv, "CodeMirror-overwrite"); + else + rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return activeElt() == this.display.input; }, + + scrollTo: methodOp(function(x, y) { + if (x != null || y != null) resolveScrollToPos(this); + if (x != null) this.curOp.scrollLeft = x; + if (y != null) this.curOp.scrollTop = y; + }), + getScrollInfo: function() { + var scroller = this.display.scroller; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, + width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, + clientHeight: displayHeight(this), clientWidth: displayWidth(this)}; + }, + + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) margin = this.options.cursorScrollMargin; + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) range.to = range.from; + range.margin = margin || 0; + + if (range.from.line != null) { + resolveScrollToPos(this); + this.curOp.scrollToPos = range; + } else { + var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), + Math.min(range.from.top, range.to.top) - range.margin, + Math.max(range.from.right, range.to.right), + Math.max(range.from.bottom, range.to.bottom) + range.margin); + this.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + }), + + setSize: methodOp(function(width, height) { + var cm = this; + function interpret(val) { + return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; + } + if (width != null) cm.display.wrapper.style.width = interpret(width); + if (height != null) cm.display.wrapper.style.height = interpret(height); + if (cm.options.lineWrapping) clearLineMeasurementCache(this); + var lineNo = cm.display.viewFrom; + cm.doc.iter(lineNo, cm.display.viewTo, function(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) + if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; } + ++lineNo; + }); + cm.curOp.forceUpdate = true; + signal(cm, "refresh", this); + }), + + operation: function(f){return runInOp(this, f);}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) + estimateLineHeights(this); + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + resetInput(this); + this.scrollTo(doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old; + }), + + getInputField: function(){return this.display.input;}, + getWrapperElement: function(){return this.display.wrapper;}, + getScrollerElement: function(){return this.display.scroller;}, + getGutterElement: function(){return this.display.gutters;} + }; + eventMixin(CodeMirror); + + // OPTION DEFAULTS + + // The default configuration options. + var defaults = CodeMirror.defaults = {}; + // Functions to run when options are changed. + var optionHandlers = CodeMirror.optionHandlers = {}; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) optionHandlers[name] = + notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; + } + + // Passed to option handlers when there is no old value. + var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function(cm, val) { + cm.setValue(val); + }, true); + option("mode", null, function(cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function(cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val) { + cm.options.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + cm.refresh(); + }, true); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); + option("electricChars", true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function(cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", function(cm, val, old) { + var next = getKeyMap(val); + var prev = old != CodeMirror.Init && getKeyMap(old); + if (prev && prev.detach) prev.detach(cm, next); + if (next.attach) next.attach(cm, prev || null); + }); + option("extraKeys", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function(cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true); + option("scrollbarStyle", "native", function(cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); + option("lineNumbers", false, function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + + option("readOnly", false, function(cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + cm.display.disabled = true; + } else { + cm.display.disabled = false; + if (!val) resetInput(cm); + } + }); + option("disableInput", false, function(cm, val) {if (!val) resetInput(cm);}, true); + option("dragDrop", true); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function(cm){cm.refresh();}, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function(cm, val) { + if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0; + }); + + option("tabindex", null, function(cm, val) { + cm.display.input.tabIndex = val || ""; + }); + option("autofocus", null); + + // MODE DEFINITION AND QUERYING + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + if (arguments.length > 2) + mode.dependencies = Array.prototype.slice.call(arguments, 2); + modes[name] = mode; + }; + + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") found = {name: found}; + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return CodeMirror.resolveMode("application/xml"); + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return CodeMirror.getMode(options, "text/plain"); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) modeObj.helperType = spec.helperType; + if (spec.modeProps) for (var prop in spec.modeProps) + modeObj[prop] = spec.modeProps[prop]; + + return modeObj; + }; + + // Minimal default mode. + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = CodeMirror.modeExtensions = {}; + CodeMirror.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + }; + + // EXTENSIONS + + CodeMirror.defineExtension = function(name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; + }; + CodeMirror.defineOption = option; + + var initHooks = []; + CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + var helpers = CodeMirror.helpers = {}; + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []}; + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + + // MODE STATE HANDLING + + // Utility functions for working with state. Exported because nested + // modes need to do this for their inner modes. + + var copyState = CodeMirror.copyState = function(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + }; + + var startState = CodeMirror.startState = function(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + }; + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + CodeMirror.innerMode = function(mode, state) { + while (mode.innerMode) { + var info = mode.innerMode(state); + if (!info || info.mode == mode) break; + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; + }; + + // STANDARD COMMANDS + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, + singleSelection: function(cm) { + cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); + }, + killLine: function(cm) { + deleteNearSelection(cm, function(range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + return {from: range.head, to: Pos(range.head.line + 1, 0)}; + else + return {from: range.head, to: Pos(range.head.line, len)}; + } else { + return {from: range.from(), to: range.to()}; + } + }); + }, + deleteLine: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; + }); + }, + delLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), to: range.from()}; + }); + }, + delWrappedLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()}; + }); + }, + delWrappedLineRight: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos }; + }); + }, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + undoSelection: function(cm) {cm.undoSelection();}, + redoSelection: function(cm) {cm.redoSelection();}, + goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, + goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, + goLineStart: function(cm) { + cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1}); + }, + goLineStartSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + return lineStartSmart(cm, range.head); + }, {origin: "+move", bias: 1}); + }, + goLineEnd: function(cm) { + cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1}); + }, + goLineRight: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + }, sel_move); + }, + goLineLeft: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div"); + }, sel_move); + }, + goLineLeftSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head); + return pos; + }, sel_move); + }, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goGroupRight: function(cm) {cm.moveH(1, "group");}, + goGroupLeft: function(cm) {cm.moveH(-1, "group");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharBefore: function(cm) {cm.deleteH(-1, "char");}, + delCharAfter: function(cm) {cm.deleteH(1, "char");}, + delWordBefore: function(cm) {cm.deleteH(-1, "word");}, + delWordAfter: function(cm) {cm.deleteH(1, "word");}, + delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, + delGroupAfter: function(cm) {cm.deleteH(1, "group");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t");}, + insertSoftTab: function(cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(new Array(tabSize - col % tabSize + 1).join(" ")); + } + cm.replaceSelections(spaces); + }, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.execCommand("insertTab"); + }, + transposeChars: function(cm) { + runInOp(cm, function() { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1); + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) + cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); + }, + newlineAndIndent: function(cm) { + runInOp(cm, function() { + var len = cm.listSelections().length; + for (var i = 0; i < len; i++) { + var range = cm.listSelections()[i]; + cm.replaceRange("\n", range.anchor, range.head, "+input"); + cm.indentLine(range.from().line + 1, null, true); + ensureCursorVisible(cm); + } + }); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + + // STANDARD KEYMAPS + + var keyMap = CodeMirror.keyMap = {}; + + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + fallthrough: "basic" + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function normalizeKeyName(name) { + var parts = name.split(/-(?!$)/), name = parts[parts.length - 1]; + var alt, ctrl, shift, cmd; + for (var i = 0; i < parts.length - 1; i++) { + var mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) cmd = true; + else if (/^a(lt)?$/i.test(mod)) alt = true; + else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true; + else if (/^s(hift)$/i.test(mod)) shift = true; + else throw new Error("Unrecognized modifier name: " + mod); + } + if (alt) name = "Alt-" + name; + if (ctrl) name = "Ctrl-" + name; + if (cmd) name = "Cmd-" + name; + if (shift) name = "Shift-" + name; + return name; + } + + // This is a kludge to keep keymaps mostly working as raw objects + // (backwards compatibility) while at the same time support features + // like normalization and multi-stroke key bindings. It compiles a + // new normalized keymap, and then updates the old object to reflect + // this. + CodeMirror.normalizeKeyMap = function(keymap) { + var copy = {}; + for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) { + var value = keymap[keyname]; + if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue; + if (value == "...") { delete keymap[keyname]; continue; } + + var keys = map(keyname.split(" "), normalizeKeyName); + for (var i = 0; i < keys.length; i++) { + var val, name; + if (i == keys.length - 1) { + name = keyname; + val = value; + } else { + name = keys.slice(0, i + 1).join(" "); + val = "..."; + } + var prev = copy[name]; + if (!prev) copy[name] = val; + else if (prev != val) throw new Error("Inconsistent bindings for " + name); + } + delete keymap[keyname]; + } + for (var prop in copy) keymap[prop] = copy[prop]; + return keymap; + }; + + var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) { + map = getKeyMap(map); + var found = map.call ? map.call(key, context) : map[key]; + if (found === false) return "nothing"; + if (found === "...") return "multi"; + if (found != null && handle(found)) return "handled"; + + if (map.fallthrough) { + if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") + return lookupKey(key, map.fallthrough, handle, context); + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle, context); + if (result) return result; + } + } + }; + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + var isModifierKey = CodeMirror.isModifierKey = function(value) { + var name = typeof value == "string" ? value : keyNames[value.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + }; + + // Look up the name of a key as indicated by an event object. + var keyName = CodeMirror.keyName = function(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) return false; + var base = keyNames[event.keyCode], name = base; + if (name == null || event.altGraphKey) return false; + if (event.altKey && base != "Alt") name = "Alt-" + name; + if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name; + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name; + if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name; + return name; + }; + + function getKeyMap(val) { + return typeof val == "string" ? keyMap[val] : val; + } + + // FROMTEXTAREA + + CodeMirror.fromTextArea = function(textarea, options) { + options = options ? copyObj(options) : {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabindex) + options.tabindex = textarea.tabindex; + if (!options.placeholder && textarea.placeholder) + options.placeholder = textarea.placeholder; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form, realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function() { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + options.finishInit = function(cm) { + cm.save = save; + cm.getTextArea = function() { return textarea; }; + cm.toTextArea = function() { + cm.toTextArea = isNaN; // Prevent this from being ran twice + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + }; + + textarea.style.display = "none"; + var cm = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + return cm; + }; + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = CodeMirror.StringStream = function(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + }; + + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == this.lineStart;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + indentation: function() { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + } + }; + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + var TextMarker = CodeMirror.TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + }; + eventMixin(TextMarker); + + // Clear the marker. + TextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) startOperation(cm); + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) signalLater(this, "clear", found.from, found.to); + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); + else if (cm) { + if (span.to != null) max = lineNo(line); + if (span.from != null) min = lineNo(line); + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) + updateLineHeight(line, textHeight(cm.display)); + } + if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { + var visual = visualLine(this.lines[i]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } + + if (min != null && cm && this.collapsed) regChange(cm, min, max + 1); + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) reCheckSelection(cm.doc); + } + if (cm) signalLater(cm, "markerCleared", cm, this); + if (withOp) endOperation(cm); + if (this.parent) this.parent.clear(); + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function(side, lineObj) { + if (side == null && this.type == "bookmark") side = 1; + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) return from; + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) return to; + } + } + return from && {from: from, to: to}; + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function() { + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) return; + runInOp(cm, function() { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + updateLineHeight(line, line.height + dHeight); + } + }); + }; + + TextMarker.prototype.attachLine = function(line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); + } + this.lines.push(line); + }; + TextMarker.prototype.detachLine = function(line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) return markTextShared(doc, from, to, options, type); + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) copyObj(options, marker, false); + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + return marker; + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true"); + if (options.insertLeft) marker.widgetNode.insertLeft = true; + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + throw new Error("Inserting collapsed marker partially overlapping an existing one"); + sawCollapsedSpans = true; + } + + if (marker.addToHistory) + addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function(line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + updateMaxLine = true; + if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { + if (lineIsHidden(doc, line)) updateLineHeight(line, 0); + }); + + if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); + + if (marker.readOnly) { + sawReadOnlySpans = true; + if (doc.history.done.length || doc.history.undone.length) + doc.clearHistory(); + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) cm.curOp.updateMaxLine = true; + if (marker.collapsed) + regChange(cm, from.line, to.line + 1); + else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css) + for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); + if (marker.atomic) reCheckSelection(cm.doc); + signalLater(cm, "markerAdded", cm, marker); + } + return marker; + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + markers[i].parent = this; + }; + eventMixin(SharedTextMarker); + + SharedTextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + this.markers[i].clear(); + signalLater(this, "clear"); + }; + SharedTextMarker.prototype.find = function(side, lineObj) { + return this.primary.find(side, lineObj); + }; + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function(doc) { + if (widget) options.widgetNode = widget.cloneNode(true); + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + if (doc.linked[i].isParent) return; + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary); + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), + function(m) { return m.parent; }); + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], linked = [marker.primary.doc];; + linkedDocs(marker.primary.doc, function(d) { linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + } + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) return span; + } + } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + for (var r, i = 0; i < spans.length; ++i) + if (spans[i] != span) (r || (r = [])).push(spans[i]); + return r; + } + // Add a span to a line. + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); + (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } + return nw; + } + function markedSpansAfter(old, endCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); + (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } + return nw; + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + if (change.full) return null; + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) return null; + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) span.to = startCh; + else if (sameLine) span.to = found.to == null ? null : found.to + offset; + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i = 0; i < last.length; ++i) { + var span = last[i]; + if (span.to != null) span.to += offset; + if (span.from == null) { + var found = getMarkedSpanFor(first, span.marker); + if (!found) { + span.from = offset; + if (sameLine) (first || (first = [])).push(span); + } + } else { + span.from += offset; + if (sameLine) (first || (first = [])).push(span); + } + } + } + // Make sure we didn't create any zero-length spans + if (first) first = clearEmptySpans(first); + if (last && last != first) last = clearEmptySpans(last); + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + for (var i = 0; i < first.length; ++i) + if (first[i].to == null) + (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); + for (var i = 0; i < gap; ++i) + newMarkers.push(gapMarkers); + newMarkers.push(last); + } + return newMarkers; + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + spans.splice(i--, 1); + } + if (!spans.length) return null; + return spans; + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) return stretched; + if (!stretched) return old; + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + if (oldCur[k].marker == span.marker) continue spans; + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old; + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function(line) { + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + (markers || (markers = [])).push(mark); + } + }); + if (!markers) return null; + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + newParts.push({from: p.from, to: m.from}); + if (dto > 0 || !mk.inclusiveRight && !dto) + newParts.push({from: m.to, to: p.to}); + parts.splice.apply(parts, newParts); + j += newParts.length - 1; + } + } + return parts; + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.attachLine(line); + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) return lenDiff; + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) return -fromCmp; + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) return toCmp; + return b.id - a.id; + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + found = sp.marker; + } + return found; + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) continue; + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; + if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) || + fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight))) + return true; + } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + line = merged.find(-1, true).line; + return line; + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + (lines || (lines = [])).push(line); + } + return lines; + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) return lineN; + return lineNo(vis); + } + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) return lineN; + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) return lineN; + while (merged = collapsedSpanAtEnd(line)) + line = merged.find(1, true).line; + return lineNo(line) + 1; + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if (sp.from == null) return true; + if (sp.marker.widgetNode) continue; + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + return true; + } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)); + } + if (span.marker.inclusiveRight && span.to == line.text.length) + return true; + for (var sp, i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) return true; + } + } + + // LINE WIDGETS + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = CodeMirror.LineWidget = function(cm, node, options) { + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) + this[opt] = options[opt]; + this.cm = cm; + this.node = node; + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + addToScrollPos(cm, null, diff); + } + + LineWidget.prototype.clear = function() { + var cm = this.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) return; + for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); + if (!ws.length) line.widgets = null; + var height = widgetHeight(this); + runInOp(cm, function() { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + updateLineHeight(line, Math.max(0, line.height - height)); + }); + }; + LineWidget.prototype.changed = function() { + var oldH = this.height, cm = this.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) return; + runInOp(cm, function() { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + updateLineHeight(line, line.height + diff); + }); + }; + + function widgetHeight(widget) { + if (widget.height != null) return widget.height; + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + parentStyle += "margin-left: -" + widget.cm.display.gutters.offsetWidth + "px;"; + if (widget.noHScroll) + parentStyle += "width: " + widget.cm.display.wrapper.clientWidth + "px;"; + removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.offsetHeight; + } + + function addLineWidget(cm, handle, node, options) { + var widget = new LineWidget(cm, node, options); + if (widget.noHScroll) cm.display.alignWidgets = true; + changeLine(cm.doc, handle, "widget", function(line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) widgets.push(widget); + else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); + widget.line = line; + if (!lineIsHidden(cm.doc, line)) { + var aboveVisible = heightAtLine(line) < cm.doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) addToScrollPos(cm, null, widget.height); + cm.curOp.forceUpdate = true; + } + return true; + }); + return widget; + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + eventMixin(Line); + Line.prototype.lineNo = function() { return lineNo(this); }; + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + if (line.order != null) line.order = null; + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) updateLineHeight(line, estHeight); + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + function extractLineClasses(type, output) { + if (type) for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) break; + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + output[prop] = lineClass[2]; + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) + output[prop] += " " + lineClass[2]; + } + return type; + } + + function callBlankLine(mode, state) { + if (mode.blankLine) return mode.blankLine(state); + if (!mode.innerMode) return; + var inner = CodeMirror.innerMode(mode, state); + if (inner.mode.blankLine) return inner.mode.blankLine(inner.state); + } + + function readToken(mode, stream, state, inner) { + for (var i = 0; i < 10; i++) { + if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode; + var style = mode.token(stream, state); + if (stream.pos > stream.start) return style; + } + throw new Error("Mode " + mode.name + " failed to advance stream."); + } + + // Utility for getTokenAt and getLineTokens + function takeToken(cm, pos, precise, asArray) { + function getObj(copy) { + return {start: stream.start, end: stream.pos, + string: stream.current(), + type: style || null, + state: copy ? copyState(doc.mode, state) : state}; + } + + var doc = cm.doc, mode = doc.mode, style; + pos = clipPos(doc, pos); + var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise); + var stream = new StringStream(line.text, cm.options.tabSize), tokens; + if (asArray) tokens = []; + while ((asArray || stream.pos < pos.ch) && !stream.eol()) { + stream.start = stream.pos; + style = readToken(mode, stream, state); + if (asArray) tokens.push(getObj(true)); + } + return asArray ? tokens : getObj(); + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; + var inner = cm.options.addModeClass && [null]; + if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses); + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) processLine(cm, text, state, stream.pos); + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses); + } + if (inner) { + var mName = inner[0].name; + if (mName) style = "m-" + (style ? mName + " " + style : mName); + } + if (!flattenSpans || curStyle != style) { + while (curStart < stream.start) { + curStart = Math.min(stream.start, curStart + 50000); + f(curStart, curStyle); + } + curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 characters + var pos = Math.min(stream.pos, curStart + 50000); + f(pos, curStyle); + curStart = pos; + } + } + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, state, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, state, function(end, style) { + st.push(end, style); + }, lineClasses, forceToEnd); + + // Run overlays, adjust style array. + for (var o = 0; o < cm.state.overlays.length; ++o) { + var overlay = cm.state.overlays[o], i = 1, at = 0; + runMode(cm, line.text, overlay.mode, true, function(end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + st.splice(i, 1, end, st[i+1], i_end); + i += 2; + at = Math.min(end, i_end); + } + if (!style) return; + if (overlay.opaque) { + st.splice(start, i - start, end, "cm-overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style; + } + } + }, lineClasses); + } + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}; + } + + function getLineStyles(cm, line, updateFrontier) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); + line.styles = result.styles; + if (result.classes) line.styleClasses = result.classes; + else if (line.styleClasses) line.styleClasses = null; + if (updateFrontier === cm.doc.frontier) cm.doc.frontier++; + } + return line.styles; + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, state, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize); + stream.start = stream.pos = startAt || 0; + if (text == "") callBlankLine(mode, state); + while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { + readToken(mode, stream, state); + stream.start = stream.pos; + } + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) return null; + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")); + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: elt("pre", [content]), content: content, col: 0, pos: 0, cm: cm}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order; + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if ((ie || webkit) && cm.getOption("lineWrapping")) + builder.addToken = buildTokenSplitSpaces(builder.addToken); + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) + builder.addToken = buildTokenBadBidi(builder.addToken, order); + builder.map = []; + var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); + insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); + if (line.styleClasses.textClass) + builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map); + (lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + // See issue #2901 + if (webkit && /\bcm-tab\b/.test(builder.content.lastChild.className)) + builder.content.className = "cm-tab-wrap-hack"; + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); + + return builder; + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + return token; + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, title, css) { + if (!text) return; + var special = builder.cm.options.specialChars, mustWrap = false; + if (!special.test(text)) { + builder.col += text.length; + var content = document.createTextNode(text); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) mustWrap = true; + builder.pos += text.length; + } else { + var content = document.createDocumentFragment(), pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(text.slice(pos, pos + skipped)); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + builder.col += tabWidth; + } else { + var txt = builder.cm.options.specialCharPlaceholder(m[0]); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt); + builder.pos++; + } + } + if (style || startStyle || endStyle || mustWrap || css) { + var fullStyle = style || ""; + if (startStyle) fullStyle += startStyle; + if (endStyle) fullStyle += endStyle; + var token = elt("span", [content], fullStyle, css); + if (title) token.title = title; + return builder.content.appendChild(token); + } + builder.content.appendChild(content); + } + + function buildTokenSplitSpaces(inner) { + function split(old) { + var out = " "; + for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; + out += " "; + return out; + } + return function(builder, text, style, startStyle, endStyle, title) { + inner(builder, text.replace(/ {3,}/g, split), style, startStyle, endStyle, title); + }; + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function(builder, text, style, startStyle, endStyle, title) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + for (var i = 0; i < order.length; i++) { + var part = order[i]; + if (part.to > start && part.from <= start) break; + } + if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title); + inner(builder, text.slice(0, part.to - start), style, startStyle, null, title); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + }; + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) { + builder.map.push(builder.pos, builder.pos + size, widget); + builder.content.appendChild(widget); + } + builder.pos += size; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i = 1; i < styles.length; i+=2) + builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options)); + return; + } + + var len = allText.length, pos = 0, i = 1, text = "", style, css; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = title = css = ""; + collapsed = null; nextChange = Infinity; + var foundBookmarks = []; + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (sp.from <= pos && (sp.to == null || sp.to > pos)) { + if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; } + if (m.className) spanStyle += " " + m.className; + if (m.css) css = m.css; + if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; + if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; + if (m.title && !title) title = m.title; + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + collapsed = sp; + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) foundBookmarks.push(m); + } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) return; + } + if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); + } + if (pos >= len) break; + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore); + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null;} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + function linesFor(start, end) { + for (var i = start, result = []; i < end; ++i) + result.push(new Line(text[i], spansFor(i), estimateHeight)); + return result; + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (change.full) { + doc.insert(0, linesFor(0, text.length)); + doc.remove(text.length, doc.size - text.length); + } else if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = linesFor(0, text.length - 1); + update(lastLine, lastLine.text, lastSpans); + if (nlines) doc.remove(from.line, nlines); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + var added = linesFor(1, text.length - 1); + added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + var added = linesFor(1, text.length - 1); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1); + doc.insert(from.line + 1, added); + } + + signalLater(doc, "change", doc, change); + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, height = 0; i < lines.length; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) lines[i].parent = this; + }, + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines); + }, + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iterN: function(at, n, op) { + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + var nextDocId = 0; + var Doc = CodeMirror.Doc = function(text, mode, firstLine) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); + if (firstLine == null) firstLine = 0; + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.frontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + + if (typeof text == "string") text = splitLines(text); + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) this.iterN(from - this.first, to - from, op); + else this.iterN(this.first, this.first + this.size, from); + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) height += lines[i].height; + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: splitLines(code), origin: "setValue", full: true}, true); + setSelection(this, simpleSelection(top)); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, + + getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, + getLineNumber: function(line) {return lineNo(line);}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") line = getLine(this, line); + return visualLine(line); + }, + + lineCount: function() {return this.size;}, + firstLine: function() {return this.first;}, + lastLine: function() {return this.first + this.size - 1;}, + + clipPos: function(pos) {return clipPos(this, pos);}, + + getCursor: function(start) { + var range = this.sel.primary(), pos; + if (start == null || start == "head") pos = range.head; + else if (start == "anchor") pos = range.anchor; + else if (start == "end" || start == "to" || start === false) pos = range.to(); + else pos = range.from(); + return pos; + }, + listSelections: function() { return this.sel.ranges; }, + somethingSelected: function() {return this.sel.somethingSelected();}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads, options)); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + extendSelections(this, map(this.sel.ranges, f), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) return; + for (var i = 0, out = []; i < ranges.length; i++) + out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head)); + if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex); + setSelection(this, normalizeSelection(out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) return lines; + else return lines.join(lineSep || "\n"); + }, + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) sel = sel.join(lineSep || "\n"); + parts[i] = sel; + } + return parts; + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + dup[i] = code; + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i = changes.length - 1; i >= 0; i--) + makeChange(this, changes[i]); + if (newSel) setSelectionReplaceHistory(this, newSel); + else if (this.cm) ensureCursorVisible(this.cm); + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend;}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done; + for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone; + return {undo: done, redo: undone}; + }, + clearHistory: function() {this.history = new History(this.history.maxGeneration);}, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; + return this.history.generation; + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration); + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)}; + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + if (!line[prop]) line[prop] = cls; + else if (classTest(cls).test(line[prop])) return false; + else line[prop] += " " + cls; + return true; + }); + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) return false; + else if (cls == null) line[prop] = null; + else { + var found = cur.match(classTest(cls)); + if (!found) return false; + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true; + }); + }), + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark"); + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker.parent || span.marker); + } + return markers; + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function(line) { + var spans = line.markedSpans; + if (spans) for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(lineNo == from.line && from.ch > span.to || + span.from == null && lineNo != from.line|| + lineNo == to.line && span.from > to.ch) && + (!filter || filter(span.marker))) + found.push(span.marker.parent || span.marker); + } + ++lineNo; + }); + return found; + }, + getAllMarks: function() { + var markers = []; + this.iter(function(line) { + var sps = line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) + if (sps[i].from != null) markers.push(sps[i].marker); + }); + return markers; + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first; + this.iter(function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)); + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) return 0; + this.iter(this.first, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc; + }, + + linkedDoc: function(options) { + if (!options) options = {}; + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) from = options.from; + if (options.to != null && options.to < to) to = options.to; + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); + if (options.sharedHist) copy.history = this.history; + (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy; + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) other = other.doc; + if (this.linked) for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) continue; + this.linked.splice(i, 1); + other.unlinkDoc(this); + detachSharedMarkers(findSharedMarkers(this)); + break; + } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode;}, + getEditor: function() {return this.cm;} + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor".split(" "); + for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments);}; + })(Doc.prototype[prop]); + + eventMixin(Doc); + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) continue; + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) continue; + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) throw new Error("This document is already in use."); + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + if (!cm.options.lineWrapping) findMaxLine(cm); + cm.options.mode = doc.modeOption; + regChange(cm); + } + + // LINE UTILITIES + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document."); + for (var chunk = doc; !chunk.lines;) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function(line) { + var text = line.text; + if (n == end.line) text = text.slice(0, end.ch); + if (n == start.line) text = text.slice(start.ch); + out.push(text); + ++n; + }); + return out; + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function(line) { out.push(line.text); }); + return out; + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) for (var n = line; n; n = n.parent) n.height += diff; + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first; + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i = 0; i < chunk.children.length; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) break; + else h += line.height; + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i = 0; i < p.children.length; ++i) { + var cur = p.children[i]; + if (cur == chunk) break; + else h += cur.height; + } + } + return h; + } + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line) { + var order = line.order; + if (order == null) order = line.order = bidiOrdering(line.text); + return order; + } + + // HISTORY + + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) array.pop(); + else break; + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done); + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done); + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done); + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, ore are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + pushSelectionToHistory(doc.sel, hist.done); + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) hist.done.shift(); + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) signal(doc, "historyAdded"); + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500); + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + hist.done[hist.done.length - 1] = sel; + else + pushSelectionToHistory(sel, hist.done); + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + clearSelectionEvents(hist.undone); + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + dest.push(sel); + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { + if (line.markedSpans) + (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) return null; + for (var i = 0, out; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } + else if (out) out.push(spans[i]); + } + return !out ? spans : out.length ? out : null; + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) return null; + for (var i = 0, nw = []; i < change.text.length; ++i) + nw.push(removeClearedSpans(found[i])); + return nw; + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + for (var i = 0, copy = []; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue; + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m; + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } + } + } + return copy; + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue; + } + for (var j = 0; j < sub.changes.length; ++j) { + var cur = sub.changes[j]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break; + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // EVENT UTILITIES + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + var e_preventDefault = CodeMirror.e_preventDefault = function(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + }; + var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + }; + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; + } + var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);}; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) b = 1; + else if (e.button & 2) b = 3; + else if (e.button & 4) b = 2; + } + if (mac && e.ctrlKey && b == 1) b = 3; + return b; + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var on = CodeMirror.on = function(emitter, type, f) { + if (emitter.addEventListener) + emitter.addEventListener(type, f, false); + else if (emitter.attachEvent) + emitter.attachEvent("on" + type, f); + else { + var map = emitter._handlers || (emitter._handlers = {}); + var arr = map[type] || (map[type] = []); + arr.push(f); + } + }; + + var off = CodeMirror.off = function(emitter, type, f) { + if (emitter.removeEventListener) + emitter.removeEventListener(type, f, false); + else if (emitter.detachEvent) + emitter.detachEvent("on" + type, f); + else { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + for (var i = 0; i < arr.length; ++i) + if (arr[i] == f) { arr.splice(i, 1); break; } + } + }; + + var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); + }; + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + function bnd(f) {return function(){f.apply(null, args);};}; + for (var i = 0; i < arr.length; ++i) + list.push(bnd(arr[i])); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + if (typeof e == "string") + e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) return; + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1) + set.push(arr[i]); + } + + function hasHandler(emitter, type) { + var arr = emitter._handlers && emitter._handlers[type]; + return arr && arr.length > 0; + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // MISC UTILITIES + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerGap = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + function Delayed() {this.id = null;} + Delayed.prototype.set = function(ms, f) { + clearTimeout(this.id); + this.id = setTimeout(f, ms); + }; + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + return n + (end - i); + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + }; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) nextTab = string.length; + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + return pos + Math.min(skipped, goal - col); + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) return pos; + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + spaceStrs.push(lst(spaceStrs) + " "); + return spaceStrs[n]; + } + + function lst(arr) { return arr[arr.length-1]; } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; + else if (ie) // Suppress mysterious IE10 errors + selectInput = function(node) { try { node.select(); } catch(_e) {} }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + if (array[i] == elt) return i; + return -1; + } + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) out[i] = f(array[i], i); + return out; + } + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + var ctor = function() {}; + ctor.prototype = base; + inst = new ctor(); + } + if (props) copyObj(props, inst); + return inst; + }; + + function copyObj(obj, target, overwrite) { + if (!target) target = {}; + for (var prop in obj) + if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + target[prop] = obj[prop]; + return target; + } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args);}; + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + var isWordCharBasic = CodeMirror.isWordChar = function(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + }; + function isWordChar(ch, helper) { + if (!helper) return isWordCharBasic(ch); + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true; + return helper.test(ch); + } + + function isEmpty(obj) { + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; + return true; + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } + + // DOM UTILITIES + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") e.appendChild(document.createTextNode(content)); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + var range; + if (document.createRange) range = function(node, start, end) { + var r = document.createRange(); + r.setEnd(node, end); + r.setStart(node, start); + return r; + }; + else range = function(node, start, end) { + var r = document.body.createTextRange(); + try { r.moveToElementText(node.parentNode); } + catch(e) { return r; } + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r; + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); + return e; + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e); + } + + var contains = CodeMirror.contains = function(parent, child) { + if (parent.contains) + return parent.contains(child); + while (child = child.parentNode) { + if (child.nodeType == 11) child = child.host; + if (child == parent) return true; + } + }; + + function activeElt() { return document.activeElement; } + // Older versions of IE throws unspecified error when touching + // document.activeElement in some cases (during loading, in iframe) + if (ie && ie_version < 11) activeElt = function() { + try { return document.activeElement; } + catch(e) { return document.body; } + }; + + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); } + var rmClass = CodeMirror.rmClass = function(node, cls) { + var current = node.className; + var match = classTest(cls).exec(current); + if (match) { + var after = current.slice(match.index + match[0].length); + node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); + } + }; + var addClass = CodeMirror.addClass = function(node, cls) { + var current = node.className; + if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls; + }; + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i]; + return b; + } + + // WINDOW-WIDE EVENTS + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.body.getElementsByClassName) return; + var byClass = document.body.getElementsByClassName("CodeMirror"); + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) f(cm); + } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) return; + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function() { + if (resizeTimer == null) resizeTimer = setTimeout(function() { + resizeTimer = null; + forEachCodeMirror(onResize); + }, 100); + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function() { + forEachCodeMirror(onBlur); + }); + } + + // FEATURE DETECTION + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) return false; + var div = elt('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); + } + if (zwspSupported) return elt("span", "\u200b"); + else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) return badBidiRects; + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) + var r1 = range(txt, 1, 2).getBoundingClientRect(); + return badBidiRects = (r1.right - r0.right < 3); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) nl = string.length; + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result; + } : function(string){return string.split(/\r\n?|\n/);}; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + var hasCopyEvent = (function() { + var e = elt("div"); + if ("oncopy" in e) return true; + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function"; + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) return badZoomedRects; + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1; + } + + // KEY NAMES + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) return f(from, to, "ltr"); + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); + found = true; + } + } + if (!found) f(from, to, "ltr"); + } + + function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } + function bidiRight(part) { return part.level % 2 ? part.from : part.to; } + + function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } + function lineRight(line) { + var order = getOrder(line); + if (!order) return line.text.length; + return bidiRight(lst(order)); + } + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) lineN = lineNo(visual); + var order = getOrder(visual); + var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); + return Pos(lineN, ch); + } + function lineEnd(cm, lineN) { + var merged, line = getLine(cm.doc, lineN); + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + lineN = null; + } + var order = getOrder(line); + var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); + return Pos(lineN == null ? lineNo(line) : lineN, ch); + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS); + } + return start; + } + + function compareBidiLevel(order, a, b) { + var linedir = order[0].level; + if (a == linedir) return true; + if (b == linedir) return false; + return a < b; + } + var bidiOther; + function getBidiPartAt(order, pos) { + bidiOther = null; + for (var i = 0, found; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < pos && cur.to > pos) return i; + if ((cur.from == pos || cur.to == pos)) { + if (found == null) { + found = i; + } else if (compareBidiLevel(order, cur.level, order[found].level)) { + if (cur.from != cur.to) bidiOther = found; + return i; + } else { + if (cur.from != cur.to) bidiOther = i; + return found; + } + } + } + return found; + } + + function moveInLine(line, pos, dir, byUnit) { + if (!byUnit) return pos + dir; + do pos += dir; + while (pos > 0 && isExtendingChar(line.text.charAt(pos))); + return pos; + } + + // This is needed in order to move 'visually' through bi-directional + // text -- i.e., pressing left should make the cursor go left, even + // when in RTL text. The tricky part is the 'jumps', where RTL and + // LTR text touch each other. This often requires the cursor offset + // to move more than one unit, in order to visually move one unit. + function moveVisually(line, start, dir, byUnit) { + var bidi = getOrder(line); + if (!bidi) return moveLogically(line, start, dir, byUnit); + var pos = getBidiPartAt(bidi, start), part = bidi[pos]; + var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); + + for (;;) { + if (target > part.from && target < part.to) return target; + if (target == part.from || target == part.to) { + if (getBidiPartAt(bidi, target) == pos) return target; + part = bidi[pos += dir]; + return (dir > 0) == part.level % 2 ? part.to : part.from; + } else { + part = bidi[pos += dir]; + if (!part) return null; + if ((dir > 0) == part.level % 2) + target = moveInLine(line, part.to, -1, byUnit); + else + target = moveInLine(line, part.from, 1, byUnit); + } + } + } + + function moveLogically(line, start, dir, byUnit) { + var target = start + dir; + if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir; + return target < 0 || target > line.text.length ? null : target; + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6ff + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"; + function charType(code) { + if (code <= 0xf7) return lowTypes.charAt(code); + else if (0x590 <= code && code <= 0x5f4) return "R"; + else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600); + else if (0x6ee <= code && code <= 0x8ac) return "r"; + else if (0x2000 <= code && code <= 0x200b) return "w"; + else if (code == 0x200c) return "b"; + else return "L"; + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + // Browsers seem to always treat the boundaries of block elements as being L. + var outerType = "L"; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str) { + if (!bidiRE.test(str)) return false; + var len = str.length, types = []; + for (var i = 0, type; i < len; ++i) + types.push(type = charType(str.charCodeAt(i))); + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i = 0, prev = outerType; i < len; ++i) { + var type = types[i]; + if (type == "m") types[i] = prev; + else prev = type; + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (type == "1" && cur == "r") types[i] = "n"; + else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i = 1, prev = types[0]; i < len - 1; ++i) { + var type = types[i]; + if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; + else if (type == "," && prev == types[i+1] && + (prev == "1" || prev == "n")) types[i] = prev; + prev = type; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i = 0; i < len; ++i) { + var type = types[i]; + if (type == ",") types[i] = "N"; + else if (type == "%") { + for (var end = i + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (cur == "L" && type == "1") types[i] = "L"; + else if (isStrong.test(type)) cur = type; + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i = 0; i < len; ++i) { + if (isNeutral.test(types[i])) { + for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} + var before = (i ? types[i-1] : outerType) == "L"; + var after = (end < len ? types[end] : outerType) == "L"; + var replace = before || after ? "L" : "R"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i = 0; i < len;) { + if (countsAsLeft.test(types[i])) { + var start = i; + for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} + order.push(new BidiSpan(0, start, i)); + } else { + var pos = i, at = order.length; + for (++i; i < len && types[i] != "L"; ++i) {} + for (var j = pos; j < i;) { + if (countsAsNum.test(types[j])) { + if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j)); + var nstart = j; + for (++j; j < i && countsAsNum.test(types[j]); ++j) {} + order.splice(at, 0, new BidiSpan(2, nstart, j)); + pos = j; + } else ++j; + } + if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i)); + } + } + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + if (order[0].level != lst(order).level) + order.push(new BidiSpan(order[0].level, len, len)); + + return order; + }; + })(); + + // THE END + + CodeMirror.version = "4.12.1"; + + return CodeMirror; +}); diff --git a/plugins/dynamix.vm.manager/scripts/codemirror/mode/xml/xml.js b/plugins/dynamix.vm.manager/scripts/codemirror/mode/xml/xml.js new file mode 100644 index 000000000..2f3b8f87a --- /dev/null +++ b/plugins/dynamix.vm.manager/scripts/codemirror/mode/xml/xml.js @@ -0,0 +1,384 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("xml", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var multilineTagIndentFactor = parserConfig.multilineTagIndentFactor || 1; + var multilineTagIndentPastTag = parserConfig.multilineTagIndentPastTag; + if (multilineTagIndentPastTag == null) multilineTagIndentPastTag = true; + + var Kludges = parserConfig.htmlMode ? { + autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, + 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, + 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, + 'track': true, 'wbr': true, 'menuitem': true}, + implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, + 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, + 'th': true, 'tr': true}, + contextGrabbers: { + 'dd': {'dd': true, 'dt': true}, + 'dt': {'dd': true, 'dt': true}, + 'li': {'li': true}, + 'option': {'option': true, 'optgroup': true}, + 'optgroup': {'optgroup': true}, + 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, + 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, + 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, + 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, + 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, + 'rp': {'rp': true, 'rt': true}, + 'rt': {'rp': true, 'rt': true}, + 'tbody': {'tbody': true, 'tfoot': true}, + 'td': {'td': true, 'th': true}, + 'tfoot': {'tbody': true}, + 'th': {'td': true, 'th': true}, + 'thead': {'tbody': true, 'tfoot': true}, + 'tr': {'tr': true} + }, + doNotIndent: {"pre": true}, + allowUnquoted: true, + allowMissing: true, + caseFold: true + } : { + autoSelfClosers: {}, + implicitlyClosed: {}, + contextGrabbers: {}, + doNotIndent: {}, + allowUnquoted: false, + allowMissing: false, + caseFold: false + }; + var alignCDATA = parserConfig.alignCDATA; + + // Return variables for tokenizers + var type, setStyle; + + function inText(stream, state) { + function chain(parser) { + state.tokenize = parser; + return parser(stream, state); + } + + var ch = stream.next(); + if (ch == "<") { + if (stream.eat("!")) { + if (stream.eat("[")) { + if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); + else return null; + } else if (stream.match("--")) { + return chain(inBlock("comment", "-->")); + } else if (stream.match("DOCTYPE", true, true)) { + stream.eatWhile(/[\w\._\-]/); + return chain(doctype(1)); + } else { + return null; + } + } else if (stream.eat("?")) { + stream.eatWhile(/[\w\._\-]/); + state.tokenize = inBlock("meta", "?>"); + return "meta"; + } else { + type = stream.eat("/") ? "closeTag" : "openTag"; + state.tokenize = inTag; + return "tag bracket"; + } + } else if (ch == "&") { + var ok; + if (stream.eat("#")) { + if (stream.eat("x")) { + ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); + } else { + ok = stream.eatWhile(/[\d]/) && stream.eat(";"); + } + } else { + ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); + } + return ok ? "atom" : "error"; + } else { + stream.eatWhile(/[^&<]/); + return null; + } + } + + function inTag(stream, state) { + var ch = stream.next(); + if (ch == ">" || (ch == "/" && stream.eat(">"))) { + state.tokenize = inText; + type = ch == ">" ? "endTag" : "selfcloseTag"; + return "tag bracket"; + } else if (ch == "=") { + type = "equals"; + return null; + } else if (ch == "<") { + state.tokenize = inText; + state.state = baseState; + state.tagName = state.tagStart = null; + var next = state.tokenize(stream, state); + return next ? next + " tag error" : "tag error"; + } else if (/[\'\"]/.test(ch)) { + state.tokenize = inAttribute(ch); + state.stringStartCol = stream.column(); + return state.tokenize(stream, state); + } else { + stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/); + return "word"; + } + } + + function inAttribute(quote) { + var closure = function(stream, state) { + while (!stream.eol()) { + if (stream.next() == quote) { + state.tokenize = inTag; + break; + } + } + return "string"; + }; + closure.isInAttribute = true; + return closure; + } + + function inBlock(style, terminator) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.match(terminator)) { + state.tokenize = inText; + break; + } + stream.next(); + } + return style; + }; + } + function doctype(depth) { + return function(stream, state) { + var ch; + while ((ch = stream.next()) != null) { + if (ch == "<") { + state.tokenize = doctype(depth + 1); + return state.tokenize(stream, state); + } else if (ch == ">") { + if (depth == 1) { + state.tokenize = inText; + break; + } else { + state.tokenize = doctype(depth - 1); + return state.tokenize(stream, state); + } + } + } + return "meta"; + }; + } + + function Context(state, tagName, startOfLine) { + this.prev = state.context; + this.tagName = tagName; + this.indent = state.indented; + this.startOfLine = startOfLine; + if (Kludges.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent)) + this.noIndent = true; + } + function popContext(state) { + if (state.context) state.context = state.context.prev; + } + function maybePopContext(state, nextTagName) { + var parentTagName; + while (true) { + if (!state.context) { + return; + } + parentTagName = state.context.tagName; + if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) || + !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { + return; + } + popContext(state); + } + } + + function baseState(type, stream, state) { + if (type == "openTag") { + state.tagStart = stream.column(); + return tagNameState; + } else if (type == "closeTag") { + return closeTagNameState; + } else { + return baseState; + } + } + function tagNameState(type, stream, state) { + if (type == "word") { + state.tagName = stream.current(); + setStyle = "tag"; + return attrState; + } else { + setStyle = "error"; + return tagNameState; + } + } + function closeTagNameState(type, stream, state) { + if (type == "word") { + var tagName = stream.current(); + if (state.context && state.context.tagName != tagName && + Kludges.implicitlyClosed.hasOwnProperty(state.context.tagName)) + popContext(state); + if (state.context && state.context.tagName == tagName) { + setStyle = "tag"; + return closeState; + } else { + setStyle = "tag error"; + return closeStateErr; + } + } else { + setStyle = "error"; + return closeStateErr; + } + } + + function closeState(type, _stream, state) { + if (type != "endTag") { + setStyle = "error"; + return closeState; + } + popContext(state); + return baseState; + } + function closeStateErr(type, stream, state) { + setStyle = "error"; + return closeState(type, stream, state); + } + + function attrState(type, _stream, state) { + if (type == "word") { + setStyle = "attribute"; + return attrEqState; + } else if (type == "endTag" || type == "selfcloseTag") { + var tagName = state.tagName, tagStart = state.tagStart; + state.tagName = state.tagStart = null; + if (type == "selfcloseTag" || + Kludges.autoSelfClosers.hasOwnProperty(tagName)) { + maybePopContext(state, tagName); + } else { + maybePopContext(state, tagName); + state.context = new Context(state, tagName, tagStart == state.indented); + } + return baseState; + } + setStyle = "error"; + return attrState; + } + function attrEqState(type, stream, state) { + if (type == "equals") return attrValueState; + if (!Kludges.allowMissing) setStyle = "error"; + return attrState(type, stream, state); + } + function attrValueState(type, stream, state) { + if (type == "string") return attrContinuedState; + if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return attrState;} + setStyle = "error"; + return attrState(type, stream, state); + } + function attrContinuedState(type, stream, state) { + if (type == "string") return attrContinuedState; + return attrState(type, stream, state); + } + + return { + startState: function() { + return {tokenize: inText, + state: baseState, + indented: 0, + tagName: null, tagStart: null, + context: null}; + }, + + token: function(stream, state) { + if (!state.tagName && stream.sol()) + state.indented = stream.indentation(); + + if (stream.eatSpace()) return null; + type = null; + var style = state.tokenize(stream, state); + if ((style || type) && style != "comment") { + setStyle = null; + state.state = state.state(type || style, stream, state); + if (setStyle) + style = setStyle == "error" ? style + " error" : setStyle; + } + return style; + }, + + indent: function(state, textAfter, fullLine) { + var context = state.context; + // Indent multi-line strings (e.g. css). + if (state.tokenize.isInAttribute) { + if (state.tagStart == state.indented) + return state.stringStartCol + 1; + else + return state.indented + indentUnit; + } + if (context && context.noIndent) return CodeMirror.Pass; + if (state.tokenize != inTag && state.tokenize != inText) + return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; + // Indent the starts of attribute names. + if (state.tagName) { + if (multilineTagIndentPastTag) + return state.tagStart + state.tagName.length + 2; + else + return state.tagStart + indentUnit * multilineTagIndentFactor; + } + if (alignCDATA && /$/, + blockCommentStart: "", + + configuration: parserConfig.htmlMode ? "html" : "xml", + helperType: parserConfig.htmlMode ? "html" : "xml" + }; +}); + +CodeMirror.defineMIME("text/xml", "xml"); +CodeMirror.defineMIME("application/xml", "xml"); +if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) + CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); + +}); diff --git a/plugins/dynamix.vm.manager/scripts/dynamix.vm.manager.js b/plugins/dynamix.vm.manager/scripts/dynamix.vm.manager.js new file mode 100644 index 000000000..f04a7af38 --- /dev/null +++ b/plugins/dynamix.vm.manager/scripts/dynamix.vm.manager.js @@ -0,0 +1,223 @@ +function clearHistory(){ + window.history.pushState('VMs', 'Title', '/VMs'); +} + +function ordinal_suffix_of(i) { + var j = i % 10, + k = i % 100; + if (j == 1 && k != 11) { + return i + "st"; + } + if (j == 2 && k != 12) { + return i + "nd"; + } + if (j == 3 && k != 13) { + return i + "rd"; + } + return i + "th"; +} + +function slideUpRows($tr, onComplete) { + $tr.not('tr,table').finish().fadeOut('fast'); + + $tr.filter('tr').find('td').finish().each(function(){ + $(this) + .data("paddingstate", $(this).css(["paddingTop", "paddingBottom"])) + .animate({ paddingTop: 0, paddingBottom: 0 }, { duration: 'fast' }) + .wrapInner('
') + .children() + .slideUp("fast", function() { + $(this).contents().unwrap(); + $tr.filter('tr').hide(); + if ($.isFunction(onComplete)) { + onComplete(); + } + }); + }); + + $tr.filter('table').finish().each(function(){ + $(this) + .wrap('
') + .parent() + .slideUp("fast", function() { + $(this).contents().unwrap(); + $tr.filter('table').hide(); + if ($.isFunction(onComplete)) { + onComplete(); + } + }); + }); + + return $tr; +} + +function slideDownRows($tr, onComplete) { + $tr.filter(':hidden').not('tr,table').finish().fadeIn('fast'); + + $tr.filter('tr:hidden').find('td').finish().each(function(){ + $(this) + .wrapInner('
') + .animate($(this).data("paddingstate"), { duration: 'fast', start: function() { $tr.filter('tr:hidden').show(); } }) + .children() + .slideDown("fast", function() { + $(this).contents().unwrap(); + if ($.isFunction(onComplete)) { + onComplete(); + } + }); + }); + + $tr.filter('table:hidden').finish().each(function(){ + $(this) + .wrap('
') + .show() + .parent() + .slideDown("fast", function() { + $(this).contents().unwrap(); + if ($.isFunction(onComplete)) { + onComplete(); + } + }); + }); + + return $tr; +} + +function toggleRows(what, val, what2, onComplete) { + if (val == 1) { + slideDownRows($('.'+what), onComplete); + if (arguments.length > 2) { + slideUpRows($('.'+what2), onComplete); + } + } else { + slideUpRows($('.'+what), onComplete); + if (arguments.length > 2) { + slideDownRows($('.'+what2), onComplete); + } + } +} + +function updatePrefixLabels(category) { + $("#form_content table:data(category)").filter(function() { + return $(this).data('category') == category; + }).each(function (index) { + var oldprefix = $(this).data('prefix'); + var newprefix = oldprefix; + + if (index > 0) { + newprefix = ordinal_suffix_of(index+1); + } + + $(this) + .data('prefix', newprefix) + .find('tr').each(function() { + var $td = $(this).children('td').first(); + + var old = $td.text(); + if (oldprefix && old.indexOf(oldprefix) === 0) { + old = old.replace(oldprefix + ' ', ''); + } + + $td.text(newprefix + ' ' + old); + }); + }); +} + +function bindSectionEvents(category) { + var $Filtered = $("#form_content table:data(category)").filter(function(index) { + return $(this).data('category') == category; + }); + + var count = $Filtered.length; + + $Filtered.each(function(index) { + var $table = $(this); + var config = $(this).data(); + var boolAdd = false; + var boolDelete = false; + + if (!config.hasOwnProperty('multiple')) { + return; + } + + // Clean old sections + var $first_td = $(this).find('td').first(); + + // delete section + if (!config.hasOwnProperty('minimum') || parseInt(config.minimum) < (index+1)) { + if ($first_td.children('.sectionbutton.remove').length === 0) { + var $el_remove = $('
').one('click', clickRemoveSection); + $first_td.append($el_remove); + } + boolDelete = true; + } else { + $first_td.children('.sectionbutton.remove').fadeOut('fast', function() { $(this).remove(); }); + } + + // add section (can only add from the last section) + if ((index+1) == count) { + if (!config.hasOwnProperty('maximum') || parseInt(config.maximum) > (index+1)) { + if ($first_td.children('.sectionbutton.add').length === 0) { + var $el_add = $('
').one('click', clickAddSection); + $first_td.append($el_add); + } + boolAdd = true; + } else { + $first_td.children('.sectionbutton.add').fadeOut('fast', function() { $(this).remove(); }); + } + } + + if (boolDelete || boolAdd) { + $table.addClass("multiple"); + if ($first_td.children('.sectiontab').length === 0) { + var $el_tab = $('
'); + $first_td.append($el_tab); + } + } else { + $first_td.children('.sectionbutton, .sectiontab').fadeOut('fast', function() { + $(this).remove(); + $table.removeClass("multiple"); + }); + } + }); +} + +function clickAddSection() { + var $table = $(this).closest('table'); + $(this).remove(); + var newindex = new Date().getTime(); + var config = $table.data(); + + var $template = $($('
').loadTemplate($("#tmpl" + config.category)).html().replace(/{{INDEX}}/g, newindex)); + + $template + .data({ + multiple: true, + category: config.category, + index: newindex, + minimum: config.minimum, + maximum: config.maximum + }) + .find('tr').hide() + .find("input[data-pickroot]").fileTreeAttach(); + + $table.after($template); + + updatePrefixLabels(config.category); + bindSectionEvents(config.category); + + $el_showable = $template.find('tr').not("." + (isVMAdvancedMode() ? 'basic' : 'advanced')); + + slideDownRows($el_showable); +} + +function clickRemoveSection() { + var $table = $(this).closest('table'); + var category = $table.data('category'); + + slideUpRows($table.find('tr'), function() { + $table.remove(); + updatePrefixLabels(category); + bindSectionEvents(category); + }); +} diff --git a/plugins/dynamix.vm.manager/styles/dynamix.vm.manager.css b/plugins/dynamix.vm.manager/styles/dynamix.vm.manager.css new file mode 100644 index 000000000..89c0f0304 --- /dev/null +++ b/plugins/dynamix.vm.manager/styles/dynamix.vm.manager.css @@ -0,0 +1,681 @@ +/*! + * Bootstrap v3.2.0 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} +textarea { + overflow: auto; + width: 475px; +} +* { + -webkit-box-sizing: inherit; + -moz-box-sizing: inherit; + box-sizing: inherit; +} +*:before, +*:after { + -webkit-box-sizing: inherit; + -moz-box-sizing: inherit; + box-sizing: inherit; +} +.btn { + display: inline-block; + margin-bottom: 0; + font-weight: normal; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + border-radius: 4px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.btn:focus, +.btn:active:focus, +.btn.active:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus { + color: #333333; + text-decoration: none; +} +.btn:active, +.btn.active { + outline: 0; + background-image: none; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + cursor: not-allowed; + pointer-events: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-default { + color: #333333; + background-color: #E1E1E1; + border-color: #cccccc; +} +.btn-default:hover, +.btn-default:focus, +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #333333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #ffffff; + border-color: #cccccc; +} +.btn-default .badge { + color: #ffffff; + background-color: #333333; +} +.btn-dynamix { + color: inherit; + background-color: inherit; + border-color: inherit; +} +.btn-dynamix:hover, +.btn-dynamix:focus, +.btn-dynamix:active, +.btn-dynamix.active, +.open > .dropdown-toggle.btn-dynamix { + color: inherit; + background-color: inherit; + border-color: inherit; +} +.btn-dynamix:active, +.btn-dynamix.active, +.open > .dropdown-toggle.btn-dynamix { + background-image: none; +} +.btn-dynamix.disabled, +.btn-dynamix[disabled], +fieldset[disabled] .btn-dynamix, +.btn-dynamix.disabled:hover, +.btn-dynamix[disabled]:hover, +fieldset[disabled] .btn-dynamix:hover, +.btn-dynamix.disabled:focus, +.btn-dynamix[disabled]:focus, +fieldset[disabled] .btn-dynamix:focus, +.btn-dynamix.disabled:active, +.btn-dynamix[disabled]:active, +fieldset[disabled] .btn-dynamix:active, +.btn-dynamix.disabled.active, +.btn-dynamix[disabled].active, +fieldset[disabled] .btn-dynamix.active { + background-color: inherit; + border-color: inherit; +} +.btn-dynamix .badge { + color: inherit; + background-color: inherit; +} +.btn-primary { + color: #ffffff; + background-color: #428bca; + border-color: #357ebd; +} +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #ffffff; + background-color: #3071a9; + border-color: #285e8e; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #428bca; + border-color: #357ebd; +} +.btn-primary .badge { + color: #428bca; + background-color: #ffffff; +} +.btn-success { + color: #ffffff; + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success:hover, +.btn-success:focus, +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #ffffff; + background-color: #449d44; + border-color: #398439; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success .badge { + color: #5cb85c; + background-color: #ffffff; +} +.btn-info { + color: #ffffff; + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info:hover, +.btn-info:focus, +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #ffffff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info .badge { + color: #5bc0de; + background-color: #ffffff; +} +.btn-warning { + color: #ffffff; + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning:hover, +.btn-warning:focus, +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #ffffff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #ffffff; +} +.btn-danger { + color: #ffffff; + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger:hover, +.btn-danger:focus, +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #ffffff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger .badge { + color: #d9534f; + background-color: #ffffff; +} +.btn-link { + color: #428bca; + font-weight: normal; + cursor: pointer; + border-radius: 0; +} +.btn-link, +.btn-link:active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} +.btn-link:hover, +.btn-link:focus { + color: #2a6496; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777777; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; + min-width: 13px; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 5px; +} +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus { + outline: 0; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child > .btn:last-child, +.btn-group > .btn-group:first-child > .dropdown-toggle { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.btn-group > .btn-group:last-child > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-bottom-left-radius: 4px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + float: none; + display: table-cell; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +.clearfix:before, +.clearfix:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after { + content: " "; + display: table; +} +.clearfix:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after { + clear: both; +} +table.tablesorter.kvm{margin-top:-24px;border-spacing:0 3px;} +table.tablesorter.kvm tr:nth-child(odd){background:none;} +table.tablesorter.kvm tr:nth-child(even){background:none;} +table.tablesorter.kvm th:first-child{width:3%;text-align:center;} +table.tablesorter.kvm td:first-child{width:3%;text-align:center;} +table.tablesorter.kvm th:nth-child(2){text-align:left;} +table.tablesorter.kvm td:nth-child(2){text-align:left;} +table.tablesorter.kvm th:nth-child(3){width:5%;text-align:center;} +table.tablesorter.kvm td:nth-child(3){width:5%;text-align:center;} +table.tablesorter.kvm th:nth-child(4){width:9%;text-align:center;} +table.tablesorter.kvm td:nth-child(4){width:9%;text-align:center;} +table.tablesorter.kvm th:nth-child(5){width:9%;text-align:center;} +table.tablesorter.kvm td:nth-child(5){width:9%;text-align:center;} +table.tablesorter.kvm th:nth-child(6){width:9%;text-align:center;} +table.tablesorter.kvm td:nth-child(6){width:9%;text-align:center;} +table.tablesorter.kvm th:nth-child(7){width:80px;text-align:center;} +table.tablesorter.kvm td:nth-child(7){width:80px;text-align:left;} +table.tablesorter.kvm th:nth-child(8){width:24px;text-align:center;} +table.tablesorter.kvm td:nth-child(8){width:24px;text-align:center;} +table.tablesorter.kvm tr>td+td+td{width:9%;white-space:nowrap;} +table.tablesorter.kvm-span{margin-top:-20px;} +table.tablesorter.kvm-span tr:nth-child(odd){background:none;} +table.tablesorter.kvm-span tr:nth-child(even){background:none;} +table.tablesorter.dominfo{margin-top: 5px;} +table.tablesorter.dominfo{margin-left:48px;} +table.tablesorter.domdisk{margin-top: 5px;} +table.tablesorter.domdisk{margin-left:72px;} +table.tablesorter.domdisk th:first-child{width:9%;text-align:left;} +table.tablesorter.domdisk td:first-child{width:9%;text-align:left;} +table.tablesorter.domdisk th:nth-child(2){width:9%;text-align:left;} +table.tablesorter.domdisk td:nth-child(2){width:9%;text-align:left;} +table.tablesorter.domdisk th:nth-child(3){width:9%;text-align:left;} +table.tablesorter.domdisk td:nth-child(3){width:9%;text-align:left;} +table.tablesorter.domdisk th:nth-child(4){width:9%;text-align:left;} +table.tablesorter.domdisk td:nth-child(4){width:9%;text-align:left;} +table.tablesorter.domdisk th:nth-child(5){width:9%;text-align:left;} +table.tablesorter.domdisk td:nth-child(5){width:9%;text-align:left;} +table.tablesorter.domdisk th:nth-child(6){width:9%;text-align:left;} +table.tablesorter.domdisk td:nth-child(6){width:9%;text-align:left;} +table.tablesorter.domdisk tr>td+td+td{width:9%;white-space:nowrap;} +table.tablesorter.domdisk td:last-child{width:96px;padding:0;} +table.tablesorter.domsnap{margin-top: 5px;} +table.tablesorter.domsnap{margin-left:72px;} +table.tablesorter.domsnap th:first-child{width:9%;text-align:left;} +table.tablesorter.domsnap td:first-child{width:9%;text-align:left;} +table.tablesorter.domsnap th:nth-child(2){width:9%;text-align:left;} +table.tablesorter.domsnap td:nth-child(2){width:9%;text-align:left;} +table.tablesorter.domsnap th:nth-child(3){width:9%;text-align:left;} +table.tablesorter.domsnap td:nth-child(3){width:9%;text-align:left;} +table.tablesorter.domsnap th:nth-child(4){width:9%;text-align:left;} +table.tablesorter.domsnap td:nth-child(4){width:9%;text-align:left;} +table.tablesorter.domsnap th:nth-child(5){width:9%;text-align:left;} +table.tablesorter.domsnap td:nth-child(5){width:9%;text-align:left;} +table.tablesorter.domsnap tr>td+td+td{width:9%;white-space:nowrap;} +table.tablesorter.domsnap td:last-child{width:96px;padding:0;} + +.iconstatus { + position: absolute; + z-index: 2; + bottom: -4px; + right: -4px; + font-size: 1.2em; + text-shadow: 0 0 2px #FFF; +} +.iconstatus.started { + font-size: 1.3em; +} +img.started{opacity: 1.0;} +img.stopped{opacity: 0.3;} +img.paused{opacity: 1.0;} +.started{color:#009900;} +.stopped{color:#EF3D47;} +.paused{color:#F0DD33;} + +.log{cursor:zoom-in;} diff --git a/plugins/dynamix.vm.manager/templates/Custom.form.php b/plugins/dynamix.vm.manager/templates/Custom.form.php new file mode 100644 index 000000000..f95512b9c --- /dev/null +++ b/plugins/dynamix.vm.manager/templates/Custom.form.php @@ -0,0 +1,990 @@ + + 'Windows 8.1 / 2012', + 'windows7' => 'Windows 7 / 2008', + 'windowsxp' => 'Windows XP / 2003', + 'linux' => 'Linux', + 'arch' => 'Arch', + 'centos' => 'CentOS', + 'chromeos' => 'ChromeOS', + 'coreos' => 'CoreOS', + 'debian' => 'Debian', + 'fedora' => 'Fedora', + 'freebsd' => 'FreeBSD', + 'openelec' => 'OpenELEC', + 'opensuse' => 'OpenSUSE', + 'redhat' => 'RedHat', + 'scientific' => 'Scientific', + 'slackware' => 'Slackware', + 'steamos' => 'SteamOS', + 'ubuntu' => 'Ubuntu' + ]; + + $arrConfigDefaults = [ + 'template' => [ + 'name' => 'Custom', + 'icon' => 'windows.png' + ], + 'domain' => [ + 'persistent' => 1, + 'uuid' => $lv->domain_generate_uuid(), + 'clock' => 'localtime', + 'arch' => 'x86_64', + 'machine' => 'pc', + 'mem' => 512 * 1024, + 'maxmem' => 512 * 1024, + 'password' => '', + 'cpumode' => 'host-passthrough', + 'vcpus' => 1, + 'vcpu' => [0], + 'hyperv' => 1, + 'ovmf' => 0 + ], + 'media' => [ + 'cdrom' => '', + 'drivers' => $domain_cfg['VIRTIOISO'] + ], + 'disk' => [ + [ + 'new' => '', + 'size' => '', + 'driver' => 'raw', + 'dev' => 'hda' + ] + ], + 'gpu' => [ + [ + 'id' => 'vnc', + 'keymap' => 'en-us' + ] + ], + 'audio' => [ + [ + 'id' => '' + ] + ], + 'pci' => [], + 'nic' => [ + [ + 'network' => $domain_bridge, + 'mac' => $lv->generate_random_mac_addr() + ] + ], + 'usb' => [], + 'shares' => [ + [ + 'source' => '', + 'target' => '' + ] + ] + ]; + + // If we are editing a existing VM load it's existing configuration details + $arrExistingConfig = (!empty($_GET['uuid']) ? domain_to_config($_GET['uuid']) : []); + + // Active config for this page + $arrConfig = array_replace_recursive($arrConfigDefaults, $arrExistingConfig); + + // Add any custom metadata field defaults (e.g. os) + if (empty($arrConfig['template']['os'])) { + $arrConfig['template']['os'] = ($arrConfig['domain']['clock'] == 'localtime' ? 'windows' : 'linux'); + } + + $boolRunning = (!empty($arrConfig['domain']['state']) && $arrConfig['domain']['state'] == 'running'); + + + if (array_key_exists('createvm', $_POST)) { + //DEBUG + file_put_contents('/tmp/debug_libvirt_postparams.txt', print_r($_POST, true)); + file_put_contents('/tmp/debug_libvirt_newxml.xml', $lv->config_to_xml($_POST)); + + $tmp = $lv->domain_new($_POST); + if (!$tmp){ + $arrResponse = ['error' => $lv->get_last_error()]; + } else { + $arrResponse = ['success' => true]; + + // Fire off the vnc popup if available + $res = $lv->get_domain_by_name($_POST['domain']['name']); + $vncport = $lv->domain_get_vnc_port($res); + $wsport = $lv->domain_get_ws_port($res); + + if ($vncport > 0) { + $vnc = '/plugins/dynamix.vm.manager/vnc.html?autoconnect=true&host='.$var['IPADDR'].'&port='.$wsport; + $arrResponse['vncurl'] = $vnc; + } + } + + echo json_encode($arrResponse); + exit; + } + + if (array_key_exists('updatevm', $_POST)) { + //DEBUG + file_put_contents('/tmp/debug_libvirt_postparams.txt', print_r($_POST, true)); + file_put_contents('/tmp/debug_libvirt_updatexml.xml', $lv->config_to_xml($_POST)); + + // Backup xml for existing domain in ram + $strOldXML = ''; + $boolOldAutoStart = false; + $res = $lv->domain_get_name_by_uuid($_POST['domain']['uuid']); + if ($res) { + $strOldXML = $lv->domain_get_xml($res); + $boolOldAutoStart = $lv->domain_get_autostart($res); + + //DEBUG + file_put_contents('/tmp/debug_libvirt_oldxml.xml', $strOldXML); + } + + // Remove existing domain + $lv->domain_undefine($res); + + // Save new domain + $tmp = $lv->domain_new($_POST); + if (!$tmp){ + $strLastError = $lv->get_last_error(); + + // Failure -- try to restore existing domain + $tmp = $lv->domain_define($strOldXML); + if ($tmp) $lv->domain_set_autostart($tmp, $boolOldAutoStart); + + $arrResponse = ['error' => $strLastError]; + } else { + $lv->domain_set_autostart($tmp, $_POST['domain']['autostart'] == 1); + + $arrResponse = ['success' => true]; + } + + echo json_encode($arrResponse); + exit; + } +?> + + + + + + + + + + + + + +
Operating System: + +
+
+

Select Windows for any Microsoft operating systems

+
+ + + + + + +
CPU Mode: + +
+
+
+

There are two CPU modes available to choose:

+

+ Host Passthrough
+ With this mode, the CPU visible to the guest should be exactly the same as the host CPU even in the aspects that libvirt does not understand. For the best possible performance, use this setting. +

+

+ Emulated
+ If you are having difficulties with Host Passthrough mode, you can try the emulated mode which doesn't expose the guest to host-based CPU features. This may impact the performance of your VM. +

+
+
+ + + + + + +
CPUs: +
+ + + +
+
+
+
+

By default, VMs created will be pinned to physical CPU cores to improve performance. From this view, you can adjust which actual CPU cores a VM will be pinned (minimum 1).

+
+
+ + + + + + + + + + +
Initial Memory: + + Max Memory: + +
+
+
+

Select how much memory to allocate to the VM at boot.

+
+
+
+
+

Select how much memory to allocate to the VM at boot (cannot be more than Max. Mem).

+
+
+ + + + + + +
Machine: + +
+
+
+

The machine type option primarily affects the success some users may have with various hardware and GPU pass through. For more information on the various QEMU machine types, see these links:

+ http://wiki.qemu.org/Documentation/Platforms/PC
+ http://wiki.qemu.org/Features/Q35
+

As a rule of thumb, try to get your configuration working with i440fx first and if that fails, try adjusting to Q35 to see if that changes anything.

+
+
+ + + + + + +
BIOS: + + + + +
+
+
+

+ SeaBIOS
+ is the default virtual BIOS used to create virtual machines and is compatible with all guest operating systems (Windows, Linux, etc.). +

+

+ OVMF
+ (Open Virtual Machine Firmware) adds support for booting VMs using UEFI, but virtual machine guests must also support UEFI. Assigning graphics devices to a OVMF-based virtual machine requires that the graphics device also support UEFI. +

+

+ Once a VM is created this setting cannot be adjusted. +

+
+
+ + + + + + +
Hyper-V: + +
+
+
+
+

Exposes the guest to hyper-v extensions for Microsoft operating systems. Set to "Yes" by default, but set to "No" automatically if an NVIDIA-based GPU is assigned to the guest (but can be user-toggled back to "Yes").

+
+
+
+ + + + + + +
OS Install ISO: + +
+
+

Select the virtual CD-ROM (ISO) that contains the installation media for your operating system. Clicking this field displays a list of ISOs found in the directory specified on the Settings page.

+
+ + + + + + +
VirtIO Drivers ISO: + +
+
+
+
+

Specify the virtual CD-ROM (ISO) that contains the VirtIO Windows drivers as provided by the Fedora Project. Download the latest ISO from here: https://fedoraproject.org/wiki/Windows_Virtio_Drivers#Direct_download

+

When installing Windows, you will reach a step where no disk devices will be found. There is an option to browse for drivers on that screen. Click browse and locate the additional CD-ROM in the menu. Inside there will be various folders for the different versions of Windows. Open the folder for the version of Windows you are installing and then select the AMD64 subfolder inside (even if you are on an Intel system, select AMD64). Three drivers will be found. Select them all, click next, and the vDisks you have assigned will appear.

+
+
+
+ + + $arrDisk) { + $strLabel = ($i > 0) ? appendOrdinalSuffix($i + 1) : 'Primary'; + + ?> + + + + + + + + + + + + + + + + + +
vDisk Location: + +
vDisk Size: + +
vDisk Type: + +
+ +
+

+ vDisk Location
+ Specify a path to a user share in which you wish to store the VM or specify an existing vDisk. The primary vDisk will store the operating system for your VM. +

+ +

+ vDisk Size
+ Specify a number followed by a letter. M for megabytes, G for gigabytes. +

+ +

+ vDisk Type
+ Select RAW for best performance. QCOW2 implementation is still in development. +

+ +

Additional devices can be added/removed by clicking the symbols to the left.

+
+ + + + + + $arrShare) { + $strLabel = ($i > 0) ? appendOrdinalSuffix($i + 1) : ''; + + ?> + + + + + + + + + + +
unRAID Share: + +
unRAID Mount tag: + +
+ +
+
+
+

+ unRAID Share
+ Used to create a VirtFS mapping to a Linux-based guest. Specify the path on the host here. +

+ +

+ unRAID Mount tag
+ Specify the mount tag that you will use for mounting the VirtFS share inside the VM. See this page for how to do this on a Linux-based guest: http://wiki.qemu.org/Documentation/9psetup +

+ +

Additional devices can be added/removed by clicking the symbols to the left.

+
+
+
+ + + + + + $arrGPU) { + $strLabel = ($i > 0) ? appendOrdinalSuffix($i + 1) : ''; + + ?> + + + + + + + + + + + + + + + + +
Graphics Card: + +
VNC Password:
VNC Keyboard: + +
+ +
+

+ Graphics Card
+ If you wish to assign a graphics card to the VM, select it from this list, otherwise leave it set to VNC. +

+ +

+ VNC Password
+ If you wish to require a password to connect to the VM over a VNC connection, specify one here. +

+ +

+ VNC Keyboard
+ If you wish to assign a different keyboard layout to use for a VNC connection, specify one here. +

+ +

Additional devices can be added/removed by clicking the symbols to the left.

+
+ + + + + + $arrAudio) { + $strLabel = ($i > 0) ? appendOrdinalSuffix($i + 1) : ''; + + ?> + + + + + +
Sound Card: + +
+ +
+

Select a sound device to assign to your VM. Most modern GPUs have a built-in audio device, but you can also select the on-board audio device(s) if present.

+

Additional devices can be added/removed by clicking the symbols to the left.

+
+ + + + + + $arrNic) { + $strLabel = ($i > 0) ? appendOrdinalSuffix($i + 1) : ''; + + ?> + + + + + + + + + + +
Network MAC: + +
Network Bridge: + +
+ +
+
+

+ Network MAC
+ By default, a random MAC address will be assigned here that conforms to the standards for virtual network interface controllers. You can manually adjust this if desired. +

+ +

+ Network Bridge
+ The default libvirt managed network bridge (virbr0) will be used, otherwise you may specify an alternative name for a private network bridge to the host. +

+ +

Additional devices can be added/removed by clicking the symbols to the left.

+
+
+ + + + + + + + + + +
USB Devices: +
+ $arrDev) { + ?> +
+ None available"; + } + ?> +
+
+
+

If you wish to assign any USB devices to your guest, you can select them from this list.
+ NOTE: USB hotplug support is not yet implemented, so devices must be attached before the VM is started to use them.

+
+ + + + + + +
+ + + + + + +
+ + + + + + + +
+
+

Click Create to generate the vDisks and return to the Virtual Machines page where your new VM will be created.

+
+ + + diff --git a/plugins/dynamix.vm.manager/templates/OpenELEC.form.php b/plugins/dynamix.vm.manager/templates/OpenELEC.form.php new file mode 100644 index 000000000..eb49b929e --- /dev/null +++ b/plugins/dynamix.vm.manager/templates/OpenELEC.form.php @@ -0,0 +1,1055 @@ + + [ + 'name' => '6.0.0 Beta3', + 'url' => 'https://s3.amazonaws.com/dnld.lime-technology.com/images/OpenELEC/OpenELEC-unRAID.x86_64-5.95.3_1.tar.xz', + 'size' => 153990180, + 'md5' => '8936cda74c28ddcaa165cc49ff2a477a', + 'localpath' => '', + 'valid' => '0' + ], + '5.95.2_1' => [ + 'name' => '6.0.0 Beta2', + 'url' => 'https://s3.amazonaws.com/dnld.lime-technology.com/images/OpenELEC/OpenELEC-unRAID.x86_64-5.95.2_1.tar.xz', + 'size' => 156250392, + 'md5' => 'ac70048eecbda4772e386c6f271cb5e9', + 'localpath' => '', + 'valid' => '0' + ] + ]; + + // Read localpaths in from openelec.cfg + $strOpenELECConfig = "/boot/config/plugins/dynamix.vm.manager/openelec.cfg"; + $arrOpenELECConfig = []; + + if (file_exists($strOpenELECConfig)) { + $arrOpenELECConfig = parse_ini_file($strOpenELECConfig); + } elseif (!file_exists(dirname($strOpenELECConfig))) { + @mkdir(dirname($strOpenELECConfig), 0777, true); + } + + + // Compare openelec.cfg and populate 'localpath' in $arrOEVersion + foreach ($arrOpenELECConfig as $strID => $strLocalpath) { + if (array_key_exists($strID, $arrOpenELECVersions)) { + $arrOpenELECVersions[$strID]['localpath'] = $strLocalpath; + if (file_exists($strLocalpath)) { + $arrOpenELECVersions[$strID]['valid'] = '1'; + } + } + } + + if (array_key_exists('delete_version', $_POST)) { + + //DEBUG + file_put_contents('/tmp/debug_libvirt_postparams.txt', print_r($_POST, true)); + + $arrDeleteOpenELEC = []; + if (array_key_exists($_POST['delete_version'], $arrOpenELECVersions)) { + $arrDeleteOpenELEC = $arrOpenELECVersions[$_POST['delete_version']]; + } + + $arrResponse = []; + + if (empty($arrDeleteOpenELEC)) { + $arrResponse = ['error' => 'Unknown version: ' . $_POST['delete_version']]; + } else { + // delete img file + @unlink($arrDeleteOpenELEC['localpath']); + + // Save to strOpenELECConfig + unset($arrOpenELECConfig[$_POST['delete_version']]); + $text = ''; + foreach ($arrOpenELECConfig as $key => $value) $text .= "$key=\"$value\"\n"; + file_put_contents($strOpenELECConfig, $text); + + $arrResponse = ['status' => 'ok']; + } + + echo json_encode($arrResponse); + exit; + } + + if (array_key_exists('download_path', $_POST)) { + + //DEBUG + file_put_contents('/tmp/debug_libvirt_postparams.txt', print_r($_POST, true)); + + $arrDownloadOpenELEC = []; + if (array_key_exists($_POST['download_version'], $arrOpenELECVersions)) { + $arrDownloadOpenELEC = $arrOpenELECVersions[$_POST['download_version']]; + } + + if (empty($arrDownloadOpenELEC)) { + $arrResponse = ['error' => 'Unknown version: ' . $_POST['download_version']]; + } else { + @mkdir($_POST['download_path'], 0777, true); + $_POST['download_path'] = realpath($_POST['download_path']) . '/'; + + + $strInstallScript = '/tmp/OpenELEC_' . $_POST['download_version'] . '_install.sh'; + $strLogFile = $_POST['download_path'] . basename($arrDownloadOpenELEC['url']) . '.log'; + $strTempFile = $_POST['download_path'] . basename($arrDownloadOpenELEC['url']); + $strMD5File = $strTempFile . '.md5'; + $strMD5StatusFile = $strTempFile . '.md5status'; + $strExtractedFile = $_POST['download_path'] . basename($arrDownloadOpenELEC['url'], 'tar.xz') . 'img'; + + + // Save to strOpenELECConfig + $arrOpenELECConfig[$_POST['download_version']] = $strExtractedFile; + $text = ''; + foreach ($arrOpenELECConfig as $key => $value) $text .= "$key=\"$value\"\n"; + file_put_contents($strOpenELECConfig, $text); + + + $strDownloadCmd = 'wget -nv -O ' . escapeshellarg($strTempFile) . ' ' . escapeshellarg($arrDownloadOpenELEC['url']); + $strDownloadPgrep = '-f "wget.*' . $strTempFile . '.*' . $arrDownloadOpenELEC['url'] . '"'; + + $strVerifyCmd = 'md5sum -c ' . escapeshellarg($strMD5File); + $strVerifyPgrep = '-f "md5sum.*' . $strMD5File . '"'; + + $strExtractCmd = 'tar Jxf ' . escapeshellarg($strTempFile) . ' -C ' . escapeshellarg(dirname($strTempFile)); + $strExtractPgrep = '-f "tar.*' . $strTempFile . '.*' . dirname($strTempFile) . '"'; + + $strCleanCmd = '(chmod 777 ' . escapeshellarg($_POST['download_path']) . ' ' . escapeshellarg($strExtractedFile) . '; chown nobody:users ' . escapeshellarg($_POST['download_path']) . ' ' . escapeshellarg($strExtractedFile) . '; rm ' . escapeshellarg($strTempFile) . ' ' . escapeshellarg($strTempFile.'.md5') . ' ' . escapeshellarg($strTempFile.'.md5status') . ' ' . escapeshellarg($strTempFile.'.log') . ')'; + $strCleanPgrep = '-f "chmod.*chown.*rm.*' . $strTempFile . '"'; + + $strAllCmd = "#!/bin/bash\n\n"; + $strAllCmd .= $strDownloadCmd . ' >>' . escapeshellarg($strLogFile) . ' 2>&1 && '; + $strAllCmd .= 'echo "' . $arrDownloadOpenELEC['md5'] . ' ' . $strTempFile . '" > ' . escapeshellarg($strMD5File) . ' && '; + $strAllCmd .= $strVerifyCmd . ' >' . escapeshellarg($strMD5StatusFile) . ' 2>/dev/null && '; + $strAllCmd .= $strExtractCmd . ' >>' . escapeshellarg($strLogFile) . ' 2>&1 && '; + $strAllCmd .= $strCleanCmd . ' >>' . escapeshellarg($strLogFile) . ' 2>&1 && '; + $strAllCmd .= 'rm ' . escapeshellarg($strInstallScript); + + $arrResponse = []; + + if (file_exists($strExtractedFile)) { + + if (!file_exists($strTempFile)) { + + // Status = done + $arrResponse['status'] = 'Done'; + $arrResponse['localpath'] = $strExtractedFile; + $arrResponse['localfolder'] = dirname($strExtractedFile); + + } else { + if (pgrep($strExtractPgrep)) { + + // Status = running extract + $arrResponse['status'] = 'Extracting ... '; + + } else { + + // Status = cleanup + $arrResponse['status'] = 'Cleanup ... '; + + } + } + + } else if (file_exists($strTempFile)) { + + if (pgrep($strDownloadPgrep)) { + + // Get Download percent completed + $intSize = filesize($strTempFile); + $strPercent = 0; + if ($intSize > 0) { + $strPercent = round(($intSize / $arrDownloadOpenELEC['size']) * 100); + } + + $arrResponse['status'] = 'Downloading ... ' . $strPercent . '%'; + + } else if (pgrep($strVerifyPgrep)) { + + // Status = running md5 check + $arrResponse['status'] = 'Verifying ... '; + + } else if (file_exists($strMD5StatusFile)) { + + // Status = running extract + $arrResponse['status'] = 'Extracting ... '; + + if (!pgrep($strExtractPgrep)) { + // Examine md5 status + $strMD5StatusContents = file_get_contents($strMD5StatusFile); + + if (strpos($strMD5StatusContents, ': FAILED') !== false) { + + // ERROR: MD5 check failed + unset($arrResponse['status']); + $arrResponse['error'] = 'MD5 verification failed, your download is incomplete or corrupted.'; + + } + } + + } else if (!file_exists($strMD5File)) { + + // Status = running md5 check + $arrResponse['status'] = 'Downloading ... 100%'; + + } + + } else { + + if (!pgrep($strDownloadPgrep)) { + + // Run all commands + file_put_contents($strInstallScript, $strAllCmd); + chmod($strInstallScript, 0777); + exec($strInstallScript . ' >/dev/null 2>&1 &'); + + } + + $arrResponse['status'] = 'Downloading ... '; + + } + + } + + echo json_encode($arrResponse); + exit; + } + + $arrOpenELECVersion = reset($arrOpenELECVersions); + $strOpenELECVersionID = key($arrOpenELECVersions); + + $arrConfigDefaults = [ + 'template' => [ + 'os' => 'openelec', + 'name' => 'OpenELEC', + 'icon' => 'openelec.png', + 'openelec' => $strOpenELECVersionID + ], + 'domain' => [ + 'persistent' => 1, + 'uuid' => $lv->domain_generate_uuid(), + 'clock' => 'utc', + 'arch' => 'x86_64', + 'machine' => getLatestMachineType('q35'), + 'mem' => 512 * 1024, + 'maxmem' => 512 * 1024, + 'password' => '', + 'cpumode' => 'host-passthrough', + 'vcpus' => 1, + 'vcpu' => [0], + 'hyperv' => 0, + 'ovmf' => 0 + ], + 'media' => [ + 'cdrom' => '', + 'drivers' => '' + ], + 'disk' => [ + [ + 'image' => $arrOpenELECVersion['localpath'], + 'size' => '', + 'driver' => 'raw', + 'dev' => 'hda', + 'readonly' => 1 + ] + ], + 'gpu' => [ + [ + 'id' => '', + 'keymap' => 'en-us' + ] + ], + 'audio' => [ + [ + 'id' => '' + ] + ], + 'pci' => [], + 'nic' => [ + [ + 'network' => $domain_bridge, + 'mac' => $lv->generate_random_mac_addr() + ] + ], + 'usb' => [], + 'shares' => [ + [ + 'source' => (is_dir('/mnt/user/appdata') ? '/mnt/user/appdata/OpenELEC/' : ''), + 'target' => 'appconfig' + ] + ] + ]; + + // If we are editing a existing VM load it's existing configuration details + $arrExistingConfig = (!empty($_GET['uuid']) ? domain_to_config($_GET['uuid']) : []); + + // Active config for this page + $arrConfig = array_replace_recursive($arrConfigDefaults, $arrExistingConfig); + + if (array_key_exists($arrConfig['template']['openelec'], $arrOpenELECVersions)) { + $arrConfigDefaults['disk'][0]['image'] = $arrOpenELECVersions[$arrConfig['template']['openelec']]['localpath']; + } + + $boolRunning = (!empty($arrConfig['domain']['state']) && $arrConfig['domain']['state'] == 'running'); + + + if (array_key_exists('createvm', $_POST)) { + //DEBUG + file_put_contents('/tmp/debug_libvirt_postparams.txt', print_r($_POST, true)); + file_put_contents('/tmp/debug_libvirt_newxml.xml', $lv->config_to_xml($_POST)); + + if (!empty($_POST['shares'][0]['source'])) { + @mkdir($_POST['shares'][0]['source'], 0777, true); + } + + $tmp = $lv->domain_new($_POST); + if (!$tmp){ + $arrResponse = ['error' => $lv->get_last_error()]; + } else { + $arrResponse = ['success' => true]; + } + + echo json_encode($arrResponse); + exit; + } + + if (array_key_exists('updatevm', $_POST)) { + //DEBUG + file_put_contents('/tmp/debug_libvirt_postparams.txt', print_r($_POST, true)); + file_put_contents('/tmp/debug_libvirt_updatexml.xml', $lv->config_to_xml($_POST)); + + if (!empty($_POST['shares'][0]['source'])) { + @mkdir($_POST['shares'][0]['source'], 0777, true); + } + + // Backup xml for existing domain in ram + $strOldXML = ''; + $boolOldAutoStart = false; + $res = $lv->domain_get_name_by_uuid($_POST['domain']['uuid']); + if ($res) { + $strOldXML = $lv->domain_get_xml($res); + $boolOldAutoStart = $lv->domain_get_autostart($res); + + //DEBUG + file_put_contents('/tmp/debug_libvirt_oldxml.xml', $strOldXML); + } + + // Remove existing domain + $lv->domain_undefine($res); + + // Save new domain + $tmp = $lv->domain_new($_POST); + if (!$tmp){ + $strLastError = $lv->get_last_error(); + + // Failure -- try to restore existing domain + $tmp = $lv->domain_define($strOldXML); + if ($tmp) $lv->domain_set_autostart($tmp, $boolOldAutoStart); + + $arrResponse = ['error' => $strLastError]; + } else { + $lv->domain_set_autostart($tmp, $_POST['domain']['autostart'] == 1); + + $arrResponse = ['success' => true]; + } + + echo json_encode($arrResponse); + exit; + } +?> + + + + + + + + + + + + + + + + + + + +
OpenELEC Version: + +
+
+

Select which OpenELEC version to download or use for this VM

+
+ + +
+ + + + + +
Download Folder: + +
+
+

Choose a folder where the OpenELEC image will downloaded to

+
+ + + + + + +
+ +
+
+
+
+ + +
+ + + + + +
Config Folder: + + +
+
+

Choose a folder or type in a new name off of an existing folder to specify where OpenELEC will save configuration files. If you create multiple OpenELEC VMs, these Config Folders must be unique for each instance.

+
+ + + + + + +
CPU Mode: + +
+
+
+

There are two CPU modes available to choose:

+

+ Host Passthrough
+ With this mode, the CPU visible to the guest should be exactly the same as the host CPU even in the aspects that libvirt does not understand. For the best possible performance, use this setting. +

+

+ Emulated
+ If you are having difficulties with Host Passthrough mode, you can try the emulated mode which doesn't expose the guest to host-based CPU features. This may impact the performance of your VM. +

+
+
+ + + + + + +
CPUs: +
+ + + +
+
+
+
+

By default, VMs created will be pinned to physical CPU cores to improve performance. From this view, you can adjust which actual CPU cores a VM will be pinned (minimum 1).

+
+
+ + + + + + + + + + +
Initial Memory: + + Max Memory: + +
+
+
+

Select how much memory to allocate to the VM at boot.

+
+
+
+
+

Select how much memory to allocate to the VM at boot (cannot be more than Max. Mem).

+
+
+ + + + + + +
Machine: + +
+
+
+

The machine type option primarily affects the success some users may have with various hardware and GPU pass through. For more information on the various QEMU machine types, see these links:

+ http://wiki.qemu.org/Documentation/Platforms/PC
+ http://wiki.qemu.org/Features/Q35
+

As a rule of thumb, try to get your configuration working with i440fx first and if that fails, try adjusting to Q35 to see if that changes anything.

+
+
+ + + + + + +
BIOS: + +
+
+
+

+ SeaBIOS
+ is the default virtual BIOS used to create virtual machines and is compatible with all guest operating systems (Windows, Linux, etc.). +

+

+ OVMF
+ (Open Virtual Machine Firmware) adds support for booting VMs using UEFI, but virtual machine guests must also support UEFI. Assigning graphics devices to a OVMF-based virtual machine requires that the graphics device also support UEFI. +

+

+ Once a VM is created this setting cannot be adjusted. +

+
+
+ + + $arrGPU) { + $strLabel = ($i > 0) ? appendOrdinalSuffix($i + 1) : ''; + + ?> + + + + + +
Graphics Card: + +
+ +
+

+ Graphics Card
+ If you wish to assign a graphics card to the VM, select it from this list. +

+ 1) { ?> +

Additional devices can be added/removed by clicking the symbols to the left.

+ +
+ + + + + + $arrAudio) { + $strLabel = ($i > 0) ? appendOrdinalSuffix($i + 1) : ''; + + ?> + + + + + +
Sound Card: + +
+ +
+

Select a sound device to assign to your VM. Most modern GPUs have a built-in audio device, but you can also select the on-board audio device(s) if present.

+ 1) { ?> +

Additional devices can be added/removed by clicking the symbols to the left.

+ +
+ + + + + + $arrNic) { + $strLabel = ($i > 0) ? appendOrdinalSuffix($i + 1) : ''; + + ?> + + + + + + + + + + +
Network MAC: + +
Network Bridge: + +
+ +
+
+

+ Network MAC
+ By default, a random MAC address will be assigned here that conforms to the standards for virtual network interface controllers. You can manually adjust this if desired. +

+ +

+ Network Bridge
+ The default libvirt managed network bridge (virbr0) will be used, otherwise you may specify an alternative name for a private network bridge to the host. +

+ +

Additional devices can be added/removed by clicking the symbols to the left.

+
+
+ + + + + + + + + + +
USB Devices: +
+ $arrDev) { + ?> +
+ None available"; + } + ?> +
+
+
+

If you wish to assign any USB devices to your guest, you can select them from this list.
+ NOTE: USB hotplug support is not yet implemented, so devices must be attached before the VM is started to use them.

+
+ + + + + + +
+ + + + + + +
+ + + + + + + +
+
+

Click Create to return to the Virtual Machines page where your new VM will be created.

+
+
+ + diff --git a/plugins/dynamix.vm.manager/templates/XML_Expert.form.php b/plugins/dynamix.vm.manager/templates/XML_Expert.form.php new file mode 100644 index 000000000..44879cd40 --- /dev/null +++ b/plugins/dynamix.vm.manager/templates/XML_Expert.form.php @@ -0,0 +1,198 @@ + +domain_get_name_by_uuid($strUUID); + $dom = $lv->domain_get_info($res); + + $strXML = $lv->domain_get_xml($res); + $boolRunning = ($lv->domain_state_translate($dom['state']) == 'running'); + } + + + if (array_key_exists('createvm', $_POST)) { + //DEBUG + file_put_contents('/tmp/debug_libvirt_postparams.txt', print_r($_POST, true)); + file_put_contents('/tmp/debug_libvirt_newxml.xml', $_POST['xmldesc']); + + $tmp = $lv->domain_define($_POST['xmldesc']); + if (!$tmp){ + $arrResponse = ['error' => $lv->get_last_error()]; + } else { + $lv->domain_set_autostart($tmp, $_POST['domain']['autostart'] == 1); + + $arrResponse = ['success' => true]; + } + + echo json_encode($arrResponse); + exit; + } + + if (array_key_exists('updatevm', $_POST)) { + //DEBUG + file_put_contents('/tmp/debug_libvirt_postparams.txt', print_r($_POST, true)); + file_put_contents('/tmp/debug_libvirt_updatexml.xml', $_POST['xmldesc']); + + // Backup xml for existing domain in ram + $strOldXML = ''; + $boolOldAutoStart = false; + $res = $lv->domain_get_name_by_uuid($_POST['domain']['uuid']); + if ($res) { + $strOldXML = $lv->domain_get_xml($res); + $boolOldAutoStart = $lv->domain_get_autostart($res); + + //DEBUG + file_put_contents('/tmp/debug_libvirt_oldxml.xml', $strOldXML); + } + + // Remove existing domain + $lv->domain_undefine($res); + + // Save new domain + $tmp = $lv->domain_define($_POST['xmldesc']); + if (!$tmp){ + $strLastError = $lv->get_last_error(); + + // Failure -- try to restore existing domain + $tmp = $lv->domain_define($strOldXML); + if ($tmp) $lv->domain_set_autostart($tmp, $boolOldAutoStart); + + $arrResponse = ['error' => $strLastError]; + } else { + $lv->domain_set_autostart($tmp, $_POST['domain']['autostart'] == 1); + + $arrResponse = ['success' => true]; + } + + echo json_encode($arrResponse); + exit; + } + +?> + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + diff --git a/plugins/dynamix.vm.manager/templates/images/arch.png b/plugins/dynamix.vm.manager/templates/images/arch.png new file mode 100644 index 000000000..7de8006f6 Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/arch.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/centos.png b/plugins/dynamix.vm.manager/templates/images/centos.png new file mode 100644 index 000000000..dfc878472 Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/centos.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/chromeos.png b/plugins/dynamix.vm.manager/templates/images/chromeos.png new file mode 100644 index 000000000..3bb2020ca Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/chromeos.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/coreos.png b/plugins/dynamix.vm.manager/templates/images/coreos.png new file mode 100644 index 000000000..1c05fdcd6 Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/coreos.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/debian.png b/plugins/dynamix.vm.manager/templates/images/debian.png new file mode 100644 index 000000000..e8a4740ca Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/debian.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/default.png b/plugins/dynamix.vm.manager/templates/images/default.png new file mode 100644 index 000000000..ecdab7326 Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/default.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/fedora.png b/plugins/dynamix.vm.manager/templates/images/fedora.png new file mode 100644 index 000000000..306291088 Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/fedora.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/freebsd.png b/plugins/dynamix.vm.manager/templates/images/freebsd.png new file mode 100644 index 000000000..5cc26c79c Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/freebsd.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/linux.png b/plugins/dynamix.vm.manager/templates/images/linux.png new file mode 100644 index 000000000..ba079956c Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/linux.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/openelec.png b/plugins/dynamix.vm.manager/templates/images/openelec.png new file mode 100644 index 000000000..f9ad118ab Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/openelec.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/opensuse.png b/plugins/dynamix.vm.manager/templates/images/opensuse.png new file mode 100644 index 000000000..5785809c5 Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/opensuse.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/redhat.png b/plugins/dynamix.vm.manager/templates/images/redhat.png new file mode 100644 index 000000000..8bb31e85c Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/redhat.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/scientific.png b/plugins/dynamix.vm.manager/templates/images/scientific.png new file mode 100644 index 000000000..945f7a562 Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/scientific.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/slackware.png b/plugins/dynamix.vm.manager/templates/images/slackware.png new file mode 100644 index 000000000..63912d54d Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/slackware.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/steamos.png b/plugins/dynamix.vm.manager/templates/images/steamos.png new file mode 100644 index 000000000..58255a768 Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/steamos.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/ubuntu.png b/plugins/dynamix.vm.manager/templates/images/ubuntu.png new file mode 100644 index 000000000..86d6e2b2d Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/ubuntu.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/windows.png b/plugins/dynamix.vm.manager/templates/images/windows.png new file mode 100644 index 000000000..d9a047350 Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/windows.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/windows7.png b/plugins/dynamix.vm.manager/templates/images/windows7.png new file mode 100644 index 000000000..169ff72eb Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/windows7.png differ diff --git a/plugins/dynamix.vm.manager/templates/images/windowsxp.png b/plugins/dynamix.vm.manager/templates/images/windowsxp.png new file mode 100644 index 000000000..6337a6493 Binary files /dev/null and b/plugins/dynamix.vm.manager/templates/images/windowsxp.png differ diff --git a/plugins/dynamix.vm.manager/vnc.html b/plugins/dynamix.vm.manager/vnc.html new file mode 100644 index 000000000..0ec5e603d --- /dev/null +++ b/plugins/dynamix.vm.manager/vnc.html @@ -0,0 +1,222 @@ + + + + + + noVNC + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + + +
+ + + + + +
+
+
+ +
+ + +
+ + + + + + + +
+ + + +
+ noVNC is a browser based VNC client implemented using HTML5 Canvas + and WebSockets. You will either need a VNC server with WebSockets + support (such as libvncserver) + or you will need to use + websockify + to bridge between your browser and VNC server. See the noVNC + README + and website + for more information. +
+ +
+ + +
+
+ + +
+ +
+ +
+ + +
+ + + + + +
+ + +
+ +
    +
  • +
  • +
  • +
  • +
  • +
  • +
    +
  • Path
  • +
  • +
  • +
  • Repeater ID
  • +
    + +
  • +
  • + + +
  • +
  • +
    +
  • +
+
+
+ + +
+
    +
  • +
  • +
  • +
  • +
+
+ +
+ + +
+

no
VNC

+ + +
+ + Canvas not supported. + +
+ +
+ + + + + diff --git a/plugins/dynamix/AFP.page b/plugins/dynamix/AFP.page new file mode 100644 index 000000000..89893df15 --- /dev/null +++ b/plugins/dynamix/AFP.page @@ -0,0 +1,73 @@ +Menu="NetworkServices:1" +Title="AFP" +Icon="apple-logo.png" +--- + +
+Enable AFP: +: + +> Select 'Yes' enable [AFP](/Help) protocol support. +> +> Note: changing this value with array Started may cause a brief interruption in network services. + +Connected users: +: 0) $AFPUsers--; + echo $AFPUsers; + else: + echo "not available"; + endif;?> + +  +: +
+ +> ### Overview +> AFP for unRAID includes both `netatalk` to implement Apple Filing Protocol, and `avahi` +> to implement Zeroconf, aka, Bonjour. +> +> As with SMB and NFS, you may export both disk shares and user shares via AFP. There are some important +> limitations to be aware of however: +> +> * You must be very careful when enabling AFP export of a disk share when that disk is also enabled +> for user shares. This is because `netatalk` creates several system directories in the root of shares, +> and these directories will show up as user shares. To prevent this, you may exclude the disk(s) +> from the user share file system on the [Share Settings](/Settings/ShareSettings) page. +> +> * The netatalk documentation includes a strong warning to not use symlinks anywhere in a file system +> being exprted via AFP. +> +> AFP for unRAID supports Time Machine, and all three security modes. +> +> ### Bonjour +> When AFP is enabled, your server name, with a `-AFP` suffix, should automatically appear in the left-hand pane of +> Finder alongside an Xserve icon. Clicking this icon permits you to explore the server shares using AFP protocol. +> +> In addition, if SMB is enabled, your server name, without any suffix, should also appear. This provides +> access to shares using the SMB protocol. +> +> ### Other notes +> [Prevent .DS_Store file creation on network volumes](http://hints.macworld.com/article.php?story=2005070300463515) - from the article: +> +> To prevent the creation of these files, open the Terminal and type: +> +> defaults write com.apple.desktopservices DSDontWriteNetworkStores true +> +> It may be necessary to log out and back in, or even to restart the computer for the change to take effect +> (this is what the article states). \ No newline at end of file diff --git a/plugins/dynamix/About.page b/plugins/dynamix/About.page new file mode 100644 index 000000000..fa555a7fb --- /dev/null +++ b/plugins/dynamix/About.page @@ -0,0 +1,3 @@ +Menu="Tools:90" +Type="menu" +Title="About" \ No newline at end of file diff --git a/plugins/dynamix/ArrayDevices.page b/plugins/dynamix/ArrayDevices.page new file mode 100644 index 000000000..340636979 --- /dev/null +++ b/plugins/dynamix/ArrayDevices.page @@ -0,0 +1,115 @@ +Menu="Main:1" +Title="Array Devices" +--- + + + + + + +"; +endforeach; +if ($display['total']) echo ""; +?> + +
DeviceIdentificationTemp.ReadsWritesErrorsFSSizeUsedFreeView
 
 
+ +> **Colored Status Indicator** the significance of the color indicator at the beginning of each line in *Array Devices* is as follows: +> +> Normal operation, device is active. +> +> Device is in standby mode (spun-down). +> +> Device contents emulated. +> +> Device is disabled, contents emulated. +> +> New device. +> +> No device present, position is empty. +> +> **Identification** is the *signature* that uniquely identifies a storage device. The signature +> includes the device model number, serial number, linux device id, and the device size. +> +> **Temp.** (temperature) is read directly from the device. You configure which units to use on +> the [Display Preferences](Settings/Display) page. We do not read the temperature of spun-down hard +> drives since this typically causes them to spin up; instead we display the `*` symbol. We also +> display the `*` symbol for SSD and Flash devices, though sometimes these devices do report a valid +> temperature, and sometimes they return the value `0`. +> +> **Size, Used, Free** reports the total device size, used space, and remaining space for files. These +> units are also configured on the [Display Preferences](Settings/Display) page. The +> amount of space used will be non-zero even for an empty disk due to file system overhead. +> +> *Note: for a multi-device cache pool, this data is for the entire pool as returned by btrfs.* +> +> **Reads, Writes** are a count of I/O requests sent to the device I/O drivers. These statistics may +> be cleared at any time, refer to the Array Status section below. +> +> **Errors** counts the number of *unrecoverable* errors reported by the device +> I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity +> reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable +> write error results in *disabling* the disk. +> +> **FS** indicates the file system detected in partition 1 of the device. +> +> **View** column contains a folder icon indicating the device is *mounted*. Click the icon to +> browse the file system. +> +> If "Display array totals" is enable on the [Display Preferences](Settings/Display) page, a +> **Total** line is included which provides a tally of the device statistics, including the average temperature +> of your devices. +> +> The Array must be Stopped in order to change Array device assignments. +> +> An unRAID array consists of a single Parity disk and a number of Data disks. The Data +> disks are exclusively used to store user data, and the Parity disk provides the redundancy necessary +> to recover from any singe disk failure. +> +> Since data is not striped across the array, the Parity disk must be as large, or larger than the largest Data +> disk. Parity should also be your highest performance drive. +> +> Each Data disk has its own file system and can be exported as a +> separate share. +> +> Click on the Device name to configure individual device settings and launch certain utilities. + + +
+> **Slots** select the number of device slots in your server designated for Array devices. +> The minimum number of Array slots is 2, and you must have at least one device assigned to the array. + diff --git a/plugins/dynamix/ArrayOperation.page b/plugins/dynamix/ArrayOperation.page new file mode 100644 index 000000000..914c46e10 --- /dev/null +++ b/plugins/dynamix/ArrayOperation.page @@ -0,0 +1,607 @@ +Menu="Main:5" +Title="Array Operation" +--- + + + +"; + echo ""; + echo "Maintenance mode"; + echo "Maintenance mode - if checked, Start array but do not mount disks."; + echo ""; +} +function status_indicator() { + global $var; + $ball = "/webGui/images/{$var['mdColor']}.png"; + switch ($var['mdColor']) { + case 'green-on': $help = 'Started, array protected'; break; + case 'green-blink': $help = 'Stopped'; break; + case 'yellow-on': $help = 'Started, array unprotected'; break; + case 'yellow-blink': $help = 'Stopped'; break; + } + return "$help"; +} +?> +
+ + + + + + + + +0):?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Started>Stop will take the array off-line. + +
0||file_exists('/var/run/mover.pid')?'disabled':''?>>Yes I want to do this + +
Unmountable disk present:
+".my_disk($disk['name'])." • {$disk['id']} ({$disk['device']})";?>
Format will create a file system in all Unmountable disks, discarding all data currently on those disks.
+ Yes I want to do this +
Sync will start Parity-Sync and/or Data-Rebuild.
Clear will start Clearing new data disk(s).
Check will start Read-Check of all data disks.
Parity is valid.Check will start Parity-Check. +
Write corrections to parity
Last check incomplete on , finding error. +
Error code:
Parity has not been checked yet.
Last checked , finding error.
Last check completed on , finding error. +
Duration:
Read-Check in progress.Cancel will stop the Read-Check.
Parity-Check in progress.Cancel will stop the Parity-Check.
Parity-Sync/Data-Rebuild in progress.Cancel will stop Parity-Sync/Data-Rebuild. +
WARNING: canceling may leave the array unprotected!
Clearing in progress.Cancel will stop Clearing.
Total size:
Elapsed time:
Current position:
Estimated speed:
Estimated finish:
Sync errors detected:corrected:
Starting...
Started, formatting...
Copying, % complete...
Clearing, % complete...
Stopping...
Stopped.Invalid or missing registration key.
Stopped.Too many attached devices. Please consider upgrading your registration key.
Stopped. Configuration valid.Start will bring the array on-line and start Parity-Sync and/or Data-Rebuild.
Stopped. New data disks(s) detected.Start will bring the array on-line and start Clearing new data disk(s).
Stopped. Unclean shutdown detected.Start will bring the array on-line and start Parity-Check. +
Write corrections to parity
Stopped. Configuration valid.Start will bring the array on-line.
Stopped. Configuration valid.Start will record all disk information and bring the array on-line. +
The array will be immediately available, but unprotected since parity has not been assigned.
Stopped. Configuration valid.Start will record all disk information, bring the array on-line, and start Parity-Sync. +
The array will be immediately available, but unprotected until Parity-Sync completes. +
Parity is already valid.
Stopped. Found new disk.
+".my_disk($disk['name'])." • {$disk['id']} ({$disk['device']})";?>
Start will record the new disk information and bring the expanded array on-line.
Stopped. Found new erased disk.
+".my_disk($disk['name'])." • {$disk['id']} ({$disk['device']})";?>
Start will record the new disk information and bring the expanded array on-line. +
Yes I want to do this
Stopped. Found new disk.
+".my_disk($disk['name'])." • {$disk['id']} ({$disk['device']})";?>
Clear will completely clear (set to zero) the new disk. +
Once clear completes, the array may be Started, expanding the array to include the new disk. +
Caution: any data on the new disk will be erased! +
If you want to preserve the data on the new disk, reset the array configuration and rebuild parity instead. +
Yes I want to do this
Stopped. Missing disk.Start will disable the missing disk and then bring the array on-line. +
Install a replacement disk as soon as possible. +
Yes I want to do this
Stopped. Replacement disk installed.Start will start Parity-Sync and/or Data-Rebuild. +
Yes I want to do this
Stopped. Upgrading parity.Start will bring the array on-line and start Parity-Sync. +
Yes I want to do this
Stopped. Upgrading disk.Start will bring the array on-line, start Data-Rebuild, and then expand the file system. +
Yes I want to do this
Stopped. Ugrading disk/swapping parity.Start will expand the file system of the data disk (if possible); then bring the array on-line and start Data-Rebuild. +
Yes I want to do this
Stopped. Ugrading disk/swapping parity.Copy will copy the parity information to the new parity disk. +
Once copy completes, the array may be Started, to initiate Data-Rebuild of the disabled disk. +
Yes I want to do this
Stopped. Two or more disks are wrong.
+".my_disk($disk['name'])." • {$disk['id']} ({$disk['device']})";?>
Start will just record the new disk positions and bring the array on-line. +
We recommend you start a Parity-Check afterwards just to be safe. +
Yes I want to do this
Stopped. Invalid expansion.You may not add new disk(s) and also remove existing disk(s).
Stopped. Replacement disk is too small.The replacement disk must be as big or bigger than the original.
Stopped. Disk in parity slot is not biggest.If this is a new array, move the largest disk into the parity slot. +
If you are adding a new disk or replacing a disabled disk, try Parity-Swap.
Stopped. Invalid configuration.Too many wrong and/or missing disks!
Stopped. No data disks.No array data disks have been assigned!
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
style="width:80px"> style="width:80px">Spin Down will immediately spin down all disks.
Spin Up will immediately spin up all disks.
Identify will briefly read from each disk in order.
Reboot will activate a system reset. +
Yes I want to do this +
Power down will activate a clean power down. +
Yes I want to do this +
Reboot will activate a system reset. +
Power down will activate a clean power down. +
Clear Statistics will immediately clear all disk statistics.
+
+ +
+ + + + + + + + + + +
Mover is running.Click to invoke the Mover.
+
+ + + +> **Colored Status Indicator** the significance of the color indicator of the *Array* is as follows: +> +> Array is Started and Parity is valid. +> +> Array is Stopped, Parity is valid. +> +> Array is Started, but Parity is invalid. +> +> Array is Stopped, Parity is invalid. +> + + +> #### Assigning Devices +> +> An unRAID disk array consists of a single parity disk and a number of data disks. The data +> disks are exclusively used to store user data, and the parity disk provides the redundancy necessary +> to recover from any singe disk failure. +> +> Note that we are careful to use the term *disk* when referring to an array storage device. We +> use the term *hard drive* (or sometimes just *drive*) when referring to an actual hard disk drive (HDD) +> device. This is because in a RAID system it is possible to read/write an array disk whose corresponding +> hard drive is disabled or even missing! In addition, it is useful to be able to ask, "which device is +> assigned to be the parity disk?"; or, "which device corresponds to disk2?". +> +> We therefore need a way to assign hard drives to array disks. This is accomplished here on the +> Main page when the array is stopped. There is a drop-down box for each array disk which lists all the +> unassigned devices. To assign a device simply select it from the list. Each time a device +> assignment is made, the system updates a configuration file to record the assignment. +> +> #### Requirements +> +> Unlike traditional RAID systems which stripe data across all the array devices, an unRAID server +> stores files on individual hard drives. Consequently, all file write operations will involve both the +> data disk the file is being written to, and the parity disk. For these reasons, +> +> * the parity disk size must be as large or larger than any of the data disks, +> +> and +> +> * given a choice, the parity disk should be the fastest disk in your collection. +> +> #### Guidelines +> +> Here are the steps you should follow when designing your unRAID disk array: +> +> 1. Decide which hard drive you will use for parity, and which hard drives you will use for +> data disk1, disk2, etc., and label them in some fashion. Also, find the serial number of each hard +> drive and jot it down somewhere; you will need this information later. +> +> 2. Install your hard drive devices, boot unRAID Server and bring up the webGui. If this is a fresh system +> build, then the Main page will show no disks installed. This doesn't mean the system can't detect your +> hard drives; it just means that none have been assigned yet. +> +> 3. Remember the serial numbers you recored back in step 1? For parity and each data disk, select the +> proper hard drive based on its serial number from the drop down list. +> +> #### Hot Plug +> +> You may also *hot plug* hard drives into your server if your hardware supports it. For example, +> if you are using hard drive cages, you may simply plug them into your server while powered on and +> with array Stopped. Refresh the Main page to have new unassigned devices appear in the assignment +> dropdown lists. +> +> #### Next Steps +> +> Once you have assigned all of your hard drives, refer to the Array Status section below +> and Start the array. + diff --git a/plugins/dynamix/BootDevice.page b/plugins/dynamix/BootDevice.page new file mode 100644 index 000000000..e052381e6 --- /dev/null +++ b/plugins/dynamix/BootDevice.page @@ -0,0 +1,46 @@ +Menu="Main:3" +Title="Boot Device" +--- + + + + + + +";?> + +
DeviceIdentificationTemp.ReadsWritesErrorsFSSizeUsedFreeView
 
+ +> Vital array configuration is maintained on the USB Flash device; for this reason, it must remain +> plugged in to your server. Click on [Flash](/Main/Flash?name=flash) to see the GUID and registration +> information, and to configure export settings. Since the USB Flash device is formatted using FAT file system, +> it may only be exported using SMB protocol. diff --git a/plugins/dynamix/Browse.page b/plugins/dynamix/Browse.page new file mode 100644 index 000000000..9e963102e --- /dev/null +++ b/plugins/dynamix/Browse.page @@ -0,0 +1,147 @@ +Title="Index of $dir" +Png="dirindex.png" +--- + + +/dev/null", $file); + if ($show_disk) + exec("getfattr --absolute-names -n user.LOCATIONS $path 2>/dev/null|grep -Po '^user.LOCATIONS=\"\K[^\\\"]+'", $disk); + else + $disk = array_fill(0, max(count($file),1), ''); + foreach ($file as $entry) { + $attr = explode('|', $entry); + $list[] = array( + 'type' => $attr[0], + 'name' => basename($attr[1]), + 'fext' => strtolower(pathinfo($attr[1],PATHINFO_EXTENSION)), + 'size' => $attr[2], + 'time' => $attr[3], + 'disk' => my_disk($disk[$i++])); + } +// sort by input 'field' + if ($field=='name') { + $type = array(); + $name = array(); + foreach ($list as $row) { + $type[] = $row['type']; + $name[] = strtolower($row['name']); + } + array_multisort($type,$opt, $name,$opt, $list); + } else { + $type = array(); + $indx = array(); + $name = array(); + foreach ($list as $row) { + $type[] = $row['type']; + $indx[] = $row[$field]; + $name[] = strtolower($row['name']); + } + if ($field=='size'||$field=='time') + array_multisort($type,$opt, $indx,$opt,SORT_NUMERIC, $name,$opt, $list); + else + array_multisort($type,$opt, $indx,$opt, $name,$opt, $list); + } +// return sorted list + return $list; +} + +function parent_link($text) { + global $dir, $path; + if (($dir == "/boot") || (dirname($dir) == "/mnt") || (dirname($dir) == "/mnt/user")) + return $text; + else { + $parent = urlencode_path(dirname($dir)); + return "$text"; + } +} + +// here we go.. +$show_disk = (substr_compare("/mnt/user",$dir,0,9)==0); +clearstatcache(); +if (empty($column)) $column = 'name'; +if (empty($order)) $order = 'A'; +$list = sort_by($column, $order=='A'?SORT_ASC:SORT_DESC, $show_disk); + +$order=($order=='A'?'D':'A'); +$fext_order=($column=='fext'?$order:'A'); +$name_order=($column=='name'?$order:'A'); +$size_order=($column=='size'?$order:'A'); +$time_order=($column=='time'?$order:'A'); +$disk_order=($column=='disk'?$order:'A'); +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + > + + + > + + > + + + + + +
TypeNameSizeLocationLast Modified
>
>: ,
diff --git a/plugins/dynamix/CacheDevices.page b/plugins/dynamix/CacheDevices.page new file mode 100644 index 000000000..055776e5f --- /dev/null +++ b/plugins/dynamix/CacheDevices.page @@ -0,0 +1,82 @@ +Menu="Main:2" +Title="Cache Devices" +Cond="($var['fsState']=='Stopped' || $var['cacheSbNumDisks'])" +--- + + + + + + +"; +endforeach; +?> + +
DeviceIdentificationTemp.ReadsWritesErrorsFSSizeUsedFreeView
 
+ +> **Colored Status Indicator** the significance of the color indicator at the beginning of each line in *Cache Devices* is as follows: +> +> Normal operation, device is active. +> +> Device is in standby mode (spun-down). +> +> New device. +> +> No device present, position is empty. +> +> **Cache** is a device, or device pool, *outside* the unRAID array. It may be exported for network access just +> like an Array device. Being outside the unRAID array results in significantly faster write access. +> +> There are two ways to configure the Cache: +> +> 1. As a single device, or +> 2. As a multi-device pool. +> +> When configured as a single device you may format the device using any supported file system (btrfs, reiserfs, +> or xfs). This configuration offers the highest performance, but at the cost of no data protection - if the +> single Cache device fails all data contained on it may be lost. +> +> When configured as a multi-device pool, unRAID OS will automatically select *btrfs-raid1* format (for both data +> and meta-data). btrfs permits any number of devices to be added to the pool and each copy of data is guaranteed +> to be written to two different devices. Hence the pool can withstand a single-disk failure without losing data. +> +> When [User Shares](/Settings/ShareSettings) are enabled, user shares may be configured to +> automatically make use of the Cache in order to +> speed up writes. A special background process called the *mover* can be scheduled to run +> periodically to move user share files off the Cache and onto the Array. + + +
+> **Slots** select the number of device slots in your server designated for Cache devices. + diff --git a/plugins/dynamix/CacheSettings.page b/plugins/dynamix/CacheSettings.page new file mode 100644 index 000000000..effac7416 --- /dev/null +++ b/plugins/dynamix/CacheSettings.page @@ -0,0 +1,64 @@ +Menu="ShareSettings:2" +Title="Cache Settings" +Cond="((isset($disks['cache']))&&($disks['cache']['status']!='DISK_NP'))" +--- + + + + + + +
+Use cache disk: +: + +> If set to 'Yes' then User Shares can possibly make use of the Cache Disk. You still need to enable +> use of the Cache Disk on individual user shares. + +Min. free space: +: + +> This represents a "floor" of the amount of free space remaining on the cache disk. If the free +> space becomes less than this value, then new files written to user shares with cache enabled will go to +> the array and not the cache disk. +> +> Enter a numeric value with one of these suffixes: +> +> **KB** = 1,000
+> **MB** = 1,000,000
+> **GB** = 1,000,000,000
+> **TB** = 1,000,000,000,000 +> +> If no suffix, a count of 1024-byte blocks is assumed.
+> +> Examples: +> +> **2GB** => 2,000,000,000 bytes
+> **2000000** => 2,048,000,000 bytes + +  +: >Array must be **Stopped** to change +
diff --git a/plugins/dynamix/Confirmations.page b/plugins/dynamix/Confirmations.page new file mode 100644 index 000000000..78439d022 --- /dev/null +++ b/plugins/dynamix/Confirmations.page @@ -0,0 +1,47 @@ +Menu="UserPreferences" +Title="Confirmations" +Icon="confirmations.png" +--- + + +
+ + +Confirm reboot & powerdown commands: +: + +> Choose if rebooting or powering down the server needs a confirmation checkbox. + +Confirm array stop command: +: + +> Choose if stopping the array needs a confirmation checkbox. + + +Confirm sleep command: +: + + +  +: +
diff --git a/plugins/dynamix/Credits.page b/plugins/dynamix/Credits.page new file mode 100644 index 000000000..683301880 --- /dev/null +++ b/plugins/dynamix/Credits.page @@ -0,0 +1,26 @@ +Menu="About" +Title="Credits" +--- +**unRAID webGUI** Copyright 2005-2015 [Lime Technology, Inc.](http://lime-technology.com). + +**Simple Features** Copyright 2012, Andrew Hamer-Adams. + +**Dynamix** Copyright 2012-2015, Bergware International. + +**Extended Docker Configuration Page** Copyright (C) 2014 Guilherme Jardim. + +**APC UPS Configuration Page** Copyright 2015, by Dan Landon + +**VM Manager** Copyright 2015, Eric Schultz, Derek Macias + +The Software comprising the unRAID webGui, which is all files within this repository except for +files listed below, is licensed under GPL version 2. + +* The Lime Technology logo file(s) `webGui/images/limtech-*.*` are property of Lime Technology, Inc. +and may not be used in any other project without written permission from Lime Technology, Inc. + +***unRAID*** is a registered trademark of [Lime Technology, Inc.](http://lime-technology.com). + +This file shall be included in all copies or substantial portions of the Software. + + diff --git a/plugins/dynamix/DashStats.page b/plugins/dynamix/DashStats.page new file mode 100644 index 000000000..3659605b2 --- /dev/null +++ b/plugins/dynamix/DashStats.page @@ -0,0 +1,405 @@ +Menu="Dashboard" +Title="Statistics" +--- + + +/dev/null|grep -c 'fan[0-9]_input'"); +$group = $var['shareSMBEnabled']=='yes' | $var['shareAFPEnabled']=='yes' | $var['shareNFSEnabled']=='yes'; +$names = ""; +$url = "/webGui/include/DashUpdate.php"; + +foreach ($shares as $share) { + if ($names) $names .= ','; + $names .= $share['name']; +} + +exec("awk '/^MemTotal/{print $2*1024/1.048576}' /proc/meminfo",$total); +exec("ifconfig -s|awk '/^(bond|eth|lo)/{print $1}'",$ports); + +$memory_installed = exec("dmidecode -t 17 | awk -F: '/^\tSize: [0-9]+ MB$/{t+=$2} END{print t}'"); +$memory_maximum = exec("dmidecode -t 16 | awk -F: '/^\tMaximum Capacity: [0-9]+ GB$/{t+=$2} END{print t}'"); +// If maximum < installed then roundup maximum to the next power of 2 size of installed. E.g. 6 -> 8 or 12 -> 16 +if ($memory_maximum*1024 < $memory_installed) $memory_maximum = pow(2,ceil(log($memory_installed/1024)/log(2))); + +function init_row($label) { + echo "$label".str_repeat("",25).""; +} + +function parity_status() { + global $var,$disks; + if ($disks['parity']['status']=='DISK_NP') { + echo "Parity disk not present"; + return; + } + if ($var['mdNumInvalid']==0) { + echo "Parity is valid"; + if ($var['sbSynced']==0) { + echo "Parity has not been checked yet."; + } else { + unset($time); + exec("awk '/sync completion/ {gsub(\"(time=|sec)\",\"\",x);print x;print \$NF};{x=\$NF}' /var/log/syslog|tail -2", $time); + if (!count($time)) $time = array_fill(0,2,0); + if ($time[1]==0) { + echo "Last checked on ".my_time($var['sbSynced']).day_count($var['sbSynced']).", finding {$var['sbSyncErrs']} error".($var['sbSyncErrs']==1?'.':'s.'); + echo "
Duration: ".my_check($time[0]).""; + } else { + echo "Last check incomplete on ".my_time($var['sbSynced']).day_count($var['sbSynced']).", finding {$var['sbSyncErrs']} error".($var['sbSyncErrs']==1?'.':'s.'); + echo "
Error code: ".my_error($time[1]).""; + } + } + } else { + if ($var['mdInvalidDisk']==0) { + echo "Parity is invalid"; + } else { + echo "Data is invalid"; + } + } +} +function truncate($string,$len) { + return strlen($string) < $len ? $string : substr($string,0,$len-3).'...'; +} +function export_settings($protocol,$share) { + if ($protocol!='yes' || $share['export']=='-') return "-"; + if ($share['export']=='e') return ucfirst($share['security']); + return ''.ucfirst($share['security']).''; +} +?> + + + + +-"); $i = 1; +foreach ($disks as $disk): + switch ($disk['type']): + case 'Parity': + if ($disk['status']!='DISK_NP') $row0[0] = ''; + break; + case 'Data': + case 'Cache': + if ($disk['status']!='DISK_NP') $row0[$i++] = ""; + break; + endswitch; +endforeach; +foreach ($devs as $dev): + $row0[$i++] = ""; +endforeach; +echo "".implode('',$row0); +?> + + + + + + + + + + + + + + + + + + + +2):?> +"; + if ($c+1<$cores) + echo ""; + else + echo ""; +endfor; +if ($fans>0): + echo $fans>2 ? ""; + for ($f=0; $f<$fans; $f+=2): + if ($f) echo ""; + if ($f+1<$fans) + echo ""; + else + echo ""; + endfor; +endif; +$scale = $display['scale']; +$display['scale'] = 2; +$dck = exec("df /var/lib/docker|grep -om1 '^/'"); +?> + + + + + +1):?> +"; + echo ""; +endforeach; +?> + +"; + $c += 2; +endforeach; +?> + +"; + $c += 2; +endforeach; +?> + + + + + + +"; +endforeach; +?> + + + + style='display:none'> +"; +endforeach; +?> + + + + style='display:none'> +"; +endforeach; +?> + + + + +"; +endforeach; +?> + + + + + + + + $share): + $i++; + $list = truncate($name,12); + $comment = truncate($share['comment'],28); + $security = export_settings($var['shareSMBEnabled'], $sec[$name]); + echo ""; +endforeach; +if (!count($shares)) echo ""; +?> + + + + style='display:none'> + $share): + $list = truncate($name,12); + $comment = truncate($share['comment'],28); + $security = export_settings($var['shareAFPEnabled'], $sec_afp[$name]); + echo ""; +endforeach; +if (!count($shares)) echo ""; +?> + + + + style='display:none'> + $share): + $list = truncate($name,12); + $comment = truncate($share['comment'],28); + $security = export_settings($var['shareNFSEnabled'], $sec_nfs[$name]); + echo ""; +endforeach; +if (!count($shares)) echo ""; +?> + + + + + $share): + $list = truncate($name,12); + $comment = truncate($share['comment'],28); + echo ""; +endforeach; +if (!count($shares)) echo ""; +?> + + + diff --git a/plugins/dynamix/Dashboard.page b/plugins/dynamix/Dashboard.page new file mode 100644 index 000000000..3400fe911 --- /dev/null +++ b/plugins/dynamix/Dashboard.page @@ -0,0 +1,3 @@ +Menu="Tasks:1" +Type="xmenu" +Tabs="false" \ No newline at end of file diff --git a/plugins/dynamix/DashboardApps.page b/plugins/dynamix/DashboardApps.page new file mode 100644 index 000000000..d1c8e6390 --- /dev/null +++ b/plugins/dynamix/DashboardApps.page @@ -0,0 +1,326 @@ +Menu="Dashboard:1" +Title="Apps" +Cond="(((pgrep('docker')!==false) || (pgrep('libvirtd')!==false)) && ($display['dashapps']!='none'))" +Markdown="false" +--- + + + + + +
+ + + +
+
+ + +
+ + + + + + + + diff --git a/plugins/dynamix/DateTime.page b/plugins/dynamix/DateTime.page new file mode 100644 index 000000000..bddf297c4 --- /dev/null +++ b/plugins/dynamix/DateTime.page @@ -0,0 +1,172 @@ +Menu="OtherSettings" +Title="Date and Time" +Icon="date-time.png" +--- + + + +
+Current date and time: +: + +Time zone: +: + +> Select your time zone. You may also define a custom time zone file and have it appear as a selection +> +> in the drop-down list. To do this, copy your time zone file to your flash device, with name `config/timezone`. + +Use NTP: +: + +> Select 'Yes' to use Network Time Protocol to keep your server time accurate. +> We **highly** recommend the use of a network time server, especially if you plan on using Active Directory. + +NTP server 1: +: + +> This is the primary NTP server to use. Enter a FQDN or an IP address. + +NTP server 2: +: + +> This is the alternate NTP server to use if NTP Server 1 is down. + +NTP server 3: +: + +> This is the alternate NTP Server to use if NTP Servers 1 and 2 are both down. + +New date and time: +: "> + +> Enter the current time-of-day. Use format YYYY-MM-DD HH:MM:SS. Greyed out when using NTP. + +  +: +
\ No newline at end of file diff --git a/plugins/dynamix/Device.page b/plugins/dynamix/Device.page new file mode 100644 index 000000000..e80d2024e --- /dev/null +++ b/plugins/dynamix/Device.page @@ -0,0 +1 @@ +Type="xmenu" \ No newline at end of file diff --git a/plugins/dynamix/DeviceAttributes.page b/plugins/dynamix/DeviceAttributes.page new file mode 100644 index 000000000..dd62fc9da --- /dev/null +++ b/plugins/dynamix/DeviceAttributes.page @@ -0,0 +1,35 @@ +Menu="Device" +Title="Attributes" +Cond="strpos($disks[$name]['status'],'_NP')===false" +--- + + + + + + + + + +> This list shows the SMART attributes supported by this disk. For more information about each SMART attribute, it is recommended to search online. +> +> Attributes in *orange* may require your attention. They have a **raw value** greater than zero and may indicate a pending disk failure. +> +> Special attention is required when the particular attribute raw value starts to increase over time. When in doubt, consult the Limetech forum for advice. diff --git a/plugins/dynamix/DeviceCapabilities.page b/plugins/dynamix/DeviceCapabilities.page new file mode 100644 index 000000000..9020ae928 --- /dev/null +++ b/plugins/dynamix/DeviceCapabilities.page @@ -0,0 +1,33 @@ +Menu="Device" +Title="Capabilities" +Cond="strpos($disks[$name]['status'],'_NP')===false" +--- + + + + + + + + + +> This list shows the SMART capabilities supported by this disk. +> +> Observe here the estimated duration of the SMART short and extended self-tests. diff --git a/plugins/dynamix/DeviceIdentify.page b/plugins/dynamix/DeviceIdentify.page new file mode 100644 index 000000000..50a0d408d --- /dev/null +++ b/plugins/dynamix/DeviceIdentify.page @@ -0,0 +1,31 @@ +Menu="Device" +Title="Identity" +Cond="strpos($disks[$name]['status'],'_NP')===false" +--- + + + + + + + + + +> This list shows the SMART identity information of this disk diff --git a/plugins/dynamix/DeviceInfo.page b/plugins/dynamix/DeviceInfo.page new file mode 100644 index 000000000..c25cf0a3c --- /dev/null +++ b/plugins/dynamix/DeviceInfo.page @@ -0,0 +1,473 @@ +Menu="Device:1" +Title="$name Settings" +Png="devicesettings.png" +--- + + +0 ? $refs[$i-1] : $refs[$end]; +$next = $i<$end ? $refs[$i+1] : $refs[0]; +?> + + + +
+ +Name: +: + +Partition size: +: KB (K=1024) + +Partition format: +: + + + + +Comments: +: + +> This text will appear under the *Comments* column for the share in Windows Explorer. +> Enter anything you like, up to 256 characters. + + + +File system status: +:   + + + + + +File system type: +: + + + +File system type: +: Array must be **Stopped** to change + + +1):?> + +File system type: +:   + + + +Spin down delay: +: + + + +Spinup group(s): +: + + + +  +: + +
+1):?> + +
Pool Information
+ +btrfs filesystem show: +: ".shell_exec("/sbin/btrfs filesystem show {$disk['uuid']}")."";?> + + + +btrfs filesystem df: +: ".shell_exec("/sbin/btrfs filesystem df /mnt/{$disk['name']}")."";?> + + +1):?> +
+ + + +
Balance Status
+ +btrfs balance status: +: " . implode("\n", $balance_status) . "";?> + + + + + + +  +: Options (see Help) + +> **Balance** will run the *btrfs balance* program to restripe the extents across all pool devices. +> +> The default *Options* are appropriate for btrfs-raid1. Do not change this unless you know what you are doing! + + + + + + +  +: *Running* + +> **Cancel** will cancel the balance operation in progress. + + + + +  +: + +> **Balance** is only available when the Device is Mounted. + + +
+ + + +
+ + + +
Scrub Status
+ +btrfs scrub status: +: " . implode("\n", $scrub_status) . "";?> + + + + + + +  +: Options (see Help) + +> **Scrub** runs the *btrfs scrub* program to check file system integrity. +> +> The *Options* field is initialized to include *-r* which specifies read-only. If repair is needed, you should run +> a second Scrub pass, removing the *-r* option; this will permit *btrfs scrub* to fix the file system. + + + + + + +  +: *Running* + +> **Cancel** will cancel the Scrub operation in progress. + + + + +  +: + +> **Scrub** is only available when the Device is Mounted. + + +
+ + +
+ + + +
Check Filesystem Status
+ +reiserfsck status: +: " . implode("\n", $check_status) . "";?> + + + + + + + +  +: Options (see Help) + +> **Check Filesystem** will run the *reiserfsck* program to check file system integrity on the device. +> +> The *Options* field may be filled in with specific options used to fix problems in the file system. Typically, you +> first run a Check Filesytem pass leaving *Options* blank. Upon completion, if *reiserfsck* finds any problems, you must +> run a second Check Filesystem pass, using a specific option as instructed by the first *reiserfsck* pass. +> +> After starting a Check Filesystem, you should Refresh to monitor progress and status. Depending on +> how large the file system is, and what errors might be present, the operation can take **a long time** to finish (hours). +> Not much info is printed in the window, but you can verify the operation is running by observing the read/write counters +> increasing for the device on the Main page. + + + + + + + +  +: *Running* + +> **Cancel** will cancel the Check Filesystem operation in progress. + + + + +  +: + +> **Check Fileystem** is only available when array is Started in **Mainenance** mode. + + +
+ + +
+ + + +
Check Filesystem Status
+ +xfs_repair status: +: " . implode("\n", $check_status) . "";?> + + + + + + + +  +: Options (see Help) + +> **Check Filesystem** will run the *xfs_repair* program to check file system integrity on the device. +> +> The *Options* field is initialized with *-n* which specifies check-only. If repair is needed, you should run +> a second Check Filesystem pass, setting the *Options* blank; this will permit *xfs_repair* to fix the file system. +> +> After starting a Check Filesystem, you should Refresh to monitor progress and status. Depending on +> how large the file system is, and what errors might be present, the operation can take **a long time** to finish (hours). +> Not much info is printed in the window, but you can verify the operation is running by observing the read/write counters +> increasing for the device on the Main page. + + + + + + +  +: *Running* + +> **Cancel** will cancel the Check Filesystem operation in progress. + + + + +  +: + +> **Check Fileystem** is only available when array is Started in **Mainenance** mode. + + +
+ + +
Self-Test
+
+Download SMART report: +: disabled> + +SMART self-test history: +: + +> Press **Show** to view the self-test history as is kept on the disk itself. +> This feature is only available when the disk is in active mode. + + + +SMART error log: +: + +> Press **Show** to view the error report as is kept on the disk itself. +> This feature is only available when the disk is in active mode. + + + +SMART short self-test: +: + +> Starts a *short* SMART self-test, the estimated duration can be viewed under the *Capabilities* section. This is usually a few minutes. +> +> When the disk is spun down, it will abort any running self-test. +> This feature is only available when the disk is in active mode. + +SMART extended self-test: +: + +> Starts an *extended* SMART self-test, the estimated duration can be viewed under the *Capabilities* section. This is usually several hours. +> +> When the disk is spun down, it will abort any running self-test. It is advised to disable the spin down timer of the disk +> to avoid interruption of this self-test. +> +> This feature is only available when the disk is in active mode. + +Last SMART test result: + +: + +: --- + + +> When no test is running it will show here the latest obtained self-test result (if available). +> Otherwise a progress indicator (percentage value) is shown for a running test. + +
diff --git a/plugins/dynamix/Diagnostics.page b/plugins/dynamix/Diagnostics.page new file mode 100644 index 000000000..a1af799ab --- /dev/null +++ b/plugins/dynamix/Diagnostics.page @@ -0,0 +1,74 @@ +Menu="UNRAID-OS" +Title="Diagnostics" +--- + + + + + +This utility is used for troubleshooting purposes. It will collect all of the system information and configuration files, and package these files in a single ZIP file which can be saved locally. +Subsequently, this file can be included in your correspondence with Limetech or the unRAID forum. + +This will help others to quickly get the inside information of your system and provide better support to your problem. The following information +and configuration files are collected: + +
++ */config*
+  
copy all *\*.cfg files*, *go* file and the *super.dat* file. These are configuration files. ++ */config/shares* +
copy all *\*.cfg* files. These are user share settings files. ++ *Syslog file(s)* +
copy the current *syslog* file and any previous existing *syslog* files. ++ *System* +
save output of the following commands: +
lsscsi, lspci, free, lsof, ps, ethtool & ifconfig. +
display of iommu groups. +
display of command line parameters (e.g. pcie acs override, pci stubbing, etc). +
save system variables. ++ *SMART reports* +
save a SMART report of each individual disk present in your system. ++ *Docker* +
save files *docker.log*, *libvirtd.log* and *libvirt/qemu/\*.log*. +
+ +Clicking **Download** will start the collection process and then instruct your browser to save the zip file locally. + +*No personal information such as user names, passwords, or any other file contents not specified above is included +by unRAID Server OS; however, your server name, IP address, and user share names* **will** *be included.* +*Note that 3rd-party plugins **may** or may not store personal information in plugin-specific configuration files and/or output +to the system log.* + + + +
diff --git a/plugins/dynamix/Disk.page b/plugins/dynamix/Disk.page new file mode 100644 index 000000000..776a9ddde --- /dev/null +++ b/plugins/dynamix/Disk.page @@ -0,0 +1,41 @@ +Type="xmenu" +--- + + +0 ? $refs[$i-1] : $refs[$end]; +$next = $i<$end ? $refs[$i+1] : $refs[0]; +?> + diff --git a/plugins/dynamix/DiskList.page b/plugins/dynamix/DiskList.page new file mode 100644 index 000000000..71f543958 --- /dev/null +++ b/plugins/dynamix/DiskList.page @@ -0,0 +1,100 @@ +Menu="Shares:2" +Title="Disk Shares" +Cond="$var['fsState']=='Started' && $var['shareDisk']!='no'" +--- + + +'.ucfirst($share['security']).'
'; +} +// Share size per disk +$preserve = $path==$prev; +$ssz2 = array(); +foreach (glob("state/*.ssz2", GLOB_NOSORT) as $entry) { + if ($preserve) { + $ssz2[basename($entry, ".ssz2")] = parse_ini_file($entry); + } else { + unlink($entry); + } +} +?> + + + + + + + + + + + + + + + $share_size): + if ($share_name!="total"): +?> + + + + + + + + + + + + + + + + + +
There are no exportable disk shares
+ + +> **Colored Status Indicator** the significance of the color indicator at the beginning of each line in *Disk Shares* is as follows: +> +> Mounted, underlying device has redundancy/protection. +> +> Mounted, underlying device does not have redundancy/protection. +> +> SMB security mode displayed in *italics* indicates exported hidden shares. +> +> AFP security mode displayed in *italics* indicates exported time-machine shares. diff --git a/plugins/dynamix/DiskSettings.page b/plugins/dynamix/DiskSettings.page new file mode 100644 index 000000000..04fa8f7d4 --- /dev/null +++ b/plugins/dynamix/DiskSettings.page @@ -0,0 +1,117 @@ +Menu="OtherSettings" +Title="Disk Settings" +Icon="disk-settings.png" +--- + +
+Enable auto start: +: + +> If set to 'Yes' then if the device configuration is correct upon server start-up, +> the array will be automatically Started and shares exported.
+> If set to 'No' then you must Start the array yourself. + +Default spin down delay: +: + +> This setting defines the 'default' time-out for spinning hard drives down after a period +> of no I/O activity. You may override the default value for an individual disk on the Disk Settings +> page for that disk. + +Enable spinup groups: +: + +> If set to 'Yes' then the [Spinup Groups](/Help) feature is enabled. + +Default partition format: +: + +> Defines the type of partition layout to create when formatting hard drives 2TB in size and +> smaller **only**. (All devices larger then 2TB are always set up with GPT partition tables.) +> +> **MBR: unaligned** setting will create MBR-style partition table, where the single +> partition 1 will start in the **63rd sector** from the start of the disk. This is the *traditional* +> setting for virtually all MBR-style partition tables. +> +> **MBR: 4K-aligned** setting will create an MBR-style partition table, where the single +> partition 1 will start in the **64th sector** from the start of the disk. Since the sector size is 512 bytes, +> this will *align* the start of partition 1 on a 4K-byte boundry. This is required for proper +> support of so-called *Advanced Format* drives. +> +> Unless you have a specific requirement do not change this setting from the default **MBR: 4K-aligned**. + +Default file system: +: + +> Defines the default file system type to create when an *unmountable* array device is formatted. +> +> The default file system type for a single or multi-device cache is always Btrfs. + +Force NCQ disabled: +: + +> This is a system "tunable" parameter. +> +> If set to 'Yes' then "Native Command Queuing" to array drives is disabled.
+> If set to 'No' then it is enabled. Most users find that overall system performance is better with +> NCQ turned off. + +Tunable (poll_attributes): +: + +> This "tunable" defines the disk SMART polling interval, in seconds. A value of 0 disables SMART polling (not recommended). + +Tunable (md_num_stripes): +: + +Tunable (md_sync_window): +: + +> These tunables let you control [certain properties](/Help) of the unRAID driver. +> +> Note: For each of these settings, if you delete the value and click Apply, the value is restored to its default. + +  +: +
diff --git a/plugins/dynamix/DisplaySettings.page b/plugins/dynamix/DisplaySettings.page new file mode 100644 index 000000000..6e05c7524 --- /dev/null +++ b/plugins/dynamix/DisplaySettings.page @@ -0,0 +1,337 @@ +Menu="UserPreferences" +Title="Display Settings" +Icon="display-settings.png" +--- + + +"; +$icon = ""; +?> + + + + + + +> The display settings below determine how items are displayed on screen. Use these settings to tweak the visual effects to your likings. +> +> You can experiment with these settings as desired, they only affect visual properties. + +
+ + +Date format: +: + +Time format: +: + +Number format: +: + +Number scaling: +: + +Number alignment: +: + +Page view: +: + +Table view spacing: +: + +Display array totals: +: + +Show array utilization indicator: +: + +Show banner: +: + + + +> Image will be scaled to 1270x90 pixels. The maximum image file upload size is 95 kB (97,280 bytes). + +Show Dashboard apps: +: + +Dynamix color theme: +: + +Used / Free columns: +: + +Warning disk utilization level (%): +: + +> *Warning disk utilization* sets the warning threshold for a hard disk utilization. Exceeding this threshold will result in a warning notification. +> +> When the warning level is set equal or greater than the critical level, there will be only critical notifications (warnings are not existing). + +Critical disk utilization level (%): +: + +> *Critical disk utilization* sets the critical threshold for a hard disk utilization. Exceeding this threshold will result in an alert notification. + +Temperature unit: +: + +> Selects the temperature unit for the disk temperature thresholds. Changing the unit will adjust the existing value in the disk temperature thresholds as appropriate. +> +> Make sure any newly entered values represent the selected temperature unit. + +Warning disk temperature threshold (°): +: + +> *Warning disk temperature* sets the warning threshold for a hard disk temperature. Exceeding this threshold will result in a warning notification. + +Critical disk temperature threshold (°): +: + +> *Critical disk temperature* sets the critical threshold for a hard disk temperature. Exceeding this threshold will result in an alert notification. + +Page update frequency: +: + +> *Page update* determines how often the browser will query the unRAID server to obtain the latest information. +> +> By default 'real-time' is selected. In case issues are experienced in the operation of the unRAID server, then the update frequency can be lowered or disabled altogether. +> In the latter case a 'refresh' button appears in the top of the screen to do manual page refreshing. +> +> A special option exists to disable screen updates while a parity operation is in progress. Use this option when degradation of the parity operation is observed. + + +: +
diff --git a/plugins/dynamix/FTP.page b/plugins/dynamix/FTP.page new file mode 100644 index 000000000..07aea1db4 --- /dev/null +++ b/plugins/dynamix/FTP.page @@ -0,0 +1,52 @@ +Menu="NetworkServices:999" +Title="FTP Server" +Icon="ftp-server.png" +--- + + + + + +
+ + +FTP user(s): +: + +> Enter the user names (separated by spaces) permitted to acces the server using [FTP](/Help). +> To disable FTP, clear this setting. +> +> **Note:** do not enter user name `root` since this may cause problems in the future. + +  +: + +
+ +> ### Overview +> +> unRAID includes the popular `vsftpd` FTP server. The configuration of `vsftp` is currently very +> simple: **All** user names entered above are permitted to access the server via FTP and will have +> *full read/write/delete access* to the entire server, so use with caution. diff --git a/plugins/dynamix/FeedbackButton.page b/plugins/dynamix/FeedbackButton.page new file mode 100644 index 000000000..7b82173cf --- /dev/null +++ b/plugins/dynamix/FeedbackButton.page @@ -0,0 +1,21 @@ +Menu="Buttons" +Title="Feedback" +Icon="feedback.png" +--- + + \ No newline at end of file diff --git a/plugins/dynamix/Flash.page b/plugins/dynamix/Flash.page new file mode 100644 index 000000000..e80d2024e --- /dev/null +++ b/plugins/dynamix/Flash.page @@ -0,0 +1 @@ +Type="xmenu" \ No newline at end of file diff --git a/plugins/dynamix/FlashInfo.page b/plugins/dynamix/FlashInfo.page new file mode 100644 index 000000000..e6afe1644 --- /dev/null +++ b/plugins/dynamix/FlashInfo.page @@ -0,0 +1,38 @@ +Menu="Flash" +Title="Flash Device Settings" +--- + +Flash Vendor: +: + +Flash Product: +:   + +Flash GUID: +:   + + + +  +: **Blacklisted** - Contact Support + + + +  +: [Registration Key Manager](/Tools/Registration) + + + +  +: diff --git a/plugins/dynamix/HelpButton.page b/plugins/dynamix/HelpButton.page new file mode 100644 index 000000000..cf1268331 --- /dev/null +++ b/plugins/dynamix/HelpButton.page @@ -0,0 +1,23 @@ +Menu="Buttons" +Title="Help" +Icon="help.png" +--- + + \ No newline at end of file diff --git a/plugins/dynamix/Identification.page b/plugins/dynamix/Identification.page new file mode 100644 index 000000000..41c2ddfeb --- /dev/null +++ b/plugins/dynamix/Identification.page @@ -0,0 +1,41 @@ +Menu="OtherSettings" +Title="Identification" +Icon="ident.png" +--- + + +
+ +Server name: +: > + +> The network identy of your server. Also known as *hostname* or *short hostname*. Windows networking +> refers to this as the *netBIOS name* and must be 15 characters or less in length. +> Use only alphanumeric characters (that is, "A-Z", "a-z", and "0-9"), dashes ("-"), and dots ("."); +> and, the first character must be alphanumeric. + +Description: +: > + +> This is a text field that is seen next to a server when listed within Network or Network Neighborhood +> (Windows), or Finder (Mac OS X). + +Model: +: > + +> This is the server model number. + +  +: >Array must be **Stopped** to change +
diff --git a/plugins/dynamix/InfoButton.page b/plugins/dynamix/InfoButton.page new file mode 100644 index 000000000..7e69f31da --- /dev/null +++ b/plugins/dynamix/InfoButton.page @@ -0,0 +1,21 @@ +Menu="Buttons" +Title="Info" +Icon="info.png" +--- + + \ No newline at end of file diff --git a/plugins/dynamix/InstallKey.page b/plugins/dynamix/InstallKey.page new file mode 100644 index 000000000..49c563bff --- /dev/null +++ b/plugins/dynamix/InstallKey.page @@ -0,0 +1,30 @@ +Menu="Registration" +Title="Install Key" +Cond="($var['regTy']!='Pro')" +--- + + +
+ +To install a registration key, paste the key file URL in the box below and click **Install Key**. + +Key file URL: +: + + +
diff --git a/plugins/dynamix/LICENSE b/plugins/dynamix/LICENSE new file mode 100644 index 000000000..3912109b5 --- /dev/null +++ b/plugins/dynamix/LICENSE @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/plugins/dynamix/LogButton.page b/plugins/dynamix/LogButton.page new file mode 100644 index 000000000..15590ff63 --- /dev/null +++ b/plugins/dynamix/LogButton.page @@ -0,0 +1,21 @@ +Menu="Buttons" +Title="Log" +Icon="log.png" +--- + + diff --git a/plugins/dynamix/Main.page b/plugins/dynamix/Main.page new file mode 100644 index 000000000..7b626ef43 --- /dev/null +++ b/plugins/dynamix/Main.page @@ -0,0 +1,2 @@ +Menu="Tasks:1" +Type="xmenu" \ No newline at end of file diff --git a/plugins/dynamix/MoverSettings.page b/plugins/dynamix/MoverSettings.page new file mode 100644 index 000000000..a546c520a --- /dev/null +++ b/plugins/dynamix/MoverSettings.page @@ -0,0 +1,123 @@ +Menu="Scheduler:2" +Title="Mover Settings" +Cond="(($var['shareUser']!='-')&&(isset($disks['cache']))&&($disks['cache']['status']!='DISK_NP')&&($var['shareCacheEnabled']=='yes'))" +--- + + + + +
+Mover schedule: +: + +> Choose a mover schedule ranging from hourly, daily, weekly and monthly. +> +> The interval determines how fast the mover will activated, it runs in the background. + +Day of the week: +: + +> Choose a day when the weekly schedule is selected. Otherwise disabled. + +Day of the month: +: + +> Choose a date when the monthly schedule is selected. Otherwise disabled. + +Time of the day: +: style="display:none"> +   HH:MM +: style="display:none"> + +> When an hourly schedule is selected this will set the interval in hours. An interval always starts on the whole hour (minute 0). +> +> For the other schedules choose here the time of the day the mover should start. + +Mover logging: +: + +> Write mover messages to the syslog file. + +  +: + +  + +: Mover is running. + +: Click to invoke the Mover. + +
\ No newline at end of file diff --git a/plugins/dynamix/NFS.page b/plugins/dynamix/NFS.page new file mode 100644 index 000000000..4e76e88a7 --- /dev/null +++ b/plugins/dynamix/NFS.page @@ -0,0 +1,62 @@ +Menu="NetworkServices:2" +Title="NFS" +Icon="linux-logo.png" +--- + + + +
+Enable NFS: +: + +> Select 'Yes' to enable the NFS protocol. + +Tunable (fuse_remember): +: + +> When NFS is enabled, this Tunable may be used to alleviate or solve instances of "NFS Stale File Handles" +> you might encounter with your NFS client. +> +> In essence, (fuse_remember) tells an internal subsystem (named "fuse") how long to "remember" or "cache" +> file and directory information associated with user shares. When an NFS client attempts to access a file +> (or directory) on the server, and that file (or directory) name is not cached, then you could encounter +> "stale file handle". +> +> The numeric value of this tunable is the number of seconds to cache file/directory name entries, +> where the default value of 330 indicates 5 1/2 minutes. There are two special values you may also set +> this to: +> +> * 0 which means, do not cache file/directory names at all, and +> * -1 which means cache file/directory names forever (or until array is stopped) +> +> A value of 0 would be appropriate if you are enabling NFS but only plan to use it for disk shares, +> not user shares. +> +> A value of -1 would be appropriate if no other timeout seems to solve the "stale file handle" on +> your client. Be aware that setting a value of -1 will cause the memory footprint to grow by approximatel +> 108 bytes per file/directory name cached. Depending how much RAM is installed in your server and how many +> files/directories you access via NFS this may or may not lead to out-of-memory conditions. + +  +: +
\ No newline at end of file diff --git a/plugins/dynamix/NetworkServices.page b/plugins/dynamix/NetworkServices.page new file mode 100644 index 000000000..c389c20d9 --- /dev/null +++ b/plugins/dynamix/NetworkServices.page @@ -0,0 +1,3 @@ +Menu="Settings:2" +Type="menu" +Title="Network Services" \ No newline at end of file diff --git a/plugins/dynamix/NetworkSettings.page b/plugins/dynamix/NetworkSettings.page new file mode 100644 index 000000000..49c099a8f --- /dev/null +++ b/plugins/dynamix/NetworkSettings.page @@ -0,0 +1,172 @@ +Menu="OtherSettings" +Title="Network Settings" +Icon="network-settings.png" +--- + + + +
+ +MAC address: +: + +Enable bonding: +: + +> Bonding is a feature that combines all of your physical Ethernet interfaces into a +> single *bond* interface named **bond0**. This lets you connect +> all of your ethernet ports to the same switch. + +Bonding mode: +: + +> Mode 1 (active-backup) is the recommended default. Other modes may require switch support. + +Setup bridge: +: + +> Bridging is a feature that combines all of your physical Ethernet interfaces into +> a single logical network segment. If **bonding** is also enabled, the bridge sits +> on top of the bond; this is useful for VM configurations. +> +> **Caution:** if bonding is also not enabled, do not connect two or more +> ethernet ports to the same switch unless you have STP enabled *and* the switch supports STP +> (most consumer switches **do not**). +> +> Doing so will cause an "ARP broadcast storm" and can bring down your +> entire network (unplugging all sever ethernet ports except one +> typically will restore your network). + +Bridge name: +: + +> This is the name of the bridge interface. If left blank, the name of the bridge will be **br0**. + +Bridge enable STP: +: + +> STP (Spanning Tree Protocol) prevents loops in multi-NIC bridges. This is enabled by default but +> can cause delays upon network setup; in most configurations it would be safe to disable. If unsure +> however, leave this set to **Yes**. + +Bridge forward delay: +: + +> Defines the bridge **forward delay** in seconds. +> +> Forwarding delay time is the time spent in each of the Listening and Learning states before the +> Forwarding state is entered. This delay is so that when a new bridge comes onto a busy network it +> looks at some traffic before participating. +> +> If the bridge is being used standalone (no other bridges near by), then it is safe to turn the +> forwarding delay off (set it to zero), before adding interface to a bridge. + +Obtain IP address automatically: +: + +> If set to 'Yes' the server will attempt to obtain its IP address from the local DHCP server. + +IP address: +: + +> Greyed out when using DHCP server. Otherwise specify here the IP address of the system. + +Network mask: +: + +> Greyed out when using DHCP server. Otherwise specify here the associated network mask, usually 255.255.255.0 + +Default gateway: +: + +> Greyed out when using DHCP server. Otherwise specify here the IP address of your router. + +Obtain DNS server address automatically: +: + +> If set to 'Yes' the server will use DNS server IP address returned by the local DHCP server.
+> If set to 'No' you may enter your own list. This is useful in Active Directory configruations where +> you need to set the first DNS Server entry to the IP address of your AD Domain server. + +DNS server 1: +: + +> This is the primary DNS server to use. Enter a FQDN or an IP address. +> Note: for *Active Directory* you **must** ensure this is set to the IP address of your +> AD Domain server. + +DNS server 2: +: + +> This is the DNS server to use when DNS Server 1 is down. + +DNS server 3: +: + +> This is the DNS server to use when DNS Servers 1 and 2 are both down. + +  +: >Stopped to change" : ""?> + +
diff --git a/plugins/dynamix/NewConfig.page b/plugins/dynamix/NewConfig.page new file mode 100644 index 000000000..e790a9ae9 --- /dev/null +++ b/plugins/dynamix/NewConfig.page @@ -0,0 +1,30 @@ +Menu="UNRAID-OS" +Title="New Config" +--- + +This is a utility to reset the array disk configuration so that all disks appear as "New" disks, as +if it were a fresh new server. + +This is useful when you have added or removed multiple drives and wish to rebuild parity based on +the new configuration. + +**DO NOT USE THIS UTILITY THINKING IT WILL REBUILD A FAILED DRIVE** - it will have the opposite +effect of making it ***impossible*** to rebuild an existing failed drive - you have been warned! + +
+ +Array must be stopped + +Yes I want to do this + +
\ No newline at end of file diff --git a/plugins/dynamix/NewPerms.page b/plugins/dynamix/NewPerms.page new file mode 100644 index 000000000..ef37dc7cc --- /dev/null +++ b/plugins/dynamix/NewPerms.page @@ -0,0 +1,56 @@ +Menu="UNRAID-OS" +Title="New Permissions" +--- + +This is a one-time action to be taken after upgrading from a pre-5.0 unRAID server +release to version 5.0. It is also useful for restoring default ownership/permissions on files and +directories when transitioning back from Active Directory to non-Active Directory integration. + +This utility starts a background process that goes to each of your data disks and cache disk +and changes file and directory ownership to nobody/users (i.e., uid/gid to 99/100), and sets permissions +as follows: +~~~ +For directories: + drwxrwxrwx + +For read/write files: + -rw-rw-rw- + +For readonly files: + -r--r--r-- +~~~ +Clicking Start will open another window and start the background process. Closing the window before +completion will terminate the background process - so don't do that. This process can take a long time if you have many files. + +
+ + + + Array must be started to change permissions. + + +Yes I want to do this + + +
diff --git a/plugins/dynamix/NotificationAgents.page b/plugins/dynamix/NotificationAgents.page new file mode 100644 index 000000000..82c46a2ec --- /dev/null +++ b/plugins/dynamix/NotificationAgents.page @@ -0,0 +1,143 @@ +Menu="Notifications:3" +Title="Notification Agents" +--- + +
+ + + +
+Agent as $agent) { + $name = $agent->Name; + $enabledAgent = agent_fullname("$name.sh", "enabled"); + $disabledAgent = agent_fullname("$name.sh", "disabled"); + if (is_file($disabledAgent)) { + $file = $disabledAgent; + if (is_file($enabledAgent)) unlink($enabledAgent); + } else { + $file = $enabledAgent; + } + $values = array(); + $script = ""; + if (is_file($file)) { + preg_match("/[#]{6,100}([^#]*?)[#]{6,100}/si", file_get_contents($file), $match); + if (isset($match[1])) { + foreach (explode(PHP_EOL, $match[1]) as $line) { + if (strpos($line, "=")) { + list($k, $v) = explode("=",str_replace(array("\""), "", $line),2); + $values[$k] = $v; + } + } + } + } + foreach (explode(PHP_EOL,(String) $agent->Script) as $line) if (trim($line)) $script .= trim($line)."{1}"; + echo "
$name".(is_file($enabledAgent) ? "Enabled": "Disabled")."
"; + echo "
"; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo "
Agent function:
"; + echo ""; + $i = 1; + foreach ($agent->Variables->children() as $var) { + $vName = preg_replace('#\[([^\]]*)\]#', '<$1>', (string) $var); + $vDesc = ucfirst(strtolower(preg_replace('#\[([^\]]*)\]#', '<$1>', (String) $var->attributes()->Desc))); + $vDefault = preg_replace('#\[([^\]]*)\]#', '<$1>', (String) $var->attributes()->Default); + $vHelp = preg_replace('#\[([^\]]*)\]#', '<$1>', (String) $var->attributes()->Help); + echo "
${vDesc}:
"; + if (preg_match('/title|message/', ${vDesc})) { + echo ""; + } else { + echo ""; + } + echo "
"; + if ($vHelp) echo "
$vHelp
"; + } + echo "
 
"; + echo ""; + if (is_file($file)) { + echo ""; + echo "" : " disabled>"); + } + echo "
"; +} +?> diff --git a/plugins/dynamix/Notifications.page b/plugins/dynamix/Notifications.page new file mode 100644 index 000000000..6bce84ad5 --- /dev/null +++ b/plugins/dynamix/Notifications.page @@ -0,0 +1,291 @@ +Menu="UserPreferences" +Type="xmenu" +Title="Notification Settings" +Icon="notifications.png" +--- + + + + + + + + +
+ + + + + + + + + + + + +Date format: +: + +> Select the desired date format which is used in the notifications archive. Recommended is YYYY-MM-DD, which makes the date/time column sortable in a sensible way. + +Time format: +: + +> Select the desired time format which is used in the notifications archive. Recommended is 24 hours, which makes the date/time column sortable in a sensible way. + +Display position: +: + +> Choose the position of where notifications appear on screen. Multiple notifications are stacked, bottom-to-top or top-to-bottom depending on the selected placement. + +Store notifications to flash: +: + +> By default notifications are stored on RAM disk, which will get lost upon system reboot. +> Notifications may be stored permanently on the flash drive under folder '/boot/config/plugins/dynamix' instead. + +System notifications: +: + +> By default the notifications system is disabled. Enable it here to start receiving notifications. +> The following sections give more options about which and what type of notifications will be sent. + +Plugins version notification: +: + +> Start a periodic verification and notify the user when a new version of one or more of the installed plugins is detected. +> Use the checkboxes below to select how notifications need to be given; by browser, by email and/or by custom agent. + +Docker update notification: +: + +> Start a periodic verification and notify the user when a new version of one or more of the installed dockers is detected. +> Use the checkboxes below to select how notifications need to be given; by browser, by email and/or by custom agent. + +Array status notification: +: + +> Start a periodic array health check (preventive maintenance) and notify the user the result of this check. + + +: + + +: + + +: + +> Use the checkboxes above to select what and how notifications need to be given; by browser, by email and/or by a service. +>
Tip: you can use custom notification agents; just add them to "/boot/config/plugins/dynamix/notification/agents" directory and check 'Agents'. + +Notification entity: +: Notices + >Browser   + >Email   + >Agents   + +  +: Warnings + >Browser   + >Email   + >Agents   + +  +: Alerts + >Browser   + >Email   + >Agents   + +> Notifications are classified as: +> +> *notice* - these are informative notifications and do not indicate a problem situation, e.g. a new version is available
+> *warning* - these are attentive notifications and may indicate future problems, e.g. a hard disk is hotter than usual
+> *alert* - these are serious notifications and require immediate attention, e.g. a failing hard disk
+> +> Choose for each classification how you want to be notified. + +
+SMART attribute notifications: +: >5Reallocated sectors count + +  +: >187Reported uncorrectable errors + +  +: >188Command timeout + +  +: >197Current pending sector count + +  +: >198Uncorrectable sector count + +  +: Custom attribute number +
+ +
+> The user can enable or disable notifications for the given SMART attributes. It is recommended to keep the default, which is ALL selected attributes, but when certain attributes are not present on your hard disk, these may be excluded. +> In addition a custom SMART attribute number can be given to generate notifications. Be careful in the selection, this may cause an avalance of notifcations if an inappropriate SMART attribute is chosen. +
+ + +: +
diff --git a/plugins/dynamix/NotificationsArchive.page b/plugins/dynamix/NotificationsArchive.page new file mode 100644 index 000000000..17cac5582 --- /dev/null +++ b/plugins/dynamix/NotificationsArchive.page @@ -0,0 +1,48 @@ +Menu="Notifications:4" +Title="Archived Notifications" +--- + + + + + + + + +
TimeEventSubjectDescriptionImportance
+ diff --git a/plugins/dynamix/OpenDevices.page b/plugins/dynamix/OpenDevices.page new file mode 100644 index 000000000..9c418ab6e --- /dev/null +++ b/plugins/dynamix/OpenDevices.page @@ -0,0 +1,53 @@ +Menu="Main:4" +Title="Unassigned Devices" +Cond="((count($devs)>0)&&($var['fsState']=='Started'))" +--- + + + + + + + + +"; +endforeach; +?> + +
DeviceIdentificationTemp.ReadsWritesErrorsFSSizeUsedFreeView
 
+ +> Thse are devices installed in your server but not assigned to either the parity-proteced +> array or the cache disk/pool. diff --git a/plugins/dynamix/OtherSettings.page b/plugins/dynamix/OtherSettings.page new file mode 100644 index 000000000..844bdb782 --- /dev/null +++ b/plugins/dynamix/OtherSettings.page @@ -0,0 +1,3 @@ +Menu="Settings:1" +Type="menu" +Title="System Settings" \ No newline at end of file diff --git a/plugins/dynamix/PageMap.page b/plugins/dynamix/PageMap.page new file mode 100644 index 000000000..1d1e7a2c4 --- /dev/null +++ b/plugins/dynamix/PageMap.page @@ -0,0 +1,41 @@ +Menu="WebGui" +Title="Page Map" +--- + + +"; + foreach ($pages as $page) { + $link="{$page['name']}"; + if ($page['Type'] == "menu") { + echo "{$level} ({$link}) - {$page['Title']}
"; + } else if ($page['Type'] == "xmenu") { + echo "{$level} [{$link}] - {$page['Title']}
"; + } else { + echo "{$level} {$link} - {$page['Title']}
"; + } + show_map($page['name'], $level+1); + } + echo ""; +} +echo "
"; +show_map("Tasks", 1); +show_map("Buttons", 1); +echo "
"; +?> + \ No newline at end of file diff --git a/plugins/dynamix/ParityCheck.page b/plugins/dynamix/ParityCheck.page new file mode 100644 index 000000000..4f2e50091 --- /dev/null +++ b/plugins/dynamix/ParityCheck.page @@ -0,0 +1,190 @@ +Menu="Scheduler:1" +Title="Parity Check" +Cond="($disks['parity']['status']!='DISK_NP_DSBL')" +--- + + + + +
+ + + +Scheduled parity check: +: + +> By default no parity check is scheduled. Select here the desired schedule. This can be one of the preset schedules for daily, weekly, monthly, yearly or a custom schedule. + +Day of the week: + +: + + + + +: + +> When a weekly or custom schedule is selected then choose here the preferred day of the week, otherwise this setting is unavailable. + + +Day of the month: + +Week of the month: + +: + +> When a monthly, yearly or custom schedule is selected then choose here the preferred day of the month, otherwise this setting is unavailable. + +Time of the day: +: + +> Choose the desired time to start the schedule. Granularity is given in half hour periods. + +Month of the year: +=4):?> + +: + + + + + +: + +> When a yearly or custom schedule is selected then choose here the preferred month of the year, otherwise this setting is unavailable. + +Write corrections to parity disk: +: + +> Choose here whether any parity errors found during the check, need to be corrected on the parity disk or not. + +  +: +
diff --git a/plugins/dynamix/Processes.page b/plugins/dynamix/Processes.page new file mode 100644 index 000000000..6d08c80e1 --- /dev/null +++ b/plugins/dynamix/Processes.page @@ -0,0 +1,20 @@ +Menu="UNRAID-OS" +Title="Processes" +--- + + +".shell_exec('ps -ef').""; +?> + \ No newline at end of file diff --git a/plugins/dynamix/README.md b/plugins/dynamix/README.md new file mode 100644 index 000000000..a0d352d76 --- /dev/null +++ b/plugins/dynamix/README.md @@ -0,0 +1,5 @@ +**Dynamix webGui** + +The *Dynamix* webGui is the latest iteration of the unRAID Server OS System +Management Utility. Built upon *Simple Features*, it provides real-time +screen updates, tabbed viewing and many more enhancements. diff --git a/plugins/dynamix/Registration.page b/plugins/dynamix/Registration.page new file mode 100644 index 000000000..df408c8c2 --- /dev/null +++ b/plugins/dynamix/Registration.page @@ -0,0 +1,252 @@ +Menu="About" +Title="Registration" +Type="xmenu" +--- + + + + + + +Flash GUID: +: **Error** - Contact Support + + + + + +Flash Vendor: +: + +Flash Product: +: + +Flash GUID: +: + +  +: **Blacklisted** - Contact Support + + + + +
+ + +The registration key GUID does not match the USB Flash boot device GUID + +Flash GUID: +: + + GUID: +: + +Registered to: +: + +Date Registered: +: + + + +Replaceable: +: *Anytime* + +  +: + + + +Replaceable: +:   + +  +: + + +
+ + + +
+ + +Thank you for trying unRAID Server OS! + +
+Your server will not be usable until you download a *registration key*. +Registration keys are bound to your USB Flash boot device GUID (serial number). +Here you may obtain a **free** *Trial* registration key, which is valid for 30 days and supports up to 3 storage devices. +To support more storage devices, you may purchase a *Basic*, *Plus*, or *Pro* registration key. + +Important: + ++ Please make sure your [server time](DateTime) is accurate to within 5 minutes. ++ Please make sure there is a [DNS server](NetworkSettings) specified. +
+ +Flash GUID: +: + +  +: + +
+ + + +
+ + +Thank you for trying unRAID Server OS! + +
+Your *Trial* key has expired. + +To continue using unRAID Server OS and expand your server to support more devices, you may purchase a *Basic*, *Plus*, or *Pro* registration key. +Alternately, you may request a *Trial* extension key. + +**Note:** most *Trial* extension requests are processed immediately but please allow up to one business day to receive your *Trial* extension key. +
+ +Flash GUID: +: + +  +: + +
+ + + +
+ + +Thank you for trying unRAID Server OS! + +
+Your *Trial* key allows you to attach up to 3 storage devices. One device *must* be assigned to a *Data* disk slot, whereas +the remaining two devices may be assigned any way you choose. +
+ +***Trial*** key expires on: +:   + +Flash GUID: +: + +  +: + +
+ + + + +Thank you for choosing unRAID Server OS! + + registered to: +: + +Date Registered: +: + + + +Replaceable: +: *Anytime* + + + +Replaceable: +:   + + +
+ + +Flash GUID: +: + +  +: + +
+ + + + +Thank you for choosing unRAID Server OS! + + registered to: +: + +Date Registered: +:   + + + +Replaceable: +: *Anytime* + + + +Replaceable: +:   + + +
+ + +Flash GUID: +: + +  +: + +
+ + + + +Thank you for choosing unRAID Server OS! + + registered to: +: + +Date Registered: +:   + + + +Replaceable: +: *Anytime* + + + +Replaceable: +:   + + + +Flash GUID: +: + + + +  +: diff --git a/plugins/dynamix/SMB.page b/plugins/dynamix/SMB.page new file mode 100644 index 000000000..4c2ace5be --- /dev/null +++ b/plugins/dynamix/SMB.page @@ -0,0 +1,4 @@ +Menu="NetworkServices:3" +Type="xmenu" +Title="SMB" +Icon="windows-logo.png" \ No newline at end of file diff --git a/plugins/dynamix/SMBActiveDirectory.page b/plugins/dynamix/SMBActiveDirectory.page new file mode 100644 index 000000000..69a497845 --- /dev/null +++ b/plugins/dynamix/SMBActiveDirectory.page @@ -0,0 +1,51 @@ +Menu="SMB:3" +Title="Active Directory Settings" +Cond="($var['shareSMBEnabled']=='ads')" +--- + +
+AD join status: +:   + +AD domain name (FQDN): +: + +AD short domain name: +: + +AD account login: +: + +AD account password: +: + +  +: + + + + +
+
+ +
+AD initial owner: +: + +AD initial group: +: + +  +: +
\ No newline at end of file diff --git a/plugins/dynamix/SMBWorkGroup.page b/plugins/dynamix/SMBWorkGroup.page new file mode 100644 index 000000000..7b8dd8b15 --- /dev/null +++ b/plugins/dynamix/SMBWorkGroup.page @@ -0,0 +1,34 @@ +Menu="SMB:2" +Title="Workgroup Settings" +Cond="($var['shareSMBEnabled']=='yes')" +--- + +
+Workgroup: +: + +> Enter your local network Workgroup name. Usually this is "WORKGROUP". + +Local master: +: + +> If set to 'Yes' then the server will fully participate in browser elections, and in the absense +> of other servers, will usually become the local Master Browser. + +  +: +
\ No newline at end of file diff --git a/plugins/dynamix/SMBsettings.page b/plugins/dynamix/SMBsettings.page new file mode 100644 index 000000000..d9ee66dea --- /dev/null +++ b/plugins/dynamix/SMBsettings.page @@ -0,0 +1,49 @@ +Menu="SMB:1" +Title="SMB Settings" +--- + +
+ +Enable SMB: +: + +> Select 'Yes (Workgroup)' to enable SMB (Windows Networking) protocol support. This +> also enables Windows host discovery. +> +> Select 'Yes (Active Directory)' to enable Active Directory integration. + +Hide "dot" files: +: + +> If set to 'Yes' then files starting with a '.' (dot) will appear as *hidden files* and normally +> will not appear in Windows folder lists unless you have "Show hidden files, folders, and drives" enabled +> in Windows Folder Options. + +> If set to 'No' then dot files will appear in folder lists the same as any other file. + + +  +: Array must be **Stopped** to change + +  +: + +
diff --git a/plugins/dynamix/Scheduler.page b/plugins/dynamix/Scheduler.page new file mode 100644 index 000000000..50b0f2a83 --- /dev/null +++ b/plugins/dynamix/Scheduler.page @@ -0,0 +1,4 @@ +Menu="UserPreferences" +Type="xmenu" +Title="Scheduler" +Icon="scheduler.png" diff --git a/plugins/dynamix/SecurityAFP.page b/plugins/dynamix/SecurityAFP.page new file mode 100644 index 000000000..41e0262af --- /dev/null +++ b/plugins/dynamix/SecurityAFP.page @@ -0,0 +1,116 @@ +Menu="Disk Share" +Title="AFP Security Settings" +Cond="(($var['shareAFPEnabled']!='no') && (isset($name)?array_key_exists($name,$sec_afp):0))" +--- + + + +> This section is used to configure the security settings for the share when accessed using AFP and +> appears only when AFP is enabled on the Network Services page. + +
+ + +Share name: +: + +Export: +: + +> The Export setting determines whether this share is exported via AFP (Yes or No) +> The Export setting also includes a third option (Yes - TimeMachine). This setting enables various +> special options for TimeMachine; in particular a "volume size limit". Note: Apple recommends not +> to use the volume for anything but TimeMachine due to the way locks are used. + +TimeMachine volume size limit: +: MB + +> This limits the reported volume size, preventing TimeMachine from using the entire real disk space +> for backup. For example, setting this value to "1024" would limit the reported disk space to 1GB. + +Volume dbpath: +: + +> Sets where to store netatalk database information. A directory with same name as the share will be +> created here. +> +> Leave this field blank to have the database created in the root of the share. + +Security: +: + +> The unRAID AFP implementation supports Guest access and fully supports the three security +> modes: Public, Secure, and Private. +> In general, when you click on your server's icon in Finder, you will be asked to log in as Guest or to +> specify a set of login credentials (user name/password). In order to use Secure or Private security on +> a share, you must have a user already defined on the server with appropriate access rights. +> +> Note: netatalk does not permit the user name root to be used for log in purposes. +> +> **Public** When logged into the server as Guest, an OS X user can view and read/write all shares set as +> Public. Files created or modified in the share will be owned by user `nobody` of +> the `users` group.
+> OSX users logged in with a user name/password previously created on the server can also view +> and read/write all shares set as Public. In this case, files created or modified on the server will +> be owned by the logged in user. +> +> **Secure** When logged into the server as Guest, an OS X user can view and read (but not write) all +> shares set as Secure.
+> OS X users logged in with a user name/password previously created on the server can also view and +> read all shares set as Secure. If their access right is set to read/write for the share on the server, +> they may also write the share. +> +> **Private** When logged onto the server as Guest, no Private shares are visible or accessible to any +> OS X user.
+> OS X users logged in with a user name/password previously created on the server may read or +> read/write (or have no access) according their access right for the share on the server. + +  +: +
+ + +
+ +
User Access
Guests have read-only access.
+ + +  +: +
+ + +
+ +
User Access
Guests have no access.
+ + +  +: +
+ diff --git a/plugins/dynamix/SecurityNFS.page b/plugins/dynamix/SecurityNFS.page new file mode 100644 index 000000000..a686cf0d2 --- /dev/null +++ b/plugins/dynamix/SecurityNFS.page @@ -0,0 +1,49 @@ +Menu="Disk Share" +Title="NFS Security Settings" +Cond="(($var['shareNFSEnabled']!='no') && (isset($name)?array_key_exists($name,$sec_nfs):0))" +--- + +
+ + +Share name: +: + +Export: +: + +Security: +: + +  +: +
+ + +
+ +Rule: +: + +  +: +
+ diff --git a/plugins/dynamix/SecuritySMB.page b/plugins/dynamix/SecuritySMB.page new file mode 100644 index 000000000..21c60f5cf --- /dev/null +++ b/plugins/dynamix/SecuritySMB.page @@ -0,0 +1,73 @@ +Menu="Disk Share Flash" +Title="SMB Security Settings" +Cond="(($var['shareSMBEnabled']!='no') && (isset($name)?array_key_exists($name,$sec):0))" +--- + +
+ + +Share name: +: + +Export: +: + +> This setting determines whether the share is visible and/or accessible. The 'Yes (hidden)' setting +> will *hide* the share from *browsing* but is still accessible if you know the share name. + +Security: +: + +> Summary of security modes: +> +> **Public** All users including guests have full read/write access. +> +> **Secure** All users including guests have read access, you select which of your users +> have write access. +> +> **Private** No guest access at all, you select which of your users have read/write or +> read-only access. + +  +: +
+ + +
+ +
User Access
Guests have read-only access.
+ + +  +: +
+ + +
+ +
User Access
Guests have no access.
+ + +  +: +
+ diff --git a/plugins/dynamix/Settings.page b/plugins/dynamix/Settings.page new file mode 100644 index 000000000..9e105cf63 --- /dev/null +++ b/plugins/dynamix/Settings.page @@ -0,0 +1,3 @@ +Menu="Tasks:4" +Type="xmenu" +Tabs="false" \ No newline at end of file diff --git a/plugins/dynamix/Share.page b/plugins/dynamix/Share.page new file mode 100644 index 000000000..8a9998245 --- /dev/null +++ b/plugins/dynamix/Share.page @@ -0,0 +1,40 @@ +Type="xmenu" +--- + + +0 ? $refs[$i-1] : $refs[$end]); +$next = urlencode($i<$end ? $refs[$i+1] : $refs[0]); +?> + + diff --git a/plugins/dynamix/ShareEdit.page b/plugins/dynamix/ShareEdit.page new file mode 100644 index 000000000..cc5598abb --- /dev/null +++ b/plugins/dynamix/ShareEdit.page @@ -0,0 +1,283 @@ +Menu="Share:1" +Title="Share Settings" +--- + + + "", + "name" => "", + "comment" => "", + "allocator" => "highwater", + "floor" => "", + "splitLevel" => "", + "include" => "", + "exclude" => "", + "useCache" => "no", + "cow" => "auto"); +} else if (array_key_exists($name, $shares)) { + /* edit existing share */ + $share = $shares[$name]; +} else { + /* handle share deleted case */ + echo "

Share $name has been deleted.

"; + return; +} + +/* check for empty share */ +function shareEmpty($name) { + return (($files = @scandir('/mnt/user/'.$name)) && (count($files) <= 2)); +} + +if ($var['shareUserInclude']) { + $myDisks = explode(',',$var['shareUserInclude']); +} else { + $myDisks = array(); + foreach ($disks as $disk) $myDisks[] = $disk['name']; +} + +if ($var['shareUserExclude']) { + $exclude = explode(',',$var['shareUserExclude']); + foreach ($exclude as $disk) { + $index = array_search($disk,$myDisks); + if ($index !== false) array_splice($myDisks,$index,1); + } +} +?> + +> A *Share*, also called a *User Share*, is simply the name of a top-level directory that exists on one or more of your +> storage devices (array and cache). Each share can be exported for network access. When browsing a share, we return the +> composite view of all files and subdirectories for which that top-level directory exists on each storage device. + + +
+ + +Share name: +: + +> The share name can be up to 40 characters, and is case-sensitive with these restrictions: +> +> * cannot contain a double-quote character (") +> * cannot be one of the reserved share names: flash, cache, cach2, .., disk1, disk2, .. +> +> We highly recommend to make your life easier and avoid special characters. + +Comments: +: + +> Anything you like, up to 256 characters. + +Allocation method: +: + +> This setting determines how unRAID OS will choose which disk to use when creating a new file or directory: +> +> **High-water** +> Choose the lowest numbered disk with free space still above the current *high water mark*. The +> *high water mark* is initialized with the size of the largest data disk divided by 2. If no disk +> has free space above the current *high water mark*, divide the *high water mark* by 2 and choose again. +> +> The goal of **High-water** is to write as much data as possible to each disk (in order to minimize +> how often disks need to be spun up), while at the same time, try to keep the same amount of free space on +> each disk (in order to distribute data evenly across the array). +> +> **Fill-up** +> Choose the lowest numbered disk that still has free space above the current **Minimum free space** +> setting. +> +> **Most-free** +> Choose the disk that currently has the most free space. + +Minimum free space: +: + +> The *minimum free space* available to allow writing to any disk belonging to the share.
+> +> Choose a value which is equal or greater than the biggest single file size you intend to copy to the share. +> Include units KB, MB, GB and TB as appropriate, e.g. 10MB. + +Split level: +: + +> Determines whether a directory is allowed to expand onto multiple disks. + +> **Automatically split any directory as required** +> When a new file or subdirectory needs to be created in a share, unRAID OS first chooses which disk +> it should be created on, according to the configured *Allocation method*. If the parent directory containing +> the new file or or subdiretory does not exist on this disk, then unRAID OS will first create all necessary +> parent directories, and then create the new file or subdirectory. +> +> **Automatically split only the top level directory as required** +> When a new file or subdirectory is being created in the first level subdirectory of a share, if that first +> level subdirectory does not exist on the disk being written, then the subdirectory will be created first. +> If a new file or subdirectory is being created in the second or lower level subdirectory of a share, the new +> file or subdirectory is created on the same disk as the new file or subdirectorys parent directory. +> +> **Automatically split only the top "N" level directories as required** +> Similar to previous: when a new file or subdirectory is being created, if the parent directory is at level "N", +> and does not exist on the chosen disk, unRAID OS will first create all necessary parent directories. If the +> parent directory of the new file or subdirectory is beyond level "N", then the new file or subdirectory is +> created on the same disk where the parent directory exists. +> +> **Manual: do not automatically split directories** +> When a new file or subdirectory needs to be created in a share, unRAID OS will only condider disks where the +> parent directory already exists. + +Included disk(s): +: + +> Specify the disks which can be used by the share. By default all disks are included; that is, if specific +> disks are not selected here, then the share may expand into *all* array disks. + +Excluded disk(s): +: + +> Specify the disks which can *not* be used by the share. By default no disks are excluded. + + + +Use cache disk: +: + +> Specify whether new files and subdirectories written on the share can be written onto the Cache disk/pool. +> +> **No** prohibits new files and subdirectories from being written onto the Cache disk/pool. +> +> **Yes** indicates that all new files and subdirectories should be written to the Cache disk/pool, provided +> enough free space exists on the Cache disk/pool. If there is insufficant space on the Cache disk/pool, then +> new files and directories are created on the array. When the *mover* is invoked, files and subdirectories are +> transfered off the Cache disk/pool and onto the array. +> +> **Only** indicates that all new files and subdirectories must be writen to the Cache disk/pool. If there +> is insufficient free space on the Cache disk/pool, *create* operations will fail with *out of space* status. + + + +Enable Copy-on-write: +: Set when adding new share only. + +> Set to **No** to cause the *btrfs* NOCOW (No Copy-on-Write) attribute to be set on the share directory +> when created on a device formatted with *btrfs* file system. Once set, newly created files and +> subdirectories on the device will inherit the NOCOW attribute. We recommend this setting for shares +> used to store vdisk images, including the Docker loopback image file. This setting has no effect +> on non-btrfs file systems. +> +> Set to **Auto** for normal operation, meaning COW will be in effect on devices formatted with *btrfs*. + + +  +: + +Share empty? +: Yes + +Delete +: + +Share empty? +: No + +  +: + +
diff --git a/plugins/dynamix/ShareList.page b/plugins/dynamix/ShareList.page new file mode 100644 index 000000000..0a3150eb9 --- /dev/null +++ b/plugins/dynamix/ShareList.page @@ -0,0 +1,103 @@ +Menu="Shares:1" +Title="User Shares" +Cond="$var['fsState']=="Started" && $var['shareUser']=='e'" +--- + + +'.ucfirst($share['security']).'
'; +} +// Share size per disk +$preserve = $path==$prev; +$ssz1 = array(); +foreach (glob("state/*.ssz1", GLOB_NOSORT) as $entry) { + if ($preserve) { + $ssz1[basename($entry, ".ssz1")] = parse_ini_file($entry); + } else { + unlink($entry); + } +} +?> + + + + $share): + $ball = "/webGui/images/{$share['color']}.png"; + switch ($share['color']) { + case 'green-on': $help = 'All files protected'; break; + case 'yellow-on': $help = 'Some or all files unprotected'; break; + } +?> + + + + + + + + + + + $disk_size): + if ($disk_name!="total"): +?> + + + + + + + + + + + + + + + + + +
There are no user shares
+ +
+ + +

':' disabled>User shares must be enabled to add shares.'?>

+ +

+ +
+ +> **Colored Status Indicator** the significance of the color indicator at the beginning of each line in *User Shares* is as follows: +> +> All files are on protected storage. +> +> Some or all files are on unprotected storage. +> +> SMB security mode displayed in *italics* indicates exported hidden shares. +> +> AFP security mode displayed in *italics* indicates exported time-machine shares. diff --git a/plugins/dynamix/ShareSettings.page b/plugins/dynamix/ShareSettings.page new file mode 100644 index 000000000..d5b9b6cbf --- /dev/null +++ b/plugins/dynamix/ShareSettings.page @@ -0,0 +1,117 @@ +Menu="OtherSettings" +Type="xmenu" +Title="Global Share Settings" +Icon="share-settings.png" +--- + + + + + +
+ +Enable disk shares: +: + +> If set to No, disk shares are unconditionally not exported. +> +> If set to Yes, disk shares may be exported. **WARNING:** Do not copy data from a disk share to a user share +> unless you *know* what you are doing. This may result in the loss of data and is not supported. +> +> If set to Auto, only disk shares not participating in User Shares may be exported. + +Enable user shares: +: + +> If set to 'Yes' the User Shares feature is activated. + +Included disk(s): +: + +> This setting defines the set of array disks which are *included* in User Shares. +> Set this field to *blank* in order to allow **all** array disks to be included. +> +> To specify a set of disks name them like this: +> * disk1,disk2,disk3 *disk names seperated by commas* +> * disk1-3 *a range of disks* +> * disk2,disk4-6,disk10 *another example* + +Excluded disk(s): +: + +> This setting defines the set of array disk which are *excluded* from User Shares. Set this +> field to *blank* in order to not exclude any disks; otherwise, set this field as above to define the set +> of disks to exclude. +> +> **Note:** Each separate User Share also includes its own set of Included and Excluded disks which represent +> a subset of the Included/Excluded disks defined here. + +  +: >Array must be **Stopped** to change + +
diff --git a/plugins/dynamix/Shares.page b/plugins/dynamix/Shares.page new file mode 100644 index 000000000..5eee54cdc --- /dev/null +++ b/plugins/dynamix/Shares.page @@ -0,0 +1,10 @@ +Menu="Tasks:2" +Type="xmenu" +---- +Array must be started to view Shares.

"; + return; +} +if (count($pages)==2) $tabbed = false; +?> diff --git a/plugins/dynamix/SmtpSettings.page b/plugins/dynamix/SmtpSettings.page new file mode 100644 index 000000000..9ebee5c2e --- /dev/null +++ b/plugins/dynamix/SmtpSettings.page @@ -0,0 +1,190 @@ +Menu="Notifications:2" +Title="SMTP Settings" +--- + + + + + +
+ + + + +Preset service: +: + +> Select a preset service to set the basic service settings. + +Sending email address: +: + +> Email address of your mail account. This address is used as sender of the notifications. + +Email recipients: +: + +> Recipients of status and error notifications. Specify one or more email addresses, separate multiple email addresses with a space. + +Priority in header: +: + +> Set email header with high importance, when there is a problem with unRaid. + +Email subject prefix: +: + +> Set a prefix for easy recognition of unRAID messages. + +Mail server: +: + +> Specify the name of the email server. Use the preset service selection to have this filled-in automatically. + +Mail server port: +: + +> Specify the port of the email server. Use the preset service selection to have this filled-in automatically. + +Use SSL/TLS: +: + +> Specifies whether to use SSL/TLS to talk to the SMTP server. + +Use STARTTLS: +: + +> Specifies whether to use STARTTLS before starting SSL negotiation - See RFC 2487. + +Define a TLS certificate: +: + +> Select only when you have a certificate which required for communication. + +TLS certificate location: +: + +> The file name of an RSA certificate to use for TLS - as required. + +Authentication method: +: + +> Select the correct authentication method for your email server. Use test to verify that access is working properly. + +Username: +: + +Password: +: + +> Enter the username and password to login to your email account. Be aware that the password is stored unencrypted in the email configuration file. + +  +: + disabled> +
diff --git a/plugins/dynamix/SysDevs.page b/plugins/dynamix/SysDevs.page new file mode 100644 index 000000000..3c37abd20 --- /dev/null +++ b/plugins/dynamix/SysDevs.page @@ -0,0 +1,51 @@ +Menu="UNRAID-OS" +Title="System Devices" +--- + +**PCI Devices** + +> This displays the output of the `lspci` command. The numeric identifiers are used to configure PCI pass-through. + +
+ +**IOMMU Groups** + +> This displays a list of IOMMU groups available on your system. + + +

Warning: Your system has booted with the PCIe ACS Override setting enabled. The below list doesn't not reflect the way IOMMU would naturally group devices. To see natural IOMMU groups for your hardware, go to the VM Settings page and set the PCIe ACS Override setting to No.

+ +
+ +**USB Devices** + +> This displays the output of the `lsusb` command. The numeric identifiers are used to configure PCI pass-through. + +
+ +**SCSI Devices** + +> This displays the output of the `lsscsi` command. The numeric identifiers are used to configure PCI pass-through. +> +> Note that linux groups ATA, SATA and SAS devices with true SCSI devices. + +
+ diff --git a/plugins/dynamix/Syslinux.page b/plugins/dynamix/Syslinux.page new file mode 100644 index 000000000..f7de1c0db --- /dev/null +++ b/plugins/dynamix/Syslinux.page @@ -0,0 +1,47 @@ +Menu="Flash" +Title="Syslinux Configuration" +--- + +> Use this page to make changes to your `syslinux.cfg` file. You will +> need to reboot your server for these changes to take effect. + + + +
+ + +Syslinux configuration: + +: + + +: + +> Click the **Apply** button to commit the current edits. Click **Reset** to +> undo any changes you make (before Saving). Click **Done** to exit this page. +> +> Click the **Default** button to initialize the edit box with the +> factory-default contents. You still need to click **Apply** in order to +>commit the change. + +
diff --git a/plugins/dynamix/Syslog.page b/plugins/dynamix/Syslog.page new file mode 100644 index 000000000..38f75d374 --- /dev/null +++ b/plugins/dynamix/Syslog.page @@ -0,0 +1,39 @@ +Menu="UNRAID-OS" +Title="System Log" +--- + + +".shell_exec('cat /var/log/syslog').""; +?> + + diff --git a/plugins/dynamix/Tools.page b/plugins/dynamix/Tools.page new file mode 100644 index 000000000..99a97c719 --- /dev/null +++ b/plugins/dynamix/Tools.page @@ -0,0 +1,3 @@ +Menu="Tasks:90" +Type="xmenu" +Tabs="false" \ No newline at end of file diff --git a/plugins/dynamix/UNRAID-OS.page b/plugins/dynamix/UNRAID-OS.page new file mode 100644 index 000000000..fc25d22bc --- /dev/null +++ b/plugins/dynamix/UNRAID-OS.page @@ -0,0 +1,3 @@ +Menu="Tools:10" +Type="menu" +Title="unRAID OS" \ No newline at end of file diff --git a/plugins/dynamix/UserAdd.page b/plugins/dynamix/UserAdd.page new file mode 100644 index 000000000..53c2c8e67 --- /dev/null +++ b/plugins/dynamix/UserAdd.page @@ -0,0 +1,146 @@ +Menu="UserList" +Title="Add User" +--- + + +"; +$icon = ""; +?> + + + + + + +
+User name: +: + +> Usernames may be up to 32 characters long and must start with a **lower case letter** or an underscore, +> followed by **lower case letters**, digits, underscores, or dashes. They can end with a dollar sign. +> In regular expression terms: `[a-z_][a-z0-9_-]*[$]?` + +Description: +: + +> Up to 64 characters. + +Custom image: +: Drag-n-drop a PNG file or click the image at the left. + +> The image will be scaled to 48x48 pixels in size. The maximum image file upload size is 95 kB (97,280 bytes). + +Password: +: + +> Up to 40 characters. + +Retype password: +: + +  +: +
diff --git a/plugins/dynamix/UserEdit.page b/plugins/dynamix/UserEdit.page new file mode 100644 index 000000000..f14f720ed --- /dev/null +++ b/plugins/dynamix/UserEdit.page @@ -0,0 +1,155 @@ +Menu="UserList" +Title="Edit User" +--- + + + +

User has been deleted.


+ + + + +"; +$icon = ""; +?> + + + + + + +
+ +User name: +: + +Description: +: + +Custom image: +: + + + + + + Drag-n-drop a PNG file or click the image at the left. + +> The image will be scaled to 48x48 pixels in size. The maximum image file upload size is 95 kB (97,280 bytes). + + +  + +Delete + +: )"> +
+

+
+ +Password: +: + +Retype password: +: + +  +: +
diff --git a/plugins/dynamix/UserList.page b/plugins/dynamix/UserList.page new file mode 100644 index 000000000..cc32c92df --- /dev/null +++ b/plugins/dynamix/UserList.page @@ -0,0 +1,23 @@ +Menu="Users" +Title="Users" +--- + + + +

+ +
+
+ +
diff --git a/plugins/dynamix/UserPreferences.page b/plugins/dynamix/UserPreferences.page new file mode 100644 index 000000000..43b826f22 --- /dev/null +++ b/plugins/dynamix/UserPreferences.page @@ -0,0 +1,3 @@ +Menu="Settings:3" +Type="menu" +Title="User Preferences" \ No newline at end of file diff --git a/plugins/dynamix/Users.page b/plugins/dynamix/Users.page new file mode 100644 index 000000000..5ca8184e2 --- /dev/null +++ b/plugins/dynamix/Users.page @@ -0,0 +1,2 @@ +Menu="Tasks:3" +Type="xmenu" \ No newline at end of file diff --git a/plugins/dynamix/Utilities.page b/plugins/dynamix/Utilities.page new file mode 100644 index 000000000..81e274e8d --- /dev/null +++ b/plugins/dynamix/Utilities.page @@ -0,0 +1,3 @@ +Menu="Settings" +Title="User Utilities" +Type="menu" \ No newline at end of file diff --git a/plugins/dynamix/Vars.page b/plugins/dynamix/Vars.page new file mode 100644 index 000000000..33755e1e5 --- /dev/null +++ b/plugins/dynamix/Vars.page @@ -0,0 +1,26 @@ +Menu="WebGui" +Title="Vars" +--- + + + +
+ diff --git a/plugins/dynamix/WebGui.page b/plugins/dynamix/WebGui.page new file mode 100644 index 000000000..13001ae42 --- /dev/null +++ b/plugins/dynamix/WebGui.page @@ -0,0 +1,3 @@ +Menu="Tools:20" +Type="menu" +Title="webGUI" \ No newline at end of file diff --git a/plugins/dynamix/default.cfg b/plugins/dynamix/default.cfg new file mode 100644 index 000000000..f477ef241 --- /dev/null +++ b/plugins/dynamix/default.cfg @@ -0,0 +1,61 @@ +[confirm] +down="1" +stop="1" +[display] +date="%c" +time="%R" +number=".," +unit="C" +scale="-1" +align="right" +view="" +total="1" +banner="image" +dashapps="icons" +tabs="1" +usage="0" +text="1" +warning="70" +critical="90" +hot="45" +max="55" +theme="white" +refresh="1000" +[parity] +mode="0" +hour="0 0" +dotm="1" +month="1" +day="0" +cron="" +write="" +[notify] +date="d-m-Y" +time="H:i" +position="top-right" +path="/tmp/notifications" +system="" +entity="1" +normal="1" +warning="1" +alert="1" +plugin="1" +report="1" +version="" +status="" +events="5|187|188|197|198" +custom="" +[ssmtp] +root="" +RcptTo="" +SetEmailPriority="True" +Subject="unRAID Status: " +server="smtp.gmail.com" +port="465" +UseTLS="YES" +UseSTARTTLS="NO" +UseTLSCert="NO" +TLSCert="" +AuthMethod="login" +AuthUser="" +AuthPass="" diff --git a/plugins/dynamix/dynamix.plg b/plugins/dynamix/dynamix.plg new file mode 100644 index 000000000..e35aff869 --- /dev/null +++ b/plugins/dynamix/dynamix.plg @@ -0,0 +1,63 @@ + + + + + + +]> + + + + + +Dynamix webGui v&version; +------------------------- + +Please refer to +**<a href="https://github.com/limetech/dynamix/commits/master" target="_blank">commit history</a>** +on github. + + + + +"https://github.com/limetech/&name;/archive/&version;.tar.gz" + + + + + +rm -r /tmp/plugins/&name;-&version; 2>/dev/null +tar -xf /boot/config/plugins/&name;/&name;-&version;.tar.gz -C /tmp/plugins +mv /tmp/plugins/&name;-&version;/* /usr/local/emhttp 2>/dev/null +for Plugin in /tmp/plugins/&name;-&version;/plugins/* ; do + rm -r /usr/local/emhttp/plugins/$(basename $Plugin) 2>/dev/null + mv $Plugin /usr/local/emhttp/plugins/$(basename $Plugin) +done +rm -r /tmp/plugins/&name;-&version; +find /boot/config/plugins/&name; -type f -iname "*.tar.gz" ! -iname "&name;-&version;.tar.gz" -delete + + + + + + +rm -r /boot/config/plugins/&name;/&name;-&version;.tar.gz 2>/dev/null +echo "*********************" +echo "Please reboot server." +echo "*********************" + + + + diff --git a/plugins/dynamix/icons/about.png b/plugins/dynamix/icons/about.png new file mode 100644 index 000000000..92d0a7051 Binary files /dev/null and b/plugins/dynamix/icons/about.png differ diff --git a/plugins/dynamix/icons/activedirectorysettings.png b/plugins/dynamix/icons/activedirectorysettings.png new file mode 100644 index 000000000..f4d9748f5 Binary files /dev/null and b/plugins/dynamix/icons/activedirectorysettings.png differ diff --git a/plugins/dynamix/icons/adduser.png b/plugins/dynamix/icons/adduser.png new file mode 100644 index 000000000..292fcc1f0 Binary files /dev/null and b/plugins/dynamix/icons/adduser.png differ diff --git a/plugins/dynamix/icons/afp.png b/plugins/dynamix/icons/afp.png new file mode 100644 index 000000000..3234924ad Binary files /dev/null and b/plugins/dynamix/icons/afp.png differ diff --git a/plugins/dynamix/icons/afpsecuritysettings.png b/plugins/dynamix/icons/afpsecuritysettings.png new file mode 100644 index 000000000..3234924ad Binary files /dev/null and b/plugins/dynamix/icons/afpsecuritysettings.png differ diff --git a/plugins/dynamix/icons/apps.png b/plugins/dynamix/icons/apps.png new file mode 100644 index 000000000..3bc0bd32f Binary files /dev/null and b/plugins/dynamix/icons/apps.png differ diff --git a/plugins/dynamix/icons/archivednotifications.png b/plugins/dynamix/icons/archivednotifications.png new file mode 100644 index 000000000..9119aa7d5 Binary files /dev/null and b/plugins/dynamix/icons/archivednotifications.png differ diff --git a/plugins/dynamix/icons/arraydevices.png b/plugins/dynamix/icons/arraydevices.png new file mode 100644 index 000000000..bce0e7bd8 Binary files /dev/null and b/plugins/dynamix/icons/arraydevices.png differ diff --git a/plugins/dynamix/icons/arrayoperation.png b/plugins/dynamix/icons/arrayoperation.png new file mode 100644 index 000000000..2ac2a6278 Binary files /dev/null and b/plugins/dynamix/icons/arrayoperation.png differ diff --git a/plugins/dynamix/icons/attributes.png b/plugins/dynamix/icons/attributes.png new file mode 100644 index 000000000..1c7d26a04 Binary files /dev/null and b/plugins/dynamix/icons/attributes.png differ diff --git a/plugins/dynamix/icons/balancestatus.png b/plugins/dynamix/icons/balancestatus.png new file mode 100644 index 000000000..374e05cea Binary files /dev/null and b/plugins/dynamix/icons/balancestatus.png differ diff --git a/plugins/dynamix/icons/bootdevice.png b/plugins/dynamix/icons/bootdevice.png new file mode 100644 index 000000000..c2d68cc39 Binary files /dev/null and b/plugins/dynamix/icons/bootdevice.png differ diff --git a/plugins/dynamix/icons/boxcar.png b/plugins/dynamix/icons/boxcar.png new file mode 100644 index 000000000..7ffac0c65 Binary files /dev/null and b/plugins/dynamix/icons/boxcar.png differ diff --git a/plugins/dynamix/icons/cachedevices.png b/plugins/dynamix/icons/cachedevices.png new file mode 100644 index 000000000..08b9f7588 Binary files /dev/null and b/plugins/dynamix/icons/cachedevices.png differ diff --git a/plugins/dynamix/icons/cachedevicesettings.png b/plugins/dynamix/icons/cachedevicesettings.png new file mode 100644 index 000000000..08b9f7588 Binary files /dev/null and b/plugins/dynamix/icons/cachedevicesettings.png differ diff --git a/plugins/dynamix/icons/cachesettings.png b/plugins/dynamix/icons/cachesettings.png new file mode 100644 index 000000000..08b9f7588 Binary files /dev/null and b/plugins/dynamix/icons/cachesettings.png differ diff --git a/plugins/dynamix/icons/capabilities.png b/plugins/dynamix/icons/capabilities.png new file mode 100644 index 000000000..816a0c3f8 Binary files /dev/null and b/plugins/dynamix/icons/capabilities.png differ diff --git a/plugins/dynamix/icons/confirmations.png b/plugins/dynamix/icons/confirmations.png new file mode 100644 index 000000000..1c16683b9 Binary files /dev/null and b/plugins/dynamix/icons/confirmations.png differ diff --git a/plugins/dynamix/icons/credits.png b/plugins/dynamix/icons/credits.png new file mode 100644 index 000000000..9f28939c7 Binary files /dev/null and b/plugins/dynamix/icons/credits.png differ diff --git a/plugins/dynamix/icons/dateandtime.png b/plugins/dynamix/icons/dateandtime.png new file mode 100644 index 000000000..e17fc0a1c Binary files /dev/null and b/plugins/dynamix/icons/dateandtime.png differ diff --git a/plugins/dynamix/icons/default.png b/plugins/dynamix/icons/default.png new file mode 100644 index 000000000..7b6cc6f31 Binary files /dev/null and b/plugins/dynamix/icons/default.png differ diff --git a/plugins/dynamix/icons/devicesettings.png b/plugins/dynamix/icons/devicesettings.png new file mode 100644 index 000000000..b9fcda69e Binary files /dev/null and b/plugins/dynamix/icons/devicesettings.png differ diff --git a/plugins/dynamix/icons/diagnostics.png b/plugins/dynamix/icons/diagnostics.png new file mode 100644 index 000000000..609aef9b1 Binary files /dev/null and b/plugins/dynamix/icons/diagnostics.png differ diff --git a/plugins/dynamix/icons/dirindex.png b/plugins/dynamix/icons/dirindex.png new file mode 100644 index 000000000..050a800c0 Binary files /dev/null and b/plugins/dynamix/icons/dirindex.png differ diff --git a/plugins/dynamix/icons/disksettings.png b/plugins/dynamix/icons/disksettings.png new file mode 100644 index 000000000..bce0e7bd8 Binary files /dev/null and b/plugins/dynamix/icons/disksettings.png differ diff --git a/plugins/dynamix/icons/diskshares.png b/plugins/dynamix/icons/diskshares.png new file mode 100644 index 000000000..639472892 Binary files /dev/null and b/plugins/dynamix/icons/diskshares.png differ diff --git a/plugins/dynamix/icons/displaysettings.png b/plugins/dynamix/icons/displaysettings.png new file mode 100644 index 000000000..2f7fd0852 Binary files /dev/null and b/plugins/dynamix/icons/displaysettings.png differ diff --git a/plugins/dynamix/icons/edituser.png b/plugins/dynamix/icons/edituser.png new file mode 100644 index 000000000..ea47e448d Binary files /dev/null and b/plugins/dynamix/icons/edituser.png differ diff --git a/plugins/dynamix/icons/emailsettings.png b/plugins/dynamix/icons/emailsettings.png new file mode 100644 index 000000000..b11ee3dd9 Binary files /dev/null and b/plugins/dynamix/icons/emailsettings.png differ diff --git a/plugins/dynamix/icons/feedback.png b/plugins/dynamix/icons/feedback.png new file mode 100755 index 000000000..7bc9233ea Binary files /dev/null and b/plugins/dynamix/icons/feedback.png differ diff --git a/plugins/dynamix/icons/filesystemstatus.png b/plugins/dynamix/icons/filesystemstatus.png new file mode 100644 index 000000000..539e4ded8 Binary files /dev/null and b/plugins/dynamix/icons/filesystemstatus.png differ diff --git a/plugins/dynamix/icons/flashdevicesettings.png b/plugins/dynamix/icons/flashdevicesettings.png new file mode 100644 index 000000000..c2d68cc39 Binary files /dev/null and b/plugins/dynamix/icons/flashdevicesettings.png differ diff --git a/plugins/dynamix/icons/ftpserver.png b/plugins/dynamix/icons/ftpserver.png new file mode 100644 index 000000000..892d9f7a4 Binary files /dev/null and b/plugins/dynamix/icons/ftpserver.png differ diff --git a/plugins/dynamix/icons/globalsharesettings.png b/plugins/dynamix/icons/globalsharesettings.png new file mode 100644 index 000000000..92717888a Binary files /dev/null and b/plugins/dynamix/icons/globalsharesettings.png differ diff --git a/plugins/dynamix/icons/gplv2.png b/plugins/dynamix/icons/gplv2.png new file mode 100644 index 000000000..09c5757ed Binary files /dev/null and b/plugins/dynamix/icons/gplv2.png differ diff --git a/plugins/dynamix/icons/health.png b/plugins/dynamix/icons/health.png new file mode 100644 index 000000000..767d1e3da Binary files /dev/null and b/plugins/dynamix/icons/health.png differ diff --git a/plugins/dynamix/icons/help.png b/plugins/dynamix/icons/help.png new file mode 100644 index 000000000..1a067323f Binary files /dev/null and b/plugins/dynamix/icons/help.png differ diff --git a/plugins/dynamix/icons/identification.png b/plugins/dynamix/icons/identification.png new file mode 100644 index 000000000..4f1d771ef Binary files /dev/null and b/plugins/dynamix/icons/identification.png differ diff --git a/plugins/dynamix/icons/identity.png b/plugins/dynamix/icons/identity.png new file mode 100644 index 000000000..9a1e641eb Binary files /dev/null and b/plugins/dynamix/icons/identity.png differ diff --git a/plugins/dynamix/icons/info.png b/plugins/dynamix/icons/info.png new file mode 100644 index 000000000..522fdcb9b Binary files /dev/null and b/plugins/dynamix/icons/info.png differ diff --git a/plugins/dynamix/icons/log.png b/plugins/dynamix/icons/log.png new file mode 100644 index 000000000..be461ca99 Binary files /dev/null and b/plugins/dynamix/icons/log.png differ diff --git a/plugins/dynamix/icons/moversettings.png b/plugins/dynamix/icons/moversettings.png new file mode 100644 index 000000000..a4b725d3c Binary files /dev/null and b/plugins/dynamix/icons/moversettings.png differ diff --git a/plugins/dynamix/icons/networkservices.png b/plugins/dynamix/icons/networkservices.png new file mode 100644 index 000000000..af36d9292 Binary files /dev/null and b/plugins/dynamix/icons/networkservices.png differ diff --git a/plugins/dynamix/icons/networksettings.png b/plugins/dynamix/icons/networksettings.png new file mode 100644 index 000000000..7b2a02c06 Binary files /dev/null and b/plugins/dynamix/icons/networksettings.png differ diff --git a/plugins/dynamix/icons/newconfig.png b/plugins/dynamix/icons/newconfig.png new file mode 100644 index 000000000..dec741630 Binary files /dev/null and b/plugins/dynamix/icons/newconfig.png differ diff --git a/plugins/dynamix/icons/newpermissions.png b/plugins/dynamix/icons/newpermissions.png new file mode 100644 index 000000000..f00a77201 Binary files /dev/null and b/plugins/dynamix/icons/newpermissions.png differ diff --git a/plugins/dynamix/icons/nfs.png b/plugins/dynamix/icons/nfs.png new file mode 100644 index 000000000..299d67c60 Binary files /dev/null and b/plugins/dynamix/icons/nfs.png differ diff --git a/plugins/dynamix/icons/nfssecuritysettings.png b/plugins/dynamix/icons/nfssecuritysettings.png new file mode 100644 index 000000000..299d67c60 Binary files /dev/null and b/plugins/dynamix/icons/nfssecuritysettings.png differ diff --git a/plugins/dynamix/icons/notificationagents.png b/plugins/dynamix/icons/notificationagents.png new file mode 100644 index 000000000..1ba5fd0cb Binary files /dev/null and b/plugins/dynamix/icons/notificationagents.png differ diff --git a/plugins/dynamix/icons/notificationsettings.png b/plugins/dynamix/icons/notificationsettings.png new file mode 100644 index 000000000..210a72ab4 Binary files /dev/null and b/plugins/dynamix/icons/notificationsettings.png differ diff --git a/plugins/dynamix/icons/pagemap.png b/plugins/dynamix/icons/pagemap.png new file mode 100644 index 000000000..adfbaad45 Binary files /dev/null and b/plugins/dynamix/icons/pagemap.png differ diff --git a/plugins/dynamix/icons/paritycheck.png b/plugins/dynamix/icons/paritycheck.png new file mode 100644 index 000000000..4f0717d08 Binary files /dev/null and b/plugins/dynamix/icons/paritycheck.png differ diff --git a/plugins/dynamix/icons/paritydevicesettings.png b/plugins/dynamix/icons/paritydevicesettings.png new file mode 100644 index 000000000..326d5280a Binary files /dev/null and b/plugins/dynamix/icons/paritydevicesettings.png differ diff --git a/plugins/dynamix/icons/poolinformation.png b/plugins/dynamix/icons/poolinformation.png new file mode 100644 index 000000000..30de8abb3 Binary files /dev/null and b/plugins/dynamix/icons/poolinformation.png differ diff --git a/plugins/dynamix/icons/processes.png b/plugins/dynamix/icons/processes.png new file mode 100644 index 000000000..e7956eab0 Binary files /dev/null and b/plugins/dynamix/icons/processes.png differ diff --git a/plugins/dynamix/icons/prowl.png b/plugins/dynamix/icons/prowl.png new file mode 100644 index 000000000..89549c84d Binary files /dev/null and b/plugins/dynamix/icons/prowl.png differ diff --git a/plugins/dynamix/icons/pushbullet.png b/plugins/dynamix/icons/pushbullet.png new file mode 100644 index 000000000..9b7cfd35e Binary files /dev/null and b/plugins/dynamix/icons/pushbullet.png differ diff --git a/plugins/dynamix/icons/pushover.png b/plugins/dynamix/icons/pushover.png new file mode 100644 index 000000000..e0afe3edf Binary files /dev/null and b/plugins/dynamix/icons/pushover.png differ diff --git a/plugins/dynamix/icons/registration.png b/plugins/dynamix/icons/registration.png new file mode 100644 index 000000000..df5b08f4e Binary files /dev/null and b/plugins/dynamix/icons/registration.png differ diff --git a/plugins/dynamix/icons/releasenotes.png b/plugins/dynamix/icons/releasenotes.png new file mode 100644 index 000000000..f4d4e7242 Binary files /dev/null and b/plugins/dynamix/icons/releasenotes.png differ diff --git a/plugins/dynamix/icons/scrubstatus.png b/plugins/dynamix/icons/scrubstatus.png new file mode 100644 index 000000000..cf039f40b Binary files /dev/null and b/plugins/dynamix/icons/scrubstatus.png differ diff --git a/plugins/dynamix/icons/self-test.png b/plugins/dynamix/icons/self-test.png new file mode 100644 index 000000000..2180292bb Binary files /dev/null and b/plugins/dynamix/icons/self-test.png differ diff --git a/plugins/dynamix/icons/sharesettings.png b/plugins/dynamix/icons/sharesettings.png new file mode 100644 index 000000000..e5095de21 Binary files /dev/null and b/plugins/dynamix/icons/sharesettings.png differ diff --git a/plugins/dynamix/icons/smartchecks.png b/plugins/dynamix/icons/smartchecks.png new file mode 100644 index 000000000..26f3db804 Binary files /dev/null and b/plugins/dynamix/icons/smartchecks.png differ diff --git a/plugins/dynamix/icons/smb.png b/plugins/dynamix/icons/smb.png new file mode 100644 index 000000000..d6c2f6aaf Binary files /dev/null and b/plugins/dynamix/icons/smb.png differ diff --git a/plugins/dynamix/icons/smbsecuritysettings.png b/plugins/dynamix/icons/smbsecuritysettings.png new file mode 100644 index 000000000..d6c2f6aaf Binary files /dev/null and b/plugins/dynamix/icons/smbsecuritysettings.png differ diff --git a/plugins/dynamix/icons/smbsettings.png b/plugins/dynamix/icons/smbsettings.png new file mode 100644 index 000000000..d6c2f6aaf Binary files /dev/null and b/plugins/dynamix/icons/smbsettings.png differ diff --git a/plugins/dynamix/icons/smtpsettings.png b/plugins/dynamix/icons/smtpsettings.png new file mode 100644 index 000000000..6f7ab37fb Binary files /dev/null and b/plugins/dynamix/icons/smtpsettings.png differ diff --git a/plugins/dynamix/icons/statistics.png b/plugins/dynamix/icons/statistics.png new file mode 100644 index 000000000..7ca1b2f44 Binary files /dev/null and b/plugins/dynamix/icons/statistics.png differ diff --git a/plugins/dynamix/icons/syslinuxconfiguration.png b/plugins/dynamix/icons/syslinuxconfiguration.png new file mode 100644 index 000000000..fc7443128 Binary files /dev/null and b/plugins/dynamix/icons/syslinuxconfiguration.png differ diff --git a/plugins/dynamix/icons/systemdevices.png b/plugins/dynamix/icons/systemdevices.png new file mode 100644 index 000000000..296a0a21a Binary files /dev/null and b/plugins/dynamix/icons/systemdevices.png differ diff --git a/plugins/dynamix/icons/systeminformation.png b/plugins/dynamix/icons/systeminformation.png new file mode 100644 index 000000000..2dd04abcd Binary files /dev/null and b/plugins/dynamix/icons/systeminformation.png differ diff --git a/plugins/dynamix/icons/systemlog.png b/plugins/dynamix/icons/systemlog.png new file mode 100644 index 000000000..02661159c Binary files /dev/null and b/plugins/dynamix/icons/systemlog.png differ diff --git a/plugins/dynamix/icons/systemsettings.png b/plugins/dynamix/icons/systemsettings.png new file mode 100644 index 000000000..715bef649 Binary files /dev/null and b/plugins/dynamix/icons/systemsettings.png differ diff --git a/plugins/dynamix/icons/unassigneddevices.png b/plugins/dynamix/icons/unassigneddevices.png new file mode 100644 index 000000000..043f42827 Binary files /dev/null and b/plugins/dynamix/icons/unassigneddevices.png differ diff --git a/plugins/dynamix/icons/unraidos.png b/plugins/dynamix/icons/unraidos.png new file mode 100644 index 000000000..967af802c Binary files /dev/null and b/plugins/dynamix/icons/unraidos.png differ diff --git a/plugins/dynamix/icons/userpreferences.png b/plugins/dynamix/icons/userpreferences.png new file mode 100644 index 000000000..f67fcb0ae Binary files /dev/null and b/plugins/dynamix/icons/userpreferences.png differ diff --git a/plugins/dynamix/icons/users.png b/plugins/dynamix/icons/users.png new file mode 100644 index 000000000..1694ed8bf Binary files /dev/null and b/plugins/dynamix/icons/users.png differ diff --git a/plugins/dynamix/icons/usershares.png b/plugins/dynamix/icons/usershares.png new file mode 100644 index 000000000..a6578252c Binary files /dev/null and b/plugins/dynamix/icons/usershares.png differ diff --git a/plugins/dynamix/icons/userutilities.png b/plugins/dynamix/icons/userutilities.png new file mode 100644 index 000000000..2dd04abcd Binary files /dev/null and b/plugins/dynamix/icons/userutilities.png differ diff --git a/plugins/dynamix/icons/vars.png b/plugins/dynamix/icons/vars.png new file mode 100644 index 000000000..8e1f3b814 Binary files /dev/null and b/plugins/dynamix/icons/vars.png differ diff --git a/plugins/dynamix/icons/webgui.png b/plugins/dynamix/icons/webgui.png new file mode 100644 index 000000000..29e9a9e73 Binary files /dev/null and b/plugins/dynamix/icons/webgui.png differ diff --git a/plugins/dynamix/icons/workgroupsettings.png b/plugins/dynamix/icons/workgroupsettings.png new file mode 100644 index 000000000..f78cb0808 Binary files /dev/null and b/plugins/dynamix/icons/workgroupsettings.png differ diff --git a/plugins/dynamix/images/apple-logo.png b/plugins/dynamix/images/apple-logo.png new file mode 100644 index 000000000..3ee342a7f Binary files /dev/null and b/plugins/dynamix/images/apple-logo.png differ diff --git a/plugins/dynamix/images/application.png b/plugins/dynamix/images/application.png new file mode 100644 index 000000000..1dee9e366 Binary files /dev/null and b/plugins/dynamix/images/application.png differ diff --git a/plugins/dynamix/images/bad.png b/plugins/dynamix/images/bad.png new file mode 100644 index 000000000..b35e94d64 Binary files /dev/null and b/plugins/dynamix/images/bad.png differ diff --git a/plugins/dynamix/images/banner.png b/plugins/dynamix/images/banner.png new file mode 100644 index 000000000..9981e9f03 Binary files /dev/null and b/plugins/dynamix/images/banner.png differ diff --git a/plugins/dynamix/images/black-off.png b/plugins/dynamix/images/black-off.png new file mode 100644 index 000000000..b0b38b42d Binary files /dev/null and b/plugins/dynamix/images/black-off.png differ diff --git a/plugins/dynamix/images/blue-blink.png b/plugins/dynamix/images/blue-blink.png new file mode 100644 index 000000000..27a140aa3 Binary files /dev/null and b/plugins/dynamix/images/blue-blink.png differ diff --git a/plugins/dynamix/images/blue-on.png b/plugins/dynamix/images/blue-on.png new file mode 100644 index 000000000..61d57a381 Binary files /dev/null and b/plugins/dynamix/images/blue-on.png differ diff --git a/plugins/dynamix/images/cdrom.png b/plugins/dynamix/images/cdrom.png new file mode 100644 index 000000000..3b2df0bfc Binary files /dev/null and b/plugins/dynamix/images/cdrom.png differ diff --git a/plugins/dynamix/images/close.png b/plugins/dynamix/images/close.png new file mode 100644 index 000000000..38657e4ca Binary files /dev/null and b/plugins/dynamix/images/close.png differ diff --git a/plugins/dynamix/images/code.png b/plugins/dynamix/images/code.png new file mode 100644 index 000000000..0c76bd129 Binary files /dev/null and b/plugins/dynamix/images/code.png differ diff --git a/plugins/dynamix/images/confirmations.png b/plugins/dynamix/images/confirmations.png new file mode 100644 index 000000000..9c451bb94 Binary files /dev/null and b/plugins/dynamix/images/confirmations.png differ diff --git a/plugins/dynamix/images/css.png b/plugins/dynamix/images/css.png new file mode 100644 index 000000000..f907e44b3 Binary files /dev/null and b/plugins/dynamix/images/css.png differ diff --git a/plugins/dynamix/images/date-time.png b/plugins/dynamix/images/date-time.png new file mode 100644 index 000000000..f43e3655e Binary files /dev/null and b/plugins/dynamix/images/date-time.png differ diff --git a/plugins/dynamix/images/db.png b/plugins/dynamix/images/db.png new file mode 100644 index 000000000..bddba1f98 Binary files /dev/null and b/plugins/dynamix/images/db.png differ diff --git a/plugins/dynamix/images/default.png b/plugins/dynamix/images/default.png new file mode 100644 index 000000000..1e9dcc7fe Binary files /dev/null and b/plugins/dynamix/images/default.png differ diff --git a/plugins/dynamix/images/delete.png b/plugins/dynamix/images/delete.png new file mode 100644 index 000000000..304f77eb9 Binary files /dev/null and b/plugins/dynamix/images/delete.png differ diff --git a/plugins/dynamix/images/directory.png b/plugins/dynamix/images/directory.png new file mode 100644 index 000000000..784e8fa48 Binary files /dev/null and b/plugins/dynamix/images/directory.png differ diff --git a/plugins/dynamix/images/disk-image.png b/plugins/dynamix/images/disk-image.png new file mode 100644 index 000000000..784d73d50 Binary files /dev/null and b/plugins/dynamix/images/disk-image.png differ diff --git a/plugins/dynamix/images/disk-settings.png b/plugins/dynamix/images/disk-settings.png new file mode 100644 index 000000000..21cd6ae1c Binary files /dev/null and b/plugins/dynamix/images/disk-settings.png differ diff --git a/plugins/dynamix/images/disk.health.png b/plugins/dynamix/images/disk.health.png new file mode 100644 index 000000000..b4ecd0552 Binary files /dev/null and b/plugins/dynamix/images/disk.health.png differ diff --git a/plugins/dynamix/images/disk.png b/plugins/dynamix/images/disk.png new file mode 100644 index 000000000..493cbe5d6 Binary files /dev/null and b/plugins/dynamix/images/disk.png differ diff --git a/plugins/dynamix/images/display-settings.png b/plugins/dynamix/images/display-settings.png new file mode 100644 index 000000000..d2b920dcc Binary files /dev/null and b/plugins/dynamix/images/display-settings.png differ diff --git a/plugins/dynamix/images/doc.png b/plugins/dynamix/images/doc.png new file mode 100644 index 000000000..ae8ecbf47 Binary files /dev/null and b/plugins/dynamix/images/doc.png differ diff --git a/plugins/dynamix/images/dynamix.email.notify.png b/plugins/dynamix/images/dynamix.email.notify.png new file mode 100644 index 000000000..2f95cd3f4 Binary files /dev/null and b/plugins/dynamix/images/dynamix.email.notify.png differ diff --git a/plugins/dynamix/images/dynamix.png b/plugins/dynamix/images/dynamix.png new file mode 100644 index 000000000..d1077af01 Binary files /dev/null and b/plugins/dynamix/images/dynamix.png differ diff --git a/plugins/dynamix/images/explore.png b/plugins/dynamix/images/explore.png new file mode 100644 index 000000000..0ba939184 Binary files /dev/null and b/plugins/dynamix/images/explore.png differ diff --git a/plugins/dynamix/images/feedback_bugreport.jpg b/plugins/dynamix/images/feedback_bugreport.jpg new file mode 100644 index 000000000..3024a93ee Binary files /dev/null and b/plugins/dynamix/images/feedback_bugreport.jpg differ diff --git a/plugins/dynamix/images/feedback_comment.jpg b/plugins/dynamix/images/feedback_comment.jpg new file mode 100644 index 000000000..176746dbf Binary files /dev/null and b/plugins/dynamix/images/feedback_comment.jpg differ diff --git a/plugins/dynamix/images/feedback_featurerequest.jpg b/plugins/dynamix/images/feedback_featurerequest.jpg new file mode 100644 index 000000000..a4fc23709 Binary files /dev/null and b/plugins/dynamix/images/feedback_featurerequest.jpg differ diff --git a/plugins/dynamix/images/file-types.png b/plugins/dynamix/images/file-types.png new file mode 100644 index 000000000..ce6525fd9 Binary files /dev/null and b/plugins/dynamix/images/file-types.png differ diff --git a/plugins/dynamix/images/file.png b/plugins/dynamix/images/file.png new file mode 100644 index 000000000..8b8b1ca00 Binary files /dev/null and b/plugins/dynamix/images/file.png differ diff --git a/plugins/dynamix/images/film.png b/plugins/dynamix/images/film.png new file mode 100644 index 000000000..b0ce7bb19 Binary files /dev/null and b/plugins/dynamix/images/film.png differ diff --git a/plugins/dynamix/images/flash.png b/plugins/dynamix/images/flash.png new file mode 100644 index 000000000..72dd7ba9e Binary files /dev/null and b/plugins/dynamix/images/flash.png differ diff --git a/plugins/dynamix/images/folder_open.png b/plugins/dynamix/images/folder_open.png new file mode 100644 index 000000000..4e3548352 Binary files /dev/null and b/plugins/dynamix/images/folder_open.png differ diff --git a/plugins/dynamix/images/ftp-server.png b/plugins/dynamix/images/ftp-server.png new file mode 100644 index 000000000..d163cada1 Binary files /dev/null and b/plugins/dynamix/images/ftp-server.png differ diff --git a/plugins/dynamix/images/good.png b/plugins/dynamix/images/good.png new file mode 100644 index 000000000..b9037dc2e Binary files /dev/null and b/plugins/dynamix/images/good.png differ diff --git a/plugins/dynamix/images/green-blink.png b/plugins/dynamix/images/green-blink.png new file mode 100644 index 000000000..bcddf41db Binary files /dev/null and b/plugins/dynamix/images/green-blink.png differ diff --git a/plugins/dynamix/images/green-on.png b/plugins/dynamix/images/green-on.png new file mode 100644 index 000000000..0891ab8e9 Binary files /dev/null and b/plugins/dynamix/images/green-on.png differ diff --git a/plugins/dynamix/images/grey-off.png b/plugins/dynamix/images/grey-off.png new file mode 100644 index 000000000..27a140aa3 Binary files /dev/null and b/plugins/dynamix/images/grey-off.png differ diff --git a/plugins/dynamix/images/hot.png b/plugins/dynamix/images/hot.png new file mode 100644 index 000000000..bf2fee66e Binary files /dev/null and b/plugins/dynamix/images/hot.png differ diff --git a/plugins/dynamix/images/html.png b/plugins/dynamix/images/html.png new file mode 100644 index 000000000..6ed2490ed Binary files /dev/null and b/plugins/dynamix/images/html.png differ diff --git a/plugins/dynamix/images/ident.png b/plugins/dynamix/images/ident.png new file mode 100644 index 000000000..af480dce2 Binary files /dev/null and b/plugins/dynamix/images/ident.png differ diff --git a/plugins/dynamix/images/information.png b/plugins/dynamix/images/information.png new file mode 100644 index 000000000..12cd1aef9 Binary files /dev/null and b/plugins/dynamix/images/information.png differ diff --git a/plugins/dynamix/images/java.png b/plugins/dynamix/images/java.png new file mode 100644 index 000000000..b7bfcd15f Binary files /dev/null and b/plugins/dynamix/images/java.png differ diff --git a/plugins/dynamix/images/limetech-logo-black.png b/plugins/dynamix/images/limetech-logo-black.png new file mode 100644 index 000000000..d2096358a Binary files /dev/null and b/plugins/dynamix/images/limetech-logo-black.png differ diff --git a/plugins/dynamix/images/limetech-logo-white.png b/plugins/dynamix/images/limetech-logo-white.png new file mode 100644 index 000000000..afebbfc45 Binary files /dev/null and b/plugins/dynamix/images/limetech-logo-white.png differ diff --git a/plugins/dynamix/images/linux-logo.png b/plugins/dynamix/images/linux-logo.png new file mode 100644 index 000000000..a4f99d3df Binary files /dev/null and b/plugins/dynamix/images/linux-logo.png differ diff --git a/plugins/dynamix/images/linux.png b/plugins/dynamix/images/linux.png new file mode 100644 index 000000000..52699bfee Binary files /dev/null and b/plugins/dynamix/images/linux.png differ diff --git a/plugins/dynamix/images/loading.gif b/plugins/dynamix/images/loading.gif new file mode 100644 index 000000000..95333bdfa Binary files /dev/null and b/plugins/dynamix/images/loading.gif differ diff --git a/plugins/dynamix/images/max.png b/plugins/dynamix/images/max.png new file mode 100644 index 000000000..ef35e2b03 Binary files /dev/null and b/plugins/dynamix/images/max.png differ diff --git a/plugins/dynamix/images/maximise.png b/plugins/dynamix/images/maximise.png new file mode 100644 index 000000000..211052ba4 Binary files /dev/null and b/plugins/dynamix/images/maximise.png differ diff --git a/plugins/dynamix/images/music.png b/plugins/dynamix/images/music.png new file mode 100644 index 000000000..a8b3ede3d Binary files /dev/null and b/plugins/dynamix/images/music.png differ diff --git a/plugins/dynamix/images/network-settings.png b/plugins/dynamix/images/network-settings.png new file mode 100644 index 000000000..6da724b19 Binary files /dev/null and b/plugins/dynamix/images/network-settings.png differ diff --git a/plugins/dynamix/images/notice.png b/plugins/dynamix/images/notice.png new file mode 100644 index 000000000..0c55d7bc1 Binary files /dev/null and b/plugins/dynamix/images/notice.png differ diff --git a/plugins/dynamix/images/notifications.png b/plugins/dynamix/images/notifications.png new file mode 100644 index 000000000..92719369c Binary files /dev/null and b/plugins/dynamix/images/notifications.png differ diff --git a/plugins/dynamix/images/noview.png b/plugins/dynamix/images/noview.png new file mode 100644 index 000000000..cc93de1fe Binary files /dev/null and b/plugins/dynamix/images/noview.png differ diff --git a/plugins/dynamix/images/pdf.png b/plugins/dynamix/images/pdf.png new file mode 100644 index 000000000..8f8095e46 Binary files /dev/null and b/plugins/dynamix/images/pdf.png differ diff --git a/plugins/dynamix/images/php.png b/plugins/dynamix/images/php.png new file mode 100644 index 000000000..7868a2594 Binary files /dev/null and b/plugins/dynamix/images/php.png differ diff --git a/plugins/dynamix/images/picture.png b/plugins/dynamix/images/picture.png new file mode 100644 index 000000000..4a158fef7 Binary files /dev/null and b/plugins/dynamix/images/picture.png differ diff --git a/plugins/dynamix/images/plg.png b/plugins/dynamix/images/plg.png new file mode 100644 index 000000000..01bdf1214 Binary files /dev/null and b/plugins/dynamix/images/plg.png differ diff --git a/plugins/dynamix/images/ppt.png b/plugins/dynamix/images/ppt.png new file mode 100644 index 000000000..c4eff0387 Binary files /dev/null and b/plugins/dynamix/images/ppt.png differ diff --git a/plugins/dynamix/images/psd.png b/plugins/dynamix/images/psd.png new file mode 100644 index 000000000..73c5b3f24 Binary files /dev/null and b/plugins/dynamix/images/psd.png differ diff --git a/plugins/dynamix/images/red-blink.png b/plugins/dynamix/images/red-blink.png new file mode 100644 index 000000000..9f5c3e8e6 Binary files /dev/null and b/plugins/dynamix/images/red-blink.png differ diff --git a/plugins/dynamix/images/red-off.png b/plugins/dynamix/images/red-off.png new file mode 100644 index 000000000..9f5c3e8e6 Binary files /dev/null and b/plugins/dynamix/images/red-off.png differ diff --git a/plugins/dynamix/images/red-on.png b/plugins/dynamix/images/red-on.png new file mode 100644 index 000000000..9f5c3e8e6 Binary files /dev/null and b/plugins/dynamix/images/red-on.png differ diff --git a/plugins/dynamix/images/ruby.png b/plugins/dynamix/images/ruby.png new file mode 100644 index 000000000..f59b7c436 Binary files /dev/null and b/plugins/dynamix/images/ruby.png differ diff --git a/plugins/dynamix/images/scheduler.png b/plugins/dynamix/images/scheduler.png new file mode 100644 index 000000000..1b0d86064 Binary files /dev/null and b/plugins/dynamix/images/scheduler.png differ diff --git a/plugins/dynamix/images/script.png b/plugins/dynamix/images/script.png new file mode 100644 index 000000000..63fe6ceff Binary files /dev/null and b/plugins/dynamix/images/script.png differ diff --git a/plugins/dynamix/images/settings.png b/plugins/dynamix/images/settings.png new file mode 100644 index 000000000..3a34bc8d1 Binary files /dev/null and b/plugins/dynamix/images/settings.png differ diff --git a/plugins/dynamix/images/share-settings.png b/plugins/dynamix/images/share-settings.png new file mode 100644 index 000000000..36549688f Binary files /dev/null and b/plugins/dynamix/images/share-settings.png differ diff --git a/plugins/dynamix/images/sort-asc.png b/plugins/dynamix/images/sort-asc.png new file mode 100644 index 000000000..a88d7975f Binary files /dev/null and b/plugins/dynamix/images/sort-asc.png differ diff --git a/plugins/dynamix/images/sort-both.png b/plugins/dynamix/images/sort-both.png new file mode 100644 index 000000000..27dcbe5a4 Binary files /dev/null and b/plugins/dynamix/images/sort-both.png differ diff --git a/plugins/dynamix/images/sort-desc.png b/plugins/dynamix/images/sort-desc.png new file mode 100644 index 000000000..def071ed5 Binary files /dev/null and b/plugins/dynamix/images/sort-desc.png differ diff --git a/plugins/dynamix/images/spinner.gif b/plugins/dynamix/images/spinner.gif new file mode 100644 index 000000000..85b99d46b Binary files /dev/null and b/plugins/dynamix/images/spinner.gif differ diff --git a/plugins/dynamix/images/sum.png b/plugins/dynamix/images/sum.png new file mode 100644 index 000000000..16415baf3 Binary files /dev/null and b/plugins/dynamix/images/sum.png differ diff --git a/plugins/dynamix/images/swf.png b/plugins/dynamix/images/swf.png new file mode 100644 index 000000000..5769120b1 Binary files /dev/null and b/plugins/dynamix/images/swf.png differ diff --git a/plugins/dynamix/images/tipsy.gif b/plugins/dynamix/images/tipsy.gif new file mode 100644 index 000000000..74eebae2d Binary files /dev/null and b/plugins/dynamix/images/tipsy.gif differ diff --git a/plugins/dynamix/images/txt.png b/plugins/dynamix/images/txt.png new file mode 100644 index 000000000..813f712f7 Binary files /dev/null and b/plugins/dynamix/images/txt.png differ diff --git a/plugins/dynamix/images/ui-bg_flat_0_aaaaaa_40x100.png b/plugins/dynamix/images/ui-bg_flat_0_aaaaaa_40x100.png new file mode 100644 index 000000000..227590939 Binary files /dev/null and b/plugins/dynamix/images/ui-bg_flat_0_aaaaaa_40x100.png differ diff --git a/plugins/dynamix/images/ui-bg_flat_75_ffffff_40x100.png b/plugins/dynamix/images/ui-bg_flat_75_ffffff_40x100.png new file mode 100644 index 000000000..fe6b8fce5 Binary files /dev/null and b/plugins/dynamix/images/ui-bg_flat_75_ffffff_40x100.png differ diff --git a/plugins/dynamix/images/ui-bg_glass_55_fbf9ee_1x400.png b/plugins/dynamix/images/ui-bg_glass_55_fbf9ee_1x400.png new file mode 100644 index 000000000..b29f3895a Binary files /dev/null and b/plugins/dynamix/images/ui-bg_glass_55_fbf9ee_1x400.png differ diff --git a/plugins/dynamix/images/ui-bg_glass_65_ffffff_1x400.png b/plugins/dynamix/images/ui-bg_glass_65_ffffff_1x400.png new file mode 100644 index 000000000..b60c88fec Binary files /dev/null and b/plugins/dynamix/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/plugins/dynamix/images/ui-bg_glass_75_dadada_1x400.png b/plugins/dynamix/images/ui-bg_glass_75_dadada_1x400.png new file mode 100644 index 000000000..d04ef6eb2 Binary files /dev/null and b/plugins/dynamix/images/ui-bg_glass_75_dadada_1x400.png differ diff --git a/plugins/dynamix/images/ui-bg_glass_75_e6e6e6_1x400.png b/plugins/dynamix/images/ui-bg_glass_75_e6e6e6_1x400.png new file mode 100644 index 000000000..e2d58652c Binary files /dev/null and b/plugins/dynamix/images/ui-bg_glass_75_e6e6e6_1x400.png differ diff --git a/plugins/dynamix/images/ui-bg_glass_95_fef1ec_1x400.png b/plugins/dynamix/images/ui-bg_glass_95_fef1ec_1x400.png new file mode 100644 index 000000000..145a7d5d7 Binary files /dev/null and b/plugins/dynamix/images/ui-bg_glass_95_fef1ec_1x400.png differ diff --git a/plugins/dynamix/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/plugins/dynamix/images/ui-bg_highlight-soft_75_cccccc_1x100.png new file mode 100644 index 000000000..69f0a9855 Binary files /dev/null and b/plugins/dynamix/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ diff --git a/plugins/dynamix/images/ui-icons_222222_256x240.png b/plugins/dynamix/images/ui-icons_222222_256x240.png new file mode 100644 index 000000000..c1cb1170c Binary files /dev/null and b/plugins/dynamix/images/ui-icons_222222_256x240.png differ diff --git a/plugins/dynamix/images/ui-icons_2e83ff_256x240.png b/plugins/dynamix/images/ui-icons_2e83ff_256x240.png new file mode 100644 index 000000000..84b601bf0 Binary files /dev/null and b/plugins/dynamix/images/ui-icons_2e83ff_256x240.png differ diff --git a/plugins/dynamix/images/ui-icons_454545_256x240.png b/plugins/dynamix/images/ui-icons_454545_256x240.png new file mode 100644 index 000000000..b6db1acdd Binary files /dev/null and b/plugins/dynamix/images/ui-icons_454545_256x240.png differ diff --git a/plugins/dynamix/images/ui-icons_888888_256x240.png b/plugins/dynamix/images/ui-icons_888888_256x240.png new file mode 100644 index 000000000..feea0e202 Binary files /dev/null and b/plugins/dynamix/images/ui-icons_888888_256x240.png differ diff --git a/plugins/dynamix/images/ui-icons_cd0a0a_256x240.png b/plugins/dynamix/images/ui-icons_cd0a0a_256x240.png new file mode 100644 index 000000000..ed5b6b093 Binary files /dev/null and b/plugins/dynamix/images/ui-icons_cd0a0a_256x240.png differ diff --git a/plugins/dynamix/images/user.png b/plugins/dynamix/images/user.png new file mode 100644 index 000000000..6bc36fbb8 Binary files /dev/null and b/plugins/dynamix/images/user.png differ diff --git a/plugins/dynamix/images/windows-logo.png b/plugins/dynamix/images/windows-logo.png new file mode 100644 index 000000000..a542c2a78 Binary files /dev/null and b/plugins/dynamix/images/windows-logo.png differ diff --git a/plugins/dynamix/images/xls.png b/plugins/dynamix/images/xls.png new file mode 100644 index 000000000..b977d7e52 Binary files /dev/null and b/plugins/dynamix/images/xls.png differ diff --git a/plugins/dynamix/images/yellow-blink.png b/plugins/dynamix/images/yellow-blink.png new file mode 100644 index 000000000..3858e4b0f Binary files /dev/null and b/plugins/dynamix/images/yellow-blink.png differ diff --git a/plugins/dynamix/images/yellow-on.png b/plugins/dynamix/images/yellow-on.png new file mode 100644 index 000000000..7e4e8fc80 Binary files /dev/null and b/plugins/dynamix/images/yellow-on.png differ diff --git a/plugins/dynamix/images/zip.png b/plugins/dynamix/images/zip.png new file mode 100644 index 000000000..fd4bbccdf Binary files /dev/null and b/plugins/dynamix/images/zip.png differ diff --git a/plugins/dynamix/include/DashUpdate.php b/plugins/dynamix/include/DashUpdate.php new file mode 100644 index 000000000..53df296d7 --- /dev/null +++ b/plugins/dynamix/include/DashUpdate.php @@ -0,0 +1,163 @@ + + 0) || ($last > 0 && $smart > $last)) $thumb = 'bad'; + my_insert($source, ""); +} +function my_usage(&$source,$used) { + my_insert($source, $used ? "
$used
" : "-"); +} +function my_temp($value,$unit) { + return ($unit=='C' ? $value : round(9/5*$value+32))." $unit"; +} +function my_clock($time) { + if (!$time) return 'less than a minute'; + $days = floor($time/1440); + $hour = $time/60%24; + $mins = $time%60; + return plus($days,'day',($hour|$mins)==0).plus($hour,'hour',$mins==0).plus($mins,'minute',true); +} +function plus($val,$word,$last) { + return $val>0?(($val||$last)?($val.' '.$word.($val!=1?'s':'').($last ?'':', ')):''):''; +} +function mhz($speed) { + return "$speed MHz"; +} +function rpm($speed) { + return "$speed RPM"; +} +switch ($_POST['cmd']) { +case 'disk': + $i = 2; + $disks = parse_ini_file("state/disks.ini",true); + $devs = parse_ini_file("state/devs.ini",true); + $row1 = array_fill(0,26,""); my_insert($row1[0],"Active"); + $row2 = array_fill(0,26,""); my_insert($row2[0],"Inactive"); + $row3 = array_fill(0,26,""); my_insert($row3[0],"Unassigned"); + $row4 = array_fill(0,26,""); my_insert($row4[0],"Faulty"); + $row5 = array_fill(0,26,""); my_insert($row5[0],"Heat alarm"); + $row6 = array_fill(0,26,""); my_insert($row6[0],"SMART status"); + $row7 = array_fill(0,26,""); my_insert($row7[0],"Utilization"); + foreach ($disks as $disk) { + $state = $disk['color']; + $n = 0; + switch ($disk['type']) { + case 'Parity': + if ($disk['status']!='DISK_NP') $n = 1; + break; + case 'Data': + if ($disk['status']!='DISK_NP') $n = $i++; + break; + case 'Cache': + if ($disk['status']!='DISK_NP') $n = $i++; + if ($disk['name']!='cache') $disk['fsStatus']=='-'; + break;} + if ($n>0) { + switch ($state) { + case 'grey-off': + break; //ignore + case 'green-on': + my_insert($row1[$n],""); + break; + case 'green-blink': + my_insert($row2[$n],""); + break; + case 'blue-on': + case 'blue-blink': + my_insert($row3[$n],""); + break; + default: + my_insert($row4[$n],""); + break;} + $temp = $disk['temp']; + if ($temp>=$_POST['hot']) my_insert($row5[$n],""); + if ($disk['device'] && !strpos($state,'blink')) my_smart($row6[$n],$disk['name']); + my_usage($row7[$n],($n>1 && $disk['fsStatus']=='Mounted')?(round((1-$disk['fsFree']/$disk['fsSize'])*100).'%'):''); + } + } + foreach ($devs as $dev) my_insert($row3[$i++],""); + echo "".implode('',$row1).""; + echo "".implode('',$row2).""; + echo "".implode('',$row3).""; + echo "".implode('',$row4).""; + echo "".implode('',$row5).""; + echo "".implode('',$row6).""; + echo "".implode('',$row7).""; +break; +case 'sys': + exec("grep -Po '^Mem(Total|Available):\s+\K\d+' /proc/meminfo",$memory); + exec("df /boot /var/log /var/lib/docker|grep -Po '\d+%'",$sys); + $cpu = min(@file_get_contents('state/cpuload.ini'),100); + $mem = max(round((1-$memory[1]/$memory[0])*100),0); + echo "{$cpu}%#{$mem}%#".implode('#',$sys); +break; +case 'cpu': + exec("grep -Po '^cpu MHz\s+: \K\d+' /proc/cpuinfo",$speeds); + echo implode('#',array_map('mhz',$speeds)); +break; +case 'fan': + exec("sensors -uA 2>/dev/null|grep -Po 'fan\d_input: \K\d+'",$rpms); + echo implode('#',array_map('rpm',$rpms)); +break; +case 'port': + switch ($_POST['view']) { + case 'main': + $ports = explode(',',$_POST['ports']); $i = 0; + foreach ($ports as $port) { + unset($info); + if ($port=='bond0') { + $ports[$i++] = exec("grep -Pom1 '^Bonding Mode: \K.+' /proc/net/bonding/bond0"); + } else if ($port=='lo') { + $ports[$i++] = str_replace('yes','loopback',exec("ethtool lo|grep -Pom1 '^\s+Link detected: \K.+'")); + } else { + exec("ethtool $port|grep -Po '^\s+(Speed|Duplex): \K[^U]+'",$info); + $ports[$i++] = $info[0] ? "{$info[0]} - ".strtolower($info[1])." duplex" : "not connected"; + } + } + break; + case 'port': exec("ifconfig -s|awk '/^(bond|eth|lo)/{print $3\"#\"$7}'",$ports); break; + case 'link': exec("ifconfig -s|awk '/^(bond|eth|lo)/{print \"Errors: \"$4\"
Drops: \"$5\"
Overruns: \"$6\"#Errors: \"$8\"
Drops: \"$9\"
Overruns: \"$10}'",$ports); break; + default: $ports = array();} + echo implode('#',$ports); +break; +case 'parity': + $var = parse_ini_file("state/var.ini"); + echo "".($var['mdNumInvalid']==0 ? 'Parity-Check' : ($var['mdInvalidDisk']==0 ? 'Parity-Sync' : 'Data-Rebuild'))." in progress... Completed: ".number_format(($var['mdResyncPos']/($var['mdResync']/100+1)),0)." %.". + "
Elapsed time: ".my_clock(floor(($var['currTime']-$var['sbUpdated'])/60)).". Estimated finish: ".my_clock(round(((($var['mdResyncDt']*(($var['mdResync']-$var['mdResyncPos'])/($var['mdResyncDb']/100+1)))/100)/60),0)).""; +break; +case 'shares': + $names = explode(',',$_POST['names']); + switch ($_POST['com']) { + case 'smb': + exec("lsof /mnt/user /mnt/disk* 2>/dev/null|awk '/^smbd/ && $0!~/\.AppleD(B|ouble)/ && $5==\"REG\"'|awk -F/ '{print $4}'",$lsof); + $counts = array_count_values($lsof); $count = array(); + foreach ($names as $name) $count[] = isset($counts[$name]) ? $counts[$name] : 0; + echo implode('#',$count); + break; + case 'afp': + case 'nfs': + // not available + break;} +break;} diff --git a/plugins/dynamix/include/DefaultPageLayout.php b/plugins/dynamix/include/DefaultPageLayout.php new file mode 100644 index 000000000..6f8cf249e --- /dev/null +++ b/plugins/dynamix/include/DefaultPageLayout.php @@ -0,0 +1,320 @@ + + + + +<?=$var['NAME']?>/<?=$myPage['name']?> + + + + + + + + +"> +"> + + + + + + + + + + + + +
+ +
"; + +// Build page content +echo "
"; +$tab = 1; +$view = $myPage['name']; +$pages = array(); +if ($myPage['text']) $pages[$view] = $myPage; +if ($myPage['Type']=='xmenu') $pages = array_merge($pages, find_pages($view)); +if (isset($myPage['Tabs'])) $display['tabs'] = strtolower($myPage['Tabs'])=='true' ? 0 : 1; +$tabbed = $display['tabs']==0 && count($pages)>1; + +foreach ($pages as $page) { + $close = false; + if (isset($page['Title'])) { + eval("\$title=\"{$page['Title']}\";"); + if ($tabbed) { + echo "
"; + $close = true; + } else { + if ($tab==1) echo "
"; + echo "
"; + echo tab_title($title,$page['root'],isset($page['Png'])?$page['Png']:false); + echo "
"; + } + $tab++; + } + if (isset($page['Type']) && $page['Type']=='menu') { + $pgs = find_pages($page['name']); + foreach ($pgs as $pg) { + @eval("\$title=\"{$pg['Title']}\";"); + $link = "$path/{$pg['name']}"; + if ($icon = isset($pg['Icon'])) { + $icon = "{$pg['root']}/images/{$pg['Icon']}"; + if (!file_exists($icon)) { $icon = "{$pg['root']}/{$pg['Icon']}"; if (!file_exists($icon)) $icon = false; } + } + if (!$icon) $icon = "/webGui/images/default.png"; + echo ""; + } + } + $text = $page['text']; + if (!isset($page['Markdown']) || $page['Markdown'] == 'true') { + $text = Markdown($text); + } + eval("?>$text"); + if ($close) echo "
"; +} +?> +
+ +'; +switch ($var['fsState']) { +case 'Stopped': + echo 'Array Stopped'; break; +case 'Starting': + echo 'Array Starting'; break; +default: + echo 'Array Started'; break; +} +echo "• Dynamix webGui v"; +echo exec("/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/plugin version /var/log/plugins/dynamix.plg"); +echo "unRAID™ webGui © 2015, Lime Technology, Inc."; +if (isset($myPage['Author'])) { + echo " | Page author: {$myPage['Author']}"; + if (isset($myPage['Version'])) echo ", version: {$myPage['Version']}"; +} +echo "
"; +?> + + + diff --git a/plugins/dynamix/include/DeleteLogFile.php b/plugins/dynamix/include/DeleteLogFile.php new file mode 100644 index 000000000..36f8f7582 --- /dev/null +++ b/plugins/dynamix/include/DeleteLogFile.php @@ -0,0 +1,16 @@ + + diff --git a/plugins/dynamix/include/DeviceList.php b/plugins/dynamix/include/DeviceList.php new file mode 100644 index 000000000..78342590b --- /dev/null +++ b/plugins/dynamix/include/DeviceList.php @@ -0,0 +1,394 @@ + +"; + } + else + $ctrl = ""; + $ball = "/webGui/images/{$disk['color']}.png"; + switch ($disk['color']) { + case 'green-on': $help = 'Normal operation, device is active'; break; + case 'green-blink': $help = 'Device is in standby mode (spun-down)'; break; + case 'blue-on': $help = ($disk['name']=='preclear' ? 'Unassigned device' : 'New device'); break; + case 'blue-blink': $help = ($disk['name']=='preclear' ? 'Unassigned device, in standby mode' : 'New device, in stadby mode (spun-down)'); break; + case 'yellow-on': $help = ($disk['type']=='Parity' ? 'Parity is invalid' : 'Device contents emulated'); break; + case 'yellow-blink': $help = 'Device contents emulated, in standby mode (spun-down)'; break; + case 'red-on': + case 'red-blink': $help = ($disk['type']=='Parity' ? 'Parity device is disabled' : 'Device is disabled, contents emulated'); break; + case 'red-off': $help = ($disk['type']=='Parity' ? 'Parity device missing' : 'Device is missing (disabled), contents emulated'); break; + case 'grey-off': $help = 'Device not present'; break; + } + switch ($type) { + case 'Flash': + $device = "Flash"; + break; + default: + $device = "Device"; + break; + } + $status = "${help}"; + $link = strpos($disk['status'], 'DISK_NP')===false ? "$name" : $name; + return $ctrl.$status.$link; +} +function device_browse($disk) { + global $path; + if ($disk['fsStatus']=='Mounted') { + $dir = $disk['name']=="flash" ? "/boot" : "/mnt/{$disk['name']}"; + return ""; + } +} +function device_desc($disk) { + global $var; + $size = my_scale($disk['size']*1024, $unit); + return "{$disk['id']} - $size $unit ({$disk['device']})"; +} +function assignment($disk) { + global $var, $disks, $devs, $screen; + $out = "
"; + $out .= ""; + $out .= "
"; +} +function render_used_and_free($disk) { + global $display; + if ($disk['type']=='Parity' || $disk['fsStatus']=='-') { + echo ""; + } else if ($disk['fsStatus']=='Mounted') { + echo "{$disk['fsType']}"; + echo "".my_scale($disk['fsSize']*1024, $unit)." $unit"; + switch ($display['text']) { + case 0: + $text1 = true; $text2 = true; break; + case 1: case 2: + $text1 = false; $text2 = false; break; + case 10: case 20: + $text1 = true; $text2 = false; break; + case 11: case 21: + $text1 = false; $text2 = true; break; + } + if ($text1) { + echo "".my_scale($disk['fsUsed']*1024, $unit)." $unit"; + } else { + $used = $disk['fsSize'] ? 100 - round(100*$disk['fsFree']/$disk['fsSize']) : 0; + echo "
".my_scale($disk['fsUsed']*1024, $unit)." $unit
"; + } + if ($text2) { + echo "".my_scale($disk['fsFree']*1024, $unit)." $unit"; + } else { + $free = $disk['fsSize'] ? round(100*$disk['fsFree']/$disk['fsSize']) : 0; + echo "
".my_scale($disk['fsFree']*1024, $unit)." $unit
"; + } + } else { + echo "{$disk['fsStatus']}"; + } +} +function array_offline($disk) { + echo ""; + switch ($disk['status']) { + case "DISK_NP": + case "DISK_OK_NP": + case "DISK_NP_DSBL": + echo "".device_info($disk).""; + echo "".assignment($disk).""; + echo ""; + break; + case "DISK_OK": + case "DISK_INVALID": + case "DISK_DSBL": + case "DISK_DSBL_NEW": + case "DISK_NEW": + echo "".device_info($disk).""; + echo "".assignment($disk).""; + echo "".my_temp($disk['temp']).""; + echo ""; + break; + case "DISK_NP_MISSING": + echo "".device_info($disk)."Missing"; + echo "".assignment($disk)."{$disk['idSb']} - ".my_scale($disk['sizeSb']*1024, $unit)." $unit"; + echo ""; + break; + case "DISK_WRONG": + echo "".device_info($disk)."Wrong"; + echo "".assignment($disk)."{$disk['idSb']} - ".my_scale($disk['sizeSb']*1024, $unit)." $unit"; + echo "".my_temp($disk['temp']).""; + echo ""; + break; + } + echo ""; +} +function array_online($disk) { + global $display, $temps, $counts, $tot_size, $tot_used, $tot_free, $reads, $writes, $errors; + if (is_numeric($disk['temp'])) { + $temps += $disk['temp']; + $counts++; + } + $reads += $disk['numReads']; + $writes += $disk['numWrites']; + $errors += $disk['numErrors']; + if (isset($disk['fsFree']) && $disk['type']!='Parity') { + $disk['fsUsed'] = $disk['fsSize'] - $disk['fsFree']; + $tot_size += $disk['fsSize']; + $tot_free += $disk['fsFree']; + $tot_used += $disk['fsUsed']; + } + echo ""; + switch ($disk['status']) { + case "DISK_NP": +// Suppress empty slots to keep device list short +// this actually should be configurable +// echo "".device_info($disk).""; +// echo "Not installed"; +// echo ""; + break; + case "DISK_OK_NP": + echo "".device_info($disk).""; + echo "Not Installed"; + echo ""; + render_used_and_free($disk); + echo "".device_browse($disk).""; + break; + case "DISK_NP_DSBL": + echo "".device_info($disk).""; + if ($disk['type']=="Parity") { + echo "Not installed"; + echo ""; + } else { + echo "Not installed"; + echo ""; + render_used_and_free($disk); + echo "".device_browse($disk).""; + } + break; + case "DISK_DSBL": + echo "".device_info($disk).""; + echo "".device_desc($disk).""; + echo "".my_temp($disk['temp']).""; + echo "".my_number($disk['numReads']).""; + echo "".my_number($disk['numWrites']).""; + echo "".my_number($disk['numErrors']).""; + if ($disk['type']=="Parity") { + echo ""; + } else { + render_used_and_free($disk); + echo "".device_browse($disk).""; + } + break; + default: + echo "".device_info($disk).""; + echo "".device_desc($disk).""; + echo "".my_temp($disk['temp']).""; + echo "".my_number($disk['numReads']).""; + echo "".my_number($disk['numWrites']).""; + echo "".my_number($disk['numErrors']).""; + render_used_and_free($disk); + echo "".device_browse($disk).""; + break; + } + echo ""; +} +function my_clock($time) { + if (!$time) return 'less than a minute'; + $days = floor($time/1440); + $hour = $time/60%24; + $mins = $time%60; + return plus($days,'day',($hour|$mins)==0).plus($hour,'hour',$mins==0).plus($mins,'minute',true); +} +function read_disk($device, $item) { + global $var; + $smart = "/var/local/emhttp/smart/$device"; + if (!file_exists($smart) || (time()-filemtime($smart)>=$var['poll_attributes'])) exec("smartctl -n standby -A /dev/$device > $smart"); + $temp = exec("awk '/Temperature/{print \$10;exit}' $smart"); + switch ($item) { + case 'color': return $temp ? 'blue-on' : 'blue-blink'; + case 'temp' : return $temp ? $temp : '*'; + } +} +function show_totals($text) { + global $var, $display, $temps, $counts, $tot_size, $tot_used, $tot_free, $reads, $writes, $errors; + echo ""; + echo "Total"; + echo "$text"; + echo "".($counts>0?my_temp(round($temps/$counts, 1)):'*').""; + echo "".my_number($reads).""; + echo "".my_number($writes).""; + echo "".my_number($errors).""; + echo ""; + if (strstr($text,"Array") && ($var['startMode'] == "Normal")) { + echo "".my_scale($tot_size*1024, $unit)." $unit"; + switch ($display['text']) { + case 0: + $text1 = true; $text2 = true; break; + case 1: case 2: + $text1 = false; $text2 = false; break; + case 10: case 20: + $text1 = true; $text2 = false; break; + case 11: case 21: + $text1 = false; $text2 = true; break; + } + if ($text1) { + echo "".my_scale($tot_used*1024, $unit)." $unit"; + } else { + $used = $tot_size ? 100 - round(100*$tot_free/$tot_size) : 0; + echo "
".my_scale($tot_used*1024, $unit)." $unit
"; + } + if ($text2) { + echo "".my_scale($tot_free*1024, $unit)." $unit"; + } else { + $free = $tot_size ? round(100*$tot_free/$tot_size) : 0; + echo "
".my_scale($tot_free*1024, $unit)." $unit
"; + } + echo ""; + } + else + echo ""; + echo ""; +} +function array_slots() { + global $var; + $min = max($var['sbNumDisks'], 3); + $max = $var['MAX_ARRAYSZ']; + $out = ""; + $out .= "
"; + $out .= ""; + $out .= "
"; + return $out; +} +function cache_slots() { + global $var; + $min = $var['cacheSbNumDisks']; + $max = $var['MAX_CACHESZ']; + $out = ""; + $out .= "
"; + $out .= ""; + $out .= "
"; + return $out; +} +switch ($_POST['device']) { +case 'array': + if ($var['fsState']=='Stopped') { + foreach ($disks as $disk) {if ($disk['type']=='Parity') array_offline($disk);} + foreach ($disks as $disk) {if ($disk['type']=='Data') array_offline($disk);} + echo "Slots:".array_slots().""; + } else { + foreach ($disks as $disk) {if ($disk['type']=='Parity') array_online($disk);} + foreach ($disks as $disk) {if ($disk['type']=='Data') array_online($disk);} + if ($display['total']) show_totals("Array of ".my_word($var['mdNumDisks'])." devices"); + } + break; +case 'flash': + $disk = &$disks['flash']; + $disk['fsUsed'] = $disk['fsSize'] - $disk['fsFree']; + echo ""; + echo "".device_info($disk).""; + echo "".device_desc($disk).""; + echo "*"; + echo "".my_number($disk['numReads']).""; + echo "".my_number($disk['numWrites']).""; + echo "".my_number($disk['numErrors']).""; + render_used_and_free($disk); + echo "".device_browse($disk).""; + echo ""; + break; +case 'cache': + if ($var['fsState']=='Stopped') { + foreach ($disks as $disk) {if ($disk['type']=='Cache') array_offline($disk);} + echo "Slots:".cache_slots().""; + echo ""; + } else { + foreach ($disks as $disk) {if ($disk['type']=='Cache') array_online($disk);} + if ($display['total'] && $var['cacheSbNumDisks']>1) show_totals("Pool of ".my_word($var['cacheNumDevices'])." devices"); + } + break; +case 'open': + $status = isset($confirm['preclear']) ? '' : '_NP'; + foreach ($devs as $dev) { + $dev['name'] = 'preclear'; + $dev['color'] = read_disk($dev['device'], 'color'); + $dev['temp'] = read_disk($dev['device'], 'temp'); + $dev['status'] = $status; + echo ""; + echo "".device_info($dev).""; + echo "".device_desc($dev).""; + echo "".my_temp($dev['temp']).""; + if (file_exists("/tmp/preclear_stat_{$dev['device']}")) { + $text = exec("cut -d'|' -f3 /tmp/preclear_stat_{$dev['device']} | sed 's:\^n:\:g'"); + if (strpos($text,'Total time')===false) $text = 'Preclear in progress... '.$text; + echo "$text"; + } else + echo ""; + echo ""; + } + break; +case 'parity': + $data = array(); + if ($var['mdResync']>0) { + $data[] = my_scale($var['mdResync']*1024, $unit)." $unit"; + $data[] = my_clock(floor(($var['currTime']-$var['sbUpdated'])/60)); + $data[] = my_scale($var['mdResyncPos']*1024, $unit)." $unit (".number_format(($var['mdResyncPos']/($var['mdResync']/100+1)),1,substr($display['number'],0,1),'')." %)"; + $data[] = my_scale($var['mdResyncDb']/$var['mdResyncDt']*1024, $unit, 1)." $unit/sec"; + $data[] = my_clock(round(((($var['mdResyncDt']*(($var['mdResync']-$var['mdResyncPos'])/($var['mdResyncDb']/100+1)))/100)/60),0)); + $data[] = $var['sbSyncErrs']; + echo implode(';',$data); + } + break; +} +?> diff --git a/plugins/dynamix/include/Download.php b/plugins/dynamix/include/Download.php new file mode 100644 index 000000000..e4f17d89a --- /dev/null +++ b/plugins/dynamix/include/Download.php @@ -0,0 +1,37 @@ + + diff --git a/plugins/dynamix/include/Feedback.php b/plugins/dynamix/include/Feedback.php new file mode 100644 index 000000000..066e31b18 --- /dev/null +++ b/plugins/dynamix/include/Feedback.php @@ -0,0 +1,193 @@ + + + + + + +
+
+ + + + +
+
+
+
+
+ +
+ +
+
+ +

NOTE: Submission of this bug report will automatically send your system diagnostics to Lime Technology.

+ +
+
+ +
+ +
+
+ +
+ + + diff --git a/plugins/dynamix/include/FileTree.php b/plugins/dynamix/include/FileTree.php new file mode 100644 index 000000000..6602db37e --- /dev/null +++ b/plugins/dynamix/include/FileTree.php @@ -0,0 +1,84 @@ + prevents debug users from exploring system's directory structure + * ex: $root = $_SERVER['DOCUMENT_ROOT']; + */ +$root = '/'; +if( !$root ) exit("ERROR: Root filesystem directory not set in jqueryFileTree.php"); + +$postDir = $root.(isset($_POST['dir']) ? $_POST['dir'] : '' ); +if (substr($postDir, -1) != '/') { + $postDir .= '/'; +} + +$filters = (array)(isset($_POST['filter']) ? $_POST['filter'] : ''); + +// set checkbox if multiSelect set to true +$checkbox = ( isset($_POST['multiSelect']) && $_POST['multiSelect'] == 'true' ) ? "" : null; + +if( file_exists($postDir) ) { + + $files = scandir($postDir); + $returnDir = substr($postDir, strlen($root)); + + natcasesort($files); + + if( count($files) > 2 ) { // The 2 accounts for . and .. + + echo "
    "; + + // All dirs + if ($_POST['show_parent'] == "true" ) echo ""; + foreach( $files as $file ) { + if( file_exists($postDir . $file) && $file != '.' && $file != '..' ) { + if( is_dir($postDir . $file) ) { + $htmlRel = htmlentities($returnDir . $file, ENT_QUOTES); + $htmlName = htmlentities((strlen($file) > 33) ? substr($file,0,33).'...' : $file); + + echo ""; + } + } + } + + // All files + foreach( $files as $file ) { + if( file_exists($postDir . $file) && $file != '.' && $file != '..' ) { + if( !is_dir($postDir . $file) ) { + $htmlRel = htmlentities($returnDir . $file, ENT_QUOTES); + $htmlName = htmlentities($file); + $ext = strtolower(preg_replace('/^.*\./', '', $file)); + + foreach ($filters as $filter) { + if (empty($filter) | $ext==$filter) { + echo "
  • {$checkbox}" . $htmlName . "
  • "; + } + } + } + } + } + + echo "
"; + } +} + +?> diff --git a/plugins/dynamix/include/FileUpload.php b/plugins/dynamix/include/FileUpload.php new file mode 100644 index 000000000..ed741ede9 --- /dev/null +++ b/plugins/dynamix/include/FileUpload.php @@ -0,0 +1,29 @@ + diff --git a/plugins/dynamix/include/Helpers.php b/plugins/dynamix/include/Helpers.php new file mode 100644 index 000000000..c9802e6b9 --- /dev/null +++ b/plugins/dynamix/include/Helpers.php @@ -0,0 +1,206 @@ + +=10000 ? $comma : '')); + } else { + $base = $value ? floor(log($value, 1000)) : 0; + if ($scale>0 && $base>$scale) $base = $scale; + $unit = $units[$base]; + $value = round($value/pow(1000, $base), $precision ? $precision : 2); + return number_format($value, $precision ? $precision : (($value-intval($value)==0 || $value>=100) ? 0 : ($value>=10 ? 1 : 2)), $dot, ($value>=10000 ? $comma : '')); + } +} +function my_number($value) { + global $display; + $number = $display['number']; + $dot = substr($number,0,1); + $comma = substr($number,1,1); + return number_format($value, 0, $dot, ($value>=10000 ? $comma : '')); +} +function my_time($time, $fmt = NULL) { + global $display; + if (!$fmt) $fmt = $display['date'].($display['date']!='%c' ? ", {$display['time']}" : ""); + return $time ? strftime($fmt, $time) : "unset"; +} +function my_temp($value) { + global $display; + $unit = $display['unit']; + $dot = substr($display['number'],0,1); + return is_numeric($value) ? (($unit=='C' ? str_replace('.', $dot, $value) : round(9/5*$value+32))." $unit") : $value; +} +function my_disk($name) { + return ucfirst(preg_replace(array('/^(parity|disk|cache)([0-9]+)/','/^parity,cache,disk/'),array('$1 $2','Parity, Cache, Disk '),$name)); +} +function my_word($num) { + $words = array('zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen','nineteen','twenty'); + return $num0) { + $used = $arraysize ? 100-round(100*$arrayfree/$arraysize) : 0; + echo "
{$used}%
"; + } else { + echo "
".($var['fsState']=='Started'?'Maintenance':'off-line')."
"; + } +} +function usage_color($limit,$free) { + global $display; + if ($display['text']==1 || intval($display['text']/10)==1) return ''; + if (!$free) { + if ($limit>=$display['critical']) return 'redbar'; + if ($limit>=$display['warning']) return 'orangebar'; + return 'greenbar'; + } else { + if ($limit<=100-$display['critical']) return 'redbar'; + if ($limit<=100-$display['warning']) return 'orangebar'; + return 'greenbar'; + } +} +function my_check($time) { + global $var; + if (!$time) return "unavailable (system reboot or log rotation)"; + $days = floor($time/86400); + $hmss = $time-$days*86400; + $hour = floor($hmss/3600); + $mins = $hmss/60%60; + $secs = $hmss%60; + return plus($days,'day',($hour|$mins|$secs)==0).plus($hour,'hour',($mins|$secs)==0).plus($mins,'minute',$secs==0).plus($secs,'second',true).". Average speed: ".my_scale($var['mdResyncSize']*1024/$time,$unit,1)." $unit/sec"; +} +function my_error($code) { + switch ($code) { + case -4: + return "user abort"; + default: + return "$code"; + } +} +function mk_option($select, $value, $text, $extra = "") { + return ""; +} +function mk_option_check($name, $value, $text = "") { + if ($text) { + $checked = strpos("$name,", "$value,")===false ? "" : " selected"; + return ""; + } + if (strpos($name, 'disk')!==false) { + $checked = strpos("$value,", "$name,")===false ? "" : " selected"; + return ""; + } +} +function day_count($time) { + $now = new DateTime("@".intval(time()/86400)*86400); + $last = new DateTime("@".intval($time/86400)*86400); + $days = date_diff($last,$now)->format('%a'); + switch (true) { + case ($days<0): + return ""; + case ($days==0): + return " (today)"; + case ($days==1): + return " (yesterday)"; + case ($days<=31): + return " (".my_word($days)." days ago)"; + case ($days<=61): + return " ($days days ago)"; + case ($days>61): + return " ($days days ago)"; + } +} +function plus($val, $word, $last) { + return $val>0 ? (($val || $last) ? ($val.' '.$word.($val!=1?'s':'').($last ?'':', ')) : '') : ''; +} +function urlencode_path($path) { + return str_replace("%2F", "/", urlencode($path)); +} +function pgrep($process_name) { + $pid = exec("pgrep $process_name", $output, $retval); + return $retval == 0 ? $pid : false; +} +function input_secure_users($sec) { + global $name, $users; + echo ""; + $write_list = explode(",", $sec[$name]['writeList']); + foreach ($users as $user) { + $idx = $user['idx']; + if ($user['name'] == "root") { + echo ""; + continue; + } + if (in_array( $user['name'], $write_list)) + $userAccess = "read-write"; + else + $userAccess = "read-only"; + echo ""; + echo ""; + } + echo "
{$user['name']}
"; +} +function input_private_users($sec) { + global $name, $users; + echo ""; + $read_list = explode(",", $sec[$name]['readList']); + $write_list = explode(",", $sec[$name]['writeList']); + foreach ($users as $user) { + $idx = $user['idx']; + if ($user['name'] == "root") { + echo ""; + continue; + } + if (in_array( $user['name'], $read_list)) + $userAccess = "read-only"; + elseif (in_array( $user['name'], $write_list)) + $userAccess = "read-write"; + else + $userAccess = "no-access"; + echo ""; + echo ""; + } + echo "
{$user['name']}
"; +} +function is_block($path) { + return (@filetype(realpath($path)) == 'block'); +} +function autov($file) { + global $docroot; + clearstatcache(true, $docroot.$file); + echo "$file?v=".filemtime($docroot.$file); +} +?> diff --git a/plugins/dynamix/include/InstallKey.php b/plugins/dynamix/include/InstallKey.php new file mode 100644 index 000000000..a72cc9964 --- /dev/null +++ b/plugins/dynamix/include/InstallKey.php @@ -0,0 +1,35 @@ + +addLog('$line');"; } + +readfile("/usr/local/emhttp/logging.htm"); +$var = parse_ini_file('state/var.ini'); + +$parsed_url = parse_url($_GET['url']); +if (($parsed_url['host']=="keys.lime-technology.com")||($parsed_url['host']=="lime-technology.com")) { + addLog("Downloading {$_GET['url']} ... "); + $key_file = basename($_GET['url']); + exec("/usr/bin/wget -q -O /boot/config/$key_file {$_GET['url']}", $output, $return_var); + if ($return_var === 0) { + if ($var['mdState'] == "STARTED") + addLog("
Installing ... Please Stop array to complete key installation.
"); + else + addLog("
Installed ...
"); + } + else { + addLog("ERROR ($return_var)
"); + } +} +else + addLog("ERROR, bad or missing key file URL: {$_GET['url']}
"); +?> diff --git a/plugins/dynamix/include/Markdown.php b/plugins/dynamix/include/Markdown.php new file mode 100644 index 000000000..a151a42c2 --- /dev/null +++ b/plugins/dynamix/include/Markdown.php @@ -0,0 +1,3267 @@ + +# +# PHP Markdown & Extra +# Copyright (c) 2004-2013 Michel Fortin +# +# +# Original Markdown +# Copyright (c) 2004-2006 John Gruber +# +# + +define( 'MARKDOWN_VERSION', "1.0.1q" ); # 11 Apr 2013 +define( 'MARKDOWNEXTRA_VERSION', "1.2.7" ); # 11 Apr 2013 + +# +# Global default settings: +# + +# Change to ">" for HTML output +@define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />"); + +# Define the width of a tab for code blocks. +@define( 'MARKDOWN_TAB_WIDTH', 4 ); + +# Optional title attribute for footnote links and backlinks. +@define( 'MARKDOWN_FN_LINK_TITLE', "" ); +@define( 'MARKDOWN_FN_BACKLINK_TITLE', "" ); + +# Optional class attribute for footnote links and backlinks. +@define( 'MARKDOWN_FN_LINK_CLASS', "" ); +@define( 'MARKDOWN_FN_BACKLINK_CLASS', "" ); + +# Optional class prefix for fenced code block. +@define( 'MARKDOWN_CODE_CLASS_PREFIX', "" ); + +# Class attribute for code blocks goes on the `code` tag; +# setting this to true will put attributes on the `pre` tag instead. +@define( 'MARKDOWN_CODE_ATTR_ON_PRE', false ); + +# +# WordPress settings: +# + +# Change to false to remove Markdown from posts and/or comments. +@define( 'MARKDOWN_WP_POSTS', true ); +@define( 'MARKDOWN_WP_COMMENTS', true ); + +### Standard Function Interface ### + +@define( 'MARKDOWN_PARSER_CLASS', 'MarkdownExtra_Parser' ); + +function Markdown($text) { +# +# Initialize the parser and return the result of its transform method. +# + # Setup static parser variable. + static $parser; + if (!isset($parser)) { + $parser_class = MARKDOWN_PARSER_CLASS; + $parser = new $parser_class; + } + + # Transform text using parser. + return $parser->transform($text); +} + +### WordPress Plugin Interface ### + +/* +Plugin Name: Markdown Extra +Plugin Name: Markdown +Plugin URI: http://michelf.ca/projects/php-markdown/ +Description: Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More... +Version: 1.2.7 +Author: Michel Fortin +Author URI: http://michelf.ca/ +*/ + +if (isset($wp_version)) { + # More details about how it works here: + # + + # Post content and excerpts + # - Remove WordPress paragraph generator. + # - Run Markdown on excerpt, then remove all tags. + # - Add paragraph tag around the excerpt, but remove it for the excerpt rss. + if (MARKDOWN_WP_POSTS) { + remove_filter('the_content', 'wpautop'); + remove_filter('the_content_rss', 'wpautop'); + remove_filter('the_excerpt', 'wpautop'); + add_filter('the_content', 'mdwp_MarkdownPost', 6); + add_filter('the_content_rss', 'mdwp_MarkdownPost', 6); + add_filter('get_the_excerpt', 'mdwp_MarkdownPost', 6); + add_filter('get_the_excerpt', 'trim', 7); + add_filter('the_excerpt', 'mdwp_add_p'); + add_filter('the_excerpt_rss', 'mdwp_strip_p'); + + remove_filter('content_save_pre', 'balanceTags', 50); + remove_filter('excerpt_save_pre', 'balanceTags', 50); + add_filter('the_content', 'balanceTags', 50); + add_filter('get_the_excerpt', 'balanceTags', 9); + } + + # Add a footnote id prefix to posts when inside a loop. + function mdwp_MarkdownPost($text) { + static $parser; + if (!$parser) { + $parser_class = MARKDOWN_PARSER_CLASS; + $parser = new $parser_class; + } + if (is_single() || is_page() || is_feed()) { + $parser->fn_id_prefix = ""; + } else { + $parser->fn_id_prefix = get_the_ID() . "."; + } + return $parser->transform($text); + } + + # Comments + # - Remove WordPress paragraph generator. + # - Remove WordPress auto-link generator. + # - Scramble important tags before passing them to the kses filter. + # - Run Markdown on excerpt then remove paragraph tags. + if (MARKDOWN_WP_COMMENTS) { + remove_filter('comment_text', 'wpautop', 30); + remove_filter('comment_text', 'make_clickable'); + add_filter('pre_comment_content', 'Markdown', 6); + add_filter('pre_comment_content', 'mdwp_hide_tags', 8); + add_filter('pre_comment_content', 'mdwp_show_tags', 12); + add_filter('get_comment_text', 'Markdown', 6); + add_filter('get_comment_excerpt', 'Markdown', 6); + add_filter('get_comment_excerpt', 'mdwp_strip_p', 7); + + global $mdwp_hidden_tags, $mdwp_placeholders; + $mdwp_hidden_tags = explode(' ', + '

 
  • '); + $mdwp_placeholders = explode(' ', str_rot13( + 'pEj07ZbbBZ U1kqgh4w4p pre2zmeN6K QTi31t9pre ol0MP1jzJR '. + 'ML5IjmbRol ulANi1NsGY J7zRLJqPul liA8ctl16T K9nhooUHli')); + } + + function mdwp_add_p($text) { + if (!preg_match('{^$|^<(p|ul|ol|dl|pre|blockquote)>}i', $text)) { + $text = '

    '.$text.'

    '; + $text = preg_replace('{\n{2,}}', "

    \n\n

    ", $text); + } + return $text; + } + + function mdwp_strip_p($t) { return preg_replace('{}i', '', $t); } + + function mdwp_hide_tags($text) { + global $mdwp_hidden_tags, $mdwp_placeholders; + return str_replace($mdwp_hidden_tags, $mdwp_placeholders, $text); + } + function mdwp_show_tags($text) { + global $mdwp_hidden_tags, $mdwp_placeholders; + return str_replace($mdwp_placeholders, $mdwp_hidden_tags, $text); + } +} + +### bBlog Plugin Info ### + +function identify_modifier_markdown() { + return array( + 'name' => 'markdown', + 'type' => 'modifier', + 'nicename' => 'PHP Markdown Extra', + 'description' => 'A text-to-HTML conversion tool for web writers', + 'authors' => 'Michel Fortin and John Gruber', + 'licence' => 'GPL', + 'version' => MARKDOWNEXTRA_VERSION, + 'help' => 'Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...', + ); +} + +### Smarty Modifier Interface ### + +function smarty_modifier_markdown($text) { + return Markdown($text); +} + +### Textile Compatibility Mode ### + +# Rename this file to "classTextile.php" and it can replace Textile everywhere. + +if (strcasecmp(substr(__FILE__, -16), "classTextile.php") == 0) { + # Try to include PHP SmartyPants. Should be in the same directory. + @include_once 'smartypants.php'; + # Fake Textile class. It calls Markdown instead. + class Textile { + function TextileThis($text, $lite='', $encode='') { + if ($lite == '' && $encode == '') $text = Markdown($text); + if (function_exists('SmartyPants')) $text = SmartyPants($text); + return $text; + } + # Fake restricted version: restrictions are not supported for now. + function TextileRestricted($text, $lite='', $noimage='') { + return $this->TextileThis($text, $lite); + } + # Workaround to ensure compatibility with TextPattern 4.0.3. + function blockLite($text) { return $text; } + } +} + +# +# Markdown Parser Class +# + +class Markdown_Parser { + + ### Configuration Variables ### + + # Change to ">" for HTML output. + var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; + var $tab_width = MARKDOWN_TAB_WIDTH; + + # Change to `true` to disallow markup or entities. + var $no_markup = false; + var $no_entities = false; + + # Predefined urls and titles for reference links and images. + var $predef_urls = array(); + var $predef_titles = array(); + + ### Parser Implementation ### + + # Regex to match balanced [brackets]. + # Needed to insert a maximum bracked depth while converting to PHP. + var $nested_brackets_depth = 6; + var $nested_brackets_re; + + var $nested_url_parenthesis_depth = 4; + var $nested_url_parenthesis_re; + + # Table of hash values for escaped characters: + var $escape_chars = '\`*_{}[]()>#+-.!'; + var $escape_chars_re; + + function Markdown_Parser() { + # + # Constructor function. Initialize appropriate member variables. + # + $this->_initDetab(); + $this->prepareItalicsAndBold(); + + $this->nested_brackets_re = + str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). + str_repeat('\])*', $this->nested_brackets_depth); + + $this->nested_url_parenthesis_re = + str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). + str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + + $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + + # Sort document, block, and span gamut in ascendent priority order. + asort($this->document_gamut); + asort($this->block_gamut); + asort($this->span_gamut); + } + + # Internal hashes used during transformation. + var $urls = array(); + var $titles = array(); + var $html_hashes = array(); + + # Status flag to avoid invalid nesting. + var $in_anchor = false; + + function setup() { + # + # Called before the transformation process starts to setup parser + # states. + # + # Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); + + $this->in_anchor = false; + } + + function teardown() { + # + # Called after the transformation process to clear any variable + # which may be taking up memory unnecessarly. + # + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } + + function transform($text) { + # + # Main function. Performs some preprocessing on the input text + # and pass it through the document gamut. + # + $this->setup(); + + # Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + + # Standardize line endings: + # DOS to Unix and Mac to Unix + $text = preg_replace('{\r\n?}', "\n", $text); + + # Make sure $text ends with a couple of newlines: + $text .= "\n\n"; + + # Convert all tabs to spaces. + $text = $this->detab($text); + + # Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks($text); + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ ]*\n+/ . + $text = preg_replace('/^[ ]+$/m', '', $text); + + # Run document gamut methods. + foreach ($this->document_gamut as $method => $priority) { + $text = $this->$method($text); + } + + $this->teardown(); + + return $text . "\n"; + } + + var $document_gamut = array( + # Strip link definitions, store in hashes. + "stripLinkDefinitions" => 20, + + "runBasicBlockGamut" => 30, + ); + + function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:\n+|\Z) + }xm', + array(&$this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + return ''; # String that will replace the block + } + + function hashHTMLBlocks($text) { + if ($this->no_markup) return $text; + + $less_than_tab = $this->tab_width - 1; + + # Hashify HTML blocks: + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap

    s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded: + # + # * List "a" is made of tags which can be both inline or block-level. + # These will be treated block-level when the start tag is alone on + # its line, otherwise they're not matched here and will be taken as + # inline later. + # * List "b" is made of tags which are always block-level; + # + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. + 'script|noscript|form|fieldset|iframe|math|svg|'. + 'article|section|nav|aside|hgroup|header|footer|'. + 'figure'; + + # Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' + (?> # optional tag attributes + \s # starts with whitespace + (?> + [^>"/]+ # text outside quotes + | + /+(?!>) # slash not followed by ">" + | + "[^"]*" # text inside double quotes (tolerate ">") + | + \'[^\']*\' # text inside single quotes (tolerate ">") + )* + )? + '; + $content = + str_repeat(' + (?> + [^<]+ # content without tag + | + <\2 # nested opening tag + '.$attr.' # attributes + (?> + /> + | + >', $nested_tags_level). # end of opening tag + '.*?'. # last level nested tag content + str_repeat(' + # closing nested tag + ) + | + <(?!/\2\s*> # other tags with a different name + ) + )*', + $nested_tags_level); + $content2 = str_replace('\2', '\3', $content); + + # First, look for nested blocks, e.g.: + #

    + #
    + # tags for inner block must be indented. + #
    + #
    + # + # The outermost tags must start at the left margin for this to match, and + # the inner nested divs must be indented. + # We need to do this before the next, more liberal match, because the next + # match will start at the first `
    ` and stop at the first `
    `. + $text = preg_replace_callback('{(?> + (?> + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + + # Match from `\n` to `\n`, handling nested tags + # in between. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_b_re.')# start tag = $2 + '.$attr.'> # attributes followed by > and \n + '.$content.' # content, support nesting + # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special version for tags of group a. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_a_re.')# start tag = $3 + '.$attr.'>[ ]*\n # attributes followed by > + '.$content2.' # content, support nesting + # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for
    . It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,'.$less_than_tab.'} + <(hr) # start tag = $2 + '.$attr.' # attributes + /?> # the matching end tag + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # Special case for standalone HTML comments: + + [ ]{0,'.$less_than_tab.'} + (?s: + + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # PHP and ASP-style processor instructions ( + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + ) + )}Sxmi', + array(&$this, '_hashHTMLBlocks_callback'), + $text); + + return $text; + } + function _hashHTMLBlocks_callback($matches) { + $text = $matches[1]; + $key = $this->hashBlock($text); + return "\n\n$key\n\n"; + } + + function hashPart($text, $boundary = 'X') { + # + # Called whenever a tag must be hashed when a function insert an atomic + # element in the text stream. Passing $text to through this function gives + # a unique text-token which will be reverted back when calling unhash. + # + # The $boundary argument specify what character should be used to surround + # the token. By convension, "B" is used for block elements that needs not + # to be wrapped into paragraph tags at the end, ":" is used for elements + # that are word separators and "X" is used in the general case. + # + # Swap back any tag hash found in $text so we do not have to `unhash` + # multiple times at the end. + $text = $this->unhash($text); + + # Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++$i . $boundary; + $this->html_hashes[$key] = $text; + return $key; # String that will replace the tag. + } + + function hashBlock($text) { + # + # Shortcut function for hashPart with block-level boundaries. + # + return $this->hashPart($text, 'B'); + } + + var $block_gamut = array( + # + # These are all the transformations that form block-level + # tags like paragraphs, headers, and list items. + # + "doHeaders" => 10, + "doHorizontalRules" => 20, + + "doLists" => 40, + "doCodeBlocks" => 50, + "doBlockQuotes" => 60, + ); + + function runBlockGamut($text) { + # + # Run block gamut tranformations. + # + # We need to escape raw HTML in Markdown source before doing anything + # else. This need to be done for each block, and not only at the + # begining in the Markdown function since hashed blocks can be part of + # list items and could have been indented. Indented blocks would have + # been seen as a code block in a previous pass of hashHTMLBlocks. + $text = $this->hashHTMLBlocks($text); + + return $this->runBasicBlockGamut($text); + } + + function runBasicBlockGamut($text) { + # + # Run block gamut tranformations, without hashing HTML blocks. This is + # useful when HTML blocks are known to be already hashed, like in the first + # whole-document pass. + # + foreach ($this->block_gamut as $method => $priority) { + $text = $this->$method($text); + } + + # Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs($text); + + return $text; + } + + function doHorizontalRules($text) { + # Do Horizontal Rules: + return preg_replace( + '{ + ^[ ]{0,3} # Leading space + ([-*_]) # $1: First marker + (?> # Repeated marker group + [ ]{0,2} # Zero, one, or two spaces. + \1 # Marker character + ){2,} # Group repeated at least twice + [ ]* # Tailing spaces + $ # End of line. + }mx', + "\n".$this->hashBlock("empty_element_suffix")."\n", + $text); + } + + var $span_gamut = array( + # + # These are all the transformations that occur *within* block-level + # tags like paragraphs, headers, and list items. + # + # Process character escapes, code spans, and inline HTML + # in one shot. + "parseSpan" => -30, + + # Process anchor and image tags. Images must come first, + # because ![foo][f] looks like an anchor. + "doImages" => 10, + "doAnchors" => 20, + + # Make links out of things like `` + # Must come after doAnchors, because you can use < and > + # delimiters in inline links like [this](). + "doAutoLinks" => 30, + "encodeAmpsAndAngles" => 40, + + "doItalicsAndBold" => 50, + "doHardBreaks" => 60, + ); + + function runSpanGamut($text) { + # + # Run span gamut tranformations. + # + foreach ($this->span_gamut as $method => $priority) { + $text = $this->$method($text); + } + + return $text; + } + + function doHardBreaks($text) { + # Do hard breaks: + return preg_replace_callback('/ {2,}\n/', + array(&$this, '_doHardBreaks_callback'), $text); + } + function _doHardBreaks_callback($matches) { + return $this->hashPart("empty_element_suffix\n"); + } + + function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + ) + }xs', + array(&$this, '_doAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link text][1] + # or [link text](/foo) + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeAttribute($url); + + $result = "titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $url = $this->encodeAttribute($url); + + $result = "encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + + return $this->hashPart($result); + } + + function doImages($text) { + # + # Turn Markdown image shortcuts into tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array(&$this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + ) + }xs', + array(&$this, '_doImages_inline_callback'), $text); + + return $text; + } + function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeAttribute($this->urls[$link_id]); + $result = "\"$alt_text\"";titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeAttribute($url); + $result = "\"$alt_text\"";encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + function doHeaders($text) { + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + # + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 + # ## Header 2 + # ## Header 2 with closing hashes ## + # ... + # ###### Header 6 + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + function _doHeaders_callback_setext($matches) { + # Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) + return $matches[0]; + + $level = $matches[2]{0} == '=' ? 1 : 2; + $block = "".$this->runSpanGamut($matches[1]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $block = "".$this->runSpanGamut($matches[2]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + function doLists($text) { + # + # Form HTML ordered (numbered) and unordered (bulleted) lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $markers_relist = array( + $marker_ul_re => $marker_ol_re, + $marker_ol_re => $marker_ul_re, + ); + + foreach ($markers_relist as $marker_re => $other_marker_re) { + # Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces + ('.$marker_re.') # $4 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $5 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + | + (?= # Lookahead for another kind of list + \n + \3 # Must have the same indentation + '.$other_marker_re.'[ ]+ + ) + ) + ) + '; // mx + + # We use a different prefix before nested lists than top-level lists. + # See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + } + + return $text; + } + function _doLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $result = $this->hashBlock("<$list_type>\n" . $result . ""); + return "\n". $result ."\n\n"; + } + + var $list_level = 0; + + function processListItems($list_str, $marker_any_re) { + # + # Process the contents of a single ordered or unordered list, splitting it + # into individual list items. + # + # The $this->list_level global keeps track of when we're inside a list. + # Each time we enter a list, we increment it; when we leave a list, + # we decrement. If it's zero, we're not in a list anymore. + # + # We do this because when we're not inside a list, we want to treat + # something like this: + # + # I recommend upgrading to version + # 8. Oops, now this line is treated + # as a sub-list. + # + # As a single paragraph, despite the fact that the second line starts + # with a digit-period-space sequence. + # + # Whereas when we're inside a list (or sub-list), that line will be + # treated as the start of a sub-list. What a kludge, huh? This is + # an aspect of Markdown's syntax that's hard to parse perfectly + # without resorting to mind-reading. Perhaps the solution is to + # change the syntax rules such that sub-lists must start with a + # starting cardinal number; e.g. "1." or "a.". + + $this->list_level++; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array(&$this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + # Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } + else { + # Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = preg_replace('/\n+$/', '', $item); + $item = $this->runSpanGamut($item); + } + + return "
  • " . $item . "
  • \n"; + } + + function doCodeBlocks($text) { + # + # Process Markdown `
    ` blocks.
    +	#
    +		$text = preg_replace_callback('{
    +				(?:\n\n|\A\n?)
    +				(	            # $1 = the code block -- one or more lines, starting with a space/tab
    +				  (?>
    +					[ ]{'.$this->tab_width.'}  # Lines must start with a tab or a tab-width of spaces
    +					.*\n+
    +				  )+
    +				)
    +				((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
    +			}xm',
    +			array(&$this, '_doCodeBlocks_callback'), $text);
    +
    +		return $text;
    +	}
    +	function _doCodeBlocks_callback($matches) {
    +		$codeblock = $matches[1];
    +
    +		$codeblock = $this->outdent($codeblock);
    +		$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
    +
    +		# trim leading newlines and trailing newlines
    +		$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
    +
    +		$codeblock = "
    $codeblock\n
    "; + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + + function makeCodeSpan($code) { + # + # Create a code span markup for $code. Called from handleSpanToken. + # + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + return $this->hashPart("$code"); + } + + var $em_relist = array( + '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + # Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + # Construct master expression from list. + $token_re = '{('. implode('|', $token_relist) .')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + function doItalicsAndBold($text) { + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + # + # Get prepared regular expression for seraching emphasis tokens + # in current context. + # + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + # + # Each loop iteration search for the next emphasis token. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + # Reached end of text span: empty stack without emitting. + # any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + # Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + # Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + # Other closing marker: close one em or strong and + # change current token state to match the other + $token_stack[0] = str_repeat($token{0}, 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + # Reached closing marker for both em and strong. + # Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + } else { + # Reached opening three-char emphasis marker. Push on token + # stack; will be handled by the special condition above. + $em = $token{0}; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + # Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + # Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + # Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + # Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + return $text_stack[0]; + } + + function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array(&$this, '_doBlockQuotes_callback'), $text); + + return $text; + } + function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + # trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); # recurse + + $bq = preg_replace('/^/m', " ", $bq); + # These leading spaces cause problem with
     content,
    +		# so we need to fix that:
    +		$bq = preg_replace_callback('{(\s*
    .+?
    )}sx', + array(&$this, '_doBlockQuotes_callback2'), $bq); + + return "\n". $this->hashBlock("
    \n$bq\n
    ")."\n\n"; + } + function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + function formParagraphs($text) { + # + # Params: + # $text - string to process with html

    tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap

    tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + # Is a paragraph. + $value = $this->runSpanGamut($value); + $value = preg_replace('/^([ ]*)/', "

    ", $value); + $value .= "

    "; + $grafs[$key] = $this->unhash($value); + } + else { + # Is a block. + # Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 =
    tag +//
    ]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (
    ) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// # We can't call Markdown(), because that resets the hash; +// # that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// # Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + function encodeAttribute($text) { + # + # Encode text for a double-quoted HTML attribute. This function + # is *not* suitable for attributes enclosed in single quotes. + # + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '"', $text); + return $text; + } + + function encodeAmpsAndAngles($text) { + # + # Smart processing for ampersands and angle brackets that need to + # be encoded. Valid character entities are left alone unless the + # no-entities mode is set. + # + if ($this->no_entities) { + $text = str_replace('&', '&', $text); + } else { + # Ampersand-encoding based entirely on Nat Irons's Amputator + # MT plugin: + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&', $text);; + } + # Encode remaining <'s + $text = str_replace('<', '<', $text); + + return $text; + } + + function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i', + array(&$this, '_doAutoLinks_url_callback'), $text); + + # Email addresses: + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + (?: + [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ + | + ".*?" + ) + \@ + (?: + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + | + \[[\d.a-fA-F:]+\] # IPv4 & IPv6 + ) + ) + > + }xi', + array(&$this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + function _doAutoLinks_url_callback($matches) { + $url = $this->encodeAttribute($matches[1]); + $link = "$url"; + return $this->hashPart($link); + } + function _doAutoLinks_email_callback($matches) { + $address = $matches[1]; + $link = $this->encodeEmailAddress($address); + return $this->hashPart($link); + } + + function encodeEmailAddress($addr) { + # + # Input: an email address, e.g. "foo@example.com" + # + # Output: the email address as a mailto link, with each character + # of the address encoded as either a decimal or hex entity, in + # the hopes of foiling most address harvesting spam bots. E.g.: + # + #

    foo@exampl + # e.com

    + # + # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + # With some optimizations by Milian Wolff. + # + $addr = "mailto:" . $addr; + $chars = preg_split('/(? $char) { + $ord = ord($char); + # Ignore non-ascii chars. + if ($ord < 128) { + $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. + # roughly 10% raw, 45% hex, 45% dec + # '@' *must* be encoded. I insist. + if ($r > 90 && $char != '@') /* do nothing */; + else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; + else $chars[$key] = '&#'.$ord.';'; + } + } + + $addr = implode('', $chars); + $text = implode('', array_slice($chars, 7)); # text without `mailto:` + $addr = "$text"; + + return $addr; + } + + function parseSpan($str) { + # + # Take the string $str and parse it into tokens, hashing embeded HTML, + # escaped characters and handling code spans. + # + $output = ''; + + $span_re = '{ + ( + \\\\'.$this->escape_chars_re.' + | + (?no_markup ? '' : ' + | + # comment + | + <\?.*?\?> | <%.*?%> # processing instruction + | + <[!$]?[-a-zA-Z0-9:_]+ # regular tags + (?> + \s + (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* + )? + > + | + <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag + | + # closing tag + ').' + ) + }xs'; + + while (1) { + # + # Each loop iteration seach for either the next tag, the next + # openning code span marker, or the next escaped character. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); + + # Create token from text preceding tag. + if ($parts[0] != "") { + $output .= $parts[0]; + } + + # Check if we reach the end. + if (isset($parts[1])) { + $output .= $this->handleSpanToken($parts[1], $parts[2]); + $str = $parts[2]; + } + else { + break; + } + } + + return $output; + } + + function handleSpanToken($token, &$str) { + # + # Handle $token provided by parseSpan by determining its nature and + # returning the corresponding value that should replace it. + # + switch ($token{0}) { + case "\\": + return $this->hashPart("&#". ord($token{1}). ";"); + case "`": + # Search for end marker in remaining text. + if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', + $str, $matches)) + { + $str = $matches[2]; + $codespan = $this->makeCodeSpan($matches[1]); + return $this->hashPart($codespan); + } + return $token; // return as text since no ending marker found. + default: + return $this->hashPart($token); + } + } + + function outdent($text) { + # + # Remove one level of line-leading tabs or spaces + # + return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); + } + + # String length function for detab. `_initDetab` will create a function to + # hanlde UTF-8 if the default function does not exist. + var $utf8_strlen = 'mb_strlen'; + + function detab($text) { + # + # Replace tabs with the appropriate amount of space. + # + # For each line we separate the line in blocks delemited by + # tab characters. Then we reconstruct every line by adding the + # appropriate number of space between each blocks. + + $text = preg_replace_callback('/^.*\t.*$/m', + array(&$this, '_detab_callback'), $text); + + return $text; + } + function _detab_callback($matches) { + $line = $matches[0]; + $strlen = $this->utf8_strlen; # strlen function for UTF-8. + + # Split in blocks. + $blocks = explode("\t", $line); + # Add each blocks to the line. + $line = $blocks[0]; + unset($blocks[0]); # Do not add first block twice. + foreach ($blocks as $block) { + # Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - + $strlen($line, 'UTF-8') % $this->tab_width; + $line .= str_repeat(" ", $amount) . $block; + } + return $line; + } + function _initDetab() { + # + # Check for the availability of the function in the `utf8_strlen` property + # (initially `mb_strlen`). If the function is not available, create a + # function that will loosely count the number of UTF-8 characters with a + # regular expression. + # + if (function_exists($this->utf8_strlen)) return; + $this->utf8_strlen = create_function('$text', 'return preg_match_all( + "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", + $text, $m);'); + } + + function unhash($text) { + # + # Swap back in all the tags hashed by _HashHTMLBlocks. + # + return preg_replace_callback('/(.)\x1A[0-9]+\1/', + array(&$this, '_unhash_callback'), $text); + } + function _unhash_callback($matches) { + return $this->html_hashes[$matches[0]]; + } + +} + +# +# Markdown Extra Parser Class +# + +class MarkdownExtra_Parser extends Markdown_Parser { + + ### Configuration Variables ### + + # Prefix for footnote ids. + var $fn_id_prefix = ""; + + # Optional title attribute for footnote links and backlinks. + var $fn_link_title = MARKDOWN_FN_LINK_TITLE; + var $fn_backlink_title = MARKDOWN_FN_BACKLINK_TITLE; + + # Optional class attribute for footnote links and backlinks. + var $fn_link_class = MARKDOWN_FN_LINK_CLASS; + var $fn_backlink_class = MARKDOWN_FN_BACKLINK_CLASS; + + # Optional class prefix for fenced code block. + var $code_class_prefix = MARKDOWN_CODE_CLASS_PREFIX; + # Class attribute for code blocks goes on the `code` tag; + # setting this to true will put attributes on the `pre` tag instead. + var $code_attr_on_pre = MARKDOWN_CODE_ATTR_ON_PRE; + + # Predefined abbreviations. + var $predef_abbr = array(); + + ### Parser Implementation ### + + function MarkdownExtra_Parser() { + # + # Constructor function. Initialize the parser object. + # + # Add extra escapable characters before parent constructor + # initialize the table. + $this->escape_chars .= ':|'; + + # Insert extra document, block, and span transformations. + # Parent constructor will do the sorting. + $this->document_gamut += array( + "doFencedCodeBlocks" => 5, + "stripFootnotes" => 15, + "stripAbbreviations" => 25, + "appendFootnotes" => 50, + ); + $this->block_gamut += array( + "doFencedCodeBlocks" => 5, + "doTables" => 15, + "doDefLists" => 45, + ); + $this->span_gamut += array( + "doFootnotes" => 5, + "doAbbreviations" => 70, + ); + + parent::Markdown_Parser(); + } + + # Extra variables used during extra transformations. + var $footnotes = array(); + var $footnotes_ordered = array(); + var $footnotes_ref_count = array(); + var $footnotes_numbers = array(); + var $abbr_desciptions = array(); + var $abbr_word_re = ''; + + # Give the current footnote number. + var $footnote_counter = 1; + + function setup() { + # + # Setting up Extra-specific variables. + # + parent::setup(); + + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + $this->footnote_counter = 1; + + foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + } + } + + function teardown() { + # + # Clearing Extra-specific variables. + # + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + + parent::teardown(); + } + + ### Extra Attribute Parser ### + + # Expression to use to catch attributes (includes the braces) + var $id_class_attr_catch_re = '\{((?:[ ]*[#.][-_:a-zA-Z0-9]+){1,})[ ]*\}'; + # Expression to use when parsing in a context when no capture is desired + var $id_class_attr_nocatch_re = '\{(?:[ ]*[#.][-_:a-zA-Z0-9]+){1,}[ ]*\}'; + + function doExtraAttributes($tag_name, $attr) { + # + # Parse attributes caught by the $this->id_class_attr_catch_re expression + # and return the HTML-formatted list of attributes. + # + # Currently supported attributes are .class and #id. + # + if (empty($attr)) return ""; + + # Split on components + preg_match_all('/[#.][-_:a-zA-Z0-9]+/', $attr, $matches); + $elements = $matches[0]; + + # handle classes and ids (only first id taken into account) + $classes = array(); + $id = false; + foreach ($elements as $element) { + if ($element{0} == '.') { + $classes[] = substr($element, 1); + } else if ($element{0} == '#') { + if ($id === false) $id = substr($element, 1); + } + } + + # compose attributes as string + $attr_str = ""; + if (!empty($id)) { + $attr_str .= ' id="'.$id.'"'; + } + if (!empty($classes)) { + $attr_str .= ' class="'.implode(" ", $classes).'"'; + } + return $attr_str; + } + + function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr + (?:\n+|\Z) + }xm', + array(&$this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]); + return ''; # String that will replace the block + } + + ### HTML Block Parser ### + + # Tags that are always treated as block tags: + var $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption'; + + # Tags treated as block tags only if the opening tag is alone on its line: + var $context_block_tags_re = 'style|link|script|noscript|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; + + # Tags where markdown="1" default to span mode: + var $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; + + # Tags which must not have their contents modified, no matter where + # they appear: + var $clean_tags_re = 'script|math|svg'; + + # Tags that do not need to be closed. + var $auto_close_tags_re = 'link|hr|img|param|source|track'; + + function hashHTMLBlocks($text) { + # + # Hashify HTML Blocks and "clean tags". + # + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap

    s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded. + # + # This works by calling _HashHTMLBlocks_InMarkdown, which then calls + # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" + # attribute is found within a tag, _HashHTMLBlocks_InHTML calls back + # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. + # These two functions are calling each other. It's recursive! + # + if ($this->no_markup) return $text; + + # + # Call the HTML-in-Markdown hasher. + # + list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); + + return $text; + } + function _hashHTMLBlocks_inMarkdown($text, $indent = 0, + $enclosing_tag_re = '', $span = false) + { + # + # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. + # + # * $indent is the number of space to be ignored when checking for code + # blocks. This is important because if we don't take the indent into + # account, something like this (which looks right) won't work as expected: + # + #

    + #
    + # Hello World. <-- Is this a Markdown code block or text? + #
    <-- Is this a Markdown code block or a real tag? + #
    + # + # If you don't like this, just don't indent the tag on which + # you apply the markdown="1" attribute. + # + # * If $enclosing_tag_re is not empty, stops at the first unmatched closing + # tag with that name. Nested tags supported. + # + # * If $span is true, text inside must treated as span. So any double + # newline will be replaced by a single newline so that it does not create + # paragraphs. + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to check for the presense of newlines around a block tag. + $newline_before_re = '/(?:^\n?|\n\n)*$/'; + $newline_after_re = + '{ + ^ # Start of text following the tag. + (?>[ ]*)? # Optional comment. + [ ]*\n # Must be followed by newline. + }xs'; + + # Regex to match any tag. + $block_tag_re = + '{ + ( # $2: Capture whole tag. + # Tag name. + '.$this->block_tags_re.' | + '.$this->context_block_tags_re.' | + '.$this->clean_tags_re.' | + (?!\s)'.$enclosing_tag_re.' + ) + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + # CData Block + | + # Code span marker + `+ + '. ( !$span ? ' # If not in span. + | + # Indented code block + (?: ^[ ]*\n | ^ | \n[ ]*\n ) + [ ]{'.($indent+4).'}[^\n]* \n + (?> + (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n + )* + | + # Fenced code block marker + (?<= ^ | \n ) + [ ]{0,'.($indent+3).'}~{3,} + [ ]* + (?: + \.?[-_:a-zA-Z0-9]+ # standalone class name + | + '.$this->id_class_attr_nocatch_re.' # extra attributes + )? + [ ]* + \n + ' : '' ). ' # End (if not is span). + ) + }xs'; + + $depth = 0; # Current depth inside the tag tree. + $parsed = ""; # Parsed text that will be returned. + + # + # Loop through every tag until we find the closing tag of the parent + # or loop until reaching the end of text if no parent tag specified. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($block_tag_re, $text, 2, + PREG_SPLIT_DELIM_CAPTURE); + + # If in Markdown span mode, add a empty-string span-level hash + # after each newline to prevent triggering any block element. + if ($span) { + $void = $this->hashPart("", ':'); + $newline = "$void\n"; + $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; + } + + $parsed .= $parts[0]; # Text before current tag. + + # If end of $text has been reached. Stop loop. + if (count($parts) < 3) { + $text = ""; + break; + } + + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + $tag_re = preg_quote($tag); # For use in a regular expression. + + # + # Check for: Code span marker + # + if ($tag{0} == "`") { + # Find corresponding end marker. + $tag_re = preg_quote($tag); + if (preg_match('{^(?>.+?|\n(?!\n))*?(?.*\n)*?[ ]{'.($fence_indent).'}'.$fence_re.'[ ]*(?:\n|$)}', $text, + $matches)) + { + # End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + # No end marker: just skip it. + $parsed .= $tag; + } + } + # + # Check for: Indented code block. + # + else if ($tag{0} == "\n" || $tag{0} == " ") { + # Indented code block: pass it unchanged, will be handled + # later. + $parsed .= $tag; + } + # + # Check for: Opening Block level tag or + # Opening Context Block tag (like ins and del) + # used as a block tag (tag is alone on it's line). + # + else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) || + ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) && + preg_match($newline_before_re, $parsed) && + preg_match($newline_after_re, $text) ) + ) + { + # Need to parse tag and following text using the HTML parser. + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); + + # Make sure it stays outside of any paragraph by adding newlines. + $parsed .= "\n\n$block_text\n\n"; + } + # + # Check for: Clean tag (like script, math) + # HTML Comments, processing instructions. + # + else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Need to parse tag and following text using the HTML parser. + # (don't check for markdown attribute) + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); + + $parsed .= $block_text; + } + # + # Check for: Tag with same name as enclosing tag. + # + else if ($enclosing_tag_re !== '' && + # Same name as enclosing tag. + preg_match('{^= 0); + + return array($parsed, $text); + } + function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { + # + # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. + # + # * Calls $hash_method to convert any blocks. + # * Stops when the first opening tag closes. + # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. + # (it is not inside clean tags) + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to match `markdown` attribute inside of a tag. + $markdown_attr_re = ' + { + \s* # Eat whitespace before the `markdown` attribute + markdown + \s*=\s* + (?> + (["\']) # $1: quote delimiter + (.*?) # $2: attribute value + \1 # matching delimiter + | + ([^\s>]*) # $3: unquoted attribute value + ) + () # $4: make $3 always defined (avoid warnings) + }xs'; + + # Regex to match any tag. + $tag_re = '{ + ( # $2: Capture whole tag. + + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + # CData Block + ) + }xs'; + + $original_text = $text; # Save original text in case of faliure. + + $depth = 0; # Current depth inside the tag tree. + $block_text = ""; # Temporary text holder for current text. + $parsed = ""; # Parsed text that will be returned. + + # + # Get the name of the starting tag. + # (This pattern makes $base_tag_name_re safe without quoting.) + # + if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) + $base_tag_name_re = $matches[1]; + + # + # Loop through every tag until we find the corresponding closing tag. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (count($parts) < 3) { + # + # End of $text reached with unbalenced tag(s). + # In that case, we return original text unchanged and pass the + # first character as filtered to prevent an infinite loop in the + # parent function. + # + return array($original_text{0}, substr($original_text, 1)); + } + + $block_text .= $parts[0]; # Text before current tag. + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + + # + # Check for: Auto-close tag (like
    ) + # Comments and Processing Instructions. + # + if (preg_match('{^auto_close_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Just add the tag to the block as if it was text. + $block_text .= $tag; + } + else { + # + # Increase/decrease nested tag count. Only do so if + # the tag's name match base tag's. + # + if (preg_match('{^mode = $attr_m[2] . $attr_m[3]; + $span_mode = $this->mode == 'span' || $this->mode != 'block' && + preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag); + + # Calculate indent before tag. + if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { + $strlen = $this->utf8_strlen; + $indent = $strlen($matches[1], 'UTF-8'); + } else { + $indent = 0; + } + + # End preceding block with this tag. + $block_text .= $tag; + $parsed .= $this->$hash_method($block_text); + + # Get enclosing tag name for the ParseMarkdown function. + # (This pattern makes $tag_name_re safe without quoting.) + preg_match('/^<([\w:$]*)\b/', $tag, $matches); + $tag_name_re = $matches[1]; + + # Parse the content using the HTML-in-Markdown parser. + list ($block_text, $text) + = $this->_hashHTMLBlocks_inMarkdown($text, $indent, + $tag_name_re, $span_mode); + + # Outdent markdown text. + if ($indent > 0) { + $block_text = preg_replace("/^[ ]{1,$indent}/m", "", + $block_text); + } + + # Append tag content to parsed text. + if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; + else $parsed .= "$block_text"; + + # Start over with a new block. + $block_text = ""; + } + else $block_text .= $tag; + } + + } while ($depth > 0); + + # + # Hash last block text that wasn't processed inside the loop. + # + $parsed .= $this->$hash_method($block_text); + + return array($parsed, $text); + } + + function hashClean($text) { + # + # Called whenever a tag must be hashed when a function inserts a "clean" tag + # in $text, it passes through this function and is automaticaly escaped, + # blocking invalid nested overlap. + # + return $this->hashPart($text, 'C'); + } + + function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes + ) + }xs', + array(&$this, '_doAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link text][1] + # or [link text](/foo) + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeAttribute($url); + + $result = "titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + + $url = $this->encodeAttribute($url); + + $result = "encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $attr; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + + return $this->hashPart($result); + } + + function doImages($text) { + # + # Turn Markdown image shortcuts into tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array(&$this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes + ) + }xs', + array(&$this, '_doImages_inline_callback'), $text); + + return $text; + } + function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeAttribute($this->urls[$link_id]); + $result = "\"$alt_text\"";titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeAttribute($url); + $result = "\"$alt_text\"";encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $attr; + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + function doHeaders($text) { + # + # Redefined to add id and class attribute support. + # + # Setext-style headers: + # Header 1 {#header1} + # ======== + # + # Header 2 {#header2 .class1 .class2} + # -------- + # + $text = preg_replace_callback( + '{ + (^.+?) # $1: Header text + (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes + [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer + }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 {#header1} + # ## Header 2 {#header2} + # ## Header 2 with closing hashes ## {#header3.class1.class2} + # ... + # ###### Header 6 {.class2} + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes + [ ]* + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + function _doHeaders_callback_setext($matches) { + if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) + return $matches[0]; + $level = $matches[3]{0} == '=' ? 1 : 2; + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]); + $block = "".$this->runSpanGamut($matches[1]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]); + $block = "".$this->runSpanGamut($matches[2]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + function doTables($text) { + # + # Form HTML tables. + # + $less_than_tab = $this->tab_width - 1; + # + # Find tables with leading pipe. + # + # | Header 1 | Header 2 + # | -------- | -------- + # | Cell 1 | Cell 2 + # | Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] # Optional leading pipe (present) + (.+) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + [ ]* # Allowed whitespace. + [|] .* \n # Row content. + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array(&$this, '_doTable_leadingPipe_callback'), $text); + + # + # Find tables without leading pipe. + # + # Header 1 | Header 2 + # -------- | -------- + # Cell 1 | Cell 2 + # Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + (\S.*[|].*) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + .* [|] .* \n # Row content + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array(&$this, '_DoTable_callback'), $text); + + return $text; + } + function _doTable_leadingPipe_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove leading pipe for each row. + $content = preg_replace('/^ *[|]/m', '', $content); + + return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); + } + function _doTable_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove any tailing pipes for each line. + $head = preg_replace('/[|] *$/m', '', $head); + $underline = preg_replace('/[|] *$/m', '', $underline); + $content = preg_replace('/[|] *$/m', '', $content); + + # Reading alignement from header underline. + $separators = preg_split('/ *[|] */', $underline); + foreach ($separators as $n => $s) { + if (preg_match('/^ *-+: *$/', $s)) $attr[$n] = ' align="right"'; + else if (preg_match('/^ *:-+: *$/', $s))$attr[$n] = ' align="center"'; + else if (preg_match('/^ *:-+ *$/', $s)) $attr[$n] = ' align="left"'; + else $attr[$n] = ''; + } + + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $head = $this->parseSpan($head); + $headers = preg_split('/ *[|] */', $head); + $col_count = count($headers); + $attr = array_pad($attr, $col_count, ''); + + # Write column headers. + $text = "\n"; + $text .= "\n"; + $text .= "\n"; + foreach ($headers as $n => $header) + $text .= " ".$this->runSpanGamut(trim($header))."\n"; + $text .= "\n"; + $text .= "\n"; + + # Split content by row. + $rows = explode("\n", trim($content, "\n")); + + $text .= "\n"; + foreach ($rows as $row) { + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $row = $this->parseSpan($row); + + # Split row by cell. + $row_cells = preg_split('/ *[|] */', $row, $col_count); + $row_cells = array_pad($row_cells, $col_count, ''); + + $text .= "\n"; + foreach ($row_cells as $n => $cell) + $text .= " ".$this->runSpanGamut(trim($cell))."\n"; + $text .= "\n"; + } + $text .= "\n"; + $text .= "
    "; + + return $this->hashBlock($text) . "\n"; + } + + function doDefLists($text) { + # + # Form HTML definition lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable pattern to match any entire dl list: + $whole_list_re = '(?> + ( # $1 = whole list + ( # $2 + [ ]{0,'.$less_than_tab.'} + ((?>.*\S.*\n)+) # $3 = defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another term + [ ]{0,'.$less_than_tab.'} + (?: \S.*\n )+? # defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?! # Negative lookahead for another definition + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + ) + ) + )'; // mx + + $text = preg_replace_callback('{ + (?>\A\n?|(?<=\n\n)) + '.$whole_list_re.' + }mx', + array(&$this, '_doDefLists_callback'), $text); + + return $text; + } + function _doDefLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $list = $matches[1]; + + # Turn double returns into triple returns, so that we can make a + # paragraph for the last item in a list, if necessary: + $result = trim($this->processDefListItems($list)); + $result = "
    \n" . $result . "\n
    "; + return $this->hashBlock($result) . "\n\n"; + } + + function processDefListItems($list_str) { + # + # Process the contents of a single definition list, splitting it + # into individual term and definition list items. + # + $less_than_tab = $this->tab_width - 1; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + # Process definition terms. + $list_str = preg_replace_callback('{ + (?>\A\n?|\n\n+) # leading line + ( # definition terms = $1 + [ ]{0,'.$less_than_tab.'} # leading whitespace + (?!\:[ ]|[ ]) # negative lookahead for a definition + # mark (colon) or more whitespace. + (?> \S.* \n)+? # actual term (not whitespace). + ) + (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed + # with a definition mark. + }xm', + array(&$this, '_processDefListItems_callback_dt'), $list_str); + + # Process actual definitions. + $list_str = preg_replace_callback('{ + \n(\n+)? # leading line = $1 + ( # marker space = $2 + [ ]{0,'.$less_than_tab.'} # whitespace before colon + \:[ ]+ # definition mark (colon) + ) + ((?s:.+?)) # definition text = $3 + (?= \n+ # stop at next definition mark, + (?: # next term or end of text + [ ]{0,'.$less_than_tab.'} \:[ ] | +
    | \z + ) + ) + }xm', + array(&$this, '_processDefListItems_callback_dd'), $list_str); + + return $list_str; + } + function _processDefListItems_callback_dt($matches) { + $terms = explode("\n", trim($matches[1])); + $text = ''; + foreach ($terms as $term) { + $term = $this->runSpanGamut(trim($term)); + $text .= "\n
    " . $term . "
    "; + } + return $text . "\n"; + } + function _processDefListItems_callback_dd($matches) { + $leading_line = $matches[1]; + $marker_space = $matches[2]; + $def = $matches[3]; + + if ($leading_line || preg_match('/\n{2,}/', $def)) { + # Replace marker with the appropriate whitespace indentation + $def = str_repeat(' ', strlen($marker_space)) . $def; + $def = $this->runBlockGamut($this->outdent($def . "\n\n")); + $def = "\n". $def ."\n"; + } + else { + $def = rtrim($def); + $def = $this->runSpanGamut($this->outdent($def)); + } + + return "\n
    " . $def . "
    \n"; + } + + function doFencedCodeBlocks($text) { + # + # Adding the fenced code block syntax to regular Markdown: + # + # ~~~ + # Code block + # ~~~ + # + $less_than_tab = $this->tab_width; + + $text = preg_replace_callback('{ + (?:\n|\A) + # 1: Opening marker + ( + ~{3,} # Marker: three tilde or more. + ) + [ ]* + (?: + \.?([-_:a-zA-Z0-9]+) # 2: standalone class name + | + '.$this->id_class_attr_catch_re.' # 3: Extra attributes + )? + [ ]* \n # Whitespace and newline following marker. + + # 4: Content + ( + (?> + (?!\1 [ ]* \n) # Not a closing marker. + .*\n+ + )+ + ) + + # Closing marker. + \1 [ ]* \n + }xm', + array(&$this, '_doFencedCodeBlocks_callback'), $text); + + return $text; + } + function _doFencedCodeBlocks_callback($matches) { + $classname =& $matches[2]; + $attrs =& $matches[3]; + $codeblock = $matches[4]; + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + $codeblock = preg_replace_callback('/^\n+/', + array(&$this, '_doFencedCodeBlocks_newlines'), $codeblock); + + if ($classname != "") { + if ($classname{0} == '.') + $classname = substr($classname, 1); + $attr_str = ' class="'.$this->code_class_prefix.$classname.'"'; + } else { + $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs); + } + $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; + $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; + $codeblock = "$codeblock
    "; + + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + function _doFencedCodeBlocks_newlines($matches) { + return str_repeat("empty_element_suffix", + strlen($matches[0])); + } + + # + # Redefining emphasis markers so that emphasis by underscore does not + # work in the middle of a word. + # + var $em_relist = array( + '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap

    tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + $value = trim($this->runSpanGamut($value)); + + # Check if this should be enclosed in a paragraph. + # Clean tag hashes & block tag hashes are left alone. + $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); + + if ($is_p) { + $value = "

    $value

    "; + } + $grafs[$key] = $value; + } + + # Join grafs in one text, then unhash HTML tags. + $text = implode("\n\n", $grafs); + + # Finish by removing any tag hashes still present in $text. + $text = $this->unhash($text); + + return $text; + } + + ### Footnotes + + function stripFootnotes($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [^id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1 + [ ]* + \n? # maybe *one* newline + ( # text = $2 (no blank lines allowed) + (?: + .+ # actual text + | + \n # newlines but + (?!\[\^.+?\]:\s)# negative lookahead for footnote marker. + (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed + # by non-indented content + )* + ) + }xm', + array(&$this, '_stripFootnotes_callback'), + $text); + return $text; + } + function _stripFootnotes_callback($matches) { + $note_id = $this->fn_id_prefix . $matches[1]; + $this->footnotes[$note_id] = $this->outdent($matches[2]); + return ''; # String that will replace the block + } + + function doFootnotes($text) { + # + # Replace footnote references in $text [^id] with a special text-token + # which will be replaced by the actual footnote marker in appendFootnotes. + # + if (!$this->in_anchor) { + $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); + } + return $text; + } + + function appendFootnotes($text) { + # + # Append footnote list to text. + # + $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array(&$this, '_appendFootnotes_callback'), $text); + + if (!empty($this->footnotes_ordered)) { + $text .= "\n\n"; + $text .= "
    \n"; + $text .= "empty_element_suffix ."\n"; + $text .= "
      \n\n"; + + $attr = " rev=\"footnote\""; + if ($this->fn_backlink_class != "") { + $class = $this->fn_backlink_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_backlink_title != "") { + $title = $this->fn_backlink_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + $num = 0; + + while (!empty($this->footnotes_ordered)) { + $footnote = reset($this->footnotes_ordered); + $note_id = key($this->footnotes_ordered); + unset($this->footnotes_ordered[$note_id]); + $ref_count = $this->footnotes_ref_count[$note_id]; + unset($this->footnotes_ref_count[$note_id]); + unset($this->footnotes[$note_id]); + + $footnote .= "\n"; # Need to append newline before parsing. + $footnote = $this->runBlockGamut("$footnote\n"); + $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array(&$this, '_appendFootnotes_callback'), $footnote); + + $attr = str_replace("%%", ++$num, $attr); + $note_id = $this->encodeAttribute($note_id); + + # Prepare backlink, multiple backlinks if multiple references + $backlink = ""; + for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) { + $backlink .= " "; + } + # Add backlink to last paragraph; create new paragraph if needed. + if (preg_match('{

      $}', $footnote)) { + $footnote = substr($footnote, 0, -4) . " $backlink

      "; + } else { + $footnote .= "\n\n

      $backlink

      "; + } + + $text .= "
    1. \n"; + $text .= $footnote . "\n"; + $text .= "
    2. \n\n"; + } + + $text .= "
    \n"; + $text .= "
    "; + } + return $text; + } + function _appendFootnotes_callback($matches) { + $node_id = $this->fn_id_prefix . $matches[1]; + + # Create footnote marker only if it has a corresponding footnote *and* + # the footnote hasn't been used by another marker. + if (isset($this->footnotes[$node_id])) { + $num =& $this->footnotes_numbers[$node_id]; + if (!isset($num)) { + # Transfer footnote content to the ordered list and give it its + # number + $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; + $this->footnotes_ref_count[$node_id] = 1; + $num = $this->footnote_counter++; + $ref_count_mark = ''; + } else { + $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1; + } + + $attr = " rel=\"footnote\""; + if ($this->fn_link_class != "") { + $class = $this->fn_link_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_link_title != "") { + $title = $this->fn_link_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + + $attr = str_replace("%%", $num, $attr); + $node_id = $this->encodeAttribute($node_id); + + return + "". + "$num". + ""; + } + + return "[^".$matches[1]."]"; + } + + ### Abbreviations ### + + function stripAbbreviations($text) { + # + # Strips abbreviations from text, stores titles in hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [id]*: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 + (.*) # text = $2 (no blank lines allowed) + }xm', + array(&$this, '_stripAbbreviations_callback'), + $text); + return $text; + } + function _stripAbbreviations_callback($matches) { + $abbr_word = $matches[1]; + $abbr_desc = $matches[2]; + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + return ''; # String that will replace the block + } + + function doAbbreviations($text) { + # + # Find defined abbreviations in text and wrap them in elements. + # + if ($this->abbr_word_re) { + // cannot use the /x modifier because abbr_word_re may + // contain significant spaces: + $text = preg_replace_callback('{'. + '(?abbr_word_re.')'. + '(?![\w\x1A])'. + '}', + array(&$this, '_doAbbreviations_callback'), $text); + } + return $text; + } + function _doAbbreviations_callback($matches) { + $abbr = $matches[0]; + if (isset($this->abbr_desciptions[$abbr])) { + $desc = $this->abbr_desciptions[$abbr]; + if (empty($desc)) { + return $this->hashPart("$abbr"); + } else { + $desc = $this->encodeAttribute($desc); + return $this->hashPart("$abbr"); + } + } else { + return $matches[0]; + } + } + +} + +/* + +PHP Markdown Extra +================== + +Description +----------- + +This is a PHP port of the original Markdown formatter written in Perl +by John Gruber. This special "Extra" version of PHP Markdown features +further enhancements to the syntax for making additional constructs +such as tables and definition list. + +Markdown is a text-to-HTML filter; it translates an easy-to-read / +easy-to-write structured text format into HTML. Markdown's text format +is mostly similar to that of plain text email, and supports features such +as headers, *emphasis*, code blocks, blockquotes, and links. + +Markdown's syntax is designed not as a generic markup language, but +specifically to serve as a front-end to (X)HTML. You can use span-level +HTML tags anywhere in a Markdown document, and you can use block level +HTML tags (like
    and as well). + +For more information about Markdown's syntax, see: + + + +Bugs +---- + +To file bug reports please send email to: + + + +Please include with your report: (1) the example input; (2) the output you +expected; (3) the output Markdown actually produced. + +Version History +--------------- + +See the readme file for detailed release notes for this version. + +Copyright and License +--------------------- + +PHP Markdown & Extra +Copyright (c) 2004-2013 Michel Fortin + +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2006 John Gruber + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. + +*/ +?> \ No newline at end of file diff --git a/plugins/dynamix/include/NotificationAgents.xml b/plugins/dynamix/include/NotificationAgents.xml new file mode 100644 index 000000000..3c954ce71 --- /dev/null +++ b/plugins/dynamix/include/NotificationAgents.xml @@ -0,0 +1,126 @@ + + + + Boxcar + + ACCESS_TOKEN + TITLE + MESSAGE + + + + + Prowl + + API_KEY + APP_NAME + TITLE + MESSAGE + + + + + Pushbullet + + TOKEN + TITLE + MESSAGE + + + + + Pushover + + USER_KEY + APP_TOKEN + MESSAGE + + + + diff --git a/plugins/dynamix/include/NotificationsArchive.php b/plugins/dynamix/include/NotificationsArchive.php new file mode 100644 index 000000000..5b2886d4a --- /dev/null +++ b/plugins/dynamix/include/NotificationsArchive.php @@ -0,0 +1,28 @@ + +"; else echo ""; + } + echo ""; +} +if (empty($files)) echo ""; +?> diff --git a/plugins/dynamix/include/Notify.php b/plugins/dynamix/include/Notify.php new file mode 100644 index 000000000..ec5edd319 --- /dev/null +++ b/plugins/dynamix/include/Notify.php @@ -0,0 +1,51 @@ + + $value) { + switch ($option) { + case 'e': + case 's': + case 'd': + case 'i': + case 'm': + $notify .= " -{$option} \"{$value}\""; + break; + case 'x': + case 't': + $notify .= " -{$option}"; + break; + } + } + shell_exec("$notify add"); + break; +case 'get': + echo shell_exec("$notify get"); + break; +case 'archive': + shell_exec("$notify archive '{$_POST['file']}'"); + break; +} +?> diff --git a/plugins/dynamix/include/PageBuilder.php b/plugins/dynamix/include/PageBuilder.php new file mode 100644 index 000000000..857f3b81c --- /dev/null +++ b/plugins/dynamix/include/PageBuilder.php @@ -0,0 +1,62 @@ + +".my_disk($text); +} + +// hack to embed function output in a quoted string (e.g., in a page Title) +// see: http://stackoverflow.com/questions/6219972/why-embedding-functions-inside-of-strings-is-different-than-variables +function _func($x) { return $x; } +$func = '_func'; +?> diff --git a/plugins/dynamix/include/ProcessStatus.php b/plugins/dynamix/include/ProcessStatus.php new file mode 100644 index 000000000..4a9f7b9e1 --- /dev/null +++ b/plugins/dynamix/include/ProcessStatus.php @@ -0,0 +1,36 @@ + +"; $_span = "";} + +echo $pid ? "{$span}Status:Running{$_span}" : "{$span}Status:Stopped{$_span}"; +?> \ No newline at end of file diff --git a/plugins/dynamix/include/ReloadPage.php b/plugins/dynamix/include/ReloadPage.php new file mode 100644 index 000000000..cab646464 --- /dev/null +++ b/plugins/dynamix/include/ReloadPage.php @@ -0,0 +1,26 @@ + +Copying, {$var['fsCopyPrcnt']}% complete..."; + break; +case 'Clearing': + echo "Clearing, {$var['fsClearPrcnt']}% complete..."; + break; +default: + echo substr($var['fsState'],-3)=='ing' ? 'wait' : 'stop'; + break; +} +?> \ No newline at end of file diff --git a/plugins/dynamix/include/ReplaceKey.php b/plugins/dynamix/include/ReplaceKey.php new file mode 100644 index 000000000..b2573299e --- /dev/null +++ b/plugins/dynamix/include/ReplaceKey.php @@ -0,0 +1,62 @@ + + + + + + + +
    +
    +
    + +Email address: + + + + +
    + diff --git a/plugins/dynamix/include/SMTPtest.php b/plugins/dynamix/include/SMTPtest.php new file mode 100644 index 000000000..93fa2e391 --- /dev/null +++ b/plugins/dynamix/include/SMTPtest.php @@ -0,0 +1,41 @@ + +/dev/null & echo $!',$op); + $pid = (int)$op[0]; + $timer = 0; + while ($timer<$timeout) { + sleep($sleep); + $timer += $sleep; + if (PsEnded($pid)) return true; + } + PsKill($pid); + return false; +} +function PsEnded($pid) { + exec("ps -eo pid|grep $pid",$output); + foreach ($output as $list) if (trim($list)==$pid) return false; + return true; +} +function PsKill($pid) { + exec("kill -9 $pid"); +} +if (PsExecute("/usr/local/emhttp/webGui/scripts/notify -s 'unRAID SMTP Test' -d 'Test message received!' -i 'alert' -t")) { + $result = exec("tail -3 /var/log/syslog|awk '/sSMTP/ {getline;print}'|cut -d']' -f2|cut -d'(' -f1"); + $color = strpos($result, 'Sent mail') ? 'green' : 'red'; + echo "Test result$result"; +} else { + echo "Test result: No reply from mail server"; +} +?> diff --git a/plugins/dynamix/include/SmartInfo.php b/plugins/dynamix/include/SmartInfo.php new file mode 100644 index 000000000..f63663357 --- /dev/null +++ b/plugins/dynamix/include/SmartInfo.php @@ -0,0 +1,119 @@ + +y?"{$age->y}y, ":"").($age->m?"{$age->m}m, ":"").($age->d?"{$age->d}d, ":"")."{$age->h}h)"; +} + +$port = $_POST['port']; + +switch ($_POST['cmd']) { +case "attributes": + $unraid = parse_plugin_cfg("dynamix",true); + $events = explode('|', $unraid['notify']['events']); + $temps = array(190,194); + $max = $unraid['display']['max']; + $hot = $unraid['display']['hot']; + exec("smartctl -A /dev/$port|awk 'NR>7'",$output); + foreach ($output as $line) { + if (!$line) continue; + $info = explode(' ', trim(preg_replace('/\s+/',' ',$line)), 10); + $color = ""; + if (array_search($info[0], $events)!==false && $info[9]>0) $color = " class='orange-text'"; + else if (array_search($info[0], $temps)!==false) { + if ($info[9]>=$max) $color = " class='red-text'"; else if ($info[9]>=$hot) $color = " class='orange-text'"; + } + echo ""; + if ($info[0] == 9 && is_numeric($info[9])) $info[9] .= duration($info[9]); + foreach ($info as $field) echo ""; + echo ""; + } + break; +case "capabilities": + exec("smartctl -c /dev/$port|awk 'NR>5'",$output); + $row = ["","",""]; + foreach ($output as $line) { + if (!$line) continue; + $line = preg_replace('/^_/','__',preg_replace(array('/__+/','/_ +_/'),'_',str_replace(array(chr(9),')','('),'_',$line))); + $info = array_map('trim', explode('_', preg_replace('/_( +)_ /','__',$line), 3)); + if (isset($info[0])) $row[0] .= ($row[0] ? " " : "").$info[0]; + if (isset($info[1])) $row[1] .= ($row[1] ? " " : "").$info[1]; + if (isset($info[2])) $row[2] .= ($row[2] ? " " : "").$info[2]; + if (substr($row[2],-1)=='.') { + echo ""; + $row = ["","",""]; + } + } + break; +case "identify": + exec("smartctl -i /dev/$port|awk 'NR>4'",$output); + exec("smartctl -H /dev/$port|grep 'result'|sed 's:self-assessment test result::'",$output); + foreach ($output as $line) { + if (!strlen($line)) continue; + $info = array_map('trim', explode(':', $line, 2)); + if ($info[1]=='PASSED') $info[1] = "Passed"; + if ($info[1]=='FAILED') $info[1] = "Failed"; + echo ""; + } + break; +case "save": + exec("smartctl -a /dev/$port >{$_SERVER['DOCUMENT_ROOT']}/{$_POST['file']}"); + break; +case "short": + exec("smartctl -t short /dev/$port"); + break; +case "long": + exec("smartctl -t long /dev/$port"); + break; +case "stop": + exec("smartctl -X /dev/$port"); + break; +case "update": + if (!exec("hdparm -C /dev/$port|grep -om1 active")) { + echo "Unavailable - disk must be spun up"; + break; + } + $progress = exec("smartctl -c /dev/$port|grep -Pom1 '\d+%'"); + if ($progress) { + echo " ".(100-substr($progress,0,-1))."% complete"; + break; + } + $result = trim(exec("smartctl -l selftest /dev/$port|grep -m1 '^# 1'|cut -c26-55")); + if (!$result) { + echo "No self-tests logged on this disk"; + break; + } + if (strpos($result, "Completed without error")!==false) { + echo "$result"; + break; + } + if (strpos($result, "Aborted")!==false or strpos($result, "Interrupted")!==false) { + echo "$result"; + break; + } + echo "Errors occurred - Check SMART report"; + break; +case "selftest": + echo shell_exec("smartctl -l selftest /dev/$port|awk 'NR>5'"); + break; +case "errorlog": + echo shell_exec("smartctl -l error /dev/$port|awk 'NR>5'"); + break; +} +?> diff --git a/plugins/dynamix/include/SystemInformation.php b/plugins/dynamix/include/SystemInformation.php new file mode 100644 index 000000000..6a378e5fb --- /dev/null +++ b/plugins/dynamix/include/SystemInformation.php @@ -0,0 +1,181 @@ + + + + + + + + +
    +
    Model: + +
    +
    M/B: + +
    +
    CPU: +=1000 && $cpuspeed[1]=='MHz') { + $cpuspeed[0] /= 1000; + $cpuspeed[1] = 'GHz'; + } + echo "$cpumodel @ {$cpuspeed[0]}{$cpuspeed[1]}"; +} else { + echo $cpumodel; +} +?> +
    +
    HVM: +/dev/null'); + + // If either kvm_intel or kvm_amd are loaded then Intel VT-x (vmx) or AMD-V (svm) cpu virtualzation support was found + $strLoadedModules = shell_exec("lsmod | grep '^kvm_\(amd\|intel\)'"); + + // Check for Intel VT-x (vmx) or AMD-V (svm) cpu virtualzation support + $strCPUInfo = file_get_contents('/proc/cpuinfo'); + + if (!empty($strLoadedModules)) { + // Yah! CPU and motherboard supported and enabled in BIOS + ?>Enabled'; + if (strpos($strCPUInfo, 'vmx') === false && strpos($strCPUInfo, 'svm') === false) { + // CPU doesn't support virtualzation + ?>Not AvailableDisabled'; + } +?> +
    +
    IOMMU: +Enabled'; + if (strpos($strCPUInfo, 'vmx') === false && strpos($strCPUInfo, 'svm') === false) { + // CPU doesn't support virtualzation so iommu would be impossible + ?>Not AvailableDisabled'; + } +?> +
    +
    Cache: + +
    +
    Memory: + 8 or 12 -> 16 +if ($memory_maximum*1024 < $memory_installed) {$memory_maximum = pow(2,ceil(log($memory_installed/1024)/log(2))); $star = "*";} +echo "$memory_installed MB (max. installable capacity $memory_maximum GB)".$star; +?> +
    +
    Network: + "; + if ($port=='bond0') { + echo "$port: ".exec("grep -Pom1 '^Bonding Mode: \K.+' /proc/net/bonding/bond0"); + } else { + unset($info); + exec("ethtool $port|grep -Po '^\s+(Speed|Duplex): \K[^U]+'",$info); + echo $info[0] ? "$port: {$info[0]} - {$info[1]} Duplex" : "$port: not connected"; + } +} +?> +
    +
    Kernel: +
    +
    OpenSSL: +
    +
    Uptime:
    +

    + + +More + +
    + diff --git a/plugins/dynamix/include/TrialRequest.php b/plugins/dynamix/include/TrialRequest.php new file mode 100644 index 000000000..03853f857 --- /dev/null +++ b/plugins/dynamix/include/TrialRequest.php @@ -0,0 +1,61 @@ + + + + + + + +
    +
    +
    + +Email address: + + + + +
    + diff --git a/plugins/dynamix/include/Watchdog.php b/plugins/dynamix/include/Watchdog.php new file mode 100644 index 000000000..e25a447a9 --- /dev/null +++ b/plugins/dynamix/include/Watchdog.php @@ -0,0 +1,28 @@ + +Array Stopped'; break; +case 'Starting': + echo 'Array Starting'; break; +default: + echo 'Array Started'; break; +} +if ($var['mdResync']) { + echo '•'.($var['mdNumInvalid']==0 ? 'Parity-Check:' : ($var['mdInvalidDisk']==0 ? 'Parity-Sync:' : 'Data-Rebuild:')).' '.number_format(($var['mdResyncPos']/($var['mdResync']/100+1)),1,$_POST['dot'],'').' %'; + if ($_POST['mode']<0) echo '#stop'; +} +?> \ No newline at end of file diff --git a/plugins/dynamix/include/Wrappers.php b/plugins/dynamix/include/Wrappers.php new file mode 100644 index 000000000..6322b9aa1 --- /dev/null +++ b/plugins/dynamix/include/Wrappers.php @@ -0,0 +1,35 @@ + + diff --git a/plugins/dynamix/include/local_prepend.php b/plugins/dynamix/include/local_prepend.php new file mode 100644 index 000000000..20c28eb81 --- /dev/null +++ b/plugins/dynamix/include/local_prepend.php @@ -0,0 +1,19 @@ + + diff --git a/plugins/dynamix/include/update.file.php b/plugins/dynamix/include/update.file.php new file mode 100644 index 000000000..f4c642122 --- /dev/null +++ b/plugins/dynamix/include/update.file.php @@ -0,0 +1,20 @@ + + \ No newline at end of file diff --git a/plugins/dynamix/include/update.parity.php b/plugins/dynamix/include/update.parity.php new file mode 100644 index 000000000..51c3973c1 --- /dev/null +++ b/plugins/dynamix/include/update.parity.php @@ -0,0 +1,60 @@ + +0) { + $hour = isset($_POST['hour']) ? $_POST['hour'] : '* *'; + $dotm = isset($_POST['dotm']) ? $_POST['dotm'] : '*'; + switch ($dotm) { + case '28-31': + $term = '[ $(date +%d -d tomorrow) -eq 1 ] && '; + break; + case 'W1': + $dotm = '*'; + $term = '[ $(date +%d) -le 7 ] && '; + break; + case 'W2': + $dotm = '*'; + $term = '[ $(date +%d) -ge 8 -a $(date +%d) -le 14 ] && '; + break; + case 'W3': + $dotm = '*'; + $term = '[ $(date +%d) -ge 15 -a $(date +%d) -le 21 ] && '; + break; + case 'W4': + $dotm = '*'; + $term = '[ $(date +%d) -ge 22 -a $(date +%d) -le 28 ] && '; + break; + case 'WL': + $dotm = '*'; + $term = '[ $(date +%d -d +7days) -le 7 ] && '; + break; + default: + $term = ''; + } + $month = isset($_POST['month']) ? $_POST['month'] : '*'; + $day = isset($_POST['day']) ? $_POST['day'] : '*'; + $write = isset($_POST['write']) ? $_POST['write'] : ''; + $cron = "# Generated parity check schedule:\n$hour $dotm $month $day $term/usr/local/sbin/mdcmd check $write &> /dev/null\n\n"; + } + parse_cron_cfg("dynamix", "parity-check", $cron); + unlink($memory); +} else { + file_put_contents($memory, http_build_query($_POST)); + $save = false; +} +?> \ No newline at end of file diff --git a/plugins/dynamix/javascript/context.js b/plugins/dynamix/javascript/context.js new file mode 100644 index 000000000..bf3e9b6e0 --- /dev/null +++ b/plugins/dynamix/javascript/context.js @@ -0,0 +1,166 @@ +/* + * Context.js + * Copyright Jacob Kelley + * MIT License + */ + +var context = context || (function () { + + var options = { + fadeSpeed: 100, + filter: function ($obj) { + // Modify $obj, Do not return + }, + above: 'auto', + preventDoubleContext: true, + compress: false + }; + + function initialize(opts) { + + options = $.extend({}, options, opts); + + $(document).on('click', 'html', function () { + $('.dropdown-context').fadeOut(options.fadeSpeed, function(){ + $('.dropdown-context').css({display:''}).find('.drop-left').removeClass('drop-left'); + }); + }); + if(options.preventDoubleContext){ + $(document).on('contextmenu', '.dropdown-context', function (e) { + e.preventDefault(); + }); + } + $(document).on('mouseenter', '.dropdown-submenu', function(){ + var $sub = $(this).find('.dropdown-context-sub:first'), + subWidth = $sub.width(), + subLeft = $sub.offset().left, + collision = (subWidth+subLeft) > window.innerWidth; + if(collision){ + $sub.addClass('drop-left'); + } + }); + + } + + function updateOptions(opts){ + options = $.extend({}, options, opts); + } + + function buildMenu(data, id, subMenu) { + var subClass = (subMenu) ? ' dropdown-context-sub' : '', + compressed = options.compress ? ' compressed-context' : '', + $menu = $(''); + var i = 0, linkTarget = '', useIcons = false; + for(i; i'); + } else if (typeof data[i].header !== 'undefined') { + if (typeof data[i].image !== 'undefined' && data[i].image !== '') { + $menu.append(''); + } else { + $menu.append(''); + } + + } else { + if (typeof data[i].href == 'undefined') { + data[i].href = '#'; + } + if (typeof data[i].target !== 'undefined') { + linkTarget = ' target="'+data[i].target+'"'; + } + if (useIcons){ + if (! options.compress ) { + largeIcon = " fa-lg "; + } else { + largeIcon = ""; + } + if (typeof data[i].icon !== 'undefined' && data[i].icon !== '' ) { + icon = ' '; + } else { + icon = ' '; + } + } + if (typeof data[i].subMenu !== 'undefined') { + $sub = ('
  • ' + icon + "  " + data[i].text + '
  • '); + } else { + $sub = $('
  • ' + icon + "  " + data[i].text + '
  • '); + } + if (typeof data[i].action !== 'undefined') { + var actiond = new Date(), + actionID = 'event-' + actiond.getTime() * Math.floor(Math.random()*100000), + eventAction = data[i].action; + $sub.find('a').attr('id', actionID); + $('#' + actionID).addClass('context-event'); + $(document).on('click', '#' + actionID, eventAction); + } + $menu.append($sub); + if (typeof data[i].subMenu != 'undefined') { + var subMenuData = buildMenu(data[i].subMenu, id, true); + $menu.find('li:last').append(subMenuData); + } + } + if (typeof options.filter == 'function') { + options.filter($menu.find('li:last')); + } + } + return $menu; + } + + function addContext(selector, data) { + destroyContext(selector); + + var id = selector.replace('#', ''), + $menu = buildMenu(data, id); + + $('body').append($menu); + + $(document).on('click', selector, function (e) { + e.preventDefault(); + e.stopPropagation(); + + $('.dropdown-context:not(.dropdown-context-sub)').hide(); + $dd = $('#dropdown-' + id); + + var place_above = false; + if (typeof options.above == 'boolean') { + place_above = options.above; + } else if (typeof options.above == 'string' && options.above == 'auto') { + place_above = ((e.pageY + $dd.height() + 32) > $(document).height()); + } + + if (place_above) { + $dd.addClass('dropdown-context-up').css({ + top: e.pageY - 24 - $dd.height(), + left: Math.min(Math.max(e.pageX - 13, 0), window.innerWidth - 168) + }).fadeIn(options.fadeSpeed); + } else { + $dd.removeClass('dropdown-context-up').css({ + top: e.pageY + 24, + left: Math.min(Math.max(e.pageX - 13, 0), window.innerWidth - 168) + }).fadeIn(options.fadeSpeed); + } + }); + } + + function destroyContext(selector) { + var id = selector.replace('#', ''); + + $(document).off('contextmenu', selector).off('click', '.context-event'); + $('#dropdown-' + id).remove(); + } + + return { + init: initialize, + settings: updateOptions, + attach: addContext, + destroy: destroyContext + }; +})(); \ No newline at end of file diff --git a/plugins/dynamix/javascript/dynamix.js b/plugins/dynamix/javascript/dynamix.js new file mode 100644 index 000000000..8d2172325 --- /dev/null +++ b/plugins/dynamix/javascript/dynamix.js @@ -0,0 +1,33 @@ +/* This file includes source code derived from different libraries that are originally available under various licenses. The references for these libraries and their authors are specified below. */ + +/*! jQuery v2.1.4 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b="length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){ +return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,ba=/<([\w:]+)/,ca=/<|&#?\w+;/,da=/<(?:script|style|link)/i,ea=/checked\s*(?:[^=]|=\s*.checked.)/i,fa=/^$|\/(?:java|ecma)script/i,ga=/^true\/(.*)/,ha=/^\s*\s*$/g,ia={option:[1,""],thead:[1,"
    ".date("{$_POST['date']} {$_POST['time']}", $item[1])."{$item[1]}
    No notifications available
    ".str_replace('_',' ',$field)."
    {$row[0]}{$row[1]}{$row[2]}
    ".preg_replace("/ is$/","",$info[0]).":$info[1]
    ","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ia.optgroup=ia.option,ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead,ia.th=ia.td;function ja(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function ka(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function la(a){var b=ga.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function ma(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function na(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function oa(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pa(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=oa(h),f=oa(a),d=0,e=f.length;e>d;d++)pa(f[d],g[d]);if(b)if(c)for(f=f||oa(a),g=g||oa(h),d=0,e=f.length;e>d;d++)na(f[d],g[d]);else na(a,h);return g=oa(h,"script"),g.length>0&&ma(g,!i&&oa(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(ca.test(e)){f=f||k.appendChild(b.createElement("div")),g=(ba.exec(e)||["",""])[1].toLowerCase(),h=ia[g]||ia._default,f.innerHTML=h[1]+e.replace(aa,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=oa(k.appendChild(e),"script"),i&&ma(f),c)){j=0;while(e=f[j++])fa.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(oa(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&ma(oa(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(oa(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!da.test(a)&&!ia[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(aa,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(oa(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(oa(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&ea.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(oa(c,"script"),ka),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,oa(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,la),j=0;g>j;j++)h=f[j],fa.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(ha,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qa,ra={};function sa(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function ta(a){var b=l,c=ra[a];return c||(c=sa(a,b),"none"!==c&&c||(qa=(qa||n("";K.innerHTML=S},remove:function(){var K=ad(this.id);if(K){C(K);if(Q.isGecko){delete au.frames[this.id]}}},onLoad:function(){var K=Q.isIE?ad(this.id).contentWindow:au.frames[this.id];K.location.href=this.obj.content}};Q.html=function(K,S){this.obj=K;this.id=S;this.height=K.height?parseInt(K.height,10):300;this.width=K.width?parseInt(K.width,10):500};Q.html.prototype={append:function(K,S){var aG=document.createElement("div");aG.id=this.id;aG.className="html";aG.innerHTML=this.obj.content;K.appendChild(aG)},remove:function(){var K=ad(this.id);if(K){C(K)}}};var ao=false,Y=[],q=["sb-nav-close","sb-nav-next","sb-nav-play","sb-nav-pause","sb-nav-previous"],aa,ae,Z,m=true;function N(aG,aQ,aN,aL,aR){var K=(aQ=="opacity"),aM=K?Q.setOpacity:function(aS,aT){aS.style[aQ]=""+aT+"px"};if(aL==0||(!K&&!Q.options.animate)||(K&&!Q.options.animateFade)){aM(aG,aN);if(aR){aR()}return}var aO=parseFloat(Q.getStyle(aG,aQ))||0;var aP=aN-aO;if(aP==0){if(aR){aR()}return}aL*=1000;var aH=aw(),aK=Q.ease,aJ=aH+aL,aI;var S=setInterval(function(){aI=aw();if(aI>=aJ){clearInterval(S);S=null;aM(aG,aN);if(aR){aR()}}else{aM(aG,aO+aK((aI-aH)/aL)*aP)}},10)}function aB(){aa.style.height=Q.getWindowSize("Height")+"px";aa.style.width=Q.getWindowSize("Width")+"px"}function aE(){aa.style.top=document.documentElement.scrollTop+"px";aa.style.left=document.documentElement.scrollLeft+"px"}function ay(K){if(K){aF(Y,function(S,aG){aG[0].style.visibility=aG[1]||""})}else{Y=[];aF(Q.options.troubleElements,function(aG,S){aF(document.getElementsByTagName(S),function(aH,aI){Y.push([aI,aI.style.visibility]);aI.style.visibility="hidden"})})}}function r(aG,K){var S=ad("sb-nav-"+aG);if(S){S.style.display=K?"":"none"}}function ah(K,aJ){var aI=ad("sb-loading"),aG=Q.getCurrent().player,aH=(aG=="img"||aG=="html");if(K){Q.setOpacity(aI,0);aI.style.display="block";var S=function(){Q.clearOpacity(aI);if(aJ){aJ()}};if(aH){N(aI,"opacity",1,Q.options.fadeDuration,S)}else{S()}}else{var S=function(){aI.style.display="none";Q.clearOpacity(aI);if(aJ){aJ()}};if(aH){N(aI,"opacity",0,Q.options.fadeDuration,S)}else{S()}}}function t(aO){var aJ=Q.getCurrent();ad("sb-title-inner").innerHTML=aJ.title||"";var aP,aL,S,aQ,aM;if(Q.options.displayNav){aP=true;var aN=Q.gallery.length;if(aN>1){if(Q.options.continuous){aL=aM=true}else{aL=(aN-1)>Q.current;aM=Q.current>0}}if(Q.options.slideshowDelay>0&&Q.hasNext()){aQ=!Q.isPaused();S=!aQ}}else{aP=aL=S=aQ=aM=false}r("close",aP);r("next",aL);r("play",S);r("pause",aQ);r("previous",aM);var K="";if(Q.options.displayCounter&&Q.gallery.length>1){var aN=Q.gallery.length;if(Q.options.counterType=="skip"){var aI=0,aH=aN,aG=parseInt(Q.options.counterLimit)||0;if(aG2){var aK=Math.floor(aG/2);aI=Q.current-aK;if(aI<0){aI+=aN}aH=Q.current+(aG-aK);if(aH>aN){aH-=aN}}while(aI!=aH){if(aI==aN){aI=0}K+='"}}else{K=[Q.current+1,Q.lang.of,aN].join(" ")}}ad("sb-counter").innerHTML=K;aO()}function U(aH){var K=ad("sb-title-inner"),aG=ad("sb-info-inner"),S=0.35;K.style.visibility=aG.style.visibility="";if(K.innerHTML!=""){N(K,"marginTop",0,S)}N(aG,"marginTop",0,S,aH)}function av(aG,aM){var aK=ad("sb-title"),K=ad("sb-info"),aH=aK.offsetHeight,aI=K.offsetHeight,aJ=ad("sb-title-inner"),aL=ad("sb-info-inner"),S=(aG?0.35:0);N(aJ,"marginTop",aH,S);N(aL,"marginTop",aI*-1,S,function(){aJ.style.visibility=aL.style.visibility="hidden";aM()})}function ac(K,aH,S,aJ){var aI=ad("sb-wrapper-inner"),aG=(S?Q.options.resizeDuration:0);N(Z,"top",aH,aG);N(aI,"height",K,aG,aJ)}function ar(K,aH,S,aI){var aG=(S?Q.options.resizeDuration:0);N(Z,"left",aH,aG);N(Z,"width",K,aG,aI)}function ak(aM,aG){var aI=ad("sb-body-inner"),aM=parseInt(aM),aG=parseInt(aG),S=Z.offsetHeight-aI.offsetHeight,K=Z.offsetWidth-aI.offsetWidth,aK=ae.offsetHeight,aL=ae.offsetWidth,aJ=parseInt(Q.options.viewportPadding)||20,aH=(Q.player&&Q.options.handleOversize!="drag");return Q.setDimensions(aM,aG,aK,aL,S,K,aJ,aH)}var T={};T.markup='
    {loading}
    ';T.options={animSequence:"sync",counterLimit:10,counterType:"default",displayCounter:true,displayNav:true,fadeDuration:0.35,initialHeight:160,initialWidth:320,modal:false,overlayColor:"#000",overlayOpacity:0.3,resizeDuration:0.35,showOverlay:true,troubleElements:["select","object","embed","canvas"]};T.init=function(){Q.appendHTML(document.body,s(T.markup,Q.lang));T.body=ad("sb-body-inner");aa=ad("sb-container");ae=ad("sb-overlay");Z=ad("sb-wrapper");if(!x){aa.style.position="absolute"}if(!h){var aG,K,S=/url\("(.*\.png)"\)/;aF(q,function(aI,aJ){aG=ad(aJ);if(aG){K=Q.getStyle(aG,"backgroundImage").match(S);if(K){aG.style.backgroundImage="none";aG.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true,src="+K[1]+",sizingMethod=scale);"}}})}var aH;F(au,"resize",function(){if(aH){clearTimeout(aH);aH=null}if(A){aH=setTimeout(T.onWindowResize,10)}})};T.onOpen=function(K,aG){m=false;aa.style.display="block";aB();var S=ak(Q.options.initialHeight,Q.options.initialWidth);ac(S.innerHeight,S.top);ar(S.width,S.left);if(Q.options.showOverlay){ae.style.backgroundColor=Q.options.overlayColor;Q.setOpacity(ae,0);if(!Q.options.modal){F(ae,"click",Q.close)}ao=true}if(!x){aE();F(au,"scroll",aE)}ay();aa.style.visibility="visible";if(ao){N(ae,"opacity",Q.options.overlayOpacity,Q.options.fadeDuration,aG)}else{aG()}};T.onLoad=function(S,K){ah(true);while(T.body.firstChild){C(T.body.firstChild)}av(S,function(){if(!A){return}if(!S){Z.style.visibility="visible"}t(K)})};T.onReady=function(aH){if(!A){return}var S=Q.player,aG=ak(S.height,S.width);var K=function(){U(aH)};switch(Q.options.animSequence){case"hw":ac(aG.innerHeight,aG.top,true,function(){ar(aG.width,aG.left,true,K)});break;case"wh":ar(aG.width,aG.left,true,function(){ac(aG.innerHeight,aG.top,true,K)});break;default:ar(aG.width,aG.left,true);ac(aG.innerHeight,aG.top,true,K)}};T.onShow=function(K){ah(false,K);m=true};T.onClose=function(){if(!x){M(au,"scroll",aE)}M(ae,"click",Q.close);Z.style.visibility="hidden";var K=function(){aa.style.visibility="hidden";aa.style.display="none";ay(true)};if(ao){N(ae,"opacity",0,Q.options.fadeDuration,K)}else{K()}};T.onPlay=function(){r("play",false);r("pause",true)};T.onPause=function(){r("pause",false);r("play",true)};T.onWindowResize=function(){if(!m){return}aB();var K=Q.player,S=ak(K.height,K.width);ar(S.width,S.left);ac(S.innerHeight,S.top);if(K.onWindowResize){K.onWindowResize()}};Q.skin=T;au.Shadowbox=Q})(window); +/* TableSorter v2.0.5b, Copyright Christian Bach. Sort option added to .trigger('update') by Bergware */ +!function($){$.extend({tablesorter:new function(){function benchmark(e,t){log(e+","+((new Date).getTime()-t.getTime())+"ms")}function log(e){"undefined"!=typeof console&&"undefined"!=typeof console.debug?console.log(e):alert(e)}function buildParserCache(e,t){if(e.config.debug)var r="";if(0!=e.tBodies.length){var n=e.tBodies[0].rows;if(n[0])for(var o=[],a=n[0].cells,i=a.length,s=0;i>s;s++){var d=!1;$.metadata&&$(t[s]).metadata()&&$(t[s]).metadata().sorter?d=getParserById($(t[s]).metadata().sorter):e.config.headers[s]&&e.config.headers[s].sorter&&(d=getParserById(e.config.headers[s].sorter)),d||(d=detectParserForColumn(e,n,-1,s)),e.config.debug&&(r+="column:"+s+" parser:"+d.id+"\n"),o.push(d)}return e.config.debug&&log(r),o}}function detectParserForColumn(e,t,r,n){for(var o=parsers.length,a=!1,i=!1,s=!0;""==i&&s;)r++,t[r]?(a=getNodeFromRowAndCellIndex(t,r,n),i=trimAndGetNodeText(e.config,a),e.config.debug&&log("Checking if value was empty on row:"+r)):s=!1;for(var d=1;o>d;d++)if(parsers[d].is(i,e,a))return parsers[d];return parsers[0]}function getNodeFromRowAndCellIndex(e,t,r){return e[t].cells[r]}function trimAndGetNodeText(e,t){return $.trim(getElementText(e,t))}function getParserById(e){for(var t=parsers.length,r=0;t>r;r++)if(parsers[r].id.toLowerCase()==e.toLowerCase())return parsers[r];return!1}function buildCache(e){if(e.config.debug)var t=new Date;for(var r=e.tBodies[0]&&e.tBodies[0].rows.length||0,n=e.tBodies[0].rows[0]&&e.tBodies[0].rows[0].cells.length||0,o=e.config.parsers,a={row:[],normalized:[]},i=0;r>i;++i){var s=$(e.tBodies[0].rows[i]),d=[];if(s.hasClass(e.config.cssChildRow))a.row[a.row.length-1]=a.row[a.row.length-1].add(s);else{a.row.push(s);for(var c=0;n>c;++c)d.push(o[c].format(getElementText(e.config,s[0].cells[c]),e,s[0].cells[c]));d.push(a.normalized.length),a.normalized.push(d),d=null}}return e.config.debug&&benchmark("Building cache for "+r+" rows:",t),a}function getElementText(e,t){if(!t)return"";var r=$(t),n=r.attr("data-sort-value");if(void 0!==n)return n;var o="";return e.supportsTextContent||(e.supportsTextContent=t.textContent||!1),o="simple"==e.textExtraction?e.supportsTextContent?t.textContent:t.childNodes[0]&&t.childNodes[0].hasChildNodes()?t.childNodes[0].innerHTML:t.innerHTML:"function"==typeof e.textExtraction?e.textExtraction(t):$(t).text()}function appendToTable(e,t){if(e.config.debug)var r=new Date;for(var n=t,o=n.row,a=n.normalized,i=a.length,s=a[0].length-1,d=$(e.tBodies[0]),c=[],u=0;i>u;u++){var l=a[u][s];if(c.push(o[l]),!e.config.appender)for(var f=o[l].length,g=0;f>g;g++)d[0].appendChild(o[l][g])}e.config.appender&&e.config.appender(e,c),c=null,e.config.debug&&benchmark("Rebuilt table:",r),applyWidget(e),setTimeout(function(){$(e).trigger("sortEnd")},0)}function buildHeaders(e){if(e.config.debug)var t=new Date;var r=($.metadata?!0:!1,computeTableHeaderCellIndexes(e)),n=$(e.config.selectorHeaders,e).each(function(t){if(this.column=r[this.parentNode.rowIndex+"-"+this.cellIndex],this.order=formatSortingOrder(e.config.sortInitialOrder),this.count=this.order,(checkHeaderMetadata(this)||checkHeaderOptions(e,t))&&(this.sortDisabled=!0),checkHeaderOptionsSortingLocked(e,t)&&(this.order=this.lockedOrder=checkHeaderOptionsSortingLocked(e,t)),!this.sortDisabled){var n=$(this).addClass(e.config.cssHeader);e.config.onRenderHeader&&e.config.onRenderHeader.apply(n)}e.config.headerList[t]=this});return e.config.debug&&(benchmark("Built headers:",t),log(n)),n}function computeTableHeaderCellIndexes(e){for(var t=[],r={},n=e.getElementsByTagName("THEAD")[0],o=n.getElementsByTagName("TR"),a=0;ah;h++){"undefined"==typeof t[h]&&(t[h]=[]);for(var m=t[h],p=d;d+g>p;p++)m[p]="x"}}return r}function checkCellColSpan(e,t,r){for(var n=[],o=e.tHead.rows,a=o[r].cells,i=0;i1?n=n.concat(checkCellColSpan(e,headerArr,r++)):(1==e.tHead.length||s.rowSpan>1||!o[r+1])&&n.push(s)}return n}function checkHeaderMetadata(e){return $.metadata&&$(e).metadata().sorter===!1?!0:!1}function checkHeaderOptions(e,t){return e.config.headers[t]&&e.config.headers[t].sorter===!1?!0:!1}function checkHeaderOptionsSortingLocked(e,t){return e.config.headers[t]&&e.config.headers[t].lockedOrder?e.config.headers[t].lockedOrder:!1}function applyWidget(e){for(var t=e.config.widgets,r=t.length,n=0;r>n;n++)getWidgetById(t[n]).format(e)}function getWidgetById(e){for(var t=widgets.length,r=0;t>r;r++)if(widgets[r].id.toLowerCase()==e.toLowerCase())return widgets[r]}function formatSortingOrder(e){return"Number"!=typeof e?"desc"==e.toLowerCase()?1:0:1==e?1:0}function isValueInArray(e,t){for(var r=t.length,n=0;r>n;n++)if(t[n][0]==e)return!0;return!1}function setHeadersCss(e,t,r,n){t.removeClass(n[0]).removeClass(n[1]);var o=[];t.each(function(){this.sortDisabled||(o[this.column]=$(this))});for(var a=r.length,i=0;a>i;i++)o[r[i][0]].addClass(n[r[i][1]])}function fixColumnWidth(e){var t=e.config;if(t.widthFixed){var r=$("");$("tr:first td",e.tBodies[0]).each(function(){r.append($("").css("width",$(this).width()))}),$(e).prepend(r)}}function updateHeaderSortCount(e,t){for(var r=e.config,n=t.length,o=0;n>o;o++){var a=t[o],i=r.headerList[a[0]];i.count=a[1],i.count++}}function multisort(table,sortList,cache){if(table.config.debug)var sortTime=new Date;for(var dynamicExp="sortWrapper = function(a,b) {",l=sortList.length,i=0;l>i;i++){var c=sortList[i][0],order=sortList[i][1],s="text"==table.config.parsers[c].type?0==order?makeSortFunction("text","asc",c):makeSortFunction("text","desc",c):0==order?makeSortFunction("numeric","asc",c):makeSortFunction("numeric","desc",c),e="e"+i;dynamicExp+="var "+e+" = "+s,dynamicExp+="if("+e+") { return "+e+"; } ",dynamicExp+="else { "}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;l>i;i++)dynamicExp+="}; ";return dynamicExp+="return 0; ",dynamicExp+="}; ",table.config.debug&&benchmark("Evaling expression:"+dynamicExp,new Date),eval(dynamicExp),cache.normalized.sort(sortWrapper),table.config.debug&&benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime),cache}function makeSortFunction(e,t,r){var n="a["+r+"]",o="b["+r+"]";return"text"==e&&"asc"==t?"("+n+" == "+o+" ? 0 : ("+n+" === null ? Number.POSITIVE_INFINITY : ("+o+" === null ? Number.NEGATIVE_INFINITY : ("+n+" < "+o+") ? -1 : 1 )));":"text"==e&&"desc"==t?"("+n+" == "+o+" ? 0 : ("+n+" === null ? Number.POSITIVE_INFINITY : ("+o+" === null ? Number.NEGATIVE_INFINITY : ("+o+" < "+n+") ? -1 : 1 )));":"numeric"==e&&"asc"==t?"("+n+" === null && "+o+" === null) ? 0 :("+n+" === null ? Number.POSITIVE_INFINITY : ("+o+" === null ? Number.NEGATIVE_INFINITY : "+n+" - "+o+"));":"numeric"==e&&"desc"==t?"("+n+" === null && "+o+" === null) ? 0 :("+n+" === null ? Number.POSITIVE_INFINITY : ("+o+" === null ? Number.NEGATIVE_INFINITY : "+o+" - "+n+"));":void 0}function makeSortText(e){return"((a["+e+"] < b["+e+"]) ? -1 : ((a["+e+"] > b["+e+"]) ? 1 : 0));"}function makeSortTextDesc(e){return"((b["+e+"] < a["+e+"]) ? -1 : ((b["+e+"] > a["+e+"]) ? 1 : 0));"}function makeSortNumeric(e){return"a["+e+"]-b["+e+"];"}function makeSortNumericDesc(e){return"b["+e+"]-a["+e+"];"}function sortText(e,t){return table.config.sortLocaleCompare?e.localeCompare(t):t>e?-1:e>t?1:0}function sortTextDesc(e,t){return table.config.sortLocaleCompare?t.localeCompare(e):e>t?-1:t>e?1:0}function sortNumeric(e,t){return e-t}function sortNumericDesc(e,t){return t-e}function getCachedSortType(e,t){return e[t].type}var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:!0,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:!1,cancelSelection:!0,sortList:[],headerList:[],dateFormat:"us",decimal:"/.|,/g",onRenderHeader:null,selectorHeaders:"thead th",debug:!1},this.benchmark=benchmark;var sortWrapper;this.construct=function(e){return this.each(function(){if(this.tHead&&this.tBodies){var t,r,n,o;this.config={},o=$.extend(this.config,$.tablesorter.defaults,e),t=$(this),$.data(this,"tablesorter",o),r=buildHeaders(this),this.config.parsers=buildParserCache(this,r),n=buildCache(this);var a=[o.cssDesc,o.cssAsc];fixColumnWidth(this),r.click(function(e){var i=t[0].tBodies[0]&&t[0].tBodies[0].rows.length||0;if(!this.sortDisabled&&i>0){t.trigger("sortStart");var s=($(this),this.column);if(this.order=this.count++%2,this.lockedOrder&&(this.order=this.lockedOrder),e[o.sortMultiSortKey])if(isValueInArray(s,o.sortList))for(var d=0;d0&&t.trigger("sorton",[o.sortList]),applyWidget(this)}})},this.addParser=function(e){for(var t=parsers.length,r=!0,n=0;t>n;n++)parsers[n].id.toLowerCase()==e.id.toLowerCase()&&(r=!1);r&&parsers.push(e)},this.addWidget=function(e){widgets.push(e)},this.formatFloat=function(e){var t=parseFloat(e);return isNaN(t)?0:t},this.formatInt=function(e){var t=parseInt(e);return isNaN(t)?0:t},this.isDigit=function(e){return/^[-+]?\d*$/.test($.trim(e.replace(/[,.']/g,"")))},this.clearTableBody=function(e){if($.browser.msie)for(;e.tBodies[0].firstChild;)e.tBodies[0].removeChild(e.tBodies[0].firstChild);else e.tBodies[0].innerHTML=""}}}),$.fn.extend({tablesorter:$.tablesorter.construct});var ts=$.tablesorter;ts.addParser({id:"text",is:function(){return!0},format:function(e){return $.trim(e.toLocaleLowerCase())},type:"text"}),ts.addParser({id:"digit",is:function(e,t){var r=t.config;return $.tablesorter.isDigit(e,r)},format:function(e){return $.tablesorter.formatFloat(e)},type:"numeric"}),ts.addParser({id:"currency",is:function(e){return/^[£$€?.]/.test(e)},format:function(e){return $.tablesorter.formatFloat(e.replace(new RegExp(/[£$€]/g),""))},type:"numeric"}),ts.addParser({id:"ipAddress",is:function(e){return/^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(e)},format:function(e){for(var t=e.split("."),r="",n=t.length,o=0;n>o;o++){var a=t[o];r+=2==a.length?"0"+a:a}return $.tablesorter.formatFloat(r)},type:"numeric"}),ts.addParser({id:"url",is:function(e){return/^(https?|ftp|file):\/\/$/.test(e)},format:function(e){return jQuery.trim(e.replace(new RegExp(/(https?|ftp|file):\/\//),""))},type:"text"}),ts.addParser({id:"isoDate",is:function(e){return/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(e)},format:function(e){return $.tablesorter.formatFloat(""!=e?new Date(e.replace(new RegExp(/-/g),"/")).getTime():"0")},type:"numeric"}),ts.addParser({id:"percent",is:function(e){return/\%$/.test($.trim(e))},format:function(e){return $.tablesorter.formatFloat(e.replace(new RegExp(/%/g),""))},type:"numeric"}),ts.addParser({id:"usLongDate",is:function(e){return e.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/))},format:function(e){return $.tablesorter.formatFloat(new Date(e).getTime())},type:"numeric"}),ts.addParser({id:"shortDate",is:function(e){return/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(e)},format:function(e,t){var r=t.config;return e=e.replace(/\-/g,"/"),"us"==r.dateFormat&&(e=e.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$1/$2")),"pt"==r.dateFormat?e=e.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1"):"uk"==r.dateFormat?e=e.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1"):("dd/mm/yy"==r.dateFormat||"dd-mm-yy"==r.dateFormat)&&(e=e.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/,"$1/$2/$3")),$.tablesorter.formatFloat(new Date(e).getTime())},type:"numeric"}),ts.addParser({id:"time",is:function(e){return/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(e)},format:function(e){return $.tablesorter.formatFloat(new Date("2000/01/01 "+e).getTime())},type:"numeric"}),ts.addParser({id:"metadata",is:function(){return!1},format:function(e,t,r){var n=t.config,o=n.parserMetadataName?n.parserMetadataName:"sortValue";return $(r).metadata()[o]},type:"numeric"}),ts.addWidget({id:"zebra",format:function(e){if(e.config.debug)var t=new Date;var r,n,o=-1;$("tr:visible",e.tBodies[0]).each(function(){r=$(this),r.hasClass(e.config.cssChildRow)||o++,n=o%2==0,r.removeClass(e.config.widgetZebra.css[n?0:1]).addClass(e.config.widgetZebra.css[n?1:0])}),e.config.debug&&$.tablesorter.benchmark("Applying Zebra widget",t)}})}(jQuery); +/* Readmore.js, commit 40c74c0 on 2/9/14, Copyright Jed Foster */ +(function(c){function g(b,a){this.element=b;this.options=c.extend({},h,a);c(this.element).data("max-height",this.options.maxHeight);c(this.element).data("height-margin",this.options.heightMargin);delete this.options.maxHeight;if(this.options.embedCSS&&!k){var d=".readmore-js-toggle, .readmore-js-section { "+this.options.sectionCSS+" } .readmore-js-section { overflow: hidden; }",e=document.createElement("style");e.type="text/css";e.styleSheet?e.styleSheet.cssText=d:e.appendChild(document.createTextNode(d));document.getElementsByTagName("head")[0].appendChild(e);k=!0}this._defaults=h;this._name=f;this.init()}var f="readmore",h={speed:100,maxHeight:200,heightMargin:16,moreLink:'Read More',lessLink:'Close',embedCSS:!0,sectionCSS:"display: block; width: 100%;",startOpen:!1,expandedClass:"readmore-js-expanded",collapsedClass:"readmore-js-collapsed",beforeToggle:function(){},afterToggle:function(){}},k=!1;g.prototype={init:function(){var b=this;c(this.element).each(function(){var a=c(this),d=a.css("max-height").replace(/[^-\d\.]/g,"")>a.data("max-height")?a.css("max-height").replace(/[^-\d\.]/g,""):a.data("max-height"),e=a.data("height-margin");"none"!=a.css("max-height")&&a.css("max-height","none");b.setBoxHeight(a);if(a.outerHeight(!0)<=d+e)return!0;a.addClass("readmore-js-section "+b.options.collapsedClass).data("collapsedHeight",d);a.after(c(b.options.startOpen?b.options.lessLink:b.options.moreLink).on("click",function(c){b.toggleSlider(this,a,c)}).addClass("readmore-js-toggle"));b.options.startOpen||a.css({height:d})});c(window).on("resize",function(a){b.resizeBoxes()})},toggleSlider:function(b,a,d){d.preventDefault();var e=this;d=newLink=sectionClass="";var f=!1;d=c(a).data("collapsedHeight");c(a).height()<=d?(d=c(a).data("expandedHeight")+"px",newLink="lessLink",f=!0,sectionClass=e.options.expandedClass):(newLink="moreLink",sectionClass=e.options.collapsedClass);e.options.beforeToggle(b,a,f);c(a).animate({height:d},{duration:e.options.speed,complete:function(){e.options.afterToggle(b,a,f);c(b).replaceWith(c(e.options[newLink]).on("click",function(b){e.toggleSlider(this,a,b)}).addClass("readmore-js-toggle"));c(this).removeClass(e.options.collapsedClass+" "+e.options.expandedClass).addClass(sectionClass)}})},setBoxHeight:function(b){var a=b.clone().css({height:"auto",width:b.width(),overflow:"hidden"}).insertAfter(b),c=a.outerHeight(!0);a.remove();b.data("expandedHeight",c)},resizeBoxes:function(){var b=this;c(".readmore-js-section").each(function(){var a=c(this);b.setBoxHeight(a);(a.height()>a.data("expandedHeight")||a.hasClass(b.options.expandedClass)&&a.height()-1}function s(I,A,F){F=F||{};var z=this,J=A.length,C=F.prepend&&!F.append,B=0,H=0,D=false,E;if(F.paged){var G=(F.pageNo-1)*F.elemPerPage;A=A.slice(G,G+F.elemPerPage);J=A.length}E=a.extend({},F,{complete:function(){if(this.html){if(C){z.prepend(this.html())}else{z.append(this.html())}}B++;if(B===J||D){if(D&&F&&typeof F.error==="function"){F.error.call(z)}if(F&&typeof F.complete==="function"){F.complete()}}},success:function(){H++;if(H===J){if(F&&typeof F.success==="function"){F.success()}}},error:function(){D=true}});if(!F.append&&!F.prepend){z.html("")}if(C){A.reverse()}a(A).each(function(){var K=a("
    ");n.call(K,I,this,E);if(D){return false}});return this}function c(C,A,z,B){if(u[C]){u[C].push({data:z,selection:A,settings:B})}else{u[C]=[{data:z,selection:A,settings:B}]}}function q(D,B,A,C){var z=v[D].clone();p.call(B,z,A,C);if(typeof C.success==="function"){C.success()}}function w(){return new Date().getTime()}function x(z){if(z.indexOf("?")!==-1){return z+"&_="+w()}else{return z+"?_="+w()}}function m(D,B,A,C){var z=a("
    ");v[D]=null;var E=D;if(C.overwriteCache){E=x(E)}a.ajax({url:E,async:C.async,success:function(F){z.html(F);l(z,D,B,A,C)},error:function(){k(D,B,A,C)}})}function o(z,C,B,D){var A=a("
    ");if(z.is("script")||z.is("template")){z=a.parseHTML(a.trim(z.html()))}A.html(z);p.call(C,A,B,D);if(typeof D.success==="function"){D.success()}}function p(B,z,A){f(B,z,A);a(this).each(function(){var C=a(B.html());if(A.beforeInsert){A.beforeInsert(C)}if(A.append){a(this).append(C)}else{if(A.prepend){a(this).prepend(C)}else{a(this).html(C)}}if(A.afterInsert){A.afterInsert(C)}});if(typeof A.complete==="function"){A.complete.call(a(this))}}function k(C,A,z,B){var D;if(typeof B.error==="function"){B.error.call(A)}a(u[C]).each(function(E,F){if(typeof F.settings.error==="function"){F.settings.error.call(F.selection)}});if(typeof B.complete==="function"){B.complete.call(A)}while(u[C]&&(D=u[C].shift())){if(typeof D.settings.complete==="function"){D.settings.complete.call(D.selection)}}if(typeof u[C]!=="undefined"&&u[C].length>0){u[C]=[]}}function l(z,D,B,A,C){var E;v[D]=z.clone();p.call(B,z,A,C);if(typeof C.success==="function"){C.success.call(B)}while(u[D]&&(E=u[D].shift())){p.call(E.selection,v[D].clone(),E.data,E.settings);if(typeof E.settings.success==="function"){E.settings.success.call(E.selection)}}}function f(B,z,A){z=z||{};t("data-content",B,z,A,function(C,D){C.html(e(C,D,"content",A))});t("data-content-append",B,z,A,function(C,D){C.append(e(C,D,"content",A))});t("data-content-prepend",B,z,A,function(C,D){C.prepend(e(C,D,"content",A))});t("data-content-text",B,z,A,function(C,D){C.text(e(C,D,"content",A))});t("data-innerHTML",B,z,A,function(C,D){C.html(e(C,D,"content",A))});t("data-src",B,z,A,function(C,D){C.attr("src",e(C,D,"src",A))},function(C){C.remove()});t("data-href",B,z,A,function(C,D){C.attr("href",e(C,D,"href",A))},function(C){C.remove()});t("data-alt",B,z,A,function(C,D){C.attr("alt",e(C,D,"alt",A))});t("data-id",B,z,A,function(C,D){C.attr("id",e(C,D,"id",A))});t("data-value",B,z,A,function(C,D){C.attr("value",e(C,D,"value",A))});t("data-class",B,z,A,function(C,D){C.addClass(e(C,D,"class",A))});t("data-link",B,z,A,function(C,E){var D=a("");D.attr("href",e(C,E,"link",A));D.html(C.html());C.html(D)});t("data-link-wrap",B,z,A,function(C,E){var D=a("");D.attr("href",e(C,E,"link-wrap",A));C.wrap(D)});t("data-options",B,z,A,function(C,D){a(D).each(function(){var E=a("