├── root ├── deleted.tt ├── static │ ├── images │ │ ├── ibex.jpg │ │ ├── favicon.ico │ │ └── ajax-loader.gif │ ├── jqueryui │ │ └── images │ │ │ ├── ui-icons_222222_256x240.png │ │ │ ├── ui-icons_231f20_256x240.png │ │ │ ├── ui-icons_2e83ff_256x240.png │ │ │ ├── ui-icons_454545_256x240.png │ │ │ ├── ui-icons_888888_256x240.png │ │ │ ├── ui-icons_cd0a0a_256x240.png │ │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ │ ├── ui-bg_flat_75_ffffff_40x100.png │ │ │ ├── ui-bg_glass_55_fbf9ee_1x400.png │ │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ │ ├── ui-bg_glass_75_dadada_1x400.png │ │ │ ├── ui-bg_glass_75_e6e6e6_1x400.png │ │ │ ├── ui-bg_inset-soft_95_fef1ec_1x100.png │ │ │ └── ui-bg_highlight-soft_75_bd2031_1x100.png │ └── codemirror │ │ ├── plain.css │ │ ├── csscolors.css │ │ ├── xmlcolors.css │ │ ├── jscolors.css │ │ ├── parsedummy.js │ │ ├── tokenize.js │ │ ├── highlight.js │ │ ├── mirrorframe.js │ │ ├── parsehtmlmixed.js │ │ ├── util.js │ │ ├── stringstream.js │ │ ├── parsecss.js │ │ ├── parsesparql.js │ │ ├── tokenizejavascript.js │ │ ├── parsexml.js │ │ ├── parsejavascript.js │ │ └── undo.js ├── newaccount.js ├── newexperiment.tt ├── main_iehacks.css.tt ├── login.tt ├── delete_account.tt ├── newaccount.tt ├── user.tt ├── frontpage.tt ├── githelp.tt ├── manage.tt ├── common.js ├── wrapper.tt ├── uicommon.js ├── experiments.js └── main.css.tt ├── t ├── view_TT.t ├── model_DB.t ├── view_JSON.t ├── 01app.t ├── controller_Ajax.t ├── controller_User.t ├── 02pod.t ├── controller_Experiment.t └── 03podcoverage.t ├── lib ├── IbexFarm │ ├── DeployIbex_test.pl │ ├── PasswordProtectExperiment │ │ ├── Factory.pm │ │ └── Apache.pm │ ├── FNames.pm │ ├── View │ │ ├── JSON.pm │ │ └── TT.pm │ ├── CheckEmail.pm │ ├── AjaxHeaders.pm │ ├── Quota.pm │ ├── AuthStore.pm │ ├── Util.pm │ ├── Controller │ │ ├── Experiment.pm │ │ ├── Root.pm │ │ └── User.pm │ └── DeployIbex.pm └── IbexFarm.pm ├── script ├── reset_password.sh ├── ibexfarm_cgi.pl ├── ibexfarm_test.pl ├── ibexfarm_fastcgi.pl ├── ibexfarm_create.pl ├── ibexfarm_server.pl └── ResetPassword.pl ├── README ├── MODULES ├── findconfigvars.pl ├── Makefile.PL ├── test_ibexfarm.yml ├── docker ├── docker-compose.yml ├── Readme.md └── Dockerfile └── LICENSE /root/deleted.tt: -------------------------------------------------------------------------------- 1 |

Your account was deleted. Bye!

-------------------------------------------------------------------------------- /root/static/images/ibex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/images/ibex.jpg -------------------------------------------------------------------------------- /root/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/images/favicon.ico -------------------------------------------------------------------------------- /root/static/images/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/images/ajax-loader.gif -------------------------------------------------------------------------------- /t/view_TT.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Test::More tests => 1; 4 | 5 | BEGIN { use_ok 'IbexFarm::View::TT' } 6 | 7 | -------------------------------------------------------------------------------- /t/model_DB.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Test::More tests => 1; 4 | 5 | BEGIN { use_ok 'IbexFarm::Model::DB' } 6 | 7 | -------------------------------------------------------------------------------- /t/view_JSON.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Test::More tests => 1; 4 | 5 | BEGIN { use_ok 'IbexFarm::View::JSON' } 6 | 7 | -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-icons_231f20_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-icons_231f20_256x240.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-icons_888888_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-icons_888888_256x240.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /root/static/codemirror/plain.css: -------------------------------------------------------------------------------- 1 | .editbox { 2 | margin: .4em; 3 | padding: 0; 4 | font-family: monospace; 5 | font-size: 10pt; 6 | color: black; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-bg_glass_55_fbf9ee_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-bg_glass_55_fbf9ee_1x400.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-bg_glass_75_dadada_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-bg_glass_75_dadada_1x400.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-bg_glass_75_e6e6e6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-bg_glass_75_e6e6e6_1x400.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-bg_inset-soft_95_fef1ec_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-bg_inset-soft_95_fef1ec_1x100.png -------------------------------------------------------------------------------- /root/static/jqueryui/images/ui-bg_highlight-soft_75_bd2031_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addrummond/ibexfarm/HEAD/root/static/jqueryui/images/ui-bg_highlight-soft_75_bd2031_1x100.png -------------------------------------------------------------------------------- /t/01app.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use Test::More tests => 2; 5 | 6 | BEGIN { use_ok 'Catalyst::Test', 'IbexFarm' } 7 | 8 | ok( request('/')->is_success, 'Request should succeed' ); 9 | -------------------------------------------------------------------------------- /t/controller_Ajax.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Test::More tests => 3; 4 | 5 | BEGIN { use_ok 'Catalyst::Test', 'IbexFarm' } 6 | BEGIN { use_ok 'IbexFarm::Controller::Ajax' } 7 | 8 | ok( request('/ajax')->is_success, 'Request should succeed' ); 9 | 10 | 11 | -------------------------------------------------------------------------------- /t/controller_User.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Test::More tests => 3; 4 | 5 | BEGIN { use_ok 'Catalyst::Test', 'IbexFarm' } 6 | BEGIN { use_ok 'IbexFarm::Controller::User' } 7 | 8 | ok( request('/user')->is_success, 'Request should succeed' ); 9 | 10 | 11 | -------------------------------------------------------------------------------- /t/02pod.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use Test::More; 5 | 6 | eval "use Test::Pod 1.14"; 7 | plan skip_all => 'Test::Pod 1.14 required' if $@; 8 | plan skip_all => 'set TEST_POD to enable this test' unless $ENV{TEST_POD}; 9 | 10 | all_pod_files_ok(); 11 | -------------------------------------------------------------------------------- /t/controller_Experiment.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use Test::More tests => 3; 4 | 5 | BEGIN { use_ok 'Catalyst::Test', 'IbexFarm' } 6 | BEGIN { use_ok 'IbexFarm::Controller::Experiment' } 7 | 8 | ok( request('/experiment')->is_success, 'Request should succeed' ); 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/IbexFarm/DeployIbex_test.pl: -------------------------------------------------------------------------------- 1 | use warnings; 2 | use strict; 3 | 4 | use IbexFarm::DeployIbex; 5 | 6 | deploy( 7 | name => "Foo", 8 | hashbang => "/bin/py", 9 | external_config_url => "http://localhost:3000/config", 10 | pass_params => 1, 11 | www_dir => "/tmp/www" 12 | ); 13 | -------------------------------------------------------------------------------- /root/newaccount.js: -------------------------------------------------------------------------------- 1 | // Focus appropriate filed in form on login/create account. 2 | $(document).ready(function () { 3 | var u = $("input[name=username]").get(0); 4 | if ($("input[name=email]").length || ! $(u).attr('value')) 5 | u.focus(); 6 | else 7 | $("input[name=password]").get(0).focus(); 8 | }); -------------------------------------------------------------------------------- /t/03podcoverage.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use Test::More; 5 | 6 | eval "use Test::Pod::Coverage 1.04"; 7 | plan skip_all => 'Test::Pod::Coverage 1.04 required' if $@; 8 | plan skip_all => 'set TEST_POD to enable this test' unless $ENV{TEST_POD}; 9 | 10 | all_pod_coverage_ok(); 11 | -------------------------------------------------------------------------------- /script/reset_password.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Utility for running password reset script from outside container 4 | 5 | docker exec -e RESET_USERNAME="$1" -e RESET_PASSWORD="$2" $(docker ps | fgrep docker_ibexfarm | awk '{ print $1; }') /bin/bash -c "PERL5LIB=\$IBEXFARM_src_dir/lib perl \$IBEXFARM_src_dir/script/ResetPassword.pl \$RESET_USERNAME \$RESET_PASSWORD" 6 | -------------------------------------------------------------------------------- /lib/IbexFarm/PasswordProtectExperiment/Factory.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::PasswordProtectExperiment::Factory; 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use Class::Factory; 7 | use base 'Class::Factory'; 8 | 9 | sub password_protect_experiment { die "Define password_protect_experiment() in implementation"; } 10 | sub password_unprotect_experiment { die "Define password_unprotect_experiment() in implementation"; } 11 | 12 | 1; 13 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This is the code used for the web app at https://spellout.net/ibexfarm. 2 | 3 | If you want to deploy your own Ibex Farm instance, take a look at the instructions 4 | in docker/Readme.md. 5 | 6 | 7 | -------------------------------------------------------------------------------- 8 | 9 | 10 | Author: Alex Drummond 11 | 12 | Acknowledgements: 13 | 14 | Marijn Haverbeke for his CodeMirror library (http://codemirror.net). 15 | -------------------------------------------------------------------------------- /lib/IbexFarm/FNames.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::FNames; 2 | use warnings; 3 | use strict; 4 | 5 | use IbexFarm; 6 | 7 | use parent 'Exporter'; 8 | 9 | sub is_ok_fname { 10 | my $fname = shift; 11 | return 0 if length($fname) > IbexFarm->config->{max_fname_length}; 12 | return $fname =~ /^[A-Za-z0-9_-][A-Za-z0-9_.-]*$/; 13 | } 14 | 15 | use constant OK_CHARS_DESCRIPTION => "letters, numbers, '.', '-' and '_'"; 16 | 17 | our @EXPORT = qw( is_ok_fname OK_CHARS_DESCRIPTION ); 18 | -------------------------------------------------------------------------------- /root/newexperiment.tt: -------------------------------------------------------------------------------- 1 | [% USE HTML %] 2 | [% META title = 'Create an experiment' %] 3 | 4 | [% IF error %] 5 |

6 | [% HTML.escape(error) %] 7 |

8 | [% END %] 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Experiment name:
18 |
-------------------------------------------------------------------------------- /lib/IbexFarm/View/JSON.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::View::JSON; 2 | 3 | use strict; 4 | use base 'Catalyst::View::JSON'; 5 | 6 | =head1 NAME 7 | 8 | IbexFarm::View::JSON - Catalyst JSON View 9 | 10 | =head1 SYNOPSIS 11 | 12 | See L 13 | 14 | =head1 DESCRIPTION 15 | 16 | Catalyst JSON View. 17 | 18 | =head1 AUTHOR 19 | 20 | Alex Drummond 21 | 22 | =head1 LICENSE 23 | 24 | This library is free software, you can redistribute it and/or modify 25 | it under the same terms as Perl itself. 26 | 27 | =cut 28 | 29 | 1; 30 | -------------------------------------------------------------------------------- /root/main_iehacks.css.tt: -------------------------------------------------------------------------------- 1 | form.create_account input { 2 | width: 11em; 3 | } 4 | form.create_account td.submit input { 5 | width: auto; 6 | } 7 | 8 | form.login input { 9 | width: 11em; 10 | } 11 | form.login td.submit input { 12 | width: auto; 13 | } 14 | 15 | form.update_email input { 16 | width: 16.5em; 17 | } 18 | form.update_email td.submit input { 19 | width: auto; 20 | } 21 | 22 | form.update_password input { 23 | width: 16.5em; 24 | } 25 | form.update_password td.submit input { 26 | width: auto; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /script/ibexfarm_cgi.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use Catalyst::ScriptRunner; 4 | Catalyst::ScriptRunner->run('IbexFarm', 'CGI'); 5 | 6 | 1; 7 | 8 | =head1 NAME 9 | 10 | ibexfarm_cgi.pl - Catalyst CGI 11 | 12 | =head1 SYNOPSIS 13 | 14 | See L 15 | 16 | =head1 DESCRIPTION 17 | 18 | Run a Catalyst application as a cgi script. 19 | 20 | =head1 AUTHORS 21 | 22 | Catalyst Contributors, see Catalyst.pm 23 | 24 | =head1 COPYRIGHT 25 | 26 | This library is free software. You can redistribute it and/or modify 27 | it under the same terms as Perl itself. 28 | 29 | =cut 30 | 31 | -------------------------------------------------------------------------------- /lib/IbexFarm/View/TT.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::View::TT; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use base 'Catalyst::View::TT'; 7 | 8 | __PACKAGE__->config( 9 | TEMPLATE_EXTENSION => '.tt', 10 | WRAPPER => 'wrapper.tt' 11 | ); 12 | 13 | =head1 NAME 14 | 15 | IbexFarm::View::TT - TT View for IbexFarm 16 | 17 | =head1 DESCRIPTION 18 | 19 | TT View for IbexFarm. 20 | 21 | =head1 SEE ALSO 22 | 23 | L 24 | 25 | =head1 AUTHOR 26 | 27 | Alex Drummond 28 | 29 | =head1 LICENSE 30 | 31 | This library is free software. You can redistribute it and/or modify 32 | it under the same terms as Perl itself. 33 | 34 | =cut 35 | 36 | 1; 37 | -------------------------------------------------------------------------------- /MODULES: -------------------------------------------------------------------------------- 1 | # Modules used that aren't installed with Catalyst 2 | # (not a complete list, but installing all these should 3 | # pull in the necessary dependencies). 4 | Catalyst::Plugin::RequireSSL 5 | Catalyst::View::JSON 6 | Catalyst::Plugin::ConfigLoader::Environment 7 | Template::Plugin::Filter::Minify::CSS 8 | Template::Plugin::Filter::Minify::JavaScript 9 | Catalyst::Plugin::UploadProgress 10 | HTML::GenerateUtil 11 | Class::Factory 12 | JSON::XS 13 | Digest 14 | Archive::Zip 15 | Data::Validate::URI 16 | Log::Handler 17 | Crypt::OpenPGP 18 | Params::Classify 19 | Sub::Identify 20 | Variable::Magic 21 | DateTime 22 | Class::ISA 23 | namespace::clean # latest version of. 24 | -------------------------------------------------------------------------------- /lib/IbexFarm/CheckEmail.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::CheckEmail; 2 | use warnings; 3 | use strict; 4 | 5 | use parent 'Exporter'; 6 | 7 | sub is_ok_email { 8 | # 9 | # Original regex failed for some valid email addresses. Best just not to validate, since (as is well known) 10 | # there is no sensible way to check for the validity of an email address other than by sending an email to it. 11 | # 12 | return 1; 13 | #return shift =~ /^[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?:[A-Z]{2}|com|org|net|gov|edu|mil|biz|info|mobi|name|aero|jobs|museum)\b$/; 14 | } 15 | 16 | our @EXPORT = qw( is_ok_email ); 17 | -------------------------------------------------------------------------------- /lib/IbexFarm/AjaxHeaders.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::AjaxHeaders; 2 | use warnings; 3 | use strict; 4 | 5 | use base 'Exporter'; 6 | 7 | our @EXPORT_OK = qw( ajax_headers ); 8 | 9 | sub ajax_headers { 10 | my ($c, $content_type, $encoding, $code) = @_; 11 | $code ||= 200; 12 | 13 | $c->response->code($code); 14 | $c->response->content_type($content_type); 15 | if ($encoding) { $c->response->content_encoding($encoding); } 16 | $c->response->headers->header(Pragma => 'no-cache'); 17 | $c->response->headers->header(Expires => 'Thu, 01 Jan 1970 00:00:00 GMT'); 18 | $c->response->headers->header('Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); 19 | } 20 | 21 | 1; 22 | -------------------------------------------------------------------------------- /findconfigvars.pl: -------------------------------------------------------------------------------- 1 | use warnings; 2 | use strict; 3 | 4 | # Gross! 5 | 6 | use File::Find; 7 | 8 | my @confs; 9 | find(sub { 10 | if (-f $_ && $_ !~ /~$/ && $_ ne 'findconfigvars.pl') { 11 | open(my $f, $_); 12 | my $line = 1; 13 | for my $l (<$f>) { 14 | if ($l =~ /IbexFarm->config->{([^}]+)}/) { push @confs, { varname => "$1", filename => $File::Find::name, line => $line } ; } 15 | ++$line; 16 | } 17 | } 18 | }, './'); 19 | 20 | @confs = sort { $a->{varname} cmp $b->{varname} } @confs; 21 | my $prev = { varname => '' }; 22 | @confs = grep { $_->{varname} ne $prev->{varname} && (($prev) = $_) } @confs; 23 | for (@confs) { print $_->{varname}, " in ", $_->{filename}, ":", $_->{line}, "\n"; } 24 | -------------------------------------------------------------------------------- /root/static/codemirror/csscolors.css: -------------------------------------------------------------------------------- 1 | html { 2 | cursor: text; 3 | } 4 | 5 | .editbox { 6 | margin: .4em; 7 | padding: 0; 8 | font-family: monospace; 9 | font-size: 10pt; 10 | color: black; 11 | } 12 | 13 | pre.code, .editbox { 14 | color: #666; 15 | } 16 | 17 | .editbox p { 18 | margin: 0; 19 | } 20 | 21 | span.css-at { 22 | color: #708; 23 | } 24 | 25 | span.css-unit { 26 | color: #281; 27 | } 28 | 29 | span.css-value { 30 | color: #708; 31 | } 32 | 33 | span.css-identifier { 34 | color: black; 35 | } 36 | 37 | span.css-selector { 38 | color: #11B; 39 | } 40 | 41 | span.css-important { 42 | color: #00F; 43 | } 44 | 45 | span.css-colorcode { 46 | color: #299; 47 | } 48 | 49 | span.css-comment { 50 | color: #A70; 51 | } 52 | 53 | span.css-string { 54 | color: #A22; 55 | } 56 | -------------------------------------------------------------------------------- /root/login.tt: -------------------------------------------------------------------------------- 1 | [% USE HTML %] 2 | [% META title = ' - Login' %] 3 | [% META js_scripts = 'newaccount.js' %] 4 | 5 |

Log in

6 | 7 | [% IF login_msg %] 8 |

9 | [% HTML.escape(login_msg) %] 10 |

11 | [% END %] 12 | [% IF error %] 13 |

14 | [% HTML.escape(error) %] 15 |

16 | [% END %] 17 | -------------------------------------------------------------------------------- /root/static/codemirror/xmlcolors.css: -------------------------------------------------------------------------------- 1 | html { 2 | cursor: text; 3 | } 4 | 5 | .editbox { 6 | margin: .4em; 7 | padding: 0; 8 | font-family: monospace; 9 | font-size: 10pt; 10 | color: black; 11 | } 12 | 13 | .editbox p { 14 | margin: 0; 15 | } 16 | 17 | span.xml-tagname { 18 | color: #A0B; 19 | } 20 | 21 | span.xml-attribute { 22 | color: #281; 23 | } 24 | 25 | span.xml-punctuation { 26 | color: black; 27 | } 28 | 29 | span.xml-attname { 30 | color: #00F; 31 | } 32 | 33 | span.xml-comment { 34 | color: #A70; 35 | } 36 | 37 | span.xml-cdata { 38 | color: #48A; 39 | } 40 | 41 | span.xml-processing { 42 | color: #999; 43 | } 44 | 45 | span.xml-entity { 46 | color: #A22; 47 | } 48 | 49 | span.xml-error { 50 | color: #F00 !important; 51 | } 52 | 53 | span.xml-text { 54 | color: black; 55 | } 56 | -------------------------------------------------------------------------------- /script/ibexfarm_test.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use Catalyst::ScriptRunner; 4 | Catalyst::ScriptRunner->run('IbexFarm', 'Test'); 5 | 6 | 1; 7 | 8 | =head1 NAME 9 | 10 | ibexfarm_test.pl - Catalyst Test 11 | 12 | =head1 SYNOPSIS 13 | 14 | ibexfarm_test.pl [options] uri 15 | 16 | Options: 17 | --help display this help and exits 18 | 19 | Examples: 20 | ibexfarm_test.pl http://localhost/some_action 21 | ibexfarm_test.pl /some_action 22 | 23 | See also: 24 | perldoc Catalyst::Manual 25 | perldoc Catalyst::Manual::Intro 26 | 27 | =head1 DESCRIPTION 28 | 29 | Run a Catalyst action from the command line. 30 | 31 | =head1 AUTHORS 32 | 33 | Catalyst Contributors, see Catalyst.pm 34 | 35 | =head1 COPYRIGHT 36 | 37 | This library is free software. You can redistribute it and/or modify 38 | it under the same terms as Perl itself. 39 | 40 | =cut 41 | -------------------------------------------------------------------------------- /root/static/codemirror/jscolors.css: -------------------------------------------------------------------------------- 1 | html { 2 | cursor: text; 3 | } 4 | 5 | .editbox { 6 | margin: .4em; 7 | padding: 0; 8 | font-family: monospace; 9 | font-size: 10pt; 10 | color: black; 11 | } 12 | 13 | pre.code, .editbox { 14 | color: #666666; 15 | } 16 | 17 | .editbox p { 18 | margin: 0; 19 | } 20 | 21 | span.js-punctuation { 22 | color: #666666; 23 | } 24 | 25 | span.js-operator { 26 | color: #666666; 27 | } 28 | 29 | span.js-keyword { 30 | color: #770088; 31 | } 32 | 33 | span.js-atom { 34 | color: #228811; 35 | } 36 | 37 | span.js-variable { 38 | color: black; 39 | } 40 | 41 | span.js-variabledef { 42 | color: #0000FF; 43 | } 44 | 45 | span.js-localvariable { 46 | color: #004499; 47 | } 48 | 49 | span.js-property { 50 | color: black; 51 | } 52 | 53 | span.js-comment { 54 | color: #AA7700; 55 | } 56 | 57 | span.js-string { 58 | color: #AA2222; 59 | } 60 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # IMPORTANT: if you delete this file your app will not work as 3 | # expected. You have been warned. 4 | use inc::Module::Install; 5 | 6 | name 'IbexFarm'; 7 | all_from 'lib/IbexFarm.pm'; 8 | 9 | requires 'Catalyst::Runtime' => '5.80014'; 10 | requires 'Catalyst::Plugin::ConfigLoader'; 11 | requires 'Catalyst::Plugin::Static::Simple'; 12 | requires 'Catalyst::Action::RenderView'; 13 | requires 'parent'; 14 | requires 'Config::General'; # This should reflect the config file format you've chosen 15 | # See Catalyst::Plugin::ConfigLoader for supported formats 16 | 17 | requires 'Catalyst::Plugin::Authentication'; 18 | requires 'Catalyst::Plugin::Session'; 19 | requires 'Catalyst::Plugin::Session::State::Cookie'; 20 | requires 'Catalyst::Plugin::RequireSSL'; 21 | requires 'Catalyst::Plugin::UploadProgress'; 22 | #requires 'Authentication::Store::Minimal'; 23 | 24 | catalyst; 25 | 26 | install_script glob('script/*.pl'); 27 | auto_install; 28 | WriteAll; 29 | -------------------------------------------------------------------------------- /root/delete_account.tt: -------------------------------------------------------------------------------- 1 | [% USE HTML %] 2 | [% META title = ' - Delete your account' %] 3 | 4 |

5 | « back to my account 6 |

7 |

8 | Deleting your account will permanently and irrecoverably delete all of your experiments 9 | (results, data files, everything). 10 |

11 |

12 | I purposefully do not keep backups of experiments, since I want it to be possible for people to delete 13 | all data that they have collected from participants, as may be required by the law and/or their own data protection 14 | policies. 15 |

16 |

17 | However, the hosting service that I use automatically keeps backups for a week. Thus, 18 | if you delete an experiment, the data will not be permanently deleted until around a week afterwards. 19 |

20 | 21 | 24 | -------------------------------------------------------------------------------- /root/static/codemirror/parsedummy.js: -------------------------------------------------------------------------------- 1 | var DummyParser = Editor.Parser = (function() { 2 | function tokenizeDummy(source) { 3 | while (!source.endOfLine()) source.next(); 4 | return "text"; 5 | } 6 | function parseDummy(source) { 7 | function indentTo(n) {return function() {return n;}} 8 | source = tokenizer(source, tokenizeDummy); 9 | var space = 0; 10 | 11 | var iter = { 12 | next: function() { 13 | var tok = source.next(); 14 | if (tok.type == "whitespace") { 15 | if (tok.value == "\n") tok.indentation = indentTo(space); 16 | else space = tok.value.length; 17 | } 18 | return tok; 19 | }, 20 | copy: function() { 21 | var _space = space; 22 | return function(_source) { 23 | space = _space; 24 | source = tokenizer(_source, tokenizeDummy); 25 | return iter; 26 | }; 27 | } 28 | }; 29 | return iter; 30 | } 31 | return {make: parseDummy}; 32 | })(); 33 | -------------------------------------------------------------------------------- /root/newaccount.tt: -------------------------------------------------------------------------------- 1 | [% USE HTML %] 2 | [% META title = ' - Create an account' %] 3 | [% META js_scripts = 'newaccount.js' %] 4 | 5 |

Create an Ibex Farm account

6 | 7 | [% IF error %] 8 |

9 | [% HTML.escape(error) %] 10 |

11 | [% END %] 12 | 31 | -------------------------------------------------------------------------------- /script/ibexfarm_fastcgi.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use Catalyst::ScriptRunner; 4 | Catalyst::ScriptRunner->run('IbexFarm', 'FastCGI'); 5 | 6 | 1; 7 | 8 | =head1 NAME 9 | 10 | ibexfarm_fastcgi.pl - Catalyst FastCGI 11 | 12 | =head1 SYNOPSIS 13 | 14 | ibexfarm_fastcgi.pl [options] 15 | 16 | Options: 17 | -? -help display this help and exits 18 | -l --listen Socket path to listen on 19 | (defaults to standard input) 20 | can be HOST:PORT, :PORT or a 21 | filesystem path 22 | -n --nproc specify number of processes to keep 23 | to serve requests (defaults to 1, 24 | requires -listen) 25 | -p --pidfile specify filename for pid file 26 | (requires -listen) 27 | -d --daemon daemonize (requires -listen) 28 | -M --manager specify alternate process manager 29 | (FCGI::ProcManager sub-class) 30 | or empty string to disable 31 | -e --keeperr send error messages to STDOUT, not 32 | to the webserver 33 | --proc_title Set the process title (is possible) 34 | 35 | =head1 DESCRIPTION 36 | 37 | Run a Catalyst application as fastcgi. 38 | 39 | =head1 AUTHORS 40 | 41 | Catalyst Contributors, see Catalyst.pm 42 | 43 | =head1 COPYRIGHT 44 | 45 | This library is free software. You can redistribute it and/or modify 46 | it under the same terms as Perl itself. 47 | 48 | =cut 49 | -------------------------------------------------------------------------------- /root/user.tt: -------------------------------------------------------------------------------- 1 | [% USE HTML %] 2 | [% META title = '- User' %] 3 | [% META js_scripts = 'uicommon.js experiments.js' %] 4 | 5 |

Experiments

6 |
7 | 8 |

Account admin

9 | [% IF message %] 10 |
11 | [% HTML.escape(message) %] 12 |
13 | [% END %] 14 | [% IF error %] 15 |
16 | [% HTML.escape(error) %] 17 |
18 | [% END %] 19 |

Email

20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |

Password

31 |

Enter the new password twice:

32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 |

Deletion

46 |

47 | » delete my account 48 |

49 | -------------------------------------------------------------------------------- /root/frontpage.tt: -------------------------------------------------------------------------------- 1 |

Welcome to the Ibex farm

2 | 3 | [% IF c.user_exists %] 4 |

5 | » manage my experiments 6 |

7 | [% END %] 8 |

9 | This site provides free hosting for ibex experiments. 10 |

11 |

12 | Upload data files in your browser, then send your participants a link to the experiment. 13 |

14 |
15 |
16 | [% eurl = experiment_base_url _ example_experiment_user _ '/' _ example_experiment_name _ '/experiment.html' | url %] 17 | View an example experiment 18 |
19 | 20 | [% IF experiment_count %] 21 |

22 | Currently hosting [% experiment_count %] experiment[% IF experiment_count != 1 %]s[% END %]. 23 |

24 | [% END %] 25 | [% front_page_html_message %] 26 |

27 | Contact [% webmaster_email %] if you have any issues, or try the google group. 28 |

29 |

30 | The code for this site is BSD-licensed and available on github. 31 |

32 | 33 | -------------------------------------------------------------------------------- /root/githelp.tt: -------------------------------------------------------------------------------- 1 | [% USE HTML %] 2 | [% META title = ' - Syncing from a git repo' %] 3 | 4 | [% IF experiment_name %] 5 |

« back to experiment ‘[% experiment_name %]’

6 | [% END %] 7 | 8 |

Syncing from a git repo

9 |

10 | This is pretty straightforward, but there are a few things 11 | to know: 12 |

13 |
    14 |
  • There is no git repository hosted on the ibex farm server. The method used is rather braindead: 15 | the repository is checked out, and the files are then copied over to your experiment. Files that 16 | already exist will be overwritten; files which are uploaded to your experiment but which are not in the repository will remain unaltered. 17 |
  • 18 |
  • 19 | No files are ever copied to the ‘results’ dir, in order to ensure that 20 | results are never overwritten by accident. 21 |
  • 22 |
  • 23 | The repository checkout will time out after [% timeout %] seconds. If this happens, 24 | you can just try again. That is, unless you are trying to check out an enormous 25 | repo, in which case you'll have to slim it down a bit. 26 |
  • 27 |
  • 28 | Your git repo should have the directories ‘data_includes’, 29 | ‘js_includes’, etc. in its root directory. Only files immediately 30 | contained in these directories will be copied over — subdirectories 31 | are ignored. 32 |
  • 33 |
  • 34 | Currently, it is only possible to sync from publicly readable git repos. 35 |
  • 36 |
37 | 38 |

You can get free hosting for git repositories at github.

39 | -------------------------------------------------------------------------------- /test_ibexfarm.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: IbexFarm 3 | 4 | webmaster_name: "Alex" 5 | webmaster_email: "a.d.drummond@gmail.com" 6 | 7 | ibex_archive: "/tmp/ibex-deploy.tar.gz" 8 | ibex_archive_root_dir: "ibex-deploy" 9 | ibex_version: "3.0" 10 | deployment_dir: "/tmp" 11 | deployment_www_dir: "/tmp/www/" 12 | 13 | max_fname_length: 150 14 | 15 | dirs: [ "js_includes", "css_includes", "data_includes", "chunk_includes", "server_state", "results" ] 16 | sync_dirs: [ "js_includes", "css_includes", "data_includes", "chunk_includes", "server_state" ] 17 | dirs_to_types: 18 | js_includes: 'text/javascript' 19 | css_includes: 'text/css' 20 | data_includes: 'text/javascript' 21 | chunk_includes: 'text/html' 22 | server_state: 'text/plain' 23 | results: 'text/plain' 24 | optional_dirs: 25 | server_state: 1 26 | results: 1 27 | writable: [ "data_includes/*", "results/*", "server_state/*", "chunk_includes/*" ] 28 | 29 | enforce_quotas: 0 30 | quota_max_files_in_dir: 500 31 | quota_max_file_size: 1048576 32 | quota_max_total_size: 1048576 33 | quota_record_dir: "/tmp/quota" 34 | 35 | db_name: ibexfarm 36 | db_user: lfuser 37 | db_host: localhost 38 | db_port: 5432 39 | db_password: abcd 40 | 41 | max_upload_size_bytes: 5242880 42 | 43 | experiment_password_protection: Apache 44 | 45 | git_path: "/opt/local/bin/git" 46 | git_checkout_timeout_seconds: 25 47 | 48 | event_log_file: "/tmp/event_log" 49 | 50 | #'Plugin::Authentication': 51 | # default_realm: users 52 | # realms: 53 | # users: 54 | # credential: 55 | # class: Password 56 | # password_field: password 57 | # password_type: clear 58 | # store: 59 | # class: Minimal 60 | # users: 61 | # alex: 62 | # password: abcd 63 | # roles: ["read"] 64 | -------------------------------------------------------------------------------- /script/ibexfarm_create.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Catalyst::ScriptRunner; 7 | Catalyst::ScriptRunner->run('IbexFarm', 'Create'); 8 | 9 | 1; 10 | 11 | =head1 NAME 12 | 13 | ibexfarm_create.pl - Create a new Catalyst Component 14 | 15 | =head1 SYNOPSIS 16 | 17 | ibexfarm_create.pl [options] model|view|controller name [helper] [options] 18 | 19 | Options: 20 | --force don't create a .new file where a file to be created exists 21 | --mechanize use Test::WWW::Mechanize::Catalyst for tests if available 22 | --help display this help and exits 23 | 24 | Examples: 25 | ibexfarm_create.pl controller My::Controller 26 | ibexfarm_create.pl --mechanize controller My::Controller 27 | ibexfarm_create.pl view My::View 28 | ibexfarm_create.pl view HTML TT 29 | ibexfarm_create.pl model My::Model 30 | ibexfarm_create.pl model SomeDB DBIC::Schema MyApp::Schema create=dynamic\ 31 | dbi:SQLite:/tmp/my.db 32 | ibexfarm_create.pl model AnotherDB DBIC::Schema MyApp::Schema create=static\ 33 | [Loader opts like db_schema, naming] dbi:Pg:dbname=foo root 4321 34 | [connect_info opts like quote_char, name_sep] 35 | 36 | See also: 37 | perldoc Catalyst::Manual 38 | perldoc Catalyst::Manual::Intro 39 | perldoc Catalyst::Helper::Model::DBIC::Schema 40 | perldoc Catalyst::Model::DBIC::Schema 41 | perldoc Catalyst::View::TT 42 | 43 | =head1 DESCRIPTION 44 | 45 | Create a new Catalyst Component. 46 | 47 | Existing component files are not overwritten. If any of the component files 48 | to be created already exist the file will be written with a '.new' suffix. 49 | This behavior can be suppressed with the C<-force> option. 50 | 51 | =head1 AUTHORS 52 | 53 | Catalyst Contributors, see Catalyst.pm 54 | 55 | =head1 COPYRIGHT 56 | 57 | This library is free software. You can redistribute it and/or modify 58 | it under the same terms as Perl itself. 59 | 60 | =cut 61 | -------------------------------------------------------------------------------- /lib/IbexFarm/Quota.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::Quota; 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use parent 'Exporter'; 7 | 8 | use File::Find; 9 | 10 | # Returns bool saying whether or not the quota is met 11 | # and a string describing the violation (if any). 12 | # Options (all required): 13 | # max_files_in_dir 14 | # max_file_size 15 | # max_total_size 16 | sub check_quota { 17 | my ($opts, @dirs) = @_; 18 | 19 | my $total; 20 | my %dirs_file_counts; 21 | eval { 22 | find(sub { 23 | return if $File::Find::name eq "." || $File::Find::name eq ".."; 24 | if (-f $File::Find::name) { 25 | # print STDERR $File::Find::name, "\n"; 26 | if (defined $dirs_file_counts{$File::Find::dir}) { 27 | my $n = ++($dirs_file_counts{$File::Find::dir}); 28 | if ($n > $opts->{max_files_in_dir}) { 29 | die [0, "The directory '" . $File::Find::dir . "' contains more than the maximum permitted number of files ($opts->{max_files_in_dir})"]; 30 | } 31 | } 32 | else 33 | { $dirs_file_counts{$File::Find::dir} = 1; } 34 | 35 | my $s = -s $File::Find::name; 36 | if ($s > $opts->{max_file_size}) { 37 | die [0, "The file '$_' exceeded the maximum file size of $opts->{max_file_size} bytes."]; 38 | } 39 | $total += $s; 40 | } 41 | }, @dirs); 42 | }; 43 | if ($@) { 44 | die "Weird" unless (ref($@) eq "ARRAY"); 45 | return @{$@}; 46 | } 47 | 48 | if ($total > $opts->{max_total_size}) { 49 | return (0, "The size of the directory " . $File::Find::dir . " ($total bytes) is greater than the maximum permitted ($opts->{max_total_size} bytes)."); 50 | } 51 | return (1, ""); 52 | } 53 | 54 | our @EXPORT_OK = qw( check_quota ); 55 | 56 | 1; 57 | -------------------------------------------------------------------------------- /script/ibexfarm_server.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | BEGIN { 4 | $ENV{CATALYST_SCRIPT_GEN} = 40; 5 | } 6 | 7 | use Catalyst::ScriptRunner; 8 | Catalyst::ScriptRunner->run('IbexFarm', 'Server'); 9 | 10 | 1; 11 | 12 | =head1 NAME 13 | 14 | ibexfarm_server.pl - Catalyst Test Server 15 | 16 | =head1 SYNOPSIS 17 | 18 | ibexfarm_server.pl [options] 19 | 20 | -d --debug force debug mode 21 | -f --fork handle each request in a new process 22 | (defaults to false) 23 | -? --help display this help and exits 24 | -h --host host (defaults to all) 25 | -p --port port (defaults to 3000) 26 | -k --keepalive enable keep-alive connections 27 | -r --restart restart when files get modified 28 | (defaults to false) 29 | -rd --restart_delay delay between file checks 30 | (ignored if you have Linux::Inotify2 installed) 31 | -rr --restart_regex regex match files that trigger 32 | a restart when modified 33 | (defaults to '\.yml$|\.yaml$|\.conf|\.pm$') 34 | --restart_directory the directory to search for 35 | modified files, can be set multiple times 36 | (defaults to '[SCRIPT_DIR]/..') 37 | --follow_symlinks follow symlinks in search directories 38 | (defaults to false. this is a no-op on Win32) 39 | --background run the process in the background 40 | --pidfile specify filename for pid file 41 | 42 | See also: 43 | perldoc Catalyst::Manual 44 | perldoc Catalyst::Manual::Intro 45 | 46 | =head1 DESCRIPTION 47 | 48 | Run a Catalyst Testserver for this application. 49 | 50 | =head1 AUTHORS 51 | 52 | Catalyst Contributors, see Catalyst.pm 53 | 54 | =head1 COPYRIGHT 55 | 56 | This library is free software. You can redistribute it and/or modify 57 | it under the same terms as Perl itself. 58 | 59 | =cut 60 | 61 | -------------------------------------------------------------------------------- /root/static/codemirror/tokenize.js: -------------------------------------------------------------------------------- 1 | // A framework for simple tokenizers. Takes care of newlines and 2 | // white-space, and of getting the text from the source stream into 3 | // the token object. A state is a function of two arguments -- a 4 | // string stream and a setState function. The second can be used to 5 | // change the tokenizer's state, and can be ignored for stateless 6 | // tokenizers. This function should advance the stream over a token 7 | // and return a string or object containing information about the next 8 | // token, or null to pass and have the (new) state be called to finish 9 | // the token. When a string is given, it is wrapped in a {style, type} 10 | // object. In the resulting object, the characters consumed are stored 11 | // under the content property. Any whitespace following them is also 12 | // automatically consumed, and added to the value property. (Thus, 13 | // content is the actual meaningful part of the token, while value 14 | // contains all the text it spans.) 15 | 16 | function tokenizer(source, state) { 17 | // Newlines are always a separate token. 18 | function isWhiteSpace(ch) { 19 | // The messy regexp is because IE's regexp matcher is of the 20 | // opinion that non-breaking spaces are no whitespace. 21 | return ch != "\n" && /^[\s\u00a0]*$/.test(ch); 22 | } 23 | 24 | var tokenizer = { 25 | state: state, 26 | 27 | take: function(type) { 28 | if (typeof(type) == "string") 29 | type = {style: type, type: type}; 30 | 31 | type.content = (type.content || "") + source.get(); 32 | if (!/\n$/.test(type.content)) 33 | source.nextWhile(isWhiteSpace); 34 | type.value = type.content + source.get(); 35 | return type; 36 | }, 37 | 38 | next: function () { 39 | if (!source.more()) throw StopIteration; 40 | 41 | var type; 42 | if (source.equals("\n")) { 43 | source.next(); 44 | return this.take("whitespace"); 45 | } 46 | 47 | if (source.applies(isWhiteSpace)) 48 | type = "whitespace"; 49 | else 50 | while (!type) 51 | type = this.state(source, function(s) {tokenizer.state = s;}); 52 | 53 | return this.take(type); 54 | } 55 | }; 56 | return tokenizer; 57 | } 58 | -------------------------------------------------------------------------------- /script/ResetPassword.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use IbexFarm; 4 | use Net::SSLeay; 5 | use Crypt::Argon2; 6 | use File::Spec::Functions qw( catfile ); 7 | 8 | if (scalar(@ARGV) < 1 || scalar(@ARGV) > 2) { 9 | print STDERR "Bad usage: pass username as first argument, password as optional second argument.\n"; 10 | exit 1 11 | } 12 | 13 | my $username = $ARGV[0]; 14 | my $user_file = catfile(IbexFarm->config->{deployment_dir}, $username, IbexFarm->config->{USER_FILE_NAME}); 15 | 16 | if (! -f $user_file) { 17 | print STDERR "User '$username' not found.\n"; 18 | exit 1 19 | } 20 | 21 | sub get_salt { 22 | my $length = shift; 23 | my @salt_pool = ('A' .. 'Z', 'a' .. 'z', 0 .. 9, '+','/','='); 24 | my $salt_pool_length = 26 * 2 + 10 + 3; 25 | my $rb = ''; 26 | Net::SSLeay::RAND_bytes($rb, $length); 27 | my $out = ''; 28 | for (my $i = 0; $i < $length; ++$i) { 29 | $out .= $salt_pool[ord(substr($rb, $i, $i+1)) % $salt_pool_length]; 30 | } 31 | return $out; 32 | }; 33 | 34 | sub get_random_password { 35 | my $length = 16; 36 | my @pool = ('A' .. 'Z', 'a' .. 'z', 0 .. 9); 37 | my $pool_length = 26 * 2 + 10; 38 | my $rb = ''; 39 | Net::SSLeay::RAND_bytes($rb, $length); 40 | my $out = ''; 41 | for (my $i = 0; $i < $length; ++$i) { 42 | $out .= $pool[ord(substr($rb, $i, $i+1)) % $pool_length]; 43 | } 44 | return $out; 45 | } 46 | 47 | sub make_pw_hash { 48 | my $password = shift; 49 | my $salt = get_salt(IbexFarm->config->{argon2id_salt_length}); 50 | return Crypt::Argon2::argon2id_pass( 51 | $password, 52 | $salt, 53 | IbexFarm->config->{argon2id_t_cost}, 54 | IbexFarm->config->{argon2id_m_factor}, 55 | IbexFarm->config->{argon2id_parallelism}, 56 | IbexFarm->config->{argon2id_tag_size}, 57 | ); 58 | } 59 | 60 | my $newpw; 61 | if (scalar(@ARGV) == 1) { 62 | $newpw = get_random_password(); 63 | } else { 64 | $newpw = $ARGV[1]; 65 | } 66 | my $newpwhash = make_pw_hash($newpw); 67 | 68 | IbexFarm::Util::update_json_file( 69 | $user_file, 70 | sub { 71 | my $j = shift; 72 | $j->{password} = $newpwhash; 73 | return $j; 74 | } 75 | ); 76 | 77 | print "The password for user '$username' has been reset to:\n$newpw\n"; -------------------------------------------------------------------------------- /root/static/codemirror/highlight.js: -------------------------------------------------------------------------------- 1 | // Minimal framing needed to use CodeMirror-style parsers to highlight 2 | // code. Load this along with tokenize.js, stringstream.js, and your 3 | // parser. Then call highlightText, passing a string as the first 4 | // argument, and as the second argument either a callback function 5 | // that will be called with an array of SPAN nodes for every line in 6 | // the code, or a DOM node to which to append these spans, and 7 | // optionally (not needed if you only loaded one parser) a parser 8 | // object. 9 | 10 | // Stuff from util.js that the parsers are using. 11 | var StopIteration = {toString: function() {return "StopIteration"}}; 12 | 13 | var Editor = {}; 14 | var indentUnit = 2; 15 | 16 | (function(){ 17 | function normaliseString(string) { 18 | var tab = ""; 19 | for (var i = 0; i < indentUnit; i++) tab += " "; 20 | 21 | string = string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n"); 22 | var pos = 0, parts = [], lines = string.split("\n"); 23 | for (var line = 0; line < lines.length; line++) { 24 | if (line != 0) parts.push("\n"); 25 | parts.push(lines[line]); 26 | } 27 | 28 | return { 29 | next: function() { 30 | if (pos < parts.length) return parts[pos++]; 31 | else throw StopIteration; 32 | } 33 | }; 34 | } 35 | 36 | window.highlightText = function(string, callback, parser) { 37 | parser = (parser || Editor.Parser).make(stringStream(normaliseString(string))); 38 | var line = []; 39 | if (callback.nodeType == 1) { 40 | var node = callback; 41 | callback = function(line) { 42 | for (var i = 0; i < line.length; i++) 43 | node.appendChild(line[i]); 44 | node.appendChild(document.createElement("BR")); 45 | }; 46 | } 47 | 48 | try { 49 | while (true) { 50 | var token = parser.next(); 51 | if (token.value == "\n") { 52 | callback(line); 53 | line = []; 54 | } 55 | else { 56 | var span = document.createElement("SPAN"); 57 | span.className = token.style; 58 | span.appendChild(document.createTextNode(token.value)); 59 | line.push(span); 60 | } 61 | } 62 | } 63 | catch (e) { 64 | if (e != StopIteration) throw e; 65 | } 66 | if (line.length) callback(line); 67 | } 68 | })(); 69 | -------------------------------------------------------------------------------- /lib/IbexFarm/AuthStore.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::AuthStore; 2 | 3 | # Code based on Catalyst::Authentication::Store::Minimal; 4 | 5 | use strict; 6 | use warnings; 7 | 8 | use Catalyst::Authentication::User::Hash; 9 | use File::Spec::Functions qw( splitdir catdir catfile splitpath no_upwards ); 10 | use JSON::XS; 11 | use Digest; 12 | use Crypt::Argon2; 13 | 14 | { 15 | package MyUserHash; 16 | use base 'Catalyst::Authentication::User::Hash'; 17 | 18 | sub check_password { 19 | my ($self, $password) = @_; 20 | 21 | if ($self->password =~ /^\$/) { 22 | # It's a new password. 23 | return Crypt::Argon2::argon2id_verify($self->password, $password); 24 | } else { 25 | # It's an old password. 26 | my $salt = substr($self->password, - IbexFarm->config->{user_password_salt_length}); 27 | my $digest = Digest->new(IbexFarm->config->{user_password_hash_algo}); 28 | $digest->add($password . $salt); 29 | my $b64 = $digest->b64digest; 30 | return $b64 eq substr($self->password, 0, 31 | IbexFarm->config->{user_password_hash_total_length} - IbexFarm->config->{user_password_salt_length}); 32 | } 33 | } 34 | }; 35 | 36 | sub new { 37 | my $class = shift; 38 | bless { }, $class; 39 | } 40 | 41 | sub from_session { 42 | my ($self, $c, $id) = @_; 43 | 44 | return $id if ref $id; 45 | 46 | $self->find_user({ id => $id }); 47 | } 48 | 49 | sub find_user { 50 | my ($self, $userinfo, $c) = @_; 51 | 52 | my $id = $userinfo->{id}; 53 | $id ||= $userinfo->{username}; 54 | 55 | my $udir = catdir(IbexFarm->config->{deployment_dir}, $id); 56 | return unless (-d $udir); 57 | 58 | my $ufile = catfile($udir, IbexFarm->config->{USER_FILE_NAME}); 59 | die "User dir without '", IbexFarm->config->{USER_FILE_NAME}, "' file: $udir" unless (-f $ufile); 60 | open my $f, $ufile or die "Unable to open '", IbexFarm->config->{USER_FILE_NAME}, "' file: $!"; 61 | local $/; 62 | my $contents = <$f>; 63 | my $coder = JSON::XS->new->boolean_values(\0, \1); 64 | my $json = $coder->decode($contents); 65 | die "Bad JSON in '", IbexFarm->config->{USER_FILE_NAME}, "' file" unless (ref($json) eq 'HASH'); 66 | close $f or die "Unable to close '", IbexFarm->config->{USER_FILE_NAME}, "' file: $!"; 67 | 68 | $json->{id} ||= $json->{username}; 69 | $json->{username} ||= $json->{id}; 70 | 71 | return MyUserHash->new(%$json); 72 | } 73 | 74 | 1; 75 | -------------------------------------------------------------------------------- /lib/IbexFarm/Util.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::Util; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use parent 'Exporter'; 7 | 8 | use JSON::XS; 9 | use File::Spec::Functions qw( catfile ); 10 | use Log::Handler; 11 | 12 | sub update_json_file { 13 | my ($filename, $updatef) = @_; 14 | 15 | open my $f, $filename or die "Unable to open '$filename' for reading: $!"; 16 | local $/; 17 | my $contents = <$f>; 18 | close $f or die "Unable to close '$filename' after reading: $!"; 19 | my $coder = JSON::XS->new->boolean_values(\0, \1); 20 | my $json = $coder->decode($contents); 21 | die "Bad JSON in '$filename' file" unless (ref($json) eq "HASH"); 22 | 23 | # Note: in principle, we could read version 1 of the file, then 24 | # someone else could write version 2, and we'd end up writing 25 | # version 1.1 instead of version 2.1. Not worth guarding against 26 | # this since if multiple updates are occuring at the same time, 27 | # unexpected results are going to occur whatever order we process 28 | # the updates. 29 | 30 | my $newjson = $updatef->($json); 31 | if (defined $newjson) { 32 | open my $of, ">>$filename", or die "Unable to open '$filename': $!"; 33 | flock $of, 2 or die "Unable to lock '$filename': $!"; 34 | truncate $of, 0 or die "Unable to truncate '$filename': $!"; 35 | seek $of, 0, 0 or die "Really?: $!"; 36 | print $of JSON::XS::encode_json($newjson); 37 | flock $of, 8; # Unlock; 38 | close $of or die "Unable to close '$filename' after writing: :$!"; 39 | } 40 | 41 | return $newjson; 42 | } 43 | 44 | # Files to skip when going through the contents of a directory. 45 | sub is_special_file { 46 | return shift =~ /^[:.]/; 47 | } 48 | 49 | sub get_experiment_version { 50 | my $edir = shift; 51 | open my $vf, catfile($edir, IbexFarm->config->{ibex_archive_root_dir}, 'VERSION') or die "Unable to open VERSION file"; 52 | my $version = <$vf>; 53 | close $vf or die "Unable to close 'VERSION' file: $!"; 54 | die "Unable to read from 'VERSION' file: $!" unless (defined $version); 55 | $version =~ s/\s*$//; 56 | return $version; 57 | } 58 | 59 | my $event_logger; 60 | sub log_event { 61 | my $info = shift; 62 | 63 | if (IbexFarm->config->{event_log_file}) { 64 | $event_logger = Log::Handler->get_logger("event_log") unless ($event_logger); 65 | return $event_logger->info($info); 66 | } 67 | } 68 | 69 | our @EXPORT_OK = qw( update_json_file is_special_file log_event ); 70 | -------------------------------------------------------------------------------- /root/manage.tt: -------------------------------------------------------------------------------- 1 | [% USE HTML %] 2 | [% USE URL %] 3 | [% META title = '- Manage Experiment' %] 4 | [% META external_js_scripts = '/static/codemirror/codemirror.js' %] 5 | [% META external_css_files = '/static/jqueryui/jquery-ui-1.8.4.custom.css' %] 6 | [% META js_scripts = 'uicommon.js ajaxupload.js fman.js' %] 7 | 8 |
9 |
10 | [% eurl = experiment_base_url _ c.user.username _ '/' _ experiment _ '/experiment.html' | url %] 11 | [% HTML.escape(eurl) %] 12 |
13 |

14 | Go to the my account page to view your other experiments or to create/delete experiments. 15 |

16 |

Experiment ‘[% experiment %]’ (ibex [% ibex_version %])

17 |
18 | Update from git repo» 19 | (help) 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 37 |
31 | 32 | 33 | 34 | 35 |
36 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 |

Access

47 |

48 | The level of security provided by this form of password protection is very basic. Don't rely on it to hide any sensitive information. 49 |

50 |

51 | Modifying the password may lead to odd behavior in some browsers if you do not clear your browser's cache of logged-in HTTP sessions. 52 |

53 |
54 |
55 | 56 |

Archive

57 |

You can download a 58 | zip archive 59 | of the files above.

60 |

61 | In some browsers, you may need to right click and select “save link as” to download the file. The link will only work when you are logged in. 62 |

63 | -------------------------------------------------------------------------------- /root/static/codemirror/mirrorframe.js: -------------------------------------------------------------------------------- 1 | /* Demonstration of embedding CodeMirror in a bigger application. The 2 | * interface defined here is a mess of prompts and confirms, and 3 | * should probably not be used in a real project. 4 | */ 5 | 6 | function MirrorFrame(place, options) { 7 | this.home = document.createElement("DIV"); 8 | if (place.appendChild) 9 | place.appendChild(this.home); 10 | else 11 | place(this.home); 12 | 13 | var self = this; 14 | function makeButton(name, action) { 15 | var button = document.createElement("INPUT"); 16 | button.type = "button"; 17 | button.value = name; 18 | self.home.appendChild(button); 19 | button.onclick = function(){self[action].call(self);}; 20 | } 21 | 22 | makeButton("Search", "search"); 23 | makeButton("Replace", "replace"); 24 | makeButton("Current line", "line"); 25 | makeButton("Jump to line", "jump"); 26 | makeButton("Insert constructor", "macro"); 27 | makeButton("Indent all", "reindent"); 28 | 29 | this.mirror = new CodeMirror(this.home, options); 30 | } 31 | 32 | MirrorFrame.prototype = { 33 | search: function() { 34 | var text = prompt("Enter search term:", ""); 35 | if (!text) return; 36 | 37 | var first = true; 38 | do { 39 | var cursor = this.mirror.getSearchCursor(text, first); 40 | first = false; 41 | while (cursor.findNext()) { 42 | cursor.select(); 43 | if (!confirm("Search again?")) 44 | return; 45 | } 46 | } while (confirm("End of document reached. Start over?")); 47 | }, 48 | 49 | replace: function() { 50 | // This is a replace-all, but it is possible to implement a 51 | // prompting replace. 52 | var from = prompt("Enter search string:", ""), to; 53 | if (from) to = prompt("What should it be replaced with?", ""); 54 | if (to == null) return; 55 | 56 | var cursor = this.mirror.getSearchCursor(from, false); 57 | while (cursor.findNext()) 58 | cursor.replace(to); 59 | }, 60 | 61 | jump: function() { 62 | var line = prompt("Jump to line:", ""); 63 | if (line && !isNaN(Number(line))) 64 | this.mirror.jumpToLine(Number(line)); 65 | }, 66 | 67 | line: function() { 68 | alert("The cursor is currently at line " + this.mirror.currentLine()); 69 | this.mirror.focus(); 70 | }, 71 | 72 | macro: function() { 73 | var name = prompt("Name your constructor:", ""); 74 | if (name) 75 | this.mirror.replaceSelection("function " + name + "() {\n \n}\n\n" + name + ".prototype = {\n \n};\n"); 76 | }, 77 | 78 | reindent: function() { 79 | this.mirror.reindent(); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /root/static/codemirror/parsehtmlmixed.js: -------------------------------------------------------------------------------- 1 | var HTMLMixedParser = Editor.Parser = (function() { 2 | if (!(CSSParser && JSParser && XMLParser)) 3 | throw new Error("CSS, JS, and XML parsers must be loaded for HTML mixed mode to work."); 4 | XMLParser.configure({useHTMLKludges: true}); 5 | 6 | function parseMixed(stream) { 7 | var htmlParser = XMLParser.make(stream), localParser = null, inTag = false; 8 | var iter = {next: top, copy: copy}; 9 | 10 | function top() { 11 | var token = htmlParser.next(); 12 | if (token.content == "<") 13 | inTag = true; 14 | else if (token.style == "xml-tagname" && inTag === true) 15 | inTag = token.content.toLowerCase(); 16 | else if (token.content == ">") { 17 | if (inTag == "script") 18 | iter.next = local(JSParser, "config->{deployment_www_dir}) { 15 | return catdir(IbexFarm->config->{deployment_www_dir}, 16 | $username, $expname); 17 | } 18 | else { 19 | return catdir(IbexFarm->config->{deployment_dir}, 20 | $username, $expname, 21 | IbexFarm->config->{ibex_archive_root_dir}); 22 | } 23 | }; 24 | 25 | sub password_protect_experiment { 26 | my ($self, $username, $expname, $password) = @_; 27 | 28 | my $edir = $getedir->($username, $expname); 29 | 30 | # Note that '/' cannot appear in an experiment or user name, so this 31 | # username is guaranteed to be unique. 32 | my $uname = "$username/$expname"; 33 | 34 | system(IbexFarm->config->{password_protect_apache}->{htpasswd}, 35 | "-b", 36 | IbexFarm->config->{password_protect_apache}->{passwd_file}, 37 | $uname, 38 | $password); 39 | if ($? != 0) { 40 | die "Failure ($?) executing " . IbexFarm->config->{password_protect_apache}->{htpasswd} . " for username $uname"; 41 | } 42 | 43 | open my $htaccess, ">" . catfile($edir, '.htaccess') or die "Unable to create .htaccess file (" . catfile($edir, '.htaccess') . "): $!"; 44 | my $ufile = IbexFarm->config->{password_protect_apache}->{passwd_file}; 45 | print $htaccess <($username, $expname); 62 | my $uname = "$username/$expname"; 63 | 64 | if (-f catfile($edir, '.htaccess')) { 65 | unlink catfile($edir, '.htaccess') or die "Unable to remove .htaccess file: $!"; 66 | } 67 | 68 | system(IbexFarm->config->{password_protect_apache}->{htpasswd}, 69 | "-D", 70 | IbexFarm->config->{password_protect_apache}->{passwd_file}, 71 | $uname); 72 | if ($? != 0) { 73 | die "Failure ($?) executing " . IbexFarm->config->{password_protect_apache}->{htpasswd}; 74 | } 75 | } 76 | 77 | IbexFarm::PasswordProtectExperiment::Factory->add_factory_type(Apache => __PACKAGE__); 78 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | ibexfarm: 4 | environment: 5 | IBEXFARM_src_dir: ${IBEXFARM_src_dir:-/ibexfarm.git} 6 | IBEXFARM_url_prefix: ${IBEXFARM_url_prefix:-/} 7 | IBEXFARM_experiment_base_url: ${IBEXFARM_experiment_base_url:-/ibexexps} 8 | IBEXFARM_port: ${IBEXFARM_port:-80} 9 | IBEXFARM_config_url_envvar: ${IBEXFARM_config_url_envvar:-IBEXFARM_config_url} 10 | IBEXFARM_config_url: ${IBEXFARM_config_url:-http://localhost/ajax/config} 11 | IBEXFARM_webmaster_email: ${IBEXFARM_webmaster_email:-example@example.com} 12 | IBEXFARM_webmaster_name: ${IBEXFARM_webmaster_name:-Anonymous Coward} 13 | IBEXFARM_front_page_html_message: ${IBEXFARM_front_page_html_message:-} 14 | IBEXFARM_ibex_archive_root_dir: ${IBEXFARM_ibex_archive_root_dir:-ibex} 15 | IBEXFARM_config_secret: ${IBEXFARM_config_secret:-unused-non-secret} 16 | IBEXFARM_host: ${IBEXFARM_host:-localhost} 17 | IBEXFARM_dont_chown_data_volume: ${IBEXFARM_dont_chown_data_volume:-} 18 | 19 | # See https://metacpan.org/pod/Crypt::Argon2 before changing these from defaults 20 | IBEXFARM_argon2id_t_cost: ${IBEXFARM_argon2id_t_cost:-5} 21 | IBEXFARM_argon2id_m_factor: ${IBEXFARM_argon2id_m_factor:-32M} 22 | IBEXFARM_argon2id_parallelism: ${IBEXFARM_argon2id_parallelism:-1} 23 | IBEXFARM_argon2id_tag_size: ${IBEXFARM_argon2id_tag_size:-16} 24 | IBEXFARM_argon2id_salt_length: ${IBEXFARM_argon2id_salt_length:-16} 25 | 26 | # Set to 1 to rehash old passwords using Argon2id (only useful if upgrading 27 | # from older IbexFarm version). 28 | IBEXFARM_rehash_old_passwords: ${IBEXFARM_rehash_old_passwords:-} 29 | 30 | IBEXFARM_enforce_quotas: ${IBEXFARM_enforce_quotas:-0} 31 | IBEXFARM_quota_max_files_in_dir: ${IBEXFARM_quota_max_files_in_dir:-500} 32 | IBEXFARM_quota_max_file_size: ${IBEXFARM_quota_max_file_size:-1048576} 33 | IBEXFARM_quota_max_total_size: ${IBEXFARM_quota_max_total_size:-1048576} 34 | 35 | # Don't enable this (legacy hacks for spellout.net/ibexfarm). 36 | IBEXFARM_spellout_legacy_jank: ${IBEXFARM_spellout_legacy_jank:-} 37 | 38 | # Not needed except for spellout.net/ibexfarm (for legacy reasons). 39 | # docker-compose syntax doesn't seem to make it possible to make 40 | # these values conditional on IBEXFARM_spellout_legacy_jank being 41 | # set. 42 | extra_hosts: 43 | - "spellout.user.openhosting.com:127.0.0.1" 44 | - "spellout.net:127.0.0.1" 45 | 46 | ports: 47 | - "127.0.0.1:${IBEXFARM_host_port:-8888}:${IBEXFARM_port:-80}" 48 | volumes: 49 | - '../:/code' 50 | - ${IBEXFARM_data_volume:-ibexdata}:/ibexdata 51 | build: '.' 52 | volumes: 53 | ibexdata: 54 | -------------------------------------------------------------------------------- /root/common.js: -------------------------------------------------------------------------------- 1 | // IE caches all ajax GET requests, so when using IE always use POST. 2 | /*@cc_on 3 | $.getJSON=function(uri,callback){return $.post(uri,{},callback,"json");}; 4 | (function(){var oldajax=$.ajax;$.ajax=function(opts){opts.type="POST";return oldajax(opts);};})(); 5 | @*/ 6 | 7 | var STD_TOGGLE_SPEED = "fast"; 8 | 9 | // Taken from http://www.quirksmode.org/js/cookies.html 10 | function createCookie(name,value,days) { 11 | if (days) { 12 | var date = new Date(); 13 | date.setTime(date.getTime()+(days*24*60*60*1000)); 14 | var expires = "; expires="+date.toGMTString(); 15 | } 16 | else var expires = ""; 17 | document.cookie = name+"="+value+expires+"; path=/"; 18 | } 19 | 20 | // As above. 21 | function readCookie(name) { 22 | var nameEQ = name + "="; 23 | var ca = document.cookie.split(';'); 24 | for(var i=0;i < ca.length;i++) { 25 | var c = ca[i]; 26 | while (c.charAt(0)==' ') c = c.substring(1,c.length); 27 | if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); 28 | } 29 | return null; 30 | } 31 | 32 | // As above. 33 | function eraseCookie(name) { 34 | createCookie(name,"",-1); 35 | } 36 | 37 | $.ajaxSetup({cache: false, global: false}); 38 | 39 | // Turning off caching can cause jQuery to send POST instead of GET. 40 | // For file progress queries, we must ensure that a GET request is sent. 41 | function cachedGetJSON(url, callback) { 42 | return $.ajax({ 43 | cache: true, 44 | url: url, 45 | type: "GET", 46 | success: callback, 47 | dataType: "json" 48 | }); 49 | } 50 | 51 | $(document).ready(function () { 52 | // XHTML standards compliance idiocy. 53 | $("a[rel=external]").attr('target', '_blank'); 54 | 55 | // Move to error/message div if there is one on the page. 56 | var e = $(".error"); 57 | if (e.length && ! $(e.get(0)).hasClass("noskipto")) { 58 | $(e.get(0)).attr('id', 'error'); 59 | window.location = '#error'; 60 | } 61 | else { 62 | var m = $(".message"); 63 | if (m.length && ! $(m.get(0)).hasClass("noskipto")) { 64 | $(m.get(0)).attr('id', 'message'); 65 | window.location = '#message'; 66 | } 67 | } 68 | 69 | // Message divs dissapear after a few seconds. 70 | setTimeout(function () { 71 | var ms = $(".message"); 72 | for (var i = 0; i < ms.length; ++i) { 73 | if ($(m[i]).hasClass("dontremove")) 74 | $(ms[i]).fadeTo("slow", 0); 75 | else 76 | $(ms[i]).fadeOut("slow"); 77 | } 78 | }, 3000); 79 | }); 80 | 81 | __IS_IE6__ = false; 82 | /*@cc_on 83 | @if (@_jscript_version == 5.6) 84 | __IS_IE6__ = true; 85 | @elsif (@_jscript_version == 5.7) 86 | __IS_IE6__ = !window.XMLHttpRequest; 87 | @end 88 | @*/ 89 | -------------------------------------------------------------------------------- /root/wrapper.tt: -------------------------------------------------------------------------------- 1 | 2 | [%# %] 3 | [% USE Filter.Minify.CSS %] 4 | [% USE Filter.Minify.JavaScript %] 5 | [% CSS_FILTER = c.debug ? "repeat" : "minify_css" %] 6 | [% JS_FILTER = c.debug ? "repeat" : "minify_js" %] 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | [% FOREACH f IN template.external_js_scripts.split('\s+') %] 15 | 16 | [% END %] 17 | [% FOREACH f IN template.external_css_files.split('\s+') %] 18 | 19 | [% END %] 20 | 27 | [% FOREACH f IN template.js_scripts.split('\s+') %] 28 | 35 | [% END %] 36 | 44 | [% IF IS_IE %] 45 | 55 | [% END %] 56 | Ibex farm [% template.title or '' %] 57 | 58 | 59 | 60 |
61 |
62 | 63 |

Ibex Farm

64 | 65 |
66 | 67 | home 68 | [% IF ! c.user_exists %] | log in 69 | | 70 | create an account 71 | [% END %] | 72 | ibex docs 73 | [% IF c.user_exists %] 74 |

75 | You are logged in as [% c.user.username %] (logout). 76 |

77 | [% END %] 78 |
79 | [% content %] 80 | 81 |
[%# outer2 %] 82 |
[%# outer1 %] 83 | 84 | 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Alex Drummond 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * The name of the author may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ALEX DRUMMOND BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | 27 | -------------------------------------------------------------------------------- 28 | 29 | 30 | This software makes use of the CodeMirror library (http://codemirror.net), 31 | which is licensed under the following zlib-style license: 32 | 33 | Copyright (c) 2007-2010 Marijn Haverbeke 34 | 35 | This software is provided 'as-is', without any express or implied 36 | warranty. In no event will the authors be held liable for any 37 | damages arising from the use of this software. 38 | 39 | Permission is granted to anyone to use this software for any 40 | purpose, including commercial applications, and to alter it and 41 | redistribute it freely, subject to the following restrictions: 42 | 43 | 1. The origin of this software must not be misrepresented; you must 44 | not claim that you wrote the original software. If you use this 45 | software in a product, an acknowledgment in the product 46 | documentation would be appreciated but is not required. 47 | 48 | 2. Altered source versions must be plainly marked as such, and must 49 | not be misrepresented as being the original software. 50 | 51 | 3. This notice may not be removed or altered from any source 52 | distribution. 53 | 54 | Marijn Haverbeke 55 | marijnh@gmail.com 56 | 57 | With regard to point (2) above, some minor changes have been made to the 58 | code. -------------------------------------------------------------------------------- /root/static/codemirror/util.js: -------------------------------------------------------------------------------- 1 | /* A few useful utility functions. */ 2 | 3 | // Capture a method on an object. 4 | function method(obj, name) { 5 | return function() {obj[name].apply(obj, arguments);}; 6 | } 7 | 8 | // The value used to signal the end of a sequence in iterators. 9 | var StopIteration = {toString: function() {return "StopIteration"}}; 10 | 11 | // Apply a function to each element in a sequence. 12 | function forEach(iter, f) { 13 | if (iter.next) { 14 | try {while (true) f(iter.next());} 15 | catch (e) {if (e != StopIteration) throw e;} 16 | } 17 | else { 18 | for (var i = 0; i < iter.length; i++) 19 | f(iter[i]); 20 | } 21 | } 22 | 23 | // Map a function over a sequence, producing an array of results. 24 | function map(iter, f) { 25 | var accum = []; 26 | forEach(iter, function(val) {accum.push(f(val));}); 27 | return accum; 28 | } 29 | 30 | // Create a predicate function that tests a string againsts a given 31 | // regular expression. No longer used but might be used by 3rd party 32 | // parsers. 33 | function matcher(regexp){ 34 | return function(value){return regexp.test(value);}; 35 | } 36 | 37 | // Test whether a DOM node has a certain CSS class. Much faster than 38 | // the MochiKit equivalent, for some reason. 39 | function hasClass(element, className){ 40 | var classes = element.className; 41 | return classes && new RegExp("(^| )" + className + "($| )").test(classes); 42 | } 43 | 44 | // Insert a DOM node after another node. 45 | function insertAfter(newNode, oldNode) { 46 | var parent = oldNode.parentNode; 47 | parent.insertBefore(newNode, oldNode.nextSibling); 48 | return newNode; 49 | } 50 | 51 | function removeElement(node) { 52 | if (node.parentNode) 53 | node.parentNode.removeChild(node); 54 | } 55 | 56 | function clearElement(node) { 57 | while (node.firstChild) 58 | node.removeChild(node.firstChild); 59 | } 60 | 61 | // Check whether a node is contained in another one. 62 | function isAncestor(node, child) { 63 | while (child = child.parentNode) { 64 | if (node == child) 65 | return true; 66 | } 67 | return false; 68 | } 69 | 70 | // The non-breaking space character. 71 | var nbsp = "\u00a0"; 72 | var matching = {"{": "}", "[": "]", "(": ")", 73 | "}": "{", "]": "[", ")": "("}; 74 | 75 | // Standardize a few unportable event properties. 76 | function normalizeEvent(event) { 77 | if (!event.stopPropagation) { 78 | event.stopPropagation = function() {this.cancelBubble = true;}; 79 | event.preventDefault = function() {this.returnValue = false;}; 80 | } 81 | if (!event.stop) { 82 | event.stop = function() { 83 | this.stopPropagation(); 84 | this.preventDefault(); 85 | }; 86 | } 87 | 88 | if (event.type == "keypress") { 89 | event.code = (event.charCode == null) ? event.keyCode : event.charCode; 90 | event.character = String.fromCharCode(event.code); 91 | } 92 | return event; 93 | } 94 | 95 | // Portably register event handlers. 96 | function addEventHandler(node, type, handler, removeFunc) { 97 | function wrapHandler(event) { 98 | handler(normalizeEvent(event || window.event)); 99 | } 100 | if (typeof node.addEventListener == "function") { 101 | node.addEventListener(type, wrapHandler, false); 102 | if (removeFunc) return function() {node.removeEventListener(type, wrapHandler, false);}; 103 | } 104 | else { 105 | node.attachEvent("on" + type, wrapHandler); 106 | if (removeFunc) return function() {node.detachEvent("on" + type, wrapHandler);}; 107 | } 108 | } 109 | 110 | function nodeText(node) { 111 | return node.textContent || node.innerText || node.nodeValue || ""; 112 | } 113 | 114 | function nodeTop(node) { 115 | var top = 0; 116 | while (node.offsetParent) { 117 | top += node.offsetTop; 118 | node = node.offsetParent; 119 | } 120 | return top; 121 | } 122 | 123 | function isBR(node) { 124 | var nn = node.nodeName; 125 | return nn == "BR" || nn == "br"; 126 | } 127 | function isSpan(node) { 128 | var nn = node.nodeName; 129 | return nn == "SPAN" || nn == "span"; 130 | } 131 | -------------------------------------------------------------------------------- /lib/IbexFarm.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Catalyst::Runtime 5.80; 7 | 8 | # Set flags and add plugins for the application 9 | # 10 | # -Debug: activates the debug mode for very useful log messages 11 | # ConfigLoader: will load the configuration from a Config::General file in the 12 | # application's home directory 13 | # Static::Simple: will serve static files from the application's root 14 | # directory 15 | 16 | use parent qw/Catalyst/; 17 | use Catalyst qw/ConfigLoader 18 | 19 | ConfigLoader::Environment 20 | 21 | Authentication 22 | Authentication::Credential::Password 23 | 24 | Session 25 | Session::Store::File 26 | Session::State::Cookie 27 | 28 | Cache::FileCache 29 | UploadProgress 30 | /; 31 | use IbexFarm::AuthStore; 32 | use Log::Handler; 33 | use Moose; 34 | our $VERSION = '0.01'; 35 | 36 | # Configure the application. 37 | # 38 | # Note that settings in ibexfarm.conf (or other external 39 | # configuration file that you set up manually) take precedence 40 | # over this when using ConfigLoader. Thus configuration 41 | # details given here can function as a default configuration, 42 | # with an external configuration file acting as an override for 43 | # local deployment. 44 | 45 | __PACKAGE__->config( 46 | parse_on_demand => 1, 47 | name => 'IbexFarm', 48 | default_view => 'TT', 49 | 'Plugin::Authentication' => { 50 | default_realm => 'users', 51 | realms => { 52 | users => { 53 | credential => { 54 | class => 'Password', 55 | password_field => 'password', 56 | # Weird mismatches between behavior of DBIx::Class::EncodedColumn and Crypt::SaltedHash 57 | # force us to do this manually. 58 | password_type => 'self_check', 59 | # password_type => 'salted_hash', 60 | # password_hash_type => 'SHA-512', 61 | # password_salt_len => 32 62 | }, 63 | store => { 64 | class => '+IbexFarm::AuthStore', 65 | user_model => 'Catalyst::Authentication::User::Hash', 66 | password_type => 'clear', 67 | } 68 | } 69 | } 70 | }, 71 | 'Plugin::Session' => { 72 | storage => '/ibexdata/ibexfarm_session', 73 | }, 74 | cache => { 75 | expires => 48 * 60 * 60 # seconds 76 | }, 77 | USER_FILE_NAME => 'USER', 78 | 79 | user_password_hash_algo => 'SHA-512', # legacy 80 | user_password_salt_length => 32, # legacy 81 | user_password_hash_total_length => 118, # legacy 82 | argon2id_salt_length => 16, 83 | argon2id_t_cost => 5, 84 | argon2id_m_factor => '32M', 85 | argon2id_parallelism => 1, 86 | argon2id_tag_size => 16 87 | ); 88 | 89 | after setup_finalize => sub { 90 | # Open the event log, if we're keeping one. 91 | if (__PACKAGE__->config->{event_log_file}) { 92 | my $logger = Log::Handler->create_logger("event_log"); 93 | $logger->add(file => { filename => __PACKAGE__->config->{event_log_file}, 94 | maxlevel => "debug", 95 | minlevel => "info" } ); 96 | } 97 | }; 98 | 99 | # Start the application 100 | my @args; 101 | push @args, 'Static::Simple' if ($ENV{STATIC}); 102 | __PACKAGE__->setup(@args); 103 | 104 | =head1 NAME 105 | 106 | IbexFarm - Catalyst based application 107 | 108 | =head1 SYNOPSIS 109 | 110 | script/ibexfarm_server.pl 111 | 112 | =head1 DESCRIPTION 113 | 114 | [enter your description here] 115 | 116 | =head1 SEE ALSO 117 | 118 | L, L 119 | 120 | =head1 AUTHOR 121 | 122 | Alex Drummond 123 | 124 | =head1 LICENSE 125 | 126 | This library is free software. You can redistribute it and/or modify 127 | it under the same terms as Perl itself. 128 | 129 | =cut 130 | 131 | 1; 132 | -------------------------------------------------------------------------------- /lib/IbexFarm/Controller/Experiment.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::Controller::Experiment; 2 | 3 | use strict; 4 | use warnings; 5 | use parent 'Catalyst::Controller'; 6 | use File::Spec::Functions qw( catfile catdir ); 7 | use URI; 8 | 9 | my $experiment_base_url = (sub { 10 | my $b = IbexFarm->config->{url_prefix}; 11 | my $e = IbexFarm->config->{experiment_base_url}; 12 | # URI module does weird things if the base url doesn't 13 | # end with a '/' 14 | if ($b !~ /\/$/) { 15 | $b .= '/'; 16 | } 17 | return URI->new($e)->abs($b)->as_string; 18 | })->(); 19 | 20 | sub manage :Absolute { 21 | my ($self, $c, $experiment) = (shift, shift, shift); 22 | return $c->res->redirect($c->uri_for('/login')) unless $c->user_exists; 23 | return $c->res->redirect($c->uri_for('/myaccount')) unless $experiment; 24 | $c->detach('Root', 'default') if scalar(@_); 25 | 26 | # Open USER file to get saved values for git repo/branch. 27 | open my $uf, catfile(IbexFarm->config->{deployment_dir}, $c->user->username, IbexFarm->config->{USER_FILE_NAME}) or 28 | die "Unable to open '", IbexFarm->config->{USER_FILE_NAME}, "' file for reading: $!"; 29 | local $/; 30 | my $contents = <$uf>; 31 | defined $contents or die "Error reading '", IbexFarm->config->{USER_FILE_NAME}, "' file: $!"; 32 | close $uf or die "Error closing '", IbexFarm->config->{USER_FILE_NAME}, "' file: $!"; 33 | my $coder = JSON::XS->new->boolean_values(\0, \1); 34 | my $json = $coder->decode($contents) or die "Error decoding JSON"; 35 | 36 | # So that other pages can point back to this one. 37 | $c->flash->{back_uri} = $c->uri_for('/manage/' . $experiment); 38 | $c->flash->{experiment_name} = $experiment; 39 | 40 | $c->stash->{experiment_base_url} = $experiment_base_url; 41 | $c->stash->{experiment} = $experiment; 42 | $c->stash->{ibex_version} = 43 | IbexFarm::Util::get_experiment_version(catdir(IbexFarm->config->{deployment_dir}, $c->user->username, $experiment)); 44 | 45 | # In the old (dumb) days we stored the git URL per user rather than per user per experiment. 46 | # So that we don't have to update old USER files, we still honor the old way. Note that 47 | # since the code for handling the new way comes after this code, the new way will override 48 | # the old way. (I.e. once a user has a default git URL for all his experiments, the old 49 | # 'git_repo_url' and 'git_branch_url' options will be ignored.) 50 | # However, if the user creates a new experiment, we don't want an old default git URL 51 | # specified by 'git_repo_url' to become the default for that experiment (it should start 52 | # with no default). Therefore, for any experiment created after the date that this 53 | # modification was made to the code, we ignore 'git_repo_url' entirely. 54 | # We use ctime (which though not strictly creation time, is close enough). Not sure 55 | # how this will behave on non-UNIX platforms which (a) cause Perl to report a ctime 56 | # but (b) may (?) have a significantly different semantics for it. 57 | my $expdir = catdir(IbexFarm->config->{deployment_dir}, $c->user->username, $experiment); 58 | # Was getting weird errors using File::Stat, for some reason. Should sort these out at some 59 | # point so that this isn't required. 60 | my @stats = stat($expdir) or die "Unable to stat experiment directory '$expdir'"; 61 | my $ctime = $stats[10]; 62 | if ($json->{git_repo_url} && ((! $ctime) || $ctime < 1280000116)) { # 1280000116 = 07/24/2010 3:35pm EST 63 | $json->{git_repo_branch} or die "git_repo_url but no git_repo_branch"; 64 | $c->stash->{git_repo_url} = $json->{git_repo_url}; 65 | $c->stash->{git_repo_branch} = $json->{git_repo_branch}; 66 | } 67 | # The new way. 68 | if ($json->{git_repos} && $json->{git_repos}{$experiment}) { 69 | ($json->{git_repos}{$experiment}{url} && $json->{git_repos}{$experiment}{branch}) or 70 | die "'url' and 'branch' keys should both be present"; 71 | $c->stash->{git_repo_url} = $json->{git_repos}{$experiment}{url}; 72 | $c->stash->{git_repo_branch} = $json->{git_repos}{$experiment}{branch}; 73 | } 74 | 75 | $c->stash->{template} = "manage.tt"; 76 | } 77 | 78 | 1; 79 | -------------------------------------------------------------------------------- /root/static/codemirror/stringstream.js: -------------------------------------------------------------------------------- 1 | /* String streams are the things fed to parsers (which can feed them 2 | * to a tokenizer if they want). They provide peek and next methods 3 | * for looking at the current character (next 'consumes' this 4 | * character, peek does not), and a get method for retrieving all the 5 | * text that was consumed since the last time get was called. 6 | * 7 | * An easy mistake to make is to let a StopIteration exception finish 8 | * the token stream while there are still characters pending in the 9 | * string stream (hitting the end of the buffer while parsing a 10 | * token). To make it easier to detect such errors, the stringstreams 11 | * throw an exception when this happens. 12 | */ 13 | 14 | // Make a stringstream stream out of an iterator that returns strings. 15 | // This is applied to the result of traverseDOM (see codemirror.js), 16 | // and the resulting stream is fed to the parser. 17 | var stringStream = function(source){ 18 | // String that's currently being iterated over. 19 | var current = ""; 20 | // Position in that string. 21 | var pos = 0; 22 | // Accumulator for strings that have been iterated over but not 23 | // get()-ed yet. 24 | var accum = ""; 25 | // Make sure there are more characters ready, or throw 26 | // StopIteration. 27 | function ensureChars() { 28 | while (pos == current.length) { 29 | accum += current; 30 | current = ""; // In case source.next() throws 31 | pos = 0; 32 | try {current = source.next();} 33 | catch (e) { 34 | if (e != StopIteration) throw e; 35 | else return false; 36 | } 37 | } 38 | return true; 39 | } 40 | 41 | return { 42 | // Return the next character in the stream. 43 | peek: function() { 44 | if (!ensureChars()) return null; 45 | return current.charAt(pos); 46 | }, 47 | // Get the next character, throw StopIteration if at end, check 48 | // for unused content. 49 | next: function() { 50 | if (!ensureChars()) { 51 | if (accum.length > 0) 52 | throw "End of stringstream reached without emptying buffer ('" + accum + "')."; 53 | else 54 | throw StopIteration; 55 | } 56 | return current.charAt(pos++); 57 | }, 58 | // Return the characters iterated over since the last call to 59 | // .get(). 60 | get: function() { 61 | var temp = accum; 62 | accum = ""; 63 | if (pos > 0){ 64 | temp += current.slice(0, pos); 65 | current = current.slice(pos); 66 | pos = 0; 67 | } 68 | return temp; 69 | }, 70 | // Push a string back into the stream. 71 | push: function(str) { 72 | current = current.slice(0, pos) + str + current.slice(pos); 73 | }, 74 | lookAhead: function(str, consume, skipSpaces, caseInsensitive) { 75 | function cased(str) {return caseInsensitive ? str.toLowerCase() : str;} 76 | str = cased(str); 77 | var found = false; 78 | 79 | var _accum = accum, _pos = pos; 80 | if (skipSpaces) this.nextWhileMatches(/[\s\u00a0]/); 81 | 82 | while (true) { 83 | var end = pos + str.length, left = current.length - pos; 84 | if (end <= current.length) { 85 | found = str == cased(current.slice(pos, end)); 86 | pos = end; 87 | break; 88 | } 89 | else if (str.slice(0, left) == cased(current.slice(pos))) { 90 | accum += current; current = ""; 91 | try {current = source.next();} 92 | catch (e) {break;} 93 | pos = 0; 94 | str = str.slice(left); 95 | } 96 | else { 97 | break; 98 | } 99 | } 100 | 101 | if (!(found && consume)) { 102 | current = accum.slice(_accum.length) + current; 103 | pos = _pos; 104 | accum = _accum; 105 | } 106 | 107 | return found; 108 | }, 109 | 110 | // Utils built on top of the above 111 | more: function() { 112 | return this.peek() !== null; 113 | }, 114 | applies: function(test) { 115 | var next = this.peek(); 116 | return (next !== null && test(next)); 117 | }, 118 | nextWhile: function(test) { 119 | var next; 120 | while ((next = this.peek()) !== null && test(next)) 121 | this.next(); 122 | }, 123 | matches: function(re) { 124 | var next = this.peek(); 125 | return (next !== null && re.test(next)); 126 | }, 127 | nextWhileMatches: function(re) { 128 | var next; 129 | while ((next = this.peek()) !== null && re.test(next)) 130 | this.next(); 131 | }, 132 | equals: function(ch) { 133 | return ch === this.peek(); 134 | }, 135 | endOfLine: function() { 136 | var next = this.peek(); 137 | return next == null || next == "\n"; 138 | } 139 | }; 140 | }; 141 | -------------------------------------------------------------------------------- /root/static/codemirror/parsecss.js: -------------------------------------------------------------------------------- 1 | /* Simple parser for CSS */ 2 | 3 | var CSSParser = Editor.Parser = (function() { 4 | var tokenizeCSS = (function() { 5 | function normal(source, setState) { 6 | var ch = source.next(); 7 | if (ch == "@") { 8 | source.nextWhileMatches(/\w/); 9 | return "css-at"; 10 | } 11 | else if (ch == "/" && source.equals("*")) { 12 | setState(inCComment); 13 | return null; 14 | } 15 | else if (ch == "<" && source.equals("!")) { 16 | setState(inSGMLComment); 17 | return null; 18 | } 19 | else if (ch == "=") { 20 | return "css-compare"; 21 | } 22 | else if (source.equals("=") && (ch == "~" || ch == "|")) { 23 | source.next(); 24 | return "css-compare"; 25 | } 26 | else if (ch == "\"" || ch == "'") { 27 | setState(inString(ch)); 28 | return null; 29 | } 30 | else if (ch == "#") { 31 | source.nextWhileMatches(/\w/); 32 | return "css-hash"; 33 | } 34 | else if (ch == "!") { 35 | source.nextWhileMatches(/[ \t]/); 36 | source.nextWhileMatches(/\w/); 37 | return "css-important"; 38 | } 39 | else if (/\d/.test(ch)) { 40 | source.nextWhileMatches(/[\w.%]/); 41 | return "css-unit"; 42 | } 43 | else if (/[,.+>*\/]/.test(ch)) { 44 | return "css-select-op"; 45 | } 46 | else if (/[;{}:\[\]]/.test(ch)) { 47 | return "css-punctuation"; 48 | } 49 | else { 50 | source.nextWhileMatches(/[\w\\\-_]/); 51 | return "css-identifier"; 52 | } 53 | } 54 | 55 | function inCComment(source, setState) { 56 | var maybeEnd = false; 57 | while (!source.endOfLine()) { 58 | var ch = source.next(); 59 | if (maybeEnd && ch == "/") { 60 | setState(normal); 61 | break; 62 | } 63 | maybeEnd = (ch == "*"); 64 | } 65 | return "css-comment"; 66 | } 67 | 68 | function inSGMLComment(source, setState) { 69 | var dashes = 0; 70 | while (!source.endOfLine()) { 71 | var ch = source.next(); 72 | if (dashes >= 2 && ch == ">") { 73 | setState(normal); 74 | break; 75 | } 76 | dashes = (ch == "-") ? dashes + 1 : 0; 77 | } 78 | return "css-comment"; 79 | } 80 | 81 | function inString(quote) { 82 | return function(source, setState) { 83 | var escaped = false; 84 | while (!source.endOfLine()) { 85 | var ch = source.next(); 86 | if (ch == quote && !escaped) 87 | break; 88 | escaped = !escaped && ch == "\\"; 89 | } 90 | if (!escaped) 91 | setState(normal); 92 | return "css-string"; 93 | }; 94 | } 95 | 96 | return function(source, startState) { 97 | return tokenizer(source, startState || normal); 98 | }; 99 | })(); 100 | 101 | function indentCSS(inBraces, inRule, base) { 102 | return function(nextChars) { 103 | if (!inBraces || /^\}/.test(nextChars)) return base; 104 | else if (inRule) return base + indentUnit * 2; 105 | else return base + indentUnit; 106 | }; 107 | } 108 | 109 | // This is a very simplistic parser -- since CSS does not really 110 | // nest, it works acceptably well, but some nicer colouroing could 111 | // be provided with a more complicated parser. 112 | function parseCSS(source, basecolumn) { 113 | basecolumn = basecolumn || 0; 114 | var tokens = tokenizeCSS(source); 115 | var inBraces = false, inRule = false, inDecl = false;; 116 | 117 | var iter = { 118 | next: function() { 119 | var token = tokens.next(), style = token.style, content = token.content; 120 | 121 | if (style == "css-hash") 122 | style = token.style = inRule ? "css-colorcode" : "css-identifier"; 123 | if (style == "css-identifier") { 124 | if (inRule) token.style = "css-value"; 125 | else if (!inBraces && !inDecl) token.style = "css-selector"; 126 | } 127 | 128 | if (content == "\n") 129 | token.indentation = indentCSS(inBraces, inRule, basecolumn); 130 | 131 | if (content == "{") 132 | inBraces = true; 133 | else if (content == "}") 134 | inBraces = inRule = inDecl = false; 135 | else if (content == ";") 136 | inRule = inDecl = false; 137 | else if (inBraces && style != "css-comment" && style != "whitespace") 138 | inRule = true; 139 | else if (!inBraces && style == "css-at") 140 | inDecl = true; 141 | 142 | return token; 143 | }, 144 | 145 | copy: function() { 146 | var _inBraces = inBraces, _inRule = inRule, _tokenState = tokens.state; 147 | return function(source) { 148 | tokens = tokenizeCSS(source, _tokenState); 149 | inBraces = _inBraces; 150 | inRule = _inRule; 151 | return iter; 152 | }; 153 | } 154 | }; 155 | return iter; 156 | } 157 | 158 | return {make: parseCSS, electricChars: "}"}; 159 | })(); 160 | -------------------------------------------------------------------------------- /lib/IbexFarm/Controller/Root.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::Controller::Root; 2 | 3 | use strict; 4 | use warnings; 5 | use parent 'Catalyst::Controller'; 6 | use File::Spec::Functions qw( catdir catfile ); 7 | use Archive::Zip; 8 | use IbexFarm::AjaxHeaders qw( ajax_headers ); 9 | use IbexFarm::Util; 10 | use IbexFarm::Controller::Ajax; 11 | 12 | # 13 | # Sets the actions in this controller to be registered with no prefix 14 | # so they function identically to actions created in MyApp.pm 15 | # 16 | __PACKAGE__->config->{namespace} = ''; 17 | 18 | # Used for detecting IE (so that we don't send special IE CSS if it's not required). 19 | sub begin :Private { 20 | my ($self, $c) = @_; 21 | if ($c->req->headers->{'user-agent'} =~ /MSIE/) { 22 | $c->stash->{IS_IE} = 1; 23 | } 24 | } 25 | 26 | my $experiment_count_cache = 0; 27 | my $experiment_count_cache_last_update = 0; 28 | my $get_experiment_count = sub { 29 | if ($experiment_count_cache && time - $experiment_count_cache_last_update < 120) { 30 | return $experiment_count_cache; 31 | } 32 | else { 33 | my $count = 0; 34 | my $DIR; 35 | opendir $DIR, catdir(IbexFarm->config->{deployment_dir}) or return $experiment_count_cache; 36 | while (defined (my $e = readdir($DIR))) { 37 | next if IbexFarm::Util::is_special_file($e); 38 | if (-d catdir(IbexFarm->config->{deployment_dir}, $e)) { 39 | my $DIR2; 40 | unless (opendir $DIR2, catdir(IbexFarm->config->{deployment_dir}, $e)) { 41 | close $DIR; 42 | return $experiment_count_cache; 43 | } 44 | while (defined (my $d = readdir($DIR2))) { ++$count if ($d !~ /^\./ && $d ne IbexFarm->config->{USER_FILE_NAME}); } 45 | closedir $DIR2; 46 | } 47 | } 48 | closedir $DIR; 49 | $experiment_count_cache = $count; 50 | $experiment_count_cache_last_update = time; 51 | return $count; 52 | } 53 | }; 54 | 55 | sub index :Path :Args(0) { 56 | my ( $self, $c ) = @_; 57 | 58 | $c->stash->{experiment_base_url} = IbexFarm->config->{experiment_base_url}; 59 | $c->stash->{experiment_count} = $get_experiment_count->(); 60 | $c->stash->{example_experiment_user} = IbexFarm->config->{example_experiment_user} || "example"; 61 | $c->stash->{example_experiment_name} = IbexFarm->config->{example_experiment_name} || "example"; 62 | $c->stash->{webmaster_email} = IbexFarm->config->{webmaster_email}; 63 | $c->stash->{front_page_html_message} = IbexFarm->config->{front_page_html_message}; 64 | $c->stash->{template} = "frontpage.tt"; 65 | } 66 | 67 | sub githelp :Path("githelp") :Args(0) { 68 | my ($self, $c) = @_; 69 | 70 | $c->stash->{timeout} = IbexFarm->config->{git_checkout_timeout_seconds}; 71 | $c->stash->{back_uri} = $c->flash->{back_uri}; 72 | $c->stash->{experiment_name} = $c->flash->{experiment_name}; 73 | $c->stash->{template} = "githelp.tt"; 74 | } 75 | 76 | sub zip_archive :Path("zip_archive") { 77 | my ($self, $c) = (shift, shift); 78 | my $experiment_name = shift or $c->detach('default'); 79 | $experiment_name =~ /^([^.]+)\.zip$/; 80 | ($experiment_name = $1) or $c->detach('default'); 81 | $c->detach('unauthorized') unless ($c->user_exists); 82 | 83 | my $edir = catdir(IbexFarm->config->{deployment_dir}, $c->user->username, $experiment_name, IbexFarm->config->{ibex_archive_root_dir}); 84 | my $zip = Archive::Zip->new(); 85 | for my $dir (@{IbexFarm->config->{dirs}}) { 86 | if (-d (my $dd = catdir($edir, $dir))) { 87 | my $zdir = $zip->addDirectory($dir); 88 | opendir my $DIR, $dd or die "Unable to open dir: $!"; 89 | while (defined (my $entry = readdir($DIR))) { 90 | next if IbexFarm::Util::is_special_file($entry); 91 | $zip->addFile(catfile($dd, $entry), "$dir/$entry"); # Archive::Zip always uses '/'. 92 | } 93 | } 94 | } 95 | 96 | # Neat Perl trick: you can apparently open a reference to a string to get a file handle. 97 | my $sbuf = ""; 98 | open my $sbuffh, "+<", \$sbuf; 99 | $zip->writeToFileHandle($sbuffh) == Archive::Zip::AZ_OK or die "Error compressing zip file: $!"; 100 | 101 | ajax_headers($c, 'application/zip', '', 200); 102 | $c->res->body($sbuf); 103 | return 0; 104 | } 105 | 106 | # Legacy jank for spellout.net/ibexfarm 107 | sub config_legacy :Path("ibexfarm/ajax/config") :Args(0) { 108 | $IbexFarm::Controller::Ajax::do_config->(@_); 109 | } 110 | 111 | sub bad_request :Path { 112 | my ($self, $c) = @_; 113 | $c->response->body('Bad request'); 114 | $c->response->status(404); 115 | } 116 | 117 | sub unauthorized :Path { 118 | my ($self, $c) = @_; 119 | $c->response->body('You do not have permission to access this page.'); 120 | $c->response->status(401); 121 | } 122 | 123 | sub default :Path { 124 | my ($self, $c) = @_; 125 | $c->response->body('Page not found'); 126 | $c->response->status(404); 127 | } 128 | 129 | sub end : ActionClass('RenderView') {} 130 | 131 | sub auto : Private { 132 | my ($self, $c) = @_; 133 | $c->req->base(URI->new(IbexFarm->config->{url_prefix})); 134 | } 135 | 136 | 1; 137 | -------------------------------------------------------------------------------- /lib/IbexFarm/DeployIbex.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::DeployIbex; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use base 'Exporter'; 7 | 8 | use IbexFarm::FNames; 9 | use File::Spec::Functions qw( splitpath splitdir catfile catdir ); 10 | use Fcntl qw( :flock ); 11 | use Archive::Tar; 12 | use Cwd; 13 | use File::Copy; 14 | use File::Path; 15 | 16 | # Will only work reliably with ASCII strings. 17 | my $to_py_escaped_string = sub { 18 | my $s = shift; 19 | my $r = '"'; 20 | for (my $i = 0; $i < length($s); ++$i) { 21 | $r .= "\\x" . sprintf("%x", ord(substr($s, $i, 1))); 22 | } 23 | $r .= '"'; 24 | return $r; 25 | }; 26 | 27 | my @WRITABLE; 28 | for my $p (@{IbexFarm->config->{writable}}) { 29 | push @WRITABLE, catfile(split /\//, $p); 30 | } 31 | 32 | # Args: deployment_dir, ibex_archive, ibex_archive_root_dir, name, hashbang, external_config_url, pass_params, www_dir, www_dir_perms. 33 | sub deploy { 34 | my %args = @_; 35 | use YAML; 36 | 37 | IbexFarm::FNames::is_ok_fname($args{name}) or die "Bad name!"; 38 | 39 | my $dd = catdir($args{deployment_dir}, $args{name}); 40 | if (! -d $dd) 41 | { mkdir $dd or die "Could not create deployment dir '$dd': $!"; } 42 | 43 | # If something subsequently goes wrong, we want to delete the deployment dir we just created. 44 | eval { 45 | 46 | my $oldcwd = getcwd(); 47 | chdir $dd or die "Could not change PWD to deployment dir: $!."; 48 | my $tar = Archive::Tar->new($args{ibex_archive}) or die "Could not open archive."; 49 | $tar->extract() || die "Could not extract archive"; 50 | 51 | # This file keeps a record of which of the files in the archive are 52 | # considered suitable for modification by the user. (Since this might 53 | # change from version to version, we keep a record on disk for each 54 | # experiment.) 55 | open my $wable, (">" . catfile($dd, $args{ibex_archive_root_dir}, 'WRITABLE')) or die "Unable to open 'WRITABLE' file: $!"; 56 | for my $wf (@WRITABLE) { print $wable "$wf\n" or die "Unable to write to 'WRITABLE' file: $!"; } 57 | close $wable or die "Unable to close 'WRITABLE' file: $!"; 58 | 59 | # This file keeps a record of files which the user has uploaded. 60 | # These are considered to be writable by the user. 61 | open my $upl, (">" . catfile($dd, $args{ibex_archive_root_dir}, 'UPLOADED')) or die "Unable to open 'UPLOADED' file: $!"; 62 | close $upl or die "Unable to close 'UPLOADED' file: $!"; 63 | 64 | # This file just contains the ibex version. 65 | open my $version, (">" . catfile($dd, $args{ibex_archive_root_dir}, 'VERSION')) or die "Unable to open 'VERSION' file: $!"; 66 | print $version IbexFarm->config->{ibex_version}, "\n"; 67 | close $version or die "Unable to close 'VERSION' file: $!"; 68 | 69 | for my $f ($tar->list_files) { 70 | my ($vol, $dir, $fname) = splitpath($f); 71 | if ($fname eq "server.py") { # Add config header to server.py. 72 | open my $sdotpyfh, "+<$f" or die "Unable to open server.py: $!"; 73 | local $/; 74 | my $contents = <$sdotpyfh> || die "Unable to read contents of server.py: $!"; 75 | flock $sdotpyfh, LOCK_EX or die "Unable to lock server.py: $!"; 76 | truncate $sdotpyfh, 0 or die "Unable to truncate server.py: $!"; 77 | seek $sdotpyfh, 0, 0 or die "Unable to seek server.py: $!"; # Probably redundant. 78 | if ($args{hashbang}) { print $sdotpyfh "#!$args{hashbang}\n"; } 79 | if ($args{external_config_url}) { 80 | print $sdotpyfh "EXTERNAL_CONFIG_URL = ", $to_py_escaped_string->($args{external_config_url}), "\n"; 81 | } 82 | if ($args{external_config_url_envvar}) { 83 | print $sdotpyfh "import os\nEXTERNAL_CONFIG_URL = os.environ.get(", $to_py_escaped_string->($args{external_config_url_envvar}), ", '')\n"; 84 | } 85 | if ($args{external_config_url} || $args{external_config_url_envvar}) { 86 | print $sdotpyfh "EXTERNAL_CONFIG_PASS_PARAMS = " . ($args{pass_params} ? "True" : "False") . "\n"; 87 | print $sdotpyfh "EXTERNAL_CONFIG_METHOD = 'GET'\n\n"; 88 | } 89 | print $sdotpyfh $contents; 90 | close $sdotpyfh or die "Unable to close server.py: $!"; 91 | 92 | chmod 0755, $f or die "Unable to chmod 0755 server.py: $!"; 93 | } 94 | 95 | if ($args{www_dir}) { 96 | my @ds = splitdir($dir); 97 | if ($ds[$#ds-1] eq "www") { # Copy the files in the www dir somewhere else if this was specified. 98 | my $ddd = catdir($args{www_dir}, $args{name}); 99 | if (! -d $ddd) { mkdir $ddd or die "Unable to create www dir '$ddd': $!"; } 100 | # Copy the file. 101 | if (-f $f) { 102 | copy $f, $ddd or die "Unable to copy file in www dir: $!"; 103 | if ($fname eq "server.py") { 104 | chmod 0755, catfile($ddd, $fname) or die "Unable to chmod 0755 server.py after copying: $!"; 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | $tar->clear; 112 | 113 | chdir $oldcwd; 114 | 115 | }; # End of eval { 116 | if ($@) { 117 | # To avoid weird errors resulting from deleting files/dirs above/at the CWD. 118 | chdir $args{deployment_dir} or die "Could not chdir to deployment dir: $!"; 119 | File::Path::remove_tree $dd or die "Unable to remove deployment dir following error."; 120 | die $@; 121 | } 122 | } 123 | 124 | our @EXPORT = qw( deploy ); 125 | 126 | 1; 127 | -------------------------------------------------------------------------------- /root/static/codemirror/parsesparql.js: -------------------------------------------------------------------------------- 1 | var SparqlParser = Editor.Parser = (function() { 2 | function wordRegexp(words) { 3 | return new RegExp("^(?:" + words.join("|") + ")$", "i"); 4 | } 5 | var ops = wordRegexp(["str", "lang", "langmatches", "datatype", "bound", "sameterm", "isiri", "isuri", 6 | "isblank", "isliteral", "union", "a"]); 7 | var keywords = wordRegexp(["base", "prefix", "select", "distinct", "reduced", "construct", "describe", 8 | "ask", "from", "named", "where", "order", "limit", "offset", "filter", "optional", 9 | "graph", "by", "asc", "desc"]); 10 | var operatorChars = /[*+\-<>=&|]/; 11 | 12 | var tokenizeSparql = (function() { 13 | function normal(source, setState) { 14 | var ch = source.next(); 15 | if (ch == "$" || ch == "?") { 16 | source.nextWhileMatches(/[\w\d]/); 17 | return "sp-var"; 18 | } 19 | else if (ch == "<" && !source.matches(/[\s\u00a0=]/)) { 20 | source.nextWhileMatches(/[^\s\u00a0>]/); 21 | if (source.equals(">")) source.next(); 22 | return "sp-uri"; 23 | } 24 | else if (ch == "\"" || ch == "'") { 25 | setState(inLiteral(ch)); 26 | return null; 27 | } 28 | else if (/[{}\(\),\.;\[\]]/.test(ch)) { 29 | return "sp-punc"; 30 | } 31 | else if (ch == "#") { 32 | while (!source.endOfLine()) source.next(); 33 | return "sp-comment"; 34 | } 35 | else if (operatorChars.test(ch)) { 36 | source.nextWhileMatches(operatorChars); 37 | return "sp-operator"; 38 | } 39 | else if (ch == ":") { 40 | source.nextWhileMatches(/[\w\d\._\-]/); 41 | return "sp-prefixed"; 42 | } 43 | else { 44 | source.nextWhileMatches(/[_\w\d]/); 45 | if (source.equals(":")) { 46 | source.next(); 47 | source.nextWhileMatches(/[\w\d_\-]/); 48 | return "sp-prefixed"; 49 | } 50 | var word = source.get(), type; 51 | if (ops.test(word)) 52 | type = "sp-operator"; 53 | else if (keywords.test(word)) 54 | type = "sp-keyword"; 55 | else 56 | type = "sp-word"; 57 | return {style: type, content: word}; 58 | } 59 | } 60 | 61 | function inLiteral(quote) { 62 | return function(source, setState) { 63 | var escaped = false; 64 | while (!source.endOfLine()) { 65 | var ch = source.next(); 66 | if (ch == quote && !escaped) { 67 | setState(normal); 68 | break; 69 | } 70 | escaped = !escaped && ch == "\\"; 71 | } 72 | return "sp-literal"; 73 | }; 74 | } 75 | 76 | return function(source, startState) { 77 | return tokenizer(source, startState || normal); 78 | }; 79 | })(); 80 | 81 | function indentSparql(context) { 82 | return function(nextChars) { 83 | var firstChar = nextChars && nextChars.charAt(0); 84 | if (/[\]\}]/.test(firstChar)) 85 | while (context && context.type == "pattern") context = context.prev; 86 | 87 | var closing = context && firstChar == matching[context.type]; 88 | if (!context) 89 | return 0; 90 | else if (context.type == "pattern") 91 | return context.col; 92 | else if (context.align) 93 | return context.col - (closing ? context.width : 0); 94 | else 95 | return context.indent + (closing ? 0 : indentUnit); 96 | } 97 | } 98 | 99 | function parseSparql(source) { 100 | var tokens = tokenizeSparql(source); 101 | var context = null, indent = 0, col = 0; 102 | function pushContext(type, width) { 103 | context = {prev: context, indent: indent, col: col, type: type, width: width}; 104 | } 105 | function popContext() { 106 | context = context.prev; 107 | } 108 | 109 | var iter = { 110 | next: function() { 111 | var token = tokens.next(), type = token.style, content = token.content, width = token.value.length; 112 | 113 | if (content == "\n") { 114 | token.indentation = indentSparql(context); 115 | indent = col = 0; 116 | if (context && context.align == null) context.align = false; 117 | } 118 | else if (type == "whitespace" && col == 0) { 119 | indent = width; 120 | } 121 | else if (type != "sp-comment" && context && context.align == null) { 122 | context.align = true; 123 | } 124 | 125 | if (content != "\n") col += width; 126 | 127 | if (/[\[\{\(]/.test(content)) { 128 | pushContext(content, width); 129 | } 130 | else if (/[\]\}\)]/.test(content)) { 131 | while (context && context.type == "pattern") 132 | popContext(); 133 | if (context && content == matching[context.type]) 134 | popContext(); 135 | } 136 | else if (content == "." && context && context.type == "pattern") { 137 | popContext(); 138 | } 139 | else if ((type == "sp-word" || type == "sp-prefixed" || type == "sp-uri" || type == "sp-var" || type == "sp-literal") && 140 | context && /[\{\[]/.test(context.type)) { 141 | pushContext("pattern", width); 142 | } 143 | 144 | return token; 145 | }, 146 | 147 | copy: function() { 148 | var _context = context, _indent = indent, _col = col, _tokenState = tokens.state; 149 | return function(source) { 150 | tokens = tokenizeSparql(source, _tokenState); 151 | context = _context; 152 | indent = _indent; 153 | col = _col; 154 | return iter; 155 | }; 156 | } 157 | }; 158 | return iter; 159 | } 160 | 161 | return {make: parseSparql, electricChars: "}]"}; 162 | })(); 163 | -------------------------------------------------------------------------------- /root/static/codemirror/tokenizejavascript.js: -------------------------------------------------------------------------------- 1 | /* Tokenizer for JavaScript code */ 2 | 3 | var tokenizeJavaScript = (function() { 4 | // Advance the stream until the given character (not preceded by a 5 | // backslash) is encountered, or the end of the line is reached. 6 | function nextUntilUnescaped(source, end) { 7 | var escaped = false; 8 | while (!source.endOfLine()) { 9 | var next = source.next(); 10 | if (next == end && !escaped) 11 | return false; 12 | escaped = !escaped && next == "\\"; 13 | } 14 | return escaped; 15 | } 16 | 17 | // A map of JavaScript's keywords. The a/b/c keyword distinction is 18 | // very rough, but it gives the parser enough information to parse 19 | // correct code correctly (we don't care that much how we parse 20 | // incorrect code). The style information included in these objects 21 | // is used by the highlighter to pick the correct CSS style for a 22 | // token. 23 | var keywords = function(){ 24 | function result(type, style){ 25 | return {type: type, style: "js-" + style}; 26 | } 27 | // keywords that take a parenthised expression, and then a 28 | // statement (if) 29 | var keywordA = result("keyword a", "keyword"); 30 | // keywords that take just a statement (else) 31 | var keywordB = result("keyword b", "keyword"); 32 | // keywords that optionally take an expression, and form a 33 | // statement (return) 34 | var keywordC = result("keyword c", "keyword"); 35 | var operator = result("operator", "keyword"); 36 | var atom = result("atom", "atom"); 37 | return { 38 | "if": keywordA, "while": keywordA, "with": keywordA, 39 | "else": keywordB, "do": keywordB, "try": keywordB, "finally": keywordB, 40 | "return": keywordC, "break": keywordC, "continue": keywordC, "new": keywordC, "delete": keywordC, "throw": keywordC, 41 | "in": operator, "typeof": operator, "instanceof": operator, 42 | "var": result("var", "keyword"), "function": result("function", "keyword"), "catch": result("catch", "keyword"), 43 | "for": result("for", "keyword"), "switch": result("switch", "keyword"), 44 | "case": result("case", "keyword"), "default": result("default", "keyword"), 45 | "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom 46 | }; 47 | }(); 48 | 49 | // Some helper regexps 50 | var isOperatorChar = /[+\-*&%=<>!?|]/; 51 | var isHexDigit = /[0-9A-Fa-f]/; 52 | var isWordChar = /[\w\$_]/; 53 | 54 | // Wrapper around jsToken that helps maintain parser state (whether 55 | // we are inside of a multi-line comment and whether the next token 56 | // could be a regular expression). 57 | function jsTokenState(inside, regexp) { 58 | return function(source, setState) { 59 | var newInside = inside; 60 | var type = jsToken(inside, regexp, source, function(c) {newInside = c;}); 61 | var newRegexp = type.type == "operator" || type.type == "keyword c" || type.type.match(/^[\[{}\(,;:]$/); 62 | if (newRegexp != regexp || newInside != inside) 63 | setState(jsTokenState(newInside, newRegexp)); 64 | return type; 65 | }; 66 | } 67 | 68 | // The token reader, intended to be used by the tokenizer from 69 | // tokenize.js (through jsTokenState). Advances the source stream 70 | // over a token, and returns an object containing the type and style 71 | // of that token. 72 | function jsToken(inside, regexp, source, setInside) { 73 | function readHexNumber(){ 74 | source.next(); // skip the 'x' 75 | source.nextWhileMatches(isHexDigit); 76 | return {type: "number", style: "js-atom"}; 77 | } 78 | 79 | function readNumber() { 80 | source.nextWhileMatches(/[0-9]/); 81 | if (source.equals(".")){ 82 | source.next(); 83 | source.nextWhileMatches(/[0-9]/); 84 | } 85 | if (source.equals("e") || source.equals("E")){ 86 | source.next(); 87 | if (source.equals("-")) 88 | source.next(); 89 | source.nextWhileMatches(/[0-9]/); 90 | } 91 | return {type: "number", style: "js-atom"}; 92 | } 93 | // Read a word, look it up in keywords. If not found, it is a 94 | // variable, otherwise it is a keyword of the type found. 95 | function readWord() { 96 | source.nextWhileMatches(isWordChar); 97 | var word = source.get(); 98 | var known = keywords.hasOwnProperty(word) && keywords.propertyIsEnumerable(word) && keywords[word]; 99 | return known ? {type: known.type, style: known.style, content: word} : 100 | {type: "variable", style: "js-variable", content: word}; 101 | } 102 | function readRegexp() { 103 | nextUntilUnescaped(source, "/"); 104 | source.nextWhileMatches(/[gi]/); 105 | return {type: "regexp", style: "js-string"}; 106 | } 107 | // Mutli-line comments are tricky. We want to return the newlines 108 | // embedded in them as regular newline tokens, and then continue 109 | // returning a comment token for every line of the comment. So 110 | // some state has to be saved (inside) to indicate whether we are 111 | // inside a /* */ sequence. 112 | function readMultilineComment(start){ 113 | var newInside = "/*"; 114 | var maybeEnd = (start == "*"); 115 | while (true) { 116 | if (source.endOfLine()) 117 | break; 118 | var next = source.next(); 119 | if (next == "/" && maybeEnd){ 120 | newInside = null; 121 | break; 122 | } 123 | maybeEnd = (next == "*"); 124 | } 125 | setInside(newInside); 126 | return {type: "comment", style: "js-comment"}; 127 | } 128 | function readOperator() { 129 | source.nextWhileMatches(isOperatorChar); 130 | return {type: "operator", style: "js-operator"}; 131 | } 132 | function readString(quote) { 133 | var endBackSlash = nextUntilUnescaped(source, quote); 134 | setInside(endBackSlash ? quote : null); 135 | return {type: "string", style: "js-string"}; 136 | } 137 | 138 | // Fetch the next token. Dispatches on first character in the 139 | // stream, or first two characters when the first is a slash. 140 | if (inside == "\"" || inside == "'") 141 | return readString(inside); 142 | var ch = source.next(); 143 | if (inside == "/*") 144 | return readMultilineComment(ch); 145 | else if (ch == "\"" || ch == "'") 146 | return readString(ch); 147 | // with punctuation, the type of the token is the symbol itself 148 | else if (/[\[\]{}\(\),;\:\.]/.test(ch)) 149 | return {type: ch, style: "js-punctuation"}; 150 | else if (ch == "0" && (source.equals("x") || source.equals("X"))) 151 | return readHexNumber(); 152 | else if (/[0-9]/.test(ch)) 153 | return readNumber(); 154 | else if (ch == "/"){ 155 | if (source.equals("*")) 156 | { source.next(); return readMultilineComment(ch); } 157 | else if (source.equals("/")) 158 | { nextUntilUnescaped(source, null); return {type: "comment", style: "js-comment"};} 159 | else if (regexp) 160 | return readRegexp(); 161 | else 162 | return readOperator(); 163 | } 164 | else if (isOperatorChar.test(ch)) 165 | return readOperator(); 166 | else 167 | return readWord(); 168 | } 169 | 170 | // The external interface to the tokenizer. 171 | return function(source, startState) { 172 | return tokenizer(source, startState || jsTokenState(false, true)); 173 | }; 174 | })(); 175 | -------------------------------------------------------------------------------- /root/uicommon.js: -------------------------------------------------------------------------------- 1 | function caseInsensitiveSortingFunction (x,y) { 2 | var a = String(x).toUpperCase(); 3 | var b = String(y).toUpperCase(); 4 | if (a > b) 5 | return 1 6 | if (a < b) 7 | return -1 8 | return 0; 9 | } 10 | 11 | // Add "before_show", "after_show", "before_hide", "after_hide", 12 | // "before_toggle", "after_toggle", "before_toggle_or_show" and "after_toggle_or_show" 13 | // events to jQuery. What a mess! 14 | (function () { 15 | function add2(f) { // Note that this is specifically designed for 'show', 'hide' and 'toggle'. 16 | var original = $.prototype[f]; 17 | $.prototype[f] = function (a, b) { 18 | // If we use 'trigger' instead of 'triggerHandler', we get nasty infinite 19 | // loops when things within things are hidden, which (I think) are due to 20 | // event bubbling. 21 | 22 | var cache = $(this); 23 | 24 | // We raise an event only if there is no registered handler for "before_toggle_or_show"; 25 | var cde = cache.data('events'); 26 | if ((f != "show" && f != "toggle") || ! (cde && cde.before_toggle_or_show)) 27 | cache.triggerHandler("before_" + f); 28 | else cache.triggerHandler("before_toggle_or_show"); 29 | var t = this; 30 | if (a) { 31 | return original.call(this, a, function () { 32 | var r; 33 | if (b) 34 | r = b(); 35 | if ((f != "show" && f != "toggle") || ! (cde && cde.after_toggle_or_show)) 36 | $(t).triggerHandler("after_" + f); 37 | else cache.triggerHandler("after_toggle_or_show"); 38 | return r; 39 | }); 40 | } 41 | else { 42 | var r = original.call(this, a); 43 | if ((f != "show" && f != "toggle") || ! (cde && cde.after_toggle_or_show)) 44 | $(t).triggerHandler("after_" + f); 45 | else cache.triggerHandler("after_toggle_or_show"); 46 | return r; 47 | } 48 | }; 49 | }; 50 | add2("show"); 51 | add2("hide"); 52 | add2("toggle"); 53 | })(); 54 | 55 | // Code for doing ajax spinner thingy. 56 | // By default, this waits 500ms before adding a spinner (distracting to have them 57 | // flash for <500ms). 58 | function spinnifyAjax(spincontainer, ajaxArgs, dontWait, manip) { 59 | var origError = ajaxArgs.error; 60 | var origSuccess = ajaxArgs.success; 61 | 62 | var spinner = $("
") 63 | .css('width', 16).css('height', 16) 64 | .css('background-image', "url('" + BASE_URI + 'static/images/ajax-loader.gif' + "')"); 65 | if (manip) 66 | manip(spinner); 67 | var timeoutId; 68 | if (! dontWait) timeoutId = setTimeout(function () { spincontainer.append(spinner); }, 500); 69 | else spincontainer.append(spinner); 70 | ajaxArgs.error = function () { 71 | if (timeoutId) clearTimeout(timeoutId); 72 | spinner.remove(); 73 | if (origError) return origError(); 74 | }; 75 | ajaxArgs.success = function (data) { 76 | if (timeoutId) clearTimeout(timeoutId); 77 | spinner.remove(); 78 | if(origSuccess) { return origSuccess(data); } 79 | }; 80 | return $.ajax(ajaxArgs); 81 | } 82 | function spinnifyPOST(spincontainer, url, args, callback, type, dontWait, manip) { 83 | return spinnifyAjax(spincontainer, { 84 | url: url, 85 | data: args, 86 | contentType: "application/x-www-form-urlencoded; charset=UTF-8", 87 | type: "POST", 88 | success: callback, 89 | dataType: type, 90 | }, dontWait, manip); 91 | } 92 | function spinnifyGET(spincontainer, url, callback, type, dontWait, manip) { 93 | return spinnifyAjax(spincontainer, { 94 | url: url, 95 | type: "GET", 96 | success: callback, 97 | dataType: type || "json", 98 | }, dontWait, manip); 99 | } 100 | 101 | (function (isChrome) { 102 | $.widget("ui.flash", { 103 | _init: function () { 104 | this.element.attr('id', 'highlighted'); 105 | var t = this; 106 | var color = '#BD2031'; 107 | if (this.options.type == 'message') { color = 'yellow'; } 108 | // WEIRD! 109 | // 'highlight' effect appears not to work in Chrome (beta, OS X) (jQuery bug?) 110 | if (! isChrome) { 111 | this.element.effect("highlight", {color: color}, 2500, function () { 112 | t.element.attr('id', null); 113 | if (t.options.finishedCallback) 114 | t.options.finishedCallback.call(t.element); 115 | }); 116 | } 117 | else { 118 | var old = this.element.css('background-color'); 119 | this.element.css('background-color', color); 120 | setTimeout(function () { 121 | t.element.attr('id', null); t.element.css('background-color', old); 122 | if (t.options.finishedCallback) 123 | t.options.finishedCallback.call(t.element); 124 | }, 2500); 125 | } 126 | } 127 | }); 128 | })(navigator.userAgent && navigator.userAgent.search(/Chrome/) != -1); 129 | 130 | $.widget("ui.areYouSure", { 131 | _init: function () { 132 | this.element.addClass("areYouSure"); 133 | 134 | var cancel; 135 | var chk; 136 | var del; 137 | this.element 138 | .append($("
").addClass("box") 139 | .append(this.options.question) 140 | .append($("

") 141 | .append(cancel = $("").addClass("linklike").addClass("cancel").text(" cancel")) 142 | .append(" / ") 143 | .append(chk = $("")) 144 | .append(del = $("").attr('value', this.options.actionText)))); 145 | 146 | var t = this; 147 | 148 | cancel.click(function () { 149 | t.options.cancelCallback(); 150 | }); 151 | 152 | del.click(function () { 153 | if (! chk.attr('checked')) { 154 | alert(t.options.uncheckedMessage); 155 | return; 156 | } 157 | 158 | t.options.actionCallback(); 159 | }); 160 | } 161 | }); 162 | 163 | $.widget("ui.rename", { 164 | _init: function () { 165 | var rename_inp; 166 | var rename_cancel; 167 | var rename_btn; 168 | var rename_error; 169 | this.element 170 | .addClass("rename") 171 | .append($("

").addClass("box") 172 | .append(rename_inp = $("") 173 | .attr('size', 20) 174 | .attr('type', 'text') 175 | .attr('value', this.options.name)) 176 | .append(rename_cancel = $("").addClass("linklike").text("cancel")) 177 | .append(" / ") 178 | .append(rename_btn = $("").attr("value", "rename")) 179 | .append(rename_error = $("
") 180 | .hide() 181 | .addClass("error"))) 182 | .hide(); 183 | 184 | var t = this; 185 | rename_btn.click(function () { t.options.actionCallback(rename_inp.attr('value')); }); 186 | rename_inp.keypress(function (e) { 187 | if (e.which == 13) // Return 188 | t.options.actionCallback(rename_inp.attr('value')); 189 | }); 190 | 191 | rename_cancel.click(this.options.cancelCallback); 192 | 193 | this.rename_error = rename_error; 194 | 195 | // Highlight the input field text when it's shown. 196 | rename_inp.get(0).select(); 197 | $(this.element).bind("after_toggle_or_show", null, function () { rename_inp.get(0).select(); }); 198 | // Hide the error message when the whole thing is hidden. 199 | $(this.element).bind("before_hide", null, function () { rename_error.hide(); }); 200 | }, 201 | 202 | showError: function (error) { 203 | this.rename_error.html(error); 204 | this.rename_error.show(); 205 | }, 206 | hideError: function (error) { 207 | this.rename_error.hide(STD_TOGGLE_SPEED); 208 | } 209 | }); 210 | -------------------------------------------------------------------------------- /root/experiments.js: -------------------------------------------------------------------------------- 1 | $.widget("ui.addExperimentDialog", { 2 | _init: function () { 3 | this.element.addClass("add_experiment"); 4 | 5 | var input; 6 | var action; 7 | this.element 8 | .append($("
").addClass("box") 9 | .append($("") 10 | .append($("") 11 | .append($("
").text("Name:")) 12 | .append($("") 13 | .append(input = $("") 14 | .attr('type', 'text') 15 | .attr('name', 'name') 16 | .attr('size', 20))) 17 | .append($("") 18 | .append(action = $("")))))); 19 | 20 | // Make sure the text box gets focus when it's shown. 21 | $(this.element).bind("after_toggle_or_show", null, function () { input.get(0).focus(); }); 22 | 23 | var t = this; 24 | function create () { 25 | if (input.attr('value').match(/^\s*$/)) 26 | return; 27 | 28 | spinnifyPOST(t.element, BASE_URI + 'ajax/newexperiment', { name: input.attr('value') }, function (data) { 29 | if (data.error) { 30 | t.element.find("p.error").remove(); 31 | t.element.append($("
").append($("

") 32 | .addClass("error") 33 | .html(data.error) 34 | .append(" (") 35 | .append($("").addClass("ok").text("OK").click(function () { t.element.find("p.error").hide(STD_TOGGLE_SPEED); })) 36 | .append(")"))); 37 | } 38 | else { 39 | t.element.hide(STD_TOGGLE_SPEED, function () { 40 | t.element.remove(); 41 | if (t.options.createdCallback) 42 | t.options.createdCallback(input.attr('value')); 43 | }); 44 | } 45 | }, "json"); 46 | } 47 | 48 | action.click(create); 49 | input.keypress(function (e) { 50 | if (e.which == 13) // Return 51 | create(); 52 | }); 53 | } 54 | }); 55 | 56 | $.widget("ui.showExperiment", { 57 | _init: function () { 58 | this.element.addClass("experiment"); 59 | 60 | var version = this.options.experiment[1].replace('/^\s+//', '').replace(/\s+$/, ''); 61 | 62 | var delete_; 63 | var rename; 64 | var rename_opts; 65 | var lnk; 66 | var t = this; 67 | var lock = false; 68 | var ays; 69 | this.element 70 | .append(lnk = $("").attr('href', BASE_URI + 'manage/' + escape(this.options.experiment[0])) 71 | .text(this.options.experiment[0])) 72 | .append(" (ibex ").append(version).append(") ") 73 | .append(" (").append(delete_ = $("").addClass("linklike").text("delete")) 74 | .append(" | ").append(rename = $("").addClass("linklike").text("rename")).append(")") 75 | .append(rename_opts = $("

") 76 | .rename({ 77 | name: t.options.experiment[0], 78 | actionCallback: function (newname) { 79 | if (newname.match(/^\s*$/) || newname == t.options.experiment[0]) 80 | return; 81 | 82 | spinnifyPOST(rename_opts, BASE_URI + 'ajax/rename_experiment/' + escape(t.options.experiment[0]), { newname: newname }, function (data) { 83 | if (data.error) { 84 | rename_opts.rename("showError", data.error); 85 | } 86 | else { 87 | rename_opts.rename("hideError"); 88 | if (t.options.renamedCallback) 89 | t.options.renamedCallback(newname); 90 | } 91 | }, "json"); 92 | }, 93 | cancelCallback: function () { 94 | rename_opts.hide(STD_TOGGLE_SPEED, function () { 95 | lock = false; 96 | }); 97 | } 98 | })) 99 | .append(ays = $("
").areYouSure({ 100 | question: "Are you sure you want to delete this experiment?", 101 | actionText: "delete", 102 | uncheckedMessage: "Check the box before clicking to confirm that you want to delete the experiment.", 103 | cancelCallback: function () { 104 | ays.hide(STD_TOGGLE_SPEED, function () { lock = false; }); 105 | }, 106 | actionCallback: function () { 107 | spinnifyPOST(ays, BASE_URI + 'ajax/delete_experiment/' + escape(t.options.experiment[0]), { }, function (data) { 108 | // Note that deleting an experiment cannot fail (barring some internal error in the server). 109 | t.element.hide("slow", function () { 110 | t.element.remove(); 111 | if (t.options.removedCallback) 112 | t.options.removedCallback(t.options.experiment[0]); 113 | }); 114 | }, "json"); 115 | } 116 | }).hide()); 117 | 118 | if (this.options.highlight) { 119 | lnk.flash(); 120 | window.location = "#highlighted"; 121 | } 122 | 123 | delete_.click(function () { 124 | if (lock && lock != "delete") 125 | return; 126 | 127 | // Show the "are you sure?" thing. 128 | ays.toggle(STD_TOGGLE_SPEED, function () { 129 | lock = lock ? false : "delete"; 130 | }); 131 | }); 132 | rename.click(function () { 133 | if (lock && lock != "rename") 134 | return; 135 | 136 | rename_opts.toggle(STD_TOGGLE_SPEED, function () { 137 | lock = lock ? false : "rename"; 138 | }); 139 | return true; 140 | }); 141 | } 142 | }); 143 | 144 | $.widget("ui.experimentList", { 145 | _init: function () { 146 | var t = this; 147 | 148 | function refresh (name) { 149 | t.options.highlight = name; 150 | t.element.empty(); 151 | t._init(); 152 | } 153 | 154 | spinnifyGET(this.element, this.options.url, function (data) { 155 | var experiments = data.experiments.sort(function (e1, e2) { return e1[0] < e2[0] ? -1 : (e1[0] == e2[0] ? 0 : 1) }); 156 | 157 | if (experiments.length == 0) { 158 | t.element.addClass("no_experiments"); 159 | t.element.append("You do not currently have any experiments set up.") 160 | } 161 | else { 162 | t.element.addClass("experiment_list"); 163 | var ul; 164 | t.element.append(ul = $("
    ")); 165 | 166 | for (var i = 0; i < experiments.length; ++i) { 167 | ul.append($("
  • ") 168 | .showExperiment({ experiment: experiments[i], 169 | removedCallback: refresh, 170 | renamedCallback: refresh, 171 | highlight: experiments[i][0] == t.options.highlight })); 172 | } 173 | } 174 | 175 | var cexp; 176 | var opts; 177 | t.element.append($("

    ") 178 | .append(cexp = $("") 179 | .addClass("linklike") 180 | .addClass("create_experiment") 181 | .html("» Create a new experiment")) 182 | .append(opts = $("

    ") 183 | .addExperimentDialog({ createdCallback: refresh }) 184 | .hide())); 185 | cexp.click(function () { 186 | opts.toggle(STD_TOGGLE_SPEED); 187 | }); 188 | }); 189 | } 190 | }); 191 | 192 | $(document).ready(function () { 193 | $("#experiments").experimentList({ url: BASE_URI + 'ajax/experiments' }); 194 | }); 195 | -------------------------------------------------------------------------------- /root/main.css.tt: -------------------------------------------------------------------------------- 1 | [% rounding = "8px" %] 2 | [% outer_background = "#231F20" %] 3 | [% inner_background = "#FFFFFF" %] 4 | [% heading_color = "#BD2031" %] 5 | [% text_color = outer_background %] 6 | [% link_color = "#006295" %] 7 | [% nice_color = "#C5EFFD" %] 8 | [% stdsize = "12pt" %] 9 | [% stdindent = "2em" %] 10 | [% stdindent_2 = "1em" %] 11 | [% stdsmall = "10pt" %] 12 | [% stdvsmall = "8pt" %] 13 | [% stdvvsmall = "6pt" %] 14 | [% light = "#E6E6E6" %] 15 | [% lightish = "#4c4346" %] 16 | [% lilgap = "25px" %] 17 | [% bgap = "30px" %] 18 | 19 | [% round = "border-radius: $rounding; -moz-border-radius: $rounding; -webkit-border-radius: $rounding;" %] 20 | 21 | table { 22 | border-spacing: 0; 23 | } 24 | .dir table td { 25 | padding-top: 0.2em; 26 | } 27 | 28 | body { 29 | font-family: Verdana, Helvetica, Arial, sans-serif; 30 | margin: 0; 31 | padding: 0; 32 | background-color: [% outer_background %]; 33 | } 34 | div#outer1 { 35 | position: absolute; 36 | left: 0; 37 | top: [% lilgap %]; 38 | width: 100%; 39 | background-color: [% outer_background %]; 40 | } 41 | div#outer2 { 42 | [% round %] 43 | width: 60%; 44 | margin-left: auto; 45 | margin-right: auto; 46 | margin-bottom: [% lilgap %]; 47 | padding-top: [% bgap %]; 48 | padding-left: 2em; 49 | padding-right: 2em; 50 | padding-bottom: [% bgap %]; 51 | background-color: [% inner_background %]; 52 | color: [% text_color %]; 53 | font-size: [% stdsize %]; 54 | } 55 | 56 | a#ibex { 57 | display: block; 58 | width: 250px; 59 | height: 193px; 60 | background-color: [% inner_background %]; 61 | background-image: url('[% BASE_URI _ 'static/images/ibex.jpg'%]'); 62 | background-repeat: no-repeat; 63 | } 64 | 65 | div#login_info { 66 | font-size: [% stdsmall %]; 67 | text-align: center; 68 | margin-bottom: 2em; 69 | } 70 | 71 | h1 { 72 | text-align: center; 73 | padding-top: 0; 74 | margin-top: 0; 75 | font-size: 20pt; 76 | color: [% heading_color %]; 77 | font-weight: bold; 78 | text-decoration: none; 79 | } 80 | h2 { 81 | margin-top: 1em; 82 | font-size: 16pt; 83 | color: [% text_color %]; 84 | font-weight: bold; 85 | text-decoration: none; 86 | } 87 | h2.sep { 88 | margin-top: 2em; 89 | } 90 | h3 { 91 | font-size: [% stdsize %]; 92 | font-weight: bold; 93 | text-decoration: none; 94 | color: [% text_color %]; 95 | } 96 | 97 | td { 98 | padding-left: 0; 99 | margin-left: 0; 100 | } 101 | th { 102 | text-align: left; 103 | padding-left: 0; 104 | padding-right: 0.5em; 105 | margin-left: 0; 106 | font-weight: bold; 107 | text-decoration: none; 108 | color: [% text_color %]; 109 | } 110 | form th { 111 | font-size: [% stdsize %]; 112 | text-align: right; 113 | } 114 | input { 115 | font-size: [% stdsize %]; 116 | margin-right: 0.5em; 117 | padding-left: 0; 118 | margin-left: 0; 119 | } 120 | input[type=submit] { 121 | padding-left: 0.5em; 122 | padding-right: 0.5em; 123 | } 124 | 125 | .message, .message_, .box, .error { 126 | display: table; 127 | padding: 0.5em; 128 | margin-top: 0.5em; 129 | margin-bottom: 0.5em; 130 | clear: both; 131 | [% round %] 132 | } 133 | .message, .message_ { 134 | color: [% text_color %]; 135 | background-color: [% nice_color %]; 136 | } 137 | .box { 138 | color: [% text_color %]; 139 | background-color: [% light %]; 140 | } 141 | .error { 142 | color: [% inner_background %]; 143 | background-color: [% heading_color %]; 144 | } 145 | .error a { 146 | color: [% inner_background %]; 147 | font-weight: bold; 148 | } 149 | 150 | .experiment_list li { 151 | padding-bottom: 0.5em; 152 | } 153 | 154 | /* For 'this experiment is up at */ 155 | div.message_ div { margin-top: 0.5em; text-align: center; } 156 | 157 | .areYouSure p { 158 | margin-top: 0.5em; 159 | padding-top: 0; 160 | margin-bottom: 0; 161 | padding-bottom: 0; 162 | } 163 | 164 | a { 165 | color: [% link_color %]; 166 | text-decoration: none; 167 | font-weight: bold; 168 | } 169 | .message_ a { color: [% text_color %]; } 170 | .message a { color: [% text_color %]; } 171 | a:visited { 172 | color: [% link_color %]; 173 | text-decoration: none; 174 | } 175 | a:hover { 176 | text-decoration: underline; 177 | } 178 | .linklike { 179 | color: [% link_color %]; 180 | text-decoration: none; 181 | font-weight: bold; 182 | cursor: pointer; 183 | } 184 | .linklike:hover { 185 | text-decoration: underline; 186 | } 187 | 188 | .add_experiment { 189 | margin-top: 0.5em; 190 | position: relative; 191 | left: [% stdindent %]; 192 | } 193 | li { margin-left: [% stdindent_2 %]; padding-left: 0; } 194 | .add_experiment_submit { 195 | font-size: [% stdsmall %]; 196 | } 197 | 198 | .writable { 199 | color: [% text_color %]; 200 | } 201 | .writable:visited { 202 | color: [% text_color %]; 203 | } 204 | .unwritable { 205 | color: [% lightish %]; 206 | } 207 | .unwritable:visited { 208 | color: [% lightish %]; 209 | } 210 | .writable a { font-weight: normal; } 211 | .unwritable a { font-weight: normal; } 212 | .dir .upload { 213 | font-weight: normal; 214 | text-decoration: none; 215 | } 216 | 217 | #login_info p { 218 | margin-top: 0.5em; 219 | padding-top: 0; 220 | } 221 | 222 | .field_comment { 223 | color: [% lightish %]; 224 | font-size: [% stdsmall %]; 225 | } 226 | form.create_account td.submit { 227 | padding-top: 0.5em; 228 | } 229 | form.create_account td.submit input { 230 | float: right; 231 | } 232 | 233 | form.update_email td.submit input { 234 | float: right; 235 | } 236 | form.update_password td.submit input { 237 | float: right; 238 | } 239 | 240 | form.login td.submit { 241 | padding-top: 0.5em; 242 | } 243 | form.login td.submit input { 244 | float: right; 245 | } 246 | 247 | /* For AJAX file upload weirdness. */ 248 | .hoverClass { 249 | text-decoration: underline; 250 | cursor: pointer; 251 | } 252 | 253 | p.smallprint { 254 | font-size: [% stdsmall %]; 255 | color: [% lightish %]; 256 | } 257 | p.smallprint a { 258 | color: [% lightish %]; 259 | } 260 | 261 | .not_present { 262 | font-size: [% stdsize %]; 263 | color: [% lightish %]; 264 | font-style: italic; 265 | } 266 | 267 | /*.browseDir th { color: [% heading_color %]; }*/ 268 | .browseDir { margin-bottom: 1em; } 269 | .browseDir a { font-size: [% stdsmall %]; } 270 | .browseDir .linklike { font-size: [% stdsmall %]; } 271 | 272 | #authinfo p { 273 | margin-top: 0; 274 | margin-bottom: 0; 275 | padding-top: 0; 276 | padding-bottom: 0; 277 | } 278 | 279 | .cwrap1 { 280 | clear: both; 281 | width: 100%; 282 | } 283 | .cwrap2 { 284 | float: left; 285 | position: relative; 286 | left: 50%; 287 | } 288 | .cwrap3 { 289 | position: relative; 290 | left: -50%; 291 | } 292 | 293 | h2.expheader { 294 | clear: both; 295 | } 296 | 297 | p.pwddisclaimer { 298 | margin-top: 0; 299 | padding-top: 0; 300 | margin-bottom: 1em; 301 | padding-bottom: 0; 302 | } 303 | 304 | p.nasty { 305 | margin-top: 0; 306 | padding-top: 0; 307 | } 308 | 309 | form.delete_account { 310 | padding-top: 1em; 311 | margin-left: auto; 312 | margin-right: auto; 313 | display: table; 314 | } 315 | 316 | #authinfo div { text-align: left; } 317 | 318 | p.goto { 319 | margin-top: 0; 320 | padding-top: 1em; 321 | margin-bottom: 0; 322 | padding-bottom: 0; 323 | clear: both; 324 | font-style: italic; 325 | } 326 | 327 | .ok { font-weight: bold; } 328 | 329 | .oklink { 330 | cursor: pointer; 331 | text-decoration: none; 332 | } 333 | .oklink:hover { 334 | text-decoration: underline; 335 | } 336 | 337 | .viewexamplebox { 338 | margin-top: 0; 339 | margin-bottom: 2em; 340 | } 341 | 342 | .fpfooter { 343 | clear: both; 344 | } 345 | 346 | p.pwremover { 347 | padding-top: 1em; 348 | } 349 | 350 | div#files { 351 | padding-top: 1em; 352 | } 353 | 354 | div#git > div { 355 | display: none; 356 | } 357 | div#git > span { 358 | cursor: pointer; 359 | font-size: [% stdsmall %]; 360 | font-weight: bold; 361 | } 362 | div#git td { 363 | font-size: [% stdsmall %]; 364 | } 365 | div#git input[type=text] { 366 | font-size: [% stdsmall %]; 367 | } 368 | td#gitspin { 369 | text-align: left; 370 | } 371 | 372 | .new { 373 | font-size: [% stdvvsmall %]; 374 | font-weight: bold; 375 | color: [% heading_color %]; 376 | position: relative; 377 | top: 0.25em; 378 | } 379 | #git .help { 380 | font-size: [% stdvsmall %]; 381 | } 382 | 383 | #git td.sync { 384 | vertical-align: top; 385 | } 386 | 387 | hr.fsep { 388 | border-bottom: 1px solid [% light %]; 389 | border-top: 0; 390 | width: 33%; 391 | margin-bottom: 1em; 392 | margin-left: 0; 393 | margin-right: auto; 394 | } 395 | 396 | ul.githelp li { 397 | padding-bottom: 1em; 398 | } 399 | 400 | #gitstatus { 401 | font-size: [% stdsmall %]; 402 | font-weight: bold; 403 | color: [% text_color %]; 404 | margin-right: 1em; 405 | } 406 | 407 | #gitstatus .giterror { 408 | color: [% heading_color %]; 409 | } 410 | 411 | /* For CodeMirror. Should probably have this in a separate CSS file really, 412 | but it's convenient to be able to use some of the vars defined above. 413 | Cross-check font sizes with css files in root/static/codemirror. */ 414 | .CodeMirror-line-numbers { 415 | margin-top: .4em; 416 | font-size: 10pt; 417 | font-family: monospace; 418 | color: [% lightish %]; 419 | border-right: 1px solid [% light %]; 420 | padding-right: 0.25em; 421 | margin-right: 0.5em; 422 | } 423 | -------------------------------------------------------------------------------- /root/static/codemirror/parsexml.js: -------------------------------------------------------------------------------- 1 | /* This file defines an XML parser, with a few kludges to make it 2 | * useable for HTML. autoSelfClosers defines a set of tag names that 3 | * are expected to not have a closing tag, and doNotIndent specifies 4 | * the tags inside of which no indentation should happen (see Config 5 | * object). These can be disabled by passing the editor an object like 6 | * {useHTMLKludges: false} as parserConfig option. 7 | */ 8 | 9 | var XMLParser = Editor.Parser = (function() { 10 | var Kludges = { 11 | autoSelfClosers: {"br": true, "img": true, "hr": true, "link": true, "input": true, 12 | "meta": true, "col": true, "frame": true, "base": true, "area": true}, 13 | doNotIndent: {"pre": true, "!cdata": true} 14 | }; 15 | var NoKludges = {autoSelfClosers: {}, doNotIndent: {"!cdata": true}}; 16 | var UseKludges = Kludges; 17 | var alignCDATA = false; 18 | 19 | // Simple stateful tokenizer for XML documents. Returns a 20 | // MochiKit-style iterator, with a state property that contains a 21 | // function encapsulating the current state. See tokenize.js. 22 | var tokenizeXML = (function() { 23 | function inText(source, setState) { 24 | var ch = source.next(); 25 | if (ch == "<") { 26 | if (source.equals("!")) { 27 | source.next(); 28 | if (source.equals("[")) { 29 | if (source.lookAhead("[CDATA[", true)) { 30 | setState(inBlock("xml-cdata", "]]>")); 31 | return null; 32 | } 33 | else { 34 | return "xml-text"; 35 | } 36 | } 37 | else if (source.lookAhead("--", true)) { 38 | setState(inBlock("xml-comment", "-->")); 39 | return null; 40 | } 41 | else { 42 | return "xml-text"; 43 | } 44 | } 45 | else if (source.equals("?")) { 46 | source.next(); 47 | source.nextWhileMatches(/[\w\._\-]/); 48 | setState(inBlock("xml-processing", "?>")); 49 | return "xml-processing"; 50 | } 51 | else { 52 | if (source.equals("/")) source.next(); 53 | setState(inTag); 54 | return "xml-punctuation"; 55 | } 56 | } 57 | else if (ch == "&") { 58 | while (!source.endOfLine()) { 59 | if (source.next() == ";") 60 | break; 61 | } 62 | return "xml-entity"; 63 | } 64 | else { 65 | source.nextWhileMatches(/[^&<\n]/); 66 | return "xml-text"; 67 | } 68 | } 69 | 70 | function inTag(source, setState) { 71 | var ch = source.next(); 72 | if (ch == ">") { 73 | setState(inText); 74 | return "xml-punctuation"; 75 | } 76 | else if (/[?\/]/.test(ch) && source.equals(">")) { 77 | source.next(); 78 | setState(inText); 79 | return "xml-punctuation"; 80 | } 81 | else if (ch == "=") { 82 | return "xml-punctuation"; 83 | } 84 | else if (/[\'\"]/.test(ch)) { 85 | setState(inAttribute(ch)); 86 | return null; 87 | } 88 | else { 89 | source.nextWhileMatches(/[^\s\u00a0=<>\"\'\/?]/); 90 | return "xml-name"; 91 | } 92 | } 93 | 94 | function inAttribute(quote) { 95 | return function(source, setState) { 96 | while (!source.endOfLine()) { 97 | if (source.next() == quote) { 98 | setState(inTag); 99 | break; 100 | } 101 | } 102 | return "xml-attribute"; 103 | }; 104 | } 105 | 106 | function inBlock(style, terminator) { 107 | return function(source, setState) { 108 | while (!source.endOfLine()) { 109 | if (source.lookAhead(terminator, true)) { 110 | setState(inText); 111 | break; 112 | } 113 | source.next(); 114 | } 115 | return style; 116 | }; 117 | } 118 | 119 | return function(source, startState) { 120 | return tokenizer(source, startState || inText); 121 | }; 122 | })(); 123 | 124 | // The parser. The structure of this function largely follows that of 125 | // parseJavaScript in parsejavascript.js (there is actually a bit more 126 | // shared code than I'd like), but it is quite a bit simpler. 127 | function parseXML(source) { 128 | var tokens = tokenizeXML(source), token; 129 | var cc = [base]; 130 | var tokenNr = 0, indented = 0; 131 | var currentTag = null, context = null; 132 | var consume; 133 | 134 | function push(fs) { 135 | for (var i = fs.length - 1; i >= 0; i--) 136 | cc.push(fs[i]); 137 | } 138 | function cont() { 139 | push(arguments); 140 | consume = true; 141 | } 142 | function pass() { 143 | push(arguments); 144 | consume = false; 145 | } 146 | 147 | function markErr() { 148 | token.style += " xml-error"; 149 | } 150 | function expect(text) { 151 | return function(style, content) { 152 | if (content == text) cont(); 153 | else {markErr(); cont(arguments.callee);} 154 | }; 155 | } 156 | 157 | function pushContext(tagname, startOfLine) { 158 | var noIndent = UseKludges.doNotIndent.hasOwnProperty(tagname) || (context && context.noIndent); 159 | context = {prev: context, name: tagname, indent: indented, startOfLine: startOfLine, noIndent: noIndent}; 160 | } 161 | function popContext() { 162 | context = context.prev; 163 | } 164 | function computeIndentation(baseContext) { 165 | return function(nextChars, current) { 166 | var context = baseContext; 167 | if (context && context.noIndent) 168 | return current; 169 | if (alignCDATA && /")); 189 | else if (style == "xml-cdata") { 190 | if (!context || context.name != "!cdata") pushContext("!cdata"); 191 | if (/\]\]>$/.test(content)) popContext(); 192 | cont(); 193 | } 194 | else if (harmlessTokens.hasOwnProperty(style)) cont(); 195 | else {markErr(); cont();} 196 | } 197 | function tagname(style, content) { 198 | if (style == "xml-name") { 199 | currentTag = content.toLowerCase(); 200 | token.style = "xml-tagname"; 201 | cont(); 202 | } 203 | else { 204 | currentTag = null; 205 | pass(); 206 | } 207 | } 208 | function closetagname(style, content) { 209 | if (style == "xml-name") { 210 | token.style = "xml-tagname"; 211 | if (context && content.toLowerCase() == context.name) popContext(); 212 | else markErr(); 213 | } 214 | cont(); 215 | } 216 | function endtag(startOfLine) { 217 | return function(style, content) { 218 | if (content == "/>" || (content == ">" && UseKludges.autoSelfClosers.hasOwnProperty(currentTag))) cont(); 219 | else if (content == ">") {pushContext(currentTag, startOfLine); cont();} 220 | else {markErr(); cont(arguments.callee);} 221 | }; 222 | } 223 | function attributes(style) { 224 | if (style == "xml-name") {token.style = "xml-attname"; cont(attribute, attributes);} 225 | else pass(); 226 | } 227 | function attribute(style, content) { 228 | if (content == "=") cont(value); 229 | else if (content == ">" || content == "/>") pass(endtag); 230 | else pass(); 231 | } 232 | function value(style) { 233 | if (style == "xml-attribute") cont(value); 234 | else pass(); 235 | } 236 | 237 | return { 238 | indentation: function() {return indented;}, 239 | 240 | next: function(){ 241 | token = tokens.next(); 242 | if (token.style == "whitespace" && tokenNr == 0) 243 | indented = token.value.length; 244 | else 245 | tokenNr++; 246 | if (token.content == "\n") { 247 | indented = tokenNr = 0; 248 | token.indentation = computeIndentation(context); 249 | } 250 | 251 | if (token.style == "whitespace" || token.type == "xml-comment") 252 | return token; 253 | 254 | while(true){ 255 | consume = false; 256 | cc.pop()(token.style, token.content); 257 | if (consume) return token; 258 | } 259 | }, 260 | 261 | copy: function(){ 262 | var _cc = cc.concat([]), _tokenState = tokens.state, _context = context; 263 | var parser = this; 264 | 265 | return function(input){ 266 | cc = _cc.concat([]); 267 | tokenNr = indented = 0; 268 | context = _context; 269 | tokens = tokenizeXML(input, _tokenState); 270 | return parser; 271 | }; 272 | } 273 | }; 274 | } 275 | 276 | return { 277 | make: parseXML, 278 | electricChars: "/", 279 | configure: function(config) { 280 | if (config.useHTMLKludges != null) 281 | UseKludges = config.useHTMLKludges ? Kludges : NoKludges; 282 | if (config.alignCDATA) 283 | alignCDATA = config.alignCDATA; 284 | } 285 | }; 286 | })(); 287 | -------------------------------------------------------------------------------- /docker/Readme.md: -------------------------------------------------------------------------------- 1 | These instructions guide you through setting up an Ibex Farm instance on a 2 | Linode running CentOS 8. Unlike the earlier version of these instructions at 3 | https://adrummond.net/posts/ibexfarmdocker, these instructions assume that you 4 | have a domain and want to use Caddy's automatic SSL cert management via 5 | [Letsencrypt](https://letsencrypt.org/). 6 | 7 | For simplicity, these instructions assume that you will be logging in using a 8 | username and password. It is advisible to disable the option to log in via a 9 | username and password over ssh (and use keys instead). 10 | 11 | ## Creating a linode 12 | 13 | [Linode](https://linode.com) is one of many cloud hosting providers. It's cheap 14 | and easy to use compared to more sophisticated options like 15 | [AWS](https://aws.amazon.com). 16 | 17 | If you anticipate hosting large number of experiments (more than a few hundred), 18 | then check out the ‘Storage Space’ section below. 19 | 20 | After creating a linode account, create a linode running CoreOS Container Linux. 21 | Note that the Apache and Docker configuration is quite tricky, so don't expect 22 | these exact instructions to work on other distros without significant 23 | modification. 24 | 25 | If the IP address of your linode is e.g. `192.192.192.192`, you can ssh in as 26 | follows: 27 | 28 | ```sh 29 | ssh root@192.192.192.192 30 | ``` 31 | 32 | ## Setting up the linode 33 | 34 | Ssh in as root (as in the example above). Execute the following commands: 35 | 36 | ```sh 37 | adduser ibex 38 | passwd ibex 39 | usermod -aG wheel ibex 40 | dnf update -y 41 | dnf install -y firewalld git wget 42 | systemctl enable firewalld 43 | rm -f /etc/firewalld/zones/public.xml 44 | firewall-cmd --complete-reload 45 | firewall-cmd --zone=public --add-service=http --permanent 46 | firewall-cmd --zone=public --add-service=https --permanent 47 | firewall-cmd --zone=public --add-service=ssh --permanent # may show 'already enabled' warning 48 | firewall-cmd --zone=public --add-port=443/tcp --permanent 49 | firewall-cmd --zone=public --add-port=80/tcp --permanent 50 | firewall-cmd --zone=public --add-masquerade --permanent 51 | firewall-cmd --reload 52 | ulimit -n 8192 # for caddy 53 | shutdown -r now 54 | ``` 55 | 56 | The linode will now reboot, terminating your ssh session. In a couple of 57 | minutes, ssh in again as user `ibex` (e.g. `ssh ibex@192.192.192.192`). Install 58 | docker and docker-compose: 59 | 60 | ```sh 61 | sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo 62 | sudo dnf install -y --nobest docker-ce 63 | sudo curl -L "https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 64 | sudo chmod +x /usr/local/bin/docker-compose 65 | sudo usermod -aG docker ibex 66 | ``` 67 | 68 | We can now start the docker daemon: 69 | 70 | ```sh 71 | sudo systemctl enable docker 72 | sudo systemctl start docker 73 | ``` 74 | 75 | Allow the `ibex` user to do Docker stuff by adding it to the `docker` group: 76 | 77 | ```sh 78 | sudo usermod -aG docker ibex 79 | logout # you have to log out and log in again for the new group perms to take effect 80 | ``` 81 | 82 | Ssh in again as `ibex`. Create directory called `ibexdata` to hold the Ibex Farm data: 83 | 84 | ```sh 85 | mkdir ~/ibexdata 86 | ``` 87 | 88 | Build the Ibex Farm Docker container: 89 | 90 | ```sh 91 | cd ~ 92 | git clone https://github.com/addrummond/ibexfarm 93 | cd ibexfarm/docker 94 | docker build . 95 | ``` 96 | 97 | Configure the webmaster email address and webmaster name for this instance, 98 | together with some other configuration options: 99 | 100 | ```sh 101 | sudo touch /etc/ibexenv.sh 102 | sudo chown ibex:ibex /etc/ibexenv.sh 103 | echo 'IBEXFARM_webmaster_email="example@example.com"' >> /etc/ibexenv.sh 104 | echo 'IBEXFARM_webmaster_name="Some person"' >> /etc/ibexenv.sh 105 | echo 'IBEXFARM_url_prefix="/"' >> /etc/ibexenv.sh 106 | echo 'IBEXFARM_experiment_base_url="/ibexexps"' >> /etc/ibexenv.sh 107 | ``` 108 | 109 | You may want to add the following definition 110 | to ibexenv.sh to make the Ibex Farm use the Perl code in `~/ibexfarm/docker` 111 | rather than the code inside the Docker container: 112 | 113 | ```sh 114 | echo 'IBEXFARM_src_dir=/code' >> /etc/ibexenv.sh 115 | ``` 116 | 117 | Source the preceding definitions and add them to the system profile: 118 | 119 | ```sh 120 | set -o allexport ; source /etc/ibexenv.sh ; set +o allexport 121 | sudo bash -c 'echo "set -o allexport ; source /etc/ibexenv.sh ; set +o allexport" > /etc/profile.d/ibex.sh' 122 | ``` 123 | 124 | Allow systemd to manage Docker containers: 125 | 126 | ```sh 127 | sudo setsebool -P container_manage_cgroup on 128 | ``` 129 | 130 | Create a systemd service called `ibexfarm-server` to run the docker container: 131 | 132 | ```sh 133 | printf "[Unit]\nDescription=Ibex Farm server\nWants=docker.service\nAfter=docker.service\n[Service]\nLimitNOFILE=8192\nEnvironmentFile=/etc/ibexenv.sh\nUser=ibex\nRestart=always\nRestartSec=10\nExecStartPre=/usr/bin/bash -c 'cat /etc/ibexenv.sh | xargs -n 1 echo > /tmp/ibexenv_docker'\nExecStart=/usr/local/bin/docker-compose -f /home/ibex/ibexfarm/docker/docker-compose.yml up\nExecStop=/usr/local/bin/docker-compose -f /home/ibex/ibexfarm/docker/docker-compose.yml down\n[Install]\nWantedBy=multi-user.target\n" | sudo bash -c 'tee > /etc/systemd/system/ibexfarm-server.service' 134 | sudo systemctl daemon-reload 135 | ``` 136 | 137 | Finally, start Ibex Farm using the following commands: 138 | 139 | ```sh 140 | sudo systemctl start ibexfarm-server.service 141 | sudo systemctl enable ibexfarm-server.service 142 | ``` 143 | 144 | At this point, if the server is up and running, you should be able to retrieve 145 | `index.html` by running `wget http://localhost:8888`. 146 | 147 | ## Storage space 148 | 149 | If you anticipate hosting lots of experiments on your Ibex Farm instance, you 150 | should store the `ibexdata` volume on a linode volume rather than on the root 151 | filesystem of the linode. Whereas there's no straightforward way to enlarge a 152 | linode's root filesystem, it's easy to enlarge a linode volume. See the [docker 153 | documentation](https://docs.docker.com/engine/reference/commandline/volume_create/) 154 | (and in particular the `--opt device` option to `docker volume create`) for more 155 | info. 156 | 157 | ## Setting up Caddy with https 158 | 159 | **You'll need to get a domain name pointing to the IP of your linode before 160 | following these instructions.** 161 | 162 | **Remember that DNS propagation can take a while, so wait for a few hours after 163 | you've associated your domain name with your linode's IP address.** 164 | 165 | This section steps through the process of setting up https using a free 166 | [letsencrypt](https://letsencrypt.org/) certificate. 167 | 168 | First, define your hostname: 169 | 170 | ```sh 171 | echo 'IBEXFARM_host="my.domain.name"' >> /etc/ibexenv.sh 172 | set -o allexport ; source /etc/ibexenv.sh ; set +o allexport 173 | ``` 174 | 175 | Install Caddy: 176 | 177 | ```sh 178 | cd ~ 179 | wget https://github.com/caddyserver/caddy/releases/download/v1.0.3/caddy_v1.0.3_linux_amd64.tar.gz 180 | sudo mkdir /caddy 181 | sudo useradd -r -d /caddy -M -s /sbin/nologin caddy 182 | sudo chown caddy:caddy /caddy 183 | sudo tar -xzf caddy_v1.0.3_linux_amd64.tar.gz -C /caddy 184 | sudo chown -R caddy:caddy /caddy 185 | rm ~/caddy_v1.0.3_linux_amd64.tar.gz 186 | sudo -u caddy mkdir /caddy/ssl 187 | sudo setcap CAP_NET_BIND_SERVICE=+eip /caddy/caddy 188 | ``` 189 | 190 | Create a systemd service for Caddy: 191 | 192 | ```sh 193 | printf "[Unit]\nDescription=Caddy HTTP/2 web server\nDocumentation=https://caddyserver.com/docs\nAfter=network-online.target\nWants=network-online.target systemd-networkd-wait-online.service\n[Service]\nRestart=on-abnormal\nUser=caddy\nGroup=caddy\nEnvironment=CADDYPATH=/caddy/ssl\nEnvironmentFile=/etc/ibexenv.sh\nExecStartPre=/bin/bash -c 'env > /caddy/env_on_startup'\nExecStart=/caddy/caddy -log stdout -agree=true -conf=/caddy/caddy.conf\nExecReload=/bin/kill -USR1 \$MAINPID\nKillMode=mixed\nKillSignal=SIGQUIT\nTimeoutStopSec=5s\nLimitNOFILE=1048576\nLimitNPROC=512\nPrivateTmp=true\nPrivateDevices=true\nReadWriteDirectories=/caddy/ssl\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE\nAmbientCapabilities=CAP_NET_BIND_SERVICE\nNoNewPrivileges=true\n[Install]\nWantedBy=multi-user.target\n" | sudo bash -c 'tee > /etc/systemd/system/caddy.service' 194 | sudo systemctl daemon-reload 195 | ``` 196 | 197 | Set up Caddy with automatic SSL cert management: 198 | 199 | ```sh 200 | sudo -u caddy bash -c 'printf "{\$IBEXFARM_host} {\n log syslog\n proxy {\$IBEXFARM_url_prefix} http://127.0.0.1:8888 { without {\$IBEXFARM_url_prefix} }\n proxy {\$IBEXFARM_experiment_base_url} http://127.0.0.1:8888\n tls {\$IBEXFARM_webmaster_email}\n}\n" > /caddy/caddy.conf' 201 | ``` 202 | 203 | Make sure that you've set `IBEXFARM_webmaster_email` to a real 204 | email address in `/etc/ibexenv.sh`. This email address will be associated 205 | with your SSL cert. 206 | 207 | ### Start Caddy 208 | 209 | Finally, start and enable the Caddy systemd service: 210 | 211 | ```sh 212 | sudo setenforce 0 213 | sudo systemctl start caddy.service 214 | sudo systemctl enable caddy.service 215 | ``` 216 | 217 | Unfortunately, it appears to be necessary to disable SELinux for Caddy to start. 218 | I haven't yet been able to find a resolution for this issue. You could try 219 | looking [here](https://caddy.community/t/caddy-under-centos-8-fedora-redhat/6791/6), 220 | which seems to have been updated since last I looked. 221 | 222 | You should now have access to your Ibex Farm instance over https. Caddy has been 223 | configured to redirect any http requests to https. 224 | 225 | You may wish to create an `example` user with an `example` experiment, so that 226 | the link on the homepage isn't broken. 227 | 228 | ## The docker apache user 229 | 230 | It can be useful to create a `dapache` user and group with the ids of the 231 | `apache` user and group inside the Docker container: 232 | 233 | ```sh 234 | sudo groupadd dapache -g 987654 235 | sudo useradd -g dapache -u 987654 -s /sbin/nologin dapache 236 | ``` 237 | 238 | You can then e.g. `chown` a file to `dapache:dapache` to have it owned by the 239 | Docker `apache` user. 240 | 241 | ## Long startup times 242 | 243 | If you have a large set of existing experiments, you may find that the 244 | `ibexfarm-server` process takes a long time to start up. (It takes about 10 245 | minutes on `spellout.net`.) This is due to a recursive `chown` executed in the 246 | entrypoint. After the server has been started for the first time, it is no 247 | longer necessary to run this command. You can set 248 | `IBEXFARM_dont_chown_data_volume=1` to prevent the `chown` from executing and 249 | reduce startup times. 250 | 251 | ## Admin tasks 252 | 253 | To reset the password for a user, use the reset_password.sh script: 254 | 255 | ```sh 256 | ~/ibexfarm/script/reset_password.sh username # generates new random password for user (and prints it to console) 257 | ~/ibexfarm/script/reset_password.sh username newpassword # resets the password for the user to the one specified 258 | ``` 259 | -------------------------------------------------------------------------------- /lib/IbexFarm/Controller/User.pm: -------------------------------------------------------------------------------- 1 | package IbexFarm::Controller::User; 2 | 3 | use strict; 4 | use warnings; 5 | use parent 'Catalyst::Controller'; 6 | use File::Spec::Functions qw( catfile catdir ); 7 | use IbexFarm::FNames; 8 | use IbexFarm::CheckEmail; 9 | use File::Path qw( rmtree ); 10 | use Digest; 11 | use IbexFarm::Util qw( log_event ); 12 | use Net::SSLeay; 13 | use Crypt::Argon2; 14 | 15 | my $get_salt = sub { 16 | my $length = shift; 17 | my @salt_pool = ('A' .. 'Z', 'a' .. 'z', 0 .. 9, '+','/','='); 18 | my $salt_pool_length = 26 * 2 + 10 + 3; 19 | my $rb = ''; 20 | Net::SSLeay::RAND_bytes($rb, $length); 21 | my $out = ''; 22 | for (my $i = 0; $i < $length; ++$i) { 23 | $out .= $salt_pool[ord(substr($rb, $i, $i+1)) % $salt_pool_length]; 24 | } 25 | return $out; 26 | }; 27 | 28 | my $make_pw_hash = sub { 29 | my $password = shift; 30 | my $salt = $get_salt->(IbexFarm->config->{argon2id_salt_length}); 31 | return Crypt::Argon2::argon2id_pass( 32 | $password, 33 | $salt, 34 | IbexFarm->config->{argon2id_t_cost}, 35 | IbexFarm->config->{argon2id_m_factor}, 36 | IbexFarm->config->{argon2id_parallelism}, 37 | IbexFarm->config->{argon2id_tag_size}, 38 | ); 39 | }; 40 | 41 | sub login :Absolute :Args(0) { 42 | my ($self, $c) = @_; 43 | 44 | my $username = $c->request->params->{username}; 45 | my $password = $c->request->params->{password}; 46 | 47 | if ($username && $password) { 48 | if ($c->authenticate({ username => $username, password => $password })) { 49 | log_event("User $username logged in."); 50 | 51 | IbexFarm::Util::update_json_file( 52 | catfile(IbexFarm->config->{deployment_dir}, $username, IbexFarm->config->{USER_FILE_NAME}), 53 | sub { 54 | my $j = shift; 55 | if (IbexFarm->config->{rehash_old_passwords} && $j->{password} !~ /^\$/) { 56 | # Rehash the password using a modern pw hash. 57 | log_event("Rehashing password for user $username."); 58 | $j->{password} = $make_pw_hash->($password); 59 | return $j; 60 | } else { 61 | return undef; # leave the file unmodified 62 | } 63 | } 64 | ); 65 | 66 | $c->response->redirect($c->uri_for('/myaccount')); 67 | } 68 | else { 69 | log_event("User $username FAILED to log in."); 70 | $c->stash->{username} = $username; 71 | $c->stash->{error} = "The details you entered were not recognized."; 72 | $c->stash->{template} = "login.tt"; 73 | } 74 | } 75 | else { 76 | $c->stash->{template} = "login.tt"; 77 | } 78 | } 79 | 80 | sub delete_account :Absolute :Args(0) { 81 | my ($self, $c) = @_; 82 | 83 | if (! $c->user_exists) { 84 | $c->stash->{error} = "You must be logged in to delete your account."; 85 | $c->stash->{template} = "login.tt"; 86 | } 87 | elsif ($c->req->method eq "GET") { 88 | $c->stash->{template} = "delete_account.tt"; 89 | } 90 | elsif ($c->req->method eq "POST") { 91 | # Delete the user's dir, and www dir if any. 92 | # Note that the www dir will not exist (whatever the value of 'deployment_www_dir') 93 | # if the user has never created an experiment). 94 | my @dirs = catdir(IbexFarm->config->{deployment_dir}, $c->user->username); 95 | my $www = catdir(IbexFarm->config->{deployment_www_dir}, $c->user->username); 96 | push @dirs, $www if (IbexFarm->config->{deployment_www_dir} && -d $www); 97 | my $r = rmtree(\@dirs, 0, 0); 98 | unless ($r) { 99 | die "Inconsistency when deleting user account!"; 100 | } 101 | 102 | log_event("User " . $c->user->username . " deleted."); 103 | 104 | my $username = $c->user->username; 105 | $c->logout; 106 | 107 | $c->stash->{template} = "deleted.tt"; 108 | } 109 | else { 110 | $c->detach('Root', 'bad_request'); 111 | } 112 | } 113 | 114 | sub update_email :Absolute :Args(0) { 115 | my ($self, $c) = @_; 116 | 117 | $c->detach('Root', 'bad_request') unless defined $c->req->params->{email}; 118 | 119 | if (! $c->user_exists) { 120 | $c->stash->{error} = "You must be logged in to update your email."; 121 | $c->stash->{template} = "login.tt"; 122 | } 123 | else { 124 | if ($c->req->params->{email} && ! IbexFarm::CheckEmail::is_ok_email($c->req->params->{email})) { 125 | $c->stash->{error} = "The email address you entered is not valid."; 126 | $c->stash->{template} = 'user.tt'; 127 | } 128 | else { 129 | IbexFarm::Util::update_json_file( 130 | catfile(IbexFarm->config->{deployment_dir}, $c->user->username, IbexFarm->config->{USER_FILE_NAME}), 131 | sub { 132 | my $j = shift; 133 | $j->{email_address} = $c->req->params->{email}; 134 | return $j; 135 | } 136 | ); 137 | 138 | $c->stash->{email_address} = $c->req->params->{email}; 139 | $c->stash->{message} = "Your email has been updated."; 140 | $c->stash->{template} = "user.tt"; 141 | } 142 | } 143 | } 144 | 145 | sub update_password :Absolute :Args(0) { 146 | my ($self, $c) = @_; 147 | 148 | $c->detach('Root', 'bad_request') unless (defined $c->req->params->{password1} && defined $c->req->params->{password2}); 149 | 150 | if (! $c->user_exists) { 151 | $c->stash->{error} = "You must be logged in to change your password."; 152 | $c->stash->{template} = "login.tt"; 153 | return; 154 | } 155 | 156 | my $password1 = $c->request->params->{password1}; 157 | my $password2 = $c->request->params->{password2}; 158 | 159 | if (! $password1 || ! $password2) { 160 | $c->stash->{error} = "You must fill in both password fields."; 161 | $c->stash->{template} = "user.tt"; 162 | return; 163 | } 164 | if ($password1 ne $password2) { 165 | $c->stash->{error} = "The passwords do not match."; 166 | $c->stash->{template} = "user.tt"; 167 | return; 168 | } 169 | 170 | my $pwhash = $make_pw_hash->($password1); 171 | IbexFarm::Util::update_json_file( 172 | catfile(IbexFarm->config->{deployment_dir}, $c->user->username, IbexFarm->config->{USER_FILE_NAME}), 173 | sub { 174 | my $j = shift; 175 | $j->{password} = $pwhash; 176 | return $j; 177 | } 178 | ); 179 | 180 | $c->stash->{message} = "Your password has been updated. Use the new password next time you log in."; 181 | $c->stash->{template} = "user.tt"; 182 | } 183 | 184 | sub newaccount :Absolute :Args(0) { 185 | my ($self, $c) = @_; 186 | 187 | my $username = $c->request->params->{username}; 188 | my $password = $c->request->params->{password}; 189 | my $password2 = $c->request->params->{password2}; 190 | my $email = $c->request->params->{email}; 191 | 192 | my $stash_user_info = sub { 193 | $c->stash->{username} = $username; 194 | $c->stash->{email} = $email; 195 | }; 196 | 197 | my $ispost = $c->req->method eq "POST"; 198 | 199 | if ($ispost && (! ($username && $password && $password2))) { 200 | $c->stash->{error} = "You must fill in all the fields except email."; 201 | $c->stash->{template} = "newaccount.tt"; 202 | $stash_user_info->(); 203 | } 204 | elsif ($ispost && (! IbexFarm::FNames::is_ok_fname($username))) { 205 | $c->stash->{error} = "Usernames may contain only " . IbexFarm::FNames::OK_CHARS_DESCRIPTION . "and must be less than " . IbexFarm->config->{max_fname_length} . " characters long."; 206 | $c->stash->{template} = "newaccount.tt"; 207 | $stash_user_info->(); 208 | } 209 | elsif ($ispost && ($password ne $password2)) { 210 | $c->stash->{error} = "The passwords do not match."; 211 | $c->stash->{template} = "newaccount.tt"; 212 | $stash_user_info->(); 213 | } 214 | elsif ($ispost && ($email && (! IbexFarm::CheckEmail::is_ok_email($email)))) { 215 | $c->stash->{error} = "The email address you entered is not valid. (Note that you don't have to give an email if you don't want to.)"; 216 | $c->stash->{template} = "newaccount.tt"; 217 | $stash_user_info->(); 218 | } 219 | elsif (! $ispost) { 220 | $c->stash->{template} = "newaccount.tt"; 221 | } 222 | else { 223 | # Check that a user with that username doesn't already exist. 224 | my $udir = catdir(IbexFarm->config->{deployment_dir}, $username); 225 | if (-e $udir) { 226 | $c->stash->{username} = $username; 227 | $c->stash->{email} = $email; 228 | $c->stash->{error} = "An account with that username already exists."; 229 | $c->stash->{template} = "newaccount.tt"; 230 | } 231 | else { 232 | # Log the user out, if one is logged in. 233 | $c->logout if ($c->user_exists); 234 | 235 | my $pwhash = $make_pw_hash->($password); 236 | 237 | my $user = { 238 | username => $username, 239 | password => $pwhash, 240 | email_address => $email || undef, 241 | active => 1, 242 | user_roles => [ 'user' ] 243 | }; 244 | 245 | # Create the user's dir. 246 | eval { 247 | mkdir $udir or die "Unable to create dir for user: $!"; 248 | 249 | # Write the user info to the 'USER' file. 250 | open my $f, '>' . catfile($udir, IbexFarm->config->{USER_FILE_NAME}) or die "Unable to open '", IbexFarm->config->{USER_FILE_NAME}, "' file: $!"; 251 | print $f JSON::XS::encode_json($user); 252 | close $f; 253 | }; 254 | if ($@) { 255 | if (-d $udir) { rmdir $udir or die "Unable to remove user directory following error."; } 256 | die $@; 257 | } 258 | 259 | $c->authenticate({ username => $username, password => $password }) or 260 | die "Unable to authenticate following account creation."; 261 | 262 | log_event("User account $username created."); 263 | 264 | $c->response->redirect($c->uri_for('/myaccount')); 265 | } 266 | } 267 | } 268 | 269 | sub myaccount :Absolute :Args(0) { 270 | my ($self, $c) = @_; 271 | 272 | return $c->response->redirect($c->uri_for('/login')) unless ($c->user_exists); 273 | 274 | my $ufile = catdir(IbexFarm->config->{deployment_dir}, $c->user->username, IbexFarm->config->{USER_FILE_NAME}); 275 | open my $f, $ufile or die "Unable to open '", IbexFarm->config->{USER_FILE_NAME}, "' file for reading."; 276 | local $/; 277 | my $contents = <$f>; 278 | close $f or die "Unable to close '", IbexFarm->config->{USER_FILE_NAME}, "' file after reading."; 279 | my $coder = JSON::XS->new->boolean_values(\0, \1); 280 | my $u = $coder->decode($contents); 281 | die "Bad JSON for user" unless (ref($u) eq 'HASH'); 282 | 283 | $c->stash->{email_address} = $u->{email_address}; 284 | $c->stash->{template} = 'user.tt'; 285 | } 286 | 287 | sub logout :Absolute :Args(0) { 288 | my ($self, $c) = @_; 289 | 290 | my $username = $c->user->username; 291 | $c->logout; 292 | log_event("User $username logged out."); 293 | $c->response->redirect($c->uri_for('/')); 294 | } 295 | 296 | 1; 297 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.8 2 | 3 | # Install required alpine linux packages. 4 | RUN apk update && \ 5 | apk add \ 6 | git \ 7 | perl \ 8 | perl-dev \ 9 | musl-dev \ 10 | python \ 11 | apache2 \ 12 | gcc \ 13 | make \ 14 | perl-app-cpanminus \ 15 | perl-namespace-autoclean \ 16 | perl-yaml \ 17 | perl-yaml-xs \ 18 | perl-moose \ 19 | perl-time-hires \ 20 | perl-archive-zip \ 21 | perl-params-classify \ 22 | gdbm \ 23 | apache2-dev \ 24 | curl \ 25 | apache2-utils \ 26 | apache2-ssl \ 27 | shadow \ 28 | vim 29 | 30 | # Install the remaining required perl modules using cpanm. 31 | # Build Crypt::Rijndael perl module from source 32 | # because we need to apply a patch to rijndael.h. 33 | # Build modperl from source because no stable alpine package available. 34 | RUN cd /tmp && \ 35 | wget https://archive.apache.org/dist/perl/mod_perl-2.0.10.tar.gz && \ 36 | tar -xzf mod_perl-2.0.10.tar.gz && \ 37 | cd mod_perl-2.0.10/ && \ 38 | perl Makefile.PL MP_APXS=/usr/bin/apxs && \ 39 | make && make install && \ 40 | rm -r /tmp/mod_perl-2.0.10* && \ 41 | cd /tmp && \ 42 | wget https://cpan.metacpan.org/authors/id/L/LE/LEONT/Crypt-Rijndael-1.13.tar.gz && \ 43 | cd /tmp && \ 44 | tar -xzf Crypt-Rijndael-1.13.tar.gz && \ 45 | cd Crypt-Rijndael-1.13/ && \ 46 | perl Makefile.PL && \ 47 | sed -i s/__uint8_t/uint8_t/g rijndael.h && \ 48 | sed -i s/__uint32_t/uint32_t/g rijndael.h && \ 49 | make install && \ 50 | cd .. && \ 51 | rm -rf Crypt-Rijndael-1.13/ && \ 52 | cd ~ && \ 53 | cpanm --notest --no-man-pages --no-wget --curl \ 54 | MooseX::Types \ 55 | MooseX::ConfigFromFile \ 56 | MooseX::Getopt \ 57 | MooseX::Role::Parameterized \ 58 | MooseX::SimpleConfig \ 59 | MooseX::StrictConstructor \ 60 | MooseX::Types::DateTime \ 61 | Catalyst::Devel \ 62 | Catalyst::Plugin::RequireSSL \ 63 | Catalyst::Plugin::Session::Store::File \ 64 | JSON \ 65 | JSON::XS \ 66 | Catalyst::View::JSON \ 67 | Template::Plugin::Filter::Minify::CSS \ 68 | Template::Plugin::Filter::Minify::JavaScript \ 69 | Catalyst::Plugin::Cache::FileCache \ 70 | Catalyst::Plugin::UploadProgress \ 71 | HTML::GenerateUtil \ 72 | Class::Factory \ 73 | Digest \ 74 | Data::Validate::URI \ 75 | Log::Handler \ 76 | Crypt::OpenPGP \ 77 | Variable::Magic \ 78 | DateTime \ 79 | Class::ISA \ 80 | Catalyst::Authentication::User::Hash \ 81 | Catalyst::Plugin::Session::State::Cookie \ 82 | Catalyst::View::TT \ 83 | Catalyst::Plugin::ConfigLoader::Environment \ 84 | Devel::OverloadInfo \ 85 | Net::SSLeay \ 86 | Crypt::Argon2 \ 87 | String::Random 88 | 89 | RUN mkdir /ibexfarm.git && \ 90 | git clone --depth 1 https://github.com/addrummond/ibexfarm.git /ibexfarm.git && \ 91 | chown -R apache:apache /ibexfarm.git 92 | 93 | # Checkout the revision of github.com/addrummond/ibex corresponding to 0.3.9 94 | # and create a tarball. 95 | RUN mkdir /var/ibexfarm && \ 96 | cd /tmp && \ 97 | git clone https://github.com/addrummond/ibex && \ 98 | cd ibex && \ 99 | git checkout 0e4978d73a085fffd4d1ab73098f4f81e3065c3c && \ 100 | rm -rf .git* && \ 101 | rm -rf docs && \ 102 | rm -rf contrib && \ 103 | rm -f LICENSE README example_lighttpd.conf mkdist.sh server_conf.py && \ 104 | cd .. && \ 105 | tar -czf ibex-deploy-original.tar.gz ibex && \ 106 | mv ibex-deploy-original.tar.gz /var/ibexfarm 107 | 108 | RUN mkdir -p /run/apache2 && \ 109 | sed -i 's/^Listen[\t ].*/Listen ${IBEXFARM_port}'/ /etc/apache2/httpd.conf && \ 110 | sed -i 's/LoadModule mpm_prefork/#LoadModule mpm_prefork/' /etc/apache2/httpd.conf && \ 111 | sed -i 's/#LoadModule mpm_worker_module/LoadModule mpm_worker_module/' /etc/apache2/httpd.conf && \ 112 | printf "\ 113 | LoadModule perl_module /usr/lib/apache2/mod_perl.so\n\ 114 | ServerName localhost\n\ 115 | \n\ 116 | PerlSwitches -I\${IBEXFARM_src_dir}/lib\n\ 117 | PerlModule IbexFarm\n\ 118 | \n\ 119 | \n\ 120 | SetHandler modperl\n\ 121 | PerlResponseHandler IbexFarm\n\ 122 | \n\ 123 | \n\ 124 | Alias /static/ \${IBEXFARM_src_dir}/root/static/\n\ 125 | \n\ 126 | Options none\n\ 127 | Require all granted\n\ 128 | \n\ 129 | \n\ 130 | SetHandler default-handler\n\ 131 | \n\ 132 | \n\ 133 | DocumentRoot \"/var/www\"\n\ 134 | \n\ 135 | AddHandler cgi-script .py\n\ 136 | Alias /ibexexps /ibexdata/ibexexps\n\ 137 | \n\ 138 | \n\ 139 | Options +ExecCGI +FollowSymLinks\n\ 140 | AllowOverride AuthConfig\n\ 141 | DirectoryIndex experiment.html\n\ 142 | AuthUserFile \"/ibexdata/htpasswd\"\n\ 143 | Require all granted\n\ 144 | \n\ 145 | \n\ 146 | #\n\ 147 | # Relax access to content within /var/www.\n\ 148 | #\n\ 149 | \n\ 150 | AllowOverride None\n\ 151 | # Allow open access:\n\ 152 | Require all granted\n\ 153 | \n\ 154 | \n\ 155 | # Log to stdout/stderr\n\ 156 | ErrorLog /dev/stderr\n\ 157 | TransferLog /dev/stdout\n\ 158 | \n\ 159 | LoadModule cgi_module modules/mod_cgi.so\n\ 160 | # SetEnv rather than PerlSetEnv for IBEXFARM_config_url because\n\ 161 | # it needs to go through to CGI scripts, not modperl code.\n\ 162 | SetEnv IBEXFARM_config_url \"\${IBEXFARM_config_url}\"\n\ 163 | Include /etc/apache2/perlenv\n" >> /etc/apache2/httpd.conf 164 | 165 | # Create wrapper we use to run httpd. 166 | RUN printf "#!bin/sh\n\ 167 | echo 'Starting the Ibex Farm...'\n\ 168 | cat <<\"END\">\${IBEXFARM_src_dir}/ibexfarm.yaml\n\ 169 | ---\n\ 170 | name: IbexFarm\n\ 171 | \n\ 172 | url_prefix: '/'\n\ 173 | \n\ 174 | webmaster_name: 'IBEX_WEBMASTER'\n\ 175 | webmaster_email: 'IBEX_WEBMASTER_EMAIL'\n\ 176 | \n\ 177 | ibex_archive: '/var/ibexfarm/ibex-deploy.tar.gz'\n\ 178 | ibex_version: '0.3.9'\n\ 179 | deployment_dir: '/ibexdata/deploy'\n\ 180 | deployment_www_dir: '/ibexdata/ibexexps'\n\ 181 | \n\ 182 | max_fname_length: 150\n\ 183 | \n\ 184 | dirs: [ 'js_includes', 'css_includes', 'data_includes', 'chunk_includes', 'server_state', 'results' ]\n\ 185 | sync_dirs: [ 'js_includes', 'css_includes', 'data_includes', 'chunk_includes', 'server_state' ]\n\ 186 | dirs_to_types:\n\ 187 | js_includes: 'text/javascript'\n\ 188 | css_includes: 'text/css'\n\ 189 | data_includes: 'text/javascript'\n\ 190 | chunk_includes: 'text/html'\n\ 191 | server_state: 'text/plain'\n\ 192 | results: 'text/plain'\n\ 193 | optional_dirs:\n\ 194 | server_state: 1\n\ 195 | results: 1\n\ 196 | writable: [ 'data_includes/*', 'results/*', 'server_state/*','chunk_includes/*' ]\n\ 197 | \n\ 198 | enforce_quotas: 0\n\ 199 | quota_max_files_in_dir: 500\n\ 200 | quota_max_file_size: 1048576\n\ 201 | quota_max_total_size: 1048576\n\ 202 | quota_record_dir: '/ibexdata/quota'\n\ 203 | \n\ 204 | password_protect_apache:\n\ 205 | htpasswd: '/usr/bin/htpasswd'\n\ 206 | passwd_file: '/ibexdata/htpasswd'\n\ 207 | \n\ 208 | max_upload_size_bytes: 5242880\n\ 209 | \n\ 210 | experiment_password_protection: Apache\n\ 211 | \n\ 212 | git_path: '/usr/bin/git'\n\ 213 | git_checkout_timeout_seconds: 25\n\ 214 | \n\ 215 | event_log_file: '/dev/stdout'\n\ 216 | \n\ 217 | experiment_base_url: '/ibexexps/'\n\ 218 | \n\ 219 | python_hashbang: '/usr/bin/python'\n\ 220 | \n\ 221 | config_url: 'http://localhost/ajax/config'\n\ 222 | config_permitted_hosts: ['localhost', '::1']\n\ 223 | END\n\ 224 | \n\ 225 | mkdir -p /ibexdata/deploy\n\ 226 | mkdir -p /ibexdata/ibexexps\n\ 227 | mkdir -p /ibexdata/quota\n\ 228 | mkdir -p /ibexdata/tmp\n\ 229 | \n\ 230 | if [ -z \"\${IBEXFARM_dont_chown_data_volume}\" ]; then\n\ 231 | chown -R apache:apache /ibexdata\n\ 232 | fi\n\ 233 | \n\ 234 | if [ ! -z \"\${IBEXFARM_spellout_legacy_jank}\" ]; then\n\ 235 | mkdir -p /opt/local\n\ 236 | mkdir -p /opt/local/bin\n\ 237 | ln -sf /usr/bin/python /opt/local/bin/python\n\ 238 | mkdir -p /var/l-apps\n\ 239 | mkdir -p /var/l-apps/ibexfarm\n\ 240 | ln -sf /ibexdata/deploy/ /var/l-apps/ibexfarm/deploy\n\ 241 | ln -sf /ibexdata/deploy /var/ibexfarm/deploy\n\ 242 | fi\n\ 243 | \n\ 244 | cd /tmp\n\ 245 | tar -xzf /var/ibexfarm/ibex-deploy-original.tar.gz\n\ 246 | if [ \"\${IBEXFARM_ibex_archive_root_dir}\" != \"ibex\" ]; then\n\ 247 | mv ibex \${IBEXFARM_ibex_archive_root_dir}\n\ 248 | fi\n\ 249 | tar -czf ibex-deploy.tar.gz \${IBEXFARM_ibex_archive_root_dir}\n\ 250 | mv ibex-deploy.tar.gz /var/ibexfarm/ibex-deploy.tar.gz\n\ 251 | rm -rf \${IBEXFARM_ibex_archive_root_dir}\n\ 252 | cd ~\n\ 253 | \n\ 254 | touch /ibexdata/htpasswd\n\ 255 | chown apache:apache /ibexdata/htpasswd\n\ 256 | \n\ 257 | # Work around issue with PerlSetEnv not liking empty second arg\n\ 258 | echo "" > /etc/apache2/perlenv\n\ 259 | if [ -n \"\$IBEXFARM_host\" ]; then\n\ 260 | echo PerlSetEnv IBEXFARM_host \\\"\\\${IBEXFARM_host}\\\" >> /etc/apache2/perlenv\n\ 261 | fi\n\ 262 | if [ -n \"\$IBEXFARM_url_prefix\" ]; then\n\ 263 | echo PerlSetEnv IBEXFARM_url_prefix \\\"\\\${IBEXFARM_url_prefix}\\\" >> /etc/apache2/perlenv\n\ 264 | fi\n\ 265 | if [ -n \"\$IBEXFARM_webmaster_email\" ]; then\n\ 266 | echo PerlSetEnv IBEXFARM_webmaster_email \\\"\\\${IBEXFARM_webmaster_email}\\\" >> /etc/apache2/perlenv\n\ 267 | fi\n\ 268 | if [ -n \"\$IBEXFARM_webmaster_name\" ]; then\n\ 269 | echo PerlSetEnv IBEXFARM_webmaster_name \\\"\\\${IBEXFARM_webmaster_name}\\\" >> /etc/apache2/perlenv\n\ 270 | fi\n\ 271 | if [ -n \"\$IBEXFARM_config_secret\" ]; then\n\ 272 | echo PerlSetEnv IBEXFARM_config_secret \\\"\\\${IBEXFARM_config_secret}\\\" >> /etc/apache2/perlenv\n\ 273 | fi\n\ 274 | if [ -n \"\$IBEXFARM_config_url_envvar\" ]; then\n\ 275 | echo PerlSetEnv IBEXFARM_config_url_envvar \\\"\\\${IBEXFARM_config_url_envvar}\\\" >> /etc/apache2/perlenv\n\ 276 | fi\n\ 277 | if [ -n \"\$IBEXFARM_rehash_old_passwords\" ]; then\n\ 278 | echo PerlSetEnv IBEXFARM_rehash_old_passwords \\\"\\\${IBEXFARM_rehash_old_passwords}\\\" >> /etc/apache2/perlenv\n\ 279 | fi\n\ 280 | if [ -n \"\$IBEXFARM_argon2id_t_cost\" ]; then\n\ 281 | echo PerlSetEnv IBEXFARM_argon2id_t_cost \\\"\\\${IBEXFARM_argon2id_t_cost}\\\" >> /etc/apache2/perlenv\n\ 282 | fi\n\ 283 | if [ -n \"\$IBEXFARM_argon2id_m_factor\" ]; then\n\ 284 | echo PerlSetEnv IBEXFARM_argon2id_m_factor \\\"\\\${IBEXFARM_argon2id_m_factor}\\\" >> /etc/apache2/perlenv\n\ 285 | fi\n\ 286 | if [ -n \"\$IBEXFARM_argon2id_parallelism\" ]; then\n\ 287 | echo PerlSetEnv IBEXFARM_argon2id_parallelism \\\"\\\${IBEXFARM_argon2id_parallelism}\\\" >> /etc/apache2/perlenv\n\ 288 | fi\n\ 289 | if [ -n \"\$IBEXFARM_argon2id_tag_size\" ]; then\n\ 290 | echo PerlSetEnv IBEXFARM_argon2id_tag_size \\\"\\\${IBEXFARM_argon2id_tag_size}\\\" >> /etc/apache2/perlenv\n\ 291 | fi\n\ 292 | if [ -n \"\$IBEXFARM_argon2id_salt_length\" ]; then\n\ 293 | echo PerlSetEnv IBEXFARM_argon2id_salt_length \\\"\\\${IBEXFARM_argon2id_salt_length}\\\" >> /etc/apache2/perlenv\n\ 294 | fi\n\ 295 | if [ -n \"\$IBEXFARM_ibex_archive_root_dir\" ]; then\n\ 296 | echo PerlSetEnv IBEXFARM_ibex_archive_root_dir \\\"\\\${IBEXFARM_ibex_archive_root_dir}\\\" >> /etc/apache2/perlenv\n\ 297 | fi\n\ 298 | if [ -n \"\$IBEXFARM_front_page_html_message\" ]; then\n\ 299 | echo PerlSetEnv IBEXFARM_front_page_html_message \\\"\\\${IBEXFARM_front_page_html_message}\\\" >> /etc/apache2/perlenv\n\ 300 | fi\n\ 301 | if [ -n \"\$IBEXFARM_enforce_quotas\" ]; then\n\ 302 | echo PerlSetEnv IBEXFARM_enforce_quotas \\\"\\\${IBEXFARM_enforce_quotas}\\\" >> /etc/apache2/perlenv\n\ 303 | fi\n\ 304 | if [ -n \"\$IBEXFARM_quota_max_files_in_dir\" ]; then\n\ 305 | echo PerlSetEnv IBEXFARM_quota_max_files_in_dir \\\"\\\${IBEXFARM_quota_max_files_in_dir}\\\" >> /etc/apache2/perlenv\n\ 306 | fi\n\ 307 | if [ -n \"\$IBEXFARM_quota_max_file_size\" ]; then\n\ 308 | echo PerlSetEnv IBEXFARM_quota_max_file_size \\\"\\\${IBEXFARM_quota_max_file_size}\\\" >> /etc/apache2/perlenv\n\ 309 | fi\n\ 310 | if [ -n \"\$IBEXFARM_quota_max_total_size\" ]; then\n\ 311 | echo PerlSetEnv IBEXFARM_quota_max_total_size \\\"\\\${IBEXFARM_quota_max_total_size}\\\" >> /etc/apache2/perlenv\n\ 312 | fi\n\ 313 | chown apache:apache /etc/apache2/perlenv\n\ 314 | \n\ 315 | exec /usr/sbin/httpd -D FOREGROUND\n\ 316 | " > /var/ibexfarm/start.sh && \ 317 | chmod +x /var/ibexfarm/start.sh 318 | 319 | # Fix the id of the apache user and group so we know what they are 320 | # if doing chowns in the host system. 321 | RUN groupmod -g 987654 apache && \ 322 | usermod -u 987654 apache 323 | 324 | EXPOSE 80 325 | ENTRYPOINT ["/var/ibexfarm/start.sh"] 326 | -------------------------------------------------------------------------------- /root/static/codemirror/parsejavascript.js: -------------------------------------------------------------------------------- 1 | /* Parse function for JavaScript. Makes use of the tokenizer from 2 | * tokenizejavascript.js. Note that your parsers do not have to be 3 | * this complicated -- if you don't want to recognize local variables, 4 | * in many languages it is enough to just look for braces, semicolons, 5 | * parentheses, etc, and know when you are inside a string or comment. 6 | * 7 | * See manual.html for more info about the parser interface. 8 | */ 9 | 10 | var JSParser = Editor.Parser = (function() { 11 | // Token types that can be considered to be atoms. 12 | var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true}; 13 | // Setting that can be used to have JSON data indent properly. 14 | var json = false; 15 | // Constructor for the lexical context objects. 16 | function JSLexical(indented, column, type, align, prev, info) { 17 | // indentation at start of this line 18 | this.indented = indented; 19 | // column at which this scope was opened 20 | this.column = column; 21 | // type of scope ('vardef', 'stat' (statement), 'form' (special form), '[', '{', or '(') 22 | this.type = type; 23 | // '[', '{', or '(' blocks that have any text after their opening 24 | // character are said to be 'aligned' -- any lines below are 25 | // indented all the way to the opening character. 26 | if (align != null) 27 | this.align = align; 28 | // Parent scope, if any. 29 | this.prev = prev; 30 | this.info = info; 31 | } 32 | 33 | // My favourite JavaScript indentation rules. 34 | function indentJS(lexical) { 35 | return function(firstChars) { 36 | var firstChar = firstChars && firstChars.charAt(0), type = lexical.type; 37 | var closing = firstChar == type; 38 | if (type == "vardef") 39 | return lexical.indented + 4; 40 | else if (type == "form" && firstChar == "{") 41 | return lexical.indented; 42 | else if (type == "stat" || type == "form") 43 | return lexical.indented + indentUnit; 44 | else if (lexical.info == "switch" && !closing) 45 | return lexical.indented + (/^(?:case|default)\b/.test(firstChars) ? indentUnit : 2 * indentUnit); 46 | else if (lexical.align) 47 | return lexical.column - (closing ? 1 : 0); 48 | else 49 | return lexical.indented + (closing ? 0 : indentUnit); 50 | }; 51 | } 52 | 53 | // The parser-iterator-producing function itself. 54 | function parseJS(input, basecolumn) { 55 | // Wrap the input in a token stream 56 | var tokens = tokenizeJavaScript(input); 57 | // The parser state. cc is a stack of actions that have to be 58 | // performed to finish the current statement. For example we might 59 | // know that we still need to find a closing parenthesis and a 60 | // semicolon. Actions at the end of the stack go first. It is 61 | // initialized with an infinitely looping action that consumes 62 | // whole statements. 63 | var cc = [json ? expressions : statements]; 64 | // Context contains information about the current local scope, the 65 | // variables defined in that, and the scopes above it. 66 | var context = null; 67 | // The lexical scope, used mostly for indentation. 68 | var lexical = new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false); 69 | // Current column, and the indentation at the start of the current 70 | // line. Used to create lexical scope objects. 71 | var column = 0; 72 | var indented = 0; 73 | // Variables which are used by the mark, cont, and pass functions 74 | // below to communicate with the driver loop in the 'next' 75 | // function. 76 | var consume, marked; 77 | 78 | // The iterator object. 79 | var parser = {next: next, copy: copy}; 80 | 81 | function next(){ 82 | // Start by performing any 'lexical' actions (adjusting the 83 | // lexical variable), or the operations below will be working 84 | // with the wrong lexical state. 85 | while(cc[cc.length - 1].lex) 86 | cc.pop()(); 87 | 88 | // Fetch a token. 89 | var token = tokens.next(); 90 | 91 | // Adjust column and indented. 92 | if (token.type == "whitespace" && column == 0) 93 | indented = token.value.length; 94 | column += token.value.length; 95 | if (token.content == "\n"){ 96 | indented = column = 0; 97 | // If the lexical scope's align property is still undefined at 98 | // the end of the line, it is an un-aligned scope. 99 | if (!("align" in lexical)) 100 | lexical.align = false; 101 | // Newline tokens get an indentation function associated with 102 | // them. 103 | token.indentation = indentJS(lexical); 104 | } 105 | // No more processing for meaningless tokens. 106 | if (token.type == "whitespace" || token.type == "comment") 107 | return token; 108 | // When a meaningful token is found and the lexical scope's 109 | // align is undefined, it is an aligned scope. 110 | if (!("align" in lexical)) 111 | lexical.align = true; 112 | 113 | // Execute actions until one 'consumes' the token and we can 114 | // return it. 115 | while(true) { 116 | consume = marked = false; 117 | // Take and execute the topmost action. 118 | cc.pop()(token.type, token.content); 119 | if (consume){ 120 | // Marked is used to change the style of the current token. 121 | if (marked) 122 | token.style = marked; 123 | // Here we differentiate between local and global variables. 124 | else if (token.type == "variable" && inScope(token.content)) 125 | token.style = "js-localvariable"; 126 | return token; 127 | } 128 | } 129 | } 130 | 131 | // This makes a copy of the parser state. It stores all the 132 | // stateful variables in a closure, and returns a function that 133 | // will restore them when called with a new input stream. Note 134 | // that the cc array has to be copied, because it is contantly 135 | // being modified. Lexical objects are not mutated, and context 136 | // objects are not mutated in a harmful way, so they can be shared 137 | // between runs of the parser. 138 | function copy(){ 139 | var _context = context, _lexical = lexical, _cc = cc.concat([]), _tokenState = tokens.state; 140 | 141 | return function copyParser(input){ 142 | context = _context; 143 | lexical = _lexical; 144 | cc = _cc.concat([]); // copies the array 145 | column = indented = 0; 146 | tokens = tokenizeJavaScript(input, _tokenState); 147 | return parser; 148 | }; 149 | } 150 | 151 | // Helper function for pushing a number of actions onto the cc 152 | // stack in reverse order. 153 | function push(fs){ 154 | for (var i = fs.length - 1; i >= 0; i--) 155 | cc.push(fs[i]); 156 | } 157 | // cont and pass are used by the action functions to add other 158 | // actions to the stack. cont will cause the current token to be 159 | // consumed, pass will leave it for the next action. 160 | function cont(){ 161 | push(arguments); 162 | consume = true; 163 | } 164 | function pass(){ 165 | push(arguments); 166 | consume = false; 167 | } 168 | // Used to change the style of the current token. 169 | function mark(style){ 170 | marked = style; 171 | } 172 | 173 | // Push a new scope. Will automatically link the current scope. 174 | function pushcontext(){ 175 | context = {prev: context, vars: {"this": true, "arguments": true}}; 176 | } 177 | // Pop off the current scope. 178 | function popcontext(){ 179 | context = context.prev; 180 | } 181 | // Register a variable in the current scope. 182 | function register(varname){ 183 | if (context){ 184 | mark("js-variabledef"); 185 | context.vars[varname] = true; 186 | } 187 | } 188 | // Check whether a variable is defined in the current scope. 189 | function inScope(varname){ 190 | var cursor = context; 191 | while (cursor) { 192 | if (cursor.vars[varname]) 193 | return true; 194 | cursor = cursor.prev; 195 | } 196 | return false; 197 | } 198 | 199 | // Push a new lexical context of the given type. 200 | function pushlex(type, info) { 201 | var result = function(){ 202 | lexical = new JSLexical(indented, column, type, null, lexical, info) 203 | }; 204 | result.lex = true; 205 | return result; 206 | } 207 | // Pop off the current lexical context. 208 | function poplex(){ 209 | lexical = lexical.prev; 210 | } 211 | poplex.lex = true; 212 | // The 'lex' flag on these actions is used by the 'next' function 213 | // to know they can (and have to) be ran before moving on to the 214 | // next token. 215 | 216 | // Creates an action that discards tokens until it finds one of 217 | // the given type. 218 | function expect(wanted){ 219 | return function expecting(type){ 220 | if (type == wanted) cont(); 221 | else cont(arguments.callee); 222 | }; 223 | } 224 | 225 | // Looks for a statement, and then calls itself. 226 | function statements(type){ 227 | return pass(statement, statements); 228 | } 229 | function expressions(type){ 230 | return pass(expression, expressions); 231 | } 232 | // Dispatches various types of statements based on the type of the 233 | // current token. 234 | function statement(type){ 235 | if (type == "var") cont(pushlex("vardef"), vardef1, expect(";"), poplex); 236 | else if (type == "keyword a") cont(pushlex("form"), expression, statement, poplex); 237 | else if (type == "keyword b") cont(pushlex("form"), statement, poplex); 238 | else if (type == "{") cont(pushlex("}"), block, poplex); 239 | else if (type == "function") cont(functiondef); 240 | else if (type == "for") cont(pushlex("form"), expect("("), pushlex(")"), forspec1, expect(")"), poplex, statement, poplex); 241 | else if (type == "variable") cont(pushlex("stat"), maybelabel); 242 | else if (type == "switch") cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"), block, poplex, poplex); 243 | else if (type == "case") cont(expression, expect(":")); 244 | else if (type == "default") cont(expect(":")); 245 | else if (type == "catch") cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), statement, poplex, popcontext); 246 | else pass(pushlex("stat"), expression, expect(";"), poplex); 247 | } 248 | // Dispatch expression types. 249 | function expression(type){ 250 | if (atomicTypes.hasOwnProperty(type)) cont(maybeoperator); 251 | else if (type == "function") cont(functiondef); 252 | else if (type == "keyword c") cont(expression); 253 | else if (type == "(") cont(pushlex(")"), expression, expect(")"), poplex, maybeoperator); 254 | else if (type == "operator") cont(expression); 255 | else if (type == "[") cont(pushlex("]"), commasep(expression, "]"), poplex, maybeoperator); 256 | else if (type == "{") cont(pushlex("}"), commasep(objprop, "}"), poplex, maybeoperator); 257 | else cont(); 258 | } 259 | // Called for places where operators, function calls, or 260 | // subscripts are valid. Will skip on to the next action if none 261 | // is found. 262 | function maybeoperator(type){ 263 | if (type == "operator") cont(expression); 264 | else if (type == "(") cont(pushlex(")"), commasep(expression, ")"), poplex, maybeoperator); 265 | else if (type == ".") cont(property, maybeoperator); 266 | else if (type == "[") cont(pushlex("]"), expression, expect("]"), poplex, maybeoperator); 267 | } 268 | // When a statement starts with a variable name, it might be a 269 | // label. If no colon follows, it's a regular statement. 270 | function maybelabel(type){ 271 | if (type == ":") cont(poplex, statement); 272 | else pass(maybeoperator, expect(";"), poplex); 273 | } 274 | // Property names need to have their style adjusted -- the 275 | // tokenizer thinks they are variables. 276 | function property(type){ 277 | if (type == "variable") {mark("js-property"); cont();} 278 | } 279 | // This parses a property and its value in an object literal. 280 | function objprop(type){ 281 | if (type == "variable") mark("js-property"); 282 | if (atomicTypes.hasOwnProperty(type)) cont(expect(":"), expression); 283 | } 284 | // Parses a comma-separated list of the things that are recognized 285 | // by the 'what' argument. 286 | function commasep(what, end){ 287 | function proceed(type) { 288 | if (type == ",") cont(what, proceed); 289 | else if (type == end) cont(); 290 | else cont(expect(end)); 291 | } 292 | return function commaSeparated(type) { 293 | if (type == end) cont(); 294 | else pass(what, proceed); 295 | }; 296 | } 297 | // Look for statements until a closing brace is found. 298 | function block(type){ 299 | if (type == "}") cont(); 300 | else pass(statement, block); 301 | } 302 | // Variable definitions are split into two actions -- 1 looks for 303 | // a name or the end of the definition, 2 looks for an '=' sign or 304 | // a comma. 305 | function vardef1(type, value){ 306 | if (type == "variable"){register(value); cont(vardef2);} 307 | else cont(); 308 | } 309 | function vardef2(type, value){ 310 | if (value == "=") cont(expression, vardef2); 311 | else if (type == ",") cont(vardef1); 312 | } 313 | // For loops. 314 | function forspec1(type){ 315 | if (type == "var") cont(vardef1, forspec2); 316 | else if (type == ";") pass(forspec2); 317 | else if (type == "variable") cont(formaybein); 318 | else pass(forspec2); 319 | } 320 | function formaybein(type, value){ 321 | if (value == "in") cont(expression); 322 | else cont(maybeoperator, forspec2); 323 | } 324 | function forspec2(type, value){ 325 | if (type == ";") cont(forspec3); 326 | else if (value == "in") cont(expression); 327 | else cont(expression, expect(";"), forspec3); 328 | } 329 | function forspec3(type) { 330 | if (type == ")") pass(); 331 | else cont(expression); 332 | } 333 | // A function definition creates a new context, and the variables 334 | // in its argument list have to be added to this context. 335 | function functiondef(type, value){ 336 | if (type == "variable"){register(value); cont(functiondef);} 337 | else if (type == "(") cont(pushcontext, commasep(funarg, ")"), statement, popcontext); 338 | } 339 | function funarg(type, value){ 340 | if (type == "variable"){register(value); cont();} 341 | } 342 | 343 | return parser; 344 | } 345 | 346 | return { 347 | make: parseJS, 348 | electricChars: "{}:", 349 | configure: function(obj) { 350 | if (obj.json != null) json = obj.json; 351 | } 352 | }; 353 | })(); 354 | -------------------------------------------------------------------------------- /root/static/codemirror/undo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Storage and control for undo information within a CodeMirror 3 | * editor. 'Why on earth is such a complicated mess required for 4 | * that?', I hear you ask. The goal, in implementing this, was to make 5 | * the complexity of storing and reverting undo information depend 6 | * only on the size of the edited or restored content, not on the size 7 | * of the whole document. This makes it necessary to use a kind of 8 | * 'diff' system, which, when applied to a DOM tree, causes some 9 | * complexity and hackery. 10 | * 11 | * In short, the editor 'touches' BR elements as it parses them, and 12 | * the UndoHistory stores these. When nothing is touched in commitDelay 13 | * milliseconds, the changes are committed: It goes over all touched 14 | * nodes, throws out the ones that did not change since last commit or 15 | * are no longer in the document, and assembles the rest into zero or 16 | * more 'chains' -- arrays of adjacent lines. Links back to these 17 | * chains are added to the BR nodes, while the chain that previously 18 | * spanned these nodes is added to the undo history. Undoing a change 19 | * means taking such a chain off the undo history, restoring its 20 | * content (text is saved per line) and linking it back into the 21 | * document. 22 | */ 23 | 24 | // A history object needs to know about the DOM container holding the 25 | // document, the maximum amount of undo levels it should store, the 26 | // delay (of no input) after which it commits a set of changes, and, 27 | // unfortunately, the 'parent' window -- a window that is not in 28 | // designMode, and on which setTimeout works in every browser. 29 | function UndoHistory(container, maxDepth, commitDelay, editor) { 30 | this.container = container; 31 | this.maxDepth = maxDepth; this.commitDelay = commitDelay; 32 | this.editor = editor; this.parent = editor.parent; 33 | // This line object represents the initial, empty editor. 34 | var initial = {text: "", from: null, to: null}; 35 | // As the borders between lines are represented by BR elements, the 36 | // start of the first line and the end of the last one are 37 | // represented by null. Since you can not store any properties 38 | // (links to line objects) in null, these properties are used in 39 | // those cases. 40 | this.first = initial; this.last = initial; 41 | // Similarly, a 'historyTouched' property is added to the BR in 42 | // front of lines that have already been touched, and 'firstTouched' 43 | // is used for the first line. 44 | this.firstTouched = false; 45 | // History is the set of committed changes, touched is the set of 46 | // nodes touched since the last commit. 47 | this.history = []; this.redoHistory = []; this.touched = []; 48 | } 49 | 50 | UndoHistory.prototype = { 51 | // Schedule a commit (if no other touches come in for commitDelay 52 | // milliseconds). 53 | scheduleCommit: function() { 54 | var self = this; 55 | this.parent.clearTimeout(this.commitTimeout); 56 | this.commitTimeout = this.parent.setTimeout(function(){self.tryCommit();}, this.commitDelay); 57 | }, 58 | 59 | // Mark a node as touched. Null is a valid argument. 60 | touch: function(node) { 61 | this.setTouched(node); 62 | this.scheduleCommit(); 63 | }, 64 | 65 | // Undo the last change. 66 | undo: function() { 67 | // Make sure pending changes have been committed. 68 | this.commit(); 69 | 70 | if (this.history.length) { 71 | // Take the top diff from the history, apply it, and store its 72 | // shadow in the redo history. 73 | var item = this.history.pop(); 74 | this.redoHistory.push(this.updateTo(item, "applyChain")); 75 | this.notifyEnvironment(); 76 | return this.chainNode(item); 77 | } 78 | }, 79 | 80 | // Redo the last undone change. 81 | redo: function() { 82 | this.commit(); 83 | if (this.redoHistory.length) { 84 | // The inverse of undo, basically. 85 | var item = this.redoHistory.pop(); 86 | this.addUndoLevel(this.updateTo(item, "applyChain")); 87 | this.notifyEnvironment(); 88 | return this.chainNode(item); 89 | } 90 | }, 91 | 92 | clear: function() { 93 | this.history = []; 94 | this.redoHistory = []; 95 | }, 96 | 97 | // Ask for the size of the un/redo histories. 98 | historySize: function() { 99 | return {undo: this.history.length, redo: this.redoHistory.length}; 100 | }, 101 | 102 | // Push a changeset into the document. 103 | push: function(from, to, lines) { 104 | var chain = []; 105 | for (var i = 0; i < lines.length; i++) { 106 | var end = (i == lines.length - 1) ? to : this.container.ownerDocument.createElement("BR"); 107 | chain.push({from: from, to: end, text: cleanText(lines[i])}); 108 | from = end; 109 | } 110 | this.pushChains([chain], from == null && to == null); 111 | this.notifyEnvironment(); 112 | }, 113 | 114 | pushChains: function(chains, doNotHighlight) { 115 | this.commit(doNotHighlight); 116 | this.addUndoLevel(this.updateTo(chains, "applyChain")); 117 | this.redoHistory = []; 118 | }, 119 | 120 | // Retrieve a DOM node from a chain (for scrolling to it after undo/redo). 121 | chainNode: function(chains) { 122 | for (var i = 0; i < chains.length; i++) { 123 | var start = chains[i][0], node = start && (start.from || start.to); 124 | if (node) return node; 125 | } 126 | }, 127 | 128 | // Clear the undo history, make the current document the start 129 | // position. 130 | reset: function() { 131 | this.history = []; this.redoHistory = []; 132 | }, 133 | 134 | textAfter: function(br) { 135 | return this.after(br).text; 136 | }, 137 | 138 | nodeAfter: function(br) { 139 | return this.after(br).to; 140 | }, 141 | 142 | nodeBefore: function(br) { 143 | return this.before(br).from; 144 | }, 145 | 146 | // Commit unless there are pending dirty nodes. 147 | tryCommit: function() { 148 | if (!window || !window.UndoHistory) return; // Stop when frame has been unloaded 149 | if (this.editor.highlightDirty()) this.commit(true); 150 | else this.scheduleCommit(); 151 | }, 152 | 153 | // Check whether the touched nodes hold any changes, if so, commit 154 | // them. 155 | commit: function(doNotHighlight) { 156 | this.parent.clearTimeout(this.commitTimeout); 157 | // Make sure there are no pending dirty nodes. 158 | if (!doNotHighlight) this.editor.highlightDirty(true); 159 | // Build set of chains. 160 | var chains = this.touchedChains(), self = this; 161 | 162 | if (chains.length) { 163 | this.addUndoLevel(this.updateTo(chains, "linkChain")); 164 | this.redoHistory = []; 165 | this.notifyEnvironment(); 166 | } 167 | }, 168 | 169 | // [ end of public interface ] 170 | 171 | // Update the document with a given set of chains, return its 172 | // shadow. updateFunc should be "applyChain" or "linkChain". In the 173 | // second case, the chains are taken to correspond the the current 174 | // document, and only the state of the line data is updated. In the 175 | // first case, the content of the chains is also pushed iinto the 176 | // document. 177 | updateTo: function(chains, updateFunc) { 178 | var shadows = [], dirty = []; 179 | for (var i = 0; i < chains.length; i++) { 180 | shadows.push(this.shadowChain(chains[i])); 181 | dirty.push(this[updateFunc](chains[i])); 182 | } 183 | if (updateFunc == "applyChain") 184 | this.notifyDirty(dirty); 185 | return shadows; 186 | }, 187 | 188 | // Notify the editor that some nodes have changed. 189 | notifyDirty: function(nodes) { 190 | forEach(nodes, method(this.editor, "addDirtyNode")) 191 | this.editor.scheduleHighlight(); 192 | }, 193 | 194 | notifyEnvironment: function() { 195 | if (this.onChange) this.onChange(); 196 | // Used by the line-wrapping line-numbering code. 197 | if (window.frameElement && window.frameElement.CodeMirror.updateNumbers) 198 | window.frameElement.CodeMirror.updateNumbers(); 199 | }, 200 | 201 | // Link a chain into the DOM nodes (or the first/last links for null 202 | // nodes). 203 | linkChain: function(chain) { 204 | for (var i = 0; i < chain.length; i++) { 205 | var line = chain[i]; 206 | if (line.from) line.from.historyAfter = line; 207 | else this.first = line; 208 | if (line.to) line.to.historyBefore = line; 209 | else this.last = line; 210 | } 211 | }, 212 | 213 | // Get the line object after/before a given node. 214 | after: function(node) { 215 | return node ? node.historyAfter : this.first; 216 | }, 217 | before: function(node) { 218 | return node ? node.historyBefore : this.last; 219 | }, 220 | 221 | // Mark a node as touched if it has not already been marked. 222 | setTouched: function(node) { 223 | if (node) { 224 | if (!node.historyTouched) { 225 | this.touched.push(node); 226 | node.historyTouched = true; 227 | } 228 | } 229 | else { 230 | this.firstTouched = true; 231 | } 232 | }, 233 | 234 | // Store a new set of undo info, throw away info if there is more of 235 | // it than allowed. 236 | addUndoLevel: function(diffs) { 237 | this.history.push(diffs); 238 | if (this.history.length > this.maxDepth) 239 | this.history.shift(); 240 | }, 241 | 242 | // Build chains from a set of touched nodes. 243 | touchedChains: function() { 244 | var self = this; 245 | 246 | // The temp system is a crummy hack to speed up determining 247 | // whether a (currently touched) node has a line object associated 248 | // with it. nullTemp is used to store the object for the first 249 | // line, other nodes get it stored in their historyTemp property. 250 | var nullTemp = null; 251 | function temp(node) {return node ? node.historyTemp : nullTemp;} 252 | function setTemp(node, line) { 253 | if (node) node.historyTemp = line; 254 | else nullTemp = line; 255 | } 256 | 257 | function buildLine(node) { 258 | var text = []; 259 | for (var cur = node ? node.nextSibling : self.container.firstChild; 260 | cur && !isBR(cur); cur = cur.nextSibling) 261 | if (cur.currentText) text.push(cur.currentText); 262 | return {from: node, to: cur, text: cleanText(text.join(""))}; 263 | } 264 | 265 | // Filter out unchanged lines and nodes that are no longer in the 266 | // document. Build up line objects for remaining nodes. 267 | var lines = []; 268 | if (self.firstTouched) self.touched.push(null); 269 | forEach(self.touched, function(node) { 270 | if (node && node.parentNode != self.container) return; 271 | 272 | if (node) node.historyTouched = false; 273 | else self.firstTouched = false; 274 | 275 | var line = buildLine(node), shadow = self.after(node); 276 | if (!shadow || shadow.text != line.text || shadow.to != line.to) { 277 | lines.push(line); 278 | setTemp(node, line); 279 | } 280 | }); 281 | 282 | // Get the BR element after/before the given node. 283 | function nextBR(node, dir) { 284 | var link = dir + "Sibling", search = node[link]; 285 | while (search && !isBR(search)) 286 | search = search[link]; 287 | return search; 288 | } 289 | 290 | // Assemble line objects into chains by scanning the DOM tree 291 | // around them. 292 | var chains = []; self.touched = []; 293 | forEach(lines, function(line) { 294 | // Note that this makes the loop skip line objects that have 295 | // been pulled into chains by lines before them. 296 | if (!temp(line.from)) return; 297 | 298 | var chain = [], curNode = line.from, safe = true; 299 | // Put any line objects (referred to by temp info) before this 300 | // one on the front of the array. 301 | while (true) { 302 | var curLine = temp(curNode); 303 | if (!curLine) { 304 | if (safe) break; 305 | else curLine = buildLine(curNode); 306 | } 307 | chain.unshift(curLine); 308 | setTemp(curNode, null); 309 | if (!curNode) break; 310 | safe = self.after(curNode); 311 | curNode = nextBR(curNode, "previous"); 312 | } 313 | curNode = line.to; safe = self.before(line.from); 314 | // Add lines after this one at end of array. 315 | while (true) { 316 | if (!curNode) break; 317 | var curLine = temp(curNode); 318 | if (!curLine) { 319 | if (safe) break; 320 | else curLine = buildLine(curNode); 321 | } 322 | chain.push(curLine); 323 | setTemp(curNode, null); 324 | safe = self.before(curNode); 325 | curNode = nextBR(curNode, "next"); 326 | } 327 | chains.push(chain); 328 | }); 329 | 330 | return chains; 331 | }, 332 | 333 | // Find the 'shadow' of a given chain by following the links in the 334 | // DOM nodes at its start and end. 335 | shadowChain: function(chain) { 336 | var shadows = [], next = this.after(chain[0].from), end = chain[chain.length - 1].to; 337 | while (true) { 338 | shadows.push(next); 339 | var nextNode = next.to; 340 | if (!nextNode || nextNode == end) 341 | break; 342 | else 343 | next = nextNode.historyAfter || this.before(end); 344 | // (The this.before(end) is a hack -- FF sometimes removes 345 | // properties from BR nodes, in which case the best we can hope 346 | // for is to not break.) 347 | } 348 | return shadows; 349 | }, 350 | 351 | // Update the DOM tree to contain the lines specified in a given 352 | // chain, link this chain into the DOM nodes. 353 | applyChain: function(chain) { 354 | // Some attempt is made to prevent the cursor from jumping 355 | // randomly when an undo or redo happens. It still behaves a bit 356 | // strange sometimes. 357 | var cursor = select.cursorPos(this.container, false), self = this; 358 | 359 | // Remove all nodes in the DOM tree between from and to (null for 360 | // start/end of container). 361 | function removeRange(from, to) { 362 | var pos = from ? from.nextSibling : self.container.firstChild; 363 | while (pos != to) { 364 | var temp = pos.nextSibling; 365 | removeElement(pos); 366 | pos = temp; 367 | } 368 | } 369 | 370 | var start = chain[0].from, end = chain[chain.length - 1].to; 371 | // Clear the space where this change has to be made. 372 | removeRange(start, end); 373 | 374 | // Insert the content specified by the chain into the DOM tree. 375 | for (var i = 0; i < chain.length; i++) { 376 | var line = chain[i]; 377 | // The start and end of the space are already correct, but BR 378 | // tags inside it have to be put back. 379 | if (i > 0) 380 | self.container.insertBefore(line.from, end); 381 | 382 | // Add the text. 383 | var node = makePartSpan(fixSpaces(line.text), this.container.ownerDocument); 384 | self.container.insertBefore(node, end); 385 | // See if the cursor was on this line. Put it back, adjusting 386 | // for changed line length, if it was. 387 | if (cursor && cursor.node == line.from) { 388 | var cursordiff = 0; 389 | var prev = this.after(line.from); 390 | if (prev && i == chain.length - 1) { 391 | // Only adjust if the cursor is after the unchanged part of 392 | // the line. 393 | for (var match = 0; match < cursor.offset && 394 | line.text.charAt(match) == prev.text.charAt(match); match++); 395 | if (cursor.offset > match) 396 | cursordiff = line.text.length - prev.text.length; 397 | } 398 | select.setCursorPos(this.container, {node: line.from, offset: Math.max(0, cursor.offset + cursordiff)}); 399 | } 400 | // Cursor was in removed line, this is last new line. 401 | else if (cursor && (i == chain.length - 1) && cursor.node && cursor.node.parentNode != this.container) { 402 | select.setCursorPos(this.container, {node: line.from, offset: line.text.length}); 403 | } 404 | } 405 | 406 | // Anchor the chain in the DOM tree. 407 | this.linkChain(chain); 408 | return start; 409 | } 410 | }; 411 | --------------------------------------------------------------------------------