Repository: google/CodeCity
Branch: master
Commit: fa1bd2734b80
Files: 780
Total size: 7.8 MB
Directory structure:
gitextract_2201ao9s/
├── .clang-format
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin/
│ ├── dump-core
│ ├── nginx-dev
│ └── startup.command
├── connect/
│ ├── connect.html
│ ├── connectServer
│ ├── log.html
│ └── world.html
├── core/
│ ├── README
│ ├── core_10_base.js
│ ├── core_11_$.utils.js
│ ├── core_12_$.utils.code.js
│ ├── core_13_$.Selector.js
│ ├── core_20_$.utils.html.js
│ ├── core_21_$.jssp.js
│ ├── core_22_$.connection.js
│ ├── core_23_$.servers.http.js
│ ├── core_24_$.hosts.js
│ ├── core_25_$.db.tempId.js
│ ├── core_25_$.userDatabase.js
│ ├── core_26_inline_editor.js
│ ├── core_27_editor.js
│ ├── core_28_$.servers.eval.js
│ ├── core_30_$.utils.command.js
│ ├── core_31_$.utils_world.js
│ ├── core_32_physical.js
│ ├── core_33_world.js
│ ├── core_34_$.servers.login.js
│ ├── core_34_$.servers.telnet.js
│ ├── core_40_$.startRoom.js
│ ├── core_41_deutsche_zimmer.js
│ ├── core_42_plant.js
│ ├── core_43_genetics_lab.js
│ ├── core_44_$.assistant.js
│ ├── core_45_Challenge_Room.js
│ ├── core_46_$.secuityCourse.js
│ ├── core_99_startup.js
│ └── dump_spec.json
├── database/
│ ├── README
│ └── codecity.cfg
├── docs/
│ └── setup.md
├── etc/
│ ├── apache.conf
│ ├── cc-localhost.conf
│ ├── cc-onedomain.conf
│ ├── cc-subdomain.conf
│ ├── codecity-connect.service
│ ├── codecity-login.service
│ ├── codecity-mobwrite.service
│ ├── codecity.service
│ └── gcloud-snapshot
├── login/
│ ├── login.html
│ ├── loginServer
│ └── package.json
├── minimal/
│ ├── core_01_minimal.js
│ ├── minimal.cfg
│ └── readme.txt
├── mobwrite/
│ ├── mobwrite.cfg
│ ├── mobwrite_core.py
│ ├── mobwrite_core_test.py
│ └── mobwrite_server.py
├── server/
│ ├── code.js
│ ├── codecity
│ ├── compile
│ ├── config.txt
│ ├── dump
│ ├── dumper.js
│ ├── externs/
│ │ ├── WeakRef.js
│ │ ├── buffer/
│ │ │ ├── buffer.js
│ │ │ └── package.json
│ │ ├── crypto/
│ │ │ ├── crypto.js
│ │ │ └── package.json
│ │ ├── events/
│ │ │ ├── events.js
│ │ │ └── package.json
│ │ ├── fs/
│ │ │ ├── fs.js
│ │ │ └── package.json
│ │ ├── http/
│ │ │ ├── http.js
│ │ │ └── package.json
│ │ ├── https/
│ │ │ ├── https.js
│ │ │ └── package.json
│ │ ├── net/
│ │ │ ├── net.js
│ │ │ └── package.json
│ │ ├── node.js
│ │ ├── path/
│ │ │ ├── package.json
│ │ │ └── path.js
│ │ ├── stream/
│ │ │ ├── package.json
│ │ │ └── stream.js
│ │ ├── tls/
│ │ │ ├── package.json
│ │ │ └── tls.js
│ │ └── util/
│ │ ├── package.json
│ │ └── util.js
│ ├── interpreter.js
│ ├── iterable_weakmap.js
│ ├── iterable_weakset.js
│ ├── package.json
│ ├── parser.js
│ ├── priorityqueue.js
│ ├── registry.js
│ ├── repl
│ ├── selector.js
│ ├── serialize.js
│ ├── startup/
│ │ ├── cc.js
│ │ ├── es5.js
│ │ ├── es6.js
│ │ ├── es7.js
│ │ ├── es8.js
│ │ └── esx.js
│ └── tests/
│ ├── code_test.js
│ ├── db/
│ │ ├── core_01_$.js
│ │ ├── core_01_$.system.js
│ │ ├── test.cfg
│ │ ├── test_00_start.js
│ │ ├── test_01_es5.js
│ │ ├── test_01_es6.js
│ │ ├── test_01_es7.js
│ │ ├── test_02_errors.js
│ │ ├── test_02_perms.js
│ │ ├── test_09_end.js
│ │ ├── test_10_fibonacci.js
│ │ └── test_20_reboot.js
│ ├── dump_test.js
│ ├── dumper_test.js
│ ├── interpreter_bench.js
│ ├── interpreter_common.js
│ ├── interpreter_test.js
│ ├── interpreter_unit_test.js
│ ├── iterable_weakmap_test.js
│ ├── iterable_weakset_test.js
│ ├── priorityqueue_test.js
│ ├── registry_test.js
│ ├── run
│ ├── run.js
│ ├── selector_test.js
│ ├── serialize_bench.js
│ ├── serialize_test.js
│ ├── testcases.js
│ ├── testing.js
│ └── tinycore/
│ ├── README
│ ├── core_00_es_minimal.js
│ ├── core_10_base.js
│ ├── core_13_$.utils.code.js
│ ├── core_35_$.servers.eval.js
│ ├── dump_spec.json
│ └── tiny.cfg
├── static/
│ ├── 503.html
│ ├── code/
│ │ ├── code.js
│ │ ├── common.js
│ │ ├── diff.css
│ │ ├── diff.js
│ │ ├── editor.css
│ │ ├── editor.js
│ │ ├── explorer.js
│ │ ├── mobwrite/
│ │ │ ├── demo/
│ │ │ │ ├── editor.html
│ │ │ │ ├── form.html
│ │ │ │ ├── mobwrite_form.js
│ │ │ │ └── test.html
│ │ │ ├── mobwrite_cc.js
│ │ │ └── mobwrite_core.js
│ │ ├── objectPanel.js
│ │ ├── style.css
│ │ ├── svg.css
│ │ ├── svg.js
│ │ └── tests/
│ │ ├── test.html
│ │ └── test.js
│ ├── connect/
│ │ ├── common.css
│ │ ├── common.js
│ │ ├── connect.css
│ │ ├── connect.js
│ │ ├── log.css
│ │ ├── log.js
│ │ ├── prettify.css
│ │ ├── prettify.js
│ │ ├── tests/
│ │ │ ├── test.html
│ │ │ └── test.js
│ │ ├── world.css
│ │ └── world.js
│ ├── flamethrower.html
│ ├── login-close.html
│ ├── securitystore/
│ │ ├── style.css
│ │ └── utils.js
│ └── style/
│ ├── jfk.css
│ └── svg.css
└── third_party/
├── CodeMirror/
│ ├── AUTHORS
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING.md
│ ├── LICENSE
│ ├── METADATA
│ ├── README.md
│ ├── addon/
│ │ ├── comment/
│ │ │ ├── comment.js
│ │ │ └── continuecomment.js
│ │ ├── dialog/
│ │ │ ├── dialog.css
│ │ │ └── dialog.js
│ │ ├── display/
│ │ │ ├── autorefresh.js
│ │ │ ├── fullscreen.css
│ │ │ ├── fullscreen.js
│ │ │ ├── panel.js
│ │ │ ├── placeholder.js
│ │ │ └── rulers.js
│ │ ├── edit/
│ │ │ ├── closebrackets.js
│ │ │ ├── closetag.js
│ │ │ ├── continuelist.js
│ │ │ ├── matchbrackets.js
│ │ │ ├── matchtags.js
│ │ │ └── trailingspace.js
│ │ ├── fold/
│ │ │ ├── brace-fold.js
│ │ │ ├── comment-fold.js
│ │ │ ├── foldcode.js
│ │ │ ├── foldgutter.css
│ │ │ ├── foldgutter.js
│ │ │ ├── indent-fold.js
│ │ │ ├── markdown-fold.js
│ │ │ └── xml-fold.js
│ │ ├── hint/
│ │ │ ├── anyword-hint.js
│ │ │ ├── css-hint.js
│ │ │ ├── html-hint.js
│ │ │ ├── javascript-hint.js
│ │ │ ├── show-hint.css
│ │ │ ├── show-hint.js
│ │ │ ├── sql-hint.js
│ │ │ └── xml-hint.js
│ │ ├── lint/
│ │ │ ├── coffeescript-lint.js
│ │ │ ├── css-lint.js
│ │ │ ├── html-lint.js
│ │ │ ├── javascript-lint.js
│ │ │ ├── json-lint.js
│ │ │ ├── lint.css
│ │ │ ├── lint.js
│ │ │ └── yaml-lint.js
│ │ ├── merge/
│ │ │ ├── merge.css
│ │ │ └── merge.js
│ │ ├── mode/
│ │ │ ├── loadmode.js
│ │ │ ├── multiplex.js
│ │ │ ├── multiplex_test.js
│ │ │ ├── overlay.js
│ │ │ └── simple.js
│ │ ├── runmode/
│ │ │ ├── colorize.js
│ │ │ ├── runmode-standalone.js
│ │ │ ├── runmode.js
│ │ │ └── runmode.node.js
│ │ ├── scroll/
│ │ │ ├── annotatescrollbar.js
│ │ │ ├── scrollpastend.js
│ │ │ ├── simplescrollbars.css
│ │ │ └── simplescrollbars.js
│ │ ├── search/
│ │ │ ├── jump-to-line.js
│ │ │ ├── match-highlighter.js
│ │ │ ├── matchesonscrollbar.css
│ │ │ ├── matchesonscrollbar.js
│ │ │ ├── search.js
│ │ │ └── searchcursor.js
│ │ ├── selection/
│ │ │ ├── active-line.js
│ │ │ ├── mark-selection.js
│ │ │ └── selection-pointer.js
│ │ ├── tern/
│ │ │ ├── tern.css
│ │ │ ├── tern.js
│ │ │ └── worker.js
│ │ └── wrap/
│ │ └── hardwrap.js
│ ├── bin/
│ │ ├── authors.sh
│ │ ├── lint
│ │ ├── release
│ │ ├── source-highlight
│ │ └── upload-release.js
│ ├── demo/
│ │ ├── activeline.html
│ │ ├── anywordhint.html
│ │ ├── bidi.html
│ │ ├── btree.html
│ │ ├── buffers.html
│ │ ├── changemode.html
│ │ ├── closebrackets.html
│ │ ├── closetag.html
│ │ ├── complete.html
│ │ ├── emacs.html
│ │ ├── folding.html
│ │ ├── fullscreen.html
│ │ ├── hardwrap.html
│ │ ├── html5complete.html
│ │ ├── indentwrap.html
│ │ ├── lint.html
│ │ ├── loadmode.html
│ │ ├── marker.html
│ │ ├── markselection.html
│ │ ├── matchhighlighter.html
│ │ ├── matchtags.html
│ │ ├── merge.html
│ │ ├── multiplex.html
│ │ ├── mustache.html
│ │ ├── panel.html
│ │ ├── placeholder.html
│ │ ├── preview.html
│ │ ├── requirejs.html
│ │ ├── resize.html
│ │ ├── rulers.html
│ │ ├── runmode.html
│ │ ├── search.html
│ │ ├── simplemode.html
│ │ ├── simplescrollbars.html
│ │ ├── spanaffectswrapping_shim.html
│ │ ├── sublime.html
│ │ ├── tern.html
│ │ ├── theme.html
│ │ ├── trailingspace.html
│ │ ├── variableheight.html
│ │ ├── vim.html
│ │ ├── visibletabs.html
│ │ ├── widget.html
│ │ └── xmlcomplete.html
│ ├── doc/
│ │ ├── activebookmark.js
│ │ ├── docs.css
│ │ ├── internals.html
│ │ ├── manual.html
│ │ ├── realworld.html
│ │ ├── releases.html
│ │ ├── reporting.html
│ │ ├── upgrade_v2.2.html
│ │ ├── upgrade_v3.html
│ │ └── upgrade_v4.html
│ ├── index.html
│ ├── keymap/
│ │ ├── emacs.js
│ │ ├── sublime.js
│ │ └── vim.js
│ ├── lib/
│ │ ├── codemirror.css
│ │ └── codemirror.js
│ ├── mode/
│ │ ├── apl/
│ │ │ ├── apl.js
│ │ │ └── index.html
│ │ ├── asciiarmor/
│ │ │ ├── asciiarmor.js
│ │ │ └── index.html
│ │ ├── asn.1/
│ │ │ ├── asn.1.js
│ │ │ └── index.html
│ │ ├── asterisk/
│ │ │ ├── asterisk.js
│ │ │ └── index.html
│ │ ├── brainfuck/
│ │ │ ├── brainfuck.js
│ │ │ └── index.html
│ │ ├── clike/
│ │ │ ├── clike.js
│ │ │ ├── index.html
│ │ │ ├── scala.html
│ │ │ └── test.js
│ │ ├── clojure/
│ │ │ ├── clojure.js
│ │ │ ├── index.html
│ │ │ └── test.js
│ │ ├── cmake/
│ │ │ ├── cmake.js
│ │ │ └── index.html
│ │ ├── cobol/
│ │ │ ├── cobol.js
│ │ │ └── index.html
│ │ ├── coffeescript/
│ │ │ ├── coffeescript.js
│ │ │ └── index.html
│ │ ├── commonlisp/
│ │ │ ├── commonlisp.js
│ │ │ └── index.html
│ │ ├── crystal/
│ │ │ ├── crystal.js
│ │ │ └── index.html
│ │ ├── css/
│ │ │ ├── css.js
│ │ │ ├── gss.html
│ │ │ ├── gss_test.js
│ │ │ ├── index.html
│ │ │ ├── less.html
│ │ │ ├── less_test.js
│ │ │ ├── scss.html
│ │ │ ├── scss_test.js
│ │ │ └── test.js
│ │ ├── cypher/
│ │ │ ├── cypher.js
│ │ │ ├── index.html
│ │ │ └── test.js
│ │ ├── d/
│ │ │ ├── d.js
│ │ │ ├── index.html
│ │ │ └── test.js
│ │ ├── dart/
│ │ │ ├── dart.js
│ │ │ └── index.html
│ │ ├── diff/
│ │ │ ├── diff.js
│ │ │ └── index.html
│ │ ├── django/
│ │ │ ├── django.js
│ │ │ └── index.html
│ │ ├── dockerfile/
│ │ │ ├── dockerfile.js
│ │ │ ├── index.html
│ │ │ └── test.js
│ │ ├── dtd/
│ │ │ ├── dtd.js
│ │ │ └── index.html
│ │ ├── dylan/
│ │ │ ├── dylan.js
│ │ │ ├── index.html
│ │ │ └── test.js
│ │ ├── ebnf/
│ │ │ ├── ebnf.js
│ │ │ └── index.html
│ │ ├── ecl/
│ │ │ ├── ecl.js
│ │ │ └── index.html
│ │ ├── eiffel/
│ │ │ ├── eiffel.js
│ │ │ └── index.html
│ │ ├── elm/
│ │ │ ├── elm.js
│ │ │ └── index.html
│ │ ├── erlang/
│ │ │ ├── erlang.js
│ │ │ └── index.html
│ │ ├── factor/
│ │ │ ├── factor.js
│ │ │ └── index.html
│ │ ├── fcl/
│ │ │ ├── fcl.js
│ │ │ └── index.html
│ │ ├── forth/
│ │ │ ├── forth.js
│ │ │ └── index.html
│ │ ├── fortran/
│ │ │ ├── fortran.js
│ │ │ └── index.html
│ │ ├── gas/
│ │ │ ├── gas.js
│ │ │ └── index.html
│ │ ├── gfm/
│ │ │ ├── gfm.js
│ │ │ ├── index.html
│ │ │ └── test.js
│ │ ├── gherkin/
│ │ │ ├── gherkin.js
│ │ │ └── index.html
│ │ ├── go/
│ │ │ ├── go.js
│ │ │ └── index.html
│ │ ├── groovy/
│ │ │ ├── groovy.js
│ │ │ └── index.html
│ │ ├── haml/
│ │ │ ├── haml.js
│ │ │ ├── index.html
│ │ │ └── test.js
│ │ ├── handlebars/
│ │ │ ├── handlebars.js
│ │ │ └── index.html
│ │ ├── haskell/
│ │ │ ├── haskell.js
│ │ │ └── index.html
│ │ ├── haskell-literate/
│ │ │ ├── haskell-literate.js
│ │ │ └── index.html
│ │ ├── haxe/
│ │ │ ├── haxe.js
│ │ │ └── index.html
│ │ ├── htmlembedded/
│ │ │ ├── htmlembedded.js
│ │ │ └── index.html
│ │ ├── htmlmixed/
│ │ │ ├── htmlmixed.js
│ │ │ └── index.html
│ │ ├── http/
│ │ │ ├── http.js
│ │ │ └── index.html
│ │ ├── idl/
│ │ │ ├── idl.js
│ │ │ └── index.html
│ │ ├── index.html
│ │ ├── javascript/
│ │ │ ├── index.html
│ │ │ ├── javascript.js
│ │ │ ├── json-ld.html
│ │ │ ├── test.js
│ │ │ └── typescript.html
│ │ ├── jinja2/
│ │ │ ├── index.html
│ │ │ └── jinja2.js
│ │ ├── jsx/
│ │ │ ├── index.html
│ │ │ ├── jsx.js
│ │ │ └── test.js
│ │ ├── julia/
│ │ │ ├── index.html
│ │ │ └── julia.js
│ │ ├── livescript/
│ │ │ ├── index.html
│ │ │ └── livescript.js
│ │ ├── lua/
│ │ │ ├── index.html
│ │ │ └── lua.js
│ │ ├── markdown/
│ │ │ ├── index.html
│ │ │ ├── markdown.js
│ │ │ └── test.js
│ │ ├── mathematica/
│ │ │ ├── index.html
│ │ │ └── mathematica.js
│ │ ├── mbox/
│ │ │ ├── index.html
│ │ │ └── mbox.js
│ │ ├── meta.js
│ │ ├── mirc/
│ │ │ ├── index.html
│ │ │ └── mirc.js
│ │ ├── mllike/
│ │ │ ├── index.html
│ │ │ └── mllike.js
│ │ ├── modelica/
│ │ │ ├── index.html
│ │ │ └── modelica.js
│ │ ├── mscgen/
│ │ │ ├── index.html
│ │ │ ├── mscgen.js
│ │ │ ├── mscgen_test.js
│ │ │ ├── msgenny_test.js
│ │ │ └── xu_test.js
│ │ ├── mumps/
│ │ │ ├── index.html
│ │ │ └── mumps.js
│ │ ├── nginx/
│ │ │ ├── index.html
│ │ │ └── nginx.js
│ │ ├── nsis/
│ │ │ ├── index.html
│ │ │ └── nsis.js
│ │ ├── ntriples/
│ │ │ ├── index.html
│ │ │ └── ntriples.js
│ │ ├── octave/
│ │ │ ├── index.html
│ │ │ └── octave.js
│ │ ├── oz/
│ │ │ ├── index.html
│ │ │ └── oz.js
│ │ ├── pascal/
│ │ │ ├── index.html
│ │ │ └── pascal.js
│ │ ├── pegjs/
│ │ │ ├── index.html
│ │ │ └── pegjs.js
│ │ ├── perl/
│ │ │ ├── index.html
│ │ │ └── perl.js
│ │ ├── php/
│ │ │ ├── index.html
│ │ │ ├── php.js
│ │ │ └── test.js
│ │ ├── pig/
│ │ │ ├── index.html
│ │ │ └── pig.js
│ │ ├── powershell/
│ │ │ ├── index.html
│ │ │ ├── powershell.js
│ │ │ └── test.js
│ │ ├── properties/
│ │ │ ├── index.html
│ │ │ └── properties.js
│ │ ├── protobuf/
│ │ │ ├── index.html
│ │ │ └── protobuf.js
│ │ ├── pug/
│ │ │ ├── index.html
│ │ │ └── pug.js
│ │ ├── puppet/
│ │ │ ├── index.html
│ │ │ └── puppet.js
│ │ ├── python/
│ │ │ ├── index.html
│ │ │ ├── python.js
│ │ │ └── test.js
│ │ ├── q/
│ │ │ ├── index.html
│ │ │ └── q.js
│ │ ├── r/
│ │ │ ├── index.html
│ │ │ └── r.js
│ │ ├── rpm/
│ │ │ ├── changes/
│ │ │ │ └── index.html
│ │ │ ├── index.html
│ │ │ └── rpm.js
│ │ ├── rst/
│ │ │ ├── index.html
│ │ │ └── rst.js
│ │ ├── ruby/
│ │ │ ├── index.html
│ │ │ ├── ruby.js
│ │ │ └── test.js
│ │ ├── rust/
│ │ │ ├── index.html
│ │ │ ├── rust.js
│ │ │ └── test.js
│ │ ├── sas/
│ │ │ ├── index.html
│ │ │ └── sas.js
│ │ ├── sass/
│ │ │ ├── index.html
│ │ │ ├── sass.js
│ │ │ └── test.js
│ │ ├── scheme/
│ │ │ ├── index.html
│ │ │ └── scheme.js
│ │ ├── shell/
│ │ │ ├── index.html
│ │ │ ├── shell.js
│ │ │ └── test.js
│ │ ├── sieve/
│ │ │ ├── index.html
│ │ │ └── sieve.js
│ │ ├── slim/
│ │ │ ├── index.html
│ │ │ ├── slim.js
│ │ │ └── test.js
│ │ ├── smalltalk/
│ │ │ ├── index.html
│ │ │ └── smalltalk.js
│ │ ├── smarty/
│ │ │ ├── index.html
│ │ │ └── smarty.js
│ │ ├── solr/
│ │ │ ├── index.html
│ │ │ └── solr.js
│ │ ├── soy/
│ │ │ ├── index.html
│ │ │ ├── soy.js
│ │ │ └── test.js
│ │ ├── sparql/
│ │ │ ├── index.html
│ │ │ └── sparql.js
│ │ ├── spreadsheet/
│ │ │ ├── index.html
│ │ │ └── spreadsheet.js
│ │ ├── sql/
│ │ │ ├── index.html
│ │ │ └── sql.js
│ │ ├── stex/
│ │ │ ├── index.html
│ │ │ ├── stex.js
│ │ │ └── test.js
│ │ ├── stylus/
│ │ │ ├── index.html
│ │ │ └── stylus.js
│ │ ├── swift/
│ │ │ ├── index.html
│ │ │ ├── swift.js
│ │ │ └── test.js
│ │ ├── tcl/
│ │ │ ├── index.html
│ │ │ └── tcl.js
│ │ ├── textile/
│ │ │ ├── index.html
│ │ │ ├── test.js
│ │ │ └── textile.js
│ │ ├── tiddlywiki/
│ │ │ ├── index.html
│ │ │ ├── tiddlywiki.css
│ │ │ └── tiddlywiki.js
│ │ ├── tiki/
│ │ │ ├── index.html
│ │ │ ├── tiki.css
│ │ │ └── tiki.js
│ │ ├── toml/
│ │ │ ├── index.html
│ │ │ └── toml.js
│ │ ├── tornado/
│ │ │ ├── index.html
│ │ │ └── tornado.js
│ │ ├── troff/
│ │ │ ├── index.html
│ │ │ └── troff.js
│ │ ├── ttcn/
│ │ │ ├── index.html
│ │ │ └── ttcn.js
│ │ ├── ttcn-cfg/
│ │ │ ├── index.html
│ │ │ └── ttcn-cfg.js
│ │ ├── turtle/
│ │ │ ├── index.html
│ │ │ └── turtle.js
│ │ ├── twig/
│ │ │ ├── index.html
│ │ │ └── twig.js
│ │ ├── vb/
│ │ │ ├── index.html
│ │ │ └── vb.js
│ │ ├── vbscript/
│ │ │ ├── index.html
│ │ │ └── vbscript.js
│ │ ├── velocity/
│ │ │ ├── index.html
│ │ │ └── velocity.js
│ │ ├── verilog/
│ │ │ ├── index.html
│ │ │ ├── test.js
│ │ │ └── verilog.js
│ │ ├── vhdl/
│ │ │ ├── index.html
│ │ │ └── vhdl.js
│ │ ├── vue/
│ │ │ ├── index.html
│ │ │ └── vue.js
│ │ ├── webidl/
│ │ │ ├── index.html
│ │ │ └── webidl.js
│ │ ├── xml/
│ │ │ ├── index.html
│ │ │ ├── test.js
│ │ │ └── xml.js
│ │ ├── xquery/
│ │ │ ├── index.html
│ │ │ ├── test.js
│ │ │ └── xquery.js
│ │ ├── yacas/
│ │ │ ├── index.html
│ │ │ └── yacas.js
│ │ ├── yaml/
│ │ │ ├── index.html
│ │ │ └── yaml.js
│ │ ├── yaml-frontmatter/
│ │ │ ├── index.html
│ │ │ └── yaml-frontmatter.js
│ │ └── z80/
│ │ ├── index.html
│ │ └── z80.js
│ ├── package.json
│ ├── rollup.config.js
│ ├── src/
│ │ ├── codemirror.js
│ │ ├── display/
│ │ │ ├── Display.js
│ │ │ ├── focus.js
│ │ │ ├── gutters.js
│ │ │ ├── highlight_worker.js
│ │ │ ├── line_numbers.js
│ │ │ ├── mode_state.js
│ │ │ ├── operations.js
│ │ │ ├── scroll_events.js
│ │ │ ├── scrollbars.js
│ │ │ ├── scrolling.js
│ │ │ ├── selection.js
│ │ │ ├── update_display.js
│ │ │ ├── update_line.js
│ │ │ ├── update_lines.js
│ │ │ └── view_tracking.js
│ │ ├── edit/
│ │ │ ├── CodeMirror.js
│ │ │ ├── commands.js
│ │ │ ├── deleteNearSelection.js
│ │ │ ├── drop_events.js
│ │ │ ├── fromTextArea.js
│ │ │ ├── global_events.js
│ │ │ ├── key_events.js
│ │ │ ├── legacy.js
│ │ │ ├── main.js
│ │ │ ├── methods.js
│ │ │ ├── mouse_events.js
│ │ │ ├── options.js
│ │ │ └── utils.js
│ │ ├── input/
│ │ │ ├── ContentEditableInput.js
│ │ │ ├── TextareaInput.js
│ │ │ ├── indent.js
│ │ │ ├── input.js
│ │ │ ├── keymap.js
│ │ │ ├── keynames.js
│ │ │ └── movement.js
│ │ ├── line/
│ │ │ ├── highlight.js
│ │ │ ├── line_data.js
│ │ │ ├── pos.js
│ │ │ ├── saw_special_spans.js
│ │ │ ├── spans.js
│ │ │ └── utils_line.js
│ │ ├── measurement/
│ │ │ ├── position_measurement.js
│ │ │ └── widgets.js
│ │ ├── model/
│ │ │ ├── Doc.js
│ │ │ ├── change_measurement.js
│ │ │ ├── changes.js
│ │ │ ├── chunk.js
│ │ │ ├── document_data.js
│ │ │ ├── history.js
│ │ │ ├── line_widget.js
│ │ │ ├── mark_text.js
│ │ │ ├── selection.js
│ │ │ └── selection_updates.js
│ │ ├── modes.js
│ │ └── util/
│ │ ├── StringStream.js
│ │ ├── bidi.js
│ │ ├── browser.js
│ │ ├── dom.js
│ │ ├── event.js
│ │ ├── feature_detection.js
│ │ ├── misc.js
│ │ └── operation_group.js
│ ├── test/
│ │ ├── comment_test.js
│ │ ├── contenteditable_test.js
│ │ ├── doc_test.js
│ │ ├── driver.js
│ │ ├── emacs_test.js
│ │ ├── html-hint-test.js
│ │ ├── index.html
│ │ ├── lint.js
│ │ ├── mode_test.css
│ │ ├── mode_test.js
│ │ ├── multi_test.js
│ │ ├── phantom_driver.js
│ │ ├── run.js
│ │ ├── scroll_test.js
│ │ ├── search_test.js
│ │ ├── sql-hint-test.js
│ │ ├── sublime_test.js
│ │ ├── test.js
│ │ └── vim_test.js
│ └── theme/
│ ├── 3024-day.css
│ ├── 3024-night.css
│ ├── abcdef.css
│ ├── ambiance-mobile.css
│ ├── ambiance.css
│ ├── base16-dark.css
│ ├── base16-light.css
│ ├── bespin.css
│ ├── blackboard.css
│ ├── cobalt.css
│ ├── colorforth.css
│ ├── darcula.css
│ ├── dracula.css
│ ├── duotone-dark.css
│ ├── duotone-light.css
│ ├── eclipse.css
│ ├── elegant.css
│ ├── erlang-dark.css
│ ├── gruvbox-dark.css
│ ├── hopscotch.css
│ ├── icecoder.css
│ ├── idea.css
│ ├── isotope.css
│ ├── lesser-dark.css
│ ├── liquibyte.css
│ ├── lucario.css
│ ├── material.css
│ ├── mbo.css
│ ├── mdn-like.css
│ ├── midnight.css
│ ├── monokai.css
│ ├── neat.css
│ ├── neo.css
│ ├── night.css
│ ├── oceanic-next.css
│ ├── panda-syntax.css
│ ├── paraiso-dark.css
│ ├── paraiso-light.css
│ ├── pastel-on-dark.css
│ ├── railscasts.css
│ ├── rubyblue.css
│ ├── seti.css
│ ├── shadowfox.css
│ ├── solarized.css
│ ├── ssms.css
│ ├── the-matrix.css
│ ├── tomorrow-night-bright.css
│ ├── tomorrow-night-eighties.css
│ ├── ttcn.css
│ ├── twilight.css
│ ├── vibrant-ink.css
│ ├── xq-dark.css
│ ├── xq-light.css
│ ├── yeti.css
│ └── zenburn.css
├── DMP/
│ ├── AUTHORS
│ ├── LICENSE
│ ├── METADATA
│ ├── diff_match_patch.py
│ └── diff_match_patch_uncompressed.js
├── JSHint/
│ ├── LICENSE
│ ├── README.md
│ └── jshint.js
└── SVG-Edit/
├── AUTHORS
├── LICENSE
├── METADATA
├── README.md
└── editor/
├── browser.js
├── canvg/
│ ├── canvg.js
│ └── rgbcolor.js
├── coords.js
├── draw.js
├── external/
│ └── dynamic-import-polyfill/
│ └── importModule.js
├── history.js
├── historyrecording.js
├── jquery-svg.js
├── layer.js
├── math.js
├── path.js
├── pathseg.js
├── recalculate.js
├── sanitize.js
├── select.js
├── svgcanvas.js
├── svgedit.js
├── svgtransformlist.js
├── svgutils.js
└── units.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .clang-format
================================================
Language: JavaScript
BasedOnStyle: Google
ColumnLimit: 80
================================================
FILE: .gitignore
================================================
.DS_Store
nohup.out
*~
login/loginServer.cfg
login/node_modules
connect/connectServer.cfg
connect/node_modules
server/node_modules
var/
*.city
*.city.partial
*.pyc
================================================
FILE: CONTRIBUTING.md
================================================
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Community Guidelines
This project follows [Google's Open Source Community
Guidelines](https://opensource.google.com/conduct/).
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2011
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
================================================
FILE: README.md
================================================
# Code City
Google's Code City is a social programming environment designed mainly for
education. It offers a comic book inspired virtual world where programmers can
write code collaboratively.
A list of running Code City instances may be found at https://codecity.world/
================================================
FILE: bin/dump-core
================================================
#!/bin/bash
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Create configuration file for and then start nginx, for use as a
# local development server. nginx is configured to run in the
# foreground, and to store all runtime data in /var,
# rather than in /var/ or /usr/local/var/ as it might normally.
set -e
# Get top level directory of the CodeCity git repository. It's the
# parent directory of the directory containing this script.
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. >/dev/null 2>&1 && pwd)"
cd "${repo}"
mkdir -p var/dump
# Make way for dump. Ignore errors.
rm var/dump/*.js || true
rm core/core_[1-8]*.js || true
rm database/core_*.js || true
rm database/db_*.js || true
# Get last .city file.
city=$( (cd database && ls -1 *.city |sort |tail -1) )
# Run dump.
server/dump "database/${city}" core/dump_spec.json core
# Link core file into database, except core_99_startup.js
(cd database && ln -s ../core/core_[0-8]*.js .)
================================================
FILE: bin/nginx-dev
================================================
#!/bin/bash
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Create configuration files for and then start nginx, for use as a
# local development server. nginx is configured to run in the
# foreground, and to store all runtime data in /var,
# rather than in /var/ or /usr/local/var/ as it might normally.
#
# This is done by generating var/nginx-dev.conf, which will include
# var/cc-localhost.conf, which is copied (with edits) from
# etc/cc-localhost.conf, then starting nginx specifying
# var/nginx-dev.conf as the config file to use.
set -e
# Get top level directory of the CodeCity git repository. It's the
# parent directory of the directory containing this script.
readonly repo="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. >/dev/null 2>&1 && pwd)"
# Create var directory if required
if [[ ! -d "${repo}/var" ]] ; then
if [[ -x "${repo}/var" ]] ; then
echo "${repo}/var already exists and is not a directory" 1>&2
exit 1
fi
mkdir "${repo}/var"
fi
# Find location where the nginx installation keeps its config files.
# We only want the mime.types file.
dirs=(/usr/local/etc /etc/opt /etc)
if type brew >/dev/null 2>&1 ; then # Homebrew in path?
dirs=("$(brew --prefix)/etc" "${dirs[@]}")
fi
for dir in "${dirs[@]}"; do
if [[ -d "${dir}" && -f "${dir}/nginx/mime.types" ]] ; then
readonly mimetypes="${dir}/nginx/mime.types"
break;
fi
done
if [[ -z "${mimetypes}" ]] ; then
echo "$0: can't find mime.types file." 1>&2
exit 1;
fi
cat >"$repo/var/nginx-dev.conf" < ${repo}/var/cc-localhost.conf
nginx -c "$repo/var/nginx-dev.conf"
================================================
FILE: bin/startup.command
================================================
#!/usr/bin/osascript
# Script to open four terminal windows that execute all the Code City servers
# with one double-click. For OSX.
tell application "Finder"
set basePath to (POSIX path of (container of (container of (path to me)) as alias))
end tell
tell app "Terminal"
do script "cd " & basePath & "/login
./loginServer"
do script "cd " & basePath & "/connect
./connectServer"
do script "cd " & basePath & "/mobwrite
python2 mobwrite_server.py"
do script "cd " & basePath & "/server
./codecity " & basePath & "/database/codecity.cfg"
end tell
================================================
FILE: connect/connect.html
================================================
Code City
World
Log
Pause scroll
Clear buffer
Loading...
================================================
FILE: connect/connectServer
================================================
#!/usr/bin/env node
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Node.js server that provides connection services to Code City.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
var crypto = require('crypto');
var fs = require('fs');
var http = require('http');
var net = require('net');
// Configuration constants.
const configFileName = 'connectServer.cfg';
// Global variables.
var CFG = null;
var queueList = Object.create(null);
const DEFAULT_CFG = {
// Internal port for this HTTP server. Nginx hides this from users.
httpPort: 7782,
// URL of login page (absolute or relative).
loginUrl: 'https://login.example.codecity.world/',
// URL of static folder (absolute or relative).
staticUrl: 'https://static.example.codecity.world/',
// Host of Code City.
remoteHost: 'localhost',
// Port of Code City.
remotePort: 7777,
// Age in seconds of abandoned queues to be closed.
connectionTimeout: 300
};
/**
* Class for one user's connection to Code City.
* Establishes a connection and buffers the text coming from Code City.
* @param {string} id ID of this queue.
* @constructor
*/
var Queue = function (id) {
// Save 'this' for closures below.
var thisQueue = this;
/**
* ID of this queue in queueList.
*/
this.id = id;
/**
* Time that this queue was pinged by a user. Abandoned queues are deleted.
*/
this.lastPingTime = Date.now();
/**
* The index number of the most recent memo added to the memo buffer.
*/
this.memoNum = 0;
/**
* Buffer of incomplete data from Code City.
* If undefined, drop all input until next linefeed.
*/
this.lineBuffer = '';
/**
* Maximum allowed length (in bytes) of a single memo.
*/
this.maxLineSize = 10 * 1024 * 1024;
/**
* Buffer of memos from Code City to the user.
*/
this.memoBuffer = [];
/**
* The index number of the most recent command received from the user.
*/
this.commandNum = 0;
/**
* Persistent TCP connection to Code City.
*/
this.client = new net.Socket();
this.client.on('close', this.destroy.bind(this, 'Code City closed session'));
this.client.on('error', function(error) {
console.log('TCP error for session ' + id, error);
});
this.client.on('data', function(data) {
function drop(overflow) {
console.log('Session ' + id + ' drops ' + overflow.length +
' bytes of data.');
return '{"type":"narrate","text":"[OVERFLOW: ' + overflow.length +
' bytes lost.]"}\n';
}
var text = data.toString();
if (!text) {
return;
}
if (thisQueue.lineBuffer) {
text = thisQueue.lineBuffer + text;
}
// Split into lines, while preserving the linebreaks.
var lines = text.split(/^/m);
if (thisQueue.lineBuffer === undefined) {
// Throw away continued oversized line.
var incompleteLine = lines.shift();
lines.unshift(drop(incompleteLine));
if (incompleteLine.endsWith('\n')) {
// Found the end of the oversized line. Reset the buffer.
thisQueue.lineBuffer = '';
}
}
for (var i = 0; i < lines.length; i++) {
if (lines[i].endsWith('\n')) {
if (lines[i].length > thisQueue.maxLineSize) {
// Line is complete, but oversized. Drop.
lines[i] = drop(lines[i]);
}
thisQueue.memoBuffer.push(lines[i]);
thisQueue.memoNum++;
} else {
// Incomplete line.
if (lines[i].length > thisQueue.maxLineSize) {
// Discard, and throw away everything till next linebreak.
thisQueue.memoBuffer.push(drop(lines[i]));
thisQueue.memoNum++;
thisQueue.lineBuffer = undefined;
} else {
// Save this line to the buffer so it may be completed next time.
thisQueue.lineBuffer = lines[i];
}
}
}
});
this.client.connect(CFG.remotePort, CFG.remoteHost);
};
/**
* Close this queue and deregister it.
* @param {string} msg Console message to print (with ID appended).
*/
Queue.prototype.destroy = function(msg) {
if (queueList[this.id]) {
delete queueList[this.id];
}
this.client.end();
console.log(msg + ' ' + this.id);
};
/**
* Load a file from disk, add substitutions, and serve to the web.
* @param {!Object} response HTTP server response object.
* @param {string} filename Name of template file on disk.
* @param {!Object} subs Hash of replacement strings.
*/
function serveFile(response, filename, subs) {
fs.readFile(filename, 'utf8', function(err, data) {
if (err) {
response.statusCode = 500;
console.log(err);
response.end('Unable to load file: ' + filename + '\n' + err);
}
// Inject substitutions.
for (var name in subs) {
data = data.replace(new RegExp(name, 'g'), subs[name]);
}
// Serve page to user.
response.statusCode = 200;
response.setHeader('Content-Type', 'text/html');
response.end(data);
});
}
/**
* Handles HTTP requests from web server.
* @param {!Object} request HTTP server request object
* @param {!Object} response HTTP server response object.
*/
function handleRequest(request, response) {
if (request.connection.remoteAddress !== '127.0.0.1') {
// This check is redundant, the server is only accessible to
// localhost connections.
console.log('Rejecting connection from ' + request.connection.remoteAddress);
response.end('Connection rejected.');
return;
}
var path = request.url.split('?')[0]; // Strip off any parameters.
if (request.method === 'GET' && path.endsWith('/log')) {
serveFile(response, 'log.html', {'<<>>': CFG.staticUrl});
return;
}
if (request.method === 'GET' && path.endsWith('/world')) {
serveFile(response, 'world.html', {'<<>>': CFG.staticUrl});
return;
}
if (request.method === 'GET' && path.endsWith('/')) {
var cookieList = {};
var rhc = request.headers.cookie;
rhc && rhc.split(';').forEach(function(cookie) {
var parts = cookie.split('=');
cookieList[parts.shift().trim()] = decodeURI(parts.join('='));
});
// Validate the ID to ensure there was no tampering.
var m = cookieList.ID && cookieList.ID.match(/^[0-9a-f]+$/);
if (!m) {
console.log('Missing login cookie. Redirecting.');
response.writeHead(302, { // Temporary redirect.
'Location': CFG.loginUrl
});
response.end('Login required. Redirecting.');
return;
}
var seed = (Date.now() * Math.random()).toString() + cookieList.ID;
// This ID gets transmitted a *lot* so keep it short.
var sessionId = crypto.createHash('sha3-224').update(seed).digest('base64');
if (Object.keys(queueList).length > 1000) {
response.statusCode = 429;
response.end('Too many queues open at once.');
console.log('Too many queues open at once.');
return;
}
var queue = new Queue(sessionId);
queueList[sessionId] = queue;
// Start a connection.
queue.client.write('identify as ' + cookieList.ID + '\n');
var subs = {
'<<>>': sessionId,
'<<>>': CFG.staticUrl
};
serveFile(response, 'connect.html', subs);
console.log('Hello xxxx' + cookieList.ID.substring(cookieList.ID.length - 4) +
', starting session ' + sessionId);
return;
}
if (request.method === 'POST' && path.endsWith('/ping')) {
var requestBody = '';
request.on('data', function(data) {
requestBody += data;
if (requestBody.length > 1000000) { // Megabyte of commands?
console.error('Oversized JSON: ' + requestBody.length / 1024 + 'kb');
response.statusCode = 413;
response.end('Request Entity Too Large');
}
});
request.on('end', function() {
// No ID cookie, the user has logged out.
if (!/(^|;)\s*ID=\w/.test(request.headers.cookie)) {
console.error('Not logged in');
response.statusCode = 410;
response.end('Not logged in');
return;
}
try {
var receivedJson = JSON.parse(requestBody);
if (!receivedJson['q']) {
throw Error('No queue');
}
} catch (e) {
console.error('Illegal JSON');
response.statusCode = 412;
response.end('Illegal JSON');
return;
}
ping(receivedJson, response);
});
return;
}
response.statusCode = 404;
response.end('Unknown connectServer URL: ' + request.url);
}
function ping(receivedJson, response) {
var q = receivedJson['q'];
var ackMemoNum = receivedJson['ackMemoNum'];
var cmdNum = receivedJson['cmdNum'];
var cmds = receivedJson['cmds'];
var logout = receivedJson['logout'];
var queue = queueList[q];
if (!queue) {
console.log('Unknown session ' + q);
response.statusCode = 410;
response.end('Your session has timed out');
return;
}
queue.lastPingTime = Date.now();
if (typeof ackMemoNum === 'number') {
if (ackMemoNum > queue.memoNum) {
var msg = 'Client ' + q + ' ackMemoNum ' + ackMemoNum +
', but queue.memoNum is only ' + queue.memoNum;
console.error(msg);
response.statusCode = 412;
response.end(msg);
return;
}
// Client acknowledges receipt of memos.
// Remove them from the output list.
queue.memoBuffer.splice(0,
queue.memoBuffer.length + ackMemoNum - queue.memoNum);
}
var delay = 0;
if (typeof cmdNum === 'number') {
// Client sent commands. Increase server's index for acknowledgment.
var currentIndex = cmdNum - cmds.length + 1;
for (var i = 0; i < cmds.length; i++) {
if (currentIndex > queue.commandNum) {
queue.commandNum = currentIndex;
// Send commands to Code City.
queue.client.write(cmds[i]);
delay += 200;
}
currentIndex++;
}
var ackCmdNextPing = true;
} else {
var ackCmdNextPing = false;
}
if (logout) {
pong(queue, response, ackCmdNextPing);
queue.destroy('Client disconnected');
} else {
// Wait a fifth of a second for each command,
// but don't wait for more than a second.
var delay = Math.min(delay, 1000);
var replyFunc = pong.bind(null, queue, response, ackCmdNextPing);
setTimeout(replyFunc, delay);
}
}
function pong(queue, response, ackCmdNextPing) {
var sendingJson = {};
if (ackCmdNextPing) {
sendingJson['ackCmdNum'] = queue.commandNum;
}
if (queue.memoBuffer.length) {
sendingJson['memoNum'] = queue.memoNum;
sendingJson['memos'] = queue.memoBuffer;
}
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify(sendingJson));
}
/**
* Read the JSON configuration file and return it. If none is
* present, write a stub and throw an error.
*/
function readConfigFile(filename) {
let data;
try {
data = fs.readFileSync(filename, 'utf8');
} catch (err) {
console.log(`Configuration file ${filename} not found. ` +
'Creating new file.');
data = JSON.stringify(DEFAULT_CFG, null, 2) + '\n';
fs.writeFileSync(filename, data, 'utf8');
}
CFG = JSON.parse(data);
if (!CFG.loginUrl || CFG.loginUrl === DEFAULT_CFG.loginUrl) {
throw Error(
`Configuration file ${filename} not configured. ` +
'Please edit this file.');
}
if (!CFG.loginUrl.endsWith('/')) CFG.loginUrl += '/';
if (!CFG.staticUrl.endsWith('/')) CFG.staticUrl += '/';
}
/**
* Close and destroy any abandoned queues. Called every minute.
*/
function cleanup() {
var bestBefore = Date.now() - CFG.connectionTimeout * 1000;
for (var id in queueList) {
var queue = queueList[id];
if (queue.lastPingTime < bestBefore) {
queue.destroy('Timeout of session');
}
}
}
/**
* Start up the HTTP server.
*/
function startup() {
readConfigFile(configFileName);
var server = http.createServer(handleRequest);
server.listen(CFG.httpPort, 'localhost', function(){
console.log('Connection server listening on port ' + CFG.httpPort);
});
setInterval(cleanup, 60 * 1000);
}
startup();
================================================
FILE: connect/log.html
================================================
Code City: Log frame
Connected.Disconnected.Reconnect?You see %1 here.You see %1 here.%1 is here.%1 are here.Someone%1 says, "%2"You say, "%1"%1 asks, "%2"You ask, "%1"%1 exclaims, "%2"You exclaim, "%1"%1 thinks, "%2"You think, "%1"and
================================================
FILE: connect/world.html
================================================
Code City: World frame
================================================
FILE: core/README
================================================
This database contains the latest release of the official Code City
Core. Normally, when creating a new instance, the database should be
initialised by reading all these files in, in order, into the new
server. This will normally be accomplished by symlinking them into
the /database/ directory, making sure that no .city files are present
in that directory, then starting the server using
database/codecity.cfg.
The following naming convention has been established to keep things
organised:
core_0?_*.js - ES5.1 (and later) polyfills / JS base language stuff.
core_1?_*.js - Base structure & utilities ($, $.utils, etc.)
core_2?_*.js - Web servers, editors, etc.
core_3?_*.js - Physical world infrastructure, telnet server, etc.
core_4?_*.js - Start room, demos, etc.
test_??_*.js - Any tests to be run against the database.
================================================
FILE: core/core_10_base.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Database core for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
var perms = new 'perms';
var setPerms = new 'setPerms';
var $ = function $(selector) {
return new $.Selector(selector).toValue(/*save:*/ true);
};
$.root = new 'CC.root';
$.root.name = 'root';
$.root.toString = function toString() {
return 'root';
};
$.physicals = (new 'Object.create')(null);
$.physicals.Maximilian = {};
$.physicals.Neil = {};
$.system = {};
$.system.log = new 'CC.log';
$.system.checkpoint = new 'CC.checkpoint';
$.system.shutdown = new 'CC.shutdown';
$.system.connectionListen = new 'CC.connectionListen';
$.system.connectionUnlisten = new 'CC.connectionUnlisten';
$.system.connectionWrite = new 'CC.connectionWrite';
$.system.connectionClose = new 'CC.connectionClose';
$.system.xhr = new 'CC.xhr';
$.system.onStartup = function onStartup() {
/* Do things needed at database start, when starting from a .js dump
* rather than from a .city snapshot (which preserves threads,
* listening sockets, etc.)
*/
// Listen on various sockets.
try {$.system.connectionListen(7776, $.servers.login.connection, 100);} catch(e) {}
try {$.system.connectionListen(7777, $.servers.telnet.connection, 100);} catch(e) {}
try {$.system.connectionListen(7780, $.servers.http.connection, 100);} catch(e) {}
try {$.system.connectionListen(9999, $.servers.eval.connection);} catch(e) {}
$.system.log('Startup: listeners started.');
// Restart timers and clear auto-expring caches.
$.clock.validate();
$.db.tempId.cleanNow();
suspend();
$.system.log('Startup: timers restarted and caches cleared.');
// Rebuild Selector reverse-lookup database, which is not presently
// preserved in the dump as it is a WeakMap.
$.Selector.db.populate();
$.system.log('Startup: Selector reverse-lookup DB rebuilt.');
};
Object.setOwnerOf($.system.onStartup, $.physicals.Neil);
Object.setOwnerOf($.system.onStartup.prototype, $.physicals.Maximilian);
var user = function user() {
/* The global user() is intended to be used to find the current
* user object from deeply-nested functions (to which it is
* impractical to thread cmd.user, for whatever reason).
*
* Previously user was a global variable set to the current user
* object by $.servers.telnet.connection.onReceiveLine, but this
* can cause problems when one command's execution suspends and
* another user's command runs in mean time.
*
* It is preferable to avoid using this function; instead, use
* cmd.user or this where possible.
*/
$.system.log('Auditing user() usage:\n' + (new Error()).stack);
return Object.getOwnerOf(Thread.current());
};
$.utils = {};
$.utils.validate = {};
$.servers = {};
================================================
FILE: core/core_11_$.utils.js
================================================
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Basic utilities for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.utils.validate.ownArray = function ownArray(object, key) {
// Ensure that object[key] is an array not shared with any other
// object or property, not inherited from a prototype, etc.
// If it is, relaced it with a new, unshared array with the same
// contents (if possible).
if (!object.hasOwnProperty(key) || !Array.isArray(object[key]) ||
object[key].forObj !== object || object[key].forKey !== key) {
try {
object[key] = Array.from(object[key]);
} catch (e) {
object[key] = [];
}
Object.defineProperties(object[key], {forObj: {value: object},
forKey: {value: key}});
}
};
Object.setOwnerOf($.utils.validate.ownArray, $.physicals.Maximilian);
$.utils.validate.functionPrototypes = function functionPrototypes() {
/* Find (and fix) functions that have f.prototype.constructor !== f.
*/
var u = user();
u.narrate('Looking for functions with mismatched .prototype.constructor...');
$.utils.object.spider($, function findProtosHelper(object, path) {
// Skip $.archive entirely.
if (object === $.archive) return true;
if (typeof object !== 'function') return false;
var selector = $.Selector.for(object) || new $.Selector(['$'].concat(path));
if (!object.prototype) {
if (!String(object).includes('[native code]')) {
u.narrate(String(selector) + ' has no .prototype');
}
} else if (!object.prototype.constructor) {
u.narrate(String(selector) + ' has no .prototype.constructor');
} else if (object.prototype.constructor !== object) {
u.narrate(String(selector) + ' has mismatched .prototype.constructor');
var protoProps = Object.getOwnPropertyNames(object.prototype);
var pcSelector = $.Selector.for(object.prototype.constructor);
// Does it look like a plain old boring auto-created .prototype object?
var pd = Object.getOwnPropertyDescriptor(object.prototype, 'constructor');
if (Object.getPrototypeOf(object.prototype) === Object.prototype &&
protoProps.length === 1 && protoProps[0] === 'constructor' &&
pd.writable === true && pd.enumerable === false && pd.configurable === true ) {
if (String(pcSelector) === String(selector) + '.prototype.constructor') {
u.narrate('----Fixable?: yes!');
object.prototype.constructor = object;
} else {
u.narrate('----Fixable?: yes🤞 (is ' + String(pcSelector) + ')');
// Make new .prototype object, since current one is likely shared.
var newProto = {constructor: object};
Object.setOwnerOf(newProto, Object.getOwnerOf(object));
Object.defineProperty(newProto, 'constructor', {enumerable: false});
object.prototype = newProto;
}
} else {
u.narrate('----Fixable?: NO: has properties other than .constructor' +
(pcSelector ? ' (is ' + String(pcSelector) + ')' : ''));
}
}
return false;
});
u.narrate('Done.');
};
Object.setOwnerOf($.utils.validate.functionPrototypes, $.physicals.Maximilian);
Object.setOwnerOf($.utils.validate.functionPrototypes.prototype, $.physicals.Maximilian);
$.utils.isObject = function isObject(v) {
/* Returns true iff v is an object (of any class, including Array
* and Function). */
return (typeof v === 'object' && v !== null) || typeof v === 'function';
};
Object.setOwnerOf($.utils.isObject, $.physicals.Maximilian);
$.utils.imageMatch = {};
$.utils.imageMatch.recog = function recog(svgText) {
svgText = '';
var json = $.system.xhr('https://neil.fraser.name/scripts/imageMatch.py' +
'?svg=' + encodeURIComponent(svgText));
return JSON.parse(json);
};
Object.setOwnerOf($.utils.imageMatch.recog, $.physicals.Neil);
Object.setOwnerOf($.utils.imageMatch.recog.prototype, $.physicals.Neil);
$.utils.regexp = {};
Object.setOwnerOf($.utils.regexp, $.physicals.Neil);
$.utils.regexp.escape = function escape(str) {
// Escape a string so that it may be used as a literal in a regular expression.
// Example: $.utils.regexp.escape('[...]') -> "\\[\\.\\.\\.\\]"
// Usecase: new RegExp($.utils.regexp.escape('[...]')).test('Alpha [...] Beta')
//
// Source: https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
};
Object.setOwnerOf($.utils.regexp.escape, $.physicals.Neil);
Object.setOwnerOf($.utils.regexp.escape.prototype, $.physicals.Neil);
$.utils.array = {};
$.utils.array.filterUntilFound = function filterUntilFound(array, filter1 /*, filter2, filter3... */) {
// Apply Array.prototype.filter.call(array, filterN) for each filter
// in turn until one returns a non-empty result. Return that
// result, or an empty array if there are no more filters.
filters = Array.from(arguments).slice(1);
while (filters.length > 0) {
var filter = filters.shift();
var result = array.filter(filter);
if (result.length > 0) return result;
}
return [];
};
Object.setOwnerOf($.utils.array.filterUntilFound, $.physicals.Maximilian);
$.utils.object = {};
Object.setOwnerOf($.utils.object, $.physicals.Maximilian);
$.utils.object.spider = function spider(start, callback) {
/* Spider the objects accessible transitively via the properties of
* object.
*
* Arguments:
* start: object: Starting point for traversal of the object graph.
* callback: function(object, Array 'Foo'
* Assumes incoming text is already lowercase.
*/
return str.charAt(0).toUpperCase() + str.substring(1);
};
Object.setOwnerOf($.utils.string.capitalize, $.physicals.Neil);
$.utils.string.randomCharacter = function randomCharacter(chars) {
return chars.charAt(Math.random() * chars.length);
};
$.utils.string.VOWELS = 'aeiouy';
$.utils.string.CONSONANTS = 'bcdfghjklmnpqrstvwxz';
$.utils.string.ALPHABET = 'abcdefghijklmnopqrstuvwxyz';
$.utils.string.hash = new 'CC.hash';
$.utils.string.translate = function translate(text, language) {
/* Try to translate text into the specified language using an
* external translation server.
*
* Arguments:
* text: string: the text to be translated.
* language: string: a two-character ISO 639-1 language code.
*
* Returns: the translated text.
*/
var url = 'https://translate-service.scratch.mit.edu' +
'/translate?language=' + encodeURIComponent(language) +
'&text=' + encodeURIComponent(text);
var json = $.system.xhr(url);
return JSON.parse(json).result;
};
Object.setOwnerOf($.utils.string.translate, $.physicals.Maximilian);
Object.setOwnerOf($.utils.string.translate.prototype, $.physicals.Maximilian);
$.utils.string.generateRandom = function generateRandom(length, soup) {
/* Return a string of the specified length consisting of characters from the
* given soup, or $.utils.string.generateRandom.DEFAULT_SOUP if none
* specified.
*
* E.g.: generateRandom(4, 'abc') might return 'cbca'.
*
* Arguments:
* - length: number - length of string to generate.
* - soup: string - alphabet to select characters randomly from.
*/
soup = soup || $.utils.string.generateRandom.DEFAULT_SOUP;
var out = [];
for (var i = 0; i < length; i++) {
out[i] = this.randomCharacter(soup);
}
return out.join('');
};
Object.setOwnerOf($.utils.string.generateRandom, $.physicals.Maximilian);
Object.setOwnerOf($.utils.string.generateRandom.prototype, $.physicals.Neil);
$.utils.string.generateRandom.DEFAULT_SOUP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$.utils.string.prefixLines = function prefixLines(text, prefix) {
// Prepend a common prefix onto each line of code.
// Intended for indenting code or adding '//' comment markers.
return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix);
};
Object.setOwnerOf($.utils.string.prefixLines, $.physicals.Neil);
Object.setOwnerOf($.utils.string.prefixLines.prototype, $.physicals.Neil);
================================================
FILE: core/core_12_$.utils.code.js
================================================
/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Code utilities for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.utils.code = {};
$.utils.code.rewriteForEval = function rewriteForEval(src, forceExpression) {
/* Eval treats {} as an empty block (return value undefined).
* Eval treats {'a': 1} as a syntax error.
* Eval treats {a: 1} as block with a labeled statement (return value 1).
* Detect these cases and enclose in parenthesis.
* But don't mess with: {var x = 1; x + x;}
* This is consistent with the console on Chrome and Node.
* If 'forceExpression' is true, then throw a SyntaxError if the src is
* more than one expression (e.g. '1; 2;').
*/
var ast = null;
if (!forceExpression) {
// Try to parse src as a program.
try {
ast = $.utils.code.parse(src);
} catch (e) {
// ast remains null.
}
}
if (ast) {
if (ast.type === 'Program' && ast.body.length === 1 &&
ast.body[0].type === 'BlockStatement') {
if (ast.body[0].body.length === 0) {
// This is an empty object: {}
return '({})';
}
if (ast.body[0].body.length === 1 &&
ast.body[0].body[0].type === 'LabeledStatement' &&
ast.body[0].body[0].body.type === 'ExpressionStatement') {
// This is an unquoted object literal: {a: 1}
// There might be a comment, so add a linebreak.
return '(' + src + '\n)';
}
}
return src;
}
// Try parsing src as an expression.
// This may throw.
ast = $.utils.code.parseExpressionAt(src, 0);
var remainder = src.substring(ast.end).trim();
if (remainder !== '') {
// Remainder might legally include trailing comments or semicolons.
// Remainder might illegally include more statements.
var remainderAst = null;
try {
remainderAst = $.utils.code.parse(remainder);
} catch (e) {
// remainderAst remains null.
}
if (!remainderAst) {
throw new SyntaxError('Syntax error beyond expression');
}
if (remainderAst.type !== 'Program') {
throw new SyntaxError('Unexpected code beyond expression'); // Module?
}
// Trim off any unnecessary trailing semicolons.
while (remainderAst.body[0] &&
remainderAst.body[0].type === 'EmptyStatement') {
remainderAst.body.shift();
}
if (remainderAst.body.length !== 0) {
throw new SyntaxError('Only one expression expected');
}
}
src = src.substring(0, ast.end);
if (ast.type === 'ObjectExpression' || ast.type === 'FunctionExpression') {
// {a: 1} and function () {} both need to be wrapped in parens to avoid
// being syntax errors.
src = '(' + src + ')';
}
return src;
};
Object.setOwnerOf($.utils.code.rewriteForEval, $.physicals.Maximilian);
$.utils.code.rewriteForEval.unittest = function() {
var cases = {
// Input: [Expression, Statement(s)]
'1 + 2': ['1 + 2', '1 + 2'],
'2 + 3 // Comment': ['2 + 3', '2 + 3 // Comment'],
'3 + 4;': ['3 + 4', '3 + 4;'],
'4 + 5; 6 + 7': [SyntaxError, '4 + 5; 6 + 7'],
'{}': ['({})', '({})'],
'{} // Comment': ['({})', '({})'],
'{};': ['({})', '{};'],
'{}; {}': [SyntaxError, '{}; {}'],
'{"a": 1}': ['({"a": 1})', '({"a": 1})'],
'{"a": 2} // Comment': ['({"a": 2})', '({"a": 2})'],
'{"a": 3};': ['({"a": 3})', '({"a": 3})'],
'{"a": 4}; {"a": 4}': [SyntaxError, SyntaxError],
'{b: 1}': ['({b: 1})', '({b: 1}\n)'],
'{b: 2} // Comment': ['({b: 2})', '({b: 2} // Comment\n)'],
'{b: 3};': ['({b: 3})', '{b: 3};'],
'{b: 4}; {b: 4}': [SyntaxError, '{b: 4}; {b: 4}'],
'function () {}': ['(function () {})', '(function () {})'],
'function () {} // Comment': ['(function () {})', '(function () {})'],
'function () {};': ['(function () {})', '(function () {})'],
'function () {}; function () {}': [SyntaxError, SyntaxError],
'{} + []': ['{} + []', '{} + []']
};
var actual;
for (var key in cases) {
if (!cases.hasOwnProperty(key)) continue;
// Test eval as an expression.
try {
actual = $.utils.code.rewriteForEval(key, true);
} catch (e) {
actual = SyntaxError;
}
if (actual !== cases[key][0]) {
throw new Error('Eval Expression\n' +
'Expected: ' + cases[key][0] + ' Actual: ' + actual);
}
// Test eval as a statement.
try {
actual = $.utils.code.rewriteForEval(key, false);
} catch (e) {
actual = SyntaxError;
}
if (actual !== cases[key][1]) {
throw new Error('Eval Statement\n' +
'Expected: ' + cases[key][1] + ' Actual: ' + actual);
}
}
};
$.utils.code.eval = function $_utils_code_eval(src, evalFunc) {
// Eval src and attempt to print the resulting value readably.
//
// Evaluation is done by calling evalFunc (passing src) if supplied,
// or by calling the eval built-in function (under a different name,
// so it operates in the global scope). Unhandled exceptions are
// caught and converted to a string.
//
// Caller may wish to transform input with
// $.utils.code.rewriteForEval before passing it to this function.
evalFunc = evalFunc || eval;
var out;
try {
out = evalFunc(src);
} catch (e) {
// Exception thrown. Use built-in ToString via + to avoid calling
// String, least it call a .toString method that itself throws.
// TODO(cpcallen): find an alternative way of doing this safely
// once the interpreter calls String for all string conversions.
if (e instanceof Error) {
out = 'Unhandled error: ' + e.name;
if (e.message) out += ': ' + e.message;
if (e.stack) out += '\n' + e.stack;
return out;
} else {
return 'Unhandled exception: ' + e;
}
}
// Suspend if needed.
try {(function(){})();} catch (e) {suspend();}
// Attempt to print a source-legal representation.
return $.utils.code.expressionFor(out, {
depth: 2,
abbreviateMethods: true,
proto: 'note',
owner: 'ignore'
});
};
Object.setOwnerOf($.utils.code.eval, $.physicals.Maximilian);
$.utils.code.regexps = {};
$.utils.code.regexps.README = '$.utils.code.regexps contains some RegExps useful for parsing or otherwise analysing code.\n\nSee ._generate() for how they are constructed and what they will match.\n';
$.utils.code.regexps._generate = function _generate() {
/* Generate some RegExps that match various bits of JavaScript syntax.
* The intention is that these regular expressions conform to the
* lexical grammar given ES5.1 Appendix A.1
* (https://262.ecma-international.org/5.1/#sec-A.1), in some cases
* updated to include changes in the current version of the spec
* (https://tc39.es/ecma262/#sec-lexical-grammar).
*
* TODO: add tests for generated RegExps.
*/
// Globally matches escape sequences found in string and regexp
// literals, like '\n' or '\x20' or '\u1234'. (This is basically
// the spec EscapeSequence but including the backslash prefix.)
this.escapes = /\\(?:["'\\\/0bfnrtv]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2})/g;
// Globally matches a single-quoted string literal, like "'this one'"
// and "'it\\'s'".
this.singleQuotedString =
new RegExp("'(?:[^'\\\\\\r\\n\\u2028\\u2029]|" +
this.escapes.source + ")*'", 'g');
// Globally matches a double-quoted string literal, like '"this one"'
// and '"it\'s"'.
this.doubleQuotedString =
new RegExp('"(?:[^"\\\\\\r\\n\\u2028\\u2029]|' +
this.escapes.source + ')*"', 'g');
// Globally matches a StringLiteral, like "'this one' and '"that one"'
// as well as "the 'string literal' substring of this longer string" too.
this.string = new RegExp('(?:' + this.singleQuotedString.source + '|' +
this.doubleQuotedString.source + ')', 'g');
// Globally matches a valid JavaScript IdentifierName. Note that
// this is conservative, because ANY Unicode letter can appear
// in an identifier - but the full regexp is absurdly complicated.
this.identifierName = /[A-Za-z_$][A-Za-z0-9_$]*/g;
// Matches a valid ES2020 ReservedWord.
var reserved = /await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|if|import|in|instanceof|new|null|return|super|switch|this|throw|true|try|typeof|var|void|while|with|yield/;
// Matches ES5.1 FutureReservedWords not included in reserved.
var reservedES5 = /implements|interface|let|package|protected|public|static/;
// Globally matches a valid JavaScript ReservedWord.
this.reservedWord =
new RegExp('(?:' + reserved.source + '|' + reservedES5.source + ')', 'g');
////////////////////////////////////////////////////////////////////
// Exact forms of the above. These do not get the global flag.
var keys = ['identifierName', 'reservedWord', 'string'];
for (var key, i = 0; (key = keys[i]); i++) {
this[key + 'Exact'] = new RegExp('^' + this[key].source + '$');
}
};
Object.setOwnerOf($.utils.code.regexps._generate.prototype, $.physicals.Maximilian);
$.utils.code.regexps.escapes = /\\(?:["'\\\/0bfnrtv]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2})/g;
$.utils.code.regexps.singleQuotedString = /'(?:[^'\\\r\n\u2028\u2029]|\\(?:["'\\\/0bfnrtv]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}))*'/g;
$.utils.code.regexps.doubleQuotedString = /"(?:[^"\\\r\n\u2028\u2029]|\\(?:["'\\\/0bfnrtv]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}))*"/g;
$.utils.code.regexps.string = /(?:'(?:[^'\\\r\n\u2028\u2029]|\\(?:["'\\\/0bfnrtv]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}))*'|"(?:[^"\\\r\n\u2028\u2029]|\\(?:["'\\\/0bfnrtv]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}))*")/g;
$.utils.code.regexps.identifierName = /[A-Za-z_$][A-Za-z0-9_$]*/g;
$.utils.code.regexps.reservedWord = /(?:await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|if|import|in|instanceof|new|null|return|super|switch|this|throw|true|try|typeof|var|void|while|with|yield|implements|interface|let|package|protected|public|static)/g;
$.utils.code.regexps.identifierNameExact = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
$.utils.code.regexps.reservedWordExact = /^(?:await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|if|import|in|instanceof|new|null|return|super|switch|this|throw|true|try|typeof|var|void|while|with|yield|implements|interface|let|package|protected|public|static)$/;
$.utils.code.regexps.stringExact = /^(?:'(?:[^'\\\r\n\u2028\u2029]|\\(?:["'\\\/0bfnrtv]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}))*'|"(?:[^"\\\r\n\u2028\u2029]|\\(?:["'\\\/0bfnrtv]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}))*")$/;
$.utils.code.parseString = function parseString(s) {
/* Convert a string representation of a string literal to a string.
* Basically does eval(s), but safely and only if s is a string
* literal.
*/
if (!this.regexps.stringExact.test(s)) {
throw new TypeError(this.quote(s) + ' is not a string literal');
}
return s.slice(1, -1).replace(this.regexps.escapes, function(esc) {
switch (esc[1]) {
case "'":
case '"':
case '/':
case '\\':
return esc[1];
case '0':
return '\0';
case 'b':
return '\b';
case 'f':
return '\f';
case 'n':
return '\n';
case 'r':
return '\r';
case 't':
return '\t';
case 'v':
return '\v';
case 'u':
case 'x':
return String.fromCharCode(parseInt(esc.slice(2), 16));
default:
// RegExp in call to replace has accepted something we
// don't know how to decode.
throw new Error('unknown escape sequence "' + esc + '"??');
}
});
};
Object.setOwnerOf($.utils.code.parseString, $.physicals.Maximilian);
$.utils.code.quote = function quote(str) {
// Convert a string into a string literal. We use single or double
// quotes depending on which occurs less frequently in the string to
// be escaped (prefering single quotes if it's a tie). Strictly
// speaking we only need to escape backslash, \r, \n, \u2028 (line
// separator), \u2029 (paragraph separator) and whichever quote
// character we're using, but for output readability we escape all the
// control characters.
//
// TODO(cpcallen): Consider using optimised algorithm from Node.js's
// util.format (see strEscape function in
// https://github.com/nodejs/node/blob/master/lib/util.js).
// @param {string} str The string to convert.
// @return {string} The value s as a eval-able string literal.
if (this.count(str, "'") > this.count(str, '"')) { // More 's. Use "s.
return '"' + str.replace(this.quote.doubleRE, this.quote.replace) + '"';
} else { // Equal or more "s. Use 's.
return "'" + str.replace(this.quote.singleRE, this.quote.replace) + "'";
}
};
$.utils.code.quote.singleRE = /[\x00-\x1f\\\u2028\u2029']/g;
$.utils.code.quote.doubleRE = /[\x00-\x1f\\\u2028\u2029"]/g;
$.utils.code.quote.replace = function replace(c) {
// Replace special characters with their quoted replacements.
// Intended to be used as the second argument to
// String.prototype.replace.
return $.utils.code.quote.replacements[c];
};
$.utils.code.quote.replacements = {};
$.utils.code.quote.replacements['\0'] = '\\0';
$.utils.code.quote.replacements['\x01'] = '\\x01';
$.utils.code.quote.replacements['\x02'] = '\\x02';
$.utils.code.quote.replacements['\x03'] = '\\x03';
$.utils.code.quote.replacements['\x04'] = '\\x04';
$.utils.code.quote.replacements['\x05'] = '\\x05';
$.utils.code.quote.replacements['\x06'] = '\\x06';
$.utils.code.quote.replacements['\x07'] = '\\x07';
$.utils.code.quote.replacements['\b'] = '\\b';
$.utils.code.quote.replacements['\t'] = '\\t';
$.utils.code.quote.replacements['\n'] = '\\n';
$.utils.code.quote.replacements['\v'] = '\\v';
$.utils.code.quote.replacements['\f'] = '\\f';
$.utils.code.quote.replacements['\r'] = '\\r';
$.utils.code.quote.replacements['\x0e'] = '\\x0e';
$.utils.code.quote.replacements['\x0f'] = '\\x0f';
$.utils.code.quote.replacements['"'] = '\\"';
$.utils.code.quote.replacements["'"] = "\\'";
$.utils.code.quote.replacements['\\'] = '\\\\';
$.utils.code.quote.replacements['\u2028'] = '\\u2028';
$.utils.code.quote.replacements['\u2029'] = '\\u2029';
$.utils.code.count = function count(str, searchString) {
// Count non-overlapping occurrences of searchString in str.
return str.split(searchString).length;
};
$.utils.code.isIdentifier = function isIdentifier(id) {
/* Arguments:
* - id: any - any JavaScript value.
*
* Returns: boolean - true iff id is a string representing valid
* Identifier, which is any bare word that can be used
* as a variable name (i.e., excluding reserved words).
*/
return $.utils.code.isIdentifierName(id) &&
!$.utils.code.regexps.reservedWordExact.test(id);
};
Object.setOwnerOf($.utils.code.isIdentifier, $.physicals.Maximilian);
$.utils.code.getGlobal = function getGlobal() {
// Return a pseudo global object.
var global = Object.create(null);
global.$ = $;
global.Array = Array;
global.Boolean = Boolean;
global.clearTimeout = clearTimeout;
global.Date = Date;
global.decodeURI = decodeURI;
global.decodeURIComponent = decodeURIComponent;
global.encodeURI = encodeURI;
global.encodeURIComponent = encodeURIComponent;
global.Error = Error;
global.escape = escape;
global.eval = eval;
global.EvalError = EvalError;
global.Function = Function;
global.isFinite = isFinite;
global.isNaN = isNaN;
global.JSON = JSON;
global.Math = Math;
global.Number = Number;
global.Object = Object;
global.parseFloat = parseFloat;
global.parseInt = parseInt;
global.perms = perms;
global.RangeError = RangeError;
global.ReferenceError = ReferenceError;
global.RegExp = RegExp;
global.setPerms = setPerms;
global.setTimeout = setTimeout;
global.String = String;
global.suspend = suspend;
global.SyntaxError = SyntaxError;
global.Thread = Thread;
global.TypeError = TypeError;
global.unescape = unescape;
global.URIError = URIError;
global.user = user;
global.WeakMap = WeakMap;
return global;
};
Object.setOwnerOf($.utils.code.getGlobal, $.physicals.Maximilian);
$.utils.code.parse = new 'CC.acorn.parse';
$.utils.code.parseExpressionAt = new 'CC.acorn.parseExpressionAt';
$.utils.code.isIdentifierName = function isIdentifierName(id) {
/* Arguments:
* - id: any - any JavaScript value.
*
* Returns: boolean - true iff id is a string representing valid
* IdentifierName, which is anything bare word that can appear
* after the '.' in a MemberExpresion.
*/
return typeof id === 'string' &&
$.utils.code.regexps.identifierNameExact.test(id);
};
Object.setOwnerOf($.utils.code.isIdentifierName, $.physicals.Maximilian);
Object.setOwnerOf($.utils.code.isIdentifierName.prototype, $.physicals.Maximilian);
$.utils.code.expressionFor = function expressionFor(value, options) {
/* Given an arbitrary value, return a string containing a JavaScript
* expression for it.
*
* The intention is that expressionFor(value) should return a string
* such that eval(expressionFor(value)) will be (in order of preference):
*
* - Identical to value (as determined by Object.is), or
* - An equivalent copy of value to a specified depth, or
* - Be unparsable or contain comments explaining in what way the
* result of eval will differ from original value.
*
* Arguments:
* - value: any - any JavaScript value.
* - options?: Object - optional options object. See implementation.
*
* Returns: string - an expression for value.
*
* TODO: there should be flags controlling what to do when it is not
* possible to construct an expression that will eval to an exact copy
* of value. The options should include returning valid code containing
* comments, returning unparsable code, or throwing an an error.
*/
var opts = {
depth: 10, // How deeply shall we traverse the object tree?
arrayLimit: 100, // Max number of array elements to include.
propertyLimit: 100, // Max number of properties to include.
abbreviateFunctions: false, // Elide all function bodies?
abbreviateMethods: false, // Elide method function bodies?
proto: 'set', // 'set', 'note' or (any other value) ignore prototype.
owner: 'note', // 'set', 'note' or (any other value) ignore owner.
lineLength: 80, // Line length limit, for formatting purposes.
indent: 2, // Indent for nested expressions.
seen_: [], // TODO: use Set instead of Array.
};
// Like Object.assign(opts, options) but copies inherited properties too.
for (var k in options) opts[k] = options[k];
// Helper to handle failures where expressionFor cannot or does not yet
// return an experssion that will eval to an identical copy of value.
// Typical usage: return fail('reason for failure');
function fail(message) {
// TODO: have flag to make it:
// throw new ReferenceError(message);
return $.utils.code.blockComment(message);
}
// Helper for properties in array and object literals.
function expressionForProperty(key) {
var descriptor = Object.getOwnPropertyDescriptor(value, key);
var propertyValue = descriptor.value;
if (selector) {
opts.selector = new $.Selector(selector.concat(key));
}
opts.abbreviateFunctions = opts.abbreviateMethods;
return expressionFor(propertyValue, opts);
}
var type = typeof value;
if (value === undefined || value === null ||
type === 'number' || type === 'boolean') {
if (Object.is(value, -0)) return '-0';
return String(value);
} else if (type === 'string') {
return $.utils.code.quote(value);
} else if (type !== 'function' && type !== 'object') {
throw new TypeError("unknown type '" + type + "'");
}
// value is an object of some kind (including function). Work out a selector.
var selector = $.Selector.for(value);
if (opts.selector) {
var suggestedSelectorValue = opts.selector.toValue(/*save:*/true);
if (!selector && suggestedSelectorValue === value) {
selector = opts.selector;
}
}
// Deal with already-seen objects (and nesting limit depth limit).
if (opts.seen_.includes(value)) {
return fail('cyclic or shared substructure' +
(selector ? ': ' + selector.toString() : 'with no known selector'));
} else if (opts.depth < 1) {
if (!selector) return fail(type + ' with no known selector');
return selector.toExpr();
}
// Prepare for recursive calls.
opts.seen_.push(value);
opts.depth--;
opts.lineLength -= opts.indent;
// Get the object's [[Class]] - Object, Array, Date, RegExp, Error, etc.
// Since 'class' isn't a legal variable name, re-use 'type'.
type = Object.prototype.toString.call(value).slice(8, -1);
var proto = Object.getPrototypeOf(value); // Actual prototype of value.
var expectedProto; // Expected prototype of object of same [[Class]] as value.
var prefix = '', expr = '', suffix = ''; // Concatenate to get final expression.
var entries; // Array of initialisers for object or array literal.
var notes = []; // Array of notes to postpend as comment.
// Make a note about the object's selector unless it is the expected
// one. Decide this before recursive calls mess with opts.selector.
var selectorNote = selector ? selector.toString() : '';
if (opts.selector && selectorNote === opts.selector.toString()) {
selectorNote = '';
}
if (type === 'Array') {
if (!Array.isArray(value)) throw TypeError('non-array array??');
expectedProto = Array.prototype;
prefix = '[';
suffix = ']';
entries = [];
for (var i = 0; i < value.length; i++) {
suspend();
if (i >= opts.arrayLimit) {
entries[i] = $.utils.code.blockComment('and ' + (value.length - opts.arrayLimit) + ' more');
break;
} else if (!Object.hasOwnProperty.call(value, i)) {
entries[i] = '';
continue;
}
entries[i] = expressionForProperty(String(i));
}
} else if (type === 'Date') {
expr = 'new Date(\'' + value.toJSON() + '\')';
expectedProto = Date.prototype;
} else if (type === 'Error') {
expectedProto = proto;
switch (proto) {
case EvalError.prototype: prefix = 'EvalError'; break;
case RangeError.prototype: prefix = 'RangeError'; break;
case ReferenceError.prototype: prefix = 'ReferenceError'; break;
case SyntaxError.prototype: expr = 'SyntaxError'; break;
case TypeError.prototype: expr = 'TypeError'; break;
case URIError.prototype: expr = 'URIError'; break;
case PermissionError.prototype: expr = 'PermissionError'; break;
default:
expr = 'Error';
expectedProto = Error.prototype;
}
if (typeof value.message === 'string') {
expr += '(' + $.utils.code.quote(value.message) + ')';
} else {
expr += '()';
}
} else if (type === 'Function') {
expectedProto = Function.prototype;
expr = Function.prototype.toString.call(value);
if (opts.abbreviateFunctions) {
expr = fail(expr.replace(/\{[^]*$/, '{ ... }'));
}
} else if (type === 'Object') {
expectedProto = Object.prototype;
prefix = '{';
suffix = '}';
entries = [];
var keys = Object.getOwnPropertyNames(value);
for (var i = 0; i < keys.length; i++) {
suspend();
if (i >= opts.propertyLimit) {
entries[i] = '/* and ' + (keys.length - opts.propertyLimit) + ' more */';
break;
}
var key = keys[i];
// BUG(#469): property keys that are NumericLiterals (like 3.2e4
// or 0xf00) can also appear unquoted!
entries[i] =
($.utils.code.isIdentifierName(key) ? key : $.utils.code.quote(key)) +
': ' + expressionForProperty(key);
}
} else if (type === 'RegExp') {
expr = RegExp.prototype.toString.call(value);
expectedProto = RegExp.prototype;
} else if (type === 'Thread') {
if (selector) return selector.toExpr();
expr = 'new Thread(' + fail('unable to reconstruct thread state') + ')';
expectedProto = Thread.prototype;
} else if (type === 'WeakMap') {
expectedProto = WeakMap.prototype;
expr = 'new WeakMap()';
} else {
throw new TypeError('unknown internal type ' + type);
}
// TODO: Prepend/append call to Object.defineProperties for remaining
// properties & property attributes.
// Pre/append prototype information if it is not as expected.
if (proto !== expectedProto) {
var protoString = expressionFor(proto, {depth: 0});
if (opts.proto === 'set') {
prefix = 'Object.setPrototypeOf(' + prefix;
suffix += ', ' + protoString + ')';
} else if (opts.proto === 'note') {
notes.push('[[Proto]]: ' + protoString);
}
}
// Prepend/append owner information.
var ownerString = expressionFor(Object.getOwnerOf(value), {depth: 0});
if (opts.owner === 'set') {
// BUG: Object.setOwnerOf(obj, owner) does not return obj (yet).
throw new Error("can't set owner in an expression yet");
// prefix = 'Object.setOwnerOf(' + prefix;
// suffix += ', ' + ownerString + ')';
} else if (opts.owner === 'note') {
notes.push('[[Owner]]: ' + ownerString);
}
// Prepend/append notes.
if (selectorNote) {
prefix = $.utils.code.blockComment(selectorNote) + ' ' + prefix;
}
if (notes.length) {
suffix += ' ' + $.utils.code.blockComment(notes.join(', '));
}
// Join entries choosing a suitable layout depending on available space.
if (entries && entries.length) {
// Try single-line output.
// BUG: this omits required trailing comma when there are undefined
// trailing aray elements (e.g., [1, 2, 3,,].
expr = entries.join(', ');
var result = prefix + expr + suffix;
if (result.length <= opts.lineLength && !result.includes('\n')) {
return result;
}
// Generate multi-line output.
var padding = ' '.repeat(opts.indent);
expr = '\n' + $.utils.string.prefixLines(entries.join(',\n'), padding) + ',\n';
}
return prefix + expr + suffix;
};
Object.setOwnerOf($.utils.code.expressionFor, $.physicals.Maximilian);
Object.setOwnerOf($.utils.code.expressionFor.prototype, $.physicals.Maximilian);
$.utils.code.blockComment = function blockComment(text) {
/* Format text as a block comment. Any occurences of the closing
* block comment delimiter in text will have a space inerted in them.
* If text is undefined, an empty string will be returned instead.
*
* Arguments:
* - text: string | undefined - the contents of the comment.
* Returns: string - the block comment.
*/
return text ? '/* ' + text.replace(/\*\//g, '* /') + ' */' : '';
};
Object.setOwnerOf($.utils.code.blockComment, $.physicals.Maximilian);
Object.setOwnerOf($.utils.code.blockComment.prototype, $.physicals.Maximilian);
================================================
FILE: core/core_13_$.Selector.js
================================================
/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Selector implementation for Code City core.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.Selector = function Selector(s) {
/* A Selector is a representation of a selector string in the form
* of an array (of Selector.Parts) which happens to have
* Selector.prototype (with various useful convenience methods) in
* its prototype chain.
*/
var parts;
if (typeof s === 'string') {
// Parse selector text (but check in cache first).
var cached = Selector.cache_[s];
// Copy and set owner?
if (cached) return cached;
parts = Selector.parse(s);
} else if (Array.isArray(s)) {
parts = [];
// Validate & copy parts list.
if (s.length < 1) throw new RangeError('Zero-length parts list??');
if (!$.utils.code.isIdentifier(s[0])) {
throw new TypeError('parts array must begin with an identifier');
}
parts[0] = s[0];
for (var i = 1; i < s.length; i++) {
if (typeof s[i] === 'string' || s[i] === Selector.PROTOTYPE || s[i] === Selector.OWNER) {
parts[i] = s[i];
} else if (s[i] instanceof Selector.SpecialPart) {
throw new TypeError('Invalid SpecialPart in parts array');
} else if (typeof s[i] === 'object' && s[i].type) {
// Handle normalisation of parts lists that have been roundtripped via JSON.
switch(s[i].type) {
case 'proto':
parts[i] = Selector.PROTOTYPE;
break;
case 'owner':
parts[i] = Selector.OWNER;
break;
default:
throw new TypeError('Unknown SpecialPart type ' + s[i].type);
}
} else {
throw new TypeError('Invalid part in parts array');
}
}
} else {
throw new TypeError('Not a selector or parts array');
}
Object.setPrototypeOf(parts, Selector.prototype);
Object.freeze(parts);
// Copy and set owner?
Selector.cache_[parts.toString()] = parts; // Save.
return parts;
};
Object.setOwnerOf($.Selector, $.physicals.Maximilian);
Object.setPrototypeOf($.Selector.prototype, Array.prototype);
$.Selector.prototype.isOwner = function isOwner() {
/* Returns true iff the selector represents an object owner binding.
*/
return this.length > 1 && this[this.length - 1] === this.constructor.OWNER;
};
Object.setOwnerOf($.Selector.prototype.isOwner, $.physicals.Maximilian);
$.Selector.prototype.isProp = function isProp() {
/* Returns true iff the selector represents an object property binding.
*/
return this.length > 1 && typeof this[this.length - 1] === 'string';
};
Object.setOwnerOf($.Selector.prototype.isProp, $.physicals.Maximilian);
$.Selector.prototype.isProto = function isProto() {
/* Returns true iff the selector represents an object prototype binding.
*/
return this.length > 1 && this[this.length - 1] === this.constructor.PROTOTYPE;
};
Object.setOwnerOf($.Selector.prototype.isProto, $.physicals.Maximilian);
$.Selector.prototype.isVar = function isVar() {
/* Returns true iff the selector represents a top-level variable binding.
*/
return this.length === 1 && typeof this[0] === 'string';
};
Object.setOwnerOf($.Selector.prototype.isVar, $.physicals.Maximilian);
$.Selector.prototype.toExpr = function toExpr() {
/* Return the selector as an evaluable expression yeilding the selected value.
*/
return this.toString(function(part, out) {
if (part === $.Selector.PROTOTYPE) {
out.unshift('Object.getPrototypeOf(');
out.push(')');
} else if (part === $.Selector.OWNER) {
out.unshift('Object.getOwnerOf(');
out.push(')');
} else {
throw new TypeError('Invalid part in parts array');
}
});
};
Object.setOwnerOf($.Selector.prototype.toExpr, $.physicals.Maximilian);
$.Selector.prototype.toSetExpr = function toSetExpr(valueExpr) {
/* Return an expression setting the selected value to the value of the
* supplied expression.
*
* The parameter valueExpr should be a string containing a JS expression that
* evaluates to the new value to be assigned to the selected location. It
* must not contain any non-parenthesized operators with lower precedence
* than '=' - specifically, the yield and comma operators.
*/
var lastPart = this[this.length - 1];
if (!(lastPart instanceof this.constructor.SpecialPart)) {
return this.toExpr() + ' = ' + valueExpr;
}
var objExpr = new this.constructor(this.slice(0, -1)).toExpr();
if (lastPart === this.constructor.PROTOTYPE) {
return 'Object.setPrototypeOf(' + objExpr + ', ' + valueExpr + ')';
} else if (lastPart === this.constructor.OWNER) {
return 'Object.setOwnerOf(' + objExpr + ', ' + valueExpr + ')';
} else {
throw new TypeError('Invalid part in parts array');
}
};
Object.setOwnerOf($.Selector.prototype.toSetExpr, $.physicals.Maximilian);
$.Selector.prototype.toString = function toString(specialHandler) {
/* Return the canonical selector string for this Selector.
*
* The specialHandler optional parameter, if supplied, should be callback
* which accepts a Selector.SpecialPart instance and an Array of strings, and
* pushes a string representation of the SpecialPart onto the array. (See
* Selector.prototype.toExpr for an example of how to use this.)
*/
var out = [this[0]];
for (var i = 1; i < this.length; i++) {
var part = this[i];
if (part instanceof this.constructor.SpecialPart) {
if (specialHandler) {
specialHandler(part, out);
} else {
out.push(String(part));
}
} else if ($.utils.code.isIdentifierName(part)) {
out.push('.', part);
} else if (String(Number(part)) === part) {
// String represents a number with same string representation.
out.push('[', part, ']');
} else {
out.push('[', $.utils.code.quote(part), ']');
}
}
return out.join('');
};
Object.setOwnerOf($.Selector.prototype.toString, $.physicals.Maximilian);
$.Selector.prototype.toValue = function toValue(save, global) {
/* Return value corresponding to this Selector, or throw EvalError if that is
* not possible. This function basically does
*
* return eval(this.toExpr())
*
* ...only slightly more safely.
*
* Added bonus features:
* - If this selector evaluates to an object and save is true, the selector
* will be added to the the reverse-lookup database.
* - If global is specified, global variables will be evaluated by looking
* them up as properties on that object.
*/
if (this.length === 0) throw RangeError('Invalid Selector');
var varname = this[0];
if (!$.utils.code.isIdentifier(varname)) {
throw TypeError('invalid variable identifier');
}
var v;
if (global) {
v = global[varname];
} else {
try {
var globalEval = eval;
v = globalEval(varname);
} catch (e) {
v = undefined;
}
}
for (var i = 1; i < this.length; i++) {
if (!$.utils.isObject(v)) {
var s = new this.constructor(this.slice(0, i));
throw TypeError(String(s) + ' is not an object');
}
var part = this[i];
if (typeof part === 'string') {
v = v[part];
} else if (part === this.constructor.PROTOTYPE) {
v = Object.getPrototypeOf(v);
} else if (part === this.constructor.OWNER) {
v = Object.getOwnerOf(v);
} else {
throw new Error('Not implemented');
}
}
if (save) {
this.constructor.db.set(v, this);
}
return v;
};
Object.setOwnerOf($.Selector.prototype.toValue, $.physicals.Maximilian);
$.Selector.prototype.badness = function badness() {
/* Returns a "badness" score, inversely proportional to how
* desirable a particular selector is amongst other selectors
* referring to the same object. In general, longer selectors are
* more bad, but selectors containing special parts are especially
* bad.
*/
var penalties = 0;
for (var i = 0; i < this.length; i++) {
var part = this[i];
if (part instanceof this.constructor.SpecialPart) {
penalties += 100;
} else if ($.utils.code.isIdentifierName(part)) {
penalties += 10; // We like identifiers.
} else if (String(Number(part)) === part) {
penalties += 25; // Numbers are OK.
} else {
penalties += 50; // Quoted strings are undesirable.
}
}
if (this[0] === '$') penalties += 50; // Prefer builtins.
return penalties + String(this).length;
};
Object.setOwnerOf($.Selector.prototype.badness, $.physicals.Maximilian);
$.Selector.SpecialPart = function SpecialPart(type) {
// A SpecialPart is a class for all "special" selector parts (ones
// which do not represent named variables / properties).
this.type = type;
Object.freeze(this);
};
$.Selector.SpecialPart.prototype.toString = function toString() {
return '{' + this.type + '}';
};
Object.setOwnerOf($.Selector.SpecialPart.prototype.toString, $.physicals.Maximilian);
$.Selector.PROTOTYPE = (new 'Object.create')($.Selector.SpecialPart.prototype);
$.Selector.PROTOTYPE.type = 'proto';
Object.defineProperty($.Selector.PROTOTYPE, 'type', {writable: false, configurable: false});
Object.preventExtensions($.Selector.PROTOTYPE);
$.Selector.OWNER = (new 'Object.create')($.Selector.SpecialPart.prototype);
$.Selector.OWNER.type = 'owner';
Object.defineProperty($.Selector.OWNER, 'type', {writable: false, configurable: false});
Object.preventExtensions($.Selector.OWNER);
$.Selector.parse = function parse(selector) {
// Parse a selector into an array of Parts.
var tokens = this.parse.tokenize(selector);
var parts = [];
var State = {
START: 0, GOOD: 1, DOT: 2, BRACKET: 3, BRACKET_DONE: 4,
BRACE: 5, BRACE_DONE: 6
};
var state = State.START;
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (token.type === 'whitespace') continue;
switch (state) {
case State.START:
if (token.type !== 'id') {
throw new SyntaxError('Selector must start with an identifier');
}
parts.push(token.raw);
state = State.GOOD;
break;
case State.GOOD:
if (token.type === '.') {
state = State.DOT;
} else if (token.type === '[') {
state = State.BRACKET;
} else if (token.type === '{') {
state = State.BRACE;
} else if (token.type === '^') {
// State remains unchanged.
parts.push(this.PROTOTYPE);
} else {
throw new SyntaxError('Invalid token ' + $.utils.code.quote(token.raw) + ' in selector');
}
break;
case State.DOT:
if (token.type !== 'id') {
throw new SyntaxError('"." must be followed by identifier in selector');
}
parts.push(token.raw);
state = State.GOOD;
break;
case State.BRACKET:
if (token.type === 'number') {
parts.push(String(token.raw));
} else if (token.type === 'str') {
parts.push(String(token.value));
} else {
throw new SyntaxError('"[" must be followed by numeric or string literal in selector');
}
state = State.BRACKET_DONE;
break;
case State.BRACKET_DONE:
if (token.type !== ']') {
throw new SyntaxError('Invalid token ' + $.utils.code.quote(token.raw) + ' after subscript');
}
state = State.GOOD;
break;
case State.BRACE:
if (token.type === 'id' && token.raw === 'proto') {
parts.push(this.PROTOTYPE);
} else if (token.type === 'id' && token.raw === 'owner') {
parts.push(this.OWNER);
} else {
throw new SyntaxError('"{" must be followed by "proto" or "owner"');
}
state = State.BRACE_DONE;
break;
case State.BRACE_DONE:
if (token.type !== '}') {
throw new SyntaxError('Invalid token ' + $.utils.code.quote(token.raw) + ' after special');
}
state = State.GOOD;
break;
default:
throw new Error('Invalid State in parse??');
}
}
if (state !== State.GOOD) {
throw new SyntaxError('Incomplete selector ' + selector);
}
return parts;
};
Object.setOwnerOf($.Selector.parse, $.physicals.Maximilian);
$.Selector.parse.tokenize = function tokenize(selector) {
// Tokenizes a selector string. Throws a SyntaxError if any text is
// found which does not form a valid token.
var REs = {
whitespace: /^\s+/g,
'.': /^\./g,
id: new RegExp('^' + $.utils.code.regexps.identifierName.source, 'g'),
number: /^\d+/g,
'[': /^\[/g,
']': /^\]/g,
'{': /^\{/g,
'}': /^\}/g,
'^': /^\^/g,
str: new RegExp('^' + $.utils.code.regexps.string.source, 'g'),
};
var tokens = [];
NEXT_TOKEN: for (var index = 0; index < selector.length; ) {
for (var tokenType in REs) {
if (!REs.hasOwnProperty(tokenType)) continue;
var re = REs[tokenType];
re.lastIndex = 0;
var m = re.exec(selector.slice(index));
if (!m) continue; // No match. Try next regexp.
tokens.push({
type: tokenType,
raw: m[0],
valid: true,
index: index,
});
index += re.lastIndex;
continue NEXT_TOKEN;
}
// No token matched.
throw new SyntaxError('invalid selector ' + selector);
}
// Postprocess token list to get values.
for(var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (token.type === 'number') {
token.value = Number(token.raw);
} else if (token.type === 'str') {
token.value = $.utils.code.parseString(token.raw);
}
}
return tokens;
};
Object.setOwnerOf($.Selector.parse.tokenize, $.physicals.Maximilian);
$.Selector.for = function Selector_for(object) {
/* Return a Selector for object, or undefined if none known.
*/
return this.db.get(object);
};
Object.setOwnerOf($.Selector.for, $.physicals.Maximilian);
$.Selector.db = {};
$.Selector.db.map_ = new WeakMap();
$.Selector.db.set = function set(object, selector) {
if (!$.utils.isObject(object)) return; // Ignore non-object values.
if (!(selector instanceof $.Selector)) {
throw new TypeError('Second argument must be a Selector');
}
var selectorString = selector.toString();
var known = this.map_.get(object) || [];
// See if this selector is already known.
if (known.includes(selectorString)) return; // Already known. Ignore.
// Add new entry.
known.push(selectorString);
// Sort by badness, trim to length and save.
$.Selector.sortByBadness(known);
this.map_.set(object, known.slice(0, this.diversityLimit));
};
Object.setOwnerOf($.Selector.db.set, $.physicals.Maximilian);
$.Selector.db.README = 'Selector.db is database mapping objects to Selectors.\n\nThis info is stored in Selector.db.map_, which is a WeakMap mapping objects to entries.\n\nEach entry is an object whose keys are selector strings and values are the corresponding Selectors (i.e., parts lists).';
$.Selector.db.diversityLimit = 5;
$.Selector.db.get = function get(object) {
if (!$.utils.isObject(object)) return undefined;
var known = this.map_.get(object);
while (known && known.length) {
var selector = new $.Selector(known[0]);
var value = null;
try {
value = selector.toValue();
} catch (e) {}
if (value === object) {
return selector;
} else {
known.shift(); // Remove 0th item.
}
}
return undefined; // Ran out of known, valid selectors.
};
Object.setOwnerOf($.Selector.db.get, $.physicals.Maximilian);
$.Selector.db.populate = function populate() {
/* Spider the object graph, starting from the global scope, to
* (re)build the reverse-lookup database.
*
* We apply a version of Dijkstra's algorithm, specifically a BFS
* over valid Selectors, where we reenqueue children of previously-
* -visited objects if we find a better Selector for the parent
* object.
*/
// Prevent this function from running more than once at a time.
if (populate.thread_) throw new Error('already running');
try {
populate.thread_ = Thread.current();
var queue = Object.getOwnPropertyNames($.utils.code.getGlobal()).map(
function (ss) {return new $.Selector(ss);});
var seen = new WeakMap();
for (var i = 0; i < queue.length; i++) {
suspend();
var s = queue[i];
var v = s.toValue(/*save:*/true);
if (!$.utils.isObject(v)) continue; // Skip primitives completely.
var best = $.Selector.for(v);
if (seen.has(v) && s !== best) continue;
seen.set(v, true);
var parts = [$.Selector.PROTOTYPE, $.Selector.OWNER].concat(Object.getOwnPropertyNames(v));
for (var j = 0; j < parts.length; j++) {
var part = parts[j];
if (part === 'cache_') continue; // Skip .cache_ properties.
queue.push(new $.Selector(s.concat(part)));
}
}
} finally {
populate.thread_ = null;
}
};
Object.setOwnerOf($.Selector.db.populate, $.physicals.Maximilian);
Object.setOwnerOf($.Selector.db.populate.prototype, $.physicals.Maximilian);
$.Selector.db.populate.thread_ = null;
$.Selector.sortByBadness = function sortByBadness(selectors) {
// Sort an array (or arraylike), which may contain Selectors,
// (valid) selector strings, or a mix of the two, according to their
// score, as returned by Selector.prototype.badness(), with the the
// lowest-badness ones sorted first.
if (!$.utils.isObject(selectors) || typeof selectors.length != 'number') {
throw new TypeError('argument must be an arraylike');
}
// Begin by populating a badness cache, for quick lookups.
var cache = sortByBadness.cache_;
for (var i = 0; i < selectors.length; i++) {
var s, ss = selectors[i];
if (typeof ss === 'string') {
s = new $.Selector(ss);
} else if (ss instanceof $.Selector) {
s = ss;
ss = ss.toString();
}
if (ss in cache) continue;
cache[ss] = s.badness();
}
// Do sort. Optimised for sorting selector strings, since
// that's what's needed by $.Selector.db.put.
Array.prototype.sort.call(selectors, function compare(a, b) {
if (typeof a !== 'string') a = String(a);
if (typeof b !== 'string') b = String(b);
return cache[a] - cache[b];
});
return selectors;
};
Object.setOwnerOf($.Selector.sortByBadness, $.physicals.Maximilian);
$.utils.Binding = function Binding(object, part) {
/* A binding is essentially just an (object, part) tuple, where part
* is a string or a Selector.SpecialPart.
*
* If object is null, part must be a string conforming to the
* syntax of an identifier; in this case the binding represents
* a variable in the global scope.
*/
if (object === null) {
if (!$.utils.code.isIdentifier(part)) {
throw TypeError('Invalid variable name');
}
} else if (!$.utils.isObject(object)) {
throw TypeError('Invalid object');
} else if (typeof part !== 'string' &&
part !== $.Selector.PROTOTYPE &&
part !== $.Selector.OWNER) {
throw TypeError('Invalid part');
}
this.object = object;
this.part = part;
};
Object.setOwnerOf($.utils.Binding, $.physicals.Maximilian);
$.utils.Binding.prototype.set = function set(value) {
/* Set the value of the binding. Throws TypeError if unable.
*/
if (this.object === null) {
if (!Object.prototype.hasOwnProperty.call($.utils.code.getGlobal(), this.part)) {
throw new TypeError("Can't create new global variable");
}
// Use a temporary property and an eval in the global scope (eval
// by any other name, literally) to set the global variable
// "safely". The temporary property is placed on $ rather than
// using $.db.tempId to avoid the possibility of the eval
// somehow being subverted to access a different value than
// expected due to one of the intervening objects being
// compromised (by a getter, say).
var tmpId;
do {
tmpId = 'tmp' + Math.floor(Math.random() * 0xFFFFFFFF);
} while (tmpId in $);
var evalGlobal = eval;
try {
$[tmpId] = value;
evalGlobal(this.part + ' = $.' + tmpId);
} finally {
delete $[tmpId];
}
} else if(this.part === $.Selector.PROTOTYPE) {
Object.setPrototypeOf(this.object, value);
} else if(this.part === $.Selector.OWNER) {
Object.setOwnerOf(this.object, value);
} else {
// BUG: doesn't handle non-writable properties.
this.object[this.part] = value;
}
};
Object.setOwnerOf($.utils.Binding.prototype.set, $.physicals.Maximilian);
$.utils.Binding.prototype.get = function get(inherited) {
/* Return the current value of the binding, or undefined if the
* binding does not exist.
*
* If inherited is true and the binding is a property binding that
* does not exist on the object, any inherited value will be returned
* instead.
*/
var part = this.part;
if (this.object === null) {
if (!$.utils.code.isIdentifier(part)) {
throw new TypeError('invalid variable identifier');
}
var evalGlobal = eval;
return evalGlobal(part);
} else if(part === $.Selector.PROTOTYPE) {
return Object.getPrototypeOf(this.object);
} else if(part === $.Selector.OWNER) {
return Object.getOwnerOf(this.object);
}
if (inherited || Object.prototype.hasOwnProperty.call(this.object, part)) {
return this.object[part];
} else {
return undefined;
}
};
Object.setOwnerOf($.utils.Binding.prototype.get, $.physicals.Maximilian);
$.utils.Binding.prototype.isOwner = function isOwner() {
/* Returns true iff the binding is an bject owner binding.
*/
return this.part === $.Selector.OWNER;
};
Object.setOwnerOf($.utils.Binding.prototype.isOwner, $.physicals.Maximilian);
$.utils.Binding.prototype.isProp = function isProp() {
/* Returns true iff the binding is an object property binding.
*/
return this.object !== null && typeof this.part === 'string';
};
Object.setOwnerOf($.utils.Binding.prototype.isProp, $.physicals.Maximilian);
$.utils.Binding.prototype.isProto = function isProto() {
/* Returns true iff the binding is an object prototype binding.
*/
return this.part === $.Selector.PROTOTYPE;
};
Object.setOwnerOf($.utils.Binding.prototype.isProto, $.physicals.Maximilian);
$.utils.Binding.prototype.isVar = function isVar() {
/* Returns true iff the binding is a top-level variable binding.
*/
return this.object === null;
};
Object.setOwnerOf($.utils.Binding.prototype.isVar, $.physicals.Maximilian);
$.utils.Binding.prototype.exists = function exists() {
/* Returns true iff the binding exists.
*/
var part = this.part;
if (this.object === null) {
if (!$.utils.code.isIdentifier(part)) {
throw new TypeError('invalid variable identifier');
}
var evalGlobal = eval;
try {
globalEval(varName);
return true;
} catch (e) {
return false;
}
} else if(part === $.Selector.PROTOTYPE ||
part === $.Selector.OWNER) {
return true;
}
return Object.prototype.hasOwnProperty.call(this.object, part);
};
Object.setOwnerOf($.utils.Binding.prototype.exists, $.physicals.Maximilian);
Object.setOwnerOf($.utils.Binding.prototype.exists.prototype, $.physicals.Maximilian);
$.utils.Binding.from = function from(selector) {
/* Create and return a Binding for the given selector - that is,
* such that Binding.from(s).get() === s.toValue().
*/
var part = selector[selector.length - 1];
if (selector.isVar()) {
// Global variable; no parent object.
return new this(null, part);
}
var parent = new $.Selector(selector);
parent.pop();
var object = parent.toValue();
if (!$.utils.isObject(object)) {
throw new TypeError(String(parent) + ' is not an object');
}
return new this(object, part);
};
Object.setOwnerOf($.utils.Binding.from, $.physicals.Maximilian);
$.Selector.cache_ = (new 'Object.create')(null);
$.Selector.sortByBadness.cache_ = (new 'Object.create')(null);
================================================
FILE: core/core_20_$.utils.html.js
================================================
/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview HTML utilities for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.utils.html = {};
$.utils.html.escape = function escape(text) {
// Escape text so that it is safe to print as HTML.
return String(text).replace(/&/g, '&').replace(/"/g, '"')
.replace(//g, '>');
};
Object.setOwnerOf($.utils.html.escape, $.physicals.Maximilian);
$.utils.html.preserveWhitespace = function preserveWhitespace(text) {
// Escape text so that it is safe and preserves whitespace formatting as HTML.
// Runs of three spaces (' ') need to be escaped twice ('_ ', '__ ').
return $.utils.html.escape(text)
.replace(/\t/g, '\u00A0 \u00A0 ')
.replace(/ /g, '\u00A0 ').replace(/ /g, '\u00A0 ') // Escape twice.
.replace(/^ /gm, '\u00A0')
.replace(/\n/g, ' ');
};
Object.setOwnerOf($.utils.html.preserveWhitespace, $.physicals.Maximilian);
================================================
FILE: core/core_21_$.jssp.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview JavaScript Server Pages for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.jssp = {};
Object.setOwnerOf($.jssp, $.physicals.Neil);
$.jssp.OutputBuffer = function OutputBuffer() {
/* An OutputBuffer is a mock $.servers.http.Response, used wheen we
* want a Jssp to produce a string rather than write to an HTTP
* client.
*/
this.buffer_ = '';
};
Object.setOwnerOf($.jssp.OutputBuffer, $.physicals.Maximilian);
$.jssp.OutputBuffer.prototype.write = function write(text) {
this.buffer_ += String(text);
};
Object.setOwnerOf($.jssp.OutputBuffer.prototype.write, $.physicals.Neil);
$.jssp.OutputBuffer.prototype.toString = function toString() {
return this.buffer_;
};
Object.setOwnerOf($.jssp.OutputBuffer.prototype.toString, $.physicals.Neil);
$.jssp.OutputBuffer.prototype.writeEscaped = function writeEscaped(text) {
// Same as .write, but HTML-escape the text first.
this.write($.utils.html.escape(text));
};
Object.setOwnerOf($.jssp.OutputBuffer.prototype.writeEscaped, $.physicals.Neil);
Object.setOwnerOf($.jssp.OutputBuffer.prototype.writeEscaped.prototype, $.physicals.Neil);
$.jssp.eval = function $_jssp_eval(obj, prop, opt_request, opt_response) {
/* Compile and run a JavaScript Server Page.
*
* The specified property on the given object will, if it is a string,
* be compiled to a function and then called.
*
* TODO: cache the compiled JSSP. Separate copy per owner?
*
* Arguments:
* - obj: Object - an object containing a property which is a JSSP source
* string, and which will be used as the value of 'this' when the
* the resulting function is called.
* - prop: string - name of the property on obj that contains the JSSP source.
* - opt_request: any - a value to be passed as the first argument to the
* compiled function. Most typically an instnace of $.servers.http.Request
* or some kind of options object.
* - opt_response: {write: function(string)} | undefined - an object to
* accumulate generated output. Most typically an instance of
* $.servers.http.Response. If omitted, a $.jssp.OutputBuffer will be
* supplied, and the accumulated output returned by eval as a string.
*
* Returns: any - if opt_response was omitted, this will be the generated
* string; otherwise, it will be the actual return value of the compiled
* function (typically undefined).
*/
if (!$.utils.isObject(obj)) {
throw new TypeError('first argument must be an object');
} else if (!(prop in obj)) {
throw new RangeError('"' + prop + '" not on object.');
}
var source = obj[prop];
if (typeof source !== 'string') {
throw TypeError('source property "' + prop + '" must be a string');
}
// Switch to the JSSP owner's permissions. The owner of the JSSP might
// not be the object's owner if the property is inherited.
var locationObj = $.utils.object.getPropertyLocation(obj, prop);
setPerms(Object.getOwnerOf(locationObj));
var request = opt_request;
var response = opt_response || new $.jssp.OutputBuffer();
// Compile source into a function.
var code = this.compile_(source);
code = '\n' +
'var this_ = this;\n' +
'function include(prop) {return $.jssp.eval(this_, prop, request, response);}\n' +
code;
var func;
try {
func = new Function('request, response', code);
} catch (e) {
suspend();
$.system.log('JSSP compilation error. ' + String(e) +
'. Code was:\n' + code.split('\n')
.map(function (line, lineNumber) {
return String(lineNumber) + ': ' + line;})
.join('\n'));
throw e;
}
// Create a .name for this function.
var selector = $.Selector.for(locationObj);
if (selector) {
selector = new $.Selector(selector.concat(prop));
Object.defineProperty(func, 'name', {value: selector.toString(), configurable: true});
}
var result = func.call(obj, request, response);
return opt_response ? result : response.toString();
};
Object.setOwnerOf($.jssp.eval, $.physicals.Maximilian);
Object.setOwnerOf($.jssp.eval.prototype, $.physicals.Neil);
$.jssp.compile_ = function compile_(src) {
/* Compile JavaScript Server Page srouce and return the translated source
* if successful. It is left to the caller to pass the resulting source
* code to the Function constructor.
*
* Arguments:
* - src: string - the JSSP source code.
* Returns: string - the JavaScript generated from src.
*/
if (typeof src !== 'string') {
throw new TypeError('src must be a string');
}
var tokens = src.trim().split(/(<%(?:--|:|=)?|(?:--)?%>)/);
var code = [
'// DO NOT EDIT THIS CODE: AUTOMATICALLY GENERATED BY JSSP ' +
compile_.lastModifiedTime + '.',
];
var STATES = {
LITERAL: 0,
STATEMENT: 1,
EXPRESSION: 2,
EXPRESSION_ESCAPED: 3,
COMMENT: 4
};
var state = STATES.LITERAL;
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (!token) {
continue; // Empty string caused by splitting adjacent tags.
}
switch (state) {
case STATES.LITERAL:
if (token === '<%') {
state = STATES.STATEMENT;
} else if (token === '<%=') {
state = STATES.EXPRESSION;
} else if (token === '<%:') {
state = STATES.EXPRESSION_ESCAPED;
} else if (token === '<%--') {
state = STATES.COMMENT;
} else {
code.push('response.write(' + JSON.stringify(token) + ');');
}
break;
case STATES.STATEMENT:
if (token === '%>') {
state = STATES.LITERAL;
} else {
code.push(token);
}
break;
case STATES.EXPRESSION:
case STATES.EXPRESSION_ESCAPED:
if (token === '%>') {
state = STATES.LITERAL;
} else {
token = token.trim();
if (token) {
code.push();
if (state === STATES.EXPRESSION_ESCAPED) {
code.push('response.writeEscaped(' + token + ');');
} else {
code.push('response.write(' + token + ');');
}
}
}
break;
case STATES.COMMENT:
if (token === '--%>') {
state = STATES.LITERAL;
}
break;
}
}
if (state !== STATES.LITERAL) {
throw new SyntaxError('unclosed JSSP tag');
}
return code.join('\n') + '\n';
};
Object.setOwnerOf($.jssp.compile_, $.physicals.Neil);
Object.setOwnerOf($.jssp.compile_.prototype, $.physicals.Maximilian);
================================================
FILE: core/core_22_$.connection.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Connection object for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.connection = {};
$.connection.onConnect = function onConnect() {
this.connectTime = Date.now();
this.user = null;
this.buffer = '';
this.connected = true;
};
Object.setOwnerOf($.connection.onConnect, $.physicals.Maximilian);
Object.setOwnerOf($.connection.onConnect.prototype, $.physicals.Maximilian);
$.connection.onReceive = function onReceive(text) {
this.buffer += text.replace(/\r/g, '');
var lf;
while ((lf = this.buffer.indexOf('\n')) !== -1) {
var line = this.buffer.substring(0, lf);
this.buffer = this.buffer.substring(lf + 1);
this.onReceiveLine(line);
}
};
Object.setOwnerOf($.connection.onReceive, $.physicals.Maximilian);
$.connection.onReceiveLine = function onReceiveLine(text) {
// Override this on child classes.
};
Object.setOwnerOf($.connection.onReceiveLine, $.physicals.Maximilian);
$.connection.onEnd = function onEnd() {
this.connected = false;
this.disconnectTime = Date.now();
this.close();
};
Object.setOwnerOf($.connection.onEnd, $.physicals.Maximilian);
$.connection.write = function write(text) {
$.system.connectionWrite(this, text);
};
Object.setOwnerOf($.connection.write, $.physicals.Maximilian);
$.connection.close = function close() {
$.system.connectionClose(this);
};
Object.setOwnerOf($.connection.close, $.physicals.Maximilian);
$.connection.onError = function onError(error) {
// TODO: add check for error that occurs when relistening
// fails when restarting server from checkpoint.
if (error.message === 'write after end' ||
error.message === 'This socket has been ended by the other party') {
this.connected = false;
}
};
Object.setOwnerOf($.connection.onError, $.physicals.Maximilian);
================================================
FILE: core/core_23_$.servers.http.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Webserver for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.utils.url = {};
Object.setOwnerOf($.utils.url, $.physicals.Maximilian);
$.utils.url.regexps = {};
$.utils.url.regexps.README = '$.utils.url.regexps contains some RegExps useful for parsing or otherwise analysing URLs.\n\nSee ._generate() for how they are constructed and what they will match.';
$.utils.url.regexps._generate = function _generate() {
/* Generate some RegExps that match various parts of URLs. The
* intention is that these regular expressions conform to the
* grammar given in RFC 3986, "Uniform Resource Identifier (URI):
* Generic Syntax" (https://tools.ietf.org/html/rfc3986).
*
* TODO: add tests for generated RegExps.
*/
////////////////////////////////////////////////////////////////////
// IPv4 Addresses.
// Based on https://stackoverflow.com/a/14453696/4969945
// Matches an octet, optionally with leading zeros.
var octet = '(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})';
// Matches an octet without no leading zeros.
var octetStrict = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])';
// Globally matches IPv4 addresses, optionally with leading zeros.
this.ipv4Address = new RegExp(octet + '(?:\\.' + octet + '){3}', 'g');
// Globally matches IPv4 addresses with no leading zeros.
this.ipv4AddressStrict =
new RegExp(octetStrict + '(?:\\.' + octetStrict + '){3}', 'g');
////////////////////////////////////////////////////////////////////
// IPv6 Addresses.
// Based on https://stackoverflow.com/a/17871737/4969945 but modified
// to reduce ambiguity match more greedily when unanchored.
// Matches a 32-bit hex value as it can appear in an IPv6 address.
var word = '[0-9a-fA-F]{1,4}';
// Matches an IPv4 address as it can appear in an IPv6 address.
var ipv4 = this.ipv4AddressStrict.source;
// Globally matches a valid IPv6 address.
this.ipv6Address = new RegExp(
'(?:' +
'[fF][eE]80:(?::' + word + '){0,4}%[0-9a-zA-Z]+|' + // fe80::7:8%eth0 fe80::7:8%1 (link-local IPv6 addresses with zone index)
'(?:' + word + ':){1,4}:' + ipv4 + '|' + // 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 (IPv4-Embedded IPv6 Address)
'(?:' + word + ':){7}' + word + '|' + // 1:2:3:4:5:6:7:8
'(?:' + word + ':){6}(?::' + word + '){1,1}|' + // 1:2:3:4:5:6::8 ... 1:2:3:4:5:6::8
'(?:' + word + ':){5}(?::' + word + '){1,2}|' + // 1:2:3:4:5::8 ... 1:2:3:4:5::7:8
'(?:' + word + ':){4}(?::' + word + '){1,3}|' + // 1:2:3:4::8 ... 1:2:3:4::6:7:8
'(?:' + word + ':){3}(?::' + word + '){1,4}|' + // 1:2:3::8 ... 1:2:3::5:6:7:8
'(?:' + word + ':){2}(?::' + word + '){1,5}|' + // 1:2::8 ... 1:2::4:5:6:7:8
'(?:' + word + ':){1}(?::' + word + '){1,6}|' + // 1::8 ... 1::3:4:5:6:7:8
'(?:' + word + ':){1,7}:|' + // 1:: ... 1:2:3:4:5:6:7::
'::(?:[fF]{4}(?::0{1,4})?:)?' + ipv4 + '|' + // ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses and IPv4-translated addresses)
':(?::' + word + '){1,7}|' + // ::8 ... ::2:3:4:5:6:7:8
'::' + // ::
')', 'g');
////////////////////////////////////////////////////////////////////
// DNS Domain Names.
// Matches a label (per RFC 952, updated by RFC 1123 to allow a
// it to begin with a digit), limited to 63 charcters (per RFC 1035).
var label = '[a-zA-Z0-9][a-zA-Z0-9-]{0,62}';
// Globally matches a legal (but not necessary valid!) DNS name.
// See also https://stackoverflow.com/q/106179/4969945 .
// BUG: does not limit length of name to ca. 253 characters (see
// https://devblogs.microsoft.com/oldnewthing/20120412-00/?p=7873
// for gory details).
this.dnsAddress = new RegExp(label + '(?:\\.' + label + ')*', 'g');
////////////////////////////////////////////////////////////////////
// Authority section
// Globally matches a valid IP address (v4 or v6).
this.ipAddress = new RegExp(
this.ipv4Address.source + '|\\[' + this.ipv6Address.source + '\\]', 'g');
// Globally matches a valid URL authority section (e.g. domain name
// and port); this is (not coincidentally) also the same as a valid
// HTTP Host: header value.
//
// The RegExp includes capture groups for an IP address [1] *or* a
// DNS address [2], and (optionally) a port number [3].
this.authority = new RegExp(
'(?:(' + this.ipAddress.source + ')|' +
'(' + this.dnsAddress.source + '))' +
'(?::([0-9]+))?', 'g'); // Optional port number.
////////////////////////////////////////////////////////////////////
// Exact forms of the above. These do not get the global flag.
var keys = ['ipv4Address', 'ipv4AddressStrict', 'ipv6Address',
'dnsAddress', 'ipAddress', 'authority'];
for (var key, i = 0; (key = keys[i]); i++) {
this[key + 'Exact'] = new RegExp('^' + this[key].source + '$');
}
};
Object.setOwnerOf($.utils.url.regexps._generate.prototype, $.physicals.Maximilian);
$.utils.url.regexps.ipv4Address = /(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})){3}/g;
$.utils.url.regexps.ipv4AddressStrict = /(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}/g;
$.utils.url.regexps.ipv6Address = /(?:[fF][eE]80:(?::[0-9a-fA-F]{1,4}){0,4}%[0-9a-zA-Z]+|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){6}(?::[0-9a-fA-F]{1,4}){1,1}|(?:[0-9a-fA-F]{1,4}:){5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){1}(?::[0-9a-fA-F]{1,4}){1,6}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::(?:[fF]{4}(?::0{1,4})?:)?(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|:(?::[0-9a-fA-F]{1,4}){1,7}|::)/g;
$.utils.url.regexps.dnsAddress = /[a-zA-Z0-9][a-zA-Z0-9-]{0,62}(?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,62})*/g;
$.utils.url.regexps.ipAddress = /(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})){3}|\[(?:[fF][eE]80:(?::[0-9a-fA-F]{1,4}){0,4}%[0-9a-zA-Z]+|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){6}(?::[0-9a-fA-F]{1,4}){1,1}|(?:[0-9a-fA-F]{1,4}:){5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){1}(?::[0-9a-fA-F]{1,4}){1,6}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::(?:[fF]{4}(?::0{1,4})?:)?(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|:(?::[0-9a-fA-F]{1,4}){1,7}|::)\]/g;
$.utils.url.regexps.authority = /(?:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})){3}|\[(?:[fF][eE]80:(?::[0-9a-fA-F]{1,4}){0,4}%[0-9a-zA-Z]+|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){6}(?::[0-9a-fA-F]{1,4}){1,1}|(?:[0-9a-fA-F]{1,4}:){5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){1}(?::[0-9a-fA-F]{1,4}){1,6}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::(?:[fF]{4}(?::0{1,4})?:)?(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|:(?::[0-9a-fA-F]{1,4}){1,7}|::)\])|([a-zA-Z0-9][a-zA-Z0-9-]{0,62}(?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,62})*))(?::([0-9]+))?/g;
$.utils.url.regexps.ipv4AddressExact = /^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})){3}$/;
$.utils.url.regexps.ipv4AddressStrictExact = /^(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}$/;
$.utils.url.regexps.ipv6AddressExact = /^(?:[fF][eE]80:(?::[0-9a-fA-F]{1,4}){0,4}%[0-9a-zA-Z]+|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){6}(?::[0-9a-fA-F]{1,4}){1,1}|(?:[0-9a-fA-F]{1,4}:){5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){1}(?::[0-9a-fA-F]{1,4}){1,6}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::(?:[fF]{4}(?::0{1,4})?:)?(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|:(?::[0-9a-fA-F]{1,4}){1,7}|::)$/;
$.utils.url.regexps.dnsAddressExact = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}(?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,62})*$/;
$.utils.url.regexps.ipAddressExact = /^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})){3}|\[(?:[fF][eE]80:(?::[0-9a-fA-F]{1,4}){0,4}%[0-9a-zA-Z]+|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){6}(?::[0-9a-fA-F]{1,4}){1,1}|(?:[0-9a-fA-F]{1,4}:){5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){1}(?::[0-9a-fA-F]{1,4}){1,6}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::(?:[fF]{4}(?::0{1,4})?:)?(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|:(?::[0-9a-fA-F]{1,4}){1,7}|::)\]$/;
$.utils.url.regexps.authorityExact = /^(?:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})){3}|\[(?:[fF][eE]80:(?::[0-9a-fA-F]{1,4}){0,4}%[0-9a-zA-Z]+|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){6}(?::[0-9a-fA-F]{1,4}){1,1}|(?:[0-9a-fA-F]{1,4}:){5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){1}(?::[0-9a-fA-F]{1,4}){1,6}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::(?:[fF]{4}(?::0{1,4})?:)?(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])(?:\.(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])){3}|:(?::[0-9a-fA-F]{1,4}){1,7}|::)\])|([a-zA-Z0-9][a-zA-Z0-9-]{0,62}(?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,62})*))(?::([0-9]+))?$/;
$.servers.http = {};
$.servers.http.STATUS_CODES = (new 'Object.create')(null);
$.servers.http.STATUS_CODES[100] = 'Continue';
$.servers.http.STATUS_CODES[101] = 'Switching Protocols';
$.servers.http.STATUS_CODES[102] = 'Processing';
$.servers.http.STATUS_CODES[200] = 'OK';
$.servers.http.STATUS_CODES[201] = 'Created';
$.servers.http.STATUS_CODES[202] = 'Accepted';
$.servers.http.STATUS_CODES[203] = 'Non-Authoritative Information';
$.servers.http.STATUS_CODES[204] = 'No Content';
$.servers.http.STATUS_CODES[205] = 'Reset Content';
$.servers.http.STATUS_CODES[206] = 'Partial Content';
$.servers.http.STATUS_CODES[207] = 'Multi-Status';
$.servers.http.STATUS_CODES[208] = 'Already Reported';
$.servers.http.STATUS_CODES[226] = 'IM Used';
$.servers.http.STATUS_CODES[300] = 'Multiple Choices';
$.servers.http.STATUS_CODES[301] = 'Moved Permanently';
$.servers.http.STATUS_CODES[302] = 'Found';
$.servers.http.STATUS_CODES[303] = 'See Other';
$.servers.http.STATUS_CODES[304] = 'Not Modified';
$.servers.http.STATUS_CODES[305] = 'Use Proxy';
$.servers.http.STATUS_CODES[306] = 'Switch Proxy';
$.servers.http.STATUS_CODES[307] = 'Temporary Redirect';
$.servers.http.STATUS_CODES[308] = 'Permanent Redirect';
$.servers.http.STATUS_CODES[400] = 'Bad Request';
$.servers.http.STATUS_CODES[401] = 'Unauthorized';
$.servers.http.STATUS_CODES[402] = 'Payment Required';
$.servers.http.STATUS_CODES[403] = 'Forbidden';
$.servers.http.STATUS_CODES[404] = 'Not Found';
$.servers.http.STATUS_CODES[405] = 'Method Not Allowed';
$.servers.http.STATUS_CODES[406] = 'Not Acceptable';
$.servers.http.STATUS_CODES[407] = 'Proxy Authentication Required';
$.servers.http.STATUS_CODES[408] = 'Request Timeout';
$.servers.http.STATUS_CODES[409] = 'Conflict';
$.servers.http.STATUS_CODES[410] = 'Gone';
$.servers.http.STATUS_CODES[411] = 'Length Required';
$.servers.http.STATUS_CODES[412] = 'Precondition Failed';
$.servers.http.STATUS_CODES[413] = 'Payload Too Large';
$.servers.http.STATUS_CODES[414] = 'URI Too Long';
$.servers.http.STATUS_CODES[415] = 'Unsupported Media Type';
$.servers.http.STATUS_CODES[416] = 'Range Not Satisfiable';
$.servers.http.STATUS_CODES[417] = 'Expectation Failed';
$.servers.http.STATUS_CODES[418] = "I'm a teapot";
$.servers.http.STATUS_CODES[421] = 'Misdirected Request';
$.servers.http.STATUS_CODES[422] = 'Unprocessable Entity';
$.servers.http.STATUS_CODES[423] = 'Locked';
$.servers.http.STATUS_CODES[424] = 'Failed Dependency';
$.servers.http.STATUS_CODES[426] = 'Upgrade Required';
$.servers.http.STATUS_CODES[428] = 'Precondition Required';
$.servers.http.STATUS_CODES[429] = 'Too Many Requests';
$.servers.http.STATUS_CODES[431] = 'Request Header Fields Too Large';
$.servers.http.STATUS_CODES[451] = 'Unavailable For Legal Reasons';
$.servers.http.STATUS_CODES[500] = 'Internal Server Error';
$.servers.http.STATUS_CODES[501] = 'Not Implemented';
$.servers.http.STATUS_CODES[502] = 'Bad Gateway';
$.servers.http.STATUS_CODES[503] = 'Service Unavailable';
$.servers.http.STATUS_CODES[504] = 'Gateway Timeout';
$.servers.http.STATUS_CODES[505] = 'HTTP Version Not Supported';
$.servers.http.STATUS_CODES[506] = 'Variant Also Negotiates';
$.servers.http.STATUS_CODES[507] = 'Insufficient Storage';
$.servers.http.STATUS_CODES[508] = 'Loop Detected';
$.servers.http.STATUS_CODES[510] = 'Not Extended';
$.servers.http.STATUS_CODES[511] = 'Network Authentication Required';
$.servers.http.connection = (new 'Object.create')($.connection);
$.servers.http.connection.onConnect = function onConnect() {
$.connection.onConnect.apply(this, arguments);
this.timeout = setTimeout(this.close.bind(this), 60 * 1000);
this.request = new $.servers.http.Request();
this.response = new $.servers.http.Response(this);
};
Object.setOwnerOf($.servers.http.connection.onConnect, $.physicals.Maximilian);
$.servers.http.connection.onReceive = function onReceive(data) {
this.buffer += data;
var lf;
// Start in line-delimited mode, parsing HTTP headers.
while ((lf = this.buffer.indexOf('\n')) !== -1) {
try {
this.onReceiveChunk(this.buffer.substring(0, lf + 1));
} finally {
this.buffer = this.buffer.substring(lf + 1);
}
}
if (this.request.state_ === 'body') {
// Waiting for POST data, not line-delimited.
this.onReceiveChunk(this.buffer);
this.buffer = '';
}
};
Object.setOwnerOf($.servers.http.connection.onReceive, $.physicals.Neil);
$.servers.http.connection.onReceiveChunk = function onReceiveChunk(chunk) {
if (this.request.parse(chunk)) {
$.servers.http.onRequest(this);
}
// Otherwise wait for more lines to arrive.
};
Object.setOwnerOf($.servers.http.connection.onReceiveChunk, $.physicals.Maximilian);
$.servers.http.connection.onEnd = function onEnd() {
clearTimeout(this.timeout);
$.connection.onEnd.apply(this, arguments);
};
Object.setOwnerOf($.servers.http.connection.onEnd, $.physicals.Neil);
$.servers.http.Request = function Request() {
this.headers = Object.create(null);
this.headers.cookie = Object.create(null);
this.parameters = Object.create(null);
// One of 'invalid', 'request', 'headers', 'body', 'done'.
this.state_ = 'request';
};
Object.setOwnerOf($.servers.http.Request, $.physicals.Maximilian);
$.servers.http.Request.prototype.parse = function parse(line) {
// Returns true if parsing is complete, false if more lines are needed.
if (this.state_ === 'request') {
// Match "GET /images/logo.png HTTP/1.1"
line = line.trim();
var m = line.match(/^(GET|POST) +(\S+)/);
if (!m) {
$.system.log('Unrecognized WWW request line:', line);
this.state_ = 'invalid';
return true;
}
this.method = m[1];
this.url = m[2];
this.parseUrl_(this.url);
this.state_ = 'headers';
return false;
}
if (this.state_ === 'headers') {
line = line.trim();
if (!line) { // Done parsing headers.
if (this.method === 'POST') {
this.state_ = 'body';
this.data = '';
return false;
} else {
this.parseParameters_(this.query);
this.state_ = 'done';
this.data = undefined;
return true;
}
}
var m = line.match(/^([-\w]+): +(.+)$/);
if (!m) {
$.system.log('Unrecognized WWW header line:', line);
return false;
}
var name = m[1].toLowerCase();
var value = m[2];
var existing = this.headers[name];
if (name === 'cookie') {
// Cookies are processed and presented as: request.headers.cookie.foo
var cookies = value.split(/\s*;\s*/);
for (var i = 0; i < cookies.length; i++) {
var eqIndex = cookies[i].indexOf('=');
if (eqIndex !== -1) {
var cookieName = cookies[i].substring(0, eqIndex);
var cookieValue = cookies[i].substring(eqIndex + 1);
if (cookieName === 'ID') {
// Special-case the 'ID' cookie for user login.
// Do not expose this ID string to anyone.
this.user = $.userDatabase.get(cookieValue);
} else {
// Regular cookie.
existing[cookieName] = cookieValue;
}
}
}
value = existing;
} else if (name in this.headers) {
if ($.servers.http.IncomingMessage.discardDuplicates.includes(name)) {
// Discard this duplicate.
value = existing;
} else {
// Append this header onto previously defined header.
value = existing + ', ' + value;
}
}
this.headers[name] = value;
return false;
}
if (this.state_ === 'body') {
// POST data.
this.data += line;
if (this.data.length >= this.headers['content-length']) {
this.parseParameters_(this.data);
this.state_ = 'done';
return true;
}
return false;
}
// Invalid state? Extra lines? Ignore.
return true;
};
Object.setOwnerOf($.servers.http.Request.prototype.parse, $.physicals.Neil);
$.servers.http.Request.prototype.parseUrl_ = function parseUrl_(url) {
/* Parse a URL and set this.path and this.query as appropriate:
*
* E.g. given url = '/bar/baz?data', set:
* - this.path = '/bar/baz'
* - this.query = 'data'
*
* Arguments:
* - url: string - the URL to parse.
*
* TODO(cpcallen): add check for leading "/"?
*/
var qIndex = url.indexOf('?');
if (qIndex === -1) {
this.path = url;
} else {
this.path = url.substring(0, qIndex);
this.query = url.substring(qIndex + 1);
}
};
Object.setOwnerOf($.servers.http.Request.prototype.parseUrl_, $.physicals.Maximilian);
$.servers.http.Request.prototype.parseParameters_ = function parseParameters_(data) {
if (!data) {
return;
}
var vars = data.split('&');
var name, value;
for (var i = 0; i < vars.length; i++) {
var eqIndex = vars[i].indexOf('=');
if (eqIndex === -1) {
name = vars[i];
value = true;
} else {
name = vars[i].substring(0, eqIndex);
value = vars[i].substring(eqIndex + 1);
value = decodeURIComponent(value.replace(/\+/g, ' '));
}
if (name in this.parameters) {
// ?foo=1&foo=2&foo=3
var array = this.parameters[name];
if (!Array.isArray(array)) {
array = [array];
}
array.push(value);
value = array;
}
this.parameters[name] = value;
}
};
Object.setOwnerOf($.servers.http.Request.prototype.parseParameters_, $.physicals.Neil);
$.servers.http.Request.prototype.fromSameOrigin = function fromSameOrigin() {
/* Determines if the previous page and the requested page are from the same
* origin. Normally this means that they are from the same subdomain.
* However, if pathToSubdomain is enabled then the first directory name
* is used for comparison.
*
* Return: boolean | undefined - true if from the same origin, false if
* from different origins, and undefined if missing headers prevent a
* firm conclusion one way or the other.
*
* Callers should choose whether to fail-safe or fail-deadly when the
* user's proxy strips the referer header, resulting in undefined.
*
* BUG: if referer was for subdomain routed via a root Host with
* .pathToSubdomain enabled, but .origin is for the root domain,
* fromSameOrigin will return true. This is not intended and possibly
* insecure - but probably mostly harmless: if .pathToSubdomain is
* enabled subdomains are not really secure against each other anyway.
*/
var referer = this.headers.referer; // https://foo.example.codecity.world/bar
if (!referer || !this.info) {
// Missing headers. Not enough information to know.
return undefined;
}
var origin = this.info.origin; // foo.example.codecity.world
var regexp = new RegExp('^https?://' + $.utils.regexp.escape(origin) + '/');
return regexp.test(referer);
};
Object.setOwnerOf($.servers.http.Request.prototype.fromSameOrigin, $.physicals.Maximilian);
$.servers.http.Request.prototype.hostUrl = function hostUrl(varArgs) {
/* Return the base URL for the host that handled this Request, or
* a subdomain (omitting scheme). This is derived from .headers.host,
* but with some extra magic:
*
* - Absent any argument, it will be the URL which routes to the root
* Host object serving this Request - e.g., //example.codecity.world/
* The "root" host is ordinarily just first of $.servers.http.hosts[]
* to accept the request (as opposed to one of its .subdomains).
* - If an argument is supplied, the returned URL will instead be for
* the named subdomain.
* - Multiple arguments can be supplied if there are nested subdomains.
*
* E.g.:
* request.hostUrl() => '//example.codecity.world/'
* request.hostUrl('code') => '//code.example.codecity.world/'
* request.hostUrl('foo', 'bar') => '//foo.bar.example.codecity.world/'
*
* If .pathToSubdomain is enabled on one or more Host object(s):
* request.hostUrl('code') => '//example.codecity.world/code/'
* request.hostUrl('foo', 'bar') => '//example.codecity.world/foo/bar/'
* or: '//bar.example.codecity.world/foo/'
*
* Barring bugs, the returned URL should always end with a '/'.
*
* See also $.servers.http.Host.prototype.url for cases where you need
* to generate a host URL without an incoming Request to use as reference.
*
* Arguments:
* - subdomain: string - a string denoting a subdomain of interest.
* Multiple arguments are allowed. RangeError is thrown if no such
* subdomain exists.
* Returns: string - the URL for the desired domain/subdomain.
*/
// No routing information is available? Fallback to $hosts.root.url().
if (!this.info) {
var rootHost = $.hosts.root;
return rootHost.url.apply(rootHost, arguments);
}
// Walk the tree of Hosts rooted at the root Host via which this
// Request was served.
var host = this.info.rootHost;
var authority = this.info.rootAuthority;
// Did nginx request pathToSubdomain, because it knows that it is not
// configured for real wildcard subdomains? (N.B.: header uses the
// RFC 8941 convention of "?1" for true, "?0" for false.)
var pathToSubdomainHeader =
(this.headers['codecity-pathtosubdomain'] === '?1');
for (var subdomain, i = 0; (subdomain = arguments[i]); i++) {
authority = host.urlForSubdomain(authority, subdomain, pathToSubdomainHeader);
host = host.subdomains[subdomain];
}
return '//' + authority + '/';
};
Object.setOwnerOf($.servers.http.Request.prototype.hostUrl, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.Request.prototype.hostUrl.prototype, $.physicals.Maximilian);
$.servers.http.Request.discardDuplicates = [];
$.servers.http.Request.discardDuplicates[0] = 'authorization';
$.servers.http.Request.discardDuplicates[1] = 'content-length';
$.servers.http.Request.discardDuplicates[2] = 'content-type';
$.servers.http.Request.discardDuplicates[3] = 'from';
$.servers.http.Request.discardDuplicates[4] = 'host';
$.servers.http.Request.discardDuplicates[5] = 'if-modified-since';
$.servers.http.Request.discardDuplicates[6] = 'if-unmodified-since';
$.servers.http.Request.discardDuplicates[7] = 'max-forwards';
$.servers.http.Request.discardDuplicates[8] = 'proxy-authorization';
$.servers.http.Request.discardDuplicates[9] = 'referer';
$.servers.http.Request.discardDuplicates[10] = 'user-agent';
$.servers.http.Response = function Response(connection) {
this.headersSent = false;
this.statusCode = 200;
this.headers_ = Object.create(Response.defaultHeaders);
this.cookies = [];
this.setHeader('content-type', 'text/html; charset=utf-8');
this.connection_ = connection;
};
Object.setOwnerOf($.servers.http.Response, $.physicals.Maximilian);
$.servers.http.Response.prototype.setHeader = function setHeader(name, value) {
if (this.headersSent) {
throw new Error('header already sent');
}
value = String(value).trim();
if (value.includes('\n') || value.includes('\r')) {
throw new RangeError('invalid header value');
}
// Normalize all header names as lowercase.
name = String(name).toLowerCase(name);
if (name === 'set-cookie') {
if (/^\s*ID\s*=/.test(value)) {
throw new PermissionError('not allowed to set ID cookie');
}
this.cookies.push(value);
} else {
var existing = Object.getOwnPropertyDescriptor(this.headers_, name);
if (existing) { // Header already set for this Response specifically.
if ($.servers.http.Response.discardDuplicates.includes(name)) {
// Overwrite existing value.
} else {
// Append this header onto previously defined header.
value = existing.value + ', ' + value;
}
}
this.headers_[name] = value;
}
};
Object.setOwnerOf($.servers.http.Response.prototype.setHeader, $.physicals.Maximilian);
$.servers.http.Response.prototype.writeHead = function writeHead() {
if (this.headersSent) {
throw new Error('Header already sent.');
}
this.headersSent = true;
var statusMessage = $.servers.http.STATUS_CODES[this.statusCode] || 'Unknown';
this.connection_.write('HTTP/1.0 ' + this.statusCode + ' ' + statusMessage +
'\r\n');
for (var name in this.headers_) {
// Print all header names as Title-Case.
var title = name.replace(/\w+/g, $.utils.string.capitalize);
this.connection_.write(title + ': ' + this.headers_[name] + '\r\n');
}
for (var i = 0; i < this.cookies.length; i++) {
// Print all cookies.
this.connection_.write('Set-Cookie: ' + this.cookies[i] + '\r\n');
}
this.connection_.write('\r\n');
};
Object.setOwnerOf($.servers.http.Response.prototype.writeHead, $.physicals.Maximilian);
$.servers.http.Response.prototype.setStatus = function setStatus(statusCode) {
/* Set the status code for this Response.
* Must be called before .writeHead().
*
* - statusCode: number - the HTTP status code to return.
*/
if (!(statusCode in $.servers.http.STATUS_CODES)) {
throw new RangeError('invalid HTTP status code ' + statusCode);
}
if (this.headersSent) {
throw new Error('header already sent.');
}
this.statusCode = statusCode;
};
Object.setOwnerOf($.servers.http.Response.prototype.setStatus, $.physicals.Maximilian);
$.servers.http.Response.prototype.write = function write(text) {
text = String(text);
if (text !== '') {
if (!this.headersSent) {
this.writeHead();
}
this.connection_.write(text);
}
};
Object.setOwnerOf($.servers.http.Response.prototype.write, $.physicals.Neil);
$.servers.http.Response.prototype.clearIdCookie = function clearIdCookie() {
// TODO: Security check goes here. Should be only callable by logout.
if (this.headersSent) {
throw new Error('Header already sent');
}
var request = this.connection_.request;
// Guess cookie domain.
var rootHost =
(request.info && request.info.rootAuthority) || // Request rootAuthority.
request.headers.host || // Actual Host: header value for the request.
$.hosts.root.hostname; // Configuerd hostname
var domain = rootHost ? ' Domain=' + rootHost.replace(/:\d+$/, '') : ''; // Remove port number.
var value = 'ID=; HttpOnly;' + domain + '; Path=/; Max-Age=0;';
this.cookies.push(value);
};
Object.setOwnerOf($.servers.http.Response.prototype.clearIdCookie, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.Response.prototype.clearIdCookie.prototype, $.physicals.Neil);
$.servers.http.Response.prototype.sendRedirect = function sendRedirect(url, statusCode) {
/* Write a redirect as the response.
*
* Must be called before writeHeader has been called.
*
* Arguments:
* - url: string - the destination URL for the redirect.
* - statusCode?: number - optional HTTP status code (default: 303 See Other).
*/
if (!statusCode) statusCode = 303;
this.setStatus(statusCode);
this.setHeader('Location', url);
this.writeHead();
};
Object.setOwnerOf($.servers.http.Response.prototype.sendRedirect, $.physicals.Neil);
Object.setOwnerOf($.servers.http.Response.prototype.sendRedirect.prototype, $.physicals.Maximilian);
$.servers.http.Response.prototype.sendError = function sendError(statusCode, message) {
/* Send an error status and page as the response.
*
* Must be called before writeHeader has been called. Writes a complete HTML
* document to the connection, but doesn't close the connection.
*
* Arguments:
* - statusCode: number - an HTTP status code.
* - message?: string | Error - optional status message or Error instance.
*/
this.setStatus(statusCode);
if (message instanceof Error) {
this.errorMessage_ = $.utils.html.escape(String(message)) +
'
\n <%= response.errorMessage_ %>\n\n';
$.servers.http.Response.prototype.writeEscaped = $.jssp.OutputBuffer.prototype.writeEscaped;
$.servers.http.Response.discardDuplicates = [];
$.servers.http.Response.discardDuplicates[0] = 'age';
$.servers.http.Response.discardDuplicates[1] = 'content-length';
$.servers.http.Response.discardDuplicates[2] = 'content-type';
$.servers.http.Response.discardDuplicates[3] = 'etag';
$.servers.http.Response.discardDuplicates[4] = 'expires';
$.servers.http.Response.discardDuplicates[5] = 'last-modified';
$.servers.http.Response.discardDuplicates[6] = 'location';
$.servers.http.Response.discardDuplicates[7] = 'retry-after';
$.servers.http.Response.defaultHeaders = (new 'Object.create')(null);
$.servers.http.Response.defaultHeaders['cache-control'] = 'no-store';
$.servers.http.Response.defaultHeaders.server = 'CodeCity/0.0 ($.servers.http)';
$.servers.http.Host = function Host() {
/* A Host object represents a domain or subdomain served by the
* web server. It is expected that most Host instances will be
* the values of properties of $.hosts.
*
* Methods on Host.prototype (see individual methodd documentation
* for details):
*
* - .addSubdomain() - add a new subdomain to .subdomains.
* - .handle() - try to have this host handle an incoming request.
* - .url() - return the URL for this host.
* - .urlForSubdomain() - a helper method for .url().
*
* Instance properties of Host objects (by default these all
* inherit their default values from Host.prototype):
*
* - access: string - Access control switch. It has the following
* possible values:
*
* - 'public': The host will by default serve pages to any client
* unless the handler object has .wwwAccess === 'private', in
* which case it will only be served to logged-in users.
* Unauthenticated clients will get 403 forbidden and be
* directed to login.
*
* - 'private': The host will by default only serve pages to
* logged-in users unless the hander object has .wwwAccess ===
* 'public'.
*
* - 'hidden': The host will only serve pages to logged-in users;
* any unauthenticated client will be declined (by .handle
* returning false) which will normally result in them recieving
* a 400 Unknown Host error.
*
* - hostname: string | undefined - the canonical hostname for
* this Host object. Should include the port number, if non-default.
*
* If .hostRegExp (see below) is undefined, .hostname will be used to
* decide which Requests to handle:
*
* - This host object will serve requests whose Host: header exactly
* matches .hostname itself.
* - It will pass requests whose Host: header ends with .hostname to
* the corresponding subdomain, if it exists.
*
* E.g., if .hostname = 'bar.baz', this host will serve requests for
* bar.baz and will pass requests for foo.bar.baz to .subdomains.foo
* but will reject requests for bar.baz:8080.
*
* If both .hostname and .hostRegExp are undefined, all requests will
* be served by this host or automagically passed along to a suitable
* subdomain.
*
* - hostRegExp: RegExp | undefined - a RegExp matching Host: header
* values this host should respond to. If undefined (the default),
* this .hostname (see above) will be used instead.
*
* Note that when the Host is deciding whether to serve a Request
* itself, this regexp will be treated as if it begins with /^/,
* while when the Host is trying to determine if it should be passed
* off to a subdomain it will treat it as if it begins with /(?<=\.)/
* (despite ES5.1 not supporting such look-behind assertions).
*
* This means that .hostRegExp = /bar.baz$/ will cause this Host object
* to serve 'bar.baz' and try to pass 'foo.bar.baz' to .subdomains.foo,
* but will always reject 'foobar.baz'. Anchoring with /^/ will
* prevent subdomain matching, so don't do that if you don't mean to!
*
* It's recommended that .hostRegExp be anchored with /$/, but note
* that if you want to serve pages on a non-standard ports you must
* match the port as well - e.g.: /example\.codecity\.\w+(?::\d+)$/
* will match example.codecity. with or without a port number.
*
* - pathToSubdomain: boolean | undefined - enable (or disable) mapping
* the first element (directory) of request paths to a subdomain.
*
* If set to undefined, the CodeCity-pathToSubdomain header will be
* used to determine decide whether to do this mapping on a
* per-request basis (but normally this header will be configured
* staticaly in the nginx reverse-proxy configuration).
*
* The CodeCity-pathToSubdomain header will be interpreted according
* to the RFC 8941 Structure Field Values convention, with '?1'
* meaning true and '?0' meaning false. Any other value will be
* treated as false.
*
* - subdomains: Object | null - a null-prototype
* object mapping subdomain names to their respective Host objects,
* or just null if there are no subdomains. Use .addSubdomain to
* add entries to this mapping. (Default: null.)
*/
};
Object.setOwnerOf($.servers.http.Host, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.Host.prototype, $.physicals.Maximilian);
$.servers.http.Host.prototype.handle = function handle(request, response, info) {
/* Attempt to handle an http(s) request. First tries to see if the
* request can be served by the Host object of a direct subdomain
* of this Host, then tries to handle itself, then, if
* this.pseudoSubdomains is enabled, attempts to route the request
* to a subdomain Host based on the first component of the path.
*
* Arguments:
* - request: $.servers.http.Request - the incoming request to handle.
* - response: $.servers.http.Response - the response to write to.
* - info: Object | undefined - some information used by recursive calls to
* this function.
* Returns: boolean - true iff request was for this host.
*/
if (!info) {
// Extact detailed routing info from request.
var hostHeader = request.headers.host;
info = {
origin: hostHeader, // For Request.prototype.fromSameOrigin.
path: request.path,
rootHost: this,
};
// The authorityExact RegExp gives submatches [ipAddress, dnsAddress,
// port]. Only one of the addresses capture groups will match.
var m = $.utils.url.regexps.authorityExact.exec(hostHeader);
if (!m) { // Invalid Host header.
response.sendError(400, 'Invalid Host header.');
return true; // We don't want it, but no one else should either.
} else if (m[1]) { // It's an IP address. No (real) subdomains possible.
info.rootAuthority = info.authority = hostHeader;
} else if (this.hostRegExp || this.hostname) {
var hostRegExp = this.hostRegExp ? this.hostRegExp :
new RegExp($.utils.regexp.escape(this.hostname) + '$')
// We have a .hostname or .hostRegExp, and can work out if there is a
// subdomain prefixed to request.headers.host from that.
m = hostRegExp.exec(hostHeader);
if (!m) return false; // Did not match. Not for us.
// Apply a check equivalent to a /(?<=^|\.)/ look-behind assertion.
if (m.index > 0) {
if (hostHeader[m.index - 1] !== '.') return false; // Lookbehind failed.
// Record subdomain(s) that need to be matched.
info.subdomains = hostHeader.slice(0, m.index - 1).split('.');
}
info.rootAuthority = info.authority = hostHeader.slice(m.index);
} else {
// Try to guess where the subdomain(s) end and the root hostname begins.
// To deal correclty with cases like x.x.y.z and x.y.x.y.z, be
// pessimistic and assume they're all subdomains to start with.
var potentialSubdomains = hostHeader.split('.');
info.rootAuthority = info.authority = potentialSubdomains.pop(); // TLD can't be subdomain!
for (var i = potentialSubdomains.length; i >= 0; i--) {
info.subdomains = potentialSubdomains.slice(0, i);
var r = this.handle(request, response, info);
if (r) return r;
info.rootAuthority = info.authority = potentialSubdomains[i - 1] + '.' + info.authority;
}
return false;
}
}
// Is this request for us or a subdomain of us?
if (!this.matchHostname_(info.authority)) return false;
// This request is for us or a subdomain.
// Do we need to try to find a subdomain for this request?
if (info.subdomains && info.subdomains.length) {
var subdomain = info.subdomains.pop();
if (!(subdomain in this.subdomains)) return false;
info.authority = subdomain + '.' + info.authority;
return this.subdomains[subdomain].handle(request, response, info);
}
// No, it's for us. Should we hide from unauthenticated clients?
if (this.access === 'hidden' && !($.user.isPrototypeOf(request.user))) {
return false;
}
// No. Serve reqeust.
this.route_(request, response, info);
return true;
};
Object.setOwnerOf($.servers.http.Host.prototype.handle, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.Host.prototype.handle.prototype, $.physicals.Maximilian);
$.servers.http.Host.prototype.route_ = function route_(request, response, info) {
/* Attempt to route an http(s) request for this host to the correct
* handler. If a handler is found, call it.
*
* If no handler is found, but this host has .pathToSubdomain set,
* or .pathToSubdomain is undefined but request contains a
* CodeCity-pathToSubdomain header with value '?1' (true, in RFC 8941
* Structured Field Value notation) then attempt to map the first
* element (directory name) of info.path to one of .subdomains and,
* if successful, call the corresponding subdomain host's .handle
* method.
*
* Otherwise generate a 404 error.
*
* Arguments:
* - request: $.servers.http.Request - the incoming request to handle.
* - response: $.servers.http.Response - the response to write to.
* - info: Object - some additional information generated by
* Host.prototype.handle (see that method for details).
*/
var path = info.path;
if (typeof path !== 'string' || path[0] !== '/') {
response.sendError(400, 'Invalid path "' + path + '"');
} else if (path in this) {
// Get handler object.
var obj = this[path];
if (!$.utils.isObject(obj)) {
response.sendError(500, "Handler is not an object.");
return;
}
// Check access control.
if (!($.user.isPrototypeOf(request.user)) && // Not logged in.
((this.access !== 'public' && obj.wwwAccess !== 'public') ||
obj.wwwAccess === 'private')) {
response.sendError(403);
return;
}
// Record routing info on Request object and serve page.
request.info = info;
if (typeof obj.www === 'string') {
$.jssp.eval(obj, 'www', request, response);
} else if (typeof obj.www === 'function') {
obj.www(request, response);
} else {
response.sendError(500, "Handler .www is neither a function nor a JSSP.");
}
} else if (this.subdomains &&
(this.pathToSubdomain ||
(this.pathToSubdomain === undefined &&
request.headers['codecity-pathtosubdomain'] === '?1'))) {
// Try to route to a subdomain based on top-level directory.
// E.g. https://example.codecity.world/foo/bar -> foo
var m = path.match(/^\/([-A-Za-z0-9]+)(\/.*)?$/);
var subdomain = ''; // Empty string gives good 404 message if .match fails.
if (m && (subdomain = m[1]) in this.subdomains) {
// Subdomain matched. Do we need to redirect to add a trailing '/'?
if (!m[2]) {
response.sendRedirect(subdomain + '/', 308);
return;
}
// Route to the subdomain. Modify info.path and info.origin as appropriate
info.path = m[2];
info.origin += '/' + subdomain;
if (!this.subdomains[subdomain].handle(request, response, info)) {
response.sendError(500, 'Host for pseudo-subdomain /' + subdomain + '/ rejected request.');
return;
}
} else {
response.sendError(404, 'Not Found (and /' + subdomain + '/ does not map to a subdomain).');
}
} else {
response.sendError(404);
}
};
Object.setOwnerOf($.servers.http.Host.prototype.route_, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.Host.prototype.route_.prototype, $.physicals.Maximilian);
$.servers.http.Host.prototype.matchHostname_ = function matchHostname_(hostname) {
/* Returns: boolean - true if hostname exactly matches this.hostRegExp or,
* if that is undefined, this.hostname.
*/
if (this.hostRegExp) {
if (!(this.hostRegExp instanceof RegExp)) {
throw new TypeError('invalid .hostRegExp');
}
var m = this.hostRegExp.exec(hostname);
return (m && m.index === 0); // Match only accepted if at start.
} else if (this.hostname) {
if (typeof this.hostname !== 'string') {
throw new TypeError('invalid .hostname');
}
return this.hostname === hostname; // Only exact matches.
} else {
return true;
}
};
Object.setOwnerOf($.servers.http.Host.prototype.matchHostname_, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.Host.prototype.matchHostname_.prototype, $.physicals.Maximilian);
$.servers.http.Host.prototype.hostname = undefined;
$.servers.http.Host.prototype.subdomains = null;
$.servers.http.Host.prototype.addSubdomain = function addSubdomain(name, host) {
/* Add the given Host as a subdomain of this Host.
*
* Arguments:
* - name: string - the subdomain name.
* - host: $.servers.http.Host - the Host to serve the subdomain.
*/
name = String(name);
if (!(host instanceof $.servers.http.Host)) {
throw new TypeError('host must be a Host');
}
if (!this.hasOwnProperty('subdomains')) {
this.subdomains = Object.create(null);
}
this.subdomains[name] = host;
};
Object.setOwnerOf($.servers.http.Host.prototype.addSubdomain, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.Host.prototype.addSubdomain.prototype, $.physicals.Maximilian);
$.servers.http.Host.prototype.url = function url(varArgs) {
/* Return the base URL for this host (omitting scheme).
*
* Generally prefer $.servers.http.Request.prototype.hostUrl (q.v.)
* instead of method - but in some cases it is necessary to generate
* a URL for the webserver without an existing inbound Request to use
* as reference, so this method allows one to be generated in the
* obvious way from this.hostname. As with .hostUrl:
*
* - Absent any argument, the returned URL will routes to this
* Host object.
* - If an argument is supplied, the returned URL will instead be for
* the named subdomain.
* - Multiple arguments can be supplied if there are nested subdomains.
*
* E.g.:
* rootHost.url() => '//example.codecity.world/'
* rootHost.url('code') => '//code.example.codecity.world/'
* rootHost.url('foo', 'bar') => '//foo.bar.example.codecity.world/'
*
* If .pathToSubdomain is enabled on one or more Host object(s):
* rootHost.url('code') => '//example.codecity.world/code/'
* rootHost.url('foo', 'bar') => '//example.codecity.world/foo/bar/'
* or: '//bar.example.codecity.world/foo/'
*
* Barring bugs, the returned URL should always end with a '/'.
* Arguments:
* - subdomain: string - a string denoting a subdomain of interest.
* Multiple arguments are allowed. RangeError is thrown if no such
* subdomain exists.
* Returns: string - the URL for the desired domain/subdomain.
*/
if (typeof this.hostname !== 'string') {
throw new Error('canonical hostname not set');
}
var hostname = this.hostname;
var host = this;
for (var subdomain, i = 0; (subdomain = arguments[i]); i++) {
hostname = host.urlForSubdomain(hostname, subdomain);
}
return '//' + hostname + '/';
};
Object.setOwnerOf($.servers.http.Host.prototype.url, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.Host.prototype.url.prototype, $.physicals.Maximilian);
$.servers.http.Host.prototype.pathToSubdomain = undefined;
$.servers.http.Host.prototype.urlForSubdomain = function urlForSubdomain(hostname, subdomain, pathToSubdomainHeader) {
/* A helper function for the .url method.
*
* Given a hostname for this host, add the specified subdomain
* if it exists, or throw RangeError if not.
*
* If this.pathToSubdomain is true, or this.pathToSubdomain is undefined
* and pathToSubdomainHeader is true, then the subdomain will be added as
* a directory name suffix rather than a hostname pefix.
*
* E.g.:
* rootHost.pathToSubdomain = false;
* rootHost.urlForSubdomain('example.codecity.world', 'code')
* => 'code.example.codecity.world'
*
* rootHost.pathToSubdomain = undefined;
* rootHost.urlForSubdomain('example', 'code', true)
* => 'example.codecity.world/code'
* rootHost.urlForSubdomain('example', 'code', false)
* => 'code.example.codecity.world'
* rootHost.pathToSubdomain = true;
* rootHost.urlForSubdomain('example', 'code', false)
* => 'example.codecity.world/code'
*
* Arguments:
* - hostname: string - the base hostname for this Host.
* - subdomain: string - the desired subdomain.
* - pathToSubdomainHeader: boolean | undefined - value of the
* CodeCity-pathToSubdomain header for the current request (if there
* is one).
*
* TODO: Give this function a better name, because what it returns
* is not actually a valid URL.
*/
if (!(this.subdomains && subdomain in this.subdomains)) {
throw new RangeError('nonexistent subdomain "' + subdomain + '"');
}
if (this.pathToSubdomain ||
this.pathToSubdomain === undefined && pathToSubdomainHeader) {
return hostname + '/' + subdomain;
} else {
return subdomain + '.' + hostname;
}
};
Object.setOwnerOf($.servers.http.Host.prototype.urlForSubdomain, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.Host.prototype.urlForSubdomain.prototype, $.physicals.Maximilian);
$.servers.http.Host.prototype.deleteSubdomain = function deleteSubdomain(subdomain) {
/* Delete a subdomain, or all subdomains served by a particular
* Host object.
*
* Arguments:
* - subdomain: string | $.servers.http.Host - the name of the subdomain
* to be deleted, or the Host object serving it.
*/
if (!this.subdomains) return;
if (typeof subdomain === 'string') {
delete this.subdomains[subdomain];
} else if (subdomain instanceof $.servers.http.Host) {
for (var key in this.subdomains) {
if (this.subdomains[key] === subdomain) {
delete this.subdomains[key];
}
}
} else {
throw new TypeError('argument must be subdomain name or Host object');
}
if (Object.getOwnPropertyNames(this.subdomains).length === 0) {
delete this.subdomains;
}
};
Object.setOwnerOf($.servers.http.Host.prototype.deleteSubdomain, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.Host.prototype.deleteSubdomain.prototype, $.physicals.Maximilian);
$.servers.http.onRequest = function onRequest(connection) {
/* Called from $.servers.http.connection.onReceiveChunk when the
* connection.request has been fully parsed and is ready to be handled.
*
* Arguments:
* - conenction: Object with prototype $.servers.http.connection - the
* connection to be handle.
*/
var request = connection.request;
var response = connection.response;
try {
// Call .handle(request, response) on each Host object in
// $.servers.http.hosts in order until one returns true to indicate
// that it handled the request.
for (var host, i = 0; (host = this.hosts[i]); i++) {
if (host.handle(request, response)) return;
}
// No host responded to request.
response.sendError(400, 'Unknown Host');
} catch (e) {
suspend();
$.system.log(String(e) + '\n' + e.stack);
if (response.headersSent) {
// Too late to return a proper error page. Oh well.
response.write('
');
} else {
response.sendError(500, e);
}
} finally {
suspend();
connection.close();
}
};
Object.setOwnerOf($.servers.http.onRequest, $.physicals.Maximilian);
Object.setOwnerOf($.servers.http.onRequest.prototype, $.physicals.Maximilian);
$.servers.http.hosts = [];
================================================
FILE: core/core_24_$.hosts.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Host objects for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.hosts = {};
$.hosts.root = (new 'Object.create')($.servers.http.Host.prototype);
$.hosts.root.subdomains = (new 'Object.create')(null);
$.hosts.root['/'] = {};
$.hosts.root['/'].www = '\n<% var staticUrl = request.hostUrl(\'static\'); %>\n\n\n Code City\n \n \n \n\n\n
\n \n<% } %>\n\n';
$.hosts.dummy = (new 'Object.create')($.servers.http.Host.prototype);
Object.setOwnerOf($.hosts.dummy, $.physicals.Maximilian);
$.hosts.dummy.handle = function handle(request, response, info) {
/* Report the mishandling of an http(s) request which should have
* been intercepted by the nginx front-end and proxied to one
* of the other servers.
*
* This Host object is a singleton placeholer to mark (in
* $.hosts.root.subdomains) the subdomains that should be directed
* to loginServer, connectServer, etc., or served from the /static/
* directory. As such, no requests should ever be able to reach
* this Host object except due to a misconfiguration of nginx.
*
* Arguments:
* - request: $.servers.http.Request - the incoming request to handle.
* - response: $.servers.http.Response - the response to write to.
* - info: Object - some information used by recursive calls to this function.
* Returns: boolean - always true as all requests successfully generate
* an error message.
*/
response.sendError(500, 'This request should have been intercepted by ' +
'the reverse proxy. Check nginx configuration!');
return true;
};
Object.setOwnerOf($.hosts.dummy.handle, $.physicals.Maximilian);
Object.setOwnerOf($.hosts.dummy.handle.prototype, $.physicals.Maximilian);
$.hosts.root.subdomains.system = $.hosts.system;
$.hosts.root.subdomains.connect = $.hosts.dummy;
$.hosts.root.subdomains.login = $.hosts.dummy;
$.hosts.root.subdomains.mobwrite = $.hosts.dummy;
$.hosts.root.subdomains.static = $.hosts.dummy;
$.servers.http.hosts[0] = $.hosts.root;
================================================
FILE: core/core_25_$.db.tempId.js
================================================
/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Temporary ID database for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.db = {};
$.db.tempId = {};
$.db.tempId.getObjById = function getObjById(id) {
/* Find object temporarily stored with the given ID.
*/
var record = this.tempIds_[id];
if (record) {
record.time = Date.now();
return record.obj;
}
return undefined;
};
Object.setOwnerOf($.db.tempId.getObjById, $.physicals.Maximilian);
$.db.tempId.storeObj = function storeObj(obj) {
/* Find temporary ID for obj in this.tempIds_,
* adding it if it's not already there.
*/
var records = this.tempIds_;
for (var id in records) {
if (Object.is(records[id].obj, obj)) {
records[id].time = Date.now();
return id;
}
}
do {
var id = Math.floor(Math.random() * 0xFFFFFFFF);
} while (records[id]);
records[id] = {obj: obj, time: Date.now()};
// Lazy call of cleanup.
this.cleanSoon();
return id;
};
Object.setOwnerOf($.db.tempId.storeObj, $.physicals.Maximilian);
$.db.tempId.cleanSoon = function cleanSoon() {
// Schedule a cleanup to happen in a minute.
// Allows multiple calls to be batched together.
if (!this.cleanThread_) {
this.cleanThread_ = setTimeout(this.cleanNow.bind(this), 60 * 1000);
}
};
Object.setOwnerOf($.db.tempId.cleanSoon, $.physicals.Neil);
$.db.tempId.cleanNow = function cleanNow() {
// Cleanup IDs/objects that have not been accessed in an hour.
clearTimeout(this.cleanThread_);
this.cleanThread_ = null;
var ttl = Date.now() - this.timeoutMs;
var records = this.tempIds_;
for (var id in records) {
if (records[id].time < ttl) {
delete records[id];
}
}
};
Object.setOwnerOf($.db.tempId.cleanNow, $.physicals.Neil);
$.db.tempId.timeoutMs = 3600000;
$.db.tempId.tempIds_ = (new 'Object.create')(null);
$.db.tempId.cleanThread_ = undefined;
================================================
FILE: core/core_25_$.userDatabase.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview User database for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.userDatabase = {};
Object.setOwnerOf($.userDatabase, $.physicals.Maximilian);
$.userDatabase.get = function get(id) {
// Returns the user, or undefined.
var hash = $.utils.string.hash('md5', this.salt_ + id);
var table = this.byMd5;
var value = table[hash];
if (!($.user.isPrototypeOf(value))) {
delete table[hash];
return undefined;
}
return value;
};
Object.setOwnerOf($.userDatabase.get, $.physicals.Neil);
Object.setOwnerOf($.userDatabase.get.prototype, $.physicals.Maximilian);
$.userDatabase.set = function set(id, user) {
if (!$.user.isPrototypeOf(user)) {
throw new TypeError('userDatabase only accepts $.user values');
}
var hash = $.utils.string.hash('md5', this.salt_ + id);
this.byMd5[hash] = user;
};
Object.setOwnerOf($.userDatabase.set, $.physicals.Maximilian);
Object.setOwnerOf($.userDatabase.set.prototype, $.physicals.Maximilian);
$.userDatabase.validate = function validate() {
var table = this.byMd5
for (var key in table) {
if (!($.user.isPrototypeOf(table[key]))) {
delete table[key];
}
}
};
Object.setOwnerOf($.userDatabase.validate, $.physicals.Maximilian);
Object.setOwnerOf($.userDatabase.validate.prototype, $.physicals.Maximilian);
$.userDatabase.salt_ = 'v2OU0LHchCl84mhu';
$.userDatabase.byMd5 = (new 'Object.create')(null);
================================================
FILE: core/core_26_inline_editor.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Inline code editor for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.hosts.code = (new 'Object.create')($.servers.http.Host.prototype);
$.hosts.code['/inlineEdit'] = {};
$.hosts.code['/inlineEdit'].edit = function edit(obj, name, key) {
/* Return a (valid) URL for a web editing session editing obj[key],
* where obj might more commonly be known as name.
*/
if (!$.utils.isObject(obj)) throw new TypeError('obj must be an object');
if (typeof(key) !== 'string') throw new TypeError('key must be a string');
var objId = $.db.tempId.storeObj(obj);
var url = $.hosts.root.url('code') + 'inlineEdit?objId=' + objId;
if (name) {
url += '&name=' + encodeURIComponent(name);
}
if (key) {
url += '&key=' + encodeURIComponent(key);
}
return url;
};
Object.setOwnerOf($.hosts.code['/inlineEdit'].edit, $.physicals.Maximilian);
$.hosts.code['/inlineEdit'].load = function load(obj, key) {
/* Return string containing initial editor contents for editing
* obj[key].
*/
var pd = Object.getOwnPropertyDescriptor(obj, key);
var value = pd ? pd.value : undefined;
if (typeof value === 'function') {
return Function.prototype.toString.apply(value);
} else {
return $.utils.code.expressionFor(value, {depth: 1});
}
};
Object.setOwnerOf($.hosts.code['/inlineEdit'].load, $.physicals.Maximilian);
$.hosts.code['/inlineEdit'].save = function save(obj, key, src) {
/* Eval the string src and (if successful) save the resulting value
* as obj[key]. If the value produced from src and the existing
* value of obj[key] are both objects, then an attempt will be made
* to copy any properties from the old value to the new one.
*/
var old = obj[key];
src = $.utils.code.rewriteForEval(src, /* forceExpression= */ true);
// Evaluate src in global scope (eval by any other name, literally).
// TODO: don't use eval - prefer Function constructor for
// functions; generate other values from an Acorn parse tree.
var evalGlobal = eval;
var val = evalGlobal(src);
if (typeof old === 'function' && typeof val === 'function') {
$.utils.object.transplantProperties(old, val);
}
if (typeof val === 'function') {
val.lastModifiedTime = Date.now();
// TODO: Add user.
//val.lastModifiedUser = ...;
}
obj[key] = val;
return this.load(obj, key);
};
Object.setOwnerOf($.hosts.code['/inlineEdit'].save, $.physicals.Maximilian);
$.hosts.code['/inlineEdit'].www = '<%\nvar staticUrl = request.hostUrl(\'static\');\nvar params = request.parameters;\nvar objId = params.objId;\nvar obj = $.db.tempId.getObjById(params.objId);\nif (!$.utils.isObject(obj)) {\n // Bad edit URL.\n response.sendError(404);\n return;\n}\nvar key = params.key;\nvar src = params.src;\nvar status = \'\';\nif (src) {\n try {\n if (!request.fromSameOrigin()) {\n // Security check to ensure this is being loaded by the code editor.\n throw new Error(\'Cross-origin referer: \' + String(request.headers.referer));\n }\n src = this.save(obj, key, src);\n status = \'(saved)\';\n if (typeof obj[key] === \'function\') {\n if (params.isVerb) {\n obj[key].verb = params.verb;\n obj[key].dobj = params.dobj;\n obj[key].prep = params.prep;\n obj[key].iobj = params.iobj;\n } else {\n delete obj[key].verb;\n delete obj[key].dobj;\n delete obj[key].prep;\n delete obj[key].iobj;\n }\n }\n } catch (e) {\n status = \'(ERROR: \' + String(e) + \')\';\n }\n} else {\n src = this.load(obj, key);\n}\nvar isVerb = (Object.getOwnPropertyDescriptor(obj, key) && typeof obj[key] === \'function\') && obj[key].verb ? \'checked\' : \'\';\nvar verb = $.utils.html.escape((obj[key] && obj[key].verb) || \'\');\nvar dobj = obj[key] && obj[key].dobj;\nvar prep = obj[key] && obj[key].prep;\nvar iobj = obj[key] && obj[key].iobj;\nvar name = $.utils.html.escape(params.name);\nkey = $.utils.html.escape(key);\nvar objOpts = [\'none\', \'this\', \'any\']\n%>\n\n\n Code Editor for <%= name %>.<%= key %>\n \n \n\n \n \n \n\n \n \n';
$.hosts.root.subdomains.code = $.hosts.code;
================================================
FILE: core/core_27_editor.js
================================================
/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Web-based code explorer/editor for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.hosts.code['/'] = {};
$.hosts.code['/'].www = '\n<% var staticUrl = request.hostUrl(\'static\'); %>\n\n\n Code City: Code\n \n \n \n\n\nSorry, your browser does not support frames!\n';
$.hosts.code['/editor'] = {};
$.hosts.code['/editor'].www = '\n<% var staticUrl = request.hostUrl(\'static\'); %>\n\n \n \n Code City: Code Editor\n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n
\n \n \n \n \n\n';
$.hosts.code['/objectPanel'].getType = function getType(value) {
// Return a type string for a value.
// E.g. 'string', 'object', 'array', 'boolean'.
if (value === null) {
return 'null';
}
if (Array.isArray(value)) {
return 'array';
}
if ((typeof value === 'function') && value.verb) {
return 'verb';
}
return typeof value;
};
Object.setOwnerOf($.hosts.code['/objectPanel'].getType, $.physicals.Neil);
$.hosts.code['/objectPanel'].buildData = function buildData(query) {
// Provide data for the IDE's object panels.
// Takes one input: a JSON-encoded list of parts from the 'parts' parameter.
// Returns a browser-executed JavaScript data assignment.
var data = {};
if (query) {
var parts = new $.Selector(decodeURIComponent(query));
try {
var value = (new $.Selector(parts)).toValue();
} catch (e) {
// Parts don't match a valid path.
$.system.log(String(e) + '\n' + e.stack);
// TODO(fraser): Send an informative error message.
data = null;
}
if (data) {
// For simplicity, don't provide completions for primitives (despite
// the fact that (for example) numbers inherit a '.toFixed' function).
if (value && (typeof value === 'object' || typeof value === 'function')) {
data.properties = [];
while (value !== null && value !== undefined) {
var ownProps = Object.getOwnPropertyNames(value);
// Add typeof information.
for (var i = 0; i < ownProps.length; i++) {
var prop = ownProps[i];
var type = this.getType(value[prop]);
ownProps[i] = {name: prop, type: type};
}
data.properties.push(ownProps);
value = Object.getPrototypeOf(value);
}
data.keywords = ['{proto}', '{owner}'];
// Uncomment once Set, Map, WeakSet and WeakMap exist.
//if (value instanceOf Set || value instanceOf WeakSet) {
// data.keywords.push('{keys}');
//}
//if (value instanceOf Map || value instanceOf WeakMap) {
// data.keywords.push('{keys}', '{values}');
//}
}
}
} else {
data.roots = [];
// Add typeof information.
var global = $.utils.code.getGlobal();
for (var name in global) {
data.roots.push({name: name, type: this.getType(global[name])});
}
}
return data;
};
Object.setOwnerOf($.hosts.code['/objectPanel'].buildData, $.physicals.Neil);
Object.setOwnerOf($.hosts.code['/objectPanel'].buildData.prototype, $.physicals.Neil);
$.hosts.code['/editorXhr'] = {};
Object.setOwnerOf($.hosts.code['/editorXhr'], $.physicals.Neil);
$.hosts.code['/editorXhr'].www = function code_editorXhr_www(request, response) {
/* HTTP handler for /editorXhr
* Provide data for the IDE's editors.
* Takes several inputs:
* - selector: a selector to the origin object
* - key: a temporary key to the origin object
* - src: JavaScript source representation of new value,
* implies request to save
* Writes JSON-encoded information about what is to be edited:
* - key: a temporary key to the origin object
* - src: JavaScript source representation of current value
* - butter: short status message to be displayed to user
* - saved: boolean indicating if a save was successful,
* only present if save was requested
* - login: boolean indicating if the user is logged in
*/
var data = {login: !!request.user};
try { // ends with ... finally {response.write(JSON.stringify(data));}
if (!request.fromSameOrigin()) {
// Security check to ensure this is being loaded by the code editor.
data.butter = 'Cross-origin referer: ' + String(request.headers.referer);
return;
}
var selector;
try {
selector = new $.Selector(decodeURIComponent(request.parameters.selector));
} catch (e) {
data.butter = 'Invalid selector: ' + String(e);
return;
}
// Get Binding being edited.
var object;
var part = selector[selector.length - 1];
if (selector.isVar()) {
// Global variable; no parent object.
object = null;
} else if (request.parameters.key &&
(object = $.db.tempId.getObjById(request.parameters.key))) {
// Successfully retrieved parent object from tempID DB.
} else {
// Get parent object via selector.
var parent = new $.Selector(selector.slice(0, -1));
try {
// Get parent object and populate the reverse-lookup db.
object = parent.toValue(/*save:*/true);
} catch (e) {
data.butter = e.message;
return;
}
if (!$.utils.isObject(object)) {
data.butter = String(parent) + ' is not an object';
return;
}
// Save parent object in tempId DB; send key to client.
data.key = $.db.tempId.storeObj(object);
}
var binding = new $.utils.Binding(object, part);
// Save changes.
if (request.parameters.src) {
data.saved = false;
this.save(request.parameters.src, binding, data, request.user);
}
// Populate the new value object in the reverse-lookup db.
selector.toValue(/*save:*/true);
// Load revised source.
this.load(binding, data);
} finally {
suspend();
response.write(JSON.stringify(data));
}
};
Object.setOwnerOf($.hosts.code['/editorXhr'].www, $.physicals.Maximilian);
$.hosts.code['/editorXhr'].load = function load(binding, data) {
/* The complement of save: render the current value of binding as a
* string, prefixed with metadata, postfixed with type information.
*
* This should set data.src to a string which, when passed eval, will be (in
* order of preference):
*
* - Identical to (as determined by Object.is) the current value,
* - A shallow-copy of the current value, or
* - Unparsable, such that eval will throw SyntaxError.
*
* The intention should be that it should be safe to save witout
* having made any changes and be reasonably confident nothing will
* break.
*
* Args:
* - binding: $.utils.Binding - the binding being edited.
* - data: {src: string, butter: string} - the data object to be returned
* to the client.
*/
var value = binding.get(/*inherited:*/true);
var inherited = !binding.exists();
try {
var source = this.sourceFor(value);
data.src = this.generateMetaData(value, source, inherited) + source;
} catch (e) {
suspend();
// TODO(cpcallen): Send a more informative error message.
data.butter = String(e);
throw e;
}
};
Object.setOwnerOf($.hosts.code['/editorXhr'].load, $.physicals.Maximilian);
$.hosts.code['/editorXhr'].save = function $_www_code_editor_save(src, binding, data, user) {
// Save changes by evalling src, doing post-processing as directed
// by metadata, and then calling binding.set(/* new value */).
// Sets data.saved and data.butter as appropriate to give feedback
// to user.
if (!user) {
data.butter = 'User not logged in.';
return;
}
setPerms(user);
var saveValue;
try {
suspend();
var expr = $.utils.code.rewriteForEval(src, /*forceExpression:*/true);
// Evaluate src in global scope (eval by any other name, literally).
var evalGlobal = eval;
saveValue = evalGlobal(expr);
} catch (e) {
// TODO(fraser): Send a more informative error message.
data.butter = String(e);
return;
}
var oldValue = binding.get(/*inherited:*/false); // Get actual current value.
try {
this.handleMetaData(src, oldValue, saveValue);
} catch (e) {
if (typeof e === 'string') {
// A thrown string should just be printed to the user.
data.butter = e;
return;
} else {
throw e; // Rethrow real errors.
}
}
// Record last modification data on functions.
if (typeof saveValue === 'function') {
saveValue.lastModifiedTime = Date.now();
saveValue.lastModifiedUser = user;
}
try {
binding.set(saveValue);
} catch (e) {
data.butter = String(e);
return;
}
data.saved = true;
if (binding.isProto()) {
data.butter = 'Prototype Set';
} else if (binding.isOwner()) {
data.butter = 'Owner Set';
} else {
data.butter = 'Saved';
}
};
Object.setOwnerOf($.hosts.code['/editorXhr'].save, $.physicals.Neil);
$.hosts.code['/editorXhr'].handleMetaData = function handleMetaData(src, oldValue, newValue) {
// Parse metadata directives from src and apply to newValue.
//
// The $.hosts.code['/editor'].www sends values to be edited to the
// editor front-end encoded as JavaScript expressions, optionally preceded
// by comments containing metadata about the value. The editor can
// in turn return metadata directives which will be carried out by
// this function.
//
// Supported directives (order matters for now):
// // @copy_properties true
// - Copy (most) properties from oldValue to newValue, if both
// are objects.
// // @hash 26076758802
// - Warn if old value doesn't hash to this value (conflicting
// change happened between load and save).
// // @delete_prop
// - Delete the named property from newValue.
// // @set_prop
// - Set the named property of newValue to the specified value.
//
// Throws user-printed strings (not Errors) if unable to complete.
var m = src.match(/^(?:[ \t]*(?:\/\/[^\n]*)?\n)+/);
if (!m) {
return;
}
var metaLines = m[0].split('\n');
for (var i = 0; i < metaLines.length; i++) {
var meta = metaLines[i];
if (meta.match(/^\s*\/\/\s*@copy_properties\s+true\s*$/)) {
// @copy_properties true
if (!$.utils.isObject(newValue)) {
throw "Can't copy properties onto primitive: " + newValue;
}
// Silently ignore if the old value is a primitive.
if ($.utils.isObject(oldValue)) {
$.utils.object.transplantProperties(oldValue, newValue);
}
} else if ((m = meta.match(/^\s*\/\/\s*@hash\s+(\S+)\s*$/))) {
// @hash 26076758802
var oldSource = this.sourceFor(oldValue);
var hash = $.utils.string.hash('md5', oldSource);
if (String(hash) !== m[1]) {
// The current value does not match the value when the editor was loaded.
// This means the value changed out from under the editor.
throw 'Collision: Out of date editor.';
}
} else if ((m = meta.match(/^\s*\/\/\s*@delete_prop\s+(\S+)\s*$/))) {
// @delete_prop dobj
try {
delete newValue[m[1]];
} catch (e) {
throw "Can't delete '" + m[1] + "' property.";
}
} else if ((m = meta.match(/^\s*\/\/\s*@set_prop\s+(\S+)\s*=(.+)$/))) {
// @set_prop dobj = "this"
try {
var propValue = JSON.parse(m[2]);
} catch (e) {
throw "Can't parse '" + m[1] + "' value: " + m[2];
}
try {
newValue[m[1]] = propValue;
} catch (e) {
throw "Can't set '" + m[1] + "' property.";
}
}
}
};
Object.setOwnerOf($.hosts.code['/editorXhr'].handleMetaData, $.physicals.Maximilian);
$.hosts.code['/editorXhr'].generateMetaData = function generateMetaData(value, src, inherited) {
/* Assemble any meta-data for the editor.
*
* Arguments:
* value: any - the value which will be provided as the initial value to begin
* editing from. This might be a value inherited from a prototype, if the
* binding being edited does not yet exist.
* inherited: boolean - should be set to true iff value is inherited from a
* prototype, such that saving will create a new property binding
* overriding the interhited value, rather than replacing an existing
* value.
*
* Returns: string - metadata informing $.hosts.code['/editorXhr'].save
* what to do after creating the new value from the edited description.
* At present, metadata is only generated if it is a function.
*/
var meta = '';
if ($.utils.isObject(value)) {
// TODO: add @copy_properties here, but not if the source code is a selector?
}
if (typeof value === 'function') {
if (value.lastModifiedTime) {
var date = new Date(value.lastModifiedTime);
meta += '// @last_modified_time ' + date.toString() + '\n';
}
if (value.lastModifiedUser) {
meta += '// @last_modified_user ' + String(value.lastModifiedUser) + '\n';
}
meta += '// @copy_properties ' + !inherited + '\n';
var props = ['verb', 'dobj', 'prep', 'iobj'];
for (var i = 0, prop; (prop = props[i]); i++) {
try {
meta += '// ' + (value[prop] ? '@set_prop ' + prop + ' = ' +
JSON.stringify(value[prop]) : '@delete_prop ' + prop) + '\n';
} catch (e) {
// Unstringable value, or read perms error. Skip.
}
}
if (inherited) src = 'undefined'; // What source of oldValue will be.
var hash = $.utils.string.hash('md5', src);
meta += '// @hash ' + hash + '\n';
}
return meta;
};
Object.setOwnerOf($.hosts.code['/editorXhr'].generateMetaData, $.physicals.Maximilian);
$.hosts.code['/editorXhr'].sourceFor = function sourceFor(value) {
/* Generate source code for a given value.
*
* Arguments:
* - value: any - any JavaScript value.
* Returns: string - source code for value.
*/
switch (typeof value) {
// Special-case the most common cases for efficiency and to reduce
// chance of editor breaking due to bugs in $.utils.code.
//
// TODO: consider removing special case for strings once editor frontends
// cope with single-quoted strings.
case 'function': return Function.prototype.toString.call(value);
case 'string': return JSON.stringify(value);
case 'undefined': return 'undefined';
default:
// TODO: allow user-specified options. N.B.: careful when dealing with
// editing sessions shared via MobWrite, to avoid @hash metatdata
// failures.
// TODO: add selector to options, so as to avoid including a comment
// about it in the output when it is as expected - but think through
// implications for @hash checking carefully first!
return $.utils.code.expressionFor(value, this.sourceOptions);
}
};
Object.setOwnerOf($.hosts.code['/editorXhr'].sourceFor, $.physicals.Maximilian);
Object.setOwnerOf($.hosts.code['/editorXhr'].sourceFor.prototype, $.physicals.Maximilian);
$.hosts.code['/editorXhr'].sourceOptions = {};
Object.setOwnerOf($.hosts.code['/editorXhr'].sourceOptions, $.physicals.Maximilian);
$.hosts.code['/editorXhr'].sourceOptions.depth = 3;
$.hosts.code['/editorXhr'].sourceOptions.abbreviateMethods = true;
$.hosts.code['/svg'] = {};
Object.setOwnerOf($.hosts.code['/svg'], $.physicals.Neil);
$.hosts.code['/svg'].www = '<% var staticUrl = request.hostUrl(\'static\'); %>\n\n \n Code City SVG Editor\n \n \n \n \n \n \n\n \n
\n \n \n \n";
$.hosts.code['/evalXhr'] = {};
Object.setOwnerOf($.hosts.code['/evalXhr'], $.physicals.Neil);
$.hosts.code['/evalXhr'].www = function code_evalXhr_www(request, response) {
setPerms(request.user);
var output = '';
if (!request.fromSameOrigin()) {
// Security check to ensure this is being loaded by the eval editor.
output = 'Cross-origin referer: ' + String(request.headers.referer);
} else {
try {
var src = $.utils.code.rewriteForEval(request.data);
output = $.utils.code.eval(src);
} catch (e) {
suspend();
output = String(e);
}
}
response.write(output);
};
Object.setOwnerOf($.hosts.code['/evalXhr'].www, $.physicals.Maximilian);
Object.setOwnerOf($.hosts.code['/evalXhr'].www.prototype, $.physicals.Neil);
================================================
FILE: core/core_28_$.servers.eval.js
================================================
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Eval server for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.servers.eval = {};
$.servers.eval.connection = (new 'Object.create')($.connection);
$.servers.eval.connection.onReceiveLine = function onReceiveLine(text) {
if (this !== $.servers.eval.connected) {
this.close();
return;
}
this.write('⇒ ' + $.utils.code.eval(text) + '\n');
this.write('eval> ');
};
Object.setOwnerOf($.servers.eval.connection.onReceiveLine, $.physicals.Maximilian);
Object.setOwnerOf($.servers.eval.connection.onReceiveLine.prototype, $.physicals.Maximilian);
$.servers.eval.connection.onConnect = function onConnect() {
$.connection.onConnect.apply(this, arguments);
if ($.servers.eval.connected) {
$.servers.eval.connected.close();
}
$.servers.eval.connected = this;
this.write('eval> ');
};
Object.setOwnerOf($.servers.eval.connection.onConnect, $.physicals.Maximilian);
Object.setOwnerOf($.servers.eval.connection.onConnect.prototype, $.physicals.Maximilian);
$.servers.eval.connection.close = function close() {
this.write('This session has been terminated.\n');
return $.connection.close.apply(this, arguments);
};
Object.setOwnerOf($.servers.eval.connection.close, $.physicals.Maximilian);
Object.setOwnerOf($.servers.eval.connection.close.prototype, $.physicals.Maximilian);
$.servers.eval.connection.onEnd = function onEnd() {
$.servers.eval.connected = null;
return $.connection.onEnd.apply(this, arguments);
};
Object.setOwnerOf($.servers.eval.connection.onEnd, $.physicals.Maximilian);
Object.setOwnerOf($.servers.eval.connection.onEnd.prototype, $.physicals.Maximilian);
$.servers.eval.connected = null;
================================================
FILE: core/core_30_$.utils.command.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Command parser for Code City
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.utils.command = {};
$.utils.command.prepositions = (new 'Object.create')(null);
$.utils.command.prepositions.with = 'with/using';
$.utils.command.prepositions.using = 'with/using';
$.utils.command.prepositions.at = 'at/to';
$.utils.command.prepositions.to = 'at/to';
$.utils.command.prepositions['in front of'] = 'in front of';
$.utils.command.prepositions.in = 'in/inside/into';
$.utils.command.prepositions.inside = 'in/inside/into';
$.utils.command.prepositions.into = 'in/inside/into';
$.utils.command.prepositions['on top of'] = 'on top of/on/onto/upon';
$.utils.command.prepositions.on = 'on top of/on/onto/upon';
$.utils.command.prepositions.onto = 'on top of/on/onto/upon';
$.utils.command.prepositions.upon = 'on top of/on/onto/upon';
$.utils.command.prepositions.over = 'over';
$.utils.command.prepositions.through = 'through';
$.utils.command.prepositions.under = 'under/underneath/beneath';
$.utils.command.prepositions.underneath = 'under/underneath/beneath';
$.utils.command.prepositions.beneath = 'under/underneath/beneath';
$.utils.command.prepositions.behind = 'behind';
$.utils.command.prepositions.beside = 'beside';
$.utils.command.prepositions.for = 'for/about';
$.utils.command.prepositions.about = 'for/about';
$.utils.command.prepositions.is = 'is';
$.utils.command.prepositions.as = 'as';
$.utils.command.prepositions.off = 'off/off of';
$.utils.command.prepositions['off of'] = 'off/off of';
$.utils.command.prepositions['out of'] = 'out of/from inside/from';
$.utils.command.prepositions['from inside'] = 'out of/from inside/from';
$.utils.command.prepositions.from = 'out of/from inside/from';
$.utils.command.prepositionsRegExp = /^(.*\s)?(with|using|upon|underneath|under|to|through|over|out +of|onto|on +top +of|on|off +of|off|is|into|inside|in +front +of|in|from +inside|from|for|beside|beneath|behind|at|as|about)(\s.*)?$/;
$.utils.command.prepositionOptions = [];
$.utils.command.prepositionOptions[0] = 'none';
$.utils.command.prepositionOptions[1] = 'any';
$.utils.command.prepositionOptions[2] = 'with/using';
$.utils.command.prepositionOptions[3] = 'at/to';
$.utils.command.prepositionOptions[4] = 'in front of';
$.utils.command.prepositionOptions[5] = 'in/inside/into';
$.utils.command.prepositionOptions[6] = 'on top of/on/onto/upon';
$.utils.command.prepositionOptions[7] = 'out of/from inside/from';
$.utils.command.prepositionOptions[8] = 'over';
$.utils.command.prepositionOptions[9] = 'through';
$.utils.command.prepositionOptions[10] = 'under/underneath/beneath';
$.utils.command.prepositionOptions[11] = 'behind';
$.utils.command.prepositionOptions[12] = 'beside';
$.utils.command.prepositionOptions[13] = 'for/about';
$.utils.command.prepositionOptions[14] = 'is';
$.utils.command.prepositionOptions[15] = 'as';
$.utils.command.prepositionOptions[16] = 'off/off of';
$.utils.command.parse = function parse(cmdstr, user) {
// Parse a user's command into components.
//
// Commands are generally expected to be of the form:
//
//
//
// ... where all parts but are optional, but
// required if is present.
//
// The parse will return an object with the following properties:
//
// user: The $.user object, from the parameter of the same name.
// cmdstr: The cmdstr parameter (coerced to string).
// verbstr: The first non-whitespace word of cmdstr (if any).
// argstr: The rest of cmdstr, starting from the second character
// after the verb.
// args: An array of all the rest of the words of cmdstr.
// dobjstr: Sring of args up to the (first) preposition.
// dobj: Object matching dobjstr. If dobjstr is the empty string
// then this will be null. If no object matches, it will
// be $.FAILED_MATCH. If more than one object matches it
// will be $.AMBIGUOUS_MATCH.
// prepstr: String of the (first) preposition, if any.
// iobjstr: String of args after the (first) preposition.
// iobj: Object matching iobjstr. Special values as for dobj.
//
// If cmdstr contains no non-whitespace characters, null is returned
// instead.
//
// The cmdstr, verbstr and argstr properties are "raw" strings,
// unmodified from the cmdstr parameter, while dobjstr, prepstr and
// iobjstr are normalised, being substrings of args.join(' ').
// Spit off verb from the rest.
cmdstr = String(cmdstr);
var m = cmdstr.match($.utils.command.verbRegExp);
if (!m) return null;
var verbstr = m[1];
var argstr = m[2] || '';
// Split argstr into words.
// TODO(cpcallen): support quoting.
var argstrTrimmed = argstr.trim();
var args = argstrTrimmed ? argstrTrimmed.split(/\s+/) : [];
// Recombine args and split into dobjstr / prepstr / iobjstr
var argsNormalised = args.join(' ');
var dobjstr = '';
var prepstr = '';
var iobjstr = '';
m = argsNormalised.match($.utils.command.prepositionsRegExp);
if (m) {
// Preposition found.
dobjstr = (m[1] || '').trim();
prepstr = m[2].replace(/ +/g, ' ');
iobjstr = (m[3] || '').trim();
} else {
dobjstr = argsNormalised;
}
function match(str) {
if (str === '') return null;
if (str === 'me' || str === 'myself') return user;
if (str === 'here') return user.location;
return $.utils.command.match(str, user);
}
var dobj = match(dobjstr);
var iobj = match(iobjstr);
return {
user: user,
cmdstr: cmdstr,
verbstr: verbstr,
argstr: argstr,
args: args,
dobjstr: dobjstr,
dobj: dobj,
prepstr: prepstr,
iobjstr: iobjstr,
iobj: iobj
};
};
Object.setOwnerOf($.utils.command.parse, $.physicals.Maximilian);
$.utils.command.execute = function execute(cmdstr, user) {
/* Parse and execute a user's command. Returns true if a
* verb-function was invoked; narrates an error message and
* false otherwise.
*/
var cmd = $.utils.command.parse(cmdstr, user);
if (!cmd) return false;
// Collect all objects which could host the verb.
var hosts = [user, user.location, cmd.dobj, cmd.iobj];
for (var i = 0; i < hosts.length; i++) {
var host = hosts[i];
if (!host) {
continue;
}
// Check every verb on each object for a match.
for (var prop in host) {
var func = host[prop];
if (typeof func !== 'function') continue; // Not a function.
var verbSpec = func.verb;
var dobjSpec = func.dobj;
var prepSpec = func.prep;
var iobjSpec = func.iobj; // I can't wait for ES6.
if (!verbSpec || !dobjSpec || !prepSpec || !iobjSpec) continue; // Not a verb.
var verbRegExp = new RegExp('^(?:' + verbSpec + ')$');
if (verbRegExp.test(cmd.verbstr) &&
(prepSpec === 'any' ||
$.utils.command.prepositions[cmd.prepstr] === prepSpec ||
(prepSpec == 'none' && !cmd.prepstr)) &&
(dobjSpec === 'any' || (dobjSpec === 'this' && cmd.dobj === host) ||
(dobjSpec === 'none' && !cmd.dobj)) &&
(iobjSpec === 'any' || (iobjSpec === 'this' && cmd.iobj === host) ||
(iobjSpec === 'none' && !cmd.iobj))) {
// TODO: security check/perms.
host[prop](cmd);
return true;
}
}
}
cmd.user.narrate('I don\'t understand that.');
return false;
};
Object.setOwnerOf($.utils.command.execute, $.physicals.Maximilian);
$.utils.command.verbRegExp = /^\s*(\S+)(?:\s(.*))?/;
$.utils.command.match = function match(str, context) {
/* Attempt to find an object matching str amongst context,
* context.location, and context.contents.
*
* Args:
* - str: string: prefix of name or alias of desired object.
* - context: $.physical: an object to search.
*
* Returns: an object matching str, or $.FAILED_MATCH if none or
* $.AMBIGUOUS_MATCH if more than one.
*/
str = str.trim();
// First, check for matches against universally accessible things.
try {
var v = $(str);
if ($.utils.isObject(v)) return v;
} catch (e) {
// Ignore failed Selector parse/lookup.
}
var objects = [context].concat(context.getContents());
if (context.location) {
objects = objects.concat([context.location], context.location.getContents());
}
var m = $.utils.command.matchObjects(str, objects);
switch (m.length) {
case 0:
return $.FAILED_MATCH;
case 1:
return m[0];
default:
return $.AMBIGUOUS_MATCH;
}
};
Object.setOwnerOf($.utils.command.match, $.physicals.Maximilian);
$.utils.command.matchFailed = function matchFailed(obj, objstr, user) {
/* Return true iff obj is NOT a valid match, and optionally narrate
* a suitable error message if not.
*
* If obj is null, $.FAILED_MATCH or $.AMBIGUOUS_MATCH (and objstr
* and user are supplied) call user.narrate with a suitable error
* message.
*
* Args:
* - obj: $.physical | null | $.FAILED_MATCH | $.AMBIGUOUS_MATCH:
* A match value (e.g., cmd.dobj or cmd.iobj) to be checked.
* - objstr: string:
* The string which was matched to get obj.
* - user: $.user:
* The user who typed the command.
* Returns: boolean: true if obj is a $.physical.
*/
var send = (typeof objstr === 'string' && $.user.isPrototypeOf(user));
if (obj === null) {
if (send) user.narrate('You must give the name of some object.');
return true;
} else if (obj === $.FAILED_MATCH) {
if (send) user.narrate('I see no "' + objstr + '" here.');
return true;
} else if (obj === $.AMBIGUOUS_MATCH) {
if (send) user.narrate('I don\'t know which "' + objstr + '" you mean.');
return true;
} else if ($.physical.isPrototypeOf(obj)) {
return false;
} else {
throw new TypeError('unexpected value checking match result');
}
};
Object.setOwnerOf($.utils.command.matchFailed, $.physicals.Maximilian);
Object.setOwnerOf($.utils.command.matchFailed.prototype, $.physicals.Maximilian);
$.utils.command.matchObjects = function matchObjects(str, objects) {
/* Match a string against a list of objects. Will return an array
* of zero or more objects such that (in order of preference):
* - all have .name === str.
* - all have str in their .aliases.
* - all have str as a prefix of their name or an alias.
*
* Duplicate entries in objects will be ignored; only a single copy
* will appear in the returned array.
*
* Args:
* str: string to match against names and aliases of objects.
* objects: array of $.physical objects to consider.
*
* Returns: possibly-empty array of objects matching str.
*
*/
var nameMatches = []; // These should be Sets.
var aliasMatches = [];
var partialMatches = [];
var nonMatches = []; // Non-matches will be ignored.
var matches = [nonMatches, partialMatches, aliasMatches, nameMatches];
for (var i = 0; i < objects.length; i++) {
var obj = objects[i];
var strength = $.utils.command.matchObjects.strength(str, obj);
if (!matches[strength].includes(obj)) {
matches[strength].push(obj);
}
}
// Return the highest level bin.
if (nameMatches.length) return nameMatches;
if (aliasMatches.length) return aliasMatches;
if (partialMatches.length) return partialMatches;
return [];
};
Object.setOwnerOf($.utils.command.matchObjects, $.physicals.Maximilian);
Object.setOwnerOf($.utils.command.matchObjects.prototype, $.physicals.Maximilian);
$.utils.command.matchObjects.strength = function strength(str, obj) {
/* Score str as a match for obj.
* Returns: number
* - 0: No match.
* - 1: Partial name or alias.
* - 2: Perfect alias match.
* - 3: Perfect name match.
*/
if (!str || !obj) {
return 0;
}
str = str.toLowerCase();
var name = obj.name.toLowerCase();
if (name === str) {
return 3;
}
var partial = name.startsWith(str);
if (Array.isArray(obj.aliases)) {
for (var i = 0; i < obj.aliases.length; i++) {
var alias = obj.aliases[i].toLowerCase();
if (name === alias) {
return 2;
}
partial = partial || alias.startsWith(str);
}
}
return partial ? 1 : 0;
};
Object.setOwnerOf($.utils.command.matchObjects.strength, $.physicals.Maximilian);
================================================
FILE: core/core_31_$.utils_world.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview World-related utils for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.utils.commandMenu = function commandMenu(commands) {
var cmdXml = '';
if (commands.length) {
cmdXml += '';
for (var i = 0; i < commands.length; i++) {
cmdXml += '' + $.utils.html.escape(commands[i]) + '';
}
cmdXml += '';
}
return cmdXml;
};
Object.setOwnerOf($.utils.commandMenu, $.physicals.Maximilian);
$.utils.replacePhysicalsWithName = function replacePhysicalsWithName(value) {
/* Deeply clone JSON object.
* Replace all instances of $.physical with the object's name.
*/
if (Array.isArray(value)) {
var newArray = [];
for (var i = 0; i < value.length; i++) {
newArray[i] = replacePhysicalsWithName(value[i]);
}
return newArray;
}
if ($.physical.isPrototypeOf(value)) {
return value.name;
}
if (typeof value === 'object' && value !== null) {
var newObject = {};
for (var prop in value) {
newObject[prop] = replacePhysicalsWithName(value[prop]);
}
return newObject;
}
return value;
};
Object.setOwnerOf($.utils.replacePhysicalsWithName, $.physicals.Maximilian);
================================================
FILE: core/core_32_physical.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Physical object prototype for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.physical = {};
$.physical.name = 'Physical object prototype';
$.physical.description = '';
$.physical.svgText = '';
$.physical.location = null;
$.physical.contents_ = null;
$.physical.getContents = function getContents() {
$.physical.validate.call(this);
return this.contents_.slice();
};
Object.setOwnerOf($.physical.getContents, $.physicals.Maximilian);
Object.setOwnerOf($.physical.getContents.prototype, $.physicals.Maximilian);
$.physical.addContents = function addContents(newThing, opt_neighbour) {
// Add newThing to this's contents. It will be added after
// opt_neighbour, or to the end of list if opt_neighbour not given.
// An item already in the contents list will be moved to the
// specified position.
$.physical.validate.call(this);
if (!$.physical.isPrototypeOf(newThing)) {
throw new TypeError('cannot add non-$.physical to contents');
} else if(newThing.location !== this) {
throw new RangeError('object to be added to contents must have .location set first');
}
for (var loc = this; loc; loc = loc.location) {
if (loc === newThing) {
throw new RangeError('object cannot contain itself');
}
}
var contents = this.contents_;
var index = contents.indexOf(newThing);
if (index !== -1) {
// Remove existing thing.
contents.splice(index, 1);
}
if (opt_neighbour) {
for (var i = 0, thing; (thing = contents[i]); i++) {
if (thing === opt_neighbour) {
contents.splice(i + 1, 0, newThing);
return;
}
}
// Neighbour not found, just append.
}
// Common case of appending a thing.
contents.push(newThing);
};
Object.setOwnerOf($.physical.addContents, $.physicals.Maximilian);
$.physical.removeContents = function removeContents(thing) {
var contents = this.contents_;
var index = contents.indexOf(thing);
if (index !== -1) {
contents.splice(index, 1);
}
this.contents_ = contents;
};
Object.setOwnerOf($.physical.removeContents, $.physicals.Neil);
$.physical.moveTo = function moveTo(dest, opt_neighbour) {
/* Move his object to the specified destination location.
* Attempt to position this object next to a specified neighbour, if given.
*/
$.physical.validate.call(this);
if (!$.physical.isPrototypeOf(dest) && dest !== null) {
throw new Error('destination must be a $.physical or null');
}
var src = this.location;
if (src === dest) return; // Nothing to do.
// Preliminary check for recursive move. This is formally enforced by
// $.physical.addContents(), but we bail here if it is likely to fail later.
for (var loc = dest; loc; loc = loc.location) {
if (loc === this) {
throw new RangeError('cannot move an object inside itself');
}
}
// Call this.willMoveTo(dest), and refuse move unless it returns true without suspending.
var willMove = false;
new Thread(function checkWillMoveTo() {
willMove = Boolean(this.willMoveTo(dest));
}, 0, this);
suspend(0);
if (!willMove) {
throw new PermissionError(String(this) + " isn't movable to " + String(dest));
}
// Call dest.accept(this), and refuse move unless it returns true without suspending.
var accept = false;
new Thread(function checkAccept() {
accept = (dest === null || Boolean(dest.accept(this)));
}, 0, this);
suspend(0);
if (!accept) {
throw new PermissionError(String(dest) + " doesn't accept " + String(this));
}
// Call src.onExit(this, dest).
new Thread(function callOnExit() {
if (src) src.onExit(this, dest);
}, 0, this);
suspend(0);
// Perform move.
if (src && src.removeContents) src.removeContents(this);
this.location = dest;
if (dest) {
try {
dest.addContents(this, opt_neighbour);
} finally {
if (!dest.contents_.includes(this)) {
this.location = null; // Uh oh.
dest = null;
}
}
}
// Call dest.onEnter(this, src).
new Thread(function callOnEnter() {
if (dest) dest.onEnter(this, src);
}, 0, this);
suspend(0);
};
Object.setOwnerOf($.physical.moveTo, $.physicals.Maximilian);
$.physical.look = function look(cmd) {
var html = $.jssp.eval(this, 'lookJssp', {user: cmd.user});
cmd.user.readMemo({type: "html", htmlText: html});
};
Object.setOwnerOf($.physical.look, $.physicals.Neil);
$.physical.look.verb = 'l(ook)?';
$.physical.look.dobj = 'this';
$.physical.look.prep = 'none';
$.physical.look.iobj = 'none';
$.physical.lookJssp = "
\n
\n
\n \n
\n
\n
<%: this %><%= $.utils.commandMenu(this.getCommands(request.user)) %>
',
];
}
this.user.readMemo({type: 'html', htmlText: lines.join('\n')});
};
Object.setOwnerOf($.tutorial.show, $.physicals.Maximilian);
$.tutorial.contents_ = [];
$.tutorial.contents_.forObj = $.tutorial;
Object.defineProperty($.tutorial.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.tutorial.contents_.forKey = 'contents_';
Object.defineProperty($.tutorial.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.tutorial.location = undefined;
$.tutorial.user = undefined;
$.tutorial.thread = undefined;
$.tutorial.step = undefined;
$.tutorial.room = undefined;
$.tutorial.origFunc = undefined;
$.physicals.tutorial = $.tutorial;
================================================
FILE: core/core_42_plant.js
================================================
/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Plant demo for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.seed = (new 'Object.create')($.thing);
$.seed.name = 'Generic Seed';
$.seed.aliases = [];
$.seed.aliases[0] = 'seed';
$.seed.description = 'A harmless looking seed. Try planting it in a pot, then watering it.';
$.seed.svgText = '\n';
$.seed.contents_ = [];
$.seed.contents_.forObj = $.seed;
Object.defineProperty($.seed.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.seed.contents_.forKey = 'contents_';
Object.defineProperty($.seed.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.seed.location = undefined;
$.physicals['Generic Seed'] = $.seed;
$.pot = (new 'Object.create')($.thing);
$.pot.name = 'flower pot';
$.pot.aliases = [];
$.pot.aliases[0] = 'pot';
$.pot.description = 'A clay flower pot. Try planting a seed in a pot, then watering it.';
$.pot.plant = function plant(cmd) {
cmd.user.narrate('You plant ' + String(cmd.dobj) + ' in ' + String(this) + '.');
if (cmd.user.location) {
cmd.user.location.narrate(String(cmd.user) + ' plants ' + String(cmd.dobj) + ' in ' + String(this) + '.', cmd.user);
}
cmd.dobj.moveTo(null);
this.stage = 0;
this.seed = cmd.dobj;
};
Object.setOwnerOf($.pot.plant, $.physicals.Maximilian);
$.pot.plant.verb = 'plant|put';
$.pot.plant.dobj = 'any';
$.pot.plant.prep = 'in/inside/into';
$.pot.plant.iobj = 'this';
$.pot.water = function water(cmd) {
cmd.user.narrate('You water ' + String(this) + '.');
if (cmd.user.location) {
cmd.user.location.narrate(String(cmd.user) + ' waters ' + String(this) + '.', cmd.user);
}
if (this.seed && this.stage < 4) {
if (this.stage === 2) {
var newSeed = Object.create(this.seed);
newSeed.moveTo(this.location, this);
cmd.user.location.narrate('A new seed appears.');
}
this.stage++;
this.location.updateScene(true);
}
};
Object.setOwnerOf($.pot.water, $.physicals.Neil);
$.pot.water.verb = 'water';
$.pot.water.dobj = 'this';
$.pot.water.prep = 'none';
$.pot.water.iobj = 'none';
$.pot.getCommands = function getCommands(who) {
var commands = $.thing.getCommands.call(this, who);
commands.push('water ' + this.name);
return commands;
};
Object.setOwnerOf($.pot.getCommands, $.physicals.Neil);
$.pot.contents_ = [];
$.pot.contents_.forObj = $.pot;
Object.defineProperty($.pot.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.pot.contents_.forKey = 'contents_';
Object.defineProperty($.pot.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.pot.reset = function reset(cmd) {
$.physicals["a seed"].moveTo(this.location);
this.stage = 0;
this.seed = null;
cmd.user.narrate('You reset ' + String(this) + '.');
};
Object.setOwnerOf($.pot.reset, $.physicals.Neil);
Object.setOwnerOf($.pot.reset.prototype, $.physicals.Neil);
$.pot.reset.verb = 'reset';
$.pot.reset.dobj = 'this';
$.pot.reset.prep = 'none';
$.pot.reset.iobj = 'none';
$.pot.svgText = function svgText() {
return this.stages[this.stage];
};
Object.setOwnerOf($.pot.svgText, $.physicals.Neil);
$.pot.location = undefined;
$.pot.seed = undefined;
$.pot.stage = undefined;
$.pot.stages = [];
$.pot.stages[0] = '\n';
$.pot.stages[1] = '\n\n\n\n';
$.pot.stages[2] = '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n';
$.pot.stages[3] = '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n';
$.pot.stages[4] = '\n\n\n\n\n\n \n \n \n \n \n \n \n';
$.pot.stages[5] = '\n\n\n\n\n';
$.physicals['flower pot'] = $.pot;
$.thrower = (new 'Object.create')($.thing);
$.thrower.name = 'a flame thrower';
$.thrower.aliases = [];
Object.setOwnerOf($.thrower.aliases, $.physicals.Maximilian);
$.thrower.aliases[0] = 'flame thrower';
$.thrower.aliases[1] = 'flamethrower';
$.thrower.aliases[2] = 'a flamethrower';
$.thrower.aliases[3] = 'thrower';
$.thrower.description = 'A backpack filled with napalm. A pilot light is burning quietly.';
$.thrower.svgText = '\n\n';
$.thrower.wear = function wear(user) {
if (this.savedSvg) {
user.narrate(String(this) + ' is already being worn.');
return;
}
this.moveTo(user);
this.savedSvg = user.svgText;
user.svgText += this.svgText;
if (user.location) {
user.location.updateScene(true);
user.location.narrate(String(user) + ' straps on ' + String(this) + '.', user);
}
user.narrate('You strap on ' + String(this) + '.');
};
Object.setOwnerOf($.thrower.wear, $.physicals.Neil);
$.thrower.unwear = function unwear(user) {
if (!this.savedSvg) {
user.narrate('You aren\'t wearing ' + String(this) + '.');
return;
}
user.svgText = this.savedSvg;
this.savedSvg = undefined;
if (user.location) {
user.location.updateScene(true);
user.location.narrate(String(user) + ' takes off ' + String(this) + '.', user);
}
user.narrate('You takes off ' + String(this) + '.');
};
Object.setOwnerOf($.thrower.unwear, $.physicals.Neil);
$.thrower.fire = function fire(cmd) {
var memo = {
type: 'iframe',
url: '/static/flamethrower.html',
alt: 'FIRE!!!'
};
cmd.user.location.sendMemo(memo);
if (cmd.iobj.seed) {
cmd.iobj.seed = null;
}
if (typeof cmd.iobj.stage === 'number') {
if (cmd.iobj.stage > 0) {
cmd.iobj.stage = cmd.iobj.stages.length - 1;
}
}
suspend(5000);
cmd.user.location.updateScene(true);
};
Object.setOwnerOf($.thrower.fire, $.physicals.Neil);
$.thrower.fire.verb = 'fire';
$.thrower.fire.dobj = 'this';
$.thrower.fire.prep = 'at/to';
$.thrower.fire.iobj = 'any';
$.thrower.getCommands = function getCommands(who) {
var commands = $.thing.getCommands.call(this, who);
if (this.savedSvg) {
commands.push('take off ' + this.name);
} else {
commands.push('put on ' + this.name);
}
return commands;
};
Object.setOwnerOf($.thrower.getCommands, $.physicals.Neil);
$.thrower.contents_ = [];
$.thrower.contents_.forObj = $.thrower;
Object.defineProperty($.thrower.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.thrower.contents_.forKey = 'contents_';
Object.defineProperty($.thrower.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.thrower.unwear1 = function unwear1(cmd) {
this.unwear(cmd.user);
};
Object.setOwnerOf($.thrower.unwear1, $.physicals.Neil);
Object.setOwnerOf($.thrower.unwear1.prototype, $.physicals.Neil);
$.thrower.unwear1.verb = 'take';
$.thrower.unwear1.dobj = 'this';
$.thrower.unwear1.prep = 'off/off of';
$.thrower.unwear1.iobj = 'none';
$.thrower.unwear2 = function unwear2(cmd) {
this.unwear(cmd.user);
};
Object.setOwnerOf($.thrower.unwear2, $.physicals.Neil);
Object.setOwnerOf($.thrower.unwear2.prototype, $.physicals.Neil);
$.thrower.unwear2.verb = 'take';
$.thrower.unwear2.dobj = 'none';
$.thrower.unwear2.prep = 'off/off of';
$.thrower.unwear2.iobj = 'this';
$.thrower.wear1 = function wear1(cmd) {
this.wear(cmd.user);
};
Object.setOwnerOf($.thrower.wear1, $.physicals.Neil);
Object.setOwnerOf($.thrower.wear1.prototype, $.physicals.Neil);
$.thrower.wear1.verb = 'put';
$.thrower.wear1.dobj = 'this';
$.thrower.wear1.prep = 'on top of/on/onto/upon';
$.thrower.wear1.iobj = 'none';
$.thrower.wear2 = function wear2(cmd) {
this.wear(cmd.user);
};
Object.setOwnerOf($.thrower.wear2, $.physicals.Neil);
Object.setOwnerOf($.thrower.wear2.prototype, $.physicals.Neil);
$.thrower.wear2.verb = 'put';
$.thrower.wear2.dobj = 'none';
$.thrower.wear2.prep = 'on top of/on/onto/upon';
$.thrower.wear2.iobj = 'this';
$.thrower.location = undefined;
$.thrower.savedSvg = undefined;
$.physicals['a flame thrower'] = $.thrower;
================================================
FILE: core/core_43_genetics_lab.js
================================================
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Genetics lab demo for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.physicals['Genetics Lab'] = (new 'Object.create')($.room);
$.physicals['Genetics Lab'].name = 'Genetics Lab';
$.physicals['Genetics Lab'].location = null;
$.physicals['Genetics Lab'].description = 'To create a new mouse, type: create $.cage.mousePrototype as ';
$.physicals['Genetics Lab'].contents_ = [];
$.physicals['Genetics Lab'].contents_.forObj = $.physicals['Genetics Lab'];
Object.defineProperty($.physicals['Genetics Lab'].contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.physicals['Genetics Lab'].contents_.forKey = 'contents_';
Object.defineProperty($.physicals['Genetics Lab'].contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.cage = (new 'Object.create')($.container);
$.cage.name = 'cage';
$.cage.contents_ = [];
$.cage.contents_.forObj = $.cage;
Object.defineProperty($.cage.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.cage.contents_.forKey = 'contents_';
Object.defineProperty($.cage.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.cage.isOpen = true;
$.cage.variation = 1;
$.cage.tempo = 30;
$.cage.maxPopulation = 50;
$.cage.fight = function fight(aggressor, defender) {
var capitalizedAggressorName = $.utils.string.capitalize(String(aggressor)) + '(' + aggressor.size + ' cm)';
var defenderName = String(defender) + '(' + defender.size + ' cm)';
aggressor.aggressiveness--;
var point = Math.floor(Math.random() * (aggressor.size + defender.size));
var victim = null;
if (point > defender.size) {
victim = defender;
} else if (point < defender.size) {
victim = aggressor;
}
if (victim === defender) {
this.location.narrate(capitalizedAggressorName + ' fights and kills ' + defenderName + '.');
} else if (victim === aggressor) {
this.location.narrate(capitalizedAggressorName + ' fights and is killed by ' + defenderName + '.');
} else {
this.location.narrate(capitalizedAggressorName + ' fights ' + defenderName + ' to a draw.');
}
if (victim) {
this.kill(victim);
}
};
Object.setOwnerOf($.cage.fight, $.physicals.Neil);
$.cage.kill = function kill(victim) {
if (!this.isMouse(victim)) {
this.location.narrate('ERROR: Cannot kill ' + String(victim) + " since it doesn't appear to be a mouse in here.");
return;
}
var owner = Object.getOwnerOf(victim);
if (this === $.physicals.cage.mousePrototype) {
// Can't happen. But would be catastrophic, so check anyway.
victim.moveTo(null);
throw Error('Tried to kill the prototype mouse.');
} else if (owner === this) {
// This mouse is a child.
victim.destroy();
} else {
// This mouse belongs to a user.
victim.moveTo(owner);
this.location.narrate($.utils.string.capitalize(String(victim)) + ' is ejected from ' + String(this));
}
};
Object.setOwnerOf($.cage.kill, $.physicals.Neil);
$.cage.breed = function breed(mother, father) {
mother.fertility--;
father.fertility--;
if (mother.fertility < 0 || father.fertility < 0) {
this.location.narrate('Mating failed since one of them is sterile.');
return;
}
if (mother.sex === father.sex) {
var sex = mother.sex;
if (sex === 'M') {
sex = 'male';
} else if (sex === 'F') {
sex = 'female';
}
this.location.narrate('Mating failed since both are ' + mother.sex + '.');
return;
}
var mice = this.getContents();
if (mice.length >= this.maxPopulation) {
var oldest = mice[0];
for (var i = 1; i < mice.length; i++) {
if (mice[i].generation < oldest.generation) {
oldest = mice[i];
}
}
this.location.narrate('Maximum population (' + this.maxPopulation, ') reached; ' + String(oldest) + ' dies of old age.');
this.kill(oldest);
}
var kid = Object.create(this.mousePrototype);
Object.setOwnerOf(kid, this);
kid.init(mother, father, this.variation);
kid.moveTo(this);
this.location.narrate($.utils.string.capitalize(String(kid)) + ' has been born to ' + String(mother) + ' & ' + String(father) + '.');
this.tasks.push(setTimeout(this.life.bind(this, kid), 0));
};
Object.setOwnerOf($.cage.breed, $.physicals.Neil);
$.cage.tasks = [];
$.cage.setOpen = function setOpen(newState) {
var success = Object.getPrototypeOf($.cage).setOpen.call(this, newState);
if (!success) {
return false;
}
while (this.tasks.length) {
clearTimeout(this.tasks.pop());
}
var location = this.location;
var mice = this.getContents();
if (this.isOpen) {
for (var i = mice.length - 1; i >= 0; i--) {
this.kill(mice[i]);
}
location.narrate('All mice in ' + String(this) + ' have been exterminated.');
} else {
location.narrate($.utils.string.capitalize(String(this)) + ' starts running.');
for (var i = mice.length - 1; i >= 0; i--) {
var mouse = mice[i];
if (this.isMouse(mouse)) {
location.narrate($.utils.string.capitalize(String(mouse)) + ' starts moving.');
this.startMouse(mouse);
} else {
location.narrate($.utils.string.capitalize(String(mouse)) + " isn't a valid mouse and gets thrown out.");
mouse.moveTo(location);
}
}
}
return true;
};
Object.setOwnerOf($.cage.setOpen, $.physicals.Maximilian);
Object.setOwnerOf($.cage.setOpen.prototype, $.physicals.Maximilian);
$.cage.life = function life(mouse) {
if (!this.isMouse(mouse)) {
throw Error(String(mouse) + ' is not a valid mouse.');
}
var capitalizedMouseName = $.utils.string.capitalize(String(mouse));
var self = 'itself';
if (mouse.sex === 'M') {
self = 'himself';
} else if (mouse.sex === 'F') {
self = 'herself';
}
while (true) {
this.tasks.push(suspend(Math.random() * this.tempo * 1000));
if (mouse.location !== this) {
return;
}
if (mouse.aggressiveness < 1 && mouse.fertility < 1) {
this.location.narrate(capitalizedMouseName + ' dies after a productive life.');
this.kill(mouse);
return;
}
if (mouse.aggressiveness > 0) {
try {
var victim = mouse.pickFight();
} catch (e) {
this.kill(mouse);
this.location.narrate(capitalizedMouseName + ' threw "' + e + '" in .pickFight function.');
this.location.narrate(capitalizedMouseName + ' is being executed to put it out of its misery.');
}
if (!victim) {
this.location.narrate(capitalizedMouseName + ' decides not to fight ever again.');
mouse.aggressiveness = 0;
} else if (mouse === victim) {
this.location.narrate(capitalizedMouseName + ' fights and kills ' + self + '.');
this.kill(mouse);
return;
} else if (this.isMouse(victim)) {
this.fight(mouse, victim);
} else {
this.location.narrate(capitalizedMouseName + ' returned "' + String(victim) + '" from .pickFight function.');
this.location.narrate(capitalizedMouseName + ' is being executed to put it out of its misery.');
this.kill(mouse);
return;
}
} else if (mouse.fertility > 0) {
try {
var target = mouse.proposeMate();
} catch (e) {
this.kill(mouse);
this.location.narrate($.utils.string.capitalize(String(target)) + ' threw "' + e + '" in .proposeMate function.');
this.location.narrate($.utils.string.capitalize(String(mouse)) + ' is being executed to put it out of its misery.');
}
if (!target) {
this.location.narrate(capitalizedMouseName + ' decides not to mate ever again.');
mouse.fertility = 0;
} else if (mouse === target) {
mouse.fertility--;
this.location.narrate(capitalizedMouseName + ' is caught trying to mate with ' + self + '.');
} else if (this.isMouse(target)) {
try {
var answer = target.acceptMate(mouse);
} catch (e) {
this.location.narrate($.utils.string.capitalize(String(target)) + ' threw "' + e + '" in .acceptMate function.');
this.location.narrate($.utils.string.capitalize(String(target)) + ' is being executed to put it out of its misery.');
this.kill(target);
}
if (answer) {
this.location.narrate(capitalizedMouseName + ' asked ' + String(target) + ' to mate. The answer is YES!');
this.breed(mouse, target);
} else {
this.location.narrate(capitalizedMouseName + ' asked ' + String(target) + ' to mate. The answer is NO!');
}
} else {
this.location.narrate(capitalizedMouseName + ' returned "' + String(target) + '" from .proposeMate function.');
this.location.narrate(capitalizedMouseName + ' is being executed to put it out of its misery.');
this.kill(mouse);
return;
}
}
}
};
Object.setOwnerOf($.cage.life, $.physicals.Neil);
$.cage.willAccept = function willAccept(what, src) {
// Returns true iff this is willing to accept what arriving from src.
//
// This function (or its overrides) MUST NOT have any kind of
// observable side-effect (making noise, causing some other action,
// etc.).
return this.isOpen || (this.mousePrototype.isPrototypeOf(what) && !src);
};
Object.setOwnerOf($.cage.willAccept, $.physicals.Neil);
Object.setOwnerOf($.cage.willAccept.prototype, $.physicals.Maximilian);
$.cage.mousePrototype = (new 'Object.create')($.thing);
$.cage.mousePrototype.name = 'Genetic Mouse Prototype';
$.cage.mousePrototype.location = null;
$.cage.mousePrototype.contents_ = [];
$.cage.mousePrototype.contents_.forObj = $.cage.mousePrototype;
Object.defineProperty($.cage.mousePrototype.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.cage.mousePrototype.contents_.forKey = 'contents_';
Object.defineProperty($.cage.mousePrototype.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.cage.mousePrototype.size = 10;
$.cage.mousePrototype.generation = 0;
$.cage.mousePrototype.sex = NaN;
$.cage.mousePrototype.proposeMate = function proposeMate() {
// Return who you'd like to mate with!
// Returning null will pass on this mating and all future ones.
// Reprogram this function to make it smarter!
var mice = this.location.getContents();
return mice[Math.floor(Math.random() * mice.length)];
};
Object.setOwnerOf($.cage.mousePrototype.proposeMate, $.physicals.Neil);
$.cage.mousePrototype.acceptMate = function acceptMate(whom) {
// The mouse 'whom' wishes to mate with you!
// Return true to mate with it, or false to tell it to go away.
// Reprogram this function to make it smarter!
return Math.random() > 0.5;
};
Object.setOwnerOf($.cage.mousePrototype.acceptMate, $.physicals.Neil);
$.cage.mousePrototype.pickFight = function pickFight() {
// Return who you'd like to fight with!
// Returning null will pass on this fight and all future ones.
// The bigger mouse (based on .size) usually wins.
// Reprogram this function to make it smarter!
var mice = this.location.getContents();
return mice[Math.floor(Math.random() * mice.length)];
};
Object.setOwnerOf($.cage.mousePrototype.pickFight, $.physicals.Neil);
$.cage.mousePrototype.toString = function toString() {
var prototype = Object.getPrototypeOf(this);
var pickFightOwner = (this.pickFight === prototype.pickFight) ?
null : Object.getOwnerOf(this.pickFight);
var proposeMateOwner = (this.proposeMate === prototype.proposeMate) ?
null : Object.getOwnerOf(this.proposeMate);
var acceptMateOwner = (this.acceptMate === prototype.acceptMate) ?
null : Object.getOwnerOf(this.acceptMate);
return this.name + ' (' + String(pickFightOwner) +
'/' + String(proposeMateOwner) +
'/' + String(acceptMateOwner) + ')';
};
Object.setOwnerOf($.cage.mousePrototype.toString, $.physicals.Neil);
$.cage.mousePrototype.init = function init(mother, father, variation) {
// Blend together the numeric attributes from the parents.
var thisMouse = this;
function blend(name) {
var average = (mother[name] + father[name]) / 2;
var mutation = Math.random() * 2 * variation - variation;
thisMouse[name] = Math.max(1, Math.round(average + mutation));
}
blend('size');
blend('startFertility');
this.fertility = this.startFertility;
blend('startAggressiveness');
this.aggressiveness = this.startAggressiveness;
this.generation = 1 + Math.max(mother.generation, father.generation);
// Random sex and name.
this.sex = Math.random() > 0.5 ? 'M' : 'F';
var name = '';
for (var i = 0; i < 6; i++) {
var letters = ((i % 2) == (this.sex === 'F') ? $.utils.string.VOWELS : $.utils.string.CONSONANTS);
name += $.utils.string.randomCharacter(letters);
}
this.setName($.utils.string.capitalize(name), /*tryAlternative:*/ true);
// Copy the three 'genetic' functions.
// Take two from one parent, and one from the other.
var f1 = Math.floor(Math.random() * 2);
var f2 = Math.floor(Math.random() * 2);
var f3 = (f1 === f2) ? 1 - f1 : Math.floor(Math.random() * 2);
this.proposeMate = (f1 ? mother : father).proposeMate;
this.acceptMate = (f2 ? mother : father).acceptMate;
this.pickFight = (f3 ? mother : father).pickFight;
};
Object.setOwnerOf($.cage.mousePrototype.init, $.physicals.Neil);
$.cage.mousePrototype.svgText = '\n\n\n\n\n\n\n\n\n\n';
$.cage.mousePrototype.startAggressiveness = 2;
$.cage.mousePrototype.aggressiveness = 2;
$.cage.mousePrototype.startFertility = 4;
$.cage.mousePrototype.fertility = 3;
$.cage.mousePrototype.getCommands = function getCommands(who) {
var commands = $.thing.getCommands.call(this, who);
commands.push('program ' + this.name);
return commands;
};
Object.setOwnerOf($.cage.mousePrototype.getCommands, $.physicals.Neil);
$.cage.mousePrototype.program = function program(cmd) {
// Open this mouse in the genetics editor.
var selector = $.Selector.for(this).toString();
// No need to encode $.
var query = encodeURIComponent(String(selector)).replace(/%24/g, '$');
var link = $.hosts.root.url('genetics') + 'editor?' + query;
cmd.user.readMemo({type: "link", href: link});
};
Object.setOwnerOf($.cage.mousePrototype.program, $.physicals.Maximilian);
Object.setOwnerOf($.cage.mousePrototype.program.prototype, $.physicals.Neil);
$.cage.mousePrototype.program.verb = 'program';
$.cage.mousePrototype.program.dobj = 'this';
$.cage.mousePrototype.program.prep = 'none';
$.cage.mousePrototype.program.iobj = 'none';
$.cage.mousePrototype.description = function description() {
var sex = 'multi-sexual';
if (this.sex === 'm') sex = 'male';
if (this.sex === 'f') sex = 'female';
var desc = [];
desc.push('A ' + sex + ' mouse.');
desc.push('It is ' + this.size + ' cm long, and can have ' + this.fertility + ' more children.');
desc.push('It can fight ' + this.aggressiveness + ' other mice.');
desc.push('It belongs to generation ' + this.generation + '.');
return desc.join('\n');
};
Object.setOwnerOf($.cage.mousePrototype.description, $.physicals.Neil);
Object.setOwnerOf($.cage.mousePrototype.description.prototype, $.physicals.Neil);
$.cage.isMouse = function isMouse(animal) {
return this.mousePrototype.isPrototypeOf(animal) && (animal.location === this);
};
Object.setOwnerOf($.cage.isMouse, $.physicals.Neil);
$.cage.startMouse = function startMouse(mouse) {
// Reset all attributes to defaults.
var reset = ['fertility', 'startFertility', 'generation', 'startAggressiveness', 'sex', 'size'];
for (var i = 0; i < reset.length; i++) {
delete mouse[reset[i]];
}
// First generation mice don't fight.
mouse.aggressiveness = 0;
this.tasks.push(setTimeout(this.life.bind(this, mouse), 0));
};
Object.setOwnerOf($.cage.startMouse, $.physicals.Maximilian);
$.cage.open = function open(cmd) {
if (this.isOpen) {
cmd.user.narrate($.utils.string.capitalize(String(cmd.dobj)) + ' is already open.');
return;
}
if (this.location !== cmd.user.location && this.location !== cmd.user) {
cmd.user.narrate($.utils.string.capitalize(String(cmd.dobj)) + ' is not here.');
return;
}
if (!this.setOpen(true)) {
cmd.user.narrate('You can\'t open ' + String(cmd.dobj));
return;
}
if (cmd.user.location) {
cmd.user.location.narrate(cmd.user.name + ' opens ' + String(cmd.dobj) + '.', cmd.user);
}
cmd.user.narrate('You open ' + String(cmd.dobj) + '.');
this.look(cmd);
};
Object.setOwnerOf($.cage.open, $.physicals.Maximilian);
Object.setOwnerOf($.cage.open.prototype, $.physicals.Maximilian);
$.cage.open.verb = 'open';
$.cage.open.dobj = 'this';
$.cage.open.prep = 'none';
$.cage.open.iobj = 'none';
$.cage.svgTextClosed = '\n\n\n';
$.cage.svgTextOpen = '\n\n\n\n\n';
$.cage.location = undefined;
$.physicals.cage = $.cage;
$.physicals['Genetic Mouse Prototype'] = $.cage.mousePrototype;
$.hosts.genetics = (new 'Object.create')($.servers.http.Host.prototype);
$.hosts.genetics['/editor'] = {};
$.hosts.genetics['/editor'].www = "\n<% var staticUrl = request.hostUrl('static'); %>\n\n \n \n Code City: Genetics Editor\n favicon.ico\" rel=\"shortcut icon\">\n \n style/jfk.css\">\n\n CodeMirror/lib/codemirror.css\">\n CodeMirror/addon/lint/lint.css\">\n CodeMirror/theme/eclipse.css\">\n \n \n \n \n \n \n \n \n \n \n<%\nvar mouseSelector = decodeURIComponent(request.query);\nvar mouse = $(mouseSelector);\n%>\n \n \n\n \n
\n \n
\n \n
\n
<%= mouse && mouse.name %>
\n
\n .pickFight.proposeMate.acceptMateReference\n
\n
\n \n \n \n
\n
Properties on the mice:
\n
\n
.generation → integer
\n
The initial mice placed in the cage are generation 0.\n Their children are generation 1, and so on.
\n
.sex → 'M' or 'F' or NaN
\n
Generation 0 mice have a sex of NaN, which means they are hermaphrodites\n and can be both male and female as needed for any given mating.\n Subsequent generations have a sex set randomly at birth to be either \"M\" or \"F\".\n JavaScript tip: NaN does not equal NaN.
\n
.size → integer
\n
Larger mice are more likely to win a fight against a smaller mouse.\n Generation 0 mice are all 10 cm. Subsequent births are the average of\n their parents' sizes, plus/minus a random variation.
\n
.startFertility → integer
\n
The total number of attempts a mouse has to produce offspring during its life.\n Generation 0 mice all have a startFertility of 4. Subsequent births\n are the average of their parents' fertility, plus/minus a random variation.
\n
.fertility → integer
\n
The number of remaining attempts a mouse has to produce offspring.\n This is set to startFertility at birth, and decrements with every mating attempt.
\n
.startAggressiveness → integer
\n
The total number of fights a mouse may start during its life.\n Generation 0 mice all have a startAggressiveness of 2. Subsequent births\n are the average of their parents' aggressiveness, plus/minus a random variation.
\n
.aggressiveness → integer
\n
The number of remaining fights a mouse may start. Generation 0 mice\n have their aggressiveness set to 0 (they can't start fights). Subsequent births\n have their aggressiveness set to startAggressiveness, and decrements with every\n fight started.
\n
.location → cage
\n
This is the cage in which the mouse is located. The cage has a getContents function\n that returns an array of all mice. this.location.getContents() will always include you.
\n
\n
Functions on the mice:
\n
\n
.pickFight → mouse or null
\n
Return the mouse you'd like to fight with.\n Returning null will pass on this fight and all future ones.\n The bigger mouse (based on .size) usually wins, ties are possible.\n The loser (if there is one) dies and is removed from the cage.
\n
.proposeMate() → mouse or null
\n
Return the mouse you'd like to mate with.\n Returning null will pass on this mating and all future ones.\n Only opposite-sex matings (or those involving a NaN hermaphrodite) will produce a child.\n Each proposal decrements fertility by one, regardless of whether mating is successful.
\n
.acceptMate(mouse) → boolean
\n
The mouse passed in as the first variable wishes to mate with you.\n Return true to mate with it, or false to tell it to go away. Your fertility decrements\n by one if you say yes.
\n
Ownership
\n
The owner of any function can be obtained using Object.getOwnerOf(...). This\n might be used to conduct surveys of the genes currently in the cage, and adjusting\n behaviours accordingly.
\n
\n
Lifecycle
\n \n
Each mouse (other than generation 0) is given a number of opportunites to\n fight other mice. The pickFight functions on each mouse are called one by\n one as many times as needed.
\n
Each mouse is then given a number of opportunities to mate other mice.\n The proposeMate functions on each mouse are called one by one as many times\n as needed. When a mouse returns another mouse it wishes to mate with, that\n mouse's acceptMate function is called. If this call returns true, then a mating\n is attempted.
\n
If a mating is successful (proposed mouse says yes, proposed mouse has remaining\n fertility, mice have opposite genders or are hermaphrodites), a new mouse is born.\n This mouse will inherit traits randomly from its two parents, namely the properties\n and the three functions.
\n
Shortly after a mouse has run out of all its opportunities to fight and all\n its opportunities to mate, it dies and is remove from the cage.
\n \n
Your mouse will die. The question is can your genes (functions) spread across the\n population. There are a lot of strategies, have fun!
\n
\n
\n \n\n";
$.hosts.genetics['/editorXhr'] = {};
Object.setOwnerOf($.hosts.genetics['/editorXhr'], $.physicals.Neil);
$.hosts.genetics['/editorXhr'].www = function genetics_editorXhr_www(request, response) {
var data = {login: !!request.user, saved: false};
try { // ends with ... finally {response.write(JSON.stringify(data));}
if (!request.fromSameOrigin()) {
// Security check to ensure this is being loaded by the genetics editor.
data.butter = 'Cross-origin referer: ' + String(request.headers.referer);
return;
}
var selector;
try {
selector = new $.Selector(decodeURIComponent(request.parameters.selector));
} catch (e) {
data.butter = 'Invalid selector: ' + String(e);
return;
}
if (!request.user) {
data.butter = 'User not logged in.';
return;
}
setPerms(request.user);
// Populate the (original) value object in the reverse-lookup db.
var mouse = selector.toValue(/*save:*/true);
if (!$.cage.mousePrototype.isPrototypeOf(mouse)) {
data.butter = 'Not a mouse: ' + String(request.parameters.selector);
return;
}
// Evaluate src in global scope (eval by any other name, literally).
var evalGlobal = eval;
var butter = [];
var functionNames = ['pickFight', 'proposeMate', 'acceptMate'];
for (var i = 0; i < functionNames.length; i++) {
var functionName = functionNames[i];
var src = request.parameters[functionName];
try {
suspend();
var expr = $.utils.code.rewriteForEval(src, /*forceExpression:*/true);
var saveValue = evalGlobal(expr);
mouse[functionName] = saveValue;
} catch (e) {
// TODO(fraser): Send a more informative error message.
butter.push(functionName + ': ' + String(e));
}
}
if (butter.length) {
data.butter = butter.join('\n');
} else {
data.butter = 'Saved';
data.saved = true;
}
} finally {
response.write(JSON.stringify(data));
}
};
Object.setOwnerOf($.hosts.genetics['/editorXhr'].www, $.physicals.Neil);
Object.setOwnerOf($.hosts.genetics['/editorXhr'].www.prototype, $.physicals.Neil);
$.hosts.root.subdomains.genetics = $.hosts.genetics;
================================================
FILE: core/core_44_$.assistant.js
================================================
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Voice-activated assistant demo for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.assistant = (new 'Object.create')($.thing);
$.assistant.name = 'assistant';
$.assistant.contents_ = [];
$.assistant.contents_.forObj = $.assistant;
Object.defineProperty($.assistant.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.assistant.contents_.forKey = 'contents_';
Object.defineProperty($.assistant.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.assistant.svgText = '\n';
$.assistant.onMemo = function onMemo(memo) {
if (memo.type !== 'say') return;
// Only listen to users, not self or other bots.
if (!$.user.isPrototypeOf(memo.source)) return;
// Don't respond for 1s after last successful activation.
if (!this.lastActivated) this.lastActivated = 0;
if (Date.now() - this.lastActivated < 1000) {
this.location.narrate(String(this) + ' flashes its lights in confusion.');
return;
}
// Look for activation pharase.
var text = memo.text;
var activation = new RegExp('^\\s*hey[,\\s]+' + this.name + '[,:;.!?\\s]*([^,:;.!?\\s].*)?', 'i');
var m = activation.exec(text);
if (!m) return; // Didn't hear "hello, ".
this.lastActivated = Date.now()
// Process command.
this.onCommand(m[1] || '');
};
Object.setOwnerOf($.assistant.onMemo, $.physicals.Maximilian);
$.assistant.say = function say(speech) {
if (!this.location) return;
var memo = {
type: 'say',
source: this,
where: this.location,
text: speech
};
this.location.sendMemo(memo);
};
$.assistant.onCommand = function onCommand(command) {
/* Attempt to find a handler for command, by calling methods on this
* named cmd_* until one of them returns true.
*/
suspend(2000);
var raw = String(command);
command = command.replace(/[.,!?]*$/, ''); // Trim trailing punctuation.
command = command.replace(/\s+/, ' '); // Normalise whitespace.
if (!command) {
this.say('How can I help?');
return;
}
// Look for properties on this named 'cmd_'.
var done = false;
for (var key in this) {
if (key.lastIndexOf('cmd_', 0) !== 0) continue;
var func = this[key];
if (typeof func !== 'function') continue;
if (func.call(this, command, raw)) {
done = true;
break;
}
}
if (!done) this.say('Sorry, I don\'t understand "' + command + '".');
};
Object.setOwnerOf($.assistant.onCommand, $.physicals.Maximilian);
$.assistant.cmd_time = function cmd_time(command, raw) {
// First check to see if the command looked like a request for the time.
if (!command.match(/what time is it|what('s| is) the time/i)) return false;
// It did. Tell the time.
this.say('The current time is ' + new Date().toTimeString());
return true;
};
Object.setOwnerOf($.assistant.cmd_time, $.physicals.Maximilian);
Object.setOwnerOf($.assistant.cmd_time.prototype, $.physicals.Maximilian);
$.assistant.cmd_translate = function cmd_translate(command, raw) {
// First check to see if the command looked like a request to translate some text.
var m = raw.match(/(?:what\s+is|how\s+do\s+you\s+say)\s+(?:"([^"]+)"|(.*))\s+in\s+(\w+)/i);
if (!m) return false; // Nope; try another handler.
// It did. Try to tranlsate it.
var phrase = m[1] || m[2];
var code = this.cmd_translate.languages[m[3].toLowerCase()];
var language = $.utils.string.capitalize(m[3]);
if (!code) {
this.say("Sorry; I don't know how to speak " + language);
return true;
}
try {
var translation = $.utils.string.translate(phrase, code);
this.say('"' + phrase + '" in ' + language + ' is "' + translation + '"');
} catch (e) {
this.say('Sorry: I seem to have forotten how to speak ' + language + '.');
}
return true;
};
Object.setOwnerOf($.assistant.cmd_translate, $.physicals.Maximilian);
Object.setOwnerOf($.assistant.cmd_translate.prototype, $.physicals.Maximilian);
$.assistant.cmd_translate.languages = (new 'Object.create')(null);
$.assistant.cmd_translate.languages.german = 'de';
$.assistant.cmd_translate.languages.italian = 'it';
$.assistant.cmd_translate.languages.french = 'fr';
$.assistant._README = 'The assistant works as follows:\n\nThe .onMemo handler looks for a "say" memo from $.user. If one is received, and no other has been received too recently, it calls .onCommand, passing it what was said.\n\nThe .onCommand handler waits a respectable amount of time (2s) and then attempts to find a handler for the command. It canonicalises the command, and then iterates through its own and inherited properties. Any property whose name begins with "cmd_" and whose value is a function will get called, being passed the canonicalised and raw command.\n\nEach cmd_* method is expected to do some kind of string match against the command (perhaps using a RegExp) to see if if it knows how to handle that sort of command. If it does, it should respond (perhaps using the .say method to reply to the user) and return true. If it does not know how to handle the command, it should return false.\n\n.onCommand will iterate through the .cmd_* methods until one of them returns true. If none do it will announce that it did not understand the command.';
$.assistant.description = "A squat grey cylinder that looks like it's listening.";
$.assistant.location = undefined;
$.assistant.lastActivated = undefined;
$.physicals.assistant = $.assistant;
================================================
FILE: core/core_45_Challenge_Room.js
================================================
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Challenge room demo for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.physicals['Challenge room'] = (new 'Object.create')($.room);
Object.setOwnerOf($.physicals['Challenge room'], $.physicals.Neil);
$.physicals['Challenge room'].name = 'Challenge room';
$.physicals['Challenge room'].location = null;
$.physicals['Challenge room'].contents_ = [];
$.physicals['Challenge room'].contents_[0] = (new 'Object.create')($.thing);
$.physicals['Challenge room'].contents_[1] = (new 'Object.create')($.container);
$.physicals['Challenge room'].contents_[2] = (new 'Object.create')($.thing);
$.physicals['Challenge room'].contents_.forObj = $.physicals['Challenge room'];
Object.defineProperty($.physicals['Challenge room'].contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.physicals['Challenge room'].contents_.forKey = 'contents_';
Object.defineProperty($.physicals['Challenge room'].contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.physicals['Challenge room'].reset = function reset(cmd) {
this.switch.state = false;
this.switch.movable = true;
this.switch.moveTo(this);
this.switch.movable = false;
for (var x = 0; x < 1000; x++) {
this.safe.setOpen(true, x);
}
this.food.moveTo(this.safe);
this.food.svgText = this.food.svgTextReset;
this.safe.setOpen(false);
this.safe.crack = this.safe.crackReset;
this.chest.movable = true;
this.chest.moveTo(this);
this.chest.movable = false;
this.chest.setOpen(true);
this.safe.moveTo(this.chest);
this.chest.setOpen(false);
this.girl.movable = true;
this.girl.moveTo(this);
this.girl.movable = false;
this.girl.attempts = 0;
if (cmd) {
this.sendScene(cmd.user, true);
this.narrate(cmd.user.name + ' resets ' + String(this) + '.', cmd.user);
cmd.user.narrate('You reset ' + String(this) + '.');
}
};
Object.setOwnerOf($.physicals['Challenge room'].reset, $.physicals.Neil);
Object.setOwnerOf($.physicals['Challenge room'].reset.prototype, $.physicals.Neil);
$.physicals['Challenge room'].reset.verb = 'reset';
$.physicals['Challenge room'].reset.dobj = 'none';
$.physicals['Challenge room'].reset.prep = 'none';
$.physicals['Challenge room'].reset.iobj = 'none';
$.physicals['Challenge room'].chest = $.physicals['Challenge room'].contents_[1];
$.physicals['Challenge room'].safe = (new 'Object.create')($.container);
$.physicals['Challenge room'].girl = $.physicals['Challenge room'].contents_[2];
$.physicals['Challenge room'].food = (new 'Object.create')($.thing);
$.physicals['Challenge room'].switch = $.physicals['Challenge room'].contents_[0];
$.physicals['Challenge room'].svgTextNight = '\n';
$.physicals['Challenge room'].getContents = function getContents() {
$.physical.validate.call(this);
if (this.switch.state) {
return this.contents_.slice();
} else {
var contents = [];
for (var i = 0, o; (o = this.contents_[i]); i++) {
if (o === this.switch || $.user.isPrototypeOf(o)) {
contents.push(o);
}
}
return contents;
}
};
Object.setOwnerOf($.physicals['Challenge room'].getContents, $.physicals.Neil);
$.physicals['Challenge room'].description = function description() {
return this.switch.state ? 'Can you solve the challenge?' : 'It\'s dark in here.';
};
Object.setOwnerOf($.physicals['Challenge room'].description, $.physicals.Neil);
$.physicals['Challenge room'].svgTextDay = '';
$.physicals['Challenge room'].svgText = function svgText() {
return this.switch.state ? this.svgTextDay : this.svgTextNight;
};
Object.setOwnerOf($.physicals['Challenge room'].svgText, $.physicals.Neil);
$.physicals['light switch'] = $.physicals['Challenge room'].switch;
Object.setOwnerOf($.physicals['light switch'], $.physicals.Neil);
$.physicals['light switch'].name = 'light switch';
$.physicals['light switch'].location = $.physicals['Challenge room'];
$.physicals['light switch'].contents_ = [];
$.physicals['light switch'].contents_.forObj = $.physicals['light switch'];
Object.defineProperty($.physicals['light switch'].contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.physicals['light switch'].contents_.forKey = 'contents_';
Object.defineProperty($.physicals['light switch'].contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.physicals['light switch'].svgText = function svgText() {
return this.state ? this.svgTextDay : this.svgTextNight;
};
Object.setOwnerOf($.physicals['light switch'].svgText, $.physicals.Neil);
Object.setOwnerOf($.physicals['light switch'].svgText.prototype, $.physicals.Maximilian);
$.physicals['light switch'].state = false;
$.physicals['light switch'].flip = function flip(newState, user) {
var onOff = newState ? 'on' : 'off';
if (this.state === newState) {
user.narrate('The switch is already ' + onOff + '.');
} else {
this.state = newState;
this.home.updateScene(true);
user.narrate('You turn ' + onOff + ' the switch.');
this.home.narrate(String(user) + ' turns ' + onOff + ' the switch.', user);
}
};
Object.setOwnerOf($.physicals['light switch'].flip, $.physicals.Neil);
Object.setOwnerOf($.physicals['light switch'].flip.prototype, $.physicals.Neil);
$.physicals['light switch'].flipOn1 = function flipOn1(cmd) {
this.flip(true, cmd.user);
};
Object.setOwnerOf($.physicals['light switch'].flipOn1, $.physicals.Maximilian);
Object.setOwnerOf($.physicals['light switch'].flipOn1.prototype, $.physicals.Maximilian);
$.physicals['light switch'].flipOn1.verb = 'flip|turn|switch';
$.physicals['light switch'].flipOn1.dobj = 'this';
$.physicals['light switch'].flipOn1.prep = 'on top of/on/onto/upon';
$.physicals['light switch'].flipOn1.iobj = 'none';
$.physicals['light switch'].flipOn2 = function flipOn2(cmd) {
this.flip(true, cmd.user);
};
Object.setOwnerOf($.physicals['light switch'].flipOn2, $.physicals.Maximilian);
Object.setOwnerOf($.physicals['light switch'].flipOn2.prototype, $.physicals.Maximilian);
$.physicals['light switch'].flipOn2.verb = 'flip|turn|switch';
$.physicals['light switch'].flipOn2.dobj = 'none';
$.physicals['light switch'].flipOn2.prep = 'on top of/on/onto/upon';
$.physicals['light switch'].flipOn2.iobj = 'this';
$.physicals['light switch'].flipOff2 = function flipOff2(cmd) {
this.flip(false, cmd.user);
};
Object.setOwnerOf($.physicals['light switch'].flipOff2, $.physicals.Maximilian);
Object.setOwnerOf($.physicals['light switch'].flipOff2.prototype, $.physicals.Maximilian);
$.physicals['light switch'].flipOff2.verb = 'flip|turn|switch';
$.physicals['light switch'].flipOff2.dobj = 'none';
$.physicals['light switch'].flipOff2.prep = 'off/off of';
$.physicals['light switch'].flipOff2.iobj = 'this';
$.physicals['light switch'].flipOff1 = function flipOff1(cmd) {
this.flip(false, cmd.user);
};
Object.setOwnerOf($.physicals['light switch'].flipOff1, $.physicals.Maximilian);
Object.setOwnerOf($.physicals['light switch'].flipOff1.prototype, $.physicals.Maximilian);
$.physicals['light switch'].flipOff1.verb = 'flip|turn|switch';
$.physicals['light switch'].flipOff1.dobj = 'this';
$.physicals['light switch'].flipOff1.prep = 'off/off of';
$.physicals['light switch'].flipOff1.iobj = 'none';
$.physicals['light switch'].home = $.physicals['Challenge room'];
$.physicals['light switch'].svgTextNight = '\n \n \n \n';
$.physicals['light switch'].getCommands = function getCommands(who) {
var commands = $.thing.getCommands.call(this, who);
if (this.state) {
commands.push('turn off ' + String(this));
} else {
commands.push('turn on ' + String(this));
}
return commands;
};
Object.setOwnerOf($.physicals['light switch'].getCommands, $.physicals.Neil);
Object.setOwnerOf($.physicals['light switch'].getCommands.prototype, $.physicals.Maximilian);
$.physicals['light switch'].aliases = [];
Object.setOwnerOf($.physicals['light switch'].aliases, $.physicals.Maximilian);
$.physicals['light switch'].aliases[0] = 'lightswitch';
$.physicals['light switch'].aliases[1] = 'switch';
$.physicals['light switch'].movable = false;
$.physicals['light switch'].svgTextDay = '\n\n\n\n';
$.physicals.chest = $.physicals['Challenge room'].chest;
Object.setOwnerOf($.physicals.chest, $.physicals.Neil);
$.physicals.chest.name = 'chest';
$.physicals.chest.location = $.physicals['Challenge room'];
$.physicals.chest.contents_ = [];
$.physicals.chest.contents_[0] = $.physicals.chest.location.safe;
$.physicals.chest.contents_.forObj = $.physicals.chest;
Object.defineProperty($.physicals.chest.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.physicals.chest.contents_.forKey = 'contents_';
Object.defineProperty($.physicals.chest.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.physicals.chest.svgTextClosed = '\n\n\n\n\n';
$.physicals.chest.svgTextOpen = '\n\n\n\n\n\n\n';
$.physicals.chest.isOpen = false;
$.physicals.chest.description = 'A steamer chest with a very heavy lid.';
$.physicals.chest.TIME = 5000;
$.physicals.chest.lastTime_ = 1597444509970;
$.physicals.chest.lastUser_ = $.physicals.Neil;
$.physicals.chest.open = function open(cmd) {
if (this.isOpen) {
cmd.user.narrate($.utils.string.capitalize(String(cmd.dobj)) + ' is already open.');
return;
}
if (this.location !== cmd.user.location && this.location !== cmd.user) {
cmd.user.narrate($.utils.string.capitalize(String(cmd.dobj)) + ' is not here.');
return;
}
if (this.lastUser_ === cmd.user || this.lastTime_ + this.TIME < Date.now()) {
cmd.user.narrate('You try to open the chest, but the lid is too heavy for one person.');
if (cmd.user.location) {
cmd.user.location.narrate(String(cmd.user) + ' tries to open the chest, but the lid is too heavy for one person.', cmd.user);
}
this.lastUser_ = cmd.user;
this.lastTime_ = Date.now();
return;
}
if (!this.setOpen(true)) {
cmd.user.narrate('You can\'t open ' + String(cmd.dobj));
return;
}
if (cmd.user.location) {
cmd.user.location.narrate(String(cmd.user) + ' helps ' + String(this.lastUser_.name) + ' to opens ' + String(cmd.dobj) + '.', cmd.user);
}
cmd.user.narrate('You help ' + String(this.lastUser_) + ' to open ' + String(cmd.dobj) + '.');
this.look(cmd);
this.lastUser_ = null;
this.lastTime_ = 0;
};
Object.setOwnerOf($.physicals.chest.open, $.physicals.Neil);
Object.setOwnerOf($.physicals.chest.open.prototype, $.physicals.Maximilian);
$.physicals.chest.open.verb = 'open';
$.physicals.chest.open.dobj = 'this';
$.physicals.chest.open.prep = 'none';
$.physicals.chest.open.iobj = 'none';
$.physicals.chest.movable = false;
$.physicals.chest.toFloor = true;
$.physicals.chest.setOpen = function setOpen(newState) {
this.isOpen = Boolean(newState);
if ($.room.isPrototypeOf(this.location)) {
this.location.updateScene(true);
}
return true;
};
Object.setOwnerOf($.physicals.chest.setOpen, $.physicals.Neil);
Object.setOwnerOf($.physicals.chest.setOpen.prototype, $.physicals.Maximilian);
$.physicals.safe = $.physicals.chest.location.safe;
Object.setOwnerOf($.physicals.safe, $.physicals.Neil);
$.physicals.safe.name = 'safe';
$.physicals.safe.location = $.physicals.chest;
$.physicals.safe.contents_ = [];
$.physicals.safe.contents_[0] = $.physicals.chest.location.food;
$.physicals.safe.contents_.forObj = $.physicals.safe;
Object.defineProperty($.physicals.safe.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.physicals.safe.contents_.forKey = 'contents_';
Object.defineProperty($.physicals.safe.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.physicals.safe.isOpen = false;
$.physicals.safe.description = 'The safe is secured with a three digit combination: open safe with xxx';
$.physicals.safe.open = function open(cmd) {
cmd.user.narrate('You need a three-digit combination to open the safe: open ' + String(cmd.dobj) + ' with xxx');
};
Object.setOwnerOf($.physicals.safe.open, $.physicals.Maximilian);
Object.setOwnerOf($.physicals.safe.open.prototype, $.physicals.Maximilian);
$.physicals.safe.open.verb = 'open';
$.physicals.safe.open.dobj = 'this';
$.physicals.safe.open.prep = 'none';
$.physicals.safe.open.iobj = 'none';
$.physicals.safe.openWith = function openWith(cmd) {
if (this.isOpen) {
cmd.user.narrate($.utils.string.capitalize(String(cmd.dobj)) + ' is already open.');
return;
}
if (this.location !== cmd.user.location && this.location !== cmd.user) {
cmd.user.narrate($.utils.string.capitalize(String(cmd.dobj)) + ' is not here.');
return;
}
if (!this.setOpen(true, cmd.iobjstr)) {
cmd.user.narrate('"' + cmd.iobjstr + '" is not the correct combination.');
return;
}
if (cmd.user.location) {
cmd.user.location.narrate(cmd.user.name + ' opens ' + String(cmd.dobj) + '.', cmd.user);
}
cmd.user.narrate('You open ' + String(cmd.dobj) + '.');
this.look(cmd);
};
Object.setOwnerOf($.physicals.safe.openWith, $.physicals.Maximilian);
Object.setOwnerOf($.physicals.safe.openWith.prototype, $.physicals.Maximilian);
$.physicals.safe.openWith.verb = 'open';
$.physicals.safe.openWith.dobj = 'this';
$.physicals.safe.openWith.prep = 'with/using';
$.physicals.safe.openWith.iobj = 'any';
$.physicals.safe.setOpen = function setOpen(newState, combo) {
if (newState && this.combo !== $.utils.string.hash('md5', String(combo))) {
return false;
}
return $.container.setOpen.call(this, newState);
};
Object.setOwnerOf($.physicals.safe.setOpen, $.physicals.Neil);
Object.setOwnerOf($.physicals.safe.setOpen.prototype, $.physicals.Maximilian);
$.physicals.safe.combo = 'e94550c93cd70fe748e6982b3439ad3b';
$.physicals.safe.svgTextClosed = '\n\n\n\n';
$.physicals.safe.svgTextOpen = '\n\n\n\n\n';
$.physicals.safe.getCommands = function getCommands(who) {
var commands = $.container.getCommands.call(this, who);
commands.push('crack ' + String(this));
return commands;
};
Object.setOwnerOf($.physicals.safe.getCommands, $.physicals.Neil);
Object.setOwnerOf($.physicals.safe.getCommands.prototype, $.physicals.Maximilian);
$.physicals.safe.crack = function crack(cmd) {
cmd.user.narrate('The "crack" function has not been programmed. ' +
'To do so, visit: https://google.codecity.world/blocklySafe');
// API information: To open the safe with combo 123, use:
// this.setOpen(true, 123);
// Have fun!
};
Object.setOwnerOf($.physicals.safe.crack, $.physicals.Neil);
Object.setOwnerOf($.physicals.safe.crack.prototype, $.physicals.Neil);
$.physicals.safe.crack.verb = 'crack';
$.physicals.safe.crack.dobj = 'this';
$.physicals.safe.crack.prep = 'none';
$.physicals.safe.crack.iobj = 'none';
$.physicals.safe.crackReset = $.physicals.safe.crack;
$.physicals.safe.toFloor = true;
$.physicals.food = $.physicals.chest.location.food;
Object.setOwnerOf($.physicals.food, $.physicals.Neil);
$.physicals.food.name = 'food';
$.physicals.food.location = $.physicals.safe;
$.physicals.food.contents_ = [];
$.physicals.food.contents_.forObj = $.physicals.food;
Object.defineProperty($.physicals.food.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.physicals.food.contents_.forKey = 'contents_';
Object.defineProperty($.physicals.food.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.physicals.food.svgText = '\n\n';
$.physicals.food.give = function give(cmd) {
if (cmd.iobj !== this.girl) {
return $.thing.give.call(this, cmd);
}
if (this.location !== cmd.user && this.location !== cmd.user.location) {
cmd.user.narrate("You can't reach " + String(this) + ".");
return;
}
cmd.user.narrate('You offer ' + String(this) + ' to ' + String(cmd.iobj) + '.');
if (cmd.user.location) {
cmd.user.location.narrate(
String(cmd.user) + ' offers ' + String(this) + ' to ' + String(cmd.iobj) + '.',
[cmd.user, cmd.iobj]);
}
var matches = $.utils.imageMatch.recog($.utils.object.getValue(this, 'svgText'));
var ok = this.girl.foodList.includes(matches[0]);
if (!ok) {
this.girl.attempts++;
}
var name = matches[0] || 'nothing I\'ve ever seen before.';
suspend(1);
var text = 'It looks like a ' + name + '; ' +
(ok ? 'delicious!'
: (this.girl.attempts < 3 ?
'I won\'t eat that!' :
(Math.random() >= 0.5 ?
'that won\'t keep the doctor away!' : 'some fruit would be nice!'
)
)
);
var alt = 'The girl says, "' + text +'"';
var memo = {
type: 'say',
source: this.girl,
where: this.girl.location,
text: text,
alt: alt
};
this.girl.location.sendMemo(memo);
if (ok) {
suspend(10);
memo.text = 'Thank you so much. Congratulations on solving the challenge room. Don\'t forget to turn out the light when you leave.';
memo.alt = 'The girl says, "' + text + '"';
this.girl.location.sendMemo(memo);
}
};
Object.setOwnerOf($.physicals.food.give, $.physicals.Neil);
Object.setOwnerOf($.physicals.food.give.prototype, $.physicals.Maximilian);
$.physicals.food.give.verb = 'give';
$.physicals.food.give.dobj = 'this';
$.physicals.food.give.prep = 'at/to';
$.physicals.food.give.iobj = 'any';
$.physicals.food.girl = $.physicals.chest.location.girl;
$.physicals.food.redraw = function inspect(cmd) {
// Open this object in the SVG editor.
var selector = $.Selector.for(this);
if (!selector) {
cmd.user.narrate('Unfortuantely the code editor does not know how to locate ' + String(this) + ' yet.');
return;
}
var link = '/code?' + encodeURIComponent(String(selector) + '.svgText');
cmd.user.readMemo({type: "link", href: link});
};
Object.setOwnerOf($.physicals.food.redraw, $.physicals.Neil);
Object.setOwnerOf($.physicals.food.redraw.prototype, $.physicals.Neil);
$.physicals.food.redraw.verb = 'redraw';
$.physicals.food.redraw.dobj = 'this';
$.physicals.food.redraw.prep = 'none';
$.physicals.food.redraw.iobj = 'none';
$.physicals.food.getCommands = function getCommands(who) {
var commands = $.thing.getCommands.call(this, who);
commands.push('redraw ' + this.name);
commands.push('give ' + this.name + ' to girl');
return commands;
};
Object.setOwnerOf($.physicals.food.getCommands, $.physicals.Neil);
Object.setOwnerOf($.physicals.food.getCommands.prototype, $.physicals.Maximilian);
$.physicals.food.svgTextReset = '\n\n';
$.physicals.girl = $.physicals.food.girl;
Object.setOwnerOf($.physicals.girl, $.physicals.Neil);
$.physicals.girl.name = 'girl';
$.physicals.girl.location = $.physicals.chest.location;
$.physicals.girl.contents_ = [];
$.physicals.girl.contents_.forObj = $.physicals.girl;
Object.defineProperty($.physicals.girl.contents_, 'forObj', {writable: false, enumerable: false, configurable: false});
$.physicals.girl.contents_.forKey = 'contents_';
Object.defineProperty($.physicals.girl.contents_, 'forKey', {writable: false, enumerable: false, configurable: false});
$.physicals.girl.description = 'She looks REALLY hungry.';
$.physicals.girl.svgText = ' \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n';
$.physicals.girl.get = function get(cmd) {
cmd.user.narrate('That\'s probably not appropriate.');
if (cmd.user.location) {
cmd.user.location.narrate(String(cmd.user) + ' tries to pick up ' + String(this) + '.', cmd.user);
}
};
Object.setOwnerOf($.physicals.girl.get, $.physicals.Neil);
Object.setOwnerOf($.physicals.girl.get.prototype, $.physicals.Maximilian);
$.physicals.girl.get.verb = 'get|take';
$.physicals.girl.get.dobj = 'this';
$.physicals.girl.get.prep = 'none';
$.physicals.girl.get.iobj = 'none';
$.physicals.girl.food = $.physicals.food;
$.physicals.girl.foodList = [];
$.physicals.girl.foodList[0] = 'Açaí';
$.physicals.girl.foodList[1] = 'Ackee';
$.physicals.girl.foodList[2] = 'Apple';
$.physicals.girl.foodList[3] = 'Apricot';
$.physicals.girl.foodList[4] = 'Avocado';
$.physicals.girl.foodList[5] = 'Banana';
$.physicals.girl.foodList[6] = 'Bilberry';
$.physicals.girl.foodList[7] = 'Blackberry';
$.physicals.girl.foodList[8] = 'Blackcurrant';
$.physicals.girl.foodList[9] = 'Black sapote';
$.physicals.girl.foodList[10] = 'Blueberry';
$.physicals.girl.foodList[11] = 'Boysenberry';
$.physicals.girl.foodList[12] = 'Breadfruit';
$.physicals.girl.foodList[13] = "Buddha's hand";
$.physicals.girl.foodList[14] = 'Cactus pear';
$.physicals.girl.foodList[15] = 'Crab apple';
$.physicals.girl.foodList[16] = 'Currant';
$.physicals.girl.foodList[17] = 'Cherry';
$.physicals.girl.foodList[18] = 'Cherimoya';
$.physicals.girl.foodList[19] = 'Chico fruit';
$.physicals.girl.foodList[20] = 'Cloudberry';
$.physicals.girl.foodList[21] = 'Coconut';
$.physicals.girl.foodList[22] = 'Cranberry';
$.physicals.girl.foodList[23] = 'Damson';
$.physicals.girl.foodList[24] = 'Date';
$.physicals.girl.foodList[25] = 'Dragonfruit';
$.physicals.girl.foodList[26] = 'Durian';
$.physicals.girl.foodList[27] = 'Elderberry';
$.physicals.girl.foodList[28] = 'Feijoa';
$.physicals.girl.foodList[29] = 'Fig';
$.physicals.girl.foodList[30] = 'Goji berry';
$.physicals.girl.foodList[31] = 'Gooseberry';
$.physicals.girl.foodList[32] = 'Grape';
$.physicals.girl.foodList[33] = 'Grewia asiatica';
$.physicals.girl.foodList[34] = 'Raisin';
$.physicals.girl.foodList[35] = 'Grapefruit';
$.physicals.girl.foodList[36] = 'Guava';
$.physicals.girl.foodList[37] = 'Hala Fruit';
$.physicals.girl.foodList[38] = 'Honeyberry';
$.physicals.girl.foodList[39] = 'Huckleberry';
$.physicals.girl.foodList[40] = 'Jabuticaba';
$.physicals.girl.foodList[41] = 'Jackfruit';
$.physicals.girl.foodList[42] = 'Jambul';
$.physicals.girl.foodList[43] = 'Japanese plum';
$.physicals.girl.foodList[44] = 'Jostaberry';
$.physicals.girl.foodList[45] = 'Jujube';
$.physicals.girl.foodList[46] = 'Juniper berry';
$.physicals.girl.foodList[47] = 'Kiwano';
$.physicals.girl.foodList[48] = 'Kiwifruit';
$.physicals.girl.foodList[49] = 'Kumquat';
$.physicals.girl.foodList[50] = 'Lemon';
$.physicals.girl.foodList[51] = 'Lime';
$.physicals.girl.foodList[52] = 'Loganberry';
$.physicals.girl.foodList[53] = 'Loquat';
$.physicals.girl.foodList[54] = 'Longan';
$.physicals.girl.foodList[55] = 'Lychee';
$.physicals.girl.foodList[56] = 'Mango';
$.physicals.girl.foodList[57] = 'Mangosteen';
$.physicals.girl.foodList[58] = 'Marionberry';
$.physicals.girl.foodList[59] = 'Melon';
$.physicals.girl.foodList[60] = 'Cantaloupe';
$.physicals.girl.foodList[61] = 'Galia melon';
$.physicals.girl.foodList[62] = 'Honeydew';
$.physicals.girl.foodList[63] = 'Watermelon';
$.physicals.girl.foodList[64] = 'Miracle fruit';
$.physicals.girl.foodList[65] = 'Monstera Delisiousa';
$.physicals.girl.foodList[66] = 'Mulberry';
$.physicals.girl.foodList[67] = 'Nance';
$.physicals.girl.foodList[68] = 'Nectarine';
$.physicals.girl.foodList[69] = 'Orange';
$.physicals.girl.foodList[70] = 'Blood orange';
$.physicals.girl.foodList[71] = 'Clementine';
$.physicals.girl.foodList[72] = 'Mandarine';
$.physicals.girl.foodList[73] = 'Tangerine';
$.physicals.girl.foodList[74] = 'Papaya';
$.physicals.girl.foodList[75] = 'Passionfruit';
$.physicals.girl.foodList[76] = 'Peach';
$.physicals.girl.foodList[77] = 'Pear';
$.physicals.girl.foodList[78] = 'Persimmon';
$.physicals.girl.foodList[79] = 'Plantain';
$.physicals.girl.foodList[80] = 'Plum';
$.physicals.girl.foodList[81] = 'Prune';
$.physicals.girl.foodList[82] = 'Pineapple';
$.physicals.girl.foodList[83] = 'Pineberry';
$.physicals.girl.foodList[84] = 'Plumcot';
$.physicals.girl.foodList[85] = 'Pomegranate';
$.physicals.girl.foodList[86] = 'Pomelo';
$.physicals.girl.foodList[87] = 'Purple mangosteen';
$.physicals.girl.foodList[88] = 'Quince';
$.physicals.girl.foodList[89] = 'Raspberry';
$.physicals.girl.foodList[90] = 'Salmonberry';
$.physicals.girl.foodList[91] = 'Rambutan';
$.physicals.girl.foodList[92] = 'Redcurrant';
$.physicals.girl.foodList[93] = 'Salal berry';
$.physicals.girl.foodList[94] = 'Salak';
$.physicals.girl.foodList[95] = 'Satsuma';
$.physicals.girl.foodList[96] = 'Soursop';
$.physicals.girl.foodList[97] = 'Star apple';
$.physicals.girl.foodList[98] = 'Star fruit';
$.physicals.girl.foodList[99] = 'Strawberry';
$.physicals.girl.foodList[100] = 'Surinam cherry';
$.physicals.girl.foodList[101] = 'Tamarillo';
$.physicals.girl.foodList[102] = 'Tamarind';
$.physicals.girl.foodList[103] = 'Tangelo';
$.physicals.girl.foodList[104] = 'Tayberry';
$.physicals.girl.foodList[105] = 'Ugli fruit';
$.physicals.girl.foodList[106] = 'White currant';
$.physicals.girl.foodList[107] = 'White sapote';
$.physicals.girl.foodList[108] = 'Yuzu';
$.physicals.girl.foodList[109] = 'Bell pepper';
$.physicals.girl.foodList[110] = 'Chile pepper';
$.physicals.girl.foodList[111] = 'Corn kernel';
$.physicals.girl.foodList[112] = 'Cucumber';
$.physicals.girl.foodList[113] = 'Eggplant';
$.physicals.girl.foodList[114] = 'Jalapeño';
$.physicals.girl.foodList[115] = 'Olive';
$.physicals.girl.foodList[116] = 'Pea';
$.physicals.girl.foodList[117] = 'Pumpkin';
$.physicals.girl.foodList[118] = 'Squash';
$.physicals.girl.foodList[119] = 'Tomato';
$.physicals.girl.foodList[120] = 'Zucchini';
$.physicals.girl.foodList[121] = 'asparagus';
$.physicals.girl.foodList[122] = 'apple';
$.physicals.girl.foodList[123] = 'avocado';
$.physicals.girl.foodList[124] = 'alfalfa';
$.physicals.girl.foodList[125] = 'almond';
$.physicals.girl.foodList[126] = 'arugula';
$.physicals.girl.foodList[127] = 'artichoke';
$.physicals.girl.foodList[128] = 'applesauce';
$.physicals.girl.foodList[129] = 'antelope';
$.physicals.girl.foodList[130] = 'bruscetta';
$.physicals.girl.foodList[131] = 'bacon';
$.physicals.girl.foodList[132] = 'black beans';
$.physicals.girl.foodList[133] = 'bagels';
$.physicals.girl.foodList[134] = 'baked beans';
$.physicals.girl.foodList[135] = 'bbq';
$.physicals.girl.foodList[136] = 'bison';
$.physicals.girl.foodList[137] = 'barley';
$.physicals.girl.foodList[138] = 'beer';
$.physicals.girl.foodList[139] = 'bisque';
$.physicals.girl.foodList[140] = 'bluefish';
$.physicals.girl.foodList[141] = 'bread';
$.physicals.girl.foodList[142] = 'broccoli';
$.physicals.girl.foodList[143] = 'buritto';
$.physicals.girl.foodList[144] = 'babaganoosh';
$.physicals.girl.foodList[145] = 'cabbage';
$.physicals.girl.foodList[146] = 'cake';
$.physicals.girl.foodList[147] = 'carrots';
$.physicals.girl.foodList[148] = 'carne asada';
$.physicals.girl.foodList[149] = 'celery';
$.physicals.girl.foodList[150] = 'cheese';
$.physicals.girl.foodList[151] = 'chicken';
$.physicals.girl.foodList[152] = 'catfish';
$.physicals.girl.foodList[153] = 'cheeseburger';
$.physicals.girl.foodList[154] = 'chips';
$.physicals.girl.foodList[155] = 'chocolate';
$.physicals.girl.foodList[156] = 'chowder';
$.physicals.girl.foodList[157] = 'clams';
$.physicals.girl.foodList[158] = 'coffee';
$.physicals.girl.foodList[159] = 'cookie';
$.physicals.girl.foodList[160] = 'corn';
$.physicals.girl.foodList[161] = 'cupcake';
$.physicals.girl.foodList[162] = 'crab';
$.physicals.girl.foodList[163] = 'curry';
$.physicals.girl.foodList[164] = 'cereal';
$.physicals.girl.foodList[165] = 'chimichanga';
$.physicals.girl.foodList[166] = 'dates';
$.physicals.girl.foodList[167] = 'dips';
$.physicals.girl.foodList[168] = 'duck';
$.physicals.girl.foodList[169] = 'dumpling';
$.physicals.girl.foodList[170] = 'donuts';
$.physicals.girl.foodList[171] = 'eggs';
$.physicals.girl.foodList[172] = 'enchilada';
$.physicals.girl.foodList[173] = 'eggroll';
$.physicals.girl.foodList[174] = 'english muffin';
$.physicals.girl.foodList[175] = 'edamame';
$.physicals.girl.foodList[176] = 'eel sushi';
$.physicals.girl.foodList[177] = 'fajita';
$.physicals.girl.foodList[178] = 'falafel';
$.physicals.girl.foodList[179] = 'fish';
$.physicals.girl.foodList[180] = 'franks';
$.physicals.girl.foodList[181] = 'fondu';
$.physicals.girl.foodList[182] = 'french toast';
$.physicals.girl.foodList[183] = 'french dip';
$.physicals.girl.foodList[184] = 'garlic';
$.physicals.girl.foodList[185] = 'ginger';
$.physicals.girl.foodList[186] = 'gnocchi';
$.physicals.girl.foodList[187] = 'goose';
$.physicals.girl.foodList[188] = 'granola';
$.physicals.girl.foodList[189] = 'grapes';
$.physicals.girl.foodList[190] = 'green beans';
$.physicals.girl.foodList[191] = 'guacamole';
$.physicals.girl.foodList[192] = 'gumbo';
$.physicals.girl.foodList[193] = 'grits';
$.physicals.girl.foodList[194] = 'graham crackers';
$.physicals.girl.foodList[195] = 'ham';
$.physicals.girl.foodList[196] = 'halibut';
$.physicals.girl.foodList[197] = 'hamburger';
$.physicals.girl.foodList[198] = 'honey';
$.physicals.girl.foodList[199] = 'huenos rancheros';
$.physicals.girl.foodList[200] = 'hash browns';
$.physicals.girl.foodList[201] = 'hot dogs';
$.physicals.girl.foodList[202] = 'haiku roll';
$.physicals.girl.foodList[203] = 'hummus';
$.physicals.girl.foodList[204] = 'ice cream';
$.physicals.girl.foodList[205] = 'irish stew';
$.physicals.girl.foodList[206] = 'indian food';
$.physicals.girl.foodList[207] = 'italian bread';
$.physicals.girl.foodList[208] = 'jambalaya';
$.physicals.girl.foodList[209] = 'jelly';
$.physicals.girl.foodList[210] = 'jam';
$.physicals.girl.foodList[211] = 'jerky';
$.physicals.girl.foodList[212] = 'jalapeño';
$.physicals.girl.foodList[213] = 'kale';
$.physicals.girl.foodList[214] = 'kabobs';
$.physicals.girl.foodList[215] = 'ketchup';
$.physicals.girl.foodList[216] = 'kiwi';
$.physicals.girl.foodList[217] = 'kidney beans';
$.physicals.girl.foodList[218] = 'kingfish';
$.physicals.girl.foodList[219] = 'lobster';
$.physicals.girl.foodList[220] = 'lamb';
$.physicals.girl.foodList[221] = 'linguine';
$.physicals.girl.foodList[222] = 'lasagna';
$.physicals.girl.foodList[223] = 'meatballs';
$.physicals.girl.foodList[224] = 'moose';
$.physicals.girl.foodList[225] = 'milk';
$.physicals.girl.foodList[226] = 'milkshake';
$.physicals.girl.foodList[227] = 'noodles';
$.physicals.girl.foodList[228] = 'ostrich';
$.physicals.girl.foodList[229] = 'pizza';
$.physicals.girl.foodList[230] = 'pepperoni';
$.physicals.girl.foodList[231] = 'porter';
$.physicals.girl.foodList[232] = 'pancakes';
$.physicals.girl.foodList[233] = 'quesadilla';
$.physicals.girl.foodList[234] = 'quiche';
$.physicals.girl.foodList[235] = 'reuben';
$.physicals.girl.foodList[236] = 'spinach';
$.physicals.girl.foodList[237] = 'spaghetti';
$.physicals.girl.foodList[238] = 'tater tots';
$.physicals.girl.foodList[239] = 'toast';
$.physicals.girl.foodList[240] = 'venison';
$.physicals.girl.foodList[241] = 'waffles';
$.physicals.girl.foodList[242] = 'wine';
$.physicals.girl.foodList[243] = 'walnuts';
$.physicals.girl.foodList[244] = 'yogurt';
$.physicals.girl.foodList[245] = 'ziti';
$.physicals.girl.foodList[246] = 'zucchini';
$.physicals.girl.foodList[247] = 'string bean';
$.physicals.girl.foodList[248] = 'birthday cake';
$.physicals.girl.foodList[249] = 'pear';
$.physicals.girl.foodList[250] = 'steak';
$.physicals.girl.foodList[251] = 'peanut';
$.physicals.girl.foodList[252] = 'hot dog';
$.physicals.girl.willAccept = function willAccept(what, src) {
/* Returns true iff this is willing to accept what arriving from src.
*
* This function (or its overrides) MUST NOT have any kind of
* observable side-effect (making noise, causing some other action,
* etc.).
*/
return what === this.food;
};
Object.setOwnerOf($.physicals.girl.willAccept, $.physicals.Maximilian);
Object.setOwnerOf($.physicals.girl.willAccept.prototype, $.physicals.Maximilian);
$.physicals.girl.movable = false;
$.physicals.girl.attempts = 0;
================================================
FILE: core/core_46_$.secuityCourse.js
================================================
/**
* @license
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Security course demo for Code City.
*/
//////////////////////////////////////////////////////////////////////
// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!
//////////////////////////////////////////////////////////////////////
$.securityCourse = {};
Object.setOwnerOf($.securityCourse, $.physicals.Neil);
$.securityCourse.StoreHost = function StoreHost() {
/* A $.servers.http.Host subclass for per-user stores for the security
* course.
*/
$.servers.http.Host.call(this);
var user = Object.getOwnerOf(this);
if (!($.user.isPrototypeOf(user))) {
throw new TypeError('new store must be owned by a $.user');
}
var hostname = user.name.toLowerCase();
if (hostname in $.securityCourse.storeHosts) {
throw new RangeError('a store named ' + hostname + ' already exists');
} else if (hostname in $.hosts.root.subdomains) {
throw new RangeError('the subdomain ' + hostname + ' is already in use');
}
(function inner() {
// Run set-up with non-privileged perms.
setPerms(user);
var store = Object.create($.securityCourse.storePagePrototype);
store.name = user.name + "'s Store";
this['/'] = store;
}).call(this);
$.securityCourse.storeHosts[hostname] = this;
$.hosts.root.addSubdomain(hostname, this);
};
Object.setOwnerOf($.securityCourse.StoreHost, $.physicals.Maximilian);
Object.setPrototypeOf($.securityCourse.StoreHost.prototype, $.servers.http.Host.prototype);
Object.setOwnerOf($.securityCourse.StoreHost.prototype, $.physicals.Maximilian);
$.securityCourse.StoreHost.prototype.destroy = function destroy() {
if (!(this instanceof $.securityCourse.StoreHost)) {
throw new TypeError('destroy must be called on a StoreHost');
}
var callerPerms = Thread.callers()[0].callerPerms;
if(Object.getOwnerOf(this) !== callerPerms) {
throw new PermissionError('can only be deleted by owner');
}
$.hosts.root.deleteSubdomain(this);
var stores = $.securityCourse.storeHosts;
for (var key in stores) {
if (stores[key] === this) {
delete stores[key];
}
}
};
Object.setOwnerOf($.securityCourse.StoreHost.prototype.destroy, $.physicals.Maximilian);
Object.setOwnerOf($.securityCourse.StoreHost.prototype.destroy.prototype, $.physicals.Maximilian);
$.securityCourse.storePagePrototype = {};
Object.setOwnerOf($.securityCourse.storePagePrototype, $.physicals.Neil);
$.securityCourse.storePagePrototype.www = function www(request, response) {
// This is a routing function. There's nothing interesting here. Honest.
var prop = {
'basket': 'wwwBasket',
'confirm': 'wwwConfirm',
}[request.parameters.page] || 'wwwHome';
$.jssp.eval(this, prop, request, response);
};
Object.setOwnerOf($.securityCourse.storePagePrototype.www, $.physicals.Maximilian);
Object.setOwnerOf($.securityCourse.storePagePrototype.www.prototype, $.physicals.Neil);
$.securityCourse.storePagePrototype.name = 'Security Store';
$.securityCourse.storePagePrototype.inventory = [];
Object.setOwnerOf($.securityCourse.storePagePrototype.inventory, $.physicals.Neil);
$.securityCourse.storePagePrototype.inventory[0] = {};
Object.setOwnerOf($.securityCourse.storePagePrototype.inventory[0], $.physicals.Neil);
$.securityCourse.storePagePrototype.inventory[0].name = 'Beach ball';
$.securityCourse.storePagePrototype.inventory[0].price = '2.75';
$.securityCourse.storePagePrototype.inventory[0].id = 'yYrq3jVfWK';
$.securityCourse.storePagePrototype.inventory[0].public = true;
$.securityCourse.storePagePrototype.inventory[0].img = 'beachball.png';
$.securityCourse.storePagePrototype.inventory[1] = {};
Object.setOwnerOf($.securityCourse.storePagePrototype.inventory[1], $.physicals.Neil);
$.securityCourse.storePagePrototype.inventory[1].name = 'Flip flops';
$.securityCourse.storePagePrototype.inventory[1].price = '8.50';
$.securityCourse.storePagePrototype.inventory[1].id = 'GSaYngk5Jn';
$.securityCourse.storePagePrototype.inventory[1].public = true;
$.securityCourse.storePagePrototype.inventory[1].img = 'flipflops.png';
$.securityCourse.storePagePrototype.inventory[2] = {};
Object.setOwnerOf($.securityCourse.storePagePrototype.inventory[2], $.physicals.Neil);
$.securityCourse.storePagePrototype.inventory[2].name = 'Nuclear waste';
$.securityCourse.storePagePrototype.inventory[2].price = '666';
$.securityCourse.storePagePrototype.inventory[2].id = 'iu9i5GvLeJ';
$.securityCourse.storePagePrototype.inventory[2].public = false;
$.securityCourse.storePagePrototype.inventory[2].img = 'radioactive.png';
$.securityCourse.storePagePrototype.inventory[3] = {};
Object.setOwnerOf($.securityCourse.storePagePrototype.inventory[3], $.physicals.Neil);
$.securityCourse.storePagePrototype.inventory[3].name = 'Guitar';
$.securityCourse.storePagePrototype.inventory[3].price = '24.30';
$.securityCourse.storePagePrototype.inventory[3].id = 'uazSOLHfkt';
$.securityCourse.storePagePrototype.inventory[3].public = true;
$.securityCourse.storePagePrototype.inventory[3].img = 'guitar.png';
$.securityCourse.storePagePrototype.wwwHome = '<% include(\'header\'); %>\n
\n<%\nvar staticUrl = request.hostUrl(\'static\');\nvar total = 0; \nvar order = {};\nfor (var param in request.parameters) {\n if (!param.startsWith(\'item\')) continue;\n var item = this.inventory[Number(param.substring(4))];\n var quant = Number(request.parameters[param]);\n if (!item || !quant) continue;\n var lineTotal = quant * item.price;\n total += lineTotal;\n order[item.id] = quant;\n%>\n
\n \n';
$.securityCourse.storeHosts = (new 'Object.create')(null);
$.hosts.root['/securitycourse'] = $.securityCourse;
================================================
FILE: core/core_99_startup.js
================================================
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Start core database. Note that, unlike most of the
* rest of the files in the core/ directory, this is hand-written.
*
* It assumed this file will be used when starting a full database
* dump; for that, a file in database/, generated by dump, will
* restart listeners intead.
*/
/* Optional (but recommended) configuration. The web server is
* capable of guessing its own hostname, but will be more efficient
* and secure if its configuration is specified explicitly.
*/
/* Set .hostname to the canonical hostname (including the port number,
* if non-default).
*/
// $.hosts.root.hostname = 'example.codecity.world';
// $.hosts.root.hostname = 'localhost:8080';
/* If your host has more than one name, set .hostRegExp to a regular
* expression that matches all valid name+port combinations for this
* host. It is recommended that it end with /$/, but do NOT start it
* with /^/ unless you want to break wildcard subdomains. Make sure
* it matches .hostname!
*/
// Accept either of two different hostnames.
// $.hosts.root.hostRegExp = /example.codecity.world$|codecity.example.com$/;
// Match any TLD and optional port.
// $.hosts.root.hostRegExp = /example.codecity.\w+(?::\d+)?$/;
/* Set .pathToSubdomain to true if you don't have a wildcard DNS entry
* and wildcard TLS certificate for your hostname (false if you do);
* this will enable accessing pages usually served on subdomains (such
* as the code editor) via the root hostname instead.
*
* Normally the nginx reverse proxy sends a CodeCity-pathToSubdomain
* header which will automatically enable or disable this feature, but
* you can override it here.
*/
// $.hosts.root.pathToSubdomain = false;
// Set up.
$.system.onStartup();
// Tidy up.
$.clock.movable = true;
$.clock.moveTo($.startRoom);
$.clock.movable = false;
$.tutorial.moveTo($.startRoom);
$.tutorial.reset();
$.pot.moveTo($.startRoom);
$.pot.stage = 0;
$.seed.moveTo($.startRoom);
$.thrower.moveTo($.startRoom);
$.cage.moveTo($.physicals['Genetics Lab']);
$.assistant.moveTo($.startRoom);
================================================
FILE: core/dump_spec.json
================================================
[
{
"options": {"skipBindings": ["lastModifiedTime", "lastModifiedUser"]}
}, {
"header": [
"/**",
" * @license",
" * Copyright Google LLC",
" *",
" * Licensed under the Apache License, Version 2.0 (the \"License\");",
" * you may not use this file except in compliance with the License.",
" * You may obtain a copy of the License at",
" *",
" * http://www.apache.org/licenses/LICENSE-2.0",
" *",
" * Unless required by applicable law or agreed to in writing, software",
" * distributed under the License is distributed on an \"AS IS\" BASIS,",
" * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.",
" * See the License for the specific language governing permissions and",
" * limitations under the License.",
" */",
"",
"/**",
" * @fileoverview ",
" */",
"",
"//////////////////////////////////////////////////////////////////////",
"// AUTO-GENERATED CODE FROM DUMP. EDIT WITH CAUTION!",
"//////////////////////////////////////////////////////////////////////",
"\n"
],
"filename": "../var/dump/core_00_es5.js",
"headerSubs": {
"": "2017",
"": [
"Load builtins and add polyfills to bring the server's partial",
" * JavaScript implementation up to ECMAScript 5.1 (or close to it)."
]
},
"contents": [
"Object",
"Function",
"Array",
"String",
"Boolean",
"Number",
"Date",
"RegExp",
"Error",
"EvalError",
"RangeError",
"ReferenceError",
"SyntaxError",
"TypeError",
"URIError",
"Math",
"JSON",
"decodeURI",
"decodeURIComponent",
"encodeURI",
"encodeURIComponent",
"escape",
"isFinite",
"isNaN",
"parseFloat",
"parseInt",
"unescape"
]
}, {
"filename": "../var/dump/core_00_es6.js",
"headerSubs": {
"": "2017",
"": [
"Load builtins and add polyfills to bring the server's partial",
" * JavaScript implementation to include some features of ECMAScript 6."
]
},
"contents": [
"Object.is",
"Object.assign",
"Object.setPrototypeOf",
"Array.from",
"Array.prototype.find",
"Array.prototype.findIndex",
"String.prototype.endsWith",
"String.prototype.includes",
"String.prototype.repeat",
"String.prototype.startsWith",
"Number.isFinite",
"Number.isNaN",
"Number.isSafeInteger",
"Number.EPSILON",
"Number.MAX_SAFE_INTEGER",
"Math.sign",
"Math.trunc",
"WeakMap"
]
}, {
"filename": "../var/dump/core_00_es7.js",
"headerSubs": {
"": "2017",
"": [
"Load builtins and add polyfills to bring the server's partial",
" * JavaScript implementation to include some features of ECMAScript 7."
]
},
"contents": [
"Array.prototype.includes"
]
}, {
"filename": "../var/dump/core_00_esx.js",
"headerSubs": {
"": "2017",
"": [
"Load builtins and add polyfills for Code City-specific extensions to",
" * JavaScript."
]
},
"contents": [
"Object.getOwnerOf",
"Object.setOwnerOf",
"Thread",
"PermissionError",
"Array.prototype.join",
"suspend",
"setTimeout",
"clearTimeout"
]
},
{
"filename": "core_10_base.js",
"headerSubs": {
"": "2017",
"": "Database core for Code City."
},
"prune": ["$.system.onStartup.thread_"],
"contents": [
"perms",
"setPerms",
{"path": "$", "do": "DONE"},
"$.root",
{"path": "$.physicals", "do": "DONE"},
{"path": "$.physicals.Maximilian", "do": "DONE"},
{"path": "$.physicals.Neil", "do": "DONE"},
"$.system",
"user",
{"path": "$.utils", "do": "DONE"},
{"path": "$.utils.validate", "do": "DONE"},
{"path": "$.servers", "do": "DONE"}
]
}, {
"filename": "core_11_$.utils.js",
"headerSubs": {
"": "2020",
"": "Basic utilities for Code City."
},
"contents": [
"$.utils",
"$.utils.array",
"$.utils.object",
"$.utils.string"
]
}, {
"filename": "core_12_$.utils.code.js",
"headerSubs": {
"": "2018",
"": "Code utilities for Code City."
},
"contents": ["$.utils.code"]
}, {
"filename": "core_13_$.Selector.js",
"headerSubs": {
"": "2018",
"": "Selector implementation for Code City core."
},
"pruneRest": ["$.Selector.cache_", "$.Selector.sortByBadness.cache_"],
"contents": [
"$.Selector",
"$.utils.Binding",
{"path": "$.Selector.cache_", "do": "DONE"},
{"path": "$.Selector.sortByBadness.cache_", "do": "DONE"}
]
},
{
"filename": "core_20_$.utils.html.js",
"headerSubs": {
"": "2018",
"": "HTML utilities for Code City."
},
"contents": ["$.utils.html"]
}, {
"filename": "core_21_$.jssp.js",
"headerSubs": {
"": "2017",
"": "JavaScript Server Pages for Code City."
},
"contents": ["$.jssp"]
}, {
"filename": "core_22_$.connection.js",
"headerSubs": {
"": "2017",
"": "Connection object for Code City."
},
"contents": ["$.connection"]
}, {
"filename": "core_23_$.servers.http.js",
"headerSubs": {
"": "2017",
"": "Webserver for Code City."
},
"contents": [
"$.utils.url",
"$.servers.http",
{"path": "$.servers.http.hosts", "do": "DONE"}
]
}, {
"filename": "core_24_$.hosts.js",
"headerSubs": {
"": "2017",
"": "Host objects for Code City."
},
"contents": [
{"path": "$.hosts", "do": "DONE"},
{"path": "$.hosts.root", "do": "DONE"},
{"path": "$.hosts.root.subdomains", "do": "DONE"},
"$.hosts.root['/']",
"$.hosts.root['/mirror']",
"$.hosts.root['/robots.txt']",
"$.hosts.system",
"$.hosts.dummy",
"$.hosts.root.subdomains.system",
"$.hosts.root.subdomains.connect",
"$.hosts.root.subdomains.login",
"$.hosts.root.subdomains.mobwrite",
"$.hosts.root.subdomains.static",
"$.hosts.root.subdomains.system",
{"path": "$.servers.http.hosts[0]", "do": "DONE"}
]
}, {
"filename": "core_25_$.db.tempId.js",
"headerSubs": {
"": "2018",
"": "Temporary ID database for Code City."
},
"contents": [
{"path": "$.db", "do": "DONE"},
"$.db.tempId",
{"path": "$.db.tempId.tempIds_", "do": "DONE"},
{"path": "$.db.tempId.cleanThread_", "do": "DECL"}
]
}, {
"filename": "core_25_$.userDatabase.js",
"headerSubs": {
"": "2017",
"": "User database for Code City."
},
"contents": [
"$.userDatabase",
{"path": "$.userDatabase.byMd5", "do": "DONE"}
]
}, {
"filename": "core_26_inline_editor.js",
"headerSubs": {
"": "2017",
"": "Inline code editor for Code City."
},
"contents": [
{"path": "$.hosts.code", "do": "DONE"},
"$.hosts.code['/inlineEdit']",
{"path": "$.hosts.root.subdomains.code", "do": "DONE"}
]
}, {
"filename": "core_27_editor.js",
"headerSubs": {
"": "2018",
"": "Web-based code explorer/editor for Code City."
},
"contents": [
"$.hosts.code"
]
}, {
"filename": "core_28_$.servers.eval.js",
"headerSubs": {
"": "2020",
"": "Eval server for Code City."
},
"contents": [
"$.servers.eval"
]
},
{
"filename": "core_30_$.utils.command.js",
"headerSubs": {
"": "2017",
"": "Command parser for Code City"
},
"contents": ["$.utils.command"]
}, {
"filename": "core_31_$.utils_world.js",
"headerSubs": {
"": "2017",
"": "World-related utils for Code City."
},
"contents": [
"$.utils.commandMenu",
"$.utils.replacePhysicalsWithName"
]
}, {
"filename": "core_32_physical.js",
"headerSubs": {
"": "2017",
"": "Physical object prototype for Code City."
},
"contents": [
"$.physical",
{"path": "$.physicals", "do": "DONE"},
{"path": "$.physicals['Physical object prototype']", "do": "DONE"},
"$.utils.validate.physicals",
"$.garbage"
]
}, {
"filename": "core_33_world.js",
"headerSubs": {
"": "2017",
"": "Generic physical object types for Code City."
},
"contents": [
"$.user",
"$.room",
"$.thing",
"$.container",
{"path": "$.physicals['User prototype']", "do": "DONE"},
{"path": "$.physicals['Room prototype']", "do": "DONE"},
{"path": "$.physicals['Thing prototype']", "do": "DONE"},
{"path": "$.physicals['Container prototype']", "do": "DONE"}
]
}, {
"filename": "core_34_$.servers.login.js",
"headerSubs": {
"": "2021",
"": "Login service backend server for Code City."
},
"contents": [
"$.servers.login"
]
}, {
"filename": "core_34_$.servers.telnet.js",
"headerSubs": {
"": "2017",
"": "Telnet server for Code City."
},
"pruneRest": ["$.servers.telnet.connected"],
"contents": [
"$.servers.telnet",
{"path": "$.servers.telnet.connected", "do": "DONE"}
]
},
{
"headerSubs": {
"": "2017",
"": "Initial starting room for Code City."
},
"filename": "core_40_$.startRoom.js",
"prune": ["$.clock.thread_"],
"contents": [
{"path": "$.startRoom", "do": "DONE"},
"$.startRoom{proto}",
"$.startRoom{owner}",
"$.startRoom.location",
{"path": "$.startRoom.contents_", "do": "DONE"},
{"path": "$.startRoom.contents_.forObj", "do": "DONE"},
{"path": "$.startRoom.contents_.forKey", "do": "DONE"},
"$.startRoom.name",
"$.startRoom.description",
"$.startRoom.roll",
"$.clock",
{"path": "$.clock.thread_", "do": "DONE"}
]
}, {
"headerSubs": {
"": "2018",
"": "Translation room and tutorial demo for Code City."
},
"filename": "core_41_deutsche_zimmer.js",
"contents": [
"$.physicals['Das deutsche Zimmer']",
"$.tutorial",
{"path": "$.tutorial.location", "do": "DECL"},
{"path": "$.tutorial.user", "do": "DECL"},
{"path": "$.tutorial.thread", "do": "DECL"},
{"path": "$.tutorial.step", "do": "DECL"},
{"path": "$.tutorial.room", "do": "DECL"},
{"path": "$.tutorial.origFunc", "do": "DECL"},
{"path": "$.physicals.tutorial", "do": "DONE"}
]
}, {
"filename": "core_42_plant.js",
"headerSubs": {
"": "2018",
"": "Plant demo for Code City."
},
"contents": [
"$.seed",
{"path": "$.seed.location", "do": "DECL"},
{"path": "$.physicals['Generic Seed']", "do": "DONE"},
"$.pot",
{"path": "$.pot.location", "do": "DECL"},
{"path": "$.pot.seed", "do": "DECL"},
{"path": "$.pot.stage", "do": "DECL"},
"$.pot.stages",
{"path": "$.physicals['flower pot']", "do": "DONE"},
"$.thrower",
{"path": "$.thrower.location", "do": "DECL"},
{"path": "$.thrower.savedSvg", "do": "DECL"},
{"path": "$.physicals['a flame thrower']", "do": "DONE"}
]
}, {
"filename": "core_43_genetics_lab.js",
"headerSubs": {
"": "2020",
"": "Genetics lab demo for Code City."
},
"contents": [
"$.physicals['Genetics Lab']",
{"path": "$.physicals['Genetics Lab'].contents_", "do": "DONE"},
{"path": "$.physicals['Genetics Lab'].contents_.forObj", "do": "DONE"},
{"path": "$.physicals['Genetics Lab'].contents_.forKey", "do": "DONE"},
"$.cage",
{"path": "$.cage.location", "do": "DECL"},
{"path": "$.physicals.cage", "do": "DONE"},
"$.physicals['Genetic Mouse Prototype']",
"$.hosts.genetics",
"$.hosts.root.subdomains.genetics"
]
}, {
"filename": "core_44_$.assistant.js",
"headerSubs": {
"": "2020",
"": "Voice-activated assistant demo for Code City."
},
"contents": [
"$.assistant",
{"path": "$.assistant.location", "do": "DECL"},
{"path": "$.assistant.lastActivated", "do": "DECL"},
{"path": "$.physicals.assistant", "do": "DONE"}
]
}, {
"filename": "core_45_Challenge_Room.js",
"headerSubs": {
"": "2020",
"": "Challenge room demo for Code City."
},
"contents": [
"$.physicals['Challenge room']",
"$.physicals['light switch']",
"$.physicals.chest",
"$.physicals.safe",
"$.physicals.food",
"$.physicals.girl"
]
}, {
"filename": "core_46_$.secuityCourse.js",
"headerSubs": {
"": "2021",
"": "Security course demo for Code City."
},
"contents": [
"$.securityCourse",
{"path": "$.securityCourse.storeHosts", "do": "DONE"},
{"path": "$.hosts.root['/securitycourse']", "do": "DONE"}
]
},
{
"options": {"skipBindings": []}
}, {
"filename": "../database/db_00_core_lastModified.js",
"headerSubs": {
"": "2020",
"": "Edit history info for Code City core."
},
"contents": [
"Object",
"Function",
"Array",
"String",
"Boolean",
"Number",
"Date",
"RegExp",
"Error",
"EvalError",
"RangeError",
"ReferenceError",
"SyntaxError",
"TypeError",
"URIError",
"Math",
"JSON",
"decodeURI",
"decodeURIComponent",
"encodeURI",
"encodeURIComponent",
"escape",
"isFinite",
"isNaN",
"parseFloat",
"parseInt",
"unescape",
"Object.is",
"Object.assign",
"Object.setPrototypeOf",
"Array.from",
"Array.prototype.find",
"Array.prototype.findIndex",
"String.prototype.endsWith",
"String.prototype.includes",
"String.prototype.repeat",
"String.prototype.startsWith",
"Number.isFinite",
"Number.isNaN",
"Number.isSafeInteger",
"Number.EPSILON",
"Number.MAX_SAFE_INTEGER",
"Math.sign",
"Math.trunc",
"WeakMap",
"Array.prototype.includes",
"Object.getOwnerOf",
"Object.setOwnerOf",
"Thread",
"PermissionError",
"Array.prototype.join",
"suspend",
"setTimeout",
"clearTimeout",
"perms",
"setPerms",
"$.root",
"$.system",
"user",
"$.utils",
"$.utils.array",
"$.utils.object",
"$.utils.string",
"$.utils.code",
"$.Selector",
"$.utils.Binding",
"$.utils.html",
"$.jssp",
"$.connection",
"$.utils.url",
"$.servers.http",
"$.hosts.root['/']",
"$.hosts.root['/mirror']",
"$.hosts.root['/robots.txt']",
"$.hosts.system",
"$.hosts.dummy",
"$.hosts.root.subdomains.connect",
"$.hosts.root.subdomains.login",
"$.hosts.root.subdomains.mobwrite",
"$.hosts.root.subdomains.static",
"$.hosts.root.subdomains.system",
"$.db.tempId",
"$.userDatabase",
"$.hosts.code['/inlineEdit']",
"$.hosts.code",
"$.servers.eval",
"$.utils.command",
"$.utils.commandMenu",
"$.utils.replacePhysicalsWithName",
"$.physical",
"$.utils.validate.physicals",
"$.garbage",
"$.user",
"$.room",
"$.thing",
"$.container",
"$.servers.login",
"$.servers.telnet",
"$.startRoom{proto}",
"$.startRoom{owner}",
"$.startRoom.location",
"$.startRoom.name",
"$.startRoom.description",
"$.startRoom.roll",
"$.clock",
"$.physicals['Das deutsche Zimmer']",
"$.tutorial",
"$.seed",
"$.pot",
"$.pot.stages",
"$.thrower",
"$.physicals['Genetics Lab']",
"$.cage",
"$.physicals['Genetic Mouse Prototype']",
"$.hosts.genetics",
"$.hosts.root.subdomains.genetics",
"$.assistant",
"$.physicals['Challenge room']",
"$.physicals['light switch']",
"$.physicals.chest",
"$.physicals.safe",
"$.physicals.food",
"$.physicals.girl",
"$.securityCourse",
"$.hosts.root['/securitycourse']"
]
}, {
"filename": "../database/db_01_world.js",
"headerSubs": {
"": "2017",
"": "Main database for google.codecity.world."
},
"contents": [
"$.servers.http.Host.prototype.access",
"$.hosts.root.access",
"$.hosts.system.access",
"$.db.tempId.tempIds_",
"$"
]
}, {
"options": {"treeOnly": false}
}, {
"filename": "../database/db_99_leftovers.js",
"header": "",
"rest": true
}
]
================================================
FILE: database/README
================================================
On startup, if no .city database file exists, the server will read and
execute all .js files in this directory in asciibetical order. The
following naming convention has been established to keep things
organised:
core*.js - The Code City core.
db*.js - A dumped databse, if available, less core.
test*.js - Any tests to be run against the databse.
================================================
FILE: database/codecity.cfg
================================================
{
"databaseDirectory": "./",
"checkpointInterval": 60,
"checkpointAtShutdown": true,
"checkpointMinFiles": 10,
"checkpointMaxDirectorySize": 2048
}
================================================
FILE: docs/setup.md
================================================
# Setting up a Code City Instance
This document describes how to recreate a Code City server from bare
metal. For reference, the starting point is a Google Cloud Platform
account in good standing.
## Google Compute Engine Setup
We recommend running your Code City server on a [Google Compute
Engine](https://cloud.google.com/compute) (GCE) virtual
machine[[?]](
https://en.wikipedia.org/wiki/Virtual_machine) (VM)—it’s
reliable, minimal hassle, and, thanks to [Google Cloud Platform’s
“Always Free” tier](https://cloud.google.com/free), can be (very
nearly) free!
### Create a GCE instance
This will create a dedicated GCE instance (VM) on which to run Code
City. You can skip this step if you intend to run your instance on
your own machine or another cloud provider’s hardware.
Before you begin: the GCE instance (VM) you create will run using the
permissions of a service account[[?]](
https://cloud.google.com/iam/docs/service-accounts). There is a
“Compute Engine default service account” which will work fine but has
quite broad permissions. You may wish to create a service account
with more limited permissions, to reduce the amount of damage an
attacker can do if they compromise your Code City instance and gain
control of the VM on which it runs. This is especially so if your GCP
project contains other resources (user data, etc.) which you wish to
protect. See [Appendix A: Creating a Service Account](
#appendix-a-creating-a-service-account) for instructions.
1. Go to the [Google Compute Engine
console](https://console.cloud.google.com/compute/instances).
0. Under VM Instances, click the create instance button . Enter details as follows:
* Name: choose a name for the instance. (This can be any name,
but we recommend you use a name matching your intended domain
name—e.g., `google.codecity.world` runs on an instance named
`google`.)
* Region: choose a region near where you expect your users to
be. Note that [instance pricing varies by
zone](https://cloud.google.com/compute/vm-instance-pricing).
* Zone: choose any.
* Machine type: choose an appropriate size.
* [GCP’s “Always Free” tier][always-free] offers one free
`f1-micro` instance in any of `us-west1`, `us-central1` or
`us-east1`. This size will be sufficient for many smaller
organisations/groups.
* Because of the architecture of the Code City server, there
is unlikely to be any benefit to having more than two
vCPUs (and one is generally sufficient).
* Container: no.
* Boot disk: under “Public images”, choose:
* Operating system: Debian
* Version: choose the most recent version—“Debian GNU/Linux
10 (buster)” as of this writing.
* Boot disk type: Standard persistent disk.
* Size: the default 10GB is likely to be sufficient for most
cases, but the “Always Free” program offers up to 30GB
(total, not per-instance) of persistent disk free of
charge.
* Identity and API access:
* Service account: use the default “Compute Engine default
service account” or select the one you created by
following the instructions in [appendix A](
#appendix-a-creating-a-service-account).
* Access scopes: Allow default access.
* Firewall: Allow both HTTP and HTTPS traffic.
* Management:
* Recommended: tick “Enable deletion protection” to make it
harder to inadvertently delete your Code City instance.
* Security:
* Recommended: tick “Turn on Secure Boot”.
* SSH Keys: you can add your ssh public key(s) here if you
wish; doing so will cause them to be automatically added
to the corresponding `~userid/.ssh/authorized_keys` file,
but note:
* Keys added here apply only to this GCE instance. If
you expect your project to have multiple instances you
may prefer to add your SSH keys on [the Compute Engine
metadata page](
https://pantheon.corp.google.com/compute/metadata/sshKeys)
instead: keys added there will have access to all the
project's GCE instances by default (i.e., unless you
tick "Block project-wide SSH keys" on a particular
instance).
* Even if you don’t add any SSH keys now you will in any
case be able to SSH to the machine from [GCE instances
page](
https://console.cloud.google.com/compute/instances).
* Disks:
* Recommended: **un**tick “Delete boot disk when instance
deleted” so that if you _do_ delete your instance you can
recreate it easily and without losing user data.
* Networking:
* Under Network interfaces, click the pencil icon next to
the default interface.
* External IP: ignore this section for now.
* Public DNS PTR Record: optionally click “Enable” and enter
the [domain name](#set-up-an-ip-address-and-domain-name)
you intend to use for your instance,
e.g. example.codecity.world.
* Click “Done” to end editing the network interface.
0. Double-check the monthly cost estimate (at the top of the page) to
ensure it is reasonable.
0. Click “create” and you will be taken back to the VM instances
dashboard. After a few minutes, you should see your new instance
is ready, and has internal and external IP addresses.
0. Under “Connect”, click on “SSH” for your instance.
0. Verify instance is running Debian 10:
```
$ uname -a
Linux instancename 4.19.0-9-cloud-amd64 #1 SMP Debian 4.19.118-2+deb10u1 (2020-06-07) x86_64 GNU/Linux
```
[always-free]: https://cloud.google.com/free/docs/gcp-free-tier#always-free-usage-limits
[service-account]: https://cloud.google.com/iam/docs/creating-managing-service-accounts
#### Reserve a Static IP Address
For users to be able to access your instance from the Internet, it
will need a static IP address[[?]](
https://en.wikipedia.org/wiki/IP_address) (like 192.0.2.1) so that
traffic can be routed to it.
1. Go to the [Networking / External IP addresses
console](https://console.cloud.google.com/networking/addresses).
0. Click “+ Reserve Static Address”. Enter details as follows:
* Name: can be any value, but we recommend you use the same name
as for your instance. This is just used to identify the
address reservation.
* Description: enter any text you like—e.g.: “Static IP
address for example.codecity.world.”
* Network Service Tier: choose either. See [description of
options](https://cloud.google.com/network-tiers/) and [pricing
information](https://cloud.google.com/network-tiers/pricing).
* IP vesion: IPv4.
* Type: Regional.
* Region: choose the same region as your instance was
created in.
* Attached to: choose your instance from the drop-down.
0. Click “Reserve”.
0. Make a note of the external address (like 192.0.2.1) which you
have just reserved for your instance.
### Give Your Instance a Domain Name
The Domain Name System[[?]](
https://en.wikipedia.org/wiki/Domain_Name_System) is a
distributed global database that maps domain names (like
`example.org`) to IP addresses (like 192.0.2.1).
In order for users to be able to access your instance without having
to know the numeric static IP address you reserved in the previous
section, you must create a human-readable domain mame[[?]](
https://en.wikipedia.org/wiki/Domain_name) for it.
The details of this process are outside of the scope of this document,
but we have the following observations and recommendations:
* Setting up DNS involves two distinct entities: a domain name
registrar[[?]](
https://en.wikipedia.org/wiki/Domain_name_registrar), from
whom you can purchase a domain name (like `example.org`), and a
DNS provider, who runs the name servers[[?]](
https://en.wikipedia.org/wiki/Name_server) that resolve
individual DNS entries (like `www.example.com`) to specific
numeric IP addresses like the one created in the previous section.
In many cases both these services will be provided by the same
company, but many organisations will typically run their own DNS
servers, or outsource it to a [managed DNS provider](
https://en.wikipedia.org/wiki/List_of_managed_DNS_providers).
* If you are using your own domain name (e.g.,
codecity.example.org) this will be done
through your DNS provider’s configuration console or via your
internal organisational DNS service configuration.
* Alternatively we may in some cases be able to offer you the
use of a Code City subdomain (e.g.,
example.codecity.world), in which case
we will take care of this step for you. Contact us for
details.
* Because of the [same origin policy], if you’d like to allow
individual (not fully trusted) users of your instance to be able
to create their own web pages / servers, we *strongly* recommend
that you use a wildcard DNS record[[?]](
https://en.wikipedia.org/wiki/Wildcard_DNS_record), so that
each user can serve their content on an isolated subdomain (like
username.example.codecity.world).
* You will need to create (or arrange to have created) two
separate DNS "A" records for your domain name:
* The main entry, e.g. `example.codecity.world`, type `A`,
resolving to your instance's IP address, and
* The wildcard entry, e.g. `*.example.codecity.world`, also
type `A`, resolving to the same IP address.
* To facilitate obtaining the necessary wildcard
certificate[[?]](
https://en.wikipedia.org/wiki/Wildcard_certificate), we
recommend you use a [DNS provider who easily integrates with
Let’s Encrypt DNS validation][dns-providers], such as [Google
Cloud DNS](https://cloud.google.com/dns/), if possible.
[same origin policy]: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
[dns-providers]: https://community.letsencrypt.org/t/dns-providers-who-easily-integrate-with-lets-encrypt-dns-validation/86438
### Recommended: schedule regular, automatic backups of your instance’s disk
It’s always best to back up! Even though the Code City server
regularly checkpoints its database to the instance’s persistent disk,
setting up regular snapshotting of that disk will give you a separate
backup of the whole system in case of disaster.
Snapshots are [not free], but [are cheap]. A few dollars a month buys a
lot of peace of mind!
[not free]: https://cloud.google.com/compute/disks-image-pricing#persistentdisk
[are cheap]: https://cloud.google.com/compute/disks-image-pricing#persistent_disk_snapshots_storage_charges
First, create a snapshot schedule:
1. Go to the [GCE Snapshots page](
https://console.cloud.google.com/compute/snapshots) and click on
the “Snapshot Schedules” tab.
0. Click on Create Snapshot Schedule. Enter details as follows:
* Name: choose a name for the schedule, e.g. `daily-14`.
* Description: anything, e.g. “Daily snapshots, kept for 14
days”.
* Snapshot location: Regional.
* Region: choose the same region as your instance.
* Schedule frequency: hourly, daily or weekly as you prefer; for
this example: daily. (N.B.: more frequent snapshots will
result in more data to be stored and thus higher costs.)
* Start time: any. Snapshotting will not affect the running
instance, so choose whatever time of day you wish.
* Auto-delete snapshots after: choose a suitable period of time;
for this example: 14 days.
* Deletion rule: as you wish: “keep snapshots” better protects
against data loss in the event that your instance is
*inadvertently* deleted; “delete snapshots after _N_ days”
better protects against continuing to be charged fees after
you *deliberately* delete your instance.
* Enable VSS: no. (Not applicable to non-Windows instances.)
* Snapshot labels: not needed.
0. Click “Create” to create the schedule.
Now apply the schedule to your instance’s persistent disk:
4. Go to the [GCE Disks page](
https://console.cloud.google.com/compute/disks).
0. Click on the name of the persistent disk for your instance. (By
default it will have the same name you gave to your instance.)
0. Click on “Edit” at the top of the screen.
0. Under Snapshot schedule, select the schedule you created in steps
1–3.
0. Click Save.
## Set Up Machine and Install Code City
These instructions assume you are using a GCE instance running Debian
GNU/Linux 10 "buster", but feel free to adapt to your particular
set-up.
1. Log into your instance. (See instructions in first section if
using GCE.)
0. If your machine has less than 2GB of memory (check with `free -h`;
the very first number shown is total RAM), you will need to create
a swap file (this is mandatory on `f1-micro` instances):
```
sudo -i
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
swapon -s
sh -c 'echo "/swapfile none swap sw 0 0" >> /etc/fstab'
exit
```
0. Check for and install system updates, then install
[nginx](https://en.wikipedia.org/wiki/Nginx) and
[git](https://git-scm.com/):
```
sudo apt-get update
sudo apt-get upgrade –y
sudo apt-get install -y nginx git
```
0. Optionally install a text editor of your choice. Debian comes
with [`vim`](https://www.vim.org/) and
[`nano`](https://www.nano-editor.org/) preinstalled; Emacs users
might feel more at home with the lightweight editors
[`mg`](https://github.com/hboetes/mg),
[`jove`](https://github.com/jonmacs/jove) or
[`zile`](https://www.gnu.org/software/zile/), or opt for
`emacs-nox` which is GNU Emacs without X Windows bindings.
```
sudo apt-get install mg
```
0. Install [node.js](https://nodejs.org/). Code City depends on
version 12, which is more recent than the version included in
Debian 10, so we will obtain it via [NodeSource](
https://github.com/nodesource/distributions):
```
sudo -i
curl -sL https://deb.nodesource.com/setup_12.x | bash -
apt-get install -y nodejs
exit
```
0. Verify the correct version of node is installed:
```
$ node –-version
v12.18.4
```
(Actual version may be later than 12.18.)
### Get TLS Certificates
In order to allow incoming HTTPS connections, you will need an
TLS[[?]](https://en.wikipedia.org/wiki/Transport_Layer_Security)
server certificate[[?]](
https://en.wikipedia.org/wiki/Public_key_certificate#TLS/SSL_server_certificate
). There are two types:
* An ordinary certificate covers one or more specific domain names,
like `www.example.org`.
* A wildcard certificate includes one or more wildcard domains, like
`*.example.org`.
You will need to get a TLS certificate covering the [set of DNS entries
you [created earlier](#set-up-an-ip-address-and-domain-name). If (as
recommended) you created a wildcard DNS entry, you will also need a
corresponding wildcard TLS certificate.
There are various ways to get a TLS certificate, but a free and easy
way is to use [Certbot](https://certbot.eff.org/) to get one from
[Let’s Encrypt](https://letsencrypt.org/). That’s what we’ll do here.
#### Getting a wildcard certificate
To use Certbot to get a wildcard certificate, you will need to use the
[`dns-01` challenge](
https://letsencrypt.org/docs/challenge-types/#dns-01-challenge), which
requires being able to create DNS TXT records[[?]](
https://en.wikipedia.org/wiki/TXT_record) for your domain name.
Here’s an example of how to do this if using Google Cloud DNS; see
[full instructions on the certbot website](
https://certbot.eff.org/lets-encrypt/debianbuster-nginx) if you use
another provider.
1. Install certbot and the required plug-ins:
```
sudo apt-get install -y certbot python3-certbot-dns-google
```
0. Obtain credentials from your DNS provider, to allow Certbot to
create TXT records, proving to Let’s Encrypt that you control your
domain. It [should be possible to skip this step](
https://certbot-dns-google.readthedocs.io/en/stable/#credentials)
when using Google Cloud DNS and running Certbot on GCE instance,
but alas [due to a bug](
https://github.com/certbot/certbot/issues/7933) this doesn’t yet
work in Debian 10.
1. Go to the [Service Accounts](
https://console.cloud.google.com/iam-admin/serviceaccounts)
tab of the IAM & Admin section of the GCP console.
0. Find the service account under which your GCE instance runs;
unless you elected otherwise above, this will be the one named
“Compute Engine default service account”. Click on the email
address for the key to open the details pane.
0. Click on the keys tab.
0. From the Add Key pop-up menu, select “Create new key”.
* Choose Key Type: JSON.
* Click “Create”.
0. Your browser will download a file with a name of the form
scp projectID–XXXXXXXXXXXX.json. Now,
transfer this file to your GCE instance using
[`scp`](https://en.wikipedia.org/wiki/Secure_copy) **on your
local machine**, e.g. scp
projectID–XXXXXXXXXXXX.json
example.codecity.world:service-account.json,
or by pasting it into a terminal window, as follows:
* Open the `.json` file you downloaded in step 2.iv. in a
text editor, select the whole contents and copy it to the
clipboard.
* **On your instance**, enter the command `cat - >
service-account.json `
* Paste the contents of the `.json` into the SSH window.
* Type `^D` to indicate end of file.
0. This credentials file will be needed when initially obtaining
the TLS certificate as well as every few months when [Certbot
will automatically renew it](
https://certbot.eff.org/docs/using.html#automated-renewals),
so move it to a safe place and protect it from tampering:
```
sudo mv service-account.json /etc/service-account.json
sudo chown root:root /etc/service-account.json
sudo chmod 600 /etc/service-account.json
```
0. Request a certificate for both the base domain name for your
instance and the corresponding wildcard entry:
```
sudo certbot certonly --dns-google \
--dns-google-credentials /etc/service-account.json \
--post-hook 'systemctl reload nginx' \
-d 'example.codecity.world,*.example.codecity.world'
```
If you have more than one DNS entry pointing at your instance,
just add further comma-separated entries to the list after the
`-d` directive. (N.B.: No spaces between entries in this list!)
* Enter your email address when prompted.
* Agree the terms of service.
* Optionally agree to share your email address with the EFF.
#### Getting a non-wildcard certificate
This process is a little simpler and does not require the ability to
modify DNS TXT records.
1. Install certbot (only):
```
sudo apt-get install -y certbot
```
0. Request a certificate for the base domain name for your instance
(only):
```
sudo certbot certonly --webroot --webroot-path /var/www/html \
--post-hook 'systemctl reload nginx' \
-d example.codecity.world
```
* Enter your email address at the prompt
* Agree the terms of service.
* Optionally agree to share your email address with the EFF.
### Install Code City
1. Create an account for Code City to run under. This is to isolate
it from any other users/services on the machine, and contain the
damage in the event that the server sandbox be compromised. We’ll
call the account `codecity` here, but any username is fine:
```
sudo useradd -rms /bin/bash codecity
```
0. Become the code city account:
```
sudo -iu codecity
```
0. Clone the [Code City
repo](https://github.com/google/CodeCity). (If you are a
project collaborator, see below for [instructions on how to use
SSH instad of HTTPS](#git-code-city-by-ssh-instead-of-https).)
```
git clone https://github.com/google/CodeCity.git
```
0. Install required NPMs:
```
(cd CodeCity/server && npm ci --only=prod)
(cd CodeCity/login && npm ci --only=prod)
```
0. Exit from the `codecity` account. We’re done with it for now, and
we need to be able to sudo, which that account (deliberately) does
not have permission to do.
```
exit
```
### Configure NGINX
On Debian, per-host `nginx` configuration files are stored in
`/etc/nginx/sites-available` and enabled by symlinking them into
`/etc/nginx-sites-enabled`. There is a `default` config supplied by
the `nginx` package, which should be disabled (unless you have already
modified it to serve other virutal hosts).
1. Install the NGINX configuration file. If you have a wildcard DNS
record and corresponding wildcard TLS certificate, use the
“subdomain” configuration:
```
sudo cp ~codecity/CodeCity/etc/cc-subdomain.conf \
/etc/nginx/sites-available/codecity
```
Otherwise, use the “onedomain” configuration:
```
sudo cp ~codecity/CodeCity/etc/cc-onedomain.conf \
/etc/nginx/sites-available/codecity
```
0. Edit /etc/nginx/sites-enabled to replace INSTANCENAME with the
name(s) of your instance. (See comments for details. You may use
another editor instead of nano if you wish!)
```
sudo nano /etc/nginx/sites-available/codecity
```
0. Enable the new configuration and reload (or restart) NGINX:
```
sudo rm /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/codecity \
/etc/nginx/sites-enabled/codecity
sudo systemctl reload-or-restart nginx
```
### Create an API Key for OAuth
The usual set-up for public Code City instances is to use [OAuth
2.0](https://oauth.net/2/) via Google’s OAuth service for logins.
This step will set up the necessary credentials to allow users to log
in to your instance using their Google (Gmail) account. You can skip
this step if you intend to use a different login mechanism.
This must be done after installing nginx and CodeCity and obtaining a
TLS certificate because it depends on `logo-auth.png` being served by
nginx.
1. Optionally replace `~codecity/CodeCity/static/logo-auth.png` with
a logo representing your instance or organisation. It should be a
120x120px PNG image.
0. Make sure you can access your desired logo using your web browser.
If you are using a wildcard DNS configuration, it should be
accessible via a URL like
static.example.codecity.world/logo-auth.png;
for a single-domain configuration it will instead be
example.codecity.world/static/logo-auth.png.
Make a note of this URL.
0. Go to [APIs & Services > OAuth consent screen](
https://console.cloud.google.com/apis/credentials/consent). Enter
details as follows:
* Email address: select a suitable contact email address or
Google Group for users of your service.
* Product name: this should include the name of your
organisation; it may optionally contain the name “Code City”;
it should not include “Google” or the like. N.B.: the same
details are used for all services offered via a given Google
Cloud Platform account.
* Homepage URL: this could be your organisation’s homepage or
the URL of the front page for your instance (perhaps
example.codecity.world or
codecity.yourdomain.tld)
* Product logo URL: Should point at the URL for your your logo,
as determined in step 2 above.
* Privacy policy URL: Provide a link to your privacy policy.
* Terms of service URL: may be left blank.
0. Go to the [APIs & Services > Credentials
console](https://console.cloud.google.com/apis/credentials).
0. Click “Create credentials”; choose OAuth client ID. Enter details
as follows:
* Application type: Web application
* Name: a suitable full name for your instance, (e.g. “Code City
for Springfield Highschool”).
* Authorized JavaScript origins: may be left blank.
* Authorized redirect URIs: for wildcard DNS configurations this
will be of the form `https://login.example.codecity.world/`;
for single-domain configurations it will instead be
`https://example.codecity.world/login/`.
0. Click Save.
0. Now click on the newly-created client ID. Make a note of the
Client ID (it will be a long string like
“00000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com”)
and the Client Secret (a shorter but similarly opaque jumble of
characters). You will need these later.
See also the [complete GCP OAuth 2.0 documentation](
https://support.google.com/cloud/answer/6158849) for more information.
### Configure Code City
1. Become the code city account:
```
sudo -iu codecity
```
0. Create a config file for loginServer:
* Run loginServer once to create an empty config file:
```
(cd ~/CodeCity/login && ./loginServer)
```
Open `~/CodeCity/login/loginServer.cfg` in the text editor of
your choice.
* Set `connectUrl` to the URL for the connect server. If using
a wildcard DNS entry for your instance, it will be the
`connect.` subdomain of your instance’s name; otherwise it
will be the `/connect` path on your instance. For example:
* With wildcard DNS: `https://connect.example.codecity.world/`
* Without wildcard DNS:`https://example.codecity.world/connect/`
* Set `staticUrl` to the URL nginx will serve static content on.
This works similarly to the previous entry, e.g.:
* With wildcard DNS: `https://static.example.codecity.world/`
* Without wildcard DNS:`https://example.codecity.world/static/`
* Set `clientID` and `clientSecret` to the values obtained
earlier from [Google’s API Console](
https://console.developers.google.com/apis).
* Set `cookieDomain` to your instance’s base domain name, e.g.:
`example.codecity.world`.
* Set `password` to a secret, random string. If you don’t have
a convenient way to generate one locally, you can copy a
[random string from random.org].
* Optionally, set `emailRegexp` to a [JavaScript regexp]
matching email addresses which should be permitted to log in
to your instance—e.g., `^.*@myorganisation\\.org$`.
0. Create and edit a config file for connectServer:
* Run connectServer once to create an empty config file:
```
(cd ~/CodeCity/connect && ./connectServer)
```
Open `~/CodeCity/connect/connectServer.cfg` in the text editor
of your choice.
* Set `loginUrl` to the URL for the login server, e.g.:
* With wildcard DNS: `https://login.example.codecity.world/`
* Without wildcard DNS:`https://example.codecity.world/login/`
* Set `staticUrl` and `password` to the _same_
values used in `loginServer.cfg`.
0. Modify the configuration for the in-core HTTP server:
* Open the file `~/CodeCity/core/core_99_startup.js` in the text
editor of your choice. Find the optional configuration
section near the top of the file.
* Set `$.hosts.root.hostname` to your instance’s domain
name—e.g., $.hosts.root.hostname =
'example.codecity.world';
* Set `$.hosts.root.pathToSubdomain = false;` if you are using a
wildcard DNS entry for your instance; otherwise set it to
`true`.
* If you have more than one DNS entry for your instance, set
`$.hosts.root.hostRegExp` according to the instructions
provided.
0. Save the file, exit your editor and exit from the `codecity`
account.
```
exit
```
[JavaScript regexp]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
[random string from random.org]: https://www.random.org/strings/?num=1&len=20&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new
### Configure Systemd and Start Code City Servers
1. Install the systemd config files:
```
sudo cp ~codecity/CodeCity/etc/*.service /etc/systemd/system
```
0. Check the contents of `/etc/systemd/system/codecity.service`,
…`codecity-login.service` and …`codecity-connect.service`; verify
paths, usernames etc. all match the values created in previous
steps.
0. Enable Code City services with systemd:
```
sudo systemctl enable --now codecity
```
0. Verify you can connect to your new Code City instance by pointing
your web browser its domain name—e.g., `https://example.codecity.world`.
Congratulations, you're done!
## Appendix A: Creating a Service Account
To protect any other Google Cloud Platform services you run against
the consequences of your Code City instance being compromised, you can
have the GCE VM it runs on use only the limited permissions of a
custom service account, rather than the extensive permissions held by
the Compute Engine default service account. To do this there are three
steps:
1. Creating one or more roles for the service account.
0. Creating a service account
0. Applying the service account to the GCE instance.
### Create Role(s)
You should create one role for running CodeCity instances on GCE, and
optionally create a second role if you intend to have your GCE
instance [obtain wildcard TLS certificates using certbot via the ACME
HTTP-01 challenge](#getting-a-wildcard-certificate).
1. Go to the [Roles tab of IAM & Admin](
https://console.cloud.google.com/iam-admin/roles).
0. Create a role for running a Code City instance by clicking “+
Create Role” then enter details as follows:
* Title: “CodeCity Instance”.
* Description: “Base role for all CodeCity instances. Created
on: …”.
* ID: `instance`.
* Add the following permissions by clicking “+ Add Permissions”
button, typing the name of the permission into the “Filter
table” field (*not* the “Filter permissions by role” field),
selecting required permission, and clicking Add:
* `compute.globalOperations.get`
* `compute.zoneOperations.get`
* `logging.logEntries.create`
* Click “Create”.
0. Optionally create a second role for obtaining wildcard certs by
again clicking “+ Create Role” and entering:
* Title: “CodeCity Cert-via-DNS”
* Description: “Add-on permission to allow an instance to update
its own wildcard letsencrypt SSL cert via an ACME dns-01
challenge. Created on: …”.
* ID: `dnscert`.
* Add the following permisions:
* `dns.changes.create`
* `dns.changes.get`
* `dns.managedZones.list`
* `dns.resourceRecordSets.create`
* `dns.resourceRecordSets.delete`
* `dns.resourceRecordSets.list`
* `dns.resourceRecordSets.update`
* Click “Create”.
### Create a Service Account
Now you can create a service account for your instance.
1. Go to the [Service Accounts tab of IAM & Admin](
https://console.cloud.google.com/iam-admin/serviceaccounts).
0. Click “+ Create Servce Account” and enter details as follows:
* Service account name: choose and appropriate name such as
“CodeCity instance” or “instance-myInstanceName”.
* Service account ID: modify suggested ID if desired.
* Service account description: “Service account for the
example.codecity.world instance” or similar.
0. Click “Create”.
* Where it says “Select a role”, select “CodeCity Instance”.
* If you intend to use certbot to obtain a wildcare DNS cert,
click “+ Add Another Role” and select “CodeCity Cert-via-DNS”.
0. Click “Continue”.
0. Click “Done.
### Use the Service Account to run your GCE Instance
If you have not yet done so, simply [follow the instructions to create
a GCE instance for Code City](#create-a-gce-instance) and, when you
get to the “Identity and API access” section of the creation wizard,
select the service account you created previously.
Otherwise, if you have already created your GCE instance, configure it
to use the newly-create service account as follows:
1. Go to the [VM instances tab](
https://console.cloud.google.com/compute/instances).
0. Select the VM instance you created previously.
0. Click the stop button at the top of the page to stop it, if it is
running.
0. Click the name of your instance to view its “VM instnace details”
page.
0. Click “Edit”.
0. Scroll down to “Service account” and select the service account
you created previously.
## Appendix B: Remote Debugging
If you need to debug the server because it has stopped responding to
network activity, here’s how to do that:
1. SSH in to the GCE instance and enable the inspector on the running server:
* $ sudo kill -s SIGUSR1 `pidof codecity`
0. Look in /var/log/daemon.log for a message like:
```
Feb 26 01:22:49 google codecity[19464]: Debugger listening on ws://127.0.0.1:9229/8df977b7-024d-464d-84c2-44321dd5b398
Feb 26 01:22:49 google codecity[19464]: For help, see: https://nodejs.org/en/docs/inspector
```
Note the port number (in this case 9229).
0. SSH in to the GCE instance again, enabling port forwarding:
```
ssh -L 9229:localhost:9229 google.codecity.world
```
* The initial 9229 can be replaced with a local port number of
your choice.
* The `:localhost:` directive ensures that only processes
running on your local machine can make use of the port
forward.
0. Open the inspector in Chrome by going to
[`chrome://inspect`](chrome://inspect).
(Based on [node.js debugging documentation](
https://nodejs.org/en/docs/guides/debugging-getting-started/) and [a
related blog post](
https://hackernoon.com/debugging-node-without-restarting-processes-bd5d5c98f200).)
## Appendix C: Additional Instructions for Code City Collaborators
### GIT Code City by SSH instead of HTTPS
During early development, the Code City repository was private so it
was necessary to do the `git clone` by SSH instead of HTTPS. We
continue to do this on our production instance to allow commits to the
`prod` branch to be made from there if necessary.
To avoid putting SSH private keys on the instance, we use SSH agent
forwarding. This would mostly be automatic except that we also need
to be able to use our personal credentials as the user `codecity`.
The solution is adapted [from Server Fault](
https://serverfault.com/questions/107187).
#### Preparation (do once)
1. Add your SSH public key (ideally, one from a hardware token or the
like) to [your GitHub account](https://github.com/settings/keys).
0. Verify that you can ssh to GitHub from your local workstation:
```
ssh -T git@github.com
```
Output should look like “Hi username! You've
successfully authenticated, but GitHub does not provide shell
access.”
0. Edit `~/.ssh/config` to add the following directive, if not
already present:
```
ForwardAgent yes
```
This will enable agent forwarding by default.
#### When setting up a CodeCity GCE instance
These instructions replace step 3 of [Install & Configure Code
City](#install--configure-code-city).
4. Ensure that your SSH public key can be used to log in to the
instance. This can be done in two ways:
* Preferred: add it to [the Compute Engine metadata page](
https://pantheon.corp.google.com/compute/metadata/sshKeys).
* Alternatively: add it to the instance at creation or by
editing the instance on [the GCE instances page](
https://console.cloud.google.com/compute/instances).
* Alternatively: log into the instance initially using the
browser-based SSH available via the GCE console. Use the text
editor of your choice to append your SSH public key to
`~/.ssh/authorized_keys`.
0. Ensure you can ssh to your instance from the command line of your
local machine. Use `-A` to enable agent forwarding (`ssh -A
example.codecity.world`) or add `ForwardAgent yes` to your
`~/.ssh/config` file.
0. From your instance, verify that agent forwarding is working:
```
ssh -T git@github.com
```
(Expected output similar to step 2 above above.)
0. Install the acl package:
```
sudo apt-get install -y acl
```
0. On your instance, after creating the `codecity` user, add the
following to your `.bashrc` or `.bash_login`:
```
setfacl -m codecity:x $(dirname "$SSH_AUTH_SOCK")
setfacl -m codecity:rw "$SSH_AUTH_SOCK"
```
0. Modify the machine’s sudo config to tell sudo not to wipe
`SSH_AUTH_SOCK` from the environment:
```
sudo visudo -f /etc/sudoers.d/ssh-agent-forwarding
```
* Add the line
```
Defaults env_keep+=SSH_AUTH_SOCK
```
then save and exit.
0. When becoming the codecity user, be sure to use “`sudo -iu
codecity`” instead of “`sudo su - cc`”—the latter will clear the
needed `SSH_AUTH_SOCK` environment variable.
0. Install Code City using the SSH repository path:
```
git clone git@github.com:google/CodeCity.git
```
#### Getting SSL Certificates
Google-internal GCE instances are by default firewalled to prevent
inbound access from the Internet; this causes Certbot’s ACME checks to
fail. The preferred solution is to use the DNS-01 challenge, even if
no wildcard cert is required.
================================================
FILE: etc/apache.conf
================================================
# Template for Apache configuration.
# HTTP should redirect to HTTPS.
RewriteEngine On
RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
# '.academy' should redirect to '.world'.
ServerName XXXXX.codecity.academy
RewriteEngine On
RewriteRule ^/?(.*) https://XXXXX.codecity.world/$1 [R,L]
SSLEngine on
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/XXXXX.codecity.world/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/XXXXX.codecity.world/privkey.pem
# '.games' should redirect to '.world'.
ServerName XXXXX.codecity.games
RewriteEngine On
RewriteRule ^/?(.*) https://XXXXX.codecity.world/$1 [R,L]
SSLEngine on
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/XXXXX.codecity.world/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/XXXXX.codecity.world/privkey.pem
ServerName XXXXX.codecity.world
Alias /static /home/cc/CodeCity/static
Options Indexes Includes FollowSymLinks
AllowOverride All
Require all granted
ProxyPass /login http://localhost:7781/login
ProxyPassReverse /login http://localhost:7781/login
ProxyPass /connect http://localhost:7782/connect
ProxyPassReverse /connect http://localhost:7782/connect
ProxyPass /mobwrite http://localhost:7783/mobwrite
ProxyPassReverse /mobwrite http://localhost:7783/mobwrite
# Must be last, or else it will grab all requests.
ProxyPass /static !
ProxyPass / http://localhost:7780/
ProxyPassReverse / http://localhost:7780/
ErrorLog /home/cc/error.log
CustomLog /home/cc/access.log combined
SSLEngine on
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/XXXXX.codecity.world/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/XXXXX.codecity.world/privkey.pem
================================================
FILE: etc/cc-localhost.conf
================================================
# Nginx configuration for Code City on localhost.
# Warning: This configuration is insecure, users can hijack each other's perms.
#
# The easiest way to use this file is to leave it unedited and instead
# start nginx using bin/nginx-dev, which will dynamically create
# suitable config files on the fly.
# Configuration applying to all servers.
error_page 502 503 504 =503 /static/503.html;
# Configuration applying to all proxy forwarding.
proxy_set_header Host $http_host;
proxy_set_header Forwarded $proxy_add_forwarded; # See below.
proxy_set_header CodeCity-pathToSubdomain "?1";
proxy_pass_header Server;
proxy_next_upstream_tries 1;
proxy_max_temp_file_size 0;
proxy_connect_timeout 10s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
server {
# Listen on port 8080 for both IPv6 and IPv4.
listen [::]:8080 ipv6only=off;
location / {
# Proxy to Code City port 7780.
proxy_pass http://127.0.0.1:7780/;
}
location /static/ {
# Static files.
autoindex on;
index index.html;
# Edit to be full path to CodeCity directory.
# E.g. /home/userid/src/CodeCity
root REPOSITORY;
}
location /login {
# Proxy to loginServer.js port 7781.
proxy_pass http://127.0.0.1:7781/login;
}
location /connect {
# Proxy to connectServer.js port 7782.
proxy_pass http://127.0.0.1:7782/connect;
}
location /mobwrite {
# Proxy to mobwrite_server.py port 7783.
proxy_pass http://127.0.0.1:7783/mobwrite;
}
}
# Configuration for generating Forwarded: header, based on example from
# https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
#
# Conceal the IP address of incoming connections by default, as it is
# PII and we try to avoid giving users any chance to get their hands
# on each other's PII. To enable inclusion of actual IP addresses of
# incoming connections in the Forwarded header, uncomment the first
# two matchers below.
map $remote_addr $proxy_forwarded_for {
# IPv4 addresses can be sent as-is.
# ~^[0-9.]+$ "for=$remote_addr";
# IPv6 addresses need to be bracketed and quoted.
# ~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
# Unix domain socket names cannot be represented in RFC 7239 syntax.
default "for=unknown";
}
# Append host and proto.
map $proxy_forwarded_for $proxy_forwarded_elem {
default "$proxy_forwarded_for;host=\"$http_host\";proto=$scheme";
}
map $http_forwarded $proxy_add_forwarded {
# If the incoming Forwarded header is syntactically valid, append to it.
"~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";
# Otherwise, replace it.
default "$proxy_forwarded_elem";
}
================================================
FILE: etc/cc-onedomain.conf
================================================
# Nginx configuration for Code City using a single domain.
# Warning: This configuration is insecure, users can hijack each other's perms.
# Configuration applying to all servers.
error_page 502 503 504 =503 /static/503.html;
# Configuration applying to all proxy forwarding.
proxy_set_header Host $http_host;
proxy_set_header Forwarded $proxy_add_forwarded; # See below.
proxy_set_header CodeCity-pathToSubdomain "?1";
proxy_pass_header Server;
proxy_next_upstream_tries 1;
proxy_max_temp_file_size 0;
proxy_connect_timeout 10s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
# Redirect all http traffic to https, except for ACME HTTP-01 challenges.
server {
# Listen on port 80 for both IPv6 and IPv4.
listen [::]:80 ipv6only=off;
location / {
return 301 https://$host$request_uri;
}
# Serve ACME challenge files to enable automatic Certbot SSL
# certificate renewals using HTTP-01 challenges.
location /.well-known/ {
# Serve these from the usual Debain default path so it doesn't
# matter whether this config file is installed yet or not, and to
# avoid having certbot have to write to /home/codecity/
root /var/www/html;
}
}
# Code City configuration
server {
# Listen on port 443 for both IPv6 and IPv4.
listen [::]:443 ssl ipv6only=off;
# Replace INSTANCENAME with the domain name of your instance. Make
# sure that the resulting filenames point at the certificate files
# created by certbot.
ssl_certificate /etc/letsencrypt/live/INSTANCENAME/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/INSTANCENAME/privkey.pem;
# Canonicalise to a single domain.
#
# Replace INSTANCENAME (both places) with the domain name of your
# instance. If you have more than one domain name for you instance,
# put the canonical one here.
if ( $host != INSTANCENAME ) {
return 301 https://INSTANCENAME$request_uri;
}
location / {
# Proxy to Code City port 7780.
proxy_pass http://127.0.0.1:7780/;
}
location /static/ {
# Static files.
autoindex on;
index index.html;
# If requried, edit to be full path to CodeCity directory. Nginx
# will add /static/ automatically since that's the location.
# E.g.: /home/codecity/CodeCity;
root /home/codecity/CodeCity;
}
location /login {
# Proxy to loginServer.js port 7781.
proxy_pass http://127.0.0.1:7781/login;
}
location /connect {
# Proxy to connectServer.js port 7782.
proxy_pass http://127.0.0.1:7782/connect;
}
location /mobwrite {
# Proxy to mobwrite_server.py port 7783.
proxy_pass http://127.0.0.1:7783/mobwrite;
}
}
# Configuration for generating Forwarded: header, based on example from
# https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
#
# Conceal the IP address of incoming connections by default, as it is
# PII and we try to avoid giving users any chance to get their hands
# on each other's PII. To enable inclusion of actual IP addresses of
# incoming connections in the Forwarded header, uncomment the first
# two matchers below.
map $remote_addr $proxy_forwarded_for {
# IPv4 addresses can be sent as-is.
# ~^[0-9.]+$ "for=$remote_addr"
# IPv6 addresses need to be bracketed and quoted.
# ~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
# Unix domain socket names cannot be represented in RFC 7239 syntax.
default "for=unknown";
}
# Append host and proto.
map $proxy_forwarded_for $proxy_forwarded_elem {
default "$proxy_forwarded_for;host=\"$http_host\";proto=$scheme";
}
map $http_forwarded $proxy_add_forwarded {
# If the incoming Forwarded header is syntactically valid, append to it.
"~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";
# Otherwise, replace it.
default "$proxy_forwarded_elem";
}
================================================
FILE: etc/cc-subdomain.conf
================================================
# Nginx configuration for Code City using multiple subdomains.
# Configuration applying to all servers.
# Replace INSTANCENAME with the domain name of your instance; make
# sure the result is prefixed with the 'static' subdomain.
# E.g.: static.example.codecity.world
error_page 502 503 504 =503 https://static.INSTANCENAME/503.html;
# Configuration applying to all proxy forwarding.
proxy_set_header Host $http_host;
proxy_set_header Forwarded $proxy_add_forwarded; # See below.
proxy_set_header CodeCity-pathToSubdomain "?0";
proxy_pass_header Server;
proxy_next_upstream_tries 1;
proxy_max_temp_file_size 0;
proxy_connect_timeout 10s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
# Redirect all http traffic to https.
server {
# Listen on port 80 for both IPv6 and IPv4.
listen [::]:80 ipv6only=off;
return 301 https://$host$request_uri;
}
# Code City configuration
server {
# Listen on port 443 for both IPv6 and IPv4.
listen [::]:443 ssl ipv6only=off;
# Replace INSTANCENAME with the domain name of your instance. Make
# sure that the resulting filenames point at the certificate files
# created by certbot.
ssl_certificate /etc/letsencrypt/live/INSTANCENAME/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/INSTANCENAME/privkey.pem;
# # Canonicalise to a single domain.
# #
# # Replace regular expression with one that matches all non-canonical
# # domains.
# if ( $host ~ ^example\.codecity\.(academy|games)$ ) {
# # Replace INSTANCENAME with the canonical domain name of your instance.
# # E.g.: https://example.codecity.world$request_uri
# return 301 https://INSTANCENAME$request_uri;
# }
# # Replace regular expression with one that matches all non-canonical
# # subdomains.
# if ( $host ~ ^(.*)\.example\.codecity\.(academy|games)$ ) {
# # Replace INSTANCENAME with the canonical domain name of your instance.
# # E.g.: https://$1.example.codecity.world$request_uri
# return 301 https://$1.INSTANCENAME$request_uri;
# }
location / {
# Proxy to Code City port 7780.
proxy_pass http://127.0.0.1:7780/;
}
}
# Login server.
server {
listen [::]:443 ssl;
# Replace INSTANCENAME with the domain name of your instance; make
# sure the result is prefixed with the 'login' subdomain.
# E.g.: login.example.codecity.world
server_name login.INSTANCENAME;
location / {
# Proxy to loginServer.js port 7781.
proxy_pass http://127.0.0.1:7781/;
}
}
# Connect server.
server {
listen [::]:443 ssl;
# Replace INSTANCENAME with the domain name of your instance; make
# sure the result is prefixed with the 'connect' subdomain.
# E.g.: connect.example.codecity.world
server_name connect.INSTANCENAME;
location / {
# Proxy to connectServer.js port 7782.
proxy_pass http://127.0.0.1:7782/;
}
}
# MobWrite server.
server {
listen [::]:443 ssl;
# Replace INSTANCENAME with the domain name of your instance; make
# sure the result is prefixed with the 'mobwrite' subdomain.
# E.g.: mobwrite.example.codecity.world
server_name mobwrite.INSTANCENAME;
location / {
# Proxy to mobwrite_server.py port 7783.
proxy_pass http://127.0.0.1:7783/mobwrite;
}
}
# Static file server.
server {
listen [::]:443 ssl;
# Replace INSTANCENAME with the domain name of your instance; make
# sure the result is prefixed with the 'static' subdomain.
# E.g.: static.example.codecity.world
server_name static.INSTANCENAME;
location / {
autoindex on;
index index.html;
# If required, edit to be full path to CodeCity static directory.
# E.g. /home/codecity/CodeCity/static
root /home/codecity/CodeCity/static;
}
}
# Configuration for generating Forwarded: header, based on example from
# https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
#
# Conceal the IP address of incoming connections by default, as it is
# PII and we try to avoid giving users any chance to get their hands
# on each other's PII. To enable inclusion of actual IP addresses of
# incoming connections in the Forwarded header, uncomment the first
# two matchers below.
map $remote_addr $proxy_forwarded_for {
# IPv4 addresses can be sent as-is.
# ~^[0-9.]+$ "for=$remote_addr"
# IPv6 addresses need to be bracketed and quoted.
# ~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
# Unix domain socket names cannot be represented in RFC 7239 syntax.
default "for=unknown";
}
# Append host and proto.
map $proxy_forwarded_for $proxy_forwarded_elem {
default "$proxy_forwarded_for;host=\"$http_host\";proto=$scheme";
}
map $http_forwarded $proxy_add_forwarded {
# If the incoming Forwarded header is syntactically valid, append to it.
"~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";
# Otherwise, replace it.
default "$proxy_forwarded_elem";
}
================================================
FILE: etc/codecity-connect.service
================================================
[Unit]
Description=Code City Connect Server
Documentation=https://github.com/google/CodeCity
After=network.target
[Service]
SyslogIdentifier=cc-connect
WorkingDirectory=/home/codecity/CodeCity/connect
User=codecity
Group=codecity
ExecStart=@/home/codecity/CodeCity/connect/connectServer cc-connect
Restart=on-failure
================================================
FILE: etc/codecity-login.service
================================================
[Unit]
Description=Code City Login Server
Documentation=https://github.com/google/CodeCity
After=network.target
[Service]
SyslogIdentifier=cc-login
WorkingDirectory=/home/codecity/CodeCity/login
User=codecity
Group=codecity
ExecStart=@/home/codecity/CodeCity/login/loginServer cc-login
Restart=on-failure
================================================
FILE: etc/codecity-mobwrite.service
================================================
[Unit]
Description=Code City Login Server
Documentation=https://github.com/google/CodeCity
After=network.target
[Service]
SyslogIdentifier=cc-mobwrite
WorkingDirectory=/home/codecity/CodeCity/mobwrite
User=codecity
Group=codecity
ExecStart=@/usr/bin/python2 cc-mobwrite /home/codecity/CodeCity/mobwrite/mobwrite_server.py
Restart=on-failure
================================================
FILE: etc/codecity.service
================================================
[Unit]
Description=Code City
Documentation=https://github.com/google/CodeCity
After=network.target
Wants=codecity-login.service codecity-connect.service codecity-mobwrite.service
[Service]
SyslogIdentifier=codecity
WorkingDirectory=/home/codecity/CodeCity/database
User=codecity
Group=codecity
ExecStart=@/home/codecity/CodeCity/server/codecity codecity codecity.cfg
Restart=on-failure
[Install]
WantedBy=multi-user.target
================================================
FILE: etc/gcloud-snapshot
================================================
#!/bin/bash
# Put this file in /etc/cron.daily/ to effect automatic daily snapshots.
#
# Must also have installed gcloud-snapshot.sh from:
#
# https://github.com/jacksegal/google-compute-snapshot/
#
# and enabled the "compute engine" cloud API access scope for this
# instance.
/usr/local/sbin/gcloud-snapshot.sh -d 30
================================================
FILE: login/login.html
================================================
Code City Login
Sign in
================================================
FILE: login/loginServer
================================================
#!/usr/bin/env node
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Node.js server that provides Google auth services to Code City.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
const crypto = require('crypto');
const forwardedParse = require('forwarded-parse');
const fs = require('fs').promises;
const {google} = require('googleapis');
const http = require('http');
const net = require('net');
const {URL, format: urlFormat} = require('url');
const oauth2Api = google.oauth2('v2');
// Configuration constants.
const configFileName = 'loginServer.cfg';
// Global variables
let CFG = null;
const /** !Object */ clients = {};
const DEFAULT_CFG = {
// Internal port for this HTTP server. Nginx hides this from users.
httpPort: 7781,
// URL of connect page (absolute or relative).
connectUrl: 'https://connect.example.codecity.world/',
// URL of static folder (absolute or relative).
staticUrl: 'https://static.example.codecity.world/',
// Google's API client ID.
clientId: '00000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' +
'.apps.googleusercontent.com',
// Google's API client secret.
clientSecret: 'yyyyyyyyyyyyyyyyyyyyyyyy',
// Root domain.
cookieDomain: 'example.codecity.world',
// Regexp on email addresses that must pass to allow access.
emailRegexp: '.*',
// Port number for the login service backend.
backendPort: 7776,
/* List of fields, from data object returned by
* oauth2Api.userinfo.v2.me.get, to pass along in the request the
* login service backend.
*
* Available fieldnames, and the types and meanings of their values:
* - id: string - the user's OAuth (GAIA) ID as a numeric string.
* If salt is set (below), the .id will be salted and hashed with
* sha512 before being sent to the login backend service.
* - email: string - the user's email address.
* - verified_email: boolean - has the user's email address been verified?
* - name: string - the user's full name.
* - given_name: string - the user's given name.
* - family_name: string - the user's family name.
* - picture: string - URL pointing to the user's profile picture.
* - hd: string - the hosted domain, for GSuite accounts.
*/
backendFields: ['id'],
/* Random salt for OAuth IDs. If set to '' the .id field received
* from the Oauth server will be hashed but not salted. If set to
* undefined (or not set) the .id wil be sent plaintext.
*/
salt: 'zzzzzzzzzzzzzzzz'
};
/**
* Serve an error to the given ServerResult. Also log information
* about the failed IncomingMessage to the console.
* @param {!http.IncomingMessage} request The request which triggered the error.
* @param {!http.ServerResponse} response The ServerResult to send error to.
* @param {number} statusCode the HTTP response code to be served.
* @param {string} message An additional message to include in the result.
* @return {void}
*/
function sendError(request, response, statusCode, message) {
console.log('%s %s (Host: %s): %d %s', request.method,
request.url.replace(/=[^&]*(?=&|$)/g, '=…'),
request.headers.host, statusCode, message);
response.writeHead(statusCode).end(message);
}
/**
* Load a file from disk, add substitutions, and serve to the web.
* @param {!IncomingMessage} request The request being answered.
* @param {!http.ServerResponse} response The ServerResult to send the file to.
* @param {string} filename Name of template file on disk.
* @param {!Object} subs Object-map of replacement strings.
*/
async function serveFile(request, response, filename, subs) {
let /** string */ data;
try {
data = String(await fs.readFile(filename, 'utf8'));
} catch (err) {
sendError(request, response,
500, `Unable to load file ${filename}: ${err}`);
return;
}
// Inject substitutions.
for (const name in subs) {
data = data.replace(new RegExp(name, 'g'), subs[name]);
}
// Serve page to user.
response.statusCode = 200;
response.setHeader('Content-Type', 'text/html');
response.end(data);
}
/**
* Send a string to the login service backend and return any data
* received in response.
* @param {string} query Data string to send to backend.
* @return {!Promise} a promise yeilding the data received.
*/
async function pingBackend(query) {
let result = '';
return new Promise((resolve, reject) => {
const socket = net.createConnection({port: CFG.backendPort});
socket.on('connect', () => {
socket.end(query);
});
socket.on('error', (error) => {
socket.destroy();
reject(error);
});
socket.on('data', (data) => {
result += String(data);
});
socket.on('end', () => {
resolve(result);
});
});
}
/**
* Handles HTTP requests from web server.
* @param {!Object} request HTTP server request object
* @param {!Object} response HTTP server response object.
*/
async function handleRequest(request, response) {
if (request.connection.remoteAddress !== '127.0.0.1') {
sendError(request, response, 403,
`Connection from ${request.connection.remoteAddress} denied.`);
return;
}
// Determine what URL the client contacted us on.
let proto = 'http'; // What proto we are actually listening to.
let host = request.headers.host; // Host header we actually received.
// See if the first reverse proxy knows better.
const forwarded = request.headers.forwarded;
if (forwarded) {
try {
const forwards = forwardedParse(forwarded);
if (forwards[0]) {
if (forwards[0].proto) proto = forwards[0].proto;
if (forwards[0].host) host = forwards[0].host;
}
} catch (e) {
sendError(request, response, 400,
`Forwarded header: ${e.name}: ${e.message} of "${forwarded}"`);
return;
}
}
const url = new URL(request.url, `${proto}://${host}`);
const loginUrl = urlFormat(url, {fragment: false, search: false});
// Get an authentication client for our interactions with Google.
if (!clients[loginUrl]) {
// Create client for login URL not seen before.
clients[loginUrl] = new google.auth.OAuth2(
CFG.clientId, CFG.clientSecret, loginUrl);
}
const oauth2Client = clients[loginUrl];
// No auth code? Serve login.html.
const code = url.searchParams.get('code');
if (!code) {
// Compute Google's login URL, including deciding where to
// redirect to afterwards.
const options = {scope: 'email'};
if (url.searchParams.has('after')) {
options.state = url.searchParams.get('after');
} else if (url.searchParams.has('loginThenClose')) {
options.state = CFG.staticUrl + 'login-close.html';
} else {
options.state = CFG.connectUrl;
}
const subs = {
'<<>>': oauth2Client.generateAuthUrl(options),
'<<>>': CFG.staticUrl
};
serveFile(request, response, 'login.html', subs);
return;
}
// Handle the result of an OAuth login.
let tokens;
try {
({tokens} = await oauth2Client.getToken(code));
} catch (err) {
sendError(request, response, 500, `Google OAuth2 fail: ${err}`);
return;
}
// Now tokens contains an access_token and an optional
// refresh_token. Save them.
oauth2Client.setCredentials(tokens);
let data;
try {
({data} = await oauth2Api.userinfo.v2.me.get({auth: oauth2Client}));
} catch (err) {
sendError(request, response, 500, `Google Userinfo fail: ${err}`);
return;
}
// Check email address is allowed.
const emailRegexp = new RegExp(CFG.emailRegexp || '.*');
if (!emailRegexp.test(data.email)) {
sendError(request, response, 403, `Login denied for ${data.email}`);
return;
}
// FYI: If present, data.hd contains the GSfE domai,
// e.g. 'students.gissv.org', or 'sjsu.edu'. We aren't using it
// now, but this might be used to filter users.
// Convert the OAuth (GAIA) ID into one unique for Code City. Use
// CFG.salt to salt the sha512hash. If .salt === '', then .id will
// still be hashed but not salted.
// TODO(cpcallen): it would be more secure to append salt to id.
if (('id' in data) && CFG.salt !== undefined) {
data.id = crypto.createHash('sha512')
.update(CFG.salt + data.id).digest('hex');
}
// Contact login service backend if configured.
let cookie;
if (CFG.backendPort) {
// Construct object to be passed to login service backend.
const loginData = {};
for (const name of CFG.backendFields || ['id']) {
if (name in data) loginData[name] = data[name];
}
// Ping the login service backend.
try {
cookie = await pingBackend(JSON.stringify(loginData) + '\n');
} catch (err) {
sendError(request, response, 500, `Login service backend fail: ${err}`);
return;
}
} else {
// Just use the (probably salted and hashed) id value like we used to.
cookie = data.id;
}
if (!cookie) {
sendError('Login service backend did not return a valid cookie');
return;
}
// Login successful. Issue ID cookie.
if (!url.searchParams.has('state')) {
sendError(request, response, 500,
'Login successful but loginServer forgot where to redirect to.');
return;
}
const domain = CFG.cookieDomain ? `Domain=${CFG.cookieDomain}; ` : '';
const redirectUrl = url.searchParams.get('state');
response.writeHead(302, { // Temporary redirect.
'Set-Cookie': `ID=${cookie}; HttpOnly; ${domain}Path=/`,
'Location': redirectUrl,
});
response.end('Login OK. Redirecting.');
console.log('Accepted xxxx' + cookie.substring(cookie.length - 4));
}
/**
* Read the JSON configuration file and return it. If none is
* present, write a stub and throw an error.
*/
async function readConfigFile(filename) {
let data;
try {
data = await fs.readFile(filename, 'utf8');
} catch (err) {
console.log(`Configuration file ${filename} not found. ` +
'Creating new file.');
data = JSON.stringify(DEFAULT_CFG, null, 2) + '\n';
await fs.writeFile(filename, data, 'utf8');
}
CFG = JSON.parse(data);
if (CFG.salt === DEFAULT_CFG.salt) {
throw Error(
`Configuration file ${filename} not configured. ` +
'Please edit this file.');
}
if (!CFG.connectUrl.endsWith('/')) CFG.connectUrl += '/';
if (!CFG.staticUrl.endsWith('/')) CFG.staticUrl += '/';
}
/**
* Read configuration and start up the HTTP server.
*/
async function startup() {
await readConfigFile(configFileName);
// Start an HTTP server.
const server = http.createServer(handleRequest);
server.listen(CFG.httpPort, 'localhost', () => {
console.log(`Login server listening on port ${CFG.httpPort}`);
});
}
startup();
================================================
FILE: login/package.json
================================================
{
"name": "codecity-login",
"version": "0.0.0",
"description": "Login server for the Code City project",
"main": "loginServer.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/google/CodeCity.git"
},
"author": "Google",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/google/CodeCity/issues"
},
"homepage": "https://github.com/google/CodeCity#readme",
"dependencies": {
"forwarded-parse": "^2.1.1",
"googleapis": "^59.0.0"
},
"devDependencies": {}
}
================================================
FILE: minimal/core_01_minimal.js
================================================
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Minimal database for Code City.
* @author fraser@google.com (Neil Fraser)
*/
var user = null;
var $ = {};
// System object: $.system
$.system = {};
$.system.log = new 'CC.log';
$.system.checkpoint = new 'CC.checkpoint';
$.system.shutdown = new 'CC.shutdown';
$.system.connectionListen = new 'CC.connectionListen';
$.system.connectionUnlisten = new 'CC.connectionUnlisten';
$.system.connectionWrite = new 'CC.connectionWrite';
$.system.connectionClose = new 'CC.connectionClose';
// Physical object prototype: $.physical
$.physical = {};
$.physical.name = 'Physical object prototype';
$.physical.description = '';
$.physical.location = null;
$.physical.contents_ = null;
$.physical.getContents = function() {
return this.contents_ || [];
};
$.physical.addContents = function(thing) {
var contents = this.getContents();
contents.indexOf(thing) === -1 && contents.push(thing);
this.contents_ = contents;
};
$.physical.removeContents = function(thing) {
var contents = this.getContents();
var index = contents.indexOf(thing);
if (index !== -1) {
contents.splice(index, 1);
}
this.contents_ = contents;
};
$.physical.moveTo = function(dest) {
var src = this.location;
src && src.removeContents && src.removeContents(this);
this.location = dest;
dest && dest.addContents && dest.addContents(this);
};
$.physical.look = function() {
user.tell(this.name);
user.tell(this.description);
var contents = this.getContents();
if (contents.length) {
var text = [];
for (var i = 0; i < contents.length; i++) {
text[i] = String(contents[i].name || contents[i]);
}
user.tell('Contents: ' + text.join(', '));
}
};
$.physical.look.dobj = 'this';
// Thing prototype: $.thing
$.thing = Object.create($.physical);
$.thing.name = 'Thing prototype';
$.thing.get = function() {
this.moveTo(user);
user.tell('You pick up ' + this.name + '.');
if (user.location) {
user.location.announce(user.name + ' picks up ' + this.name + '.');
}
};
$.thing.get.dobj = 'this';
$.thing.drop = function() {
this.moveTo(user.location);
user.tell('You drop ' + this.name + '.');
if (user.location) {
user.location.announce(user.name + ' drops ' + this.name + '.');
}
};
$.thing.drop.dobj = 'this';
// Room prototype: $.room
$.room = Object.create($.physical);
$.room.name = 'Room prototype';
$.room.announce = function(text) {
var contents = this.getContents();
for (var i = 0; i < contents.length; i++) {
var thing = contents[i];
if (thing !== user && thing.tell) {
thing.tell(text);
}
}
};
// User prototype: $.user
$.user = Object.create($.physical);
$.user.name = 'User prototype';
$.user.connection = null;
$.user.say = function(text) {
user.tell('You say: ' + text);
if (user.location) {
user.location.announce(user.name + ' says: ' + text);
}
};
$.user.say.dobj = 'any';
$.user.eval = function(code) {
user.tell(eval(code));
};
$.user.eval.dobj = 'any';
$.user.tell = function(text) {
if (this.connection) {
this.connection.write(text);
}
};
$.user.quit = function() {
if (this.connection) {
this.connection.close();
}
};
$.user.quit.dobj = 'none';
// Command parser.
$.execute = function(command) {
var argstr = command.trim();
var verbstr = argstr;
var dobjstr = '';
var dobj = null;
var space = command.indexOf(' ');
if (space !== -1) {
verbstr = argstr.substring(0, space).trim();
dobjstr = argstr.substring(space).trim();
}
if (!verbstr) {
return;
}
if (dobjstr) {
if (dobjstr === 'me') {
dobj = user;
} else if (dobjstr === 'here') {
dobj = user.location;
} else {
var objects = [user].concat(user.getContents());
if (user.location && user.location.getContents) {
objects.push(user.location);
objects = objects.concat(user.location.getContents());
}
for (var i = 0; i < objects.length; i++) {
var obj = objects[i];
if (obj.name &&
obj.name.toLowerCase().startsWith(dobjstr.toLowerCase())) {
dobj = obj;
break;
}
}
}
}
// Collect all objects which could host the verb.
var hosts = [user, user.location, dobj];
for (var i = 0; i < hosts.length; i++) {
var host = hosts[i];
if (!host) {
continue;
}
// Check every verb on each object for a match.
for (var prop in host) {
var func = host[prop];
if (prop === verbstr && typeof func === 'function' && func.dobj) {
if (func.dobj === 'any' || (func.dobj === 'this' && dobj === host) ||
(func.dobj === 'none' && !dobj)) {
return host[prop](dobjstr);
}
}
}
}
user.tell('Command not understood.');
};
// Database of users so that connections can bind to a user.
$.userDatabase = Object.create(null);
$.connection = {};
$.connection.onConnect = function() {
this.user = null;
this.buffer = '';
this.write('Welcome. Type name of user to connect as (Alpha or Beta).');
};
$.connection.onReceive = function(text) {
this.buffer += text.replace(/\r/g, '');
var lf;
while ((lf = this.buffer.indexOf('\n')) !== -1) {
this.onReceiveLine(this.buffer.substring(0, lf));
this.buffer = this.buffer.substring(lf + 1);
}
};
$.connection.onReceiveLine = function(text) {
if (this.user) {
user = this.user;
$.execute(text);
return;
}
// Remainder of function handles login.
text = text.trim().toLowerCase();
if ($.userDatabase[text]) {
this.user = $.userDatabase[text];
if (this.user.connection) {
this.user.connection.close();
$.system.log('Rebinding connection to ' + this.user.name);
} else {
$.system.log('Binding connection to ' + this.user.name);
}
this.user.connection = this;
this.write('Connected as ' + this.user.name);
user = this.user;
$.execute('look here');
if (user.location) {
user.location.announce(user.name + ' connects.');
}
} else {
this.write('Unknown user.');
}
};
$.connection.onEnd = function() {
if (this.user) {
if (user.location) {
user.location.announce(user.name + ' disconnects.');
}
if (this.user.connection === this) {
$.system.log('Unbinding connection from ' + this.user.name);
this.user.connection = null;
}
this.user = null;
}
};
$.connection.write = function(text) {
$.system.connectionWrite(this, text + '\n');
};
$.connection.close = function() {
$.system.connectionClose(this);
};
// Set up a room, two users, and a rock.
(function () {
var hangout = Object.create($.room);
hangout.name = 'Hangout';
hangout.description = 'A place to hang out, chat, and program.';
var alpha = Object.create($.user);
alpha.name = 'Alpha';
$.userDatabase[alpha.name.toLowerCase()] = alpha;
alpha.description = 'Looks a bit Canadian.';
alpha.moveTo(hangout);
var beta = Object.create($.user);
beta.name = 'Beta';
$.userDatabase[beta.name.toLowerCase()] = beta;
beta.description = 'Mostly harmless.';
beta.moveTo(hangout);
var rock = Object.create($.thing);
rock.name = 'Rock';
rock.description = 'Suspiciously cube shaped, made of granite.';
rock.moveTo(hangout);
$.system.connectionListen(7777, $.connection);
})();
================================================
FILE: minimal/minimal.cfg
================================================
{
"databaseDirectory": "./",
"checkpointInterval": 0,
"checkpointAtShutdown": false
}
================================================
FILE: minimal/readme.txt
================================================
Minimal Database.
This database demonstrates a very minimal Code City instance. It contains:
* Two users (Alpha and Beta)
* One room (Hangout)
* One object (Rock)
Run the database with:
node codecity.js minimal
Telnet to port 7777
Type either 'Alpha' or 'Beta' to connect as one of the two users.
Once connected, the valid commands are:
* say
* eval
* look [me|here|alpha|beta|hangout|rock]
* get rock
* drop rock
* quit
================================================
FILE: mobwrite/mobwrite.cfg
================================================
; ---------------------
; Settings for MobWrite
; ---------------------
; How long (in seconds) to compute a diff before giving up.
; Set to 0 to compute indefinitely.
DIFF_TIMEOUT = 0.1
; Demo usage should limit the maximum size of any text.
; Set to 0 to disable limit.
MAX_CHARS = 100000
; Delete any view which hasn't been accessed in a while.
; Format: {seconds|minutes|hours|days}
TIMEOUT_VIEW = 30 minutes
; Delete any text which hasn't been accessed in a while.
; TIMEOUT_TEXT should be longer than the length of TIMEOUT_VIEW
TIMEOUT_TEXT = 1 days
; How verbose the log should be.
; Choose from: CRITICAL, ERROR, WARNING, INFO, DEBUG
LOGGING = DEBUG
; Port to listen on.
LOCAL_PORT = 7783
; Restrict all Telnet connections to come from this location.
; Set to "" to allow connections from anywhere.
CONNECTION_ORIGIN = 127.0.0.1
; Name of cookie that must be present, otherwise code 410 is returned.
; This is in the form of a regexp. Set to blank to disable feature.
REQUIRED_COOKIE = ID
================================================
FILE: mobwrite/mobwrite_core.py
================================================
# Copyright 2009 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Core functions for a MobWrite client/server in Python.
"""
__author__ = "fraser@google.com (Neil Fraser)"
import datetime
import logging
import re
import diff_match_patch as dmp_module
class Configuration(dict):
def initConfig(self, filename):
"""Parse the config file and setup the preferences.
Args:
filename: Path to the config file.
Raises:
If the config is invalid, this function will thow an error.
"""
global MAX_CHARS, TIMEOUT_VIEW, TIMEOUT_TEXT
def readConfigFile(filename):
self.clear()
lineRegex = re.compile("^(\w+)\s*=\s*(.+)$")
# Attempt to open the file.
try:
f = open(filename)
except:
return
# Parse the file.
try:
for line in f:
line = line.strip()
# Comment lines start with a ;
if len(line) > 0 and not line.startswith(";"):
r = lineRegex.match(line)
if r:
self[r.group(1)] = r.group(2)
finally:
f.close()
def toTime(value):
(quantity, unit) = value.split(None, 1)
quantity = int(quantity)
if (unit == "seconds"):
delta = datetime.timedelta(seconds=quantity)
elif (unit == "minutes"):
delta = datetime.timedelta(minutes=quantity)
elif (unit == "hours"):
delta = datetime.timedelta(hours=quantity)
elif (unit == "days"):
delta = datetime.timedelta(days=quantity)
else:
raise "Config: Unknown time value."
return delta
readConfigFile(filename)
# Set each of the configuration parameters.
# If a parameter is not present, a reasonable default is specified here.
# If a configuration is invalid, throw an error.
DMP.Diff_Timeout = float(self.get("DIFF_TIMEOUT", 0.1))
MAX_CHARS = int(self.get("MAX_CHARS", 100000))
TIMEOUT_VIEW = toTime(self.get("TIMEOUT_VIEW", "30 minutes"))
TIMEOUT_TEXT = toTime(self.get("TIMEOUT_TEXT", "1 days"))
logLevel = self.get("LOGGING", "INFO")
if logLevel == "CRITICAL":
LOG.setLevel(logging.CRITICAL)
elif logLevel == "ERROR":
LOG.setLevel(logging.ERROR)
elif logLevel == "WARNING":
LOG.setLevel(logging.WARNING)
elif logLevel == "INFO":
LOG.setLevel(logging.INFO)
elif logLevel == "DEBUG":
LOG.setLevel(logging.DEBUG)
else:
raise "Config: Unknown logging level."
LOG.info("Read %d settings from %s" % (len(self), filename))
class TextObj:
# An object which stores a text.
# Object properties:
# .name - The unique name for this text, e.g 'proposal'
# .text - The text itself.
def __init__(self, *args, **kwargs):
# Setup this object
self.name = kwargs.get("name")
self.text = None
def setText(self, newtext):
# Scrub the text before setting it.
if newtext != None:
# Normalize linebreaks to LF.
newtext = re.sub(r"(\r\n|\r|\n)", "\n", newtext)
# Keep the text within the length limit.
if MAX_CHARS != 0 and len(newtext) > MAX_CHARS:
newtext = newtext[-MAX_CHARS:]
LOG.warning("Truncated text to %d characters." % MAX_CHARS)
if self.text != newtext:
self.text = newtext
class ViewObj:
# An object which contains one user's view of one text.
# Object properties:
# .username - The name for the user, e.g 'fraser'
# .filename - The name for the file, e.g 'proposal'
# .shadow - The last version of the text sent to client.
# .backup_shadow - The previous version of the text sent to client.
# .shadow_client_version - The client's version for the shadow (n).
# .shadow_server_version - The server's version for the shadow (m).
# .backup_shadow_server_version - the server's version for the backup
# shadow (m).
# .edit_stack - List of unacknowledged edits sent to the client.
# .delta_ok - Did the previous delta match the text length.
def __init__(self, *args, **kwargs):
# Setup this object
self.username = kwargs["username"]
self.filename = kwargs["filename"]
self.shadow_client_version = kwargs.get("shadow_client_version", 0)
self.shadow_server_version = kwargs.get("shadow_server_version", 0)
self.backup_shadow_server_version = kwargs.get("backup_shadow_server_version", 0)
self.shadow = kwargs.get("shadow", u"")
self.backup_shadow = kwargs.get("backup_shadow", u"")
self.edit_stack = []
self.delta_ok = True
class MobWrite:
def parseRequest(self, data):
"""Parse the raw MobWrite commands into a list of specific actions.
See: http://code.google.com/p/google-mobwrite/wiki/Protocol
Args:
data: A multi-line string of MobWrite commands.
Returns:
A list of actions, each action is a dictionary. Typical action:
{"username":"fred",
"filename":"report",
"mode":"delta",
"data":"=10+Hello-7=2",
"force":False,
"server_version":3,
"client_version":3,
"echo_username":False
}
"""
# Passing a Unicode string is an easy way to cause numerous subtle bugs.
if type(data) != str:
LOG.critical("parseRequest data type is %s" % type(data))
return []
if not (data.endswith("\n\n") or data.endswith("\r\r") or
data.endswith("\n\r\n\r") or data.endswith("\r\n\r\n")):
# There must be a linefeed followed by a blank line.
# Truncated data. Abort.
LOG.warning("Truncated data: '%s'" % data)
return []
# Parse the lines
actions = []
username = None
filename = None
server_version = None
echo_username = False
for line in data.splitlines():
if not line:
# Terminate on blank line.
break
if line.find(":") != 1:
# Invalid line.
continue
(name, value) = (line[:1], line[2:])
# Parse out a version number for file, delta or raw.
version = None
if ("FfDdRr".find(name) != -1):
div = value.find(":")
if div > 0:
try:
version = int(value[:div])
except ValueError:
LOG.warning("Invalid version number: %s" % line)
continue
value = value[div + 1:]
else:
LOG.warning("Missing version number: %s" % line)
continue
if name == "u" or name == "U":
# Remember the username.
username = value
# Client may request explicit usernames in response.
echo_username = (name == "U")
elif name == "f" or name == "F":
# Remember the filename and version.
filename = value
server_version = version
elif name == "n" or name == "N":
# Nullify this file.
filename = value
if username and filename:
action = {}
action["username"] = username
action["filename"] = filename
action["mode"] = "null"
actions.append(action)
else:
# A delta or raw action.
action = {}
if name == "d" or name == "D":
action["mode"] = "delta"
elif name == "r" or name == "R":
action["mode"] = "raw"
else:
action["mode"] = None
if name.isupper():
action["force"] = True
else:
action["force"] = False
action["server_version"] = server_version
action["client_version"] = version
action["data"] = value
action["echo_username"] = echo_username
if username and filename and action["mode"]:
action["username"] = username
action["filename"] = filename
actions.append(action)
return actions
def applyPatches(self, viewobj, diffs, action):
"""Apply a set of patches onto the view and text objects. This function must
be enclosed in a lock or transaction since the text object is shared.
Args:
viewobj: The user's view to be updated.
diffs: List of diffs to apply to both the view and the server.
action: Parameters for how forcefully to make the patch; may be modified.
"""
# Expand the fragile diffs into a full set of patches.
patches = DMP.patch_make(viewobj.shadow, diffs)
# First, update the client's shadow.
viewobj.shadow = DMP.diff_text2(diffs)
viewobj.backup_shadow = viewobj.shadow
viewobj.backup_shadow_server_version = viewobj.shadow_server_version
# Second, deal with the server's text.
textobj = viewobj.textobj
if textobj.text is None:
# A view is sending a valid delta on a file we've never heard of.
textobj.setText(viewobj.shadow)
action["force"] = False
LOG.debug("Set content: '%s'" % viewobj)
else:
if action["force"]:
# Clobber the server's text if a change was received.
if patches:
mastertext = viewobj.shadow
LOG.debug("Overwrote content: '%s'" % viewobj)
else:
mastertext = textobj.text
else:
(mastertext, results) = DMP.patch_apply(patches, textobj.text)
LOG.debug("Patched (%s): '%s'" %
(",".join(["%s" % (x) for x in results]), viewobj))
textobj.setText(mastertext)
# Global Diff/Match/Patch object.
DMP = dmp_module.diff_match_patch()
# Global logging object.
LOG = logging.getLogger("mobwrite")
# Configuration object.
CFG = Configuration()
================================================
FILE: mobwrite/mobwrite_core_test.py
================================================
#!/usr/bin/python2
# Copyright 2006 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import unittest
import mobwrite_core
# Force a module reload so to make debugging easier (at least in PythonWin).
reload(mobwrite_core)
class MobWriteCoreTest(unittest.TestCase):
def setUp(self):
mobwrite_core.LOG.setLevel(logging.ERROR)
mobwrite_core.logging.basicConfig()
def tearDown(self):
mobwrite_core.logging.shutdown()
def testParseRequest(self):
mobwrite = mobwrite_core.MobWrite()
actions = mobwrite.parseRequest("")
self.assertEquals([], actions)
actions = mobwrite.parseRequest("""u:fred
f:3:report
d:2:=10+Hello-7=2
""")
self.assertEquals([{"username":"fred",
"filename":"report",
"mode":"delta",
"data":"=10+Hello-7=2",
"force":False,
"server_version":3,
"client_version":2,
"echo_username":False
}], actions)
actions = mobwrite.parseRequest("""U:fred
f:3:report
R:2:Hello World
""")
self.assertEquals([{"username":"fred",
"filename":"report",
"mode":"raw",
"data":"Hello World",
"force":True,
"server_version":3,
"client_version":2,
"echo_username":True
}], actions)
actions = mobwrite.parseRequest("""U:fred
N:report
""")
self.assertEquals([{"username":"fred",
"filename":"report",
"mode":"null",
}], actions)
if __name__ == "__main__":
unittest.main()
================================================
FILE: mobwrite/mobwrite_server.py
================================================
#!/usr/bin/python2
# Copyright 2006 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This file is MobWrite's server-side daemon.
Runs in the background listening to a port, accepting synchronization sessions
from clients.
"""
__author__ = "fraser@google.com (Neil Fraser)"
import datetime
import glob
import os
import re
import sys
import time
import thread
import urllib
from BaseHTTPServer import BaseHTTPRequestHandler
from BaseHTTPServer import HTTPServer
import mobwrite_core
# Demo usage should limit the maximum number of connected views.
# Set to 0 to disable limit.
MAX_VIEWS = 10000
# Dictionary of all text objects.
texts = {}
# Lock to prevent simultaneous changes to the texts dictionary.
lock_texts = thread.allocate_lock()
class TextObj(mobwrite_core.TextObj):
# A persistent object which stores a text.
# Object properties:
# .lock - Access control for writing to the text on this object.
# .views - Count of views currently connected to this text.
# .lasttime - The last time that this text was modified.
# Inherited properties:
# .name - The unique name for this text, e.g 'proposal'.
# .text - The text itself.
def __init__(self, *args, **kwargs):
# Setup this object
mobwrite_core.TextObj.__init__(self, *args, **kwargs)
self.views = 0
self.lasttime = datetime.datetime.now()
self.lock = thread.allocate_lock()
# lock_texts must be acquired by the caller to prevent simultaneous
# creations of the same text.
assert lock_texts.locked(), "Can't create TextObj unless locked."
global texts
texts[self.name] = self
def setText(self, newText):
mobwrite_core.TextObj.setText(self, newText)
self.lasttime = datetime.datetime.now()
def cleanup(self):
# General cleanup task.
if self.views > 0:
return
terminate = False
# Lock must be acquired to prevent simultaneous deletions.
self.lock.acquire()
try:
if self.lasttime < datetime.datetime.now() - mobwrite_core.TIMEOUT_TEXT:
mobwrite_core.LOG.info("Expired text: '%s'" % self)
terminate = True
if terminate:
# Terminate in-memory copy.
global texts
lock_texts.acquire()
try:
try:
del texts[self.name]
except KeyError:
mobwrite_core.LOG.error("Text object not in text list: '%s'" % self)
finally:
lock_texts.release()
finally:
self.lock.release()
def fetch_textobj(name, view):
# Retrieve the named text object. Create it if it doesn't exist.
# Add the given view into the text object's list of connected views.
# Don't let two simultaneous creations happen, or a deletion during a
# retrieval.
lock_texts.acquire()
try:
if texts.has_key(name):
textobj = texts[name]
mobwrite_core.LOG.debug("Accepted text: '%s'" % name)
else:
textobj = TextObj(name=name)
mobwrite_core.LOG.debug("Creating text: '%s'" % name)
textobj.views += 1
finally:
lock_texts.release()
return textobj
# Dictionary of all view objects.
views = {}
# Lock to prevent simultaneous changes to the views dictionary.
lock_views = thread.allocate_lock()
class ViewObj(mobwrite_core.ViewObj):
# A persistent object which contains one user's view of one text.
# Object properties:
# .lasttime - The last time that a web connection serviced this object.
# .textobj - The shared text object being worked on.
# Inherited properties:
# .username - The name for the user, e.g 'fraser'
# .filename - The name for the file, e.g 'proposal'
# .shadow - The last version of the text sent to client.
# .backup_shadow - The previous version of the text sent to client.
# .shadow_client_version - The client's version for the shadow (n).
# .shadow_server_version - The server's version for the shadow (m).
# .backup_shadow_server_version - the server's version for the backup
# shadow (m).
# .edit_stack - List of unacknowledged edits sent to the client.
# .delta_ok - Did the previous delta match the text length.
def __init__(self, *args, **kwargs):
# Setup this object
mobwrite_core.ViewObj.__init__(self, *args, **kwargs)
self.lasttime = datetime.datetime.now()
self.textobj = fetch_textobj(self.filename, self)
# lock_views must be acquired by the caller to prevent simultaneous
# creations of the same view.
assert lock_views.locked(), "Can't create ViewObj unless locked."
global views
views[(self.username, self.filename)] = self
def cleanup(self):
# General cleanup task.
# Delete myself if I've been idle too long.
# Don't delete during a retrieval.
lock_views.acquire()
try:
if self.lasttime < datetime.datetime.now() - mobwrite_core.TIMEOUT_VIEW:
mobwrite_core.LOG.info("Idle out: '%s'" % self)
global views
try:
del views[(self.username, self.filename)]
except KeyError:
mobwrite_core.LOG.error("View object not in view list: '%s'" % self)
self.textobj.views -= 1
finally:
lock_views.release()
def nullify(self):
self.lasttime = datetime.datetime.min
self.cleanup()
def fetch_viewobj(username, filename):
# Retrieve the named view object. Create it if it doesn't exist.
# Don't let two simultaneous creations happen, or a deletion during a
# retrieval.
lock_views.acquire()
try:
key = (username, filename)
if views.has_key(key):
viewobj = views[key]
viewobj.lasttime = datetime.datetime.now()
mobwrite_core.LOG.debug("Accepting view: '%s'" % viewobj)
else:
if MAX_VIEWS != 0 and len(views) > MAX_VIEWS:
viewobj = None
mobwrite_core.LOG.critical("Overflow: Can't create new view.")
else:
viewobj = ViewObj(username=username, filename=filename)
mobwrite_core.LOG.debug("Creating view: '%s'" % viewobj)
finally:
lock_views.release()
return viewobj
class DaemonMobWrite(BaseHTTPRequestHandler, mobwrite_core.MobWrite):
def do_POST(self):
connection_origin = mobwrite_core.CFG.get("CONNECTION_ORIGIN", "")
if connection_origin and self.client_address[0] != connection_origin:
raise IOError("Connection refused from %s (only %s allowed)." %
(self.client_address[0], connection_origin))
mobwrite_core.LOG.info("Connection accepted from " + self.client_address[0])
required_cookie = mobwrite_core.CFG.get("REQUIRED_COOKIE", "")
if required_cookie and (('Cookie' not in self.headers) or
(not re.search(r'(^|;)\s*%s=\w' % required_cookie, self.headers['Cookie']))):
self.send_headers(410)
self.wfile.write("Required cookie not found.\n")
return
# Read the POST data.
content_length = int(self.headers['Content-Length'])
data = self.rfile.read(content_length)
div = data.find("q=")
if div == -1:
self.send_headers(400)
self.wfile.write("'q=' parameter not found in data:\n")
self.wfile.write(data)
return
data = data[div + 2:]
data = urllib.unquote(data)
self.send_headers(200)
self.wfile.write(self.handleRequest(data))
self.wfile.write("\n") # Terminating blank line.
# Goodbye
mobwrite_core.LOG.debug("Disconnecting.")
def send_headers(self, code):
origin = self.headers['Origin']
self.send_response(code)
self.send_header('Content-type', 'text/plain')
self.send_header('Access-Control-Allow-Origin', origin)
self.send_header('Access-Control-Allow-Credentials', 'true')
self.end_headers()
def handleRequest(self, text):
actions = self.parseRequest(text)
return self.doActions(actions)
def doActions(self, actions):
output = []
viewobj = None
last_username = None
last_filename = None
for action_index in xrange(len(actions)):
# Use an indexed loop in order to peek ahead one step to detect
# username/filename boundaries.
action = actions[action_index]
username = action["username"]
filename = action["filename"]
# Fetch the requested view object.
if not viewobj:
viewobj = fetch_viewobj(username, filename)
if viewobj is None:
# Too many views connected at once.
# Send back nothing. Pretend the return packet was lost.
return ""
viewobj.delta_ok = True
textobj = viewobj.textobj
if action["mode"] == "null":
# Nullify the text.
mobwrite_core.LOG.debug("Nullifying: '%s'" % viewobj)
textobj.lock.acquire()
try:
textobj.setText(None)
finally:
textobj.lock.release()
viewobj.nullify();
viewobj = None
continue
if (action["server_version"] != viewobj.shadow_server_version and
action["server_version"] == viewobj.backup_shadow_server_version):
# Client did not receive the last response. Roll back the shadow.
mobwrite_core.LOG.warning("Rollback from shadow %d to backup shadow %d" %
(viewobj.shadow_server_version, viewobj.backup_shadow_server_version))
viewobj.shadow = viewobj.backup_shadow
viewobj.shadow_server_version = viewobj.backup_shadow_server_version
viewobj.edit_stack = []
# Remove any elements from the edit stack with low version numbers which
# have been acked by the client.
x = 0
while x < len(viewobj.edit_stack):
if viewobj.edit_stack[x][0] <= action["server_version"]:
del viewobj.edit_stack[x]
else:
x += 1
if action["mode"] == "raw":
# It's a raw text dump.
data = urllib.unquote(action["data"]).decode("utf-8")
mobwrite_core.LOG.info("Got %db raw text: '%s'" % (len(data), viewobj))
viewobj.delta_ok = True
# First, update the client's shadow.
viewobj.shadow = data
viewobj.shadow_client_version = action["client_version"]
viewobj.shadow_server_version = action["server_version"]
viewobj.backup_shadow = viewobj.shadow
viewobj.backup_shadow_server_version = viewobj.shadow_server_version
viewobj.edit_stack = []
if action["force"] or textobj.text is None:
# Clobber the server's text.
textobj.lock.acquire()
try:
if textobj.text != data:
textobj.setText(data)
mobwrite_core.LOG.debug("Overwrote content: '%s'" % viewobj)
finally:
textobj.lock.release()
elif action["mode"] == "delta":
# It's a delta.
mobwrite_core.LOG.info("Got '%s' delta: '%s'" % (action["data"], viewobj))
if action["server_version"] != viewobj.shadow_server_version:
# Can't apply a delta on a mismatched shadow version.
viewobj.delta_ok = False
mobwrite_core.LOG.warning("Shadow version mismatch: %d != %d" %
(action["server_version"], viewobj.shadow_server_version))
elif action["client_version"] > viewobj.shadow_client_version:
# Client has a version in the future?
viewobj.delta_ok = False
mobwrite_core.LOG.warning("Future delta: %d > %d" %
(action["client_version"], viewobj.shadow_client_version))
elif action["client_version"] < viewobj.shadow_client_version:
# We've already seen this diff.
pass
mobwrite_core.LOG.warning("Repeated delta: %d < %d" %
(action["client_version"], viewobj.shadow_client_version))
else:
# Expand the delta into a diff using the client shadow.
try:
diffs = mobwrite_core.DMP.diff_fromDelta(viewobj.shadow, action["data"])
except ValueError:
diffs = None
viewobj.delta_ok = False
mobwrite_core.LOG.warning("Delta failure, expected %d length: '%s'" %
(len(viewobj.shadow), viewobj))
viewobj.shadow_client_version += 1
if diffs != None:
# Textobj lock required for read/patch/write cycle.
textobj.lock.acquire()
try:
self.applyPatches(viewobj, diffs, action)
finally:
textobj.lock.release()
# Generate output if this is the last action or the username/filename
# will change in the next iteration.
if ((action_index + 1 == len(actions)) or
actions[action_index + 1]["username"] != username or
actions[action_index + 1]["filename"] != filename):
print_username = None
print_filename = None
if action["echo_username"] and last_username != username:
# Print the username if the previous action was for a different user.
print_username = username
if last_filename != filename or last_username != username:
# Print the filename if the previous action was for a different user
# or file.
print_filename = filename
output.append(self.generateDiffs(viewobj, print_username,
print_filename, action["force"]))
last_username = username
last_filename = filename
# Dereference the view object so that a new one can be created.
viewobj = None
return "".join(output)
def generateDiffs(self, viewobj, print_username, print_filename, force):
output = []
if print_username:
output.append("u:%s\n" % print_username)
if print_filename:
output.append("F:%d:%s\n" % (viewobj.shadow_client_version, print_filename))
textobj = viewobj.textobj
mastertext = textobj.text
if viewobj.delta_ok:
if mastertext is None:
mastertext = ""
# Create the diff between the view's text and the master text.
diffs = mobwrite_core.DMP.diff_main(viewobj.shadow, mastertext)
mobwrite_core.DMP.diff_cleanupEfficiency(diffs)
text = mobwrite_core.DMP.diff_toDelta(diffs)
if force:
# Client sending 'D' means number, no error.
# Client sending 'R' means number, client error.
# Both cases involve numbers, so send back an overwrite delta.
viewobj.edit_stack.append((viewobj.shadow_server_version,
"D:%d:%s\n" % (viewobj.shadow_server_version, text)))
else:
# Client sending 'd' means text, no error.
# Client sending 'r' means text, client error.
# Both cases involve text, so send back a merge delta.
viewobj.edit_stack.append((viewobj.shadow_server_version,
"d:%d:%s\n" % (viewobj.shadow_server_version, text)))
viewobj.shadow_server_version += 1
mobwrite_core.LOG.info("Sent '%s' delta: '%s'" % (text, viewobj))
else:
# Error; server could not parse client's delta.
# Send a raw dump of the text.
viewobj.shadow_client_version += 1
if mastertext is None:
mastertext = ""
viewobj.edit_stack.append((viewobj.shadow_server_version,
"r:%d:\n" % viewobj.shadow_server_version))
mobwrite_core.LOG.info("Sent empty raw text: '%s'" % viewobj)
else:
# Force overwrite of client.
text = mastertext
text = text.encode("utf-8")
text = urllib.quote(text, "!~*'();/?:@&=+$,# ")
viewobj.edit_stack.append((viewobj.shadow_server_version,
"R:%d:%s\n" % (viewobj.shadow_server_version, text)))
mobwrite_core.LOG.info("Sent %db raw text: '%s'" %
(len(text), viewobj))
viewobj.shadow = mastertext
for edit in viewobj.edit_stack:
output.append(edit[1])
return "".join(output)
def cleanup_thread():
# Every minute cleanup.
while True:
mobwrite_core.LOG.info("Running cleanup task.")
for v in views.values():
v.cleanup()
for v in texts.values():
v.cleanup()
timeout = datetime.datetime.now() - mobwrite_core.TIMEOUT_TEXT
time.sleep(60)
def main():
mobwrite_core.CFG.initConfig("./mobwrite.cfg")
# Start up a thread that does timeouts and cleanup.
thread.start_new_thread(cleanup_thread, ())
port = int(mobwrite_core.CFG.get("LOCAL_PORT", 3017))
mobwrite_core.LOG.info("Listening on port %d..." % port)
s = HTTPServer(("", port), DaemonMobWrite)
try:
s.serve_forever()
except KeyboardInterrupt:
mobwrite_core.LOG.info("Shutting down.")
s.socket.close()
if __name__ == "__main__":
mobwrite_core.logging.basicConfig()
main()
mobwrite_core.logging.shutdown()
================================================
FILE: server/code.js
================================================
/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Utilities for manipulating JavaScript code.
* @author cpcallen@google.com (Christopher Allen)
*/
'use strict';
/**
* A collection of useful regular expressions.
* @const
*/
var regexps = {};
/**
* Matches (globally) escape sequences found in string and regexp
* literals, like '\n' or '\x20' or '\u1234'.
* @const
*/
regexps.escapes = /\\(?:["'\\\/0bfnrtv]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2})/g;
/**
* Matches a single-quoted string literal, like "'this one'" and
* "'it\\'s'".
* @const
*/
regexps.singleQuotedString =
new RegExp("'(?:[^'\\\\\\r\\n\\u2028\\u2029]|" +
regexps.escapes.source + ")*'", 'g');
/**
* Matches a double-quoted string literal, like '"this one"' and
* '"it\'s"'.
* @const
*/
regexps.doubleQuotedString =
new RegExp('"(?:[^"\\\\\\r\\n\\u2028\\u2029]|' +
regexps.escapes.source + ')*"', 'g');
/**
* Matches a string literal, like "'this one' and '"that one"' as well
* as "the 'string literal' substring of this longer string" too.
* @const
*/
regexps.string = new RegExp('(?:' + regexps.singleQuotedString.source + '|' +
regexps.doubleQuotedString.source + ')', 'g');
/**
* Matches exaclty a string literal, like "'this one'" but notably not
* " 'this one' " (because it contains other characters not part of
* the literal).
* @const
*/
regexps.stringExact = new RegExp('^' + regexps.string.source + '$');
/**
* RegExp matching a valid JavaScript identifier (strictly an
* IdentifierName, which does not exclude ReservedWord). Note that
* this is fairly conservative, because ANY Unicode letter can appear
* in an identifier - but the full regexp is absurdly complicated.
* @const
*/
regexps.identifier = /[A-Za-z_$][A-Za-z0-9_$]*/g;
/**
* RegExp matching exactly a valid JavaScript identifier. See note
* for .identifier, above.
* @const
*/
regexps.identifierExact = new RegExp('^' + regexps.identifier.source + '$');
/**
* Convert a string representation of a string literal to a string.
* Basically does eval(s), but safely and only if s is a string literal.
* @param {string} s A string consisting of exactly a string literal.
* @return {string} The string value of the literal s.
*/
var parseString = function(s) {
if (!regexps.stringExact.test(s)) {
throw new TypeError(quote(s) + ' is not a string literal');
};
return s.slice(1, -1).replace(regexps.escapes, function(esc) {
switch (esc[1]) {
case "'":
case '"':
case '/':
case '\\':
return esc[1];
case '0':
return '\0';
case 'b':
return '\b';
case 'f':
return '\f';
case 'n':
return '\n';
case 'r':
return '\r';
case 't':
return '\t';
case 'v':
return '\v';
case 'u':
case 'x':
return String.fromCharCode(parseInt(esc.slice(2), 16));
default:
// RegExp in call to replace has accepted something we
// don't know how to decode.
throw new Error('unknown escape sequence "' + esc + '"??');
}
});
};
/**
* Convert a string into a string literal. We use single or double
* quotes depending on which occurs less frequently in the string to
* be escaped (prefering single quotes if it's a tie). Strictly
* speaking we only need to escape backslash, \r, \n, \u2028 (line
* separator), \u2029 (paragraph separator) and whichever quote
* character we're using, but for output readability we escape all the
* control characters.
*
* TODO(cpcallen): Consider using optimised algorithm from Node.js's
* util.format (see strEscape function in
* https://github.com/nodejs/node/blob/master/lib/util.js).
* @param {string} str The string to convert.
* @return {string} The value s as a eval-able string literal.
*/
var quote = function(str) {
if (count(str, "'") > count(str, '"')) { // More 's. Use "s.
return '"' + str.replace(quote.doubleRE, quote.replace) + '"';
} else { // Equal or more "s. Use 's.
return "'" + str.replace(quote.singleRE, quote.replace) + "'";
}
};
/**
* Regexp for characters to be escaped in a single-quoted string.
*/
quote.singleRE = /[\x00-\x1f\\\u2028\u2029']/g;
/**
* Regexp for characters to be escaped in a single-quoted string.
*/
quote.doubleRE = /[\x00-\x1f\\\u2028\u2029"]/g;
/**
* Replacer function (for either case)
* @param {string} c Single UTF-16 code unit ("character") string to
* be replaced.
* @return {string} Multi-character string containing escaped
* representation of c.
*/
quote.replace = function(c) {
return quote.replacements[c];
};
/**
* Map of replacements for quote function.
*/
quote.replacements = {
'\x00': '\\0', '\x01': '\\x01', '\x02': '\\x02', '\x03': '\\x03',
'\x04': '\\x04', '\x05': '\\x05', '\x06': '\\x06', '\x07': '\\x07',
'\x08': '\\b', '\x09': '\\t', '\x0a': '\\n', '\x0b': '\\v',
'\x0c': '\\f', '\x0d': '\\r', '\x0e': '\\x0e', '\x0f': '\\x0f',
'"': '\\"', "'": "\\'", '\\': '\\\\',
'\u2028': '\\u2028', '\u2029': '\\u2029',
};
/**
* Count non-overlapping occurrences of searchString in str.
*
* There are many possible implementations; using .split works pretty
* well but this is slightly faster at time of writing. See
* https://jsperf.com/count-the-number-of-characters-in-a-string for
* latest performance measurements.
* @param {string} str The string to be searched.
* @param {string} searchString The string to count occurrences of.
* @return {number} The number of occurrences of searchString in str.
*/
var count = function(str, searchString) {
var index = 0;
for(var count = 0; ; count++) {
index = str.indexOf(searchString, index);
if (index === -1) return count;
index += searchString.length;
}
};
exports.quote = quote;
exports.regexps = regexps;
exports.parseString = parseString;
// For unit testing only!
exports.testOnly = {
count: count,
}
================================================
FILE: server/codecity
================================================
#!/usr/bin/env node
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview A virtual world of collaborative coding.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const Interpreter = require('./interpreter');
const Parser = require('./parser').Parser;
const Serializer = require('./serialize');
var CodeCity = {};
CodeCity.databaseDirectory = '';
CodeCity.interpreter = null;
CodeCity.config = null;
/**
* Start a running instance of Code City. May be called on a command line.
* @param {string=} configFile Path and filename of configuration file.
* If not present, look for the configuration file as a command line parameter.
*/
CodeCity.startup = function(configFile) {
// process.argv is a list containing: ['node', 'codecity', 'db/google.cfg']
configFile = configFile || process.argv[2];
if (!configFile) {
console.error('Configuration file not found.\n' +
'Usage: node %s ', process.argv[1]);
process.exit(1);
}
var contents = CodeCity.loadFile(configFile);
CodeCity.config = CodeCity.parseJson(contents);
// Find the most recent database file.
var dir = CodeCity.config.databaseDirectory || './';
if (dir[0] === '/') {
CodeCity.databaseDirectory = dir;
} else {
CodeCity.databaseDirectory = path.join(path.dirname(configFile), dir);
}
if (!fs.existsSync(CodeCity.databaseDirectory)) {
console.error('Database directory not found: ' +
CodeCity.databaseDirectory);
process.exit(1);
}
// Find the most recent database file.
var checkpoint = CodeCity.allCheckpoints()[0];
// Load the interpreter.
if (checkpoint) {
var filename = path.join(CodeCity.databaseDirectory, checkpoint);
CodeCity.interpreter = CodeCity.loadCheckpoint(filename);
} else {
// Database not found, load one or more startup files instead.
console.log('Unable to find database file in %s, looking for startup ' +
'file(s) instead.', CodeCity.databaseDirectory);
CodeCity.interpreter = CodeCity.loadStartup(CodeCity.databaseDirectory);
}
// Checkpoint at regular intervals.
// TODO: Let the interval be configurable from the database.
var interval = CodeCity.config.checkpointInterval || 600;
CodeCity.config.checkpointInterval = interval;
if (interval > 0) {
setInterval(CodeCity.checkpoint, interval * 1000);
}
console.log('Load complete. Starting Code City.');
CodeCity.interpreter.start();
};
/**
* Create an Interpreter instance with desired options and initialise
* it with custom builtins.
* @return {!Interpreter}
*/
CodeCity.makeInterpreter = function() {
var intrp = new Interpreter({
trimEval: true,
trimProgram: true,
methodNames: true,
stackLimit: 10000,
});
CodeCity.initSystemFunctions(intrp);
CodeCity.initLibraryFunctions(intrp);
return intrp;
};
/**
* Create an Interpreter instance and deserialise a .city checkpoint
* into it.
* @param {string} filename The filename of the .city file to read.
* @return {!Interpreter}
*/
CodeCity.loadCheckpoint = function(filename) {
var intrp = CodeCity.makeInterpreter();
var flatpack = CodeCity.parseJson(CodeCity.loadFile(filename));
Serializer.deserialize(flatpack, intrp);
console.log('Checkpoint %s loaded.', filename);
return intrp;
};
/**
* Create an Interpreter instance and load startup .js files into it.
* @param {string} dir The directory containing startup files to be read.
* @return {!Interpreter}
*/
CodeCity.loadStartup = function(dir) {
var intrp = CodeCity.makeInterpreter();
var fileCount = 0;
var files = fs.readdirSync(dir);
for (var i = 0; i < files.length; i++) {
if (files[i].match(/^(core|db|test).*\.js$/)) {
var filename = path.join(dir, files[i]);
var contents = CodeCity.loadFile(filename);
console.log('Loading startup file %s', filename);
intrp.createThreadForSrc(contents);
fileCount++;
}
}
if (fileCount === 0) {
console.error('Unable to find startup file(s) in %s', dir);
process.exit(1);
}
console.log('Loaded %d startup file(s) from %s', fileCount, dir);
return intrp;
};
/**
* Open a file and read its contents. Die if there's an error.
* @param {string} filename
* @return {string} File contents.
*/
CodeCity.loadFile = function(filename) {
// Load the specified file from disk.
try {
return fs.readFileSync(filename, 'utf8').toString();
} catch (e) {
console.error('Unable to open file: %s', filename);
console.info(e);
process.exit(1);
}
};
/**
* Parse text as JSON value. Die if there's an error.
* @param {string} text
* @return {*} JSON value.
*/
CodeCity.parseJson = function(text) {
// Convert from text to JSON.
try {
return JSON.parse(text);
} catch (e) {
console.error('Syntax error in parsing JSON');
console.info(e);
process.exit(1);
}
};
/**
* Return a list of all currently saved checkpoints, ordered from most
* to least recent.
* @return {!Array} Array of filenames for checkpoints.
*/
CodeCity.allCheckpoints = function() {
var files = fs.readdirSync(CodeCity.databaseDirectory);
files = files.filter((file) => CodeCity.allCheckpoints.regexp_.test(file));
files.sort().reverse();
return files;
};
CodeCity.allCheckpoints.regexp_ =
/^\d{4}-\d\d-\d\dT\d\d\.\d\d\.\d\d(\.\d{1,3})?Z?\.city$/;
/**
* Delete as many checkpoints as needed until there's room to fit a new one.
*/
CodeCity.deleteCheckpointsIfNeeded = function() {
var checkpoints = CodeCity.allCheckpoints();
var minFiles = Math.max(0, CodeCity.config.checkpointMinFiles || 0);
if (!checkpoints.length || checkpoints.length < minFiles) {
return; // Not enough checkpoints saved.
}
// Look up size of last checkpoint.
var lastCheckpointSize =
CodeCity.fileSize(checkpoints[checkpoints.length - 1]);
var directorySize = checkpoints.reduce((sum, fileName) =>
sum + CodeCity.fileSize(fileName), 0);
// Budget for a possible 10% growth.
var estimateNext = directorySize + lastCheckpointSize * 1.1;
var maxSize = CodeCity.config.checkpointMaxDirectorySize * 1024 * 1024;
if (typeof maxSize !== 'number') {
maxSize = Infinity;
}
if (estimateNext < maxSize) {
return; // There's room.
}
// Choose and delete one file.
var deleteFile = CodeCity.chooseCheckpointToDelete(checkpoints);
var fullPath = path.join(CodeCity.databaseDirectory, deleteFile);
console.log('Deleting checkpoint ' + fullPath);
fs.unlinkSync(fullPath);
// Do it again, until no delete is needed.
CodeCity.deleteCheckpointsIfNeeded();
};
/**
* Given a list of checkpoint filenames, choose one to delete.
* See https://neil.fraser.name/software/backup/
* @param {!Array} checkpoints Array of checkpoint filenames.
* @return {string} Filename of checkpoint to delete.
*/
CodeCity.chooseCheckpointToDelete = function(checkpoints) {
// Convert all filenames (e.g. '2018-11-09T18.49.50.548Z.city')
// into ISO-8601 format (e.g. '2018-11-09T18:49:50.548Z'),
// then parse as milliseconds.
var checkpointTimes = checkpoints.map((name) =>
Date.parse(name.slice(0, -5).replace('.', ':').replace('.', ':')));
var currentTime = Date.now();
var totalTime = currentTime - checkpointTimes[checkpointTimes.length - 1];
var interval = CodeCity.config.checkpointInterval * 1000;
// Planning to delete one checkpoint.
var checkpointCount = checkpoints.length - 1;
// Compute ideal times.
var missing = Math.max(totalTime / interval - checkpointCount, 0);
var decayRate = (missing + 1) ** (1 / checkpointCount);
var idealTimes = new Array(checkpointTimes.length);
for (var n = 0; n < checkpointTimes.length; n++) {
idealTimes[n] = currentTime - (interval * (n + decayRate ** n - 1));
}
// Choose one backup for deletion.
// Compute the cumulative error from the right side. Store in array.
var rightDiff = new Array(checkpointTimes.length);
var accumulator = 0;
for (var n = checkpointTimes.length - 1; n >= 1; n--) {
accumulator += Math.abs(checkpointTimes[n] - idealTimes[n]);
rightDiff[n] = accumulator;
}
// Compute the cumulative error from the left side (with backups shifted by
// one position, as would happen after a deletion).
// Use rightDiff array to compute total error for each possible deletion.
accumulator = 0;
var minDiff = Infinity;
var minIndex = 0;
for (var n = 1; n < checkpointTimes.length - 1; n++) {
accumulator += Math.abs(checkpointTimes[n - 1] - idealTimes[n]);
var diff = accumulator + rightDiff[n + 1];
if (diff < minDiff) {
// Smallest total error yet. Save this candidate.
minDiff = diff;
minIndex = n;
}
}
return checkpoints[minIndex];
};
/**
* Find the size of a file in the current database directory.
* @param {string} fileName Name of file.
* @return {number} Number of bytes in file.
*/
CodeCity.fileSize = function(fileName) {
var fullPath = path.join(CodeCity.databaseDirectory, fileName);
return fs.statSync(fullPath).size;
};
/**
* Save the database to disk.
* @param {boolean} sync True if Code City intends to shutdown afterwards.
* False if Code City is running this in the background.
*/
CodeCity.checkpoint = function(sync) {
console.log('Checkpointing...');
CodeCity.deleteCheckpointsIfNeeded();
try {
CodeCity.interpreter.pause();
var json = Serializer.serialize(CodeCity.interpreter);
} finally {
sync || CodeCity.interpreter.start();
}
// JSON.stringify(json) would work, but adding linebreaks so that every
// object is on its own line makes the output more readable.
var text = [];
for (var i = 0; i < json.length; i++) {
text.push(JSON.stringify(json[i]));
}
text = '[' + text.join(',\n') + ']';
var filename = (new Date()).toISOString().replace(/:/g, '.') + '.city';
filename = path.join(CodeCity.databaseDirectory, filename);
var tmpFilename = filename + '.partial';
try {
fs.writeFileSync(tmpFilename, text);
fs.renameSync(tmpFilename, filename);
console.log('Checkpoint ' + filename + ' complete.');
} catch (e) {
console.error('Checkpoint failed! ' + e);
} finally {
// Attempt to remove partially-written checkpoint if it still exists.
try {
fs.unlinkSync(tmpFilename);
} catch (e) {
}
}
};
/**
* Shutdown Code City. Checkpoint the database before terminating.
* Optional parameter is exit code (if numeric) or signal to (re-)kill
* process with (if string). Re-killing after checkpointing allows
* systemd to accurately determine cause of death. Defaults to 0.
* @param {string|number=} code Exit code or signal.
*/
CodeCity.shutdown = function(code) {
if (CodeCity.config.checkpointAtShutdown !== false) {
CodeCity.checkpoint(true);
}
if (typeof code === 'string') {
process.kill(process.pid, code);
} else {
process.exit(code || 0);
}
};
/**
* Print one line to the log. Allows for interpolated strings.
* @param {...*} var_args Arbitrary arguments for console.log.
*/
CodeCity.log = function(var_args) {
console.log.apply(console, arguments);
};
/**
* Initialize user-callable system functions.
* These are not part of any JavaScript standard.
* BUG(#280): provide (new) NativeFunction wrappers.
* @param {!Interpreter} intrp The Interpreter instance to initialize.
*/
CodeCity.initSystemFunctions = function(intrp) {
intrp.createNativeFunction('CC.log', CodeCity.log, false);
intrp.createNativeFunction('CC.checkpoint', CodeCity.checkpoint, false);
intrp.createNativeFunction('CC.shutdown', function(code) {
CodeCity.shutdown(Number(code));
}, false);
};
/**
* Initialize user-callable library functions.
* These are not part of any JavaScript standard.
* @param {!Interpreter} intrp The Interpreter instance to initialize.
*/
CodeCity.initLibraryFunctions = function(intrp) {
new intrp.NativeFunction({
id: 'CC.acorn.parse', length: 1,
/** @type {!Interpreter.NativeCallImpl} */
call: function(intrp, thread, state, thisVal, args) {
var code = args[0];
var perms = state.scope.perms;
if (typeof code !== 'string') {
throw new intrp.Error(perms, intrp.TYPE_ERROR,
'argument to parse must be a string');
}
try {
var ast = Parser.parse(code);
} catch (e) {
throw intrp.errorNativeToPseudo(e, perms);
}
return intrp.nativeToPseudo(ast, perms);
}
});
new intrp.NativeFunction({
id: 'CC.acorn.parseExpressionAt', length: 2,
/** @type {!Interpreter.NativeCallImpl} */
call: function(intrp, thread, state, thisVal, args) {
var code = args[0];
var offset = args[1];
var perms = state.scope.perms;
if (typeof code !== 'string') {
throw new intrp.Error(perms, intrp.TYPE_ERROR,
'first argument to parseExpressionAt must be a string');
}
if (typeof offset !== 'number') {
throw new intrp.Error(perms, intrp.TYPE_ERROR,
'second argument to parseExpressionAt must be a number');
}
try {
var ast = Parser.parseExpressionAt(code, offset);
} catch (e) {
throw intrp.errorNativeToPseudo(e, perms);
}
return intrp.nativeToPseudo(ast, perms);
}
});
new intrp.NativeFunction({
id: 'CC.hash', length: 2,
/** @type {!Interpreter.NativeCallImpl} */
call: function(intrp, thread, state, thisVal, args) {
var hash = String(args[0]);
var data = args[1];
var perms = state.scope.perms;
var hashes = crypto.getHashes();
if (!hashes.includes(hash)) {
throw new intrp.Error(perms, intrp.RANGE_ERROR,
'first argument to hash must be one of:\n' +
hashes.map(function(h) {return " '" + h + "'\n";}).join(''));
}
if (typeof data !== 'string') {
throw new intrp.Error(perms, intrp.TYPE_ERROR,
'second argument to hash must be a string');
}
try {
return String(crypto.createHash(hash).update(data).digest('hex'));
} catch (e) {
throw intrp.errorNativeToPseudo(e, perms);
}
}
});
};
///////////////////////////////////////////////////////////////////////////////
// Main program.
// If this file is executed form a command line, startup Code City.
// Otherwise, if it is required as a library, do nothing.
if (require.main === module) {
CodeCity.startup();
// SIGTERM and SIGINT shut down server.
process.once('SIGTERM', CodeCity.shutdown.bind(null, 'SIGTERM'));
process.once('SIGINT', CodeCity.shutdown.bind(null, 'SIGINT'));
// SIGHUP forces checkpoint.
process.on('SIGHUP', CodeCity.checkpoint.bind(null, false));
}
///////////////////////////////////////////////////////////////////////////////
// Exports
module.exports = CodeCity;
================================================
FILE: server/compile
================================================
#!/bin/bash
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Run the Closure Compiler, in --checks-only mode, on the Code City
# server (and optionally server unit tests as well).
# Compile tests, which also compile server:
readonly ENTRY_POINT=tests/run.js
# Aternatively, compile server alone:
# readonly ENTRY_POINT=codecity.js
# List of arguments to feed to compiler - flags and filenames.
args=(-O=ADVANCED_OPTIMIZATIONS
--checks-only
--module_resolution=NODE
--process_common_js_modules
--assume_function_wrapper
--dependency_mode=PRUNE
--externs=externs/node.js
--externs=externs/WeakRef.js
--hide_warnings_for=node_modules/acorn
node_modules/acorn/package.json
node_modules/acorn/**.mjs
iterable_weakmap.js
iterable_weakset.js
registry.js
parser.js
interpreter.js
serialize.js
code.js
selector.js
dumper.js
codecity
priorityqueue.js
dump
tests/*.js
)
# Set current directory to the one containing this script.
cd "$(dirname "${BASH_SOURCE[0]}")"
# Temporarily symlink extern declarations for node builtins into
# node_modules/, and add their .js and package.json files to args.
declare -a builtins
for path in externs/*; do
if [[ -d "${path}" && -f "${path}/package.json" ]]; then
builtins+=("$(basename "${path}")")
fi
done
for builtin in "${builtins[@]}"; do
link="node_modules/${builtin}"
if [[ -e "${link}" ]]; then
if [[ -L "${link}" ]]; then
rm "${link}" # Remove old symlink.
else
echo "$0: aborting because ${link} already exists and is not a symlink" \
1>&2
exit 1
fi
fi
ln -s "../externs/${builtin}" "${link}"
args+=("${link}"/{*.js,package.json})
done
google-closure-compiler "${args[@]}" --entry_point="${ENTRY_POINT}"
return="$?"
# Remove extern symlinks.
for builtin in "${builtins[@]}"; do
link="node_modules/${builtin}"
if [[ ! -L "${link}" ]]; then
echo "$0: aborting because ${link} is no longer a symlink" 1>&2
exit 1
fi
rm "${link}"
done
exit ${return}
================================================
FILE: server/config.txt
================================================
Documentation for config file options:
"databaseDirectory": string
Relative path from this config file to the database directory.
Defaults to "./" (current directory).
"checkpointInterval": number
Number of seconds between regular checkpoints.
If 0, then no regular checkpoints.
Defaults to 600 (10 minutes).
TODO: Move this configuration option into the database.
"checkpointAtShutdown": boolean
If true, save a checkpoint when the server shuts down.
If false, don't save a checkpoint, which results in lost data.
Defaults to true.
"checkpointMinFiles": number
Minimum number of checkpoint files in a directory. While there are
fewer than this number, then no checkpoints will be deleted.
Defaults to 0.
"checkpointMaxDirectorySize": number
Maximum number of megabytes allowed for checkpoints in checkpoint
directory. If this value is exceeded and checkpointMinFiles is also
satisfied, then one or more old checkpoints will be deleted to make
room for the next checkpoint.
Defaults to Infinity.
================================================
FILE: server/dump
================================================
#!/usr/bin/env node
/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Infrastructure to save the state of an Interpreter as
* eval-able JS. Mainly a wrapper around Dumper, handling
* the application of a dupmp configuration.
* @author cpcallen@google.com (Christopher Allen)
*/
'use strict';
var code = require('./code');
var CodeCity = require('./codecity');
var Do = require('./dumper').Do;
var Dumper = require('./dumper').Dumper;
var DumperOptions = require('./dumper').DumperOptions;
var Writable = require('./dumper').Writable;
var fs = require('fs');
var Interpreter = require('./interpreter');
var path = require('path');
var Selector = require('./selector');
/**
* Dump an Interpreter using a given dump specification.
* @param {!Interpreter} intrp1 An interpreter initialised exactly as
* the one the ouptut JS will be executed by.
* @param {!Interpreter} intrp2 An interpreter containing state
* modifications (relative to intrp1) to be dumped.
* @param {!Array} config The dump specification.
* @param {string=} directory A directory relative to which
* non-absolute filenames in config should be written. If none is
* supplied then they will be treated as relative to the current
* directory.
* @param {boolean=} verbose Print message describing what is being done.
*/
var dump = function(intrp1, intrp2, config, directory, verbose) {
var dumper = new Dumper(intrp1, intrp2, {verbose: verbose});
if (verbose) console.log('Dumper initialised.');
// Skip everything that's explicitly mentioned in the config, so
// that paths won't get dumped until it's their turn.
for (var item, i = 0; (item = config[i]); i++) {
if (!item.contents) continue;
for (var entry, j = 0; (entry = item.contents[j]); j++) {
dumper.skip(entry.selector);
}
}
var /** string */ header = '';
// Dump the specified paths, in order.
for (var item, i = 0; (item = config[i]); i++) {
if ('options' in item) { // An OptionsItem.
dumper.setOptions(item.options);
continue;
}
// A FileItem.
var filename = item.filename;
if (verbose) console.log('Dumping to %s...', filename);
if (directory !== undefined && !path.isAbsolute(filename)) {
filename = path.normalize(path.join(directory, filename));
}
var outputStream = new SyncWriter(filename);
dumper.setOptions({output: outputStream});
for (var selector, j = 0; (selector = item.prune[j]); j++) {
dumper.prune(selector);
}
for (var selector, j = 0; (selector = item.pruneRest[j]); j++) {
dumper.pruneRest(selector);
}
if (item.header !== undefined) header = item.header;
var fileHeader = header;
for (var key in item.headerSubs) {
fileHeader = fileHeader.replace(key, item.headerSubs[key]);
}
if (fileHeader) dumper.write(fileHeader);
if (item.contents) {
for (var entry, j = 0; (entry = item.contents[j]); j++) {
if (verbose) console.log('Dumping: %s', entry.selector);
dumper.unskip(entry.selector);
dumper.dumpBinding(entry.selector, entry.do);
dumper.write('\n');
}
}
if (item.rest) {
if (verbose) console.log('Dumping rest.');
dumper.dump();
}
outputStream.end();
}
};
/**
* Convert a dump plan from an !Array to
* !Array, with validataion and a few conversions:
*
* - Whereas as in the input, paths will will be represented by
* selector strings, in the corresponding output the properties will
* be Selectors. The SpecConfigEntry path: will become .selector in
* the corresponding COnfigEntry.
*
* - Whereas the input will specify do: values as strings
* (e.g. "RECURSE"), the output will have Do enum values
* (e.g. Do.RECURSE) instead.
*
* - A plain selector string ss, appearing in the contents: array of a
* SpecFileItem, will be replaced by the ContentEntry
* {selector: new Selector(ss), do: Do.RECURSE, reorder: false}.
*
* - All optional boolean-valued properties will be normalised to
* exist, defaulting to false.
*
* @param {*} spec The dump plan to be validated. If this is not an
* !Array, TypeError will be thrown.
* @return {!Array}
*/
var configFromSpec = function(spec) {
var /** !Array */ config = [];
/** @type {function(string, number=)} */
function reject(message, j) {
var prefix = 'spec[' + i + ']';
if (j !== undefined) prefix = prefix + '.contents[' + j + ']';
if (message[0] !== '.') prefix = prefix + ' ';
throw new TypeError(prefix + message);
}
if (!Array.isArray(spec)) {
throw new TypeError('spec must be an array of SpecConfigItems');
}
for (var i = 0; i < spec.length; i++) {
var item = spec[i];
if (typeof item !== 'object' || item === null) {
reject('not a SpecConfigItem object');
}
if ('filename' in item) { // It's a SpecFileItem.
var /** (string|undefined) */ header;
var /** !Object */ headerSubs = {};
var /** !Array */ prune = [];
var /** !Array */ pruneRest = [];
var /** !Array */ contents = [];
if (typeof item.filename !== 'string') {
// TODO(cpcallen): add better filename validity check?
reject('.filename is not a string');
} else if (!Array.isArray(item.contents) && item.contents !== undefined) {
reject('.contents is not an array');
} else if (typeof item.rest !== 'boolean' && item.rest !== undefined) {
reject('.rest is not a boolean');
}
if (item.header instanceof Array) {
header = item.header.join('\n');
} else if (typeof item.header === 'string' || item.header === undefined) {
header = item.header;
} else {
reject('.header is not string or array of strings');
}
if (item.headerSubs instanceof Object) {
for (var key in item.headerSubs) {
var value = item.headerSubs[key];
if (value instanceof Array) {
headerSubs[key] = value.join('\n');
} else if (typeof value === 'string') {
headerSubs[key] = value;
} else {
reject('.headerSubs.' + key + ' is not a string');
}
}
} else if (item.headerSubs !== undefined) {
reject('.headerSubs is not an object');
}
if ('prune' in item) {
if (!Array.isArray(item.prune)) {
reject('.prune is not an array');
}
for (var j = 0; j < item.prune.length; j++) {
prune.push(new Selector(item.prune[j]));
}
}
if ('pruneRest' in item) {
if (!Array.isArray(item.pruneRest)) {
reject('.pruneRest is not an array');
}
for (j = 0; j < item.pruneRest.length; j++) {
pruneRest.push(new Selector(item.pruneRest[j]));
}
}
if ('contents' in item) {
for (j = 0; j < item.contents.length; j++) {
var entry = item.contents[j];
if (typeof entry === 'string') {
var selector = new Selector(entry);
contents.push({selector: selector, do: Do.RECURSE, reorder: false});
continue;
} else if (typeof entry !== 'object' || entry === null) {
reject('not a SpecContentEntry object', j);
} else if (typeof entry.path !== 'string') {
reject('.path not a vaid selector string', j);
} else if (!Do.hasOwnProperty(entry.do)) {
reject('.do: ' + entry.do + ' is not a valid Do value', j);
} else if (typeof entry.reorder !== 'boolean' &&
entry.reorder !== undefined) {
reject('.reorder must be boolean or omitted', j);
}
contents.push({
selector: new Selector(entry.path),
do: Do[entry.do],
reorder: Boolean(entry.reorder),
});
}
} else if (!item.rest) {
reject('must specify one of .contents or .rest');
}
config.push({
filename: item.filename,
header: header,
headerSubs: headerSubs,
prune: prune,
pruneRest: pruneRest,
contents: contents, // Possibly empty.
rest: Boolean(item.rest),
});
} else if ('options' in item) { // It's a SpecOptionsItem.
if (typeof item.options !== 'object') {
reject('.options is not a DumperOptions object');
// TODO(cpcallen): additional type checks?
}
config.push({options: item.options});
} else {
reject('is neither a SpecFileItem nor a SpecOptionsItem');
}
}
return config;
};
/**
* A synchronous writable stream, with an API that is a simplified
* subset of stream.Writable.
* @constructor
* @struct
* @implements Writable
* @param {string} filename The file to write to.
*/
var SyncWriter = function(filename) {
/** @type {number|null} */
this.fd = fs.openSync(filename, 'w', 0o600);
};
/**
* Write string to file.
* @override
* @param {string} s String to write.
* @returns {void}
*/
SyncWriter.prototype.write = function(s) {
if (this.fd === null) throw Error('stream already ended');
fs.writeSync(this.fd, s);
};
/**
* Close file.
* @returns {void}
*/
SyncWriter.prototype.end = function() {
if (this.fd === null) throw Error('stream already ended');
fs.closeSync(this.fd);
this.fd = null;
};
///////////////////////////////////////////////////////////////////////////////
// Data types used to specify a dump configuration.
///////////////////////////////////////////////////////////////////////////////
// For internal use; strict types:
/**
* A processed-and-ready-to-use configuration entry.
* @typedef {!OptionsItem|!FileItem}
*/
var ConfigItem;
/**
* A processed-and-ready-to-use configuration entry setting general
* options.
* @typedef {{copyright: (string|!Array|undefined),
* options: !DumperOptions}}
*/
var OptionsItem;
/**
* A processed-and-ready-to-use configuration entry for a single
* output file.
* @typedef {{filename: string,
* header: (string|undefined),
* headerSubs: !Object,
* prune: !Array,
* pruneRest: !Array,
* contents: !Array,
* rest: boolean}}
*/
var FileItem;
/**
* The type of the values of .contents entries of a ConfigEntry.
*
* - selector: is a Selector identifying the variable or property
* binding this entry applies to.
*
* - do: is a Do value speciifying how much of selector to dump.
*
* - reorder: is a boolean specifying whether it is acceptable to
* allow property or set/map entry entries to be created (by the
* output JS) in a different order than they apear in the
* interpreter instance being serialised. If false, output may
* contain placeholder entries like:
*
* var obj = {};
* obj.foo = undefined; // placeholder
* obj.bar = function() { ... };
*
* to allow obj.foo to be defined later while still preserving
* property order.
*
* @typedef {{selector: !Selector,
* do: Do,
* reorder: boolean}}
*/
var ContentEntry = function() {};
//////////////////////////////////////////////////////////////////////
// For dump_spec.json use; loose, JSON-compatible types:
/** @typedef {!SpecOptionsItem|!SpecFileItem} */
var SpecConfigItem;
/**
* An OptionsItem, but with Selectors represented by selector strings.
*
* @typedef {{options: !DumperOptions}}
*/
var SpecOptionsItem;
/**
* A FileItem represented as plain old JavaScript object (i.e., as
* ouptut by JSON.parse):
*
* - Do values are reprsesented by the coresponding strings (e.g.,
* "RECURSE" instead of Do.RECURSE).
* - For convenience, header: and the values of headerSubs: may be an
* arrays of strings, which will be joined with newlines.
* - Contents entries can be just a selector string, which will be
* treated as Do.RECURSE.
*
* @typedef {{filename: string,
* header: (string|!Array|undefined),
* headerSubs: (!Object|string)>|undefined),
* prune: (!Array|undefined),
* pruneRest: (!Array|undefined),
* contents: (!Array|undefined),
* rest: (boolean|undefined)}}
*/
var SpecFileItem;
/**
* Like a ContentEntry, but with a string instead of a Do value.
* @typedef {{path: string,
* do: string,
* reorder: (boolean|undefined)}}
*/
var SpecContentEntry;
///////////////////////////////////////////////////////////////////////////////
// Main program.
///////////////////////////////////////////////////////////////////////////////
if (require.main === module) {
if (process.argv.length < 4) {
console.log(
'usage: dump <.city file>