aboutsummaryrefslogtreecommitdiffstats
path: root/etherpad
diff options
context:
space:
mode:
authorAlexander Sulfrian <alexander@sulfrian.net>2010-06-08 09:01:43 +0200
committerAlexander Sulfrian <alexander@sulfrian.net>2010-06-08 09:01:43 +0200
commitd1fa08fdc9cb11dccee76d668ff85df30458c295 (patch)
tree1d19df6405103577d872902486792e8c23bce711 /etherpad
parentd7c5ad7d6263fd1baf9bfdbaa4c50b70ef2fbdb2 (diff)
parent70d1f9d6fcaefe611e778b8dbf3bafea8934aa08 (diff)
downloadetherpad-d1fa08fdc9cb11dccee76d668ff85df30458c295.tar.gz
etherpad-d1fa08fdc9cb11dccee76d668ff85df30458c295.tar.xz
etherpad-d1fa08fdc9cb11dccee76d668ff85df30458c295.zip
Merge remote branch 'upstream/master'
Conflicts: etherpad/src/etherpad/control/pro/admin/pro_admin_control.js etherpad/src/etherpad/control/pro/pro_main_control.js etherpad/src/etherpad/control/pro_help_control.js etherpad/src/etherpad/globals.js etherpad/src/etherpad/legacy_urls.js etherpad/src/etherpad/pne/pne_utils.js etherpad/src/etherpad/pro/pro_utils.js etherpad/src/main.js etherpad/src/plugins/fileUpload/templates/fileUpload.ejs etherpad/src/plugins/testplugin/templates/page.ejs etherpad/src/static/css/pad2_ejs.css etherpad/src/static/css/pro-help.css etherpad/src/static/img/jun09/pad/protop.gif etherpad/src/static/js/store.js etherpad/src/themes/default/templates/framed/framedheader-pro.ejs etherpad/src/themes/default/templates/main/home.ejs etherpad/src/themes/default/templates/pro-help/main.ejs etherpad/src/themes/default/templates/pro-help/pro-help-template.ejs infrastructure/com.etherpad/licensing.scala trunk/etherpad/src/etherpad/collab/ace/contentcollector.js trunk/etherpad/src/etherpad/collab/ace/linestylefilter.js trunk/etherpad/src/static/css/home-opensource.css trunk/etherpad/src/static/js/ace.js trunk/etherpad/src/static/js/linestylefilter_client.js trunk/etherpad/src/templates/email/eepnet_license_info.ejs trunk/etherpad/src/templates/pad/pad_body2.ejs trunk/etherpad/src/templates/pad/pad_content.ejs trunk/etherpad/src/templates/pad/padfull_body.ejs trunk/etherpad/src/templates/pro/admin/pne-license-manager.ejs
Diffstat (limited to 'etherpad')
-rw-r--r--etherpad/.gitignore9
-rw-r--r--etherpad/bin/.gitignore1
-rw-r--r--etherpad/bin/etherpad.default47
-rwxr-xr-xetherpad/bin/java-version.sh72
-rwxr-xr-xetherpad/bin/rebuildjar.sh161
-rwxr-xr-xetherpad/bin/run-local.sh65
-rwxr-xr-xetherpad/bin/setup-mysql-db.sh25
-rw-r--r--etherpad/etc/etherpad.local.properties.tmpl23
-rw-r--r--etherpad/etc/etherpad.localdev-default.properties23
-rw-r--r--etherpad/lib/dnsjava-2.0.6.jarbin0 -> 268823 bytes
-rw-r--r--etherpad/lib/jbcrypt-0.3.jarbin0 -> 15505 bytes
-rw-r--r--etherpad/lib/jcommon-1.0.15.jarbin0 -> 309294 bytes
-rw-r--r--etherpad/lib/jfreechart-1.0.12.jarbin0 -> 1368681 bytes
-rw-r--r--etherpad/src/etherpad/admin/plugins.js247
-rw-r--r--etherpad/src/etherpad/admin/shell.js127
-rw-r--r--etherpad/src/etherpad/billing/billing.js800
-rw-r--r--etherpad/src/etherpad/billing/fields.js219
-rw-r--r--etherpad/src/etherpad/billing/team_billing.js422
-rw-r--r--etherpad/src/etherpad/collab/collab_server.js778
-rw-r--r--etherpad/src/etherpad/collab/collabroom_server.js359
-rw-r--r--etherpad/src/etherpad/collab/genimg.js55
-rw-r--r--etherpad/src/etherpad/collab/json_sans_eval.js178
-rw-r--r--etherpad/src/etherpad/collab/readonly_server.js174
-rw-r--r--etherpad/src/etherpad/collab/server_utils.js204
-rw-r--r--etherpad/src/etherpad/control/aboutcontrol.js263
-rw-r--r--etherpad/src/etherpad/control/admin/pluginmanager.js71
-rw-r--r--etherpad/src/etherpad/control/admincontrol.js1482
-rw-r--r--etherpad/src/etherpad/control/blogcontrol.js199
-rw-r--r--etherpad/src/etherpad/control/connection_diagnostics_control.js87
-rw-r--r--etherpad/src/etherpad/control/global_pro_account_control.js143
-rw-r--r--etherpad/src/etherpad/control/historycontrol.js226
-rw-r--r--etherpad/src/etherpad/control/loadtestcontrol.js93
-rw-r--r--etherpad/src/etherpad/control/maincontrol.js54
-rw-r--r--etherpad/src/etherpad/control/pad/pad_changeset_control.js280
-rw-r--r--etherpad/src/etherpad/control/pad/pad_control.js754
-rw-r--r--etherpad/src/etherpad/control/pad/pad_importexport_control.js319
-rw-r--r--etherpad/src/etherpad/control/pad/pad_view_control.js287
-rw-r--r--etherpad/src/etherpad/control/pne_manual_control.js75
-rw-r--r--etherpad/src/etherpad/control/pne_tracker_control.js48
-rw-r--r--etherpad/src/etherpad/control/pro/account_control.js369
-rw-r--r--etherpad/src/etherpad/control/pro/admin/account_manager_control.js260
-rw-r--r--etherpad/src/etherpad/control/pro/admin/license_manager_control.js128
-rw-r--r--etherpad/src/etherpad/control/pro/admin/pro_admin_control.js280
-rw-r--r--etherpad/src/etherpad/control/pro/admin/pro_config_control.js54
-rw-r--r--etherpad/src/etherpad/control/pro/admin/team_billing_control.js447
-rw-r--r--etherpad/src/etherpad/control/pro/pro_main_control.js150
-rw-r--r--etherpad/src/etherpad/control/pro/pro_padlist_control.js200
-rw-r--r--etherpad/src/etherpad/control/pro_beta_control.js136
-rw-r--r--etherpad/src/etherpad/control/pro_signup_control.js173
-rw-r--r--etherpad/src/etherpad/control/scriptcontrol.js75
-rw-r--r--etherpad/src/etherpad/control/static_control.js76
-rw-r--r--etherpad/src/etherpad/control/statscontrol.js1214
-rw-r--r--etherpad/src/etherpad/control/store/eepnet_checkout_control.js757
-rw-r--r--etherpad/src/etherpad/control/store/storecontrol.js201
-rw-r--r--etherpad/src/etherpad/control/testcontrol.js74
-rw-r--r--etherpad/src/etherpad/db_migrations/m0000_test.js23
-rw-r--r--etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js38
-rw-r--r--etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js47
-rw-r--r--etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js29
-rw-r--r--etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js38
-rw-r--r--etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js73
-rw-r--r--etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js29
-rw-r--r--etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js67
-rw-r--r--etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js31
-rw-r--r--etherpad/src/etherpad/db_migrations/m0009_pad_tables.js31
-rw-r--r--etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js71
-rw-r--r--etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js33
-rw-r--r--etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js30
-rw-r--r--etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js54
-rw-r--r--etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js102
-rw-r--r--etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js25
-rw-r--r--etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js35
-rw-r--r--etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js30
-rw-r--r--etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js82
-rw-r--r--etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js24
-rw-r--r--etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js25
-rw-r--r--etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js57
-rw-r--r--etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js30
-rw-r--r--etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js32
-rw-r--r--etherpad/src/etherpad/db_migrations/m0024_statistics_table.js42
-rw-r--r--etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js26
-rw-r--r--etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js37
-rw-r--r--etherpad/src/etherpad/db_migrations/m0027_pro_config.js27
-rw-r--r--etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js29
-rw-r--r--etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js31
-rw-r--r--etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js26
-rw-r--r--etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js24
-rw-r--r--etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js39
-rw-r--r--etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js30
-rw-r--r--etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js42
-rw-r--r--etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js28
-rw-r--r--etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js45
-rw-r--r--etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js32
-rw-r--r--etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js26
-rw-r--r--etherpad/src/etherpad/db_migrations/m0040_create_plugin_tables.js40
-rw-r--r--etherpad/src/etherpad/db_migrations/migration_runner.js148
-rw-r--r--etherpad/src/etherpad/debug.js26
-rw-r--r--etherpad/src/etherpad/globals.js49
-rw-r--r--etherpad/src/etherpad/helpers.js306
-rw-r--r--etherpad/src/etherpad/importexport/importexport.js241
-rw-r--r--etherpad/src/etherpad/legacy_urls.js37
-rw-r--r--etherpad/src/etherpad/licensing.js163
-rw-r--r--etherpad/src/etherpad/log.js255
-rw-r--r--etherpad/src/etherpad/metrics/metrics.js438
-rw-r--r--etherpad/src/etherpad/pad/activepads.js52
-rw-r--r--etherpad/src/etherpad/pad/chatarchive.js67
-rw-r--r--etherpad/src/etherpad/pad/dbwriter.js338
-rw-r--r--etherpad/src/etherpad/pad/easysync2migration.js675
-rw-r--r--etherpad/src/etherpad/pad/exporthtml.js383
-rw-r--r--etherpad/src/etherpad/pad/importhtml.js230
-rw-r--r--etherpad/src/etherpad/pad/model.js655
-rw-r--r--etherpad/src/etherpad/pad/noprowatcher.js110
-rw-r--r--etherpad/src/etherpad/pad/pad_migrations.js206
-rw-r--r--etherpad/src/etherpad/pad/pad_security.js237
-rw-r--r--etherpad/src/etherpad/pad/padevents.js170
-rw-r--r--etherpad/src/etherpad/pad/padusers.js397
-rw-r--r--etherpad/src/etherpad/pad/padutils.js191
-rw-r--r--etherpad/src/etherpad/pad/revisions.js103
-rw-r--r--etherpad/src/etherpad/pne/pne_utils.js149
-rw-r--r--etherpad/src/etherpad/pro/domains.js141
-rw-r--r--etherpad/src/etherpad/pro/pro_account_auto_signin.js101
-rw-r--r--etherpad/src/etherpad/pro/pro_accounts.js592
-rw-r--r--etherpad/src/etherpad/pro/pro_config.js92
-rw-r--r--etherpad/src/etherpad/pro/pro_ldap_support.js217
-rw-r--r--etherpad/src/etherpad/pro/pro_pad_db.js232
-rw-r--r--etherpad/src/etherpad/pro/pro_pad_editors.js104
-rw-r--r--etherpad/src/etherpad/pro/pro_padlist.js289
-rw-r--r--etherpad/src/etherpad/pro/pro_padmeta.js111
-rw-r--r--etherpad/src/etherpad/pro/pro_quotas.js141
-rw-r--r--etherpad/src/etherpad/pro/pro_utils.js169
-rw-r--r--etherpad/src/etherpad/quotas.js50
-rw-r--r--etherpad/src/etherpad/sessions.js203
-rw-r--r--etherpad/src/etherpad/statistics/exceptions.js231
-rw-r--r--etherpad/src/etherpad/statistics/statistics.js1248
-rw-r--r--etherpad/src/etherpad/store/checkout.js300
-rw-r--r--etherpad/src/etherpad/store/eepnet_checkout.js101
-rw-r--r--etherpad/src/etherpad/store/eepnet_trial.js241
-rw-r--r--etherpad/src/etherpad/testing/testutils.js23
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0000_test.js22
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js48
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js89
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js42
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js214
-rw-r--r--etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js22
-rw-r--r--etherpad/src/etherpad/usage_stats/usage_stats.js162
-rw-r--r--etherpad/src/etherpad/utils.js464
-rw-r--r--etherpad/src/main.js434
-rw-r--r--etherpad/src/plugins/fileUpload/controllers/fileUpload.js87
-rw-r--r--etherpad/src/plugins/fileUpload/hooks.js11
-rw-r--r--etherpad/src/plugins/fileUpload/main.js19
-rw-r--r--etherpad/src/plugins/fileUpload/models.js95
-rw-r--r--etherpad/src/plugins/fileUpload/templates/fileUpload.ejs32
-rw-r--r--etherpad/src/plugins/fileUpload/templates/fileUploaded.ejs5
-rw-r--r--etherpad/src/plugins/kafoo/main.js16
-rw-r--r--etherpad/src/plugins/testplugin/controllers/testplugin.js58
-rw-r--r--etherpad/src/plugins/testplugin/hooks.js15
-rw-r--r--etherpad/src/plugins/testplugin/main.js23
-rw-r--r--etherpad/src/plugins/testplugin/static/js/main.js11
-rw-r--r--etherpad/src/plugins/testplugin/static/js/test.js1
-rw-r--r--etherpad/src/plugins/testplugin/templates/page.ejs23
-rw-r--r--etherpad/src/plugins/testplugin/templates/testplugin.ejs33
-rw-r--r--etherpad/src/plugins/twitterStyleTags/controllers/tagBrowser.js103
-rw-r--r--etherpad/src/plugins/twitterStyleTags/hooks.js56
-rw-r--r--etherpad/src/plugins/twitterStyleTags/main.js45
-rw-r--r--etherpad/src/plugins/twitterStyleTags/models/tagQuery.js227
-rw-r--r--etherpad/src/plugins/twitterStyleTags/static/css/pad.css70
-rw-r--r--etherpad/src/plugins/twitterStyleTags/static/css/tagBrowser.css90
-rw-r--r--etherpad/src/plugins/twitterStyleTags/static/js/main.js48
-rw-r--r--etherpad/src/plugins/twitterStyleTags/templates/tagBrowser.ejs115
-rw-r--r--etherpad/src/plugins/twitterStyleTags/templates/tagRss.ejs69
-rw-r--r--etherpad/src/plugins/urlIndexer/controllers/urlBrowser.js132
-rw-r--r--etherpad/src/plugins/urlIndexer/hooks.js49
-rw-r--r--etherpad/src/plugins/urlIndexer/main.js34
-rw-r--r--etherpad/src/plugins/urlIndexer/templates/urlBrowser.ejs53
-rw-r--r--etherpad/src/static/crossdomain.xml12
-rw-r--r--etherpad/src/static/css/admin/admin-stats.css183
-rw-r--r--etherpad/src/static/css/admin/pluginmanager.css62
-rw-r--r--etherpad/src/static/css/broadcast.css386
-rw-r--r--etherpad/src/static/css/etherpad.css770
-rw-r--r--etherpad/src/static/css/framedpage.css175
-rw-r--r--etherpad/src/static/css/global-pro-account.css52
-rw-r--r--etherpad/src/static/css/home-opensource.css40
-rw-r--r--etherpad/src/static/css/lib/jquery.contextmenu.css244
-rw-r--r--etherpad/src/static/css/pad2_ejs.css910
-rw-r--r--etherpad/src/static/css/pro-signup.css69
-rw-r--r--etherpad/src/static/css/pro/account.css254
-rw-r--r--etherpad/src/static/css/pro/framedpage-pro.css125
-rw-r--r--etherpad/src/static/css/pro/padlist.css115
-rw-r--r--etherpad/src/static/css/pro/pro-admin.css343
-rw-r--r--etherpad/src/static/css/pro/pro-home.css65
-rw-r--r--etherpad/src/static/favicon.icobin0 -> 1354 bytes
-rw-r--r--etherpad/src/static/img/davy/bg/home-createpad.pngbin0 -> 4327 bytes
-rw-r--r--etherpad/src/static/img/davy/bg/product.pngbin0 -> 161 bytes
-rw-r--r--etherpad/src/static/img/davy/btn/createpad-small.gifbin0 -> 3148 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/backgrad.gifbin0 -> 697 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/colorpicker.gifbin0 -> 2020 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/connectingbar.gifbin0 -> 10819 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/docpaneledge2.pngbin0 -> 635 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/docpanelmiddle2.pngbin0 -> 295 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_background.gifbin0 -> 181 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_background_left.gifbin0 -> 204 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_background_right.gifbin0 -> 867 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_bold.gifbin0 -> 224 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_clearauthorship.gifbin0 -> 397 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_groupleft.gifbin0 -> 186 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_groupright.gifbin0 -> 185 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_indent.gifbin0 -> 99 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_insertunorderedlist.gifbin0 -> 147 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_italic.gifbin0 -> 201 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_outdent.gifbin0 -> 99 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_redo.gifbin0 -> 232 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_save.gifbin0 -> 139 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_strikethrough.gifbin0 -> 336 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_underline.gifbin0 -> 223 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbar_undo.gifbin0 -> 230 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/editbarback.gifbin0 -> 368 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/feedbackbox2.gifbin0 -> 6262 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/fileicons.gifbin0 -> 1397 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/hdraggie.gifbin0 -> 453 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/icon_import_export.gifbin0 -> 96 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/icon_pad_options.gifbin0 -> 67 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/icon_saved_revisions.gifbin0 -> 81 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/icon_security.gifbin0 -> 87 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/icon_time_slider.gifbin0 -> 74 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/inviteshare.gifbin0 -> 511 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/inviteshare2.gifbin0 -> 1836 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/layoutbuttons.gifbin0 -> 3750 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/ok_or_cancel.gifbin0 -> 1630 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/overlay2.pngbin0 -> 149 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/padtop5.gifbin0 -> 3872 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/padtop5.pngbin0 -> 6604 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/padtop5.xcfbin0 -> 44819 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/padtopback2.gifbin0 -> 372 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/public.gifbin0 -> 1034 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/roundcorner_left.gifbin0 -> 123 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/roundcorner_right.gifbin0 -> 131 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/roundcorner_right_orange.gifbin0 -> 171 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/savedrevarrows.gifbin0 -> 866 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/savedrevsgfx2.gifbin0 -> 1904 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/sharebox4.gifbin0 -> 5788 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/sharedistri.gifbin0 -> 85 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/syncdone.gifbin0 -> 211 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/syncing.gifbin0 -> 673 bytes
-rw-r--r--etherpad/src/static/img/jun09/pad/viewbargfx.gifbin0 -> 155 bytes
-rw-r--r--etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-cyan-menu-item-hover.gifbin0 -> 52 bytes
-rw-r--r--etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-menu-item-hover.gifbin0 -> 52 bytes
-rw-r--r--etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-semitransparent-menu-item-hover.pngbin0 -> 2837 bytes
-rw-r--r--etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-human-menu-item-hover.gifbin0 -> 195 bytes
-rw-r--r--etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-osx-menu-item-hover.gifbin0 -> 87 bytes
-rw-r--r--etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-bg.gifbin0 -> 64 bytes
-rw-r--r--etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-menu-item-hover.gifbin0 -> 347 bytes
-rw-r--r--etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-xp-bg.gifbin0 -> 223 bytes
-rw-r--r--etherpad/src/static/img/may09/doc.gifbin0 -> 632 bytes
-rw-r--r--etherpad/src/static/img/may09/html.gifbin0 -> 1040 bytes
-rw-r--r--etherpad/src/static/img/may09/pdf.gifbin0 -> 398 bytes
-rw-r--r--etherpad/src/static/img/may09/txt.gifbin0 -> 381 bytes
-rw-r--r--etherpad/src/static/img/misc/status-ball.gifbin0 -> 1553 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/crushed_button_depressed.pngbin0 -> 4134 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/crushed_button_undepressed.pngbin0 -> 4166 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/crushed_current_location.pngbin0 -> 1009 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/crushed_timeslider_mockup.pngbin0 -> 8164 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/current_location.pngbin0 -> 1100 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/pause.pngbin0 -> 2883 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/play.pngbin0 -> 3017 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/play_button.pngbin0 -> 4867 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/star.pngbin0 -> 3241 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/star_selected.pngbin0 -> 3242 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/stepper_buttons.pngbin0 -> 4858 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/timeslider_background.pngbin0 -> 915 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/timeslider_left.pngbin0 -> 1653 bytes
-rw-r--r--etherpad/src/static/img/pad/timeslider/timeslider_right.pngbin0 -> 1581 bytes
-rw-r--r--etherpad/src/static/img/pro/box/blue-boxtop.gifbin0 -> 523 bytes
-rw-r--r--etherpad/src/static/img/pro/buttons/bluebutton120.gifbin0 -> 951 bytes
-rw-r--r--etherpad/src/static/img/pro/header/pro-header-logo.pngbin0 -> 5527 bytes
-rw-r--r--etherpad/src/static/img/pro/header/pro-header-plustopnav-back.gifbin0 -> 474 bytes
-rw-r--r--etherpad/src/static/img/pro/padlist/gear-drop.gifbin0 -> 300 bytes
-rw-r--r--etherpad/src/static/img/pro/padlist/paper-icon.gifbin0 -> 619 bytes
-rw-r--r--etherpad/src/static/img/pro/padlist/trash-icon.gifbin0 -> 1080 bytes
-rw-r--r--etherpad/src/static/img/pro/topnav/pro-topnav-back.gifbin0 -> 137 bytes
-rw-r--r--etherpad/src/static/img/pro/topnav/pro-topnav-notch.gifbin0 -> 92 bytes
-rw-r--r--etherpad/src/static/js/billing.js111
-rw-r--r--etherpad/src/static/js/billing_shared.js94
-rw-r--r--etherpad/src/static/js/broadcast.js610
-rw-r--r--etherpad/src/static/js/broadcast_revisions.js119
-rw-r--r--etherpad/src/static/js/broadcast_slider.js401
-rw-r--r--etherpad/src/static/js/collab_client.js628
-rw-r--r--etherpad/src/static/js/confirmation.js21
-rw-r--r--etherpad/src/static/js/connection_diagnostics.js126
-rw-r--r--etherpad/src/static/js/draggable.js60
-rw-r--r--etherpad/src/static/js/etherpad.js217
-rw-r--r--etherpad/src/static/js/jquery-1.2.6.js3549
-rw-r--r--etherpad/src/static/js/jquery-1.3.2.js4376
-rw-r--r--etherpad/src/static/js/json2.js498
-rw-r--r--etherpad/src/static/js/lib/jquery.contextmenu.js284
-rw-r--r--etherpad/src/static/js/pad2.js591
-rw-r--r--etherpad/src/static/js/pad_chat.js295
-rw-r--r--etherpad/src/static/js/pad_connectionstatus.js63
-rw-r--r--etherpad/src/static/js/pad_cookie.js101
-rw-r--r--etherpad/src/static/js/pad_docbar.js347
-rw-r--r--etherpad/src/static/js/pad_editbar.js107
-rw-r--r--etherpad/src/static/js/pad_editor.js136
-rw-r--r--etherpad/src/static/js/pad_impexp.js187
-rw-r--r--etherpad/src/static/js/pad_modals.js364
-rw-r--r--etherpad/src/static/js/pad_savedrevs.js408
-rw-r--r--etherpad/src/static/js/pad_userlist.js604
-rw-r--r--etherpad/src/static/js/pad_utils.js359
-rw-r--r--etherpad/src/static/js/plugins.js22
-rw-r--r--etherpad/src/static/js/pricing.js19
-rw-r--r--etherpad/src/static/js/pro/guest-knock-client.js53
-rw-r--r--etherpad/src/static/js/pro/pro-padlist-client.js104
-rw-r--r--etherpad/src/static/js/pro/signin-client.js27
-rw-r--r--etherpad/src/static/js/pulse.jquery.js105
-rw-r--r--etherpad/src/static/js/statpage.js143
-rw-r--r--etherpad/src/static/js/store.js116
-rw-r--r--etherpad/src/static/js/swfobject.js24
-rw-r--r--etherpad/src/static/js/timeslider.js663
-rw-r--r--etherpad/src/static/js/undo-xpopup.js25
-rw-r--r--etherpad/src/static/robots.txt1
-rw-r--r--etherpad/src/templates/pad/exporthtml.ejs28
-rw-r--r--etherpad/src/templates/pro/admin/pro-config.ejs55
-rw-r--r--etherpad/src/themes/default/templates/500_body.ejs26
-rw-r--r--etherpad/src/themes/default/templates/admin/pluginmanager.ejs74
-rw-r--r--etherpad/src/themes/default/templates/email/padinvite.ejs18
-rw-r--r--etherpad/src/themes/default/templates/framed/framedfooter.ejs13
-rw-r--r--etherpad/src/themes/default/templates/framed/framedheader-pro.ejs78
-rw-r--r--etherpad/src/themes/default/templates/framed/framedheader.ejs13
-rw-r--r--etherpad/src/themes/default/templates/framed/framedpage-pro.ejs31
-rw-r--r--etherpad/src/themes/default/templates/framed/framedpage.ejs37
-rw-r--r--etherpad/src/themes/default/templates/html.ejs43
-rw-r--r--etherpad/src/themes/default/templates/main/home.ejs62
-rw-r--r--etherpad/src/themes/default/templates/main/pro_signup_body.ejs71
-rw-r--r--etherpad/src/themes/default/templates/misc/pad_default.ejs16
-rw-r--r--etherpad/src/themes/default/templates/notice.ejs16
-rw-r--r--etherpad/src/themes/default/templates/pad/create_body.ejs26
-rw-r--r--etherpad/src/themes/default/templates/pad/pad_body2.ejs505
-rw-r--r--etherpad/src/themes/default/templates/pad/pad_iphone_body.ejs29
-rw-r--r--etherpad/src/themes/default/templates/pad/padview_body.ejs141
-rw-r--r--etherpad/src/themes/default/templates/page.ejs135
-rw-r--r--etherpad/src/themes/default/templates/pro-account/recover.ejs48
-rw-r--r--etherpad/src/themes/default/templates/pro-account/sign-in.ejs57
-rw-r--r--etherpad/src/themes/default/templates/pro/account/account-welcome-email.ejs32
-rw-r--r--etherpad/src/themes/default/templates/pro/account/forgot-password-email.ejs22
-rw-r--r--etherpad/src/themes/default/templates/pro/account/forgot-password.ejs66
-rw-r--r--etherpad/src/themes/default/templates/pro/account/my-account.ejs67
-rw-r--r--etherpad/src/themes/default/templates/pro/account/signin.ejs81
-rw-r--r--etherpad/src/themes/default/templates/pro/admin/account-manager.ejs59
-rw-r--r--etherpad/src/themes/default/templates/pro/admin/admin-template.ejs31
-rw-r--r--etherpad/src/themes/default/templates/pro/admin/delete-account.ejs35
-rw-r--r--etherpad/src/themes/default/templates/pro/admin/manage-account.ejs64
-rw-r--r--etherpad/src/themes/default/templates/pro/admin/new-account.ejs86
-rw-r--r--etherpad/src/themes/default/templates/pro/padlist/pro-padlist.ejs49
-rw-r--r--etherpad/src/themes/default/templates/pro/pro_home.ejs103
352 files changed, 49733 insertions, 0 deletions
diff --git a/etherpad/.gitignore b/etherpad/.gitignore
new file mode 100644
index 0000000..5f41ae6
--- /dev/null
+++ b/etherpad/.gitignore
@@ -0,0 +1,9 @@
+data/
+build
+derby.log
+local
+*.swp
+appjet-eth*.jar
+etherpad-pne-*.jar
+
+
diff --git a/etherpad/bin/.gitignore b/etherpad/bin/.gitignore
new file mode 100644
index 0000000..00fd678
--- /dev/null
+++ b/etherpad/bin/.gitignore
@@ -0,0 +1 @@
+run-david.sh
diff --git a/etherpad/bin/etherpad.default b/etherpad/bin/etherpad.default
new file mode 100644
index 0000000..dcceea9
--- /dev/null
+++ b/etherpad/bin/etherpad.default
@@ -0,0 +1,47 @@
+# Default settings for etherpad. This file is sourced by /bin/sh from
+# /etc/init.d/etherpad.
+
+# User and group to run as
+ETHERPAD_USER="etherpad"
+ETHERPAD_GROUP="etherpad"
+
+# Setup paths
+ETHERPAD_HOME=/usr/local/etherpad
+JAVA=/usr/bin/java
+JAVA_HOME=/usr/lib/jvm/java-6-openjdk
+SCALA=/usr/bin/scala
+SCALA_HOME=/usr/share/java
+MYSQL_CONNECTOR_JAR=/usr/share/java/mysql-connector-java.jar
+
+# Maximum amount of RAM to allocate
+MXRAM="1G"
+
+# Classpath
+CP="$ETHERPAD_HOME/etherpad/appjet-eth-dev.jar:$ETHERPAD_HOME/etherpad/data"
+for f in "$ETHERPAD_HOME"/etherpad/lib/*.jar; do
+ CP="$CP:$f"
+done
+
+# Java options
+JAVA_OPTS=""
+
+# Config file
+CFGFILE=/etc/etherpad/local.properties
+
+# Default options
+ETHERPAD_OPTS="-classpath $CP \
+ -server \
+ -Xmx${MXRAM} \
+ -Xms${MXRAM} \
+ -Djava.awt.headless=true \
+ -XX:MaxGCPauseMillis=500 \
+ -XX:+UseConcMarkSweepGC \
+ -XX:+CMSIncrementalMode \
+ -XX:CMSIncrementalSafetyFactor=50 \
+ -XX:+PrintGCDetails \
+ -XX:+PrintGCTimeStamps \
+ -Xloggc:$ETHERPAD_HOME/etherpad/data/logs/backend/jvm-gc.log \
+ -Dappjet.jmxremote=true \
+ $JAVA_OPTS \
+ net.appjet.oui.main \
+ --configFile=$CFGFILE"
diff --git a/etherpad/bin/java-version.sh b/etherpad/bin/java-version.sh
new file mode 100755
index 0000000..639920b
--- /dev/null
+++ b/etherpad/bin/java-version.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+# This script attempts to find an existing installation of Java that meets a minimum version
+# requirement on a Linux machine. If it is successful, it will export a JAVA_HOME environment
+# variable that can be used by another calling script.
+#
+# To specify the required version, set the REQUIRED_VERSION to the major version required,
+# e.g. 1.3, but not 1.3.1.
+REQUIRED_VERSION=1.6
+
+# Transform the required version string into a number that can be used in comparisons
+REQUIRED_VERSION=`echo $REQUIRED_VERSION | sed -e 's;\.;0;g'`
+# Check JAVA_HOME directory to see if Java version is adequate
+if [ $JAVA_HOME ]
+then
+ JAVA_EXE=$JAVA_HOME/bin/java
+ $JAVA_EXE -version 2> tmp.ver
+ VERSION=`cat tmp.ver | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
+ echo $VERSION
+ rm tmp.ver
+ VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
+ if [ $VERSION ]
+ then
+ if [ $VERSION -ge $REQUIRED_VERSION ]
+ then
+ JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
+ else
+ JAVA_HOME=
+ fi
+ else
+ JAVA_HOME=
+ fi
+fi
+
+# If the existing JAVA_HOME directory is adequate, then leave it alone
+# otherwise, use 'locate' to search for other possible java candidates and
+# check their versions.
+if [ $JAVA_HOME ]
+then
+ :
+else
+ for JAVA_EXE in `locate bin/java | grep java$ | xargs echo`
+ do
+ if [ $JAVA_HOME ]
+ then
+ :
+ else
+ $JAVA_EXE -version 2> tmp.ver 1> /dev/null
+ VERSION=`cat tmp.ver | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
+ rm tmp.ver
+ VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
+ if [ $VERSION ]
+ then
+ if [ $VERSION -ge $REQUIRED_VERSION ]
+ then
+ JAVA_HOME=`echo $JAVA_EXE`
+ else
+ echo "JAVA Version too old - Please install a new Java version"
+ fi
+ fi
+ fi
+ done
+fi
+
+# If the correct Java version is detected, then export the JAVA_HOME environment variable
+if [ $JAVA_HOME ]
+then
+ `export JAVA_HOME="$JAVA_HOME"`
+ export JAVA_HOME
+ #echo $JAVA_HOME
+fi
+
+
diff --git a/etherpad/bin/rebuildjar.sh b/etherpad/bin/rebuildjar.sh
new file mode 100755
index 0000000..d32d994
--- /dev/null
+++ b/etherpad/bin/rebuildjar.sh
@@ -0,0 +1,161 @@
+#!/bin/bash -e
+
+# Copyright 2009 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+bin/java-version.sh
+
+if [ -z "$JAR" ]; then
+ if [ ! -z $(which fastjar 2>/dev/null) ]; then
+ # http://lists.gnu.org/archive/html/fastjar-dev/2009-12/msg00000.html
+ version=`fastjar --version | grep fastjar | sed 's/.* //g'`
+ if [[ "$version" = "0.97" || "$version" = "0.98" ]]; then
+ echo "fastjar version $version can't build EtherPad. Falling back to standard jar."
+ JAR=jar
+ else
+ JAR=fastjar
+ fi
+ else
+ JAR=jar
+ fi
+fi
+
+[ -z "$JAVA_HOME" ] && read -p "\$JAVA_HOME is not set, please enter the path to your Java installation: " JAVA_HOME
+if [ ! -e "$JAVA_HOME" ]; then
+ echo "The path to \$JAVA_HOME ($JAVA_HOME) does not exist, please check and try again."
+ exit 1
+else
+ export JAVA_HOME
+fi
+
+[ -z "$SCALA_HOME" ] && read -p "\$SCALA_HOME is not set, please enter the path to your Scala installation: " SCALA_HOME
+if [ ! -e "$SCALA_HOME" ]; then
+ echo "The path to \$SCALA_HOME ($SCALA_HOME) does not exist, please check and try again."
+ exit 1
+else
+ export SCALA_HOME
+fi
+
+if [ -z "$SCALA" ]; then
+ if [ `which scala 2>/dev/null 1>/dev/null` ]; then
+ SCALA=`which scala`
+ echo "Using 'scala' binary found at $SCALA. Set \$SCALA to use another one."
+ elif [ -x "$SCALA_HOME/bin/scala" ]; then
+ SCALA="$SCALA_HOME/bin/scala"
+ echo "Using 'scala' binary found at $SCALA. Set \$SCALA to use another one."
+ else
+ read -p "\$SCALA is not set and the 'scala' binary could not be found, please enter the path to the file: " SCALA
+ fi
+fi
+if [ ! -x "$SCALA" ]; then
+ echo "The path to \$SCALA ($SCALA) is not an executable file, please check and try again."
+ exit 1
+else
+ export SCALA
+fi
+
+if [ -z "$JAVA" ]; then
+ if [ `which java 2>/dev/null 1>/dev/null` ]; then
+ JAVA=`which java`
+ echo "Using 'java' binary found at $JAVA. Set \$JAVA to use another one."
+ elif [ -x "$JAVA_HOME/bin/java" ]; then
+ JAVA="$JAVA_HOME/bin/java"
+ echo "Using 'java' binary found at $JAVA. Set \$JAVA to use another one."
+ else
+ read -p "\$JAVA is not set and the 'java' binary could not be found, please enter the path to the file: " JAVA
+ fi
+fi
+if [ ! -x "$JAVA" ]; then
+ echo "The path to \$JAVA ($JAVA) is not an executeable file, please check and try again."
+ exit 1
+else
+ export JAVA
+fi
+
+[ -z "$MYSQL_CONNECTOR_JAR" ] && read -p "\$MYSQL_CONNECTOR_JAR is not set, please enter the path to the MySQL JDBC driver .jar file: " MYSQL_CONNECTOR_JAR
+if [ ! -e "$MYSQL_CONNECTOR_JAR" ]; then
+ echo "The path to \$MYSQL_CONNECTOR_JAR ($MYSQL_CONNECTOR_JAR) does not exist, please check and try again."
+ exit 1
+else
+ export MYSQL_CONNECTOR_JAR
+fi
+
+# Check for javac version. Unfortunately, javac doesn't tell you whether
+# it's Sun Java or OpenJDK, but the "java" binary that's in the same
+# directory will.
+if [ -e "$JAVA_HOME/bin/java" ]; then
+ ($JAVA_HOME/bin/java -version 2>&1) | {
+ while read file; do
+ javaver=$file
+ done
+ for word in $javaver; do
+ if [ $word != "Java" ]; then
+ echo "$JAVA_HOME/bin/java is from a non-Sun compiler, and may not be able to compile EtherPad. If you get syntax errors, you should point \$JAVA_HOME at a Sun Java JDK installation instead."
+ fi
+ break
+ done
+ }
+fi
+
+function notify {
+ if [ ! -z $(which growlnotify 2>/dev/null) ]; then
+ echo $0 finished | growlnotify
+ fi
+}
+trap notify EXIT
+
+source ../infrastructure/bin/compilecache.sh
+
+suffix="-dev";
+if [ "$1" == "prod" ]; then
+ suffix="";
+ shift;
+fi
+
+OWD=`pwd`
+cd ../infrastructure
+JAR=$JAR bin/makejar.sh $@
+
+rm -rf build/etherpad-jars
+mkdir -p build/etherpad-jars
+
+echo "including etherpad JARs..."
+
+JARFILES="echo ../etherpad/lib/*.jar"
+function genjar {
+ echo "unzipping JARs..."
+ pushd $1 >> /dev/null
+
+ for a in ../../../etherpad/lib/*.jar; do
+ $JAR xf $a
+ rm -rf META-INF/{MANIFEST.MF,NOTICE{,.txt},LICENSE{,.txt},INDEX.LIST,SUN_MICR.{RSA,SF},maven}
+ done
+
+ popd >> /dev/null
+}
+cacheonfiles JAR-etherpad "$JARFILES" genjar 1
+
+echo "updating..."
+
+pushd buildcache/JAR-etherpad >> /dev/null
+$JAR uf ../../build/appjet.jar `ls . | grep -v "^t$"`
+
+echo "done."
+
+popd >> /dev/null
+
+dst="$OWD/appjet-eth$suffix.jar"
+cp -f build/appjet.jar $dst
+cd $OWD
+echo "wrote $dst"
diff --git a/etherpad/bin/run-local.sh b/etherpad/bin/run-local.sh
new file mode 100755
index 0000000..a559fce
--- /dev/null
+++ b/etherpad/bin/run-local.sh
@@ -0,0 +1,65 @@
+#!/bin/bash -e
+
+# Copyright 2009 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+mkdir -p data/appjet
+
+MXRAM="1G"
+if [ ! -z $1 ]; then
+ if [ ! '-' = `echo $1 | head -c 1` ]; then
+ MXRAM="$1";
+ shift;
+ fi
+fi
+
+CP="appjet-eth-dev.jar:data"
+for f in lib/*.jar; do
+ CP="$CP:$f"
+done
+
+if [ -z "$JAVA" ]; then
+ JAVA=java
+fi
+
+# etherpad properties file
+cfg_file=./etc/etherpad.local.properties
+if [ ! -f $cfg_file ]; then
+ cfg_file=./etc/etherpad.localdev-default.properties
+fi
+if [[ $1 == "--cfg" ]]; then
+ cfg_file=${2}
+ shift;
+ shift;
+fi
+
+echo "Using config file: ${cfg_file}"
+
+exec $JAVA -classpath $CP \
+ -server \
+ -Xmx${MXRAM} \
+ -Xms${MXRAM} \
+ -Djava.awt.headless=true \
+ -XX:MaxGCPauseMillis=500 \
+ -XX:+UseConcMarkSweepGC \
+ -XX:+CMSIncrementalMode \
+ -XX:CMSIncrementalSafetyFactor=50 \
+ -XX:+PrintGCDetails \
+ -XX:+PrintGCTimeStamps \
+ -Xloggc:./data/logs/backend/jvm-gc.log \
+ -Dappjet.jmxremote=true \
+ $JAVA_OPTS \
+ net.appjet.oui.main \
+ --configFile=${cfg_file} \
+ "$@"
diff --git a/etherpad/bin/setup-mysql-db.sh b/etherpad/bin/setup-mysql-db.sh
new file mode 100755
index 0000000..45901ac
--- /dev/null
+++ b/etherpad/bin/setup-mysql-db.sh
@@ -0,0 +1,25 @@
+#!/bin/bash -e
+
+# Copyright 2009 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+db="etherpad"
+
+echo "Creating etherpad ${db}..."
+echo "create database ${db};" | ${mysql} -u root -p
+
+echo "Granting priviliges..."
+echo "grant all privileges on ${db}.* to 'etherpad'@'localhost' identified by 'password';" | ${mysql} -u root -p
+
+echo "Success"
diff --git a/etherpad/etc/etherpad.local.properties.tmpl b/etherpad/etc/etherpad.local.properties.tmpl
new file mode 100644
index 0000000..d9bb899
--- /dev/null
+++ b/etherpad/etc/etherpad.local.properties.tmpl
@@ -0,0 +1,23 @@
+alwaysHttps = false
+ajstdlibHome = ../infrastructure/framework-src/modules
+appjetHome = ./data/appjet
+devMode = false
+etherpad.adminPass = __db_admin_password__
+etherpad.fakeProduction = false
+etherpad.isProduction = true
+etherpad.proAccounts = true
+etherpad.SQL_JDBC_DRIVER = com.mysql.jdbc.Driver
+etherpad.SQL_JDBC_URL = jdbc:mysql://__dbc_dbserver__:__dbc_dbport__/__dbc_dbname__
+etherpad.SQL_PASSWORD = __dbc_dbpass__
+etherpad.SQL_USERNAME = __dbc_dbuser__
+hidePorts = false
+listen = 9000
+logDir = /var/log/etherpad
+modulePath = ./src
+motdPage = /ep/pad/view/ro.3PfHCD0ApLc/latest?fullScreen=1&slider=0&sidebar=0
+topdomains = __db_topdomains__,localhost,localhost.localdomain
+transportPrefix = /comet
+transportUseWildcardSubdomains = true
+useHttpsUrls = false
+useVirtualFileRoot = ./src
+theme = default
diff --git a/etherpad/etc/etherpad.localdev-default.properties b/etherpad/etc/etherpad.localdev-default.properties
new file mode 100644
index 0000000..374101f
--- /dev/null
+++ b/etherpad/etc/etherpad.localdev-default.properties
@@ -0,0 +1,23 @@
+alwaysHttps = false
+ajstdlibHome = ../infrastructure/framework-src/modules
+appjetHome = ./data/appjet
+devMode = true
+etherpad.adminPass = password
+etherpad.fakeProduction = false
+etherpad.isProduction = false
+etherpad.proAccounts = true
+etherpad.SQL_JDBC_DRIVER = com.mysql.jdbc.Driver
+etherpad.SQL_JDBC_URL = jdbc:mysql://localhost:3306/etherpad
+etherpad.SQL_PASSWORD = password
+etherpad.SQL_USERNAME = etherpad
+hidePorts = false
+listen = 9000
+logDir = ./data/logs
+modulePath = ./src
+motdPage = /ep/pad/view/ro.3PfHCD0ApLc/latest?fullScreen=1&slider=0&sidebar=0
+topdomains = localhost,localbox.info
+transportPrefix = /comet
+transportUseWildcardSubdomains = true
+useHttpsUrls = false
+useVirtualFileRoot = ./src
+theme = default
diff --git a/etherpad/lib/dnsjava-2.0.6.jar b/etherpad/lib/dnsjava-2.0.6.jar
new file mode 100644
index 0000000..e41f9b0
--- /dev/null
+++ b/etherpad/lib/dnsjava-2.0.6.jar
Binary files differ
diff --git a/etherpad/lib/jbcrypt-0.3.jar b/etherpad/lib/jbcrypt-0.3.jar
new file mode 100644
index 0000000..04fbbb3
--- /dev/null
+++ b/etherpad/lib/jbcrypt-0.3.jar
Binary files differ
diff --git a/etherpad/lib/jcommon-1.0.15.jar b/etherpad/lib/jcommon-1.0.15.jar
new file mode 100644
index 0000000..d0dc26d
--- /dev/null
+++ b/etherpad/lib/jcommon-1.0.15.jar
Binary files differ
diff --git a/etherpad/lib/jfreechart-1.0.12.jar b/etherpad/lib/jfreechart-1.0.12.jar
new file mode 100644
index 0000000..73be90f
--- /dev/null
+++ b/etherpad/lib/jfreechart-1.0.12.jar
Binary files differ
diff --git a/etherpad/src/etherpad/admin/plugins.js b/etherpad/src/etherpad/admin/plugins.js
new file mode 100644
index 0000000..385e2ca
--- /dev/null
+++ b/etherpad/src/etherpad/admin/plugins.js
@@ -0,0 +1,247 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ *
+ * 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("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.collab.server_utils");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padusers");
+import("etherpad.pro.pro_utils");
+import("etherpad.helpers");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("exceptionutils");
+import("execution");
+
+jimport("java.io.File",
+ "java.io.DataInputStream",
+ "java.io.FileInputStream",
+ "java.lang.Byte",
+ "java.io.FileReader",
+ "java.io.BufferedReader",
+ "net.appjet.oui.JarVirtualFile");
+
+pluginsLoaded = false;
+pluginModules = {};
+plugins = {};
+hooks = {};
+clientHooks = {};
+
+function loadAvailablePlugin(pluginName) {
+ if (plugins[pluginName] != undefined)
+ return plugins[pluginName];
+
+ var pluginsDir = new Packages.java.io.File("src/plugins");
+
+ var pluginFile = new Packages.java.io.File(pluginsDir, pluginName + '/main.js');
+ if (pluginFile.exists()) {
+ var pluginModulePath = pluginFile.getPath().replace(new RegExp("src/\(.*\)\.js"), "$1").replace("/", ".", "g");
+ var importStmt = "import('" + pluginModulePath + "')";
+ try {
+ var res = execution.fancyAssEval(importStmt, "main;");
+ res = new res.init();
+ return res;
+ } catch (e) {
+ log.info({errorLoadingPlugin:exceptionutils.getStackTracePlain(e)});
+ }
+ }
+ return null;
+}
+
+function loadAvailablePlugins() {
+ var pluginsDir = new Packages.java.io.File("src/plugins");
+
+ var pluginNames = pluginsDir.list();
+
+ for (i = 0; i < pluginNames.length; i++) {
+ var plugin = loadAvailablePlugin(pluginNames[i]);
+ if (plugin != null)
+ pluginModules[pluginNames[i]] = plugin
+ }
+}
+
+function loadPluginHooks(pluginName) {
+ function registerHookNames(hookSet, type) {
+ return function (hook) {
+ var row = {hook:hook, type:type, plugin:pluginName};
+ if (hookSet[hook] == undefined) hookSet[hook] = [];
+ hookSet[hook].push(row);
+ return row;
+ }
+ }
+ plugins[pluginName] = pluginModules[pluginName].hooks.map(registerHookNames(hooks, 'server'));
+ if (pluginModules[pluginName].client != undefined && pluginModules[pluginName].client.hooks != undefined)
+ plugins[pluginName] = plugins[pluginName].concat(pluginModules[pluginName].client.hooks.map(registerHookNames(clientHooks, 'client')));
+}
+
+function unloadPluginHooks(pluginName) {
+ for (var hookSet in [hooks, clientHooks])
+ for (var hookName in hookSet) {
+ var hook = hookSet[hookName];
+ for (i = hook.length - 1; i >= 0; i--)
+ if (hook[i].plugin == pluginName)
+ hook.splice(i, 1);
+ }
+ delete plugins[pluginName];
+}
+
+function loadInstalledHooks() {
+ var sql = '' +
+ 'select ' +
+ ' hook.name as hook, ' +
+ ' hook_type.name as type, ' +
+ ' plugin.name as plugin, ' +
+ ' plugin_hook.original_name as original ' +
+ 'from ' +
+ ' plugin ' +
+ ' left outer join plugin_hook on ' +
+ ' plugin.id = plugin_hook.plugin_id ' +
+ ' left outer join hook on ' +
+ ' plugin_hook.hook_id = hook.id ' +
+ ' left outer join hook_type on ' +
+ ' hook.type_id = hook_type.id ' +
+ 'order by hook.name, plugin.name';
+
+ var rows = sqlobj.executeRaw(sql, {});
+ for (var i = 0; i < rows.length; i++) {
+ var row = rows[i];
+
+ if (plugins[row.plugin] == undefined)
+ plugins[row.plugin] = [];
+ plugins[row.plugin].push(row);
+
+ var hookSet;
+
+ if (row.type == 'server')
+ hookSet = hooks;
+ else if (row.type == 'client')
+ hookSet = clientHooks;
+
+ if (hookSet[row.hook] == undefined)
+ hookSet[row.hook] = [];
+ if (row.hook != 'null')
+ hookSet[row.hook].push(row);
+ }
+}
+
+function selectOrInsert(table, columns) {
+ var res = sqlobj.selectSingle(table, columns);
+ if (res !== null)
+ return res;
+ sqlobj.insert(table, columns);
+ return sqlobj.selectSingle(table, columns);
+}
+
+function saveInstalledHooks(pluginName) {
+ var plugin = sqlobj.selectSingle('plugin', {name:pluginName});
+
+ if (plugin !== null) {
+ sqlobj.deleteRows('plugin_hook', {plugin_id:plugin.id});
+ if (plugins[pluginName] === undefined)
+ sqlobj.deleteRows('plugin', {name:pluginName});
+ }
+
+ if (plugins[pluginName] !== undefined) {
+ if (plugin === null)
+ plugin = selectOrInsert('plugin', {name:pluginName});
+
+ for (var i = 0; i < plugins[pluginName].length; i++) {
+ var row = plugins[pluginName][i];
+
+ var hook_type = selectOrInsert('hook_type', {name:row.type});
+ var hook = selectOrInsert('hook', {name:row.hook, type_id:hook_type.id});
+
+ sqlobj.insert("plugin_hook", {plugin_id:plugin.id, hook_id:hook.id});
+ }
+ }
+}
+
+
+function loadPlugins(force) {
+ if (pluginsLoaded && force == undefined) return;
+ pluginsLoaded = true;
+ loadAvailablePlugins();
+ loadInstalledHooks();
+}
+
+
+/* User API */
+function enablePlugin(pluginName) {
+ loadPlugins();
+ loadPluginHooks(pluginName);
+ saveInstalledHooks(pluginName);
+ try {
+ pluginModules[pluginName].install();
+ } catch (e) {
+ unloadPluginHooks(pluginName);
+ saveInstalledHooks(pluginName);
+ throw e;
+ }
+}
+
+function disablePlugin(pluginName) {
+ loadPlugins();
+ try {
+ pluginModules[pluginName].uninstall();
+ } catch (e) {
+ log.info({errorUninstallingPlugin:exceptionutils.getStackTracePlain(e)});
+ }
+ unloadPluginHooks(pluginName);
+ saveInstalledHooks(pluginName);
+}
+
+function registerClientHandlerJS() {
+ loadPlugins();
+ for (pluginName in plugins) {
+ var plugin = pluginModules[pluginName];
+ if (plugin.client !== undefined) {
+ helpers.includeJs("plugins/" + pluginName + "/main.js");
+ if (plugin.client.modules != undefined)
+ for (j = 0; j < client.modules.length; j++)
+ helpers.includeJs("plugins/" + pluginName + "/" + plugin.client.modules[j] + ".js");
+ }
+ }
+ helpers.addClientVars({hooks:clientHooks});
+ helpers.includeJs("plugins.js");
+}
+
+function callHook(hookName, args) {
+ loadPlugins();
+ if (hooks[hookName] === undefined)
+ return [];
+ var res = [];
+
+ for (var i = 0; i < hooks[hookName].length; i++) {
+ var plugin = hooks[hookName][i];
+ var pluginRes = pluginModules[plugin.plugin][plugin.original || hookName](args);
+ if (pluginRes != undefined && pluginRes != null)
+ for (var j = 0; j < pluginRes.length; j++)
+ res.push(pluginRes[j]); /* Don't use Array.concat as it flatterns arrays within the array */
+ }
+ return res;
+}
+
+function callHookStr(hookName, args, sep, pre, post) {
+ if (sep == undefined) sep = '';
+ if (pre == undefined) pre = '';
+ if (post == undefined) post = '';
+ return callHook(hookName, args).map(function (x) { return pre + x + post}).join(sep || "");
+}
diff --git a/etherpad/src/etherpad/admin/shell.js b/etherpad/src/etherpad/admin/shell.js
new file mode 100644
index 0000000..391d524
--- /dev/null
+++ b/etherpad/src/etherpad/admin/shell.js
@@ -0,0 +1,127 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("jsutils.cmp");
+import("jsutils.eachProperty");
+import("exceptionutils");
+import("execution");
+import("stringutils.trim");
+
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+
+function _splitCommand(cmd) {
+ var parts = [[], []];
+ var importing = true;
+ cmd.split("\n").forEach(function(l) {
+ if ((trim(l).length > 0) &&
+ (trim(l).indexOf("import") != 0)) {
+ importing = false;
+ }
+
+ if (importing) {
+ parts[0].push(l);
+ } else {
+ parts[1].push(l);
+ }
+ });
+
+ parts[0] = parts[0].join("\n");
+ parts[1] = parts[1].join("\n");
+ return parts;
+}
+
+function getResult(cmd) {
+ var resultString = (function() {
+ try {
+ var parts = _splitCommand(cmd);
+ result = execution.fancyAssEval(parts[0], parts[1]);
+ } catch (e) {
+ // if (e instanceof JavaException) {
+ // e = new net.appjet.bodylock.JSRuntimeException(e.getMessage(), e.javaException);
+ // }
+ if (appjet.config.devMode) {
+ (e.javaException || e.rhinoException || e).printStackTrace();
+ }
+ result = exceptionutils.getStackTracePlain(e);
+ }
+ var resultString;
+ try {
+ resultString = ((result && result.toString) ? result.toString() : String(result));
+ } catch (ex) {
+ resultString = "Error converting result to string: "+ex.toString();
+ }
+ return resultString;
+ })();
+ return resultString;
+}
+
+function _renderCommandShell() {
+ // run command if necessary
+ if (request.params.cmd) {
+ var cmd = request.params.cmd;
+ var resultString = getResult(cmd);
+
+ getSession().shellCommand = cmd;
+ getSession().shellResult = resultString;
+ response.redirect(request.path+(request.query?'?'+request.query:''));
+ }
+
+ var div = DIV({style: "padding: 4px; margin: 4px; background: #eee; "
+ + "border: 1px solid #338"});
+ // command div
+ var oldCmd = getSession().shellCommand || "";
+ var commandDiv = DIV({style: "width: 100%; margin: 4px 0;"});
+ commandDiv.push(FORM({style: "width: 100%;",
+ method: "POST", action: request.path + (request.query?'?'+request.query:'')},
+ TEXTAREA({name: "cmd",
+ style: "border: 1px solid #555;"
+ + "width: 100%; height: 160px; font-family: monospace;"},
+ html(oldCmd)),
+ INPUT({type: "submit"})));
+
+ // result div
+ var resultDiv = DIV({style: ""});
+ var isResult = getSession().shellResult != null;
+ if (isResult) {
+ resultDiv.push(DIV(
+ PRE({style: 'border: 1px solid #555; font-family: monospace; margin: 4px 0; padding: 4px;'},
+ getSession().shellResult)));
+ delete getSession().shellResult;
+ resultDiv.push(DIV({style: "text-align: right;"},
+ A({href: qpath({})}, "clear")));
+ } else {
+ resultDiv.push(P("result will go here"));
+ }
+
+ var t = TABLE({border: 0, cellspacing: 0, cellpadding: 0, width: "100%",
+ style: "width: 100%;"});
+ t.push(TR(TH({width: "49%", align: "left"}, " Command:"),
+ TH({width: "49%", align: "left"}, " "+(isResult ? "Result:" : ""))),
+ TR(TD({valign: "top", style: 'padding: 4px;'}, commandDiv),
+ TD({valign: "top", style: 'padding: 4px;'}, resultDiv)));
+ div.push(t);
+ return div;
+}
+
+function handleRequest() {
+ var body = BODY();
+ body.push(A({href: '/ep/admin/'}, html("&laquo; Admin")));
+ body.push(BR(), BR());
+ body.push(_renderCommandShell());
+ response.write(HTML(body));
+}
diff --git a/etherpad/src/etherpad/billing/billing.js b/etherpad/src/etherpad/billing/billing.js
new file mode 100644
index 0000000..444c233
--- /dev/null
+++ b/etherpad/src/etherpad/billing/billing.js
@@ -0,0 +1,800 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("dateutils.*");
+import("fastJSON");
+import("jsutils.eachProperty");
+import("netutils.urlPost");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("stringutils.{md5,repeat}");
+
+import("etherpad.log.{custom=>eplog}");
+
+
+jimport("java.lang.System.out.println");
+
+function clearKeys(obj, keys) {
+ var newObj = {};
+ eachProperty(obj, function(k, v) {
+ var isCopied = false;
+ keys.forEach(function(key) {
+ if (k == key.name &&
+ key.valueTest(v)) {
+ newObj[k] = key.valueReplace(v);
+ isCopied = true;
+ }
+ });
+ if (! isCopied) {
+ if (typeof(obj[k]) == 'object') {
+ newObj[k] = clearKeys(v, keys);
+ } else {
+ newObj[k] = v;
+ }
+ }
+ });
+ return newObj;
+}
+
+function replaceWithX(s) {
+ return repeat("X", s.length);
+}
+
+function log(obj) {
+ eplog('billing', clearKeys(obj, [
+ {name: "ACCT",
+ valueTest: function(s) { return /^\d{15,16}$/.test(s) },
+ valueReplace: replaceWithX},
+ {name: "CVV2",
+ valueTest: function(s) { return /^\d{3,4}$/.test(s) },
+ valueReplace: replaceWithX}]));
+}
+
+var _USER = function() { return appjet.config['etherpad.paypal.user'] || "zamfir_1239051855_biz_api1.gmail.com"; }
+var _PWD = function() { return appjet.config['etherpad.paypal.pwd'] || "1239051867"; }
+var _SIGNATURE = function() { return appjet.config['etherpad.paypal.signature'] || "AQU0e5vuZCvSg-XJploSa.sGUDlpAwAy5fz.FhtfOQ25Qa9sFLDt7Bmp"; }
+var _RECEIVER = function() { return appjet.config['etherpad.paypal.receiver'] || "zamfir_1239051855_biz@gmail.com"; }
+var _paypalApiUrl = function() { return appjet.config['etherpad.paypal.apiUrl'] || "https://api-3t.sandbox.paypal.com/nvp"; }
+var _paypalWebUrl = function() { return appjet.config['etherpad.paypal.webUrl'] || "https://www.sandbox.paypal.com/cgi-bin/webscr"; }
+function paypalPurchaseUrl(token) {
+ return (appjet.config['etherpad.paypal.purchaseUrl'] || "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=")+token;
+}
+
+function getPurchase(id) {
+ return sqlobj.selectSingle('billing_purchase', {id: id});
+}
+
+function getPurchaseForCustomer(customerId) {
+ return sqlobj.selectSingle('billing_purchase', {customer: customerId});
+}
+
+function updatePurchase(id, fields) {
+ sqlobj.updateSingle('billing_purchase', {id: id}, fields);
+}
+
+function getInvoicesForPurchase(purchaseId) {
+ return sqlobj.selectMulti('billing_invoice', {purchase: purchaseId});
+}
+
+function getInvoice(id) {
+ return sqlobj.selectSingle('billing_invoice', {id: id});
+}
+
+function createInvoice() {
+ return _newInvoice();
+}
+
+function updateInvoice(id, fields) {
+ sqlobj.updateSingle('billing_invoice', {id: id}, fields)
+}
+
+function getTransaction(id) {
+ return sqlobj.selectSingle('billing_transaction', {id: id});
+}
+function getTransactionByExternalId(txnId) {
+ return sqlobj.selectSingle('billing_transaction', {txnId: txnId});
+}
+
+function getTransactionsForCustomer(customerId) {
+ return sqlobj.selectMulti('billing_transaction', {customer: customerId});
+}
+
+function getPendingTransactionsForCustomer(customerId) {
+ return sqlobj.selectMulti('billing_transaction', {customer: customerId, status: 'pending'});
+}
+
+function _updateTransaction(id, fields) {
+ return sqlobj.updateSingle('billing_transaction', {id: id}, fields);
+}
+
+function getAdjustments(invoiceId) {
+ return sqlobj.selectMulti('billing_adjustment', {invoice: invoiceId});
+}
+
+function createSubscription(customer, product, dollars, couponCode) {
+ var purchaseId = _newPurchase(customer, product, dollarsToCents(dollars), couponCode);
+ _purchaseActive(purchaseId);
+ updatePurchase(purchaseId, {type: 'subscription', paidThrough: nextMonth(noon(new Date))});
+ return purchaseId;
+}
+
+function _newPurchase(customer, product, cents, couponCode) {
+ var purchaseId = sqlobj.insert('billing_purchase', {
+ customer: customer,
+ product: product,
+ cost: cents,
+ coupon: couponCode,
+ status: 'inactive'
+ });
+ return purchaseId;
+}
+
+function _newInvoice() {
+ var invoiceId = sqlobj.insert('billing_invoice', {
+ time: new Date(),
+ purchase: -1,
+ amt: 0,
+ status: 'pending'
+ });
+ return invoiceId;
+}
+
+function _newTransaction(customer, cents) {
+ var transactionId = sqlobj.insert('billing_transaction', {
+ customer: customer,
+ time: new Date(),
+ amt: cents,
+ status: 'new'
+ });
+ return transactionId;
+}
+
+function _newAdjustment(transaction, invoice, cents) {
+ sqlobj.insert('billing_adjustment', {
+ transaction: transaction,
+ invoice: invoice,
+ time: new Date(),
+ amt: cents
+ });
+}
+
+function _transactionSuccess(transaction, txnId, payInfo) {
+ _updateTransaction(transaction, {
+ status: 'success', txnId: txnId, time: new Date(), payInfo: payInfo
+ });
+}
+
+function _transactionFailure(transaction, txnId) {
+ _updateTransaction(transaction, {
+ status: 'failure', txnId: txnId, time: new Date()
+ });
+}
+
+function _transactionPending(transaction, txnId) {
+ _updateTransaction(transaction, {
+ status: 'pending', txnId: txnId, time: new Date()
+ });
+}
+
+function _invoicePaid(invoice) {
+ updateInvoice(invoice, {status: 'paid'});
+}
+
+function _purchaseActive(purchase) {
+ updatePurchase(purchase, {status: 'active'});
+}
+
+function _purchaseExtend(purchase, monthCount) {
+ var expiration = getPurchase(purchase).paidThrough;
+ for (var i = monthCount; i > 0; i--) {
+ expiration = nextMonth(expiration);
+ }
+ // paying your invoice always makes you current.
+ if (expiration < new Date) {
+ expiration = nextMonth(new Date);
+ }
+ updatePurchase(purchase, {paidThrough: expiration});
+}
+
+function _doPost(url, body) {
+ try {
+ var ret = urlPost(url, body);
+ } catch (e) {
+ if (e.javaException) {
+ net.appjet.oui.exceptionlog.apply(e.javaException);
+ }
+ return { error: e };
+ }
+ return { value: ret };
+}
+
+function _doPaypalNvpPost(properties0) {
+ return {
+ status: 'failure',
+ errorMessage: "Billing has been discontinued. No new services may be purchased."
+ }
+ // var properties = {
+ // USER: _USER(),
+ // PWD: _PWD(),
+ // SIGNATURE: _SIGNATURE(),
+ // VERSION: "56.0"
+ // }
+ // eachProperty(properties0, function(k, v) {
+ // if (v !== undefined) {
+ // properties[k] = v;
+ // }
+ // })
+ // log({'type': 'api call', 'value': properties});
+ // var ret = _doPost(_paypalApiUrl(), properties);
+ // if (ret.error) {
+ // return {
+ // status: 'failure',
+ // exception: ret.error.javaException || ret.error,
+ // errorMessage: ret.error.message
+ // }
+ // }
+ // ret = ret.value;
+ // var paypalResponse = {};
+ // ret.content.split("&").forEach(function(x) {
+ // var parts = x.split("=");
+ // paypalResponse[decodeURIComponent(parts[0])] =
+ // decodeURIComponent(parts[1]);
+ // })
+ //
+ // var res = paypalResponse;
+ // log(res)
+ // if (res.ACK == "Success" || res.ACK == "SuccessWithWarning") {
+ // return {
+ // status: 'success',
+ // response: res
+ // }
+ // } else {
+ // errors = [];
+ // for (var i = 0; res['L_LONGMESSAGE'+i]; ++i) {
+ // errors.push(res['L_LONGMESSAGE'+i]);
+ // }
+ // return {
+ // status: 'failure',
+ // errorMessage: errors.join(", "),
+ // errorMessages: errors,
+ // response: res
+ // }
+ // }
+}
+
+// status -> 'completion', 'bad', 'redundant', 'possible_fraud'
+function handlePaypalNotification() {
+ var content = (typeof(request.content) == 'string' ? request.content : undefined);
+ if (! content) {
+ return new BillingResult('bad', "no content");
+ }
+ log({'type': 'paypal-notification', 'content': content});
+ var params = {};
+ content.split("&").forEach(function(x) {
+ var parts = x.split("=");
+ params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
+ });
+ var txnId = params.txn_id;
+ var properties = [];
+ for(var i in params) {
+ properties.push(i+" -> "+params[i]);
+ }
+ var debugString = properties.join(", ");
+ log({'type': 'parsed-paypal-notification', 'value': debugString});
+ var transaction = getTransactionByExternalId(txnId);
+ log({'type': 'notification-transaction', 'value': (transaction || {})});
+ if (_RECEIVER() != params.receiver_email) {
+ return new BillingResult('possible_fraud', debugString);
+ }
+ if (params.payment_status == "Completed" && transaction &&
+ (transaction.status == 'pending' || transaction.status == 'new')) {
+ var ret = _doPost(_paypalWebUrl(), "cmd=_notify-validate&"+content);
+ if (ret.error || ret.value.content != "VERIFIED") {
+ return new BillingResult('possible_fraud', debugString);
+ }
+ var invoice = getInvoice(params.invoice);
+ if (invoice.amt != dollarsToCents(params.mc_gross)) {
+ return new BillingResult('possible_fraud', debugString);
+ }
+
+ sqlcommon.inTransaction(function () {
+ _transactionSuccess(transaction.id, txnId, "via eCheck");
+ _invoicePaid(invoice.id);
+ _purchaseActive(invoice.purchase);
+ });
+ var purchase = getPurchase(invoice.purchase);
+ return new BillingResult('completion', debugString, null,
+ new PurchaseInfo(params.custom,
+ invoice.id,
+ transaction.id,
+ params.txn_id,
+ purchase.id,
+ centsToDollars(invoice.amt),
+ purchase.couponCode,
+ purchase.time,
+ undefined));
+ } else {
+ return new BillingResult('redundant', debugString);
+ }
+}
+
+function _expressCheckoutCustom(invoiceId, transactionId) {
+ return md5("zimki_sucks"+invoiceId+transactionId);
+}
+
+function PurchaseInfo(custom, invoiceId, transactionId, paypalId, purchaseId, dollars, couponCode, time, token, description) {
+ this.__defineGetter__("custom", function() { return custom });
+ this.__defineGetter__("invoiceId", function() { return invoiceId });
+ this.__defineGetter__("transactionId", function() { return transactionId });
+ this.__defineGetter__("paypalId", function() { return paypalId });
+ this.__defineGetter__("purchaseId", function() { return purchaseId });
+ this.__defineGetter__("cost", function() { return dollars });
+ this.__defineGetter__("couponCode", function() { return couponCode });
+ this.__defineGetter__("time", function() { return time });
+ this.__defineGetter__("token", function() { return token });
+ this.__defineGetter__("description", function() { return description });
+}
+
+function PayerInfo(paypalResult) {
+ this.__defineGetter__("payerId", function() { return paypalResult.response.PAYERID });
+ this.__defineGetter__("email", function() { return paypalResult.response.EMAIL });
+ this.__defineGetter__("businessName", function() { return paypalResult.response.BUSINESS });
+ this.__defineGetter__("nameSalutation", function() { return paypalResult.response.SALUTATION });
+ this.__defineGetter__("nameFirst", function() { return paypalResult.response.FIRSTNAME });
+ this.__defineGetter__("nameMiddle", function() { return paypalResult.response.MIDDLENAME });
+ this.__defineGetter__("nameLast", function() { return paypalResult.response.LASTNAME });
+}
+
+function BillingResult(status, debug, errorField, purchaseInfo, payerInfo) {
+ this.__defineGetter__("status", function() { return status });
+ this.__defineGetter__("debug", function() { return debug });
+ this.__defineGetter__("errorField", function() { return errorField });
+ this.__defineGetter__("purchaseInfo", function() { return purchaseInfo });
+ this.__defineGetter__("payerInfo", function() { return payerInfo });
+}
+
+function dollarsToCents(dollars) {
+ return Math.round(Number(dollars)*100);
+}
+
+function centsToDollars(cents) {
+ return Math.round(Number(cents)) / 100;
+}
+
+function verifyDollars(dollars) {
+ return Math.round(Number(dollars)*100)/100;
+}
+
+function beginExpressPurchase(invoiceId, customerId, productId, dollars, couponCode, successUrl, failureUrl, notifyUrl, authorizeOnly) {
+ var cents = dollarsToCents(dollars);
+ var time = new Date();
+ var purchaseId;
+ var transactionid;
+ if (! authorizeOnly) {
+ try {
+ sqlcommon.inTransaction(function() {
+ purchaseId = _newPurchase(customerId, productId, cents, couponCode);
+ updateInvoice(invoiceId, {purchase: purchaseId, amt: cents});
+ transactionId = _newTransaction(customerId, cents);
+ _newAdjustment(transactionId, invoiceId, cents);
+ });
+ } catch (e) {
+ if (e instanceof BillingResult) { return e; }
+ throw e;
+ }
+ }
+
+ var paypalResult =
+ _setExpressCheckout(invoiceId, transactionId, cents,
+ successUrl, failureUrl, notifyUrl, authorizeOnly);
+
+ if (paypalResult.status == 'success') {
+ var token = paypalResult.response.TOKEN;
+ return new BillingResult('success', paypalResult, null, new PurchaseInfo(
+ _expressCheckoutCustom(invoiceId, transactionId),
+ invoiceId,
+ transactionId,
+ undefined,
+ purchaseId,
+ verifyDollars(dollars),
+ couponCode,
+ time,
+ token));
+ } else {
+ return new BillingResult('failure', paypalResult);
+ }
+}
+
+function _setExpressCheckout(invoiceId, transactionId, cents, successUrl, failureUrl, notifyUrl, authorizeOnly) {
+ var properties = {
+ INVNUM: invoiceId,
+
+ METHOD: 'SetExpressCheckout',
+ CUSTOM:
+ _expressCheckoutCustom(invoiceId, transactionId),
+ MAXAMT: centsToDollars(cents),
+ RETURNURL: successUrl,
+ CANCELURL: failureUrl,
+ NOTIFYURL: notifyUrl,
+ NOSHIPPING: 1,
+ PAYMENTACTION: (authorizeOnly ? 'Authorization' : 'Sale'),
+
+ AMT: centsToDollars(cents)
+ }
+
+ return _doPaypalNvpPost(properties);
+}
+
+function continueExpressPurchase(purchaseInfo, authorizeOnly) {
+ var paypalResult = _getExpressCheckoutDetails(purchaseInfo.token, authorizeOnly)
+ if (paypalResult.status == 'success') {
+ if (! authorizeOnly) {
+ if (paypalResult.response.INVNUM != purchaseInfo.invoiceId) {
+ return new BillingResult('failure', "invoice id mismatch");
+ }
+ }
+ if (paypalResult.response.CUSTOM !=
+ _expressCheckoutCustom(purchaseInfo.invoiceId, purchaseInfo.transactionId)) {
+ return new BillingResult('failure', "custom mismatch");
+ }
+ return new BillingResult('success', paypalResult, null, null, new PayerInfo(paypalResult));
+ } else {
+ return new BillingResult('failure', paypalResult);
+ }
+}
+
+function _getExpressCheckoutDetails(token, authorizeOnly) {
+ var properties = {
+ METHOD: 'GetExpresscheckoutDetails',
+ TOKEN: token,
+ }
+
+ return _doPaypalNvpPost(properties);
+}
+
+function completeExpressPurchase(purchaseInfo, payerInfo, notifyUrl, authorizeOnly) {
+ var paypalResult = _doExpressCheckoutPayment(purchaseInfo, payerInfo, notifyUrl, authorizeOnly);
+
+ if (paypalResult.status == 'success') {
+ if (paypalResult.response.PAYMENTSTATUS == 'Completed') {
+ if (! authorizeOnly) {
+ sqlcommon.inTransaction(function() {
+ _transactionSuccess(purchaseInfo.transactionId,
+ paypalResult.response.TRANSACTIONID, "via PayPal");
+ _invoicePaid(purchaseInfo.invoiceId);
+ _purchaseActive(purchaseInfo.purchaseId);
+ });
+ }
+ return new BillingResult('success', paypalResult);
+ } else if (paypalResult.response.PAYMENTSTATUS == 'Pending') {
+ if (! authorizeOnly) {
+ sqlcommon.inTransaction(function() {
+ _transactionPending(purchaseInfo.transactionId,
+ paypalResult.response.TRANSACTIONID);
+ });
+ }
+ return new BillingResult('pending', paypalResult);
+ }
+ } else {
+ if (! authorizeOnly) {
+ sqlcommon.inTransaction(function() {
+ _transactionFailure(purchaseInfo.transactionId,
+ (paypalResult.response ?
+ paypalResult.response.TRANSACTIONID || "" :
+ ""));
+ });
+ }
+ return new BillingResult('failure', paypalResult);
+ }
+}
+
+function _doExpressCheckoutPayment(purchaseInfo, payerInfo, notifyUrl, authorizeOnly) {
+ var properties = {
+ METHOD: 'DoExpressCheckoutPayment',
+ TOKEN: purchaseInfo.token,
+ PAYMENTACTION: (authorizeOnly ? 'Authorization' : 'Sale'),
+
+ NOTIFYURL: notifyUrl,
+
+ PAYERID: payerInfo.payerId,
+
+ AMT: verifyDollars(purchaseInfo.cost), // dollars
+ INVNUM: purchaseInfo.invoiceId,
+ CUSTOM:
+ _expressCheckoutCustom(purchaseInfo.invoiceId, purchaseInfo.transactionId)
+ }
+
+ return _doPaypalNvpPost(properties);
+}
+
+// which field has error? and, is it not user-correctable?
+var _directErrorCodes = {
+ '10502': ['cardExpiration'],
+ '10504': ['cardCvv'],
+ '10505': ['addressStreet', true],
+ '10508': ['cardExpiration'],
+ '10510': ['cardType'],
+ '10512': ['nameFirst'],
+ '10513': ['nameLast'],
+ '10519': ['cardNumber'],
+ '10521': ['cardNumber'],
+ '10527': ['cardNumber'],
+ '10534': ['cardNumber', true],
+ '10535': ['cardNumber'],
+ '10536': ['invoiceId', true],
+ '10537': ['addressCountry', true],
+ '10540': ['addressStreet', true],
+ '10541': ['cardNumber', true],
+ '10554': ['address', true],
+ '10555': ['address', true],
+ '10556': ['address', true],
+ '10561': ['address'],
+ '10562': ['cardExpiration'],
+ '10563': ['cardExpiration'],
+ '10565': ['addressCountry'],
+ '10566': ['cardType'],
+ '10571': ['cardCvv'],
+ '10701': ['address'],
+ '10702': ['addressStreet'],
+ '10703': ['addressStreet2'],
+ '10704': ['addressCity'],
+ '10705': ['addressState'],
+ '10706': ['addressZip'],
+ '10707': ['addressCountry'],
+ '10708': ['address'],
+ '10709': ['addressStreet'],
+ '10710': ['addressCity'],
+ '10711': ['addressState'],
+ '10712': ['addressZip'],
+ '10713': ['addressCountry'],
+ '10714': ['address'],
+ '10715': ['addressState'],
+ '10716': ['addressZip'],
+ '10717': ['addressZip'],
+ '10718': ['addressCity,addressState'],
+ '10748': ['cardCvv'],
+ '10752': ['card'],
+ '10756': ['address,card'],
+ '10759': ['cardNumber'],
+ '10762': ['cardCvv'],
+ '11611': function(response) {
+ var avsCode = response.AVSCODE;
+ var cvv2Match = response.CVV2MATCH;
+ var errorFields = [];
+ switch (avsCode) {
+ case 'N': case 'C': case 'A': case 'B':
+ case 'R': case 'S': case 'U': case 'G':
+ case 'I': case 'E':
+ errorFields.push('address');
+ }
+ switch (cvv2Match) {
+ case 'N':
+ errorFields.push('cardCvv');
+ }
+ return [errorFields.join(",")];
+ },
+ '15004': ['cardCvv'],
+ '15005': ['cardNumber'],
+ '15006': ['cardNumber'],
+ '15007': ['cardNumber']
+}
+
+function authorizePurchase(payinfo, notifyUrl) {
+ return directPurchase(undefined, undefined, undefined, 1, undefined, payinfo, notifyUrl, true);
+}
+
+function directPurchase(invoiceId, customerId, productId, dollars, couponCode, payinfo, notifyUrl, authorizeOnly) {
+ var time = new Date();
+ var cents = dollarsToCents(dollars);
+
+ var purchaseId, transactionId;
+
+ if (! authorizeOnly) {
+ try {
+ sqlcommon.inTransaction(function() {
+ purchaseId = _newPurchase(customerId, productId, cents, couponCode);
+ updateInvoice(invoiceId, {purchase: purchaseId, amt: cents});
+ transactionId = _newTransaction(customerId, cents);
+ _newAdjustment(transactionId, invoiceId, cents);
+ });
+ } catch (e) {
+ if (e instanceof BillingResult) { return e; }
+ if (e.javaException || e.rhinoException) {
+ throw e.javaException || e.rhinoException;
+ }
+ throw e;
+ }
+ }
+
+ var paypalResult = _doDirectPurchase(invoiceId, cents, payinfo, notifyUrl, authorizeOnly);
+
+ if (paypalResult.status == 'success') {
+ if (! authorizeOnly) {
+ sqlcommon.inTransaction(function() {
+ _transactionSuccess(transactionId,
+ paypalResult.response.TRANSACTIONID,
+ payinfo.cardType+" ending in "+payinfo.cardNumber.substr(-4));
+ _invoicePaid(invoiceId);
+ _purchaseActive(purchaseId);
+ });
+ }
+ return new BillingResult('success', paypalResult, null, new PurchaseInfo(
+ undefined,
+ invoiceId,
+ transactionId,
+ paypalResult.response.TRANSACTIONID,
+ purchaseId,
+ verifyDollars(dollars),
+ couponCode,
+ time,
+ undefined));
+ } else {
+ if (! authorizeOnly) {
+ sqlcommon.inTransaction(function() {
+ _transactionFailure(transactionId,
+ (paypalResult.response ?
+ paypalResult.response.TRANSACTIONID || "":
+ ""));
+ });
+ }
+ return new BillingResult('failure', paypalResult, _getErrorCodes(paypalResult.response));
+ }
+}
+
+function _getErrorCodes(paypalResponse) {
+ var errorCodes = {userErrors: [], permanentErrors: []};
+ if (! paypalResponse) {
+ return undefined;
+ }
+ for (var i = 0; paypalResponse['L_ERRORCODE'+i]; ++i) {
+ var code = paypalResponse['L_ERRORCODE'+i];
+ var errorField = _directErrorCodes[code];
+ if (typeof(errorField) == 'function') {
+ errorField = errorField(paypalResponse);
+ }
+ if (errorField && errorField[1]) {
+ Array.prototype.push.apply(errorCodes.permanentErrors, errorField[0].split(","));
+ } else if (errorField) {
+ Array.prototype.push.apply(errorCodes.userErrors, errorField[0].split(","));
+ }
+ }
+ return errorCodes;
+}
+
+function _doDirectPurchase(invoiceId, cents, payinfo, notifyUrl, authorizeOnly) {
+ var properties = {
+ INVNUM: invoiceId,
+
+ METHOD: 'DoDirectPayment',
+ PAYMENTACTION: (authorizeOnly ? 'Authorization' : 'Sale'),
+ IPADDRESS: request.clientAddr,
+ NOTIFYURL: notifyUrl,
+
+ CREDITCARDTYPE: payinfo.cardType,
+ ACCT: payinfo.cardNumber,
+ EXPDATE: payinfo.cardExpiration,
+ CVV2: payinfo.cardCvv,
+
+ SALUTATION: payinfo.nameSalutation,
+ FIRSTNAME: payinfo.nameFirst,
+ MIDDLENAME: payinfo.nameMiddle,
+ LASTNAME: payinfo.nameLast,
+ SUFFIX: payinfo.nameSuffix,
+
+ STREET: payinfo.addressStreet,
+ STREET2: payinfo.addressStreet2,
+ CITY: payinfo.addressCity,
+ STATE: payinfo.addressState,
+ COUNTRYCODE: payinfo.addressCountry,
+ ZIP: payinfo.addressZip,
+
+ AMT: centsToDollars(cents)
+ }
+
+ return _doPaypalNvpPost(properties);
+}
+
+// function directAuthorization(payInfo, dollars, notifyUrl) {
+// var paypalResult = _doDirectPurchase(undefined, dollarsToCents(dollars), payInfo, notifyUrl, true);
+// if (paypalResult.status == 'success') {
+// return new BillingResult('success', paypalResult, null, new PurchaseInfo(
+// undefined,
+// undefined,
+// paypalResult.response.TRANSACTIONID,
+// undefined,
+// verifyDollars(dollars),
+// undefined,
+// undefined,
+// undefined));
+// } else {
+// return new BillingResult('failure', paypalResult, _getErrorCodes(paypalResult.response));
+// }
+// }
+
+function asyncRecurringPurchase(invoiceId, purchaseId, oldTransactionId, paymentInfo, dollars, monthCount, notifyUrl) {
+ var time = new Date();
+ var cents = dollarsToCents(dollars);
+
+ var purchase, transactionId;
+
+ try {
+ sqlcommon.inTransaction(function() {
+ // purchaseId = _newPurchase(customerId, productId, cents, couponCode);
+ purchase = getPurchase(purchaseId);
+ updateInvoice(invoiceId, {purchase: purchaseId, amt: cents});
+ transactionId = _newTransaction(purchase.customer, cents);
+ _newAdjustment(transactionId, invoiceId, cents);
+ });
+ } catch (e) {
+ if (e instanceof BillingResult) { return e; }
+ if (e.rhinoException) {
+ throw e.rhinoException;
+ }
+ throw e;
+ }
+
+ // do transaction using previous transaction as template
+ var paypalResult;
+ if (cents == 0) {
+ // can't actually charge nothing, so fake it.
+ paypalResult = { status: 'success', response: { TRANSACTIONID: null }}
+ } else {
+ paypalResult = _doReferenceTransaction(invoiceId, cents, oldTransactionId, notifyUrl);
+ }
+
+ if (paypalResult.status == 'success') {
+ sqlcommon.inTransaction(function() {
+ _transactionSuccess(transactionId,
+ paypalResult.response.TRANSACTIONID,
+ paymentInfo);
+ _invoicePaid(invoiceId);
+ _purchaseActive(purchaseId);
+ _purchaseExtend(purchaseId, monthCount);
+ });
+ return new BillingResult('success', paypalResult, null, new PurchaseInfo(
+ undefined,
+ invoiceId,
+ transactionId,
+ paypalResult.response.TRANSACTIONID,
+ purchaseId,
+ verifyDollars(dollars),
+ undefined,
+ time,
+ undefined));
+ } else {
+ sqlcommon.inTransaction(function() {
+ _transactionFailure(transactionId,
+ (paypalResult.response ?
+ paypalResult.response.TRANSACTIONID || "":
+ ""));
+ });
+ return new BillingResult('failure', paypalResult, _getErrorCodes(paypalResult.response));
+ }
+}
+
+function _doReferenceTransaction(invoiceId, cents, transactionId, notifyUrl) {
+ var properties = {
+ METHOD: 'DoReferenceTransaction',
+ PAYMENTACTION: 'Sale',
+
+ REFERENCEID: transactionId,
+ AMT: centsToDollars(cents),
+ INVNUM: invoiceId
+ }
+
+ return _doPaypalNvpPost(properties);
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/billing/fields.js b/etherpad/src/etherpad/billing/fields.js
new file mode 100644
index 0000000..4a307ac
--- /dev/null
+++ b/etherpad/src/etherpad/billing/fields.js
@@ -0,0 +1,219 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+// Taken from paypal's form
+var countryList = [
+ ["US", "United States"],
+ ["AL", "Albania"],
+ ["DZ", "Algeria"],
+ ["AD", "Andorra"],
+ ["AO", "Angola"],
+ ["AI", "Anguilla"],
+ ["AG", "Antigua and Barbuda"],
+ ["AR", "Argentina"],
+ ["AM", "Armenia"],
+ ["AW", "Aruba"],
+ ["AU", "Australia"],
+ ["AT", "Austria"],
+ ["AZ", "Azerbaijan Republic"],
+ ["BS", "Bahamas"],
+ ["BH", "Bahrain"],
+ ["BB", "Barbados"],
+ ["BE", "Belgium"],
+ ["BZ", "Belize"],
+ ["BJ", "Benin"],
+ ["BM", "Bermuda"],
+ ["BT", "Bhutan"],
+ ["BO", "Bolivia"],
+ ["BA", "Bosnia and Herzegovina"],
+ ["BW", "Botswana"],
+ ["BR", "Brazil"],
+ ["VG", "British Virgin Islands"],
+ ["BN", "Brunei"],
+ ["BG", "Bulgaria"],
+ ["BF", "Burkina Faso"],
+ ["BI", "Burundi"],
+ ["KH", "Cambodia"],
+ ["CA", "Canada"],
+ ["CV", "Cape Verde"],
+ ["KY", "Cayman Islands"],
+ ["TD", "Chad"],
+ ["CL", "Chile"],
+ ["C2", "China"],
+ ["CO", "Colombia"],
+ ["KM", "Comoros"],
+ ["CK", "Cook Islands"],
+ ["CR", "Costa Rica"],
+ ["HR", "Croatia"],
+ ["CY", "Cyprus"],
+ ["CZ", "Czech Republic"],
+ ["CD", "Democratic Republic of the Congo"],
+ ["DK", "Denmark"],
+ ["DJ", "Djibouti"],
+ ["DM", "Dominica"],
+ ["DO", "Dominican Republic"],
+ ["EC", "Ecuador"],
+ ["SV", "El Salvador"],
+ ["ER", "Eritrea"],
+ ["EE", "Estonia"],
+ ["ET", "Ethiopia"],
+ ["FK", "Falkland Islands"],
+ ["FO", "Faroe Islands"],
+ ["FM", "Federated States of Micronesia"],
+ ["FJ", "Fiji"],
+ ["FI", "Finland"],
+ ["FR", "France"],
+ ["GF", "French Guiana"],
+ ["PF", "French Polynesia"],
+ ["GA", "Gabon Republic"],
+ ["GM", "Gambia"],
+ ["DE", "Germany"],
+ ["GI", "Gibraltar"],
+ ["GR", "Greece"],
+ ["GL", "Greenland"],
+ ["GD", "Grenada"],
+ ["GP", "Guadeloupe"],
+ ["GT", "Guatemala"],
+ ["GN", "Guinea"],
+ ["GW", "Guinea Bissau"],
+ ["GY", "Guyana"],
+ ["HN", "Honduras"],
+ ["HK", "Hong Kong"],
+ ["HU", "Hungary"],
+ ["IS", "Iceland"],
+ ["IN", "India"],
+ ["ID", "Indonesia"],
+ ["IE", "Ireland"],
+ ["IL", "Israel"],
+ ["IT", "Italy"],
+ ["JM", "Jamaica"],
+ ["JP", "Japan"],
+ ["JO", "Jordan"],
+ ["KZ", "Kazakhstan"],
+ ["KE", "Kenya"],
+ ["KI", "Kiribati"],
+ ["KW", "Kuwait"],
+ ["KG", "Kyrgyzstan"],
+ ["LA", "Laos"],
+ ["LV", "Latvia"],
+ ["LS", "Lesotho"],
+ ["LI", "Liechtenstein"],
+ ["LT", "Lithuania"],
+ ["LU", "Luxembourg"],
+ ["MG", "Madagascar"],
+ ["MW", "Malawi"],
+ ["MY", "Malaysia"],
+ ["MV", "Maldives"],
+ ["ML", "Mali"],
+ ["MT", "Malta"],
+ ["MH", "Marshall Islands"],
+ ["MQ", "Martinique"],
+ ["MR", "Mauritania"],
+ ["MU", "Mauritius"],
+ ["YT", "Mayotte"],
+ ["MX", "Mexico"],
+ ["MN", "Mongolia"],
+ ["MS", "Montserrat"],
+ ["MA", "Morocco"],
+ ["MZ", "Mozambique"],
+ ["NA", "Namibia"],
+ ["NR", "Nauru"],
+ ["NP", "Nepal"],
+ ["NL", "Netherlands"],
+ ["AN", "Netherlands Antilles"],
+ ["NC", "New Caledonia"],
+ ["NZ", "New Zealand"],
+ ["NI", "Nicaragua"],
+ ["NE", "Niger"],
+ ["NU", "Niue"],
+ ["NF", "Norfolk Island"],
+ ["NO", "Norway"],
+ ["OM", "Oman"],
+ ["PW", "Palau"],
+ ["PA", "Panama"],
+ ["PG", "Papua New Guinea"],
+ ["PE", "Peru"],
+ ["PN", "Pitcairn Islands"],
+ ["PL", "Poland"],
+ ["PT", "Portugal"],
+ ["QA", "Qatar"],
+ ["CG", "Republic of the Congo"],
+ ["RE", "Reunion"],
+ ["RO", "Romania"],
+ ["RU", "Russia"],
+ ["VC", "Saint Vincent and the Grenadines"],
+ ["WS", "Samoa"],
+ ["SM", "San Marino"],
+ ["ST", "São Tomé and Príncipe"],
+ ["SA", "Saudi Arabia"],
+ ["SN", "Senegal"],
+ ["SC", "Seychelles"],
+ ["SL", "Sierra Leone"],
+ ["SG", "Singapore"],
+ ["SK", "Slovakia"],
+ ["SI", "Slovenia"],
+ ["SB", "Solomon Islands"],
+ ["SO", "Somalia"],
+ ["ZA", "South Africa"],
+ ["KR", "South Korea"],
+ ["ES", "Spain"],
+ ["LK", "Sri Lanka"],
+ ["SH", "St. Helena"],
+ ["KN", "St. Kitts and Nevis"],
+ ["LC", "St. Lucia"],
+ ["PM", "St. Pierre and Miquelon"],
+ ["SR", "Suriname"],
+ ["SJ", "Svalbard and Jan Mayen Islands"],
+ ["SZ", "Swaziland"],
+ ["SE", "Sweden"],
+ ["CH", "Switzerland"],
+ ["TW", "Taiwan"],
+ ["TJ", "Tajikistan"],
+ ["TZ", "Tanzania"],
+ ["TH", "Thailand"],
+ ["TG", "Togo"],
+ ["TO", "Tonga"],
+ ["TT", "Trinidad and Tobago"],
+ ["TN", "Tunisia"],
+ ["TR", "Turkey"],
+ ["TM", "Turkmenistan"],
+ ["TC", "Turks and Caicos Islands"],
+ ["TV", "Tuvalu"],
+ ["UG", "Uganda"],
+ ["UA", "Ukraine"],
+ ["AE", "United Arab Emirates"],
+ ["GB", "United Kingdom"],
+ ["UY", "Uruguay"],
+ ["VU", "Vanuatu"],
+ ["VA", "Vatican City State"],
+ ["VE", "Venezuela"],
+ ["VN", "Vietnam"],
+ ["WF", "Wallis and Futuna Islands"],
+ ["YE", "Yemen"],
+ ["ZM", "Zambia"],
+];
+
+var usaStateList = [
+ "", "AK", "AL", "AR", "AZ", "CA", "CO", "CT", "DC", "DE",
+ "FL", "GA", "HI", "IA", "ID", "IL", "IN", "KS", "KY", "LA",
+ "MA", "MD", "ME", "MI", "MN", "MO", "MS", "MT", "NC", "ND",
+ "NE", "NH", "NJ", "NM", "NV", "NY", "OH", "OK", "OR", "PA",
+ "RI", "SC", "SD", "TN", "TX", "UT", "VA", "VT", "WA", "WI",
+ "WV", "WY", "AA", "AE", "AP", "AS", "FM", "GU", "MH", "MP",
+ "PR", "PW", "VI"
+];
+
diff --git a/etherpad/src/etherpad/billing/team_billing.js b/etherpad/src/etherpad/billing/team_billing.js
new file mode 100644
index 0000000..ae8ae8a
--- /dev/null
+++ b/etherpad/src/etherpad/billing/team_billing.js
@@ -0,0 +1,422 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("execution");
+import("exceptionutils");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon.inTransaction");
+
+import("etherpad.billing.billing");
+import("etherpad.globals");
+import("etherpad.log");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_quotas");
+import("etherpad.store.checkout");
+import("etherpad.utils.renderTemplateAsString");
+
+jimport("java.lang.System.out.println");
+
+function recurringBillingNotifyUrl() {
+ return "";
+}
+
+function _billing() {
+ if (! appjet.cache.billing) {
+ appjet.cache.billing = {};
+ }
+ return appjet.cache.billing;
+}
+
+function _lpad(str, width, padDigit) {
+ str = String(str);
+ padDigit = (padDigit === undefined ? ' ' : padDigit);
+ var count = width - str.length;
+ var prepend = []
+ for (var i = 0; i < count; ++i) {
+ prepend.push(padDigit);
+ }
+ return prepend.join("")+str;
+}
+
+// utility functions
+
+function _dayToDateTime(date) {
+ return [date.getFullYear(), _lpad(date.getMonth()+1, 2, '0'), _lpad(date.getDate(), 2, '0')].join("-");
+}
+
+function _createInvoice(subscription) {
+ var maxUsers = getMaxUsers(subscription.customer);
+ var invoice = inTransaction(function() {
+ var invoiceId = billing.createInvoice();
+ billing.updateInvoice(
+ invoiceId,
+ {purchase: subscription.id,
+ amt: billing.dollarsToCents(calculateSubscriptionCost(maxUsers, subscription.coupon)),
+ users: maxUsers});
+ return billing.getInvoice(invoiceId);
+ });
+ if (invoice) {
+ resetMaxUsers(subscription.customer)
+ }
+ return invoice;
+}
+
+function getExpiredSubscriptions(date) {
+ return sqlobj.selectMulti('billing_purchase',
+ {type: 'subscription',
+ status: 'active',
+ paidThrough: ['<', _dayToDateTime(date)]});
+}
+
+function getAllSubscriptions() {
+ return sqlobj.selectMulti('billing_purchase', {type: 'subscription', status: 'active'});
+}
+
+function getSubscriptionForCustomer(customerId) {
+ return sqlobj.selectSingle('billing_purchase',
+ {type: 'subscription',
+ customer: customerId});
+}
+
+function getOrCreateInvoice(subscription) {
+ return inTransaction(function() {
+ var existingInvoice =
+ sqlobj.selectSingle('billing_invoice',
+ {purchase: subscription.id, status: 'pending'});
+ if (existingInvoice) {
+ return existingInvoice;
+ } else {
+ return _createInvoice(subscription);
+ }
+ });
+}
+
+function getLatestPendingInvoice(subscriptionId) {
+ return sqlobj.selectMulti('billing_invoice',
+ {purchase: subscriptionId, status: 'pending'},
+ {orderBy: '-time', limit: 1})[0];
+}
+
+function getLatestPaidInvoice(subscriptionId) {
+ return sqlobj.selectMulti('billing_invoice',
+ {purchase: subscriptionId, status: 'paid'},
+ {orderBy: '-time', limit: 1})[0];
+}
+
+function pendingTransactions(customer) {
+ return billing.getPendingTransactionsForCustomer(customer);
+}
+
+function checkPendingTransactions(transactions) {
+ // XXX: do nothing for now.
+ return transactions.length > 0;
+}
+
+function getRecurringBillingTransactionId(customerId) {
+ return sqlobj.selectSingle('billing_payment_info', {customer: customerId}).transaction;
+}
+
+function getRecurringBillingInfo(customerId) {
+ return sqlobj.selectSingle('billing_payment_info', {customer: customerId});
+}
+
+function clearRecurringBillingInfo(customerId) {
+ return sqlobj.deleteRows('billing_payment_info', {customer: customerId});
+}
+
+function setRecurringBillingInfo(customerId, fullName, email, paymentSummary, expiration, transactionId) {
+ var info = {
+ fullname: fullName,
+ email: email,
+ paymentsummary: paymentSummary,
+ expiration: expiration,
+ transaction: transactionId
+ }
+ inTransaction(function() {
+ if (sqlobj.selectSingle('billing_payment_info', {customer: customerId})) {
+ sqlobj.update('billing_payment_info', {customer: customerId}, info);
+ } else {
+ info.customer = customerId;
+ sqlobj.insert('billing_payment_info', info);
+ }
+ });
+}
+
+function createSubscription(customerId, couponCode) {
+ domainCacheClear(customerId);
+ return inTransaction(function() {
+ return billing.createSubscription(customerId, 'ONDEMAND', 0, couponCode);
+ });
+}
+
+function updateSubscriptionCouponCode(subscriptionId, couponCode) {
+ billing.updatePurchase(subscriptionId, {coupon: couponCode || ""});
+}
+
+function subscriptionChargeFailure(subscription, invoice, failureMessage) {
+ billing.updatePurchase(subscription.id,
+ {error: failureMessage, status: 'inactive'});
+ sendFailureEmail(subscription, invoice);
+}
+
+function subscriptionChargeSuccess(subscription, invoice) {
+ sendReceiptEmail(subscription, invoice);
+}
+
+function errorFieldsToMessage(errorCodes) {
+ var prefix = "Your payment information was rejected. Please verify your ";
+ var errorList = (errorCodes.permanentErrors ? errorCodes.permanentErrors : errorCodes.userErrors);
+
+ return prefix +
+ errorList.map(function(field) {
+ return checkout.billingCartFieldMap[field].d;
+ }).join(", ")+
+ "."
+}
+
+function getAllInvoices(customer) {
+ var purchase = getSubscriptionForCustomer(customer);
+ if (! purchase) {
+ return [];
+ }
+ return billing.getInvoicesForPurchase(purchase.id);
+}
+
+// scheduled charges
+
+function attemptCharge(invoice, subscription) {
+ var billingInfo = getRecurringBillingInfo(subscription.customer);
+ if (! billingInfo) {
+ subscriptionChargeFailure(subscription, invoice, "No billing information on file.");
+ return false;
+ }
+
+ var result =
+ billing.asyncRecurringPurchase(
+ invoice.id,
+ subscription.id,
+ billingInfo.transaction,
+ billingInfo.paymentsummary,
+ billing.centsToDollars(invoice.amt),
+ 1, // 1 month only for now
+ recurringBillingNotifyUrl);
+ if (result.status == 'success') {
+ subscriptionChargeSuccess(subscription, invoice);
+ return true;
+ } else {
+ subscriptionChargeFailure(subscription, invoice, errorFieldsToMessage(result.errorField));
+ return false;
+ }
+}
+
+function processSubscription(subscription) {
+ try {
+ var hasPendingTransactions = inTransaction(function() {
+ var transactions = pendingTransactions(subscription.customer);
+ if (checkPendingTransactions(transactions)) {
+ billing.log({type: 'pending-transactions-delay', subscription: subscription, transactions: transactions});
+ // there are actual pending transactions. wait until tomorrow.
+ return true;
+ } else {
+ return false;
+ }
+ });
+ if (hasPendingTransactions) {
+ return;
+ }
+ var invoice = getOrCreateInvoice(subscription);
+
+ return attemptCharge(invoice, subscription);
+ } catch (e) {
+ log.logException(e);
+ billing.log({message: "Thrown error",
+ exception: exceptionutils.getStackTracePlain(e),
+ subscription: subscription});
+ subscriptionChargeFailure(subscription, "Permanent failure. Please confirm your billing information.");
+ } finally {
+ domainCacheClear(subscription.customer);
+ }
+}
+
+function processAllSubscriptions() {
+ var subs = getExpiredSubscriptions(new Date);
+ println("processing "+subs.length+" subscriptions.");
+ subs.forEach(processSubscription);
+}
+
+function _scheduleNextDailyUpdate() {
+ // Run at 2:22am every day
+ var now = +(new Date);
+ var tomorrow = new Date(now + 1000*60*60*24);
+ tomorrow.setHours(2);
+ tomorrow.setMinutes(22);
+ tomorrow.setMilliseconds(222);
+ log.info("Scheduling next daily billing update for: "+tomorrow.toString());
+ var delay = +tomorrow - (+(new Date));
+ execution.scheduleTask('billing', "billingDailyUpdate", delay, []);
+}
+
+serverhandlers.tasks.billingDailyUpdate = function() {
+ return; // do nothing, there's no more billing.
+ // if (! globals.isProduction()) { return; }
+ // try {
+ // processAllSubscriptions();
+ // } finally {
+ // _scheduleNextDailyUpdate();
+ // }
+}
+
+function onStartup() {
+ execution.initTaskThreadPool("billing", 1);
+ _scheduleNextDailyUpdate();
+}
+
+// pricing
+
+function getMaxUsers(customer) {
+ return pro_quotas.getAccountUsageCount(customer);
+}
+
+function resetMaxUsers(customer) {
+ pro_quotas.resetAccountUsageCount(customer);
+}
+
+var COST_PER_USER = 8;
+
+function getCouponValue(couponCode) {
+ if (couponCode && couponCode.length == 8) {
+ return sqlobj.selectSingle('checkout_pro_referral', {id: couponCode});
+ }
+}
+
+function calculateSubscriptionCost(users, couponId) {
+ if (users <= globals.PRO_FREE_ACCOUNTS) {
+ return 0;
+ }
+ var coupon = getCouponValue(couponId);
+ var pctDiscount = (coupon ? coupon.pctDiscount : 0);
+ var freeUsers = (coupon ? coupon.freeUsers : 0);
+
+ var cost = (users - freeUsers) * COST_PER_USER;
+ cost = cost * (100-pctDiscount)/100;
+
+ return Math.max(0, cost);
+}
+
+// currentDomainsCache
+
+function _cache() {
+ if (! appjet.cache.currentDomainsCache) {
+ appjet.cache.currentDomainsCache = {};
+ }
+ return appjet.cache.currentDomainsCache;
+}
+
+function domainCacheClear(domain) {
+ delete _cache()[domain];
+}
+
+function _domainCacheGetOrUpdate(domain, f) {
+ if (domain in _cache()) {
+ return _cache()[domain];
+ }
+
+ _cache()[domain] = f();
+ return _cache()[domain];
+}
+
+// external API helpers
+
+function _getPaidThroughDate(domainId) {
+ return _domainCacheGetOrUpdate(domainId, function() {
+ var subscription = getSubscriptionForCustomer(domainId);
+ if (! subscription) {
+ return null;
+ } else {
+ return subscription.paidThrough;
+ }
+ });
+}
+
+// external API
+
+var GRACE_PERIOD_DAYS = 10;
+
+var CURRENT = 0;
+var PAST_DUE = 1;
+var SUSPENDED = 2;
+var NO_BILLING_INFO = 3;
+
+function getDomainStatus(domainId) {
+ var paidThrough = _getPaidThroughDate(domainId);
+
+ if (paidThrough == null) {
+ return NO_BILLING_INFO;
+ }
+ if (paidThrough.getTime() > new Date(Date.now()-86400*1000)) {
+ return CURRENT;
+ }
+ // less than GRACE_PERIOD_DAYS have passed since paidThrough date
+ if (paidThrough.getTime() > Date.now() - GRACE_PERIOD_DAYS*86400*1000) {
+ return PAST_DUE;
+ }
+ return SUSPENDED;
+}
+
+function getDomainDueDate(domainId) {
+ return _getPaidThroughDate(domainId);
+}
+
+function getDomainSuspensionDate(domainId) {
+ return new Date(_getPaidThroughDate(domainId).getTime() + GRACE_PERIOD_DAYS*86400*1000);
+}
+
+// emails
+
+function sendReceiptEmail(subscription, invoice) {
+ var paymentInfo = getRecurringBillingInfo(subscription.customer);
+ var coupon = getCouponValue(subscription.coupon);
+ var emailText = renderTemplateAsString('email/pro_payment_receipt.ejs', {
+ fullName: paymentInfo.fullname,
+ paymentSummary: paymentInfo.paymentsummary,
+ expiration: checkout.formatExpiration(paymentInfo.expiration),
+ invoiceNumber: invoice.id,
+ numUsers: invoice.users,
+ cost: billing.centsToDollars(invoice.amt),
+ dollars: checkout.dollars,
+ coupon: coupon,
+ globals: globals
+ });
+ var address = paymentInfo.email;
+ checkout.salesEmail(address, "sales@pad.spline.inf.fu-berlin.de", "EtherPad: Receipt for "+paymentInfo.fullname,
+ {}, emailText);
+}
+
+function sendFailureEmail(subscription, invoice, failureMessage) {
+ var domain = subscription.customer;
+ var subDomain = domains.getDomainRecord(domain).subDomain;
+ var paymentInfo = getRecurringBillingInfo(subscription.customer);
+ var emailText = renderTemplateAsString('email/pro_payment_failure.ejs', {
+ fullName: paymentInfo.fullname,
+ billingError: failureMessage,
+ balance: "US $"+checkout.dollars(billing.centsToDollars(invoice.amt)),
+ suspensionDate: checkout.formatDate(new Date(subscription.paidThrough.getTime()+GRACE_PERIOD_DAYS*86400*1000)),
+ billingAdminLink: "https://"+subDomain+".pad.spline.inf.fu-berlin.de/ep/admin/billing/"
+ });
+ var address = paymentInfo.email;
+ checkout.salesEmail(address, "sales@pad.spline.inf.fu-berlin.de", "EtherPad: Payment Failure for "+paymentInfo.fullname,
+ {}, emailText);
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/collab/collab_server.js b/etherpad/src/etherpad/collab/collab_server.js
new file mode 100644
index 0000000..78c9921
--- /dev/null
+++ b/etherpad/src/etherpad/collab/collab_server.js
@@ -0,0 +1,778 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("comet");
+import("ejs");
+import("etherpad.collab.ace.easysync2.{AttribPool,Changeset}");
+import("etherpad.log");
+import("etherpad.pad.activepads");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padusers");
+import("etherpad.pad.padevents");
+import("etherpad.pad.pad_security");
+import("etherpad.pro.pro_padmeta");
+import("fastJSON");
+import("fileutils.readFile");
+import("jsutils.{eachProperty,keys}");
+import("etherpad.collab.collabroom_server.*");
+import("etherpad.collab.readonly_server");
+jimport("java.util.concurrent.ConcurrentHashMap");
+
+var PADPAGE_ROOMTYPE = "padpage";
+
+function onStartup() {
+
+}
+
+function _padIdToRoom(padId) {
+ return "padpage/"+padId;
+}
+
+function _roomToPadId(roomName) {
+ return roomName.substring(roomName.indexOf("/")+1);
+}
+
+function removeFromMemory(pad) {
+ // notification so we can free stuff
+ if (getNumConnections(pad) == 0) {
+ var tempObj = pad.tempObj();
+ tempObj.revisionSockets = {};
+ }
+}
+
+function _getPadConnections(pad) {
+ return getRoomConnections(_padIdToRoom(pad.getId()));
+}
+
+function guestKnock(globalPadId, guestId, displayName) {
+ var askedSomeone = false;
+
+ // requires that we somehow have permission on this pad
+ model.accessPadGlobal(globalPadId, function(pad) {
+ var connections = _getPadConnections(pad);
+ connections.forEach(function(connection) {
+ // only send to pro users
+ if (! padusers.isGuest(connection.data.userInfo.userId)) {
+ askedSomeone = true;
+ var msg = { type: "SERVER_MESSAGE",
+ payload: { type: 'GUEST_PROMPT',
+ userId: guestId,
+ displayName: displayName } };
+ sendMessage(connection.connectionId, msg);
+ }
+ });
+ });
+
+ if (! askedSomeone) {
+ pad_security.answerKnock(guestId, globalPadId, "denied");
+ }
+}
+
+function _verifyUserId(userId) {
+ var result;
+ if (padusers.isGuest(userId)) {
+ // allow cookie-verified guest even if user has signed in
+ result = (userId == padusers.getGuestUserId());
+ }
+ else {
+ result = (userId == padusers.getUserId());
+ }
+ return result;
+}
+
+function _checkChangesetAndPool(cs, pool) {
+ Changeset.checkRep(cs);
+ Changeset.eachAttribNumber(cs, function(n) {
+ if (! pool.getAttrib(n)) {
+ throw new Error("Attribute pool is missing attribute "+n+" for changeset "+cs);
+ }
+ });
+}
+
+function _doWarn(str) {
+ log.warn(appjet.executionId+": "+str);
+}
+
+function _doInfo(str) {
+ log.info(appjet.executionId+": "+str);
+}
+
+function _getPadRevisionSockets(pad) {
+ var revisionSockets = pad.tempObj().revisionSockets;
+ if (! revisionSockets) {
+ revisionSockets = {}; // rev# -> socket id
+ pad.tempObj().revisionSockets = revisionSockets;
+ }
+ return revisionSockets;
+}
+
+function applyUserChanges(pad, baseRev, changeset, optSocketId, optAuthor) {
+ // changeset must be already adapted to the server's apool
+
+ var apool = pad.pool();
+ var r = baseRev;
+ while (r < pad.getHeadRevisionNumber()) {
+ r++;
+ var c = pad.getRevisionChangeset(r);
+ changeset = Changeset.follow(c, changeset, false, apool);
+ }
+
+ var prevText = pad.text();
+ if (Changeset.oldLen(changeset) != prevText.length) {
+ _doWarn("Can't apply USER_CHANGES "+changeset+" to document of length "+
+ prevText.length);
+ return;
+ }
+
+ var thisAuthor = '';
+ if (optSocketId) {
+ var connectionId = getSocketConnectionId(optSocketId);
+ if (connectionId) {
+ var connection = getConnection(connectionId);
+ if (connection) {
+ thisAuthor = connection.data.userInfo.userId;
+ }
+ }
+ }
+ if (optAuthor) {
+ thisAuthor = optAuthor;
+ }
+
+ pad.appendRevision(changeset, thisAuthor);
+ var newRev = pad.getHeadRevisionNumber();
+ if (optSocketId) {
+ _getPadRevisionSockets(pad)[newRev] = optSocketId;
+ }
+
+ var correctionChangeset = _correctMarkersInPad(pad.atext(), pad.pool());
+ if (correctionChangeset) {
+ pad.appendRevision(correctionChangeset);
+ }
+
+ ///// make document end in blank line if it doesn't:
+ if (pad.text().lastIndexOf("\n\n") != pad.text().length-2) {
+ var nlChangeset = Changeset.makeSplice(
+ pad.text(), pad.text().length-1, 0, "\n");
+ pad.appendRevision(nlChangeset);
+ }
+
+ updatePadClients(pad);
+
+ activepads.touch(pad.getId());
+ padevents.onEditPad(pad, thisAuthor);
+}
+
+function updateClient(pad, connectionId) {
+ var conn = getConnection(connectionId);
+ if (! conn) {
+ return;
+ }
+ var lastRev = conn.data.lastRev;
+ var userId = conn.data.userInfo.userId;
+ var socketId = conn.socketId;
+ while (lastRev < pad.getHeadRevisionNumber()) {
+ var r = ++lastRev;
+ var author = pad.getRevisionAuthor(r);
+ var revisionSockets = _getPadRevisionSockets(pad);
+ if (revisionSockets[r] === socketId) {
+ sendMessage(connectionId, {type:"ACCEPT_COMMIT", newRev:r});
+ }
+ else {
+ var forWire = Changeset.prepareForWire(pad.getRevisionChangeset(r), pad.pool());
+ var msg = {type:"NEW_CHANGES", newRev:r,
+ changeset: forWire.translated,
+ apool: forWire.pool,
+ author: author};
+ sendMessage(connectionId, msg);
+ }
+ }
+ conn.data.lastRev = pad.getHeadRevisionNumber();
+ updateRoomConnectionData(connectionId, conn.data);
+}
+
+function updatePadClients(pad) {
+ _getPadConnections(pad).forEach(function(connection) {
+ updateClient(pad, connection.connectionId);
+ });
+
+ readonly_server.updatePadClients(pad);
+}
+
+function applyMissedChanges(pad, missedChanges) {
+ var userInfo = missedChanges.userInfo;
+ var baseRev = missedChanges.baseRev;
+ var committedChangeset = missedChanges.committedChangeset; // may be falsy
+ var furtherChangeset = missedChanges.furtherChangeset; // may be falsy
+ var apool = pad.pool();
+
+ if (! _verifyUserId(userInfo.userId)) {
+ return;
+ }
+
+ if (committedChangeset) {
+ var wireApool1 = (new AttribPool()).fromJsonable(missedChanges.committedChangesetAPool);
+ _checkChangesetAndPool(committedChangeset, wireApool1);
+ committedChangeset = pad.adoptChangesetAttribs(committedChangeset, wireApool1);
+ }
+ if (furtherChangeset) {
+ var wireApool2 = (new AttribPool()).fromJsonable(missedChanges.furtherChangesetAPool);
+ _checkChangesetAndPool(furtherChangeset, wireApool2);
+ furtherChangeset = pad.adoptChangesetAttribs(furtherChangeset, wireApool2);
+ }
+
+ var commitWasMissed = !! committedChangeset;
+ if (commitWasMissed) {
+ var commitSocketId = missedChanges.committedChangesetSocketId;
+ var revisionSockets = _getPadRevisionSockets(pad);
+ // was the commit really missed, or did the client just not hear back?
+ // look for later changeset by this socket
+ var r = baseRev;
+ while (r < pad.getHeadRevisionNumber()) {
+ r++;
+ var s = revisionSockets[r];
+ if (! s) {
+ // changes are too old, have to drop them.
+ return;
+ }
+ if (s == commitSocketId) {
+ commitWasMissed = false;
+ break;
+ }
+ }
+ }
+ if (! commitWasMissed) {
+ // commit already incorporated by the server
+ committedChangeset = null;
+ }
+
+ var changeset;
+ if (committedChangeset && furtherChangeset) {
+ changeset = Changeset.compose(committedChangeset, furtherChangeset, apool);
+ }
+ else {
+ changeset = (committedChangeset || furtherChangeset);
+ }
+
+ if (changeset) {
+ var author = userInfo.userId;
+
+ applyUserChanges(pad, baseRev, changeset, null, author);
+ }
+}
+
+function getAllPadsWithConnections() {
+ // returns array of global pad id strings
+ return getAllRoomsOfType(PADPAGE_ROOMTYPE).map(_roomToPadId);
+}
+
+function broadcastServerMessage(msgObj) {
+ var msg = {type: "SERVER_MESSAGE", payload: msgObj};
+ getAllRoomsOfType(PADPAGE_ROOMTYPE).forEach(function(roomName) {
+ getRoomConnections(roomName).forEach(function(connection) {
+ sendMessage(connection.connectionId, msg);
+ });
+ });
+}
+
+function appendPadText(pad, txt) {
+ txt = model.cleanText(txt);
+ var oldFullText = pad.text();
+ _applyChangesetToPad(pad, Changeset.makeSplice(oldFullText,
+ oldFullText.length-1, 0, txt));
+}
+
+function setPadText(pad, txt) {
+ txt = model.cleanText(txt);
+ var oldFullText = pad.text();
+ // replace text except for the existing final (virtual) newline
+ _applyChangesetToPad(pad, Changeset.makeSplice(oldFullText, 0,
+ oldFullText.length-1, txt));
+}
+
+function setPadAText(pad, atext) {
+ var oldFullText = pad.text();
+ var deletion = Changeset.makeSplice(oldFullText, 0, oldFullText.length-1, "");
+
+ var assem = Changeset.smartOpAssembler();
+ Changeset.appendATextToAssembler(atext, assem);
+ var charBank = atext.text.slice(0, -1);
+ var insertion = Changeset.checkRep(Changeset.pack(1, atext.text.length,
+ assem.toString(), charBank));
+
+ var cs = Changeset.compose(deletion, insertion, pad.pool());
+ Changeset.checkRep(cs);
+
+ _applyChangesetToPad(pad, cs);
+}
+
+function applyChangesetToPad(pad, changeset) {
+ Changeset.checkRep(changeset);
+
+ _applyChangesetToPad(pad, changeset);
+}
+
+function _applyChangesetToPad(pad, changeset) {
+ pad.appendRevision(changeset);
+ updatePadClients(pad);
+}
+
+function getHistoricalAuthorData(pad, author) {
+ var authorData = pad.getAuthorData(author);
+ if (authorData) {
+ var data = {};
+ if ((typeof authorData.colorId) == "number") {
+ data.colorId = authorData.colorId;
+ }
+ if (authorData.name) {
+ data.name = authorData.name;
+ }
+ else {
+ var uname = padusers.getNameForUserId(author);
+ if (uname) {
+ data.name = uname;
+ }
+ }
+ return data;
+ }
+ return null;
+}
+
+function buildHistoricalAuthorDataMapFromAText(pad, atext) {
+ var map = {};
+ pad.eachATextAuthor(atext, function(author, authorNum) {
+ var data = getHistoricalAuthorData(pad, author);
+ if (data) {
+ map[author] = data;
+ }
+ });
+ return map;
+}
+
+function buildHistoricalAuthorDataMapForPadHistory(pad) {
+ var map = {};
+ pad.pool().eachAttrib(function(key, value) {
+ if (key == 'author') {
+ var author = value;
+ var data = getHistoricalAuthorData(pad, author);
+ if (data) {
+ map[author] = data;
+ }
+ }
+ });
+ return map;
+}
+
+function getATextForWire(pad, optRev) {
+ var atext;
+ if ((optRev && ! isNaN(Number(optRev))) || (typeof optRev) == "number") {
+ atext = pad.getInternalRevisionAText(Number(optRev));
+ }
+ else {
+ atext = pad.atext();
+ }
+
+ var historicalAuthorData = buildHistoricalAuthorDataMapFromAText(pad, atext);
+
+ var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool());
+ var apool = attribsForWire.pool;
+ // mutate atext (translate attribs for wire):
+ atext.attribs = attribsForWire.translated;
+
+ return {atext:atext, apool:apool.toJsonable(),
+ historicalAuthorData:historicalAuthorData };
+}
+
+function getCollabClientVars(pad) {
+ // construct object that is made available on the client
+ // as collab_client_vars
+
+ var forWire = getATextForWire(pad);
+
+ return {
+ initialAttributedText: forWire.atext,
+ rev: pad.getHeadRevisionNumber(),
+ padId: pad.getLocalId(),
+ globalPadId: pad.getId(),
+ historicalAuthorData: forWire.historicalAuthorData,
+ apool: forWire.apool,
+ clientIp: request.clientAddr,
+ clientAgent: request.headers["User-Agent"]
+ };
+}
+
+function getNumConnections(pad) {
+ return _getPadConnections(pad).length;
+}
+
+function getConnectedUsers(pad) {
+ var users = [];
+ _getPadConnections(pad).forEach(function(connection) {
+ users.push(connection.data.userInfo);
+ });
+ return users;
+}
+
+
+function bootAllUsersFromPad(pad, reason) {
+ return bootUsersFromPad(pad, reason);
+}
+
+function bootUsersFromPad(pad, reason, userInfoFilter) {
+ var connections = _getPadConnections(pad);
+ var bootedUserInfos = [];
+ connections.forEach(function(connection) {
+ if ((! userInfoFilter) || userInfoFilter(connection.data.userInfo)) {
+ bootedUserInfos.push(connection.data.userInfo);
+ bootConnection(connection.connectionId);
+ }
+ });
+ return bootedUserInfos;
+}
+
+function dumpStorageToString(pad) {
+ var lines = [];
+ var errors = [];
+ var head = pad.getHeadRevisionNumber();
+ try {
+ for(var i=0;i<=head;i++) {
+ lines.push("changeset "+i+" "+Changeset.toBaseTen(pad.getRevisionChangeset(i)));
+ }
+ }
+ catch (e) {
+ errors.push("!!!!! Error in changeset "+i+": "+e.message);
+ }
+ for(var i=0;i<=head;i++) {
+ lines.push("author "+i+" "+pad.getRevisionAuthor(i));
+ }
+ for(var i=0;i<=head;i++) {
+ lines.push("time "+i+" "+pad.getRevisionDate(i));
+ }
+ var revisionSockets = _getPadRevisionSockets(pad);
+ for(var k in revisionSockets) lines.push("socket "+k+" "+revisionSockets[k]);
+ return errors.concat(lines).join('\n');
+}
+
+function _getPadIdForSocket(socketId) {
+ var connectionId = getSocketConnectionId(socketId);
+ if (connectionId) {
+ var connection = getConnection(connectionId);
+ if (connection) {
+ return _roomToPadId(connection.roomName);
+ }
+ }
+ return null;
+}
+
+function _getUserIdForSocket(socketId) {
+ var connectionId = getSocketConnectionId(socketId);
+ if (connectionId) {
+ var connection = getConnection(connectionId);
+ if (connection) {
+ return connection.data.userInfo.userId;
+ }
+ }
+ return null;
+}
+
+function _serverDebug(msg) { /* nothing */ }
+
+function _accessSocketPad(socketId, accessType, padFunc, dontRequirePad) {
+ return _accessCollabPad(_getPadIdForSocket(socketId), accessType,
+ padFunc, dontRequirePad);
+}
+
+function _accessConnectionPad(connection, accessType, padFunc, dontRequirePad) {
+ return _accessCollabPad(_roomToPadId(connection.roomName), accessType,
+ padFunc, dontRequirePad);
+}
+
+function _accessCollabPad(padId, accessType, padFunc, dontRequirePad) {
+ if (! padId) {
+ if (! dontRequirePad) {
+ _doWarn("Collab operation \""+accessType+"\" aborted because socket "+socketId+" has no pad.");
+ }
+ return;
+ }
+ else {
+ return _accessExistingPad(padId, accessType, function(pad) {
+ return padFunc(pad);
+ }, dontRequirePad);
+ }
+}
+
+function _accessExistingPad(padId, accessType, padFunc, dontRequireExist) {
+ return model.accessPadGlobal(padId, function(pad) {
+ if (! pad.exists()) {
+ if (! dontRequireExist) {
+ _doWarn("Collab operation \""+accessType+"\" aborted because pad "+padId+" doesn't exist.");
+ }
+ return;
+ }
+ else {
+ return padFunc(pad);
+ }
+ });
+}
+
+function _handlePadUserInfo(pad, userInfo) {
+ var author = userInfo.userId;
+ var colorId = Number(userInfo.colorId);
+ var name = userInfo.name;
+
+ if (! author) return;
+
+ // update map from author to that author's last known color and name
+ var data = {colorId: colorId};
+ if (name) data.name = name;
+ pad.setAuthorData(author, data);
+ padusers.notifyUserData(data);
+}
+
+function _sendUserInfoMessage(connectionId, type, userInfo) {
+ if (translateSpecialKey(userInfo.specialKey) != 'invisible') {
+ sendMessage(connectionId, {type: type, userInfo: userInfo });
+ }
+}
+
+
+function getRoomCallbacks(roomName) {
+ var callbacks = {};
+ callbacks.introduceUsers =
+ function (joiningConnection, existingConnection) {
+ // notify users of each other
+ _sendUserInfoMessage(existingConnection.connectionId,
+ "USER_NEWINFO",
+ joiningConnection.data.userInfo);
+ _sendUserInfoMessage(joiningConnection.connectionId,
+ "USER_NEWINFO",
+ existingConnection.data.userInfo);
+ };
+ callbacks.extroduceUsers =
+ function (leavingConnection, existingConnection) {
+ _sendUserInfoMessage(existingConnection.connectionId, "USER_LEAVE",
+ leavingConnection.data.userInfo);
+ };
+ callbacks.onAddConnection =
+ function (data) {
+ model.accessPadGlobal(_roomToPadId(roomName), function(pad) {
+ _handlePadUserInfo(pad, data.userInfo);
+ padevents.onUserJoin(pad, data.userInfo);
+ readonly_server.updateUserInfo(pad, data.userInfo);
+ });
+ };
+ callbacks.onRemoveConnection =
+ function (data) {
+ model.accessPadGlobal(_roomToPadId(roomName), function(pad) {
+ padevents.onUserLeave(pad, data.userInfo);
+ });
+ };
+ callbacks.handleConnect =
+ function (data) {
+ if (roomName.indexOf("padpage/") != 0) {
+ return null;
+ }
+ if (! (data.userInfo && data.userInfo.userId &&
+ _verifyUserId(data.userInfo.userId))) {
+ return null;
+ }
+ return data.userInfo;
+ };
+ callbacks.clientReady =
+ function(newConnection, data) {
+ var padId = _roomToPadId(newConnection.roomName);
+
+ if (data.stats) {
+ log.custom("padclientstats", {padId:padId, stats:data.stats});
+ }
+
+ var lastRev = data.lastRev;
+ var isReconnectOf = data.isReconnectOf;
+ var isCommitPending = !! data.isCommitPending;
+ var connectionId = newConnection.connectionId;
+
+ newConnection.data.lastRev = lastRev;
+ updateRoomConnectionData(connectionId, newConnection.data);
+
+ if (padutils.isProPadId(padId)) {
+ pro_padmeta.accessProPad(padId, function(propad) {
+ // tell client about pad title
+ sendMessage(connectionId, {type: "CLIENT_MESSAGE", payload: {
+ type: "padtitle", title: propad.getDisplayTitle() } });
+ sendMessage(connectionId, {type: "CLIENT_MESSAGE", payload: {
+ type: "padpassword", password: propad.getPassword() } });
+ });
+ }
+
+ _accessExistingPad(padId, "CLIENT_READY", function(pad) {
+ sendMessage(connectionId, {type: "CLIENT_MESSAGE", payload: {
+ type: "padoptions", options: pad.getPadOptionsObj() } });
+
+ updateClient(pad, connectionId);
+
+ });
+
+ if (isCommitPending) {
+ // tell client that if it hasn't received an ACCEPT_COMMIT by now, it isn't coming.
+ sendMessage(connectionId, {type:"NO_COMMIT_PENDING"});
+ }
+ };
+ callbacks.handleMessage = function(connection, msg) {
+ _handleCometMessage(connection, msg);
+ };
+ return callbacks;
+}
+
+var _specialKeys = [['x375b', 'invisible']];
+
+function translateSpecialKey(specialKey) {
+ // code -> name
+ for(var i=0;i<_specialKeys.length;i++) {
+ if (_specialKeys[i][0] == specialKey) {
+ return _specialKeys[i][1];
+ }
+ }
+ return null;
+}
+
+function getSpecialKey(name) {
+ // name -> code
+ for(var i=0;i<_specialKeys.length;i++) {
+ if (_specialKeys[i][1] == name) {
+ return _specialKeys[i][0];
+ }
+ }
+ return null;
+}
+
+function _updateDocumentConnectionUserInfo(pad, socketId, userInfo) {
+ var connectionId = getSocketConnectionId(socketId);
+ if (connectionId) {
+ var updatingConnection = getConnection(connectionId);
+ updatingConnection.data.userInfo = userInfo;
+ updateRoomConnectionData(connectionId, updatingConnection.data);
+ _getPadConnections(pad).forEach(function(connection) {
+ if (connection.socketId != updatingConnection.socketId) {
+ _sendUserInfoMessage(connection.connectionId,
+ "USER_NEWINFO", userInfo);
+ }
+ });
+
+ _handlePadUserInfo(pad, userInfo);
+ padevents.onUserInfoChange(pad, userInfo);
+ readonly_server.updateUserInfo(pad, userInfo);
+ }
+}
+
+function _handleCometMessage(connection, msg) {
+
+ var socketUserId = connection.data.userInfo.userId;
+ if (! (socketUserId && _verifyUserId(socketUserId))) {
+ // user has signed out or cleared cookies, no longer auth'ed
+ bootConnection(connection.connectionId, "unauth");
+ }
+
+ if (msg.type == "USER_CHANGES") {
+ try {
+ _accessConnectionPad(connection, "USER_CHANGES", function(pad) {
+ var baseRev = msg.baseRev;
+ var wireApool = (new AttribPool()).fromJsonable(msg.apool);
+ var changeset = msg.changeset;
+ if (changeset) {
+ _checkChangesetAndPool(changeset, wireApool);
+ changeset = pad.adoptChangesetAttribs(changeset, wireApool);
+ applyUserChanges(pad, baseRev, changeset, connection.socketId);
+ }
+ });
+ }
+ catch (e if e.easysync) {
+ _doWarn("Changeset error handling USER_CHANGES: "+e);
+ }
+ }
+ else if (msg.type == "USERINFO_UPDATE") {
+ _accessConnectionPad(connection, "USERINFO_UPDATE", function(pad) {
+ var userInfo = msg.userInfo;
+ // security check
+ if (userInfo.userId == connection.data.userInfo.userId) {
+ _updateDocumentConnectionUserInfo(pad,
+ connection.socketId, userInfo);
+ }
+ else {
+ // drop on the floor
+ }
+ });
+ }
+ else if (msg.type == "CLIENT_MESSAGE") {
+ _accessConnectionPad(connection, "CLIENT_MESSAGE", function(pad) {
+ var payload = msg.payload;
+ if (payload.authId &&
+ payload.authId != connection.data.userInfo.userId) {
+ // authId, if present, must actually be the sender's userId;
+ // here it wasn't
+ }
+ else {
+ getRoomConnections(connection.roomName).forEach(
+ function(conn) {
+ if (conn.socketId != connection.socketId) {
+ sendMessage(conn.connectionId,
+ {type: "CLIENT_MESSAGE", payload: payload});
+ }
+ });
+ padevents.onClientMessage(pad, connection.data.userInfo,
+ payload);
+ }
+ });
+ }
+}
+
+function _correctMarkersInPad(atext, apool) {
+ var text = atext.text;
+
+ // collect char positions of line markers (e.g. bullets) in new atext
+ // that aren't at the start of a line
+ var badMarkers = [];
+ var iter = Changeset.opIterator(atext.attribs);
+ var offset = 0;
+ while (iter.hasNext()) {
+ var op = iter.next();
+ var listValue = Changeset.opAttributeValue(op, 'list', apool);
+ if (listValue) {
+ for(var i=0;i<op.chars;i++) {
+ if (offset > 0 && text.charAt(offset-1) != '\n') {
+ badMarkers.push(offset);
+ }
+ offset++;
+ }
+ }
+ else {
+ offset += op.chars;
+ }
+ }
+
+ if (badMarkers.length == 0) {
+ return null;
+ }
+
+ // create changeset that removes these bad markers
+ offset = 0;
+ var builder = Changeset.builder(text.length);
+ badMarkers.forEach(function(pos) {
+ builder.keepText(text.substring(offset, pos));
+ builder.remove(1);
+ offset = pos+1;
+ });
+ return builder.toString();
+}
diff --git a/etherpad/src/etherpad/collab/collabroom_server.js b/etherpad/src/etherpad/collab/collabroom_server.js
new file mode 100644
index 0000000..ab1f844
--- /dev/null
+++ b/etherpad/src/etherpad/collab/collabroom_server.js
@@ -0,0 +1,359 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("execution");
+import("comet");
+import("fastJSON");
+import("cache_utils.syncedWithCache");
+import("etherpad.collab.collab_server");
+import("etherpad.collab.readonly_server");
+import("etherpad.log");
+jimport("java.util.concurrent.ConcurrentSkipListMap");
+jimport("java.util.concurrent.CopyOnWriteArraySet");
+
+function onStartup() {
+ execution.initTaskThreadPool("collabroom_async", 1);
+}
+
+function _doWarn(str) {
+ log.warn(appjet.executionId+": "+str);
+}
+
+// deep-copies (recursively clones) an object (or value)
+function _deepCopy(obj) {
+ if ((typeof obj) != 'object' || !obj) {
+ return obj;
+ }
+ var o = {};
+ for(var k in obj) {
+ if (obj.hasOwnProperty(k)) {
+ var v = obj[k];
+ if ((typeof v) == 'object' && v) {
+ o[k] = _deepCopy(v);
+ }
+ else {
+ o[k] = v;
+ }
+ }
+ }
+ return o;
+}
+
+// calls func inside a global lock on the cache
+function _withCache(func) {
+ return syncedWithCache("collabroom_server", function(cache) {
+ if (! cache.rooms) {
+ // roomName -> { connections: CopyOnWriteArraySet<connectionId>,
+ // type: <immutable type string> }
+ cache.rooms = new ConcurrentSkipListMap();
+ }
+ if (! cache.allConnections) {
+ // connectionId -> connection object
+ cache.allConnections = new ConcurrentSkipListMap();
+ }
+ return func(cache);
+ });
+}
+
+// accesses cache without lock
+function _getCache() {
+ return _withCache(function(cache) { return cache; });
+}
+
+// if roomType is null, will only update an existing connection
+// (otherwise will insert or update as appropriate)
+function _putConnection(connection, roomType) {
+ var roomName = connection.roomName;
+ var connectionId = connection.connectionId;
+ var socketId = connection.socketId;
+ var data = connection.data;
+
+ _withCache(function(cache) {
+ var rooms = cache.rooms;
+ if (! rooms.containsKey(roomName)) {
+ // connection refers to room that doesn't exist / is empty
+ if (roomType) {
+ rooms.put(roomName, {connections: new CopyOnWriteArraySet(),
+ type: roomType});
+ }
+ else {
+ return;
+ }
+ }
+ if (roomType) {
+ rooms.get(roomName).connections.add(connectionId);
+ cache.allConnections.put(connectionId, connection);
+ }
+ else {
+ cache.allConnections.replace(connectionId, connection);
+ }
+ });
+}
+
+function _removeConnection(connection) {
+ _withCache(function(cache) {
+ var rooms = cache.rooms;
+ var thisRoom = connection.roomName;
+ var thisConnectionId = connection.connectionId;
+ if (rooms.containsKey(thisRoom)) {
+ var roomConnections = rooms.get(thisRoom).connections;
+ roomConnections.remove(thisConnectionId);
+ if (roomConnections.isEmpty()) {
+ rooms.remove(thisRoom);
+ }
+ }
+ cache.allConnections.remove(thisConnectionId);
+ });
+}
+
+function _getConnection(connectionId) {
+ // return a copy of the connection object
+ return _deepCopy(_getCache().allConnections.get(connectionId) || null);
+}
+
+function _getConnections(roomName) {
+ var array = [];
+
+ var roomObj = _getCache().rooms.get(roomName);
+ if (roomObj) {
+ var roomConnections = roomObj.connections;
+ var iter = roomConnections.iterator();
+ while (iter.hasNext()) {
+ var cid = iter.next();
+ var conn = _getConnection(cid);
+ if (conn) {
+ array.push(conn);
+ }
+ }
+ }
+ return array;
+}
+
+function sendMessage(connectionId, msg) {
+ var connection = _getConnection(connectionId);
+ if (connection) {
+ _sendMessageToSocket(connection.socketId, msg);
+ if (! comet.isConnected(connection.socketId)) {
+ // defunct socket, disconnect (later)
+ execution.scheduleTask("collabroom_async",
+ "collabRoomDisconnectSocket",
+ 0, [connection.connectionId,
+ connection.socketId]);
+ }
+ }
+}
+
+function _sendMessageToSocket(socketId, msg) {
+ var msgString = fastJSON.stringify({type: "COLLABROOM", data: msg});
+ comet.sendMessage(socketId, msgString);
+}
+
+function disconnectDefunctSocket(connectionId, socketId) {
+ var connection = _getConnection(connectionId);
+ if (connection && connection.socketId == socketId) {
+ removeRoomConnection(connectionId);
+ }
+}
+
+function _bootSocket(socketId, reason) {
+ if (reason) {
+ _sendMessageToSocket(socketId,
+ {type: "DISCONNECT_REASON", reason: reason});
+ }
+ comet.disconnect(socketId);
+}
+
+function bootConnection(connectionId, reason) {
+ var connection = _getConnection(connectionId);
+ if (connection) {
+ _bootSocket(connection.socketId, reason);
+ removeRoomConnection(connectionId);
+ }
+}
+
+function getCallbacksForRoom(roomName, roomType) {
+ if (! roomType) {
+ var room = _getCache().rooms.get(roomName);
+ if (room) {
+ roomType = room.type;
+ }
+ }
+
+ var emptyCallbacks = {};
+ emptyCallbacks.introduceUsers =
+ function (joiningConnection, existingConnection) {};
+ emptyCallbacks.extroduceUsers =
+ function extroduceUsers(leavingConnection, existingConnection) {};
+ emptyCallbacks.onAddConnection = function (joiningData) {};
+ emptyCallbacks.onRemoveConnection = function (leavingData) {};
+ emptyCallbacks.handleConnect =
+ function(data) { return /*userInfo or */null; };
+ emptyCallbacks.clientReady = function(newConnection, data) {};
+ emptyCallbacks.handleMessage = function(connection, msg) {};
+
+ if (roomType == collab_server.PADPAGE_ROOMTYPE) {
+ return collab_server.getRoomCallbacks(roomName, emptyCallbacks);
+ }
+ else if (roomType == readonly_server.PADVIEW_ROOMTYPE) {
+ return readonly_server.getRoomCallbacks(roomName, emptyCallbacks);
+ }
+ else {
+ //java.lang.System.out.println("UNKNOWN ROOMTYPE: "+roomType);
+ return emptyCallbacks;
+ }
+}
+
+// roomName must be globally unique, just within roomType;
+// data must have a userInfo.userId
+function addRoomConnection(roomName, roomType,
+ connectionId, socketId, data) {
+ var callbacks = getCallbacksForRoom(roomName, roomType);
+
+ comet.setAttribute(socketId, "connectionId", connectionId);
+
+ bootConnection(connectionId, "userdup");
+ var joiningConnection = {roomName:roomName,
+ connectionId:connectionId, socketId:socketId,
+ data:data};
+ _putConnection(joiningConnection, roomType);
+ var connections = _getConnections(roomName);
+ var joiningUser = data.userInfo.userId;
+
+ connections.forEach(function(connection) {
+ if (connection.socketId != socketId) {
+ var user = connection.data.userInfo.userId;
+ if (user == joiningUser) {
+ bootConnection(connection.connectionId, "userdup");
+ }
+ else {
+ callbacks.introduceUsers(joiningConnection, connection);
+ }
+ }
+ });
+
+ callbacks.onAddConnection(data);
+
+ return joiningConnection;
+}
+
+function removeRoomConnection(connectionId) {
+ var leavingConnection = _getConnection(connectionId);
+ if (leavingConnection) {
+ var roomName = leavingConnection.roomName;
+ var callbacks = getCallbacksForRoom(roomName);
+
+ _removeConnection(leavingConnection);
+
+ _getConnections(roomName).forEach(function (connection) {
+ callbacks.extroduceUsers(leavingConnection, connection);
+ });
+
+ callbacks.onRemoveConnection(leavingConnection.data);
+ }
+}
+
+function getConnection(connectionId) {
+ return _getConnection(connectionId);
+}
+
+function updateRoomConnectionData(connectionId, data) {
+ var connection = _getConnection(connectionId);
+ if (connection) {
+ connection.data = data;
+ _putConnection(connection);
+ }
+}
+
+function getRoomConnections(roomName) {
+ return _getConnections(roomName);
+}
+
+function getAllRoomsOfType(roomType) {
+ var rooms = _getCache().rooms;
+ var roomsIter = rooms.entrySet().iterator();
+ var array = [];
+ while (roomsIter.hasNext()) {
+ var entry = roomsIter.next();
+ var roomName = entry.getKey();
+ var roomStruct = entry.getValue();
+ if (roomStruct.type == roomType) {
+ array.push(roomName);
+ }
+ }
+ return array;
+}
+
+function getSocketConnectionId(socketId) {
+ var result = comet.getAttribute(socketId, "connectionId");
+ return result && String(result);
+}
+
+function handleComet(cometOp, cometId, msg) {
+ var cometEvent = cometOp;
+
+ function requireTruthy(x, id) {
+ if (!x) {
+ _doWarn("Collab operation rejected due to missing value, case "+id);
+ if (messageSocketId) {
+ comet.disconnect(messageSocketId);
+ }
+ response.stop();
+ }
+ return x;
+ }
+
+ if (cometEvent != "disconnect" && cometEvent != "message") {
+ response.stop();
+ }
+
+ var messageSocketId = requireTruthy(cometId, 2);
+ var messageConnectionId = getSocketConnectionId(messageSocketId);
+
+ if (cometEvent == "disconnect") {
+ if (messageConnectionId) {
+ removeRoomConnection(messageConnectionId);
+ }
+ }
+ else if (cometEvent == "message") {
+ if (msg.type == "CLIENT_READY") {
+ var roomType = requireTruthy(msg.roomType, 4);
+ var roomName = requireTruthy(msg.roomName, 11);
+
+ var socketId = messageSocketId;
+ var connectionId = messageSocketId;
+ var clientReadyData = requireTruthy(msg.data, 12);
+
+ var callbacks = getCallbacksForRoom(roomName, roomType);
+ var userInfo =
+ requireTruthy(callbacks.handleConnect(clientReadyData), 13);
+
+ var newConnection = addRoomConnection(roomName, roomType,
+ connectionId, socketId,
+ {userInfo: userInfo});
+
+ callbacks.clientReady(newConnection, clientReadyData);
+ }
+ else {
+ if (messageConnectionId) {
+ var connection = getConnection(messageConnectionId);
+ if (connection) {
+ var callbacks = getCallbacksForRoom(connection.roomName);
+ callbacks.handleMessage(connection, msg);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/collab/genimg.js b/etherpad/src/etherpad/collab/genimg.js
new file mode 100644
index 0000000..04d1b3b
--- /dev/null
+++ b/etherpad/src/etherpad/collab/genimg.js
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sync");
+import("image");
+import("blob");
+
+//jimport("java.lang.System.out.println");
+
+function _cache() {
+ sync.callsyncIfTrue(appjet.cache,
+ function() { return ! appjet.cache["etherpad-genimg"]; },
+ function() { appjet.cache["etherpad-genimg"] = { paths: {}}; });
+ return appjet.cache["etherpad-genimg"];
+}
+
+function renderPath(path) {
+ if (_cache().paths[path]) {
+ //println("CACHE HIT");
+ }
+ else {
+ //println("CACHE MISS");
+ var regexResult = null;
+ var img = null;
+ if ((regexResult =
+ /solid\/([0-9]+)x([0-9]+)\/([0-9a-fA-F]{6})\.gif/.exec(path))) {
+ var width = Number(regexResult[1]);
+ var height = Number(regexResult[2]);
+ var color = regexResult[3];
+ img = image.solidColorImageBlob(width, height, color);
+ }
+ else {
+ // our "broken image" image, red and partly transparent
+ img = image.pixelsToImageBlob(2, 2, [0x00000000, 0xffff0000,
+ 0xffff0000, 0x00000000], true, "gif");
+ }
+ _cache().paths[path] = img;
+ }
+
+ blob.serveBlob(_cache().paths[path]);
+ return true;
+}
diff --git a/etherpad/src/etherpad/collab/json_sans_eval.js b/etherpad/src/etherpad/collab/json_sans_eval.js
new file mode 100644
index 0000000..6cbd497
--- /dev/null
+++ b/etherpad/src/etherpad/collab/json_sans_eval.js
@@ -0,0 +1,178 @@
+// Copyright (C) 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * Parses a string of well-formed JSON text.
+ *
+ * If the input is not well-formed, then behavior is undefined, but it is
+ * deterministic and is guaranteed not to modify any object other than its
+ * return value.
+ *
+ * This does not use `eval` so is less likely to have obscure security bugs than
+ * json2.js.
+ * It is optimized for speed, so is much faster than json_parse.js.
+ *
+ * This library should be used whenever security is a concern (when JSON may
+ * come from an untrusted source), speed is a concern, and erroring on malformed
+ * JSON is *not* a concern.
+ *
+ * Pros Cons
+ * +-----------------------+-----------------------+
+ * json_sans_eval.js | Fast, secure | Not validating |
+ * +-----------------------+-----------------------+
+ * json_parse.js | Validating, secure | Slow |
+ * +-----------------------+-----------------------+
+ * json2.js | Fast, some validation | Potentially insecure |
+ * +-----------------------+-----------------------+
+ *
+ * json2.js is very fast, but potentially insecure since it calls `eval` to
+ * parse JSON data, so an attacker might be able to supply strange JS that
+ * looks like JSON, but that executes arbitrary javascript.
+ * If you do have to use json2.js with untrusted data, make sure you keep
+ * your version of json2.js up to date so that you get patches as they're
+ * released.
+ *
+ * @param {string} json per RFC 4627
+ * @return {Object|Array}
+ * @author Mike Samuel <mikesamuel@gmail.com>
+ */
+var jsonParse = (function () {
+ var number
+ = '(?:-?\\b(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\\b)';
+ var oneChar = '(?:[^\\0-\\x08\\x0a-\\x1f\"\\\\]'
+ + '|\\\\(?:[\"/\\\\bfnrt]|u[0-9A-Fa-f]{4}|x7c))';
+ var string = '(?:\"' + oneChar + '*\")';
+
+ // Will match a value in a well-formed JSON file.
+ // If the input is not well-formed, may match strangely, but not in an unsafe
+ // way.
+ // Since this only matches value tokens, it does not match whitespace, colons,
+ // or commas.
+ var jsonToken = new RegExp(
+ '(?:false|true|null|[\\{\\}\\[\\]]'
+ + '|' + number
+ + '|' + string
+ + ')', 'g');
+
+ // Matches escape sequences in a string literal
+ var escapeSequence = new RegExp('\\\\(?:([^ux]|x7c)|u(.{4}))', 'g');
+
+ // Decodes escape sequences in object literals
+ var escapes = {
+ '"': '"',
+ '/': '/',
+ '\\': '\\',
+ 'b': '\b',
+ 'f': '\f',
+ 'n': '\n',
+ 'r': '\r',
+ 't': '\t',
+ 'x7c': '|'
+ };
+ function unescapeOne(_, ch, hex) {
+ return ch ? escapes[ch] : String.fromCharCode(parseInt(hex, 16));
+ }
+
+ // A non-falsy value that coerces to the empty string when used as a key.
+ var EMPTY_STRING = new String('');
+ var SLASH = '\\';
+
+ // Constructor to use based on an open token.
+ var firstTokenCtors = { '{': Object, '[': Array };
+
+ return function (json) {
+ // Split into tokens
+ var toks = json.match(jsonToken);
+ // Construct the object to return
+ var result;
+ var tok = toks[0];
+ if ('{' === tok) {
+ result = {};
+ } else if ('[' === tok) {
+ result = [];
+ } else {
+ throw new Error(tok);
+ }
+
+ // If undefined, the key in an object key/value record to use for the next
+ // value parsed.
+ var key;
+ // Loop over remaining tokens maintaining a stack of uncompleted objects and
+ // arrays.
+ var stack = [result];
+ for (var i = 1, n = toks.length; i < n; ++i) {
+ tok = toks[i];
+
+ var cont;
+ switch (tok.charCodeAt(0)) {
+ default: // sign or digit
+ cont = stack[0];
+ cont[key || cont.length] = +(tok);
+ key = void 0;
+ break;
+ case 0x22: // '"'
+ tok = tok.substring(1, tok.length - 1);
+ if (tok.indexOf(SLASH) !== -1) {
+ tok = tok.replace(escapeSequence, unescapeOne);
+ }
+ cont = stack[0];
+ if (!key) {
+ if (cont instanceof Array) {
+ key = cont.length;
+ } else {
+ key = tok || EMPTY_STRING; // Use as key for next value seen.
+ break;
+ }
+ }
+ cont[key] = tok;
+ key = void 0;
+ break;
+ case 0x5b: // '['
+ cont = stack[0];
+ stack.unshift(cont[key || cont.length] = []);
+ key = void 0;
+ break;
+ case 0x5d: // ']'
+ stack.shift();
+ break;
+ case 0x66: // 'f'
+ cont = stack[0];
+ cont[key || cont.length] = false;
+ key = void 0;
+ break;
+ case 0x6e: // 'n'
+ cont = stack[0];
+ cont[key || cont.length] = null;
+ key = void 0;
+ break;
+ case 0x74: // 't'
+ cont = stack[0];
+ cont[key || cont.length] = true;
+ key = void 0;
+ break;
+ case 0x7b: // '{'
+ cont = stack[0];
+ stack.unshift(cont[key || cont.length] = {});
+ key = void 0;
+ break;
+ case 0x7d: // '}'
+ stack.shift();
+ break;
+ }
+ }
+ // Fail if we've got an uncompleted object.
+ if (stack.length) { throw new Error(); }
+ return result;
+ };
+})();
diff --git a/etherpad/src/etherpad/collab/readonly_server.js b/etherpad/src/etherpad/collab/readonly_server.js
new file mode 100644
index 0000000..e367f04
--- /dev/null
+++ b/etherpad/src/etherpad/collab/readonly_server.js
@@ -0,0 +1,174 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("comet");
+import("ejs");
+import("etherpad.collab.ace.easysync2.{AttribPool,Changeset}");
+import("etherpad.log");
+import("etherpad.pad.activepads");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padevents");
+import("etherpad.pro.pro_padmeta");
+import("fastJSON");
+import("fileutils.readFile");
+import("jsutils.eachProperty");
+import("etherpad.collab.server_utils.*");
+import("etherpad.collab.collabroom_server");
+
+jimport("java.util.concurrent.ConcurrentHashMap");
+
+jimport("java.lang.System.out.println");
+
+var PADVIEW_ROOMTYPE = 'padview';
+
+var _serverDebug = println;//function(x) {};
+
+// "view id" is either a padId or an ro.id
+function _viewIdToRoom(padId) {
+ return "padview/"+padId;
+}
+
+function _roomToViewId(roomName) {
+ return roomName.substring(roomName.indexOf("/")+1);
+}
+
+function getRoomCallbacks(roomName, emptyCallbacks) {
+ var callbacks = emptyCallbacks;
+
+ var viewId = _roomToViewId(roomName);
+
+ callbacks.handleConnect = function(data) {
+ if (data.userInfo && data.userInfo.userId) {
+ return data.userInfo;
+ }
+ return null;
+ };
+ callbacks.clientReady =
+ function(newConnection, data) {
+ newConnection.data.lastRev = data.lastRev;
+ collabroom_server.updateRoomConnectionData(newConnection.connectionId,
+ newConnection.data);
+ };
+
+ return callbacks;
+}
+
+function updatePadClients(pad) {
+ var padId = pad.getId();
+ var roId = padIdToReadonly(padId);
+
+ function update(connection) {
+ updateClient(pad, connection.connectionId);
+ }
+
+ collabroom_server.getRoomConnections(_viewIdToRoom(padId)).forEach(update);
+ collabroom_server.getRoomConnections(_viewIdToRoom(roId)).forEach(update);
+}
+
+// Get arrays of text lines and attribute lines for a revision
+// of a pad.
+function _getPadLines(pad, revNum) {
+ var atext;
+ if (revNum >= 0) {
+ atext = pad.getInternalRevisionAText(revNum);
+ } else {
+ atext = Changeset.makeAText("\n");
+ }
+
+ var result = {};
+ result.textlines = Changeset.splitTextLines(atext.text);
+ result.alines = Changeset.splitAttributionLines(atext.attribs,
+ atext.text);
+ return result;
+}
+
+function updateClient(pad, connectionId) {
+ var conn = collabroom_server.getConnection(connectionId);
+ if (! conn) {
+ return;
+ }
+ var lastRev = conn.data.lastRev;
+ while (lastRev < pad.getHeadRevisionNumber()) {
+ var r = ++lastRev;
+ var author = pad.getRevisionAuthor(r);
+ var lines = _getPadLines(pad, r-1);
+ var wirePool = new AttribPool();
+ var forwards = pad.getRevisionChangeset(r);
+ var backwards = Changeset.inverse(forwards, lines.textlines,
+ lines.alines, pad.pool());
+ var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.pool(),
+ wirePool);
+ var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.pool(),
+ wirePool);
+
+ function revTime(r) {
+ var date = pad.getRevisionDate(r);
+ var s = Math.floor((+date)/1000);
+ //java.lang.System.out.println("time "+r+": "+s);
+ return s;
+ }
+
+ var msg = {type:"NEW_CHANGES", newRev:r,
+ changeset: forwards2,
+ changesetBack: backwards2,
+ apool: wirePool.toJsonable(),
+ author: author,
+ timeDelta: revTime(r) - revTime(r-1) };
+ collabroom_server.sendMessage(connectionId, msg);
+ }
+ conn.data.lastRev = pad.getHeadRevisionNumber();
+ collabroom_server.updateRoomConnectionData(connectionId, conn.data);
+}
+
+function sendMessageToPadConnections(pad, msg) {
+ var padId = pad.getId();
+ var roId = padIdToReadonly(padId);
+
+ function update(connection) {
+ collabroom_server.sendMessage(connection.connectionId, msg);
+ }
+
+ collabroom_server.getRoomConnections(_viewIdToRoom(padId)).forEach(update);
+ collabroom_server.getRoomConnections(_viewIdToRoom(roId)).forEach(update);
+}
+
+function updateUserInfo(pad, userInfo) {
+ var msg = { type:"NEW_AUTHORDATA",
+ author: userInfo.userId,
+ data: {} };
+ var hasData = false;
+ if ((typeof (userInfo.colorId)) == "number") {
+ msg.data.colorId = userInfo.colorId;
+ hasData = true;
+ }
+ if (userInfo.name) {
+ msg.data.name = userInfo.name;
+ hasData = true;
+ }
+ if (hasData) {
+ sendMessageToPadConnections(pad, msg);
+ }
+}
+
+function broadcastNewRevision(pad, revObj) {
+ var msg = { type:"NEW_SAVEDREV",
+ savedRev: revObj };
+
+ delete revObj.ip; // we try not to share info like IP addresses on slider
+
+ sendMessageToPadConnections(pad, msg);
+}
diff --git a/etherpad/src/etherpad/collab/server_utils.js b/etherpad/src/etherpad/collab/server_utils.js
new file mode 100644
index 0000000..ece3aea
--- /dev/null
+++ b/etherpad/src/etherpad/collab/server_utils.js
@@ -0,0 +1,204 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("comet");
+import("ejs");
+import("etherpad.collab.ace.easysync2.{AttribPool,Changeset}");
+import("etherpad.log");
+import("etherpad.pad.activepads");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padevents");
+import("etherpad.pro.pro_padmeta");
+import("fastJSON");
+import("fileutils.readFile");
+import("jsutils.eachProperty");
+
+jimport("java.util.Random");
+jimport("java.lang.System");
+
+import("etherpad.collab.collab_server");
+// importClass(java.util.Random);
+// importClass(java.lang.System);
+
+var _serverDebug = function() {};
+var _dmesg = function() { System.out.println(arguments[0]+""); };
+
+/// Begin readonly/padId conversion code
+/// TODO: refactor into new file?
+var _baseRandomNumber = 0x123123; // keep this number seekrit
+
+function _map(array, func) {
+ for(var i=0; i<array.length; i++) {
+ array[i] = func(array[i]);
+ }
+ return array;
+}
+
+function parseUrlId(readOnlyIdOrLocalPadId) {
+ var localPadId;
+ var viewId;
+ var isReadOnly;
+ var roPadId;
+ var globalPadId;
+ if(isReadOnlyId(readOnlyIdOrLocalPadId)) {
+ isReadOnly = true;
+ globalPadId = readonlyToPadId(readOnlyIdOrLocalPadId);
+ localPadId = padutils.globalToLocalId(globalPadId);
+ var globalPadIdCheck = padutils.getGlobalPadId(localPadId);
+ if (globalPadId != globalPadIdCheck) {
+ // domain doesn't match
+ response.forbid();
+ }
+ roPadId = readOnlyIdOrLocalPadId;
+ viewId = roPadId;
+ }
+ else {
+ isReadOnly = false;
+ localPadId = readOnlyIdOrLocalPadId;
+ globalPadId = padutils.getGlobalPadId(localPadId);
+ viewId = globalPadId;
+ roPadId = padIdToReadonly(globalPadId);
+ }
+
+ return {localPadId:localPadId, viewId:viewId, isReadOnly:isReadOnly,
+ roPadId:roPadId, globalPadId:globalPadId};
+}
+
+function isReadOnlyId(str) {
+ return str.indexOf("ro.") == 0;
+}
+
+/*
+ for now, we just make it 'hard to guess'
+ TODO: make it impossible to find read/write page through hash
+*/
+function readonlyToPadId (readOnlyHash) {
+
+ // readOnly hashes must start with 'ro-'
+ if(!isReadOnlyId(readOnlyHash)) return null;
+ else {
+ readOnlyHash = readOnlyHash.substring(3, readOnlyHash.length);
+ }
+
+ // convert string to series of numbers between 1 and 64
+ var result = _strToArray(readOnlyHash);
+
+ var sum = result.pop();
+ // using a secret seed to util.random, transform each number using + and %
+ var seed = _baseRandomNumber + sum;
+ var rand = new Random(seed);
+
+ _map(result, function(elem) {
+ return ((64 + elem - rand.nextInt(64)) % 64);
+ });
+
+ // convert array of numbers back to a string
+ return _arrayToStr(result);
+}
+
+/*
+ Temporary code. see comment at readonlyToPadId.
+*/
+function padIdToReadonly (padid) {
+ var result = _strToArray(padid);
+ var sum = 0;
+
+ if(padid.length > 1) {
+ for(var i=0; i<result.length; i++) {
+ sum = (sum + result[i] + 1) % 64;
+ }
+ } else {
+ sum = 64;
+ }
+
+ var seed = _baseRandomNumber + sum;
+ var rand = new Random(seed);
+
+ _map(result, function(elem) {
+ var randnum = rand.nextInt(64);
+ return ((elem + randnum) % 64);
+ });
+
+ result.push(sum);
+ return "ro." + _arrayToStr(result);
+}
+
+// little reversable string encoding function
+// 0-9 are the numbers 0-9
+// 10-35 are the uppercase letters A-Z
+// 36-61 are the lowercase letters a-z
+// 62 are all other characters
+function _strToArray(str) {
+ var result = new Array(str.length);
+ for(var i=0; i<str.length; i++) {
+ result[i] = str.charCodeAt(i);
+
+ if (_between(result[i], '0'.charCodeAt(0), '9'.charCodeAt(0))) {
+ result[i] -= '0'.charCodeAt(0);
+ }
+ else if(_between(result[i], 'A'.charCodeAt(0), 'Z'.charCodeAt(0))) {
+ result[i] -= 'A'.charCodeAt(0); // A becomes 0
+ result[i] += 10; // A becomes 10
+ }
+ else if(_between(result[i], 'a'.charCodeAt(0), 'z'.charCodeAt(0))) {
+ result[i] -= 'a'.charCodeAt(0); // a becomes 0
+ result[i] += 36; // a becomes 36
+ } else if(result[i] == '$'.charCodeAt(0)) {
+ result[i] = 62;
+ } else {
+ result[i] = 63; // if not alphanumeric or $, we default to 63
+ }
+ }
+ return result;
+}
+
+function _arrayToStr(array) {
+ var result = "";
+ for(var i=0; i<array.length; i++) {
+ if(_between(array[i], 0, 9)) {
+ result += String.fromCharCode(array[i] + '0'.charCodeAt(0));
+ }
+ else if(_between(array[i], 10, 35)) {
+ result += String.fromCharCode(array[i] - 10 + 'A'.charCodeAt(0));
+ }
+ else if(_between(array[i], 36, 61)) {
+ result += String.fromCharCode(array[i] - 36 + 'a'.charCodeAt(0));
+ }
+ else if(array[i] == 62) {
+ result += "$";
+ } else {
+ result += "-";
+ }
+ }
+ return result;
+}
+
+function _between(charcode, start, end) {
+ return charcode >= start && charcode <= end;
+}
+
+/* a short little testing function, converts back and forth */
+// function _testEncrypt(str) {
+// var encrypted = padIdToReadonly(str);
+// var decrypted = readonlyToPadId(encrypted);
+// _dmesg(str + " " + encrypted + " " + decrypted);
+// if(decrypted != str) {
+// _dmesg("ERROR: " + str + " and " + decrypted + " do not match");
+// }
+// }
+
+// _testEncrypt("testing$");
diff --git a/etherpad/src/etherpad/control/aboutcontrol.js b/etherpad/src/etherpad/control/aboutcontrol.js
new file mode 100644
index 0000000..9d77142
--- /dev/null
+++ b/etherpad/src/etherpad/control/aboutcontrol.js
@@ -0,0 +1,263 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("email.sendEmail");
+import("funhtml.*", "stringutils.*");
+import("netutils");
+import("execution");
+
+import("etherpad.utils.*");
+import("etherpad.log");
+import("etherpad.globals.*");
+import("etherpad.quotas");
+import("etherpad.sessions.getSession");
+import("etherpad.store.eepnet_trial");
+import("etherpad.store.checkout");
+import("etherpad.store.eepnet_checkout");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+
+function render_product() {
+ if (request.params.from) { response.redirect(request.path); }
+ renderFramed("about/product_body.ejs");
+}
+
+function render_faq() {
+ renderFramed("about/faq_body.ejs", {
+ LI: LI,
+ H2: H2,
+ A: A,
+ html: html
+ });
+}
+
+function render_pne_faq() {
+ renderFramed("about/pne-faq.ejs");
+}
+
+function render_company() {
+ renderFramed("about/company_body.ejs");
+}
+
+function render_contact() {
+ renderFramed("about/contact_body.ejs");
+}
+
+function render_privacy() {
+ renderFramed("about/privacy_body.ejs");
+}
+
+function render_tos() {
+ renderFramed("about/tos_body.ejs");
+}
+
+function render_testimonials() {
+ renderFramed("about/testimonials.ejs");
+}
+
+function render_appjet() {
+ response.redirect("/ep/blog/posts/etherpad-and-appjet");
+// renderFramed("about/appjet_body.ejs");
+}
+
+function render_screencast() {
+ if (request.params.from) { response.redirect(request.path); }
+ var screencastUrl;
+// if (isProduction()) {
+ screencastUrl = encodeURIComponent("http://etherpad.s3.amazonaws.com/epscreencast800x600.flv");
+// } else {
+// screencastUrl = encodeURIComponent("/static/flv/epscreencast800x600.flv");
+// }
+ renderFramed("about/screencast_body.ejs", {screencastUrl: screencastUrl});
+}
+
+function render_forums() {
+ renderFramed("about/forums_body.ejs");
+}
+
+function render_blog() {
+ renderFramed("about/blog_body.ejs");
+}
+
+function render_really_real_time() {
+ renderFramed("about/simultaneously.ejs");
+}
+
+function render_simultaneously() {
+ renderFramed("about/simultaneously.ejs");
+}
+
+//----------------------------------------------------------------
+// pricing
+//----------------------------------------------------------------
+
+function render_pricing() {
+ renderFramed("about/pricing.ejs", {
+ trialDays: eepnet_trial.getTrialDays(),
+ costPerUser: checkout.dollars(eepnet_checkout.COST_PER_USER)
+ });
+}
+
+function render_pricing_free() {
+ renderFramed("about/pricing_free.ejs", {
+ maxUsersPerPad: quotas.getMaxSimultaneousPadEditors()
+ });
+}
+
+function render_pricing_eepnet() {
+ renderFramed("about/pricing_eepnet.ejs", {
+ trialDays: eepnet_trial.getTrialDays(),
+ costPerUser: checkout.dollars(eepnet_checkout.COST_PER_USER)
+ });
+}
+
+function render_pricing_pro() {
+ renderFramed("about/pricing_pro.ejs", {});
+}
+
+function render_eepnet_pricing_contact_post() {
+ response.setContentType("text/plain; charset=utf-8");
+ var data = {};
+ var fields = ['firstName', 'lastName', 'email', 'orgName',
+ 'jobTitle', 'phone', 'estUsers', 'industry'];
+
+ if (!getSession().pricingContactData) {
+ getSession().pricingContactData = {};
+ }
+
+ function err(m) {
+ response.write(m);
+ response.stop();
+ }
+
+ fields.forEach(function(f) {
+ getSession().pricingContactData[f] = request.params[f];
+ });
+
+ fields.forEach(function(f) {
+ data[f] = request.params[f];
+ if (!(data[f] && (data[f].length > 0))) {
+ err("All fields are required.");
+ }
+ });
+
+ if (!isValidEmail(data.email)) {
+ err("Error: Invalid Email");
+ }
+
+ // log this data to a file
+ fields.ip = request.clientAddr;
+ fields.sessionReferer = getSession().initialReferer;
+ log.custom("eepnet_pricing_inquiry", fields);
+
+ // submit web2lead
+ var ref = getSession().initialReferer;
+ var googleQuery = extractGoogleQuery(ref);
+ var wlparams = {
+ oid: "00D80000000b7ey",
+ first_name: data.firstName,
+ last_name: data.lastName,
+ email: data.email,
+ company: data.orgName,
+ title: data.jobTitle,
+ phone: data.phone,
+ '00N80000003FYtG': data.estUsers,
+ '00N80000003FYto': ref,
+ '00N80000003FYuI': googleQuery,
+ lead_source: 'EEPNET Pricing Inquiry',
+ industry: data.industry,
+ retURL: 'http://'+request.host+'/ep/store/salesforce-web2lead-ok'
+ };
+
+ var result = netutils.urlPost(
+ "http://www.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8",
+ wlparams, {});
+
+ // now send an email sales notification
+ var hostname = ipToHostname(request.clientAddr) || "unknown";
+ var subject = 'EEPNET Pricing Inquiry: '+data.email+' / '+hostname;
+ var body = [
+ "", "This is an automated email.", "",
+ data.firstName+" "+data.lastName+" ("+data.orgName+") has inquired about EEPNET pricing.",
+ "",
+ "This record has automatically been added to SalesForce. See the salesforce lead page for more details.",
+ "", "Session Referer: "+ref, ""
+ ].join("\n");
+ var toAddr = 'sales@pad.spline.inf.fu-berlin.de';
+ if (isTestEmail(data.email)) {
+ toAddr = 'blackhole@appjet.com';
+ }
+ sendEmail(toAddr, 'sales@pad.spline.inf.fu-berlin.de', subject, {}, body);
+
+ // all done!
+ response.write("OK");
+}
+
+function render_pricing_interest_signup() {
+ response.setContentType('text/plain; charset=utf-8');
+
+ var email = request.params.email;
+ var interestedNet = request.params.interested_net;
+ var interestedHosted = request.params.interested_hosted;
+
+ if (!isValidEmail(email)) {
+ response.write("Error: Invalid Email");
+ response.stop();
+ }
+
+ log.custom("pricing_interest",
+ {email: email,
+ net: interestedNet,
+ hosted: interestedHosted});
+
+ response.write('OK');
+}
+
+function render_pricing_eepnet_users() {
+ renderFramed('about/pricing_eepnet_users.ejs', {});
+}
+
+function render_pricing_eepnet_support() {
+ renderFramed('about/pricing_eepnet_support.ejs', {});
+}
+
+
+//------------------------------------------------------------
+// survey
+
+function render_survey() {
+ var id = request.params.id;
+ log.custom("pro-user-survey", { surveyProAccountId: (id || "unknown") });
+ response.redirect("http://www.surveymonkey.com/s.aspx?sm=yT3ALP0pb_2fP_2bHtcfzvpkXQ_3d_3d");
+}
+
+
+//------------------------------------------------------------
+
+import("etherpad.billing.billing");
+
+function render_testbillingnotify() {
+ var ret = billing.handlePaypalNotification();
+ if (ret.status == 'completion') {
+ // do something with purchase ret.purchaseInfo
+ } else if (ret.status != 'redundant') {
+ java.lang.System.out.println("Whoa error: "+ret.toSource());
+ }
+ response.write("ok");
+}
+
diff --git a/etherpad/src/etherpad/control/admin/pluginmanager.js b/etherpad/src/etherpad/control/admin/pluginmanager.js
new file mode 100644
index 0000000..c4bee5b
--- /dev/null
+++ b/etherpad/src/etherpad/control/admin/pluginmanager.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ *
+ * 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("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.collab.server_utils");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padusers");
+import("etherpad.pro.pro_utils");
+import("etherpad.helpers");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.admin.plugins");
+import("etherpad.pad.padutils");
+
+
+function onRequest() {
+ plugins.loadPlugins();
+
+ if (request.params.action == 'install') {
+ plugins.enablePlugin(request.params.plugin);
+ } else if (request.params.action == 'uninstall') {
+ plugins.disablePlugin(request.params.plugin);
+ } else if (request.params.action == 'reinstall') {
+ plugins.disablePlugin(request.params.plugin);
+ plugins.loadPlugins(1);
+ plugins.enablePlugin(request.params.plugin);
+ }
+
+ helpers.addClientVars({
+ userAgent: request.headers["User-Agent"],
+ debugEnabled: request.params.djs,
+ clientIp: request.clientAddr,
+ colorPalette: COLOR_PALETTE,
+ serverTimestamp: +(new Date),
+ isProPad: pro_utils.isProDomainRequest(),
+ userIsGuest: padusers.isGuest(padusers.getUserId()),
+ userId: padusers.getUserId(),
+ });
+
+
+ padutils.setOptsAndCookiePrefs(request);
+ var prefs = helpers.getClientVar('cookiePrefsToSet');
+ var bodyClass = (prefs.isFullWidth ? "fullwidth" : "limwidth")
+
+ renderHtml("admin/pluginmanager.ejs",
+ {
+ prefs: prefs,
+ config: appjet.config,
+ bodyClass: 'nonpropad',
+ isPro: pro_utils.isProDomainRequest(),
+ isProAccountHolder: pro_utils.isProDomainRequest() && ! padusers.isGuest(padusers.getUserId()),
+ account: getSessionProAccount(), // may be falsy
+ });
+ return true;
+}
diff --git a/etherpad/src/etherpad/control/admincontrol.js b/etherpad/src/etherpad/control/admincontrol.js
new file mode 100644
index 0000000..ec48824
--- /dev/null
+++ b/etherpad/src/etherpad/control/admincontrol.js
@@ -0,0 +1,1482 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("netutils");
+import("funhtml.*");
+import("stringutils.{html,sprintf,startsWith,md5}");
+import("jsutils.*");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("varz");
+import("comet");
+import("dispatch.{Dispatcher,PrefixMatcher,DirMatcher,forward}");
+
+import("etherpad.billing.team_billing");
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.licensing");
+import("etherpad.sessions.getSession");
+import("etherpad.sessions");
+import("etherpad.statistics.statistics");
+import("etherpad.log");
+import("etherpad.admin.shell");
+import("etherpad.usage_stats.usage_stats");
+import("etherpad.control.blogcontrol");
+import("etherpad.control.pro_beta_control");
+import("etherpad.control.statscontrol");
+import("etherpad.statistics.exceptions");
+import("etherpad.store.checkout");
+
+import("etherpad.pad.activepads");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.dbwriter");
+import("etherpad.collab.collab_server");
+
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.domains");
+import("etherpad.admin.plugins");
+import("etherpad.control.admin.pluginmanager");
+
+jimport("java.lang.System.out.println");
+
+jimport("net.appjet.oui.cometlatencies");
+jimport("net.appjet.oui.appstats");
+
+
+//----------------------------------------------------------------
+
+function _isAuthorizedAdmin() {
+ if (!isProduction()) {
+ return true;
+ }
+ return (getSession().adminAuth === true);
+}
+
+var _mainLinks = [
+ ['exceptions', 'Exceptions Monitor'],
+ ['usagestats/', 'Usage Stats'],
+ ['padinspector', 'Pad Inspector'],
+ ['dashboard', 'Dashboard'],
+ ['eepnet-licenses', 'EEPNET Licenses'],
+ ['config', 'appjet.config'],
+ ['shell', 'Shell'],
+ ['timings', 'timing data'],
+ ['broadcast-message', 'Pad Broadcast'],
+// ['analytics', 'Google Analytics'],
+ ['varz', 'varz'],
+ ['genlicense', 'Manually generate a license key'],
+ ['flows', 'Flows (warning: slow)'],
+ ['diagnostics', 'Pad Connection Diagnostics'],
+ ['cachebrowser', 'Cache Browser'],
+ ['pne-tracker', 'PNE Tracking Stats'],
+ ['reload-blog-db', 'Reload blog DB'],
+ ['pro-domain-accounts', 'Pro Domain Accounts'],
+ ['beta-valve', 'Beta Valve'],
+ ['reset-subscription', "Reset Subscription"],
+ ['pluginmanager/', "Plugin manager"]
+];
+
+function onRequest(name) {
+ if (name == "auth") {
+ return;
+ }
+ if (!_isAuthorizedAdmin()) {
+ getSession().cont = request.path;
+ response.redirect('/ep/admin/auth');
+ }
+
+ var disp = new Dispatcher();
+
+ disp.addLocations(plugins.callHook("handleAdminPath"));
+
+ disp.addLocations([
+ [PrefixMatcher('/ep/admin/pluginmanager/'), forward(pluginmanager)],
+ [PrefixMatcher('/ep/admin/usagestats/'), forward(statscontrol)]
+ ]);
+
+ return disp.dispatch();
+}
+
+function _commonHead() {
+ return HEAD(STYLE(
+ "html {font-family:Verdana,Helvetica,sans-serif;}",
+ "body {padding: 2em;}"
+ ));
+}
+
+//----------------------------------------------------------------
+
+function render_auth() {
+ var cont = getSession().cont;
+ if (getSession().message) {
+ response.write(DIV(P(B(getSession().message))));
+ delete getSession().message;
+ }
+ if (request.method == "GET") {
+ response.write(FORM({method: "POST", action: request.path},
+ P("Are you an admin?"),
+ LABEL("Password:"),
+ INPUT({type: "password", name: "password", value: ""}),
+ INPUT({type: "submit", value: "submit"})
+ ));
+ }
+ if (request.method == "POST") {
+ var pass = request.params.password;
+ if (pass === appjet.config['etherpad.adminPass']) {
+ getSession().adminAuth = true;
+ if (cont) {
+ response.redirect(cont);
+ } else {
+ response.redirect("/ep/admin/main");
+ }
+ } else {
+ getSession().message = "Bad Password.";
+ response.redirect(request.path);
+ }
+ }
+}
+
+function render_main() {
+ var div = DIV();
+
+ div.push(A({href: "/"}, html("&laquo;"), " home"));
+ div.push(H1("Admin"));
+
+ function addMenuItem(l) {
+ div.push(DIV(A({href: l[0]}, l[1])));
+ }
+
+ plugins.callHook("adminMenu").forEach(addMenuItem);
+ _mainLinks.forEach(addMenuItem);
+
+ if (sessions.isAnEtherpadAdmin()) {
+ div.push(P(A({href: "/ep/admin/setadminmode?v=false"},
+ "Exit Admin Mode")));
+ }
+ else {
+ div.push(P(A({href: "/ep/admin/setadminmode?v=true"},
+ "Enter Admin Mode")));
+ }
+ response.write(HTML(_commonHead(), BODY(div)));
+}
+
+//----------------------------------------------------------------
+
+function render_config() {
+
+ vars = [];
+ eachProperty(appjet.config, function(k,v) {
+ vars.push(k);
+ });
+
+ vars.sort();
+
+ response.setContentType('text/plain; charset=utf-8');
+ vars.forEach(function(v) {
+ response.write("appjet.config."+v+" = "+appjet.config[v]+"\n");
+ });
+}
+
+//----------------------------------------------------------------
+
+function render_test() {
+ response.setContentType("text/plain");
+ response.write(Packages.net.appjet.common.util.ExpiringMapping + "\n");
+ var m = new Packages.net.appjet.common.util.ExpiringMapping(10 * 1000);
+ response.write(m.toString() + "\n");
+ m.get("test");
+ return;
+ response.write(m.toString());
+}
+
+function render_dashboard() {
+ var body = BODY();
+ body.push(A({href: '/ep/admin/'}, html("&laquo; Admin")));
+ body.push(H1({style: "border-bottom: 1px solid black;"}, "Dashboard"));
+
+ /*
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "License"));
+ var license = licensing.getLicense();
+ body.push(P(TT(" Licensed To (name): "+license.personName)));
+ body.push(P(TT(" Licensed To (organization): "+license.organizationName)));
+ body.push(P(TT(" Software Edition: "+license.editionName)));
+ var quota = ((license.userQuota > 0) ? license.userQuota : 'unlimited');
+ body.push(P(TT(" User Quota: "+quota)));
+ var expires = (license.expiresDate ? (license.expiresDate.toString()) : 'never');
+ body.push(P(TT(" Expires: "+expires)));
+ */
+
+ /*
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Active User Quota"));
+
+ var activeUserCount = licensing.getActiveUserCount();
+ var activeUserQuota = licensing.getActiveUserQuota();
+ var activeUserWindowStart = licensing.getActiveUserWindowStart();
+
+ body.push(P(TT(" Since ", B(activeUserWindowStart.toString()), ", ",
+ "you have used ", B(activeUserCount), " of ", B(activeUserQuota),
+ " active users.")));
+*/
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Uptime"));
+ body.push(P({style: "margin-left: 25px;"}, "Server running for "+renderServerUptime()+"."))
+
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Response codes"));
+ body.push(renderResponseCodes());
+
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Comet Connections"));
+ body.push(renderPadConnections());
+
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Comet Stats"));
+ body.push(renderCometStats());
+
+ body.push(H2({style: "color: #226; font-size: 1em;"}, "Recurring revenue, monthly"));
+ body.push(renderRevenueStats());
+
+ response.write(HTML(_commonHead(), body));
+}
+
+// Note: This function is called by the PNE dashboard (pro_admin_control.js)! Be careful.
+function renderPadConnections() {
+ var d = DIV();
+ var lastCount = cometlatencies.lastCount();
+
+ if (lastCount.isDefined()) {
+ var countMap = {};
+ Array.prototype.map.call(lastCount.get().elements().collect().toArray().unbox(
+ java.lang.Class.forName("java.lang.Object")),
+ function(x) {
+ countMap[x._1()] = x._2();
+ });
+ var totalConnected = 0;
+ var ul = UL();
+ eachProperty(countMap, function(k,v) {
+ ul.push(LI(k+": "+v));
+ if (/^\d+$/.test(v)) {
+ totalConnected += Number(v);
+ }
+ });
+ ul.push(LI(B("Total: ", totalConnected)));
+ d.push(ul);
+ } else {
+ d.push("Still collecting data... check back in a minute.");
+ }
+ return d;
+}
+
+// Note: This function is called by the PNE dashboard (pro_admin_control.js)! Be careful.
+function renderCometStats() {
+ var d = DIV();
+ var lastStats = cometlatencies.lastStats();
+ var lastCount = cometlatencies.lastCount();
+
+
+ if (lastStats.isDefined()) {
+ d.push(P("Realtime transport latency percentiles (microseconds):"));
+ var ul = UL();
+ lastStats.map(scalaF1(function(s) {
+ ['50', '90', '95', '99', 'max'].forEach(function(id) {
+ var fn = id;
+ if (id != "max") {
+ fn = ("p"+fn);
+ id = id+"%";
+ }
+ ul.push(LI(id, ": <", s[fn](), html("&micro;"), "s"));
+ });
+ }));
+ d.push(ul);
+ } else {
+ d.push(P("Still collecting data... check back in a minutes."));
+ }
+
+ /* ["p50", "p90", "p95", "p99", "max"].forEach(function(id) {
+ ul.push(LI(B(
+
+ return DIV(P(sprintf("50%% %d\t90%% %d\t95%% %d\t99%% %d\tmax %d",
+ s.p50(), s.p90(), s.p95(), s.p99(), s.max())),
+ P(sprintf("%d total messages", s.count())));
+ }})).get();*/
+
+
+ return d;
+}
+
+// Note: This function is called by the PNE dashboard (pro_admin_control.js)! Be careful.
+function renderResponseCodes() {
+ var statusCodeFrequencyNames = ["minute", "hour", "day", "week"];
+ var data = { };
+ var statusCodes = appstats.stati();
+ for (var i = 0; i < statusCodes.length; ++i) {
+ var name = statusCodeFrequencyNames[i];
+ var map = statusCodes[i];
+ map.foreach(scalaF1(function(pair) {
+ if (! (pair._1() in data)) data[pair._1()] = {};
+ var scmap = data[pair._1()];
+ scmap[name] = pair._2().count();
+ }));
+ };
+ var stats = TABLE({id: "responsecodes-table", style: "margin-left: 25px;",
+ border: 1, cellspacing: 0, cellpadding: 4},
+ TR.apply(TR, statusCodeFrequencyNames.map(function(name) {
+ return TH({colspan: 2}, "Last", html("&nbsp;"), name);
+ })));
+ var sortedStati = [];
+ eachProperty(data, function(k) {
+ sortedStati.push(k);
+ });
+ sortedStati.sort();
+ sortedStati.forEach(function(k, i) { // k is status code.
+ var row = TR();
+ statusCodeFrequencyNames.forEach(function(name) {
+ row.push(TD({style: 'width: 2em;'}, data[k][name] ? k+":" : ""));
+ row.push(TD(data[k][name] ? data[k][name] : ""));
+ });
+ stats.push(row);
+ });
+ return stats;
+}
+
+// Note: This function is called by the PNE dashboard (pro_admin_control.js)! Be careful.
+function renderServerUptime() {
+ var labels = ["seconds", "minutes", "hours", "days"];
+ var ratios = [60, 60, 24];
+ var time = appjet.uptime / 1000;
+ var pos = 0;
+ while (pos < ratios.length && time / ratios[pos] > 1.1) {
+ time = time / ratios[pos];
+ pos++;
+ }
+ return sprintf("%.1f %s", time, labels[pos]);
+}
+
+function renderRevenueStats() {
+ var subs = team_billing.getAllSubscriptions();
+ var total = 0;
+ var totalUsers = 0;
+ subs.forEach(function(sub) {
+ var users = team_billing.getMaxUsers(sub.customer);
+ var cost = team_billing.calculateSubscriptionCost(users, sub.coupon);
+ if (cost > 0) {
+ totalUsers += users;
+ total += cost;
+ }
+ });
+ return "US $"+checkout.dollars(total)+", from "+subs.length+" domains and "+totalUsers+" users.";
+}
+
+//----------------------------------------------------------------
+// Broadcasting Messages
+//----------------------------------------------------------------
+
+function render_broadcast_message_get() {
+ var body = BODY(FORM({action: request.path, method: 'post'},
+ H3('Broadcast Message to All Active Pad Clients:'),
+ TEXTAREA({name: 'msgtext', style: 'width: 100%; height: 100px;'}),
+ H3('JavaScript code to be eval()ed on client (optional, be careful!): '),
+ TEXTAREA({name: 'jscode', style: 'width: 100%; height: 100px;'}),
+ INPUT({type: 'submit', value: 'Broadcast Now'})));
+ response.write(HTML(body));
+}
+
+function render_broadcast_message_post() {
+ var msgText = request.params.msgtext;
+ var jsCode = request.params.jscode;
+ if (!(msgText || jsCode)) {
+ response.write("No mesage text or jscode specified.");
+ response.stop();
+ return;
+ }
+ collab_server.broadcastServerMessage({
+ type: 'NOTICE',
+ text: msgText,
+ js: jsCode
+ });
+ response.write(HTML(BODY(P("OK"), P(A({href: request.path}, "back")))));
+}
+
+function render_shell() {
+ shell.handleRequest();
+}
+
+//----------------------------------------------------------------
+// pad inspector
+//----------------------------------------------------------------
+
+function _getPadUrl(globalPadId) {
+ var superdomain = pro_utils.getRequestSuperdomain();
+ var domain;
+ if (padutils.isProPadId(globalPadId)) {
+ var domainId = padutils.getDomainId(globalPadId);
+ domain = domains.getDomainRecord(domainId).subDomain +
+ '.' + superdomain;
+ }
+ else {
+ domain = superdomain;
+ }
+ var localId = padutils.globalToLocalId(globalPadId);
+ return "http://"+httpHost(domain)+"/"+localId;
+}
+
+function render_padinspector_get() {
+ var padId = request.params.padId;
+ if (!padId) {
+ response.write(FORM({action: request.path, method: 'get', style: 'border: 1px solid #ccc; background-color: #eee; padding: .2em 1em;'},
+ P("Pad Lookup: ",
+ INPUT({name: 'padId', value: '<enter pad id>'}),
+ INPUT({type: 'submit'}))));
+
+ // show recently active pads; the number of them may vary; lots of
+ // activity in a pad will push others off the list
+ response.write(H3("Recently Active Pads:"));
+ var recentlyActiveTable = TABLE({cellspacing: 0, cellpadding: 6, border: 1,
+ style: 'font-family: monospace;'});
+ var recentPads = activepads.getActivePads();
+ recentPads.forEach(function (info) {
+ var time = info.timestamp; // number
+ var pid = info.padId;
+ model.accessPadGlobal(pid, function(pad) {
+ if (pad.exists()) {
+ var numRevisions = pad.getHeadRevisionNumber();
+ var connected = collab_server.getNumConnections(pad);
+ recentlyActiveTable.push(
+ TR(TD(B(pid)),
+ TD({style: 'font-style: italic;'}, timeAgo(time)),
+ TD(connected+" connected"),
+ TD(numRevisions+" revisions"),
+ TD(A({href: qpath({padId: pid, revtext: "HEAD"})}, "HEAD")),
+ TD(A({href: qpath({padId: pid})}, "inspect")),
+ TD(A({href: qpath({padId: pid, snoop: 1})}, "snoop"))
+ ));
+ }
+ }, "r");
+ });
+ response.write(recentlyActiveTable);
+ response.stop();
+ }
+ if (startsWith(padId, '/')) {
+ padId = padId.substr(1);
+ }
+ if (request.params.snoop) {
+ sessions.setIsAnEtherpadAdmin(true);
+ response.redirect(_getPadUrl(padId));
+ }
+ if (request.params.setsupportstimeslider) {
+ var v = (String(request.params.setsupportstimeslider).toLowerCase() ==
+ 'true');
+ model.accessPadGlobal(padId, function(pad) {
+ pad.setSupportsTimeSlider(v);
+ });
+ response.write("on pad "+padId+": setSupportsTimeSlider("+v+")");
+ response.stop();
+ }
+ model.accessPadGlobal(padId, function(pad) {
+ if (! pad.exists()) {
+ response.write("Pad not found: /"+padId);
+ }
+ else {
+ var headRev = pad.getHeadRevisionNumber();
+ var div = DIV({style: 'font-family: monospace;'});
+
+ if (request.params.revtext) {
+ var i;
+ if (request.params.revtext == "HEAD") {
+ i = headRev;
+ } else {
+ i = Number(request.params.revtext);
+ }
+ var infoObj = {};
+ div.push(H2(A({href: request.path}, "PadInspector"),
+ ' > ', A({href: request.path+'?padId='+padId}, "/"+padId),
+ ' > ', "Revision ", i, "/", headRev,
+ SPAN({style: 'color: #949;'}, ' [ ', pad.getRevisionDate(i).toString(), ' ] ')));
+ div.push(H3("Browse Revisions: ",
+ ((i > 0) ? A({id: 'previous', href: qpath({revtext: (i-1)})}, '<< previous') : ''),
+ ' ',
+ ((i < pad.getHeadRevisionNumber()) ? A({id: 'next', href: qpath({revtext:(i+1)})}, 'next >>') : '')),
+ DIV({style: 'padding: 1em; border: 1px solid #ccc;'},
+ pad.getRevisionText(i, infoObj)));
+ if (infoObj.badLastChar) {
+ div.push(P("Bad last character of text (not newline): "+infoObj.badLastChar));
+ }
+ } else if (request.params.dumpstorage) {
+ div.push(P(collab_server.dumpStorageToString(pad)));
+ } else if (request.params.showlatest) {
+ div.push(P(pad.text()));
+ } else {
+ div.push(H2(A({href: request.path}, "PadInspector"), ' > ', "/"+padId));
+ // no action
+ div.push(P(A({href: qpath({revtext: 'HEAD'})}, 'HEAD='+headRev)));
+ div.push(P(A({href: qpath({dumpstorage: 1})}, 'dumpstorage')));
+ var supportsTimeSlider = pad.getSupportsTimeSlider();
+ if (supportsTimeSlider) {
+ div.push(P(A({href: qpath({setsupportstimeslider: 'false'})}, 'hide slider')));
+ }
+ else {
+ div.push(P(A({href: qpath({setsupportstimeslider: 'true'})}, 'show slider')));
+ }
+ }
+ }
+
+ var script = SCRIPT({type: 'text/javascript'}, html([
+ '$(document).keydown(function(e) {',
+ ' var h = undefined;',
+ ' if (e.keyCode == 37) { h = $("#previous").attr("href"); }',
+ ' if (e.keyCode == 39) { h = $("#next").attr("href"); }',
+ ' if (h) { window.location.href = h; }',
+ '});'
+ ].join('\n')));
+
+ response.write(HTML(
+ HEAD(SCRIPT({type: 'text/javascript', src: '/static/js/jquery-1.3.2.js?'+(+(new Date))})),
+ BODY(div, script)));
+ }, "r");
+}
+
+function render_analytics() {
+ response.redirect("https://www.google.com/analytics/reporting/?reset=1&id=12611622");
+}
+
+//----------------------------------------------------------------
+// eepnet license display
+//----------------------------------------------------------------
+
+function render_eepnet_licenses() {
+ var data = sqlobj.selectMulti('eepnet_signups', {}, {orderBy: 'date'});
+ var t = TABLE({border: 1, cellspacing: 0, cellpadding: 2});
+ var cols = ['date','email','orgName','firstName','lastName', 'jobTitle','phone','estUsers'];
+ data.forEach(function(x) {
+ var tr = TR();
+ cols.forEach(function(colname) {
+ tr.push(TD(x[colname]));
+ });
+ t.push(tr);
+ });
+ response.write(HTML(BODY({style: 'font-family: monospace;'}, t)));
+}
+
+//----------------------------------------------------------------
+// pad integrity
+//----------------------------------------------------------------
+
+/*function render_changesettest_get() {
+ var nums = [0, 1, 2, 3, 0xfffffff, 0x02345678, 4];
+ var str = Changeset.numberArrayToString(nums);
+ var result = Changeset.numberArrayFromString(str);
+ var resultArray = result[0];
+ var remainingString = result[1];
+ var bad = false;
+ if (remainingString) {
+ response.write(P("remaining string length is: "+remainingString.length));
+ bad = true;
+ }
+ if (nums.length != resultArray.length) {
+ response.write(P("length mismatch: "+nums.length+" / "+resultArray.length));
+ bad = true;
+ }
+ response.write(P(nums[2]));
+ for(var i=0;i<nums.length;i++) {
+ var a = nums[i];
+ var b = resultArray[i];
+ if (a !== b) {
+ response.write(P("mismatch at element "+i+": "+a+" / "+b));
+ bad = true;
+ }
+ }
+ if (! bad) {
+ response.write("SUCCESS");
+ }
+}*/
+
+/////////
+
+function render_appendtest() {
+ var padId = request.params.padId;
+ var mode = request.params.mode;
+ var text = request.params.text;
+
+ model.accessPadGlobal(padId, function(pad) {
+ if (mode == "append") {
+ collab_server.appendPadText(pad, text);
+ }
+ else if (mode == "replace") {
+ collab_server.setPadText(pad, text);
+ }
+ });
+}
+
+//function render_flushall() {
+// dbwriter.writeAllToDB(null, true);
+// response.write("OK");
+//}
+
+//function render_flushpad() {
+// var padId = request.params.padId;
+// model.accessPadGlobal(padId, function(pad) {
+// dbwriter.writePad(pad, true);
+// });
+// response.write("OK");
+//}
+
+/*function render_foo() {
+ locking.doWithPadLock("CAT", function() {
+ sqlbase.createJSONTable("STUFF");
+ sqlbase.putJSON("STUFF", "dogs", {very:"bad"});
+ response.write(sqlbase.getJSON("STUFF", "dogs")); // {very:"bad"}
+ response.write(',');
+ response.write(sqlbase.getJSON("STUFF", "cats")); // undefined
+ response.write("<br/>");
+
+ sqlbase.createStringArrayTable("SEQUENCES");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 0, "1");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 1, "1");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 2, "2");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 3, "3");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 4, "5");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 30, "number30");
+ sqlbase.putStringArrayElement("SEQUENCES", "fibo", 29, "number29");
+ sqlbase.deleteStringArrayElement("SEQUENCES", "fibo", 29);
+ sqlbase.putConsecutiveStringArrayElements("SEQUENCES", "fibo", 19, [19,20,21,22]);
+ var a = [];
+ for(var i=0;i<31;i++) {
+ a.push(sqlbase.getStringArrayElement("SEQUENCES", "fibo", i));
+ }
+ response.write(a.join(',')); // 1,1,2,3,5,,, ... 19,20,21,22, ... ,,,number30
+ });
+}*/
+
+function render_timings() {
+ var timer = Packages.net.appjet.ajstdlib.timer;
+ var opnames = timer.getOpNames();
+
+ response.write(P(A({href: '/ep/admin/timingsreset'}, "reset all")));
+
+ var t = TABLE({border: 1, cellspacing: 0, cellpadding: 3, style: 'font-family: monospace;'});
+ t.push(TR(TH("operation"),
+ TH("sample_count"),
+ TH("total_ms"),
+ TH("avg_ms")));
+
+ function r(x) {
+ return sprintf("%09.2f", x);
+ }
+ var rows = [];
+ for (var i = 0; i < opnames.length; i++) {
+ var stats = timer.getStats(opnames[i]);
+ rows.push([String(opnames[i]),
+ Math.floor(stats[0]),
+ stats[1],
+ stats[2]]);
+ }
+
+ var si = Number(request.params.sb || 0);
+
+ rows.sort(function(a,b) { return cmp(b[si],a[si]); });
+
+ rows.forEach(function(row) {
+ t.push(TR(TD(row[0]),
+ TD(row[1]),
+ TD(r(row[2])),
+ TD(r(row[3]))));
+ });
+
+ response.write(t);
+}
+
+function render_timingsreset() {
+ Packages.net.appjet.ajstdlib.timer.reset();
+ response.redirect('/ep/admin/timings');
+}
+
+// function render_jsontest() {
+// response.setContentType('text/plain; charset=utf-8');
+
+// var a = [];
+// a[0] = 5;
+// a[1] = 6;
+// a[9] = 8;
+// a['foo'] = "should appear";
+
+// jtest(a);
+
+// var obj1 = {
+// a: 1,
+// b: 2,
+// q: [true,true,,,,,,false,false,,,,{},{a:{a:{a:{a:{a:{a:[[{a:{a:false}}]]}}}}}}],
+// c: "foo",
+// d: {
+// nested: { obj: 'yo' },
+// bar: "baz"
+// },
+// e: 3.6,
+// 1: "numeric value",
+// 2: "anohter numeric value",
+// 2.46: "decimal numeric value",
+// foo: 3.212312310,
+// bar: 0.234242e-10,
+// baz: null,
+// ar: [{}, '1', [], [[[[]]]]],
+// n1: null,
+// n2: undefined,
+// n3: false,
+// n4: "null",
+// n5: "undefined"
+// };
+
+// jtest(obj1);
+
+// var obj2 = {
+// t1: 1232738532270
+// };
+
+// jtest(obj2);
+
+// // a javascript object plus numeric ids
+// var obj3 = {};
+// obj3["foo"] = "bar";
+// obj3[1] = "aaron";
+// obj3[2] = "iba";
+
+// jtest(obj3);
+
+// function jtest(x) {
+// response.write('----------------------------------------------------------------\n\n');
+
+// var str1 = JSON.stringify(x);
+// var str2 = fastJSON.stringify(x);
+
+// var str1_ = JSON.stringify(JSON.parse(str1));
+// var str2_ = fastJSON.stringify(fastJSON.parse(str2));
+
+// response.write([str1,str2].join('\n') + '\n\n');
+// response.write([str1_,str2_].join('\n') + '\n\n');
+// }
+// }
+
+function render_varz() {
+ var varzes = varz.getSnapshot();
+ response.setContentType('text/plain; charset=utf-8');
+ for (var k in varzes) {
+ response.write(k+': '+varzes[k]+'\n');
+ }
+}
+
+function render_extest() {
+ throw new Error("foo");
+}
+
+
+function _diagnosticRecordToHtml(obj) {
+ function valToHtml(o, noborder) {
+ if (typeof (o) != 'object') {
+ return String(o);
+ }
+ var t = TABLE((noborder ? {} : {style: "border-left: 1px solid black; border-top: 1px solid black;"}));
+ if (typeof (o.length) != 'number') {
+ eachProperty(o, function(k, v) {
+ var tr = TR();
+ tr.push(TD({valign: "top", align: "right"}, B(k)));
+ tr.push(TD(valToHtml(v)));
+ t.push(tr);
+ });
+ } else {
+ if (o.length == 0) return "(empty array)";
+ for (var i = 0; i < o.length; ++i) {
+ var tr = TR();
+ tr.push(TD({valign: "top", align: "right"}, B(i)));
+ tr.push(TD(valToHtml(o[i])));
+ t.push(tr);
+ }
+ }
+ return t;
+ }
+ return valToHtml(obj, true);
+}
+
+function render_diagnostics() {
+ var start = Number(request.params.start || 0);
+ var count = Number(request.params.count || 100);
+ var diagnostic_entries = sqlbase.getAllJSON("PAD_DIAGNOSTIC", start, count);
+ var expandArray = request.params.expand || [];
+
+ if (typeof (expandArray) == 'string') expandArray = [expandArray];
+ var expand = {};
+ for (var i = 0; i < expandArray.length; ++i) {
+ expand[expandArray[i]] = true;
+ }
+
+ function makeLink(text, expand, collapse, start0, count0) {
+ start0 = (typeof(start0) == "number" ? start0 : start);
+ count0 = count0 || count;
+ collapse = collapse || [];
+ expand = expand || [];
+
+ var collapseObj = {};
+ for (var i = 0; i < collapse.length; ++i) {
+ collapseObj[collapse[i]] = true;
+ }
+ var expandString =
+ expandArray.concat(expand).filter(function(x) { return ! collapseObj[x] }).map(function(x) { return "expand="+encodeURIComponent(x) }).join("&");
+
+ var url = request.path + "?start="+start0+"&count="+count0+"&"+expandString+(expand.length == 1 ? "#"+md5(expand[0]) : "");
+
+ return A({href: url}, text);
+ }
+
+ var t = TABLE({border: 1, cellpadding: 2, style: "font-family: monospace;"});
+ diagnostic_entries.forEach(function(ent) {
+ var tr = TR()
+ tr.push(TD({valign: "top", align: "right"}, (new Date(Number(ent.id.split("-")[0]))).toString()));
+ tr.push(TD({valign: "top", align: "right"}, ent.id));
+ if (expand[ent.id]) {
+ tr.push(TD(A({name: md5(ent.id)}, makeLink("(collapse)", false, [ent.id])), BR(),
+ _diagnosticRecordToHtml(ent.value)));
+ } else {
+ tr.push(TD(A({name: md5(ent.id)}, makeLink(_diagnosticRecordToHtml({padId: ent.value.padId, disconnectedMessage: ent.value.disconnectedMessage}), [ent.id]))));
+ }
+ t.push(tr);
+ });
+
+ var body = BODY();
+ body.push(P("Showing entries ", start, "-", start+diagnostic_entries.length, ". ",
+ (start > 0 ? makeLink("Show previous "+count+".", [], [], start-count) : ""),
+ (diagnostic_entries.length == count ? makeLink("Show next "+count+".", [], [], start+count) : "")));
+ body.push(t);
+
+ response.write(HTML(body));
+}
+
+//----------------------------------------------------------------
+import("etherpad.billing.billing");
+
+function render_testbillingdirect() {
+ var invoiceId = billing.createInvoice();
+ var ret = billing.directPurchase(invoiceId, 0, 'EEPNET', 500, 'DISCOUNT', {
+ cardType: "Visa",
+ cardNumber: "4501251685453214",
+ cardExpiration: "042019",
+ cardCvv: "123",
+ nameSalutation: "Dr.",
+ nameFirst: "John",
+ nameMiddle: "D",
+ nameLast: "Zamfirescu",
+ nameSuffix: "none",
+ addressStreet: "531 Main St. Apt. 1227",
+ addressStreet2: "",
+ addressCity: "New York",
+ addressState: "NY",
+ addressCountry: "US",
+ addressZip: "10044"
+ }, "https://"+request.host+"/ep/about/testbillingnotify");
+ if (ret.status == 'success') {
+ response.write(P("Success! Invoice id: "+ret.purchaseInfo.invoiceId+" for "+ret.purchaseInfo.cost));
+ } else {
+ response.write(P("Failure: "+ret.toSource()))
+ }
+}
+
+function render_testbillingrecurring() {
+ var invoiceId = billing.createInvoice();
+ var ret = billing.directPurchase(invoiceId, 0, 'EEPNET', 1, 'DISCOUNT', {
+ cardType: "Visa",
+ cardNumber: "4501251685453214",
+ cardExpiration: "042019",
+ cardCvv: "123",
+ nameSalutation: "Dr.",
+ nameFirst: "John",
+ nameMiddle: "D",
+ nameLast: "Zamfirescu",
+ nameSuffix: "none",
+ addressStreet: "531 Main St. Apt. 1227",
+ addressStreet2: "",
+ addressCity: "New York",
+ addressState: "NY",
+ addressCountry: "US",
+ addressZip: "10044"
+ }, "https://"+request.host+"/ep/about/testbillingnotify", true);
+ if (ret.status == 'success') {
+ var transactionId = billing.getTransaction(ret.purchaseInfo.transactionId).txnId;
+ var purchaseId = ret.purchaseInfo.purchaseId;
+ response.write(P("Direct billing successful. PayPal transaction id: ", transactionId));
+
+ invoiceId = billing.createInvoice();
+ ret = billing.asyncRecurringPurchase(
+ invoiceId, purchaseId, transactionId, 500,
+ "https://"+request.host+"/ep/about/testbillingnotify");
+ if (ret.status == 'success') {
+ response.write(P("Woot! Recurrent billing successful! ", ret.purchaseInfo.invoiceId, " for ", ret.purchaseInfo.cost));
+ } else {
+ response.write(P("Failure: "+ret.toSource()));
+ }
+ } else {
+ response.write("Direct billing failure: "+ret.toSource());
+ }
+}
+
+function render_testbillingexpress() {
+ var urlPrefix = "http://"+request.host+request.path;
+ var session = sessions.getSession();
+ var notifyUrl = "http://"+request.host+"/ep/about/testbillingnotify";
+
+ switch (request.params.step) {
+ case '0':
+ response.write(P("You'll be charged $400 for EEPNET. Click the link below to go to paypal."));
+ response.write(A({href: urlPrefix+"?step=1"}, "Link"));
+ break;
+ case '1':
+ var ret = billing.beginExpressPurchase(1, 'EEPNET', 400, 'DISCOUNT', urlPrefix+"?step=2", urlPrefix+"?step=0", notifyUrl);
+ if (ret.status != 'success') {
+ response.write("Error: "+ret.debug.toSource());
+ response.stop();
+ }
+ session.purchaseInfo = ret.purchaseInfo;
+ response.redirect(paypalPurchaseUrl(ret.purchaseInfo.token));
+ break;
+ case '2':
+ var ret = billing.continueExpressPurchase(session.purchaseInfo);
+ if (! ret.status == 'success') {
+ response.write("Error: "+ret.debug.toSource());
+ response.stop();
+ }
+ session.payerInfo = ret.payerInfo;
+
+ response.write(P("You approved the transaction. Click 'confirm' to confirm."));
+ response.write(A({href: urlPrefix+"?step=3"}, "Confirm"));
+ break;
+ case '3':
+ var ret = billing.completeExpressPurchase(session.purchaseInfo, session.payerInfo, notifyUrl);
+ if (ret.status == 'failure') {
+ response.write("Error: "+ret.debug.toSource());
+ response.stop();
+ }
+ if (ret.status == 'pending') {
+ response.write("Your charge is pending. You will be notified by email when your payment clears. Your invoice number is "+session.purchaseInfo.invoiceId);
+ response.stop();
+ }
+
+ response.write(P("Purchase completed: invoice # is "+session.purchaseInfo.invoiceId+" for "+session.purchaseInfo.cost));
+ break;
+ default:
+ response.redirect(request.path+"?step=0");
+ }
+}
+
+//----------------------------------------------------------------
+
+function render_genlicense_get() {
+
+ var t = TABLE({border: 1});
+ function ti(id, label) {
+ t.push(TR(TD({align: "right"}, LABEL({htmlFor: id}, label+":")),
+ TD(INPUT({id: id, name: id, type: 'text', size: 40}))));
+ }
+
+ ti("name", "Name of Licensee");
+ ti("org", "Name of Organization");
+ ti("userQuota", "User Quota");
+
+ t.push(TR(TD({align: "right"}, LABEL("Software Edtition:")),
+ TD( SELECT({name: "edition"},
+ OPTION({value: licensing.getEditionId('PRIVATE_NETWORK_EVALUATION')},
+ "Private Network EVALUATION"),
+ OPTION({value: licensing.getEditionId('PRIVATE_NETWORK')},
+ "Private Network")))));
+
+ ti("expdays", "Number of days until expiration\n(leave blank if never expires)");
+
+ t.push(TR(TD({colspan: 2}, INPUT({type: "submit"}))));
+
+ var f = FORM({action: request.path, method: "post"});
+ f.push(t);
+
+ response.write(HTML(BODY(f)));
+}
+
+function render_genlicense_post() {
+ var name = request.params.name;
+ var org = request.params.org;
+ var editionId = +request.params.edition;
+ var editionName = licensing.getEditionName(editionId);
+ var userQuota = +request.params.userQuota;
+
+ var expiresTime = null;
+ if (request.params.expdays) {
+ expiresTime = +(new Date) + 1000*60*60*24*(+request.params.expdays);
+ }
+
+ var licenseKey = licensing.generateNewKey(
+ name,
+ org,
+ expiresTime,
+ editionId,
+ userQuota
+ );
+
+ // verify
+ if (!licensing.isValidKey(licenseKey)) {
+ throw Error("License key I just created is not valid: "+licenseKey);
+ }
+
+ // TODO: write to database??
+ //
+
+ // display
+ var licenseInfo = licensing.decodeLicenseInfoFromKey(licenseKey);
+ var t = TABLE({border: 1});
+ function line(k, v) {
+ t.push(TR(TD({align: "right"}, k+":"),
+ TD(v)));
+ }
+
+ var key = licenseKey.split(":")[2];
+ if ((key.length % 2) != 0) {
+ key = key + "+";
+ }
+ var keyLine1 = key.substr(0, key.length/2);
+ var keyLine2 = key.substr(key.length/2, key.length);
+
+ line("Name", licenseInfo.personName);
+ line("Organization", licenseInfo.organizationName);
+ line("Key", P(keyLine1, BR(), keyLine2));
+ line("Software Edition", licenseInfo.editionName);
+ line("User Quota", licenseInfo.userQuota);
+ line("Expires", (+licenseInfo.expiresDate > 0) ? licenseInfo.expiresDate.toString() : "(never)");
+
+ response.write(HTML(BODY(t)));
+}
+
+//----------------------------------------------------------------
+
+import("etherpad.metrics.metrics");
+
+function render_flows() {
+ if (request.params.imgId && getSession()[request.params.imgId]) {
+ var arr = getSession()[request.params.imgId];
+ metrics[arr[0]](arr[1], Array.prototype.slice.call(arr[2]));
+ response.stop();
+ }
+
+ function drawHistogram(name, h) {
+ var imgKey = Math.round(Math.random()*1e12);
+ print(IMG({src: request.path+"?imgId="+imgKey}));
+ getSession()[imgKey] = ["respondWithPieChart", name, h];
+ }
+
+ var body = BODY();
+ function print() {
+ for (var i = 0; i < arguments.length; ++i) {
+ body.push(arguments[i]);
+ }
+ }
+
+ var [startDate, endDate] = [7, 1].map(function(a) { return new Date(Date.now() - 86400*1000*a); });
+
+ var allFlows = metrics.getFlows(startDate, endDate);
+
+/*
+ print(P("All flows:"));
+
+ eachProperty(allFlows, function(k, flows) {
+ print(P(k, html(" &raquo; ")));
+ flows.forEach(function(flow) {
+ print(P(flow.toString()));
+ });
+ });
+ response.write(HTML(body));
+ return;
+*/
+
+ print(P("Parsing logs from: "+startDate+" through "+endDate));
+
+ var fs =
+ [metrics.getFunnel(startDate, endDate, ['/ep/about/pricing', '/ep/about/pricing-eepnet', '/ep/store/eepnet-eval-signup'], true),
+ metrics.getFunnel(startDate, endDate, ['/ep/about/pricing', '/ep/about/pricing-free'], true),
+ metrics.getFunnel(startDate, endDate, ['/ep/about/pricing', '/ep/about/pricing-eepod'], true),
+ metrics.getFunnel(startDate, endDate, ['/ep/about/pricing', '/ep/store/eepnet-eval-signup'], true),
+ metrics.getFunnel(startDate, endDate, ['/', '(pad)']),
+ metrics.getFunnel(startDate, endDate, ['/', '/ep/pad/newpad'], true),
+ metrics.getFunnel(startDate, endDate, ['/ep/about/screencast', '(pad)'])];
+
+ function vcnt(i, i2) {
+ return fs[i].visitorCounts[i2];
+ }
+ function pct(f) {
+ return ""+Math.round(f*10000)/100+"%"
+ }
+ function cntAndPct(i, i2) {
+ if (i2 === undefined) { i2 = 1; }
+ return ""+vcnt(i, i2)+" ("+pct(vcnt(i, i2)/vcnt(i, i2-1))+")";
+ }
+ print(P("Of ", vcnt(0, 0), " visitors to the pricing page, ",
+ cntAndPct(0), " of them viewed eepnet, (", cntAndPct(0, 2), " of those downloaded), ",
+ cntAndPct(1), " of them viewed free, and ",
+ cntAndPct(2), " of them viewed eepod. ",
+ cntAndPct(3), " of them clicked on the eval signup link straight up."
+ ),
+ P("Of ", vcnt(4, 0), " visitors to the home page, ",
+ cntAndPct(4), " of them went to a pad page in the same flow; ",
+ cntAndPct(5), " of them clicked the new pad button immediately."),
+ P("Of ", vcnt(6, 0), " vistitors to the screencast page, ",
+ cntAndPct(6), " of them visisted a pad page in the same flow."));
+
+ var origins = metrics.getOrigins(startDate, endDate, true);
+ print(P("Flow first origins: "));
+ drawHistogram("first origins", origins.flowFirsts);
+
+ var firstHits = metrics.getOrigins(startDate, endDate, false, true);
+ var padFirstHits = 0;
+ var nonPadFirstHits = 0;
+ print(P("First paths hit: "));
+ drawHistogram("first paths", firstHits.flowFirsts);
+ firstHits.flowFirsts.filter(function(x) {
+ if (x.value != '/' && ! startsWith(x.value, "/ep/")) {
+ padFirstHits += x.count;
+ return false;
+ }
+ nonPadFirstHits += x.count;
+ return true;
+ });
+ print(P("Some pad page: "+padFirstHits),
+ P("Non-pad page: "+nonPadFirstHits));
+
+ var exitsFromHomepage = metrics.getExits(startDate, endDate, '/', true);
+ print(P("Exits from homepage: "));
+ drawHistogram("exits", exitsFromHomepage.histogram)
+
+ response.write(HTML(body));
+}
+
+//----------------------------------------------------------------
+
+import("etherpad.pad.pad_migrations");
+
+function render_padmigrations() {
+ var residue = (request.params.r || 0);
+ var modulus = (request.params.m || 1);
+ var name = (request.params.n || (residue+"%"+modulus));
+ pad_migrations.runBackgroundMigration(residue, modulus, name);
+ response.write("done");
+ return true;
+}
+
+// TODO: add ability to delete entries?
+// TODO: show sizes?
+function render_cachebrowser() {
+ var path = request.params.path;
+ if (path && path.charAt(0) == ',') {
+ path = path.substr(1);
+ }
+ var pathArg = (path || "");
+ var c = appjet.cache;
+ if (path) {
+ path.split(",").forEach(function(part) {
+ c = c[part];
+ });
+ }
+
+ var d = DIV({style: 'font-family: monospace; text-decoration: none;'});
+
+ d.push(H3("appjet.cache --> "+pathArg.split(",").join(" --> ")));
+
+ var t = TABLE({border: 1});
+ keys(c).sort().forEach(function(k) {
+ var v = c[k];
+ if (v && (typeof(v) == 'object') && (!v.getDate)) {
+ t.push(TR(TD(A({style: 'text-decoration: none;',
+ href: request.path+"?path="+pathArg+","+k}, k))));
+ } else {
+ t.push(TR(TD(k), TD(v)));
+ }
+ });
+
+ d.push(t);
+ response.write(d);
+}
+
+function render_pne_tracker_get() {
+ var data = sqlobj.selectMulti('pne_tracking_data', {}, {});
+ data.sort(function(x, y) { return cmp(y.date, x.date); });
+
+ var t = TABLE();
+
+ var headrow = TR();
+ ['date', 'remote host', 'keyHash', 'name', 'value'].forEach(function(x) {
+ headrow.push(TH({align: "left", style: "padding: 0 6px;"}, x));
+ });
+ t.push(headrow);
+
+ data.forEach(function(d) {
+ var tr = TR();
+
+ tr.push(TD(d.date.toString().split(' ').slice(0,5).join('-')));
+
+ if (d.remoteIp) {
+ tr.push(TD(netutils.getHostnameFromIp(d.remoteIp) || d.remoteIp));
+ } else {
+ tr.push(TD("-"));
+ }
+
+ if (d.keyHash) {
+ tr.push(TD(A({href: '/ep/admin/pne-tracker-lookup-keyhash?hash='+d.keyHash}, d.keyHash)));
+ } else {
+ tr.push(TD("-"));
+ }
+
+ tr.push(TD(d.name));
+ tr.push(TD(d.value));
+
+ t.push(tr);
+ });
+
+ response.write(HTML(HEAD(html("<style>td { border-bottom: 1px solid #ddd; border-right: 1px solid #ddd; padding: 0 6px; } \n tr:hover { background: #ffc; }</style>"),
+ BODY({style: "font-family: monospace; font-size: 12px;"}, t))));
+}
+
+function render_pne_tracker_lookup_keyhash_get() {
+ var hash = request.params.hash;
+ // brute force it
+ var allLicenses = sqlobj.selectMulti('eepnet_signups', {}, {});
+ var record = null;
+ var i = 0;
+ while (i < allLicenses.length && record == null) {
+ var d = allLicenses[i];
+ if (md5(d.licenseKey).substr(0, 16) == hash) {
+ record = d;
+ }
+ i++;
+ }
+ if (!record) {
+ response.write("Not found. Perhaps this was a test download from local development, or a paid customer whose licenses we don't currently look through on this page.");
+ } else {
+ var kl = keys(record).sort();
+ var t = TABLE();
+ kl.forEach(function(k) {
+ t.push(TR(TH({align: "right"}, k+":"),
+ TD({style: "padding-left: 1em;"}, record[k])));
+ });
+ response.write(HTML(BODY(DIV({style: "font-family: monospace;"},
+ DIV(H1("Trial Signup Record:")), t))));
+ }
+}
+
+function render_reload_blog_db_get() {
+ var d = DIV();
+ if (request.params.ok) {
+ d.push(DIV(P("OK")));
+ }
+ d.push(FORM({method: "post", action: request.path},
+ INPUT({type: "submit", value: "Reload Blog DB Now"})));
+ response.write(HTML(BODY(d)));
+}
+
+function render_reload_blog_db_post() {
+ blogcontrol.reloadBlogDb();
+ response.redirect(request.path+"?ok=1");
+}
+
+function render_pro_domain_accounts() {
+ var accounts = sqlobj.selectMulti('pro_accounts', {}, {});
+ var domains = sqlobj.selectMulti('pro_domains', {}, {});
+
+ // build domain map
+ var domainMap = {};
+ domains.forEach(function(d) { domainMap[d.id] = d; });
+ accounts.sort(function(a,b) { return cmp(b.lastLoginDate, a.lastLoginDate); });
+
+ var b = BODY({style: "font-family: monospace;"});
+ b.push(accounts.length + " pro accounts.");
+ var t = TABLE({border: 1});
+ t.push(TR(TH("email"),
+ TH("domain"),
+ TH("lastLogin")));
+ accounts.forEach(function(u) {
+ t.push(TR(TD(u.email),
+ TD(domainMap[u.domainId].subDomain+"."+request.domain),
+ TD(u.lastLoginDate)));
+ });
+
+ b.push(t);
+
+ response.write(HTML(b));
+}
+
+
+function render_beta_valve_get() {
+ var d = DIV(
+ P("Beta Valve Status: ",
+ (pro_beta_control.isValveOpen() ?
+ SPAN({style: "color: green;"}, B("OPEN")) :
+ SPAN({style: "color: red;"}, B("CLOSED")))),
+ P(FORM({action: '/ep/admin/beta-valve-toggle', method: "post"},
+ BUTTON({type: "submit"}, "Toggle"))));
+
+ var t = TABLE({border: 1, cellspacing: 0, cellpadding: 4, style: "font-family: monospace;"});
+ var signupList = sqlobj.selectMulti('pro_beta_signups', {}, {});
+ signupList.sort(function(a, b) {
+ return cmp(b.signupDate, a.signupDate);
+ });
+
+ d.push(HR());
+
+ if (getSession().betaAdminMessage) {
+ d.push(DIV({style: "border: 1px solid #ccc; padding: 1em; background: #eee;"},
+ getSession().betaAdminMessage));
+ delete getSession().betaAdminMessage;
+ }
+
+ d.push(P(signupList.length + " beta signups"));
+
+ d.push(FORM({action: '/ep/admin/beta-invite-multisend', method: 'post'},
+ P("Send ", INPUT({type: 'text', name: 'count', size: 3}), " invites."),
+ INPUT({type: "submit"})));
+
+ t.push(TR(TH("id"), TH("email"), TH("signupDate"),
+ TH("activationDate"), TH("activationCode"), TH(' ')));
+
+ signupList.forEach(function(s) {
+ var tr = TR();
+ tr.push(TD(s.id),
+ TD(s.email),
+ TD(s.signupDate),
+ TD(s.isActivated ? s.activationDate : "-"),
+ TD(s.activationCode));
+ if (!s.activationCode) {
+ tr.push(TD(FORM({action: '/ep/admin/beta-invite-send', method: 'post'},
+ INPUT({type: 'hidden', name: 'id', value: s.id}),
+ INPUT({type: 'submit', value: "Send Invite"}))));
+ } else {
+ tr.push(TD(' '));
+ }
+ t.push(tr);
+ });
+ d.push(t);
+ response.write(d);
+}
+
+function render_beta_valve_toggle_post() {
+ pro_beta_control.toggleValve();
+ response.redirect('/ep/admin/beta-valve');
+}
+
+function render_beta_invite_send_post() {
+ var id = request.params.id;
+ pro_beta_control.sendInvite(id);
+ response.redirect('/ep/admin/beta-valve');
+}
+
+function render_beta_invite_multisend_post() {
+ var count = request.params.count;
+ var signupList = sqlobj.selectMulti('pro_beta_signups', {}, {});
+ signupList.sort(function(a, b) {
+ return cmp(a.signupDate, b.signupDate);
+ });
+ var sent = 0;
+ for (var i = 0; ((i < signupList.length) && (sent < count)); i++) {
+ var record = signupList[i];
+ if (!record.activationCode) {
+ pro_beta_control.sendInvite(record.id);
+ sent++;
+ }
+ }
+ getSession().betaAdminMessage = (sent+" invites sent.");
+ response.redirect('/ep/admin/beta-valve');
+}
+
+function render_usagestats() {
+ response.redirect("/ep/admin/usagestats/");
+}
+
+function render_exceptions() {
+ exceptions.render();
+}
+
+function render_setadminmode() {
+ sessions.setIsAnEtherpadAdmin(
+ String(request.params.v).toLowerCase() == "true");
+ response.redirect("/ep/admin/");
+}
+
+// --------------------------------------------------------------
+// billing-related
+// --------------------------------------------------------------
+
+// some of these functions are only used from selenium tests, and so have no UI.
+
+function render_setdomainpaidthrough() {
+ var domainName = request.params.domain;
+ var when = new Date(Number(request.params.paidthrough));
+ if (! domainName || ! when) {
+ response.write("fail");
+ response.stop();
+ }
+ var domain = domains.getDomainRecordFromSubdomain(domainName);
+ var domainId = domain.id;
+
+ var subscription = team_billing.getSubscriptionForCustomer(domainId);
+ if (subscription) {
+ billing.updatePurchase(subscription.id, {paidThrough: when});
+ team_billing.domainCacheClear(domainId);
+ response.write("OK");
+ } else {
+ response.write("fail");
+ }
+}
+
+function render_runsubscriptions() {
+ team_billing.processAllSubscriptions();
+ response.write("OK");
+}
+
+function render_reset_subscription() {
+ var body = BODY();
+ if (request.isGet) {
+ body.push(FORM({method: "POST"},
+ "Subdomain: ", INPUT({type: "text", name: "subdomain"}), BUTTON({name: "clear"}, "Go")));
+ } else if (request.isPost) {
+ if (! request.params.confirm) {
+ var domain = domains.getDomainRecordFromSubdomain(request.params.subdomain);
+ var admins = pro_accounts.listAllDomainAdmins(domain.id);
+ body.push(P("Domain ", domain.subDomain, ".", request.domain, "; admins:"));
+ var p = UL();
+ admins.forEach(function(admin) {
+ p.push(LI(admin.fullName, " <", admin.email, ">"));
+ });
+ body.push(p);
+ var subscription = team_billing.getSubscriptionForCustomer(domain.id);
+ if (subscription) {
+ body.push(P("Subscription is currently ", subscription.status, ", and paid through: ", checkout.formatDate(subscription.paidThrough), "."))
+ body.push(FORM({method: "POST"},
+ INPUT({type: "hidden", name: "subdomain", value: request.params.subdomain}),
+ "Are you sure? ", BUTTON({name: "confirm", value: "yes"}, "YES")));
+ } else {
+ body.push(P("No current subscription"));
+ }
+ } else {
+ var domain = domains.getDomainRecordFromSubdomain(request.params.subdomain);
+ sqlcommon.inTransaction(function() {
+ team_billing.resetMaxUsers(domain.id);
+ sqlobj.deleteRows('billing_purchase', {customer: domain.id, type: 'subscription'});
+ team_billing.domainCacheClear(domain.id);
+ team_billing.clearRecurringBillingInfo(domain.id);
+ });
+ body.push("Done!")
+ }
+ }
+ body.push(A({href: request.path}, html("&laquo; back")));
+ response.write(HTML(body));
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/control/blogcontrol.js b/etherpad/src/etherpad/control/blogcontrol.js
new file mode 100644
index 0000000..9ec485d
--- /dev/null
+++ b/etherpad/src/etherpad/control/blogcontrol.js
@@ -0,0 +1,199 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//blogcontrol
+
+import("jsutils.*");
+import("atomfeed");
+import("funhtml.*");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.quotas");
+
+//----------------------------------------------------------------
+// bloghelpers
+//----------------------------------------------------------------
+bloghelpers = {};
+bloghelpers.disqusDeveloper = function() {
+ if (isProduction()) {
+ return '';
+ }
+ return [
+ '<script type="text/javascript">',
+ ' var disqus_developer = 1;',
+ '</script>'
+ ].join('\n');
+};
+
+bloghelpers.feedburnerUrl = function() {
+ var name = isProduction() ? "TheEtherPadBlog" : "TheEtherPadBlogDev";
+ return "http://feeds.feedburner.com/"+name;
+};
+
+bloghelpers.feedLink = function() {
+ return [
+ '<link rel="alternate"',
+ ' title="EtherPad Blog Feed"',
+ ' href="', bloghelpers.feedburnerUrl(), '"',
+ ' type="application/rss+xml" />'
+ ].join('');
+};
+
+bloghelpers.dfmt = function(d) {
+ return d.toString().split(' ').slice(0,3).join(' ');
+};
+
+bloghelpers.feedbuttonHtml = function() {
+ var aProps = {
+ href: bloghelpers.feedburnerUrl(),
+ rel: "alternate",
+ type: "application/rss+xml"
+ };
+
+ return SPAN(A(aProps,
+ IMG({src: "http://www.feedburner.com/fb/images/pub/feed-icon32x32.png",
+ alt: "EtherPad Blog Feed",
+ style: "vertical-align:middle; border:0;"}))).toHTML();
+};
+
+bloghelpers.getMaxUsersPerPad = function() {
+ return quotas.getMaxSimultaneousPadEditors()
+};
+
+//----------------------------------------------------------------
+// posts "database"
+//----------------------------------------------------------------
+
+function _wrapPost(p) {
+ var wp = {};
+ keys(p).forEach(function(k) { wp[k] = p[k]; });
+ wp.url = function() {
+ return "http://"+request.host+"/ep/blog/posts/"+p.id;
+ };
+ wp.renderContent = function() {
+ return renderTemplateAsString("blog/posts/"+p.id+".ejs",
+ {post: wp, bloghelpers: bloghelpers});
+ };
+ return wp;
+}
+
+function _addPost(id, title, author, published, updated) {
+ if (!appjet.cache.blogDB) {
+ appjet.cache.blogDB = {
+ posts: [],
+ postMap: {}
+ };
+ }
+ var p = {id: id, title: title, author: author, published: published, updated: updated};
+ appjet.cache.blogDB.posts.push(p);
+ appjet.cache.blogDB.postMap[p.id] = p;
+}
+
+function _getPostById(id) {
+ var p = appjet.cache.blogDB.postMap[id];
+ if (!p) { return null; }
+ return _wrapPost(p);
+}
+
+function _getAllPosts() {
+ return [];
+}
+
+function _sortBlogDB() {
+ appjet.cache.blogDB.posts.sort(function(a,b) { return cmp(b.published, a.published); });
+}
+
+//----------------------------------------------------------------
+// Posts
+//----------------------------------------------------------------
+
+function _initBlogDB() {
+ return;
+}
+
+function reloadBlogDb() {
+ delete appjet.cache.blogDB;
+ _initBlogDB();
+}
+
+function onStartup() {
+ reloadBlogDb();
+}
+
+//----------------------------------------------------------------
+// onRequest
+//----------------------------------------------------------------
+function onRequest(name) {
+ // nothing yet.
+}
+
+//----------------------------------------------------------------
+// main
+//----------------------------------------------------------------
+function render_main() {
+ renderFramed('blog/blog_main_body.ejs',
+ {posts: _getAllPosts(), bloghelpers: bloghelpers});
+}
+
+//----------------------------------------------------------------
+// render_feed
+//----------------------------------------------------------------
+function render_feed() {
+ var lastModified = new Date(); // TODO: most recent of all entries modified
+
+ var entries = [];
+ _getAllPosts().forEach(function(post) {
+ entries.push({
+ title: post.title,
+ author: post.author,
+ published: post.published,
+ updated: post.updated,
+ href: post.url(),
+ content: post.renderContent()
+ });
+ });
+
+ response.setContentType("application/atom+xml; charset=utf-8");
+
+ response.write(atomfeed.renderFeed(
+ "The EtherPad Blog", new Date(), entries,
+ "http://"+request.host+"/ep/blog/"));
+}
+
+//----------------------------------------------------------------
+// render_post
+//----------------------------------------------------------------
+function render_post(name) {
+ var p = _getPostById(name);
+ if (!p) {
+ return false;
+ }
+ renderFramed('blog/blog_post_body.ejs', {
+ post: p, bloghelpers: bloghelpers,
+ posts: _getAllPosts()
+ });
+ return true;
+}
+
+//----------------------------------------------------------------
+// render_new_from_etherpad()
+//----------------------------------------------------------------
+
+function render_new_from_etherpad() {
+ return "";
+}
+
diff --git a/etherpad/src/etherpad/control/connection_diagnostics_control.js b/etherpad/src/etherpad/control/connection_diagnostics_control.js
new file mode 100644
index 0000000..aaa1bb3
--- /dev/null
+++ b/etherpad/src/etherpad/control/connection_diagnostics_control.js
@@ -0,0 +1,87 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.*");
+import("etherpad.helpers.*");
+
+//----------------------------------------------------------------
+// Connection diagnostics
+//----------------------------------------------------------------
+
+/*
+function _getDiagnosticsCollection() {
+ var db = storage.getRoot("connection_diagnostics");
+ if (!db.diagnostics) {
+ db.diagnostics = new StorableCollection();
+ }
+ return db.diagnostics;
+}
+*/
+
+function render_main_get() {
+ /*
+ var diagnostics = _getDiagnosticsCollection();
+
+ var data = new StorableObject({
+ ip: request.clientAddr,
+ userAgent: request.headers['User-Agent']
+ });
+
+ diagnostics.add(data);
+
+ helpers.addClientVars({
+ diagnosticStorableId: data.id
+ });
+*/
+ renderFramed("main/connection_diagnostics_body.ejs");
+}
+
+function render_submitdata_post() {
+ response.setContentType('text/plain; charset=utf-8');
+ /*
+ var id = request.params.diagnosticStorableId;
+ var storedData = storage.getStorable(id);
+ if (!storedData) {
+ response.write("Error retreiving diagnostics record.");
+ response.stop();
+ }
+ var diagnosticData = JSON.parse(request.params.dataJson);
+ eachProperty(diagnosticData, function(k,v) {
+ storedData[k] = v;
+ });
+*/
+ response.write("OK");
+}
+
+function render_submitemail_post() {
+ response.setContentType('text/plain; charset=utf-8');
+ /*
+ var id = request.params.diagnosticStorableId;
+ var data = storage.getStorable(id);
+ if (!data) {
+ response.write("Error retreiving diagnostics record.");
+ response.stop();
+ }
+ var email = request.params.email;
+ if (!isValidEmail(email)) {
+ response.write("Invalid email address.");
+ response.stop();
+ }
+ data.email = email;
+*/
+ response.write("OK");
+}
+
diff --git a/etherpad/src/etherpad/control/global_pro_account_control.js b/etherpad/src/etherpad/control/global_pro_account_control.js
new file mode 100644
index 0000000..65d2124
--- /dev/null
+++ b/etherpad/src/etherpad/control/global_pro_account_control.js
@@ -0,0 +1,143 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("stringutils");
+import("stringutils.*");
+import("email.sendEmail");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.utils.*");
+import("etherpad.sessions.getSession");
+
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_utils");
+
+jimport("java.lang.System.out.println");
+
+function onRequest() {
+ if (!getSession().oldFormData) {
+ getSession().oldFormData = {};
+ }
+ return false; // not handled yet.
+}
+
+function _errorDiv() {
+ var m = getSession().proAccountControlError;
+ delete getSession().proAccountControlError;
+ if (m) {
+ return DIV({className: "error"}, m);
+ }
+ return "";
+}
+
+function _redirectError(m) {
+ getSession().proAccountControlError = m;
+ response.redirect(request.path);
+}
+
+
+function render_main_get() {
+ response.redirect('/ep/pro-account/sign-in');
+}
+
+function render_sign_in_get() {
+ renderFramed('pro-account/sign-in.ejs', {
+ oldData: getSession().oldFormData,
+ errorDiv: _errorDiv
+ });
+}
+
+
+function render_sign_in_post() {
+ var email = trim(request.params.email);
+ var password = request.params.password;
+ var subDomain = request.params.subDomain;
+
+ subDomain = subDomain.toLowerCase();
+
+ getSession().oldFormData.email = email;
+ getSession().oldFormData.subDomain = subDomain;
+
+ var domainRecord = domains.getDomainRecordFromSubdomain(subDomain);
+ if (!domainRecord) {
+ _redirectError("Site address not found: "+subDomain+"."+request.host);
+ }
+
+ var instantSigninKey = stringutils.randomString(20);
+ syncedWithCache('global_signin_passwords', function(c) {
+ c[instantSigninKey] = {
+ email: email,
+ password: password
+ };
+ });
+
+ response.redirect(
+ "https://"+subDomain+"."+httpsHost(request.host)+
+ "/ep/account/sign-in?instantSigninKey="+instantSigninKey);
+}
+
+function render_recover_get() {
+ renderFramed('pro-account/recover.ejs', {
+ oldData: getSession().oldFormData,
+ errorDiv: _errorDiv
+ });
+}
+
+function render_recover_post() {
+
+ function _recoverLink(accountRecord, domainRecord) {
+ var host = (domainRecord.subDomain + "." + httpsHost(request.host));
+ return (
+ "https://"+host+"/ep/account/forgot-password?instantSubmit=1&email="+
+ encodeURIComponent(accountRecord.email));
+ }
+
+ var email = trim(request.params.email);
+
+ // lookup all domains associated with this email
+ var accountList = pro_accounts.getAllAccountsWithEmail(email);
+ println("account records matching ["+email+"]: "+accountList.length);
+
+ var domainList = [];
+ for (var i = 0; i < accountList.length; i++) {
+ domainList[i] = domains.getDomainRecord(accountList[i].domainId);
+ }
+
+ if (accountList.length == 0) {
+ _redirectError("No accounts were found associated with the email address \""+email+"\".");
+ }
+ if (accountList.length == 1) {
+ response.redirect(_recoverLink(accountList[0], domainList[0]));
+ }
+ if (accountList.length > 1) {
+ var fromAddr = '"EtherPad" <noreply@pad.spline.inf.fu-berlin.de>';
+ var subj = "EtherPad: account information";
+ var body = renderTemplateAsString(
+ 'pro/account/global-multi-domain-recover-email.ejs', {
+ accountList: accountList,
+ domainList: domainList,
+ recoverLink: _recoverLink,
+ email: email
+ }
+ );
+ sendEmail(email, fromAddr, subj, {}, body);
+ pro_utils.renderFramedMessage("Instructions have been sent to "+email+".");
+ }
+}
+
+
diff --git a/etherpad/src/etherpad/control/historycontrol.js b/etherpad/src/etherpad/control/historycontrol.js
new file mode 100644
index 0000000..a78cfad
--- /dev/null
+++ b/etherpad/src/etherpad/control/historycontrol.js
@@ -0,0 +1,226 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("etherpad.utils.render404");
+import("etherpad.pad.model");
+import("etherpad.collab.collab_server");
+import("etherpad.collab.ace.easysync2.*");
+import("jsutils.eachProperty");
+
+function _urlCache() {
+ if (!appjet.cache.historyUrlCache) {
+ appjet.cache.historyUrlCache = {};
+ }
+ return appjet.cache.historyUrlCache;
+}
+
+function _replyWithJSONAndCache(obj) {
+ obj.apiversion = _VERSION;
+ var output = fastJSON.stringify(obj);
+ _urlCache()[request.path] = output;
+ response.write(output);
+ response.stop();
+}
+
+function _replyWithJSON(obj) {
+ obj.apiversion = _VERSION;
+ response.write(fastJSON.stringify(obj));
+ response.stop();
+}
+
+function _error(msg, num) {
+ _replyWithJSON({error: String(msg), errornum: num});
+}
+
+var _VERSION = 1;
+
+var _ERROR_REVISION_NUMBER_TOO_LARGE = 14;
+
+function _do_text(padId, r) {
+ if (! padId) render404();
+ model.accessPadGlobal(padId, function(pad) {
+ if (! pad.exists()) {
+ render404();
+ }
+ if (r > pad.getHeadRevisionNumber()) {
+ _error("Revision number too large", _ERROR_REVISION_NUMBER_TOO_LARGE);
+ }
+ var text = pad.getInternalRevisionText(r);
+ text = _censorText(text);
+ _replyWithJSONAndCache({ text: text });
+ });
+}
+
+function _do_stat(padId) {
+ var obj = {};
+ if (! padId) {
+ obj.exists = false;
+ }
+ else {
+ model.accessPadGlobal(padId, function(pad) {
+ if (! pad.exists()) {
+ obj.exists = false;
+ }
+ else {
+ obj.exists = true;
+ obj.latestRev = pad.getHeadRevisionNumber();
+ }
+ });
+ }
+ _replyWithJSON(obj);
+}
+
+function _censorText(text) {
+ // may not change length of text
+ return text.replace(/(http:\/\/pad.spline.inf.fu-berlin.de\/)(\w+)/g, function(url, u1, u2) {
+ return u1 + u2.replace(/\w/g, '-');
+ });
+}
+
+function _do_changes(padId, first, last) {
+ if (! padId) render404();
+
+ var charPool = [];
+ var changeList = [];
+
+ function charPoolText(txt) {
+ charPool.push(txt);
+ return _encodeVarInt(txt.length);
+ }
+
+ model.accessPadGlobal(padId, function(pad) {
+
+ if (first > pad.getHeadRevisionNumber() || last > pad.getHeadRevisionNumber()) {
+ _error("Revision number too large", _ERROR_REVISION_NUMBER_TOO_LARGE);
+ }
+
+ var curAText = Changeset.makeAText("\n");
+ if (first > 0) {
+ curAText = pad.getInternalRevisionAText(first - 1);
+ }
+ curAText.text = _censorText(curAText.text);
+ var lastTimestamp = null;
+ for(var r=first;r<=last;r++) {
+ var binRev = [];
+ var timestamp = +pad.getRevisionDate(r);
+ binRev.push(_encodeTimeStamp(timestamp, lastTimestamp));
+ lastTimestamp = timestamp;
+ binRev.push(_encodeVarInt(1)); // fake author
+
+ var c = pad.getRevisionChangeset(r);
+ var splices = Changeset.toSplices(c);
+ splices.forEach(function (splice) {
+ var startChar = splice[0];
+ var endChar = splice[1];
+ var newText = splice[2];
+ oldText = curAText.text.substring(startChar, endChar);
+
+ if (oldText.length == 0) {
+ binRev.push('+');
+ binRev.push(_encodeVarInt(startChar));
+ binRev.push(charPoolText(newText));
+ }
+ else if (newText.length == 0) {
+ binRev.push('-');
+ binRev.push(_encodeVarInt(startChar));
+ binRev.push(charPoolText(oldText));
+ }
+ else {
+ binRev.push('*');
+ binRev.push(_encodeVarInt(startChar));
+ binRev.push(charPoolText(oldText));
+ binRev.push(charPoolText(newText));
+ }
+ });
+ changeList.push(binRev.join(''));
+
+ curAText = Changeset.applyToAText(c, curAText, pad.pool());
+ }
+
+ _replyWithJSONAndCache({charPool: charPool.join(''), changes: changeList.join(',')});
+
+ });
+}
+
+function render_history(padOpaqueRef, rest) {
+ if (_urlCache()[request.path]) {
+ response.write(_urlCache()[request.path]);
+ response.stop();
+ return true;
+ }
+ var padId;
+ if (padOpaqueRef == "CSi1xgbFXl" || padOpaqueRef == "13sentences") {
+ // made-up, hard-coded opaque ref, should be a table for these
+ padId = "jbg5HwzUX8";
+ }
+ else if (padOpaqueRef == "dO1j7Zf34z" || padOpaqueRef == "foundervisa") {
+ // made-up, hard-coded opaque ref, should be a table for these
+ padId = "3hS7kQyDXG";
+ }
+ else {
+ padId = null;
+ }
+ var regexResult;
+ if ((regexResult = /^stat$/.exec(rest))) {
+ _do_stat(padId);
+ }
+ else if ((regexResult = /^text\/(\d+)$/.exec(rest))) {
+ var r = Number(regexResult[1]);
+ _do_text(padId, r);
+ }
+ else if ((regexResult = /^changes\/(\d+)-(\d+)$/.exec(rest))) {
+ _do_changes(padId, Number(regexResult[1]), Number(regexResult[2]));
+ }
+ else {
+ return false;
+ }
+}
+
+function _encodeVarInt(num) {
+ var n = +num;
+ if (isNaN(n)) {
+ throw new Error("Can't encode non-number "+num);
+ }
+ var chars = [];
+ var done = false;
+ while (! done) {
+ if (n < 32) done = true;
+ var nd = (n % 32);
+ if (chars.length > 0) {
+ // non-first, will become non-last digit
+ nd = (nd | 32);
+ }
+ chars.push(_BASE64_DIGITS[nd]);
+ n = Math.floor(n / 32)
+ }
+ return chars.reverse().join('');
+}
+var _BASE64_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._";
+
+function _encodeTimeStamp(tMillis, baseMillis) {
+ var t = Math.floor(tMillis/1000);
+ var base = Math.floor(baseMillis/1000);
+ var absolute = ["+", t];
+ var resultPair = absolute;
+ if (((typeof base) == "number") && base <= t) {
+ var relative = ["", t - base];
+ if (relative[1] < absolute[1]) {
+ resultPair = relative;
+ }
+ }
+ return resultPair[0] + _encodeVarInt(resultPair[1]);
+}
diff --git a/etherpad/src/etherpad/control/loadtestcontrol.js b/etherpad/src/etherpad/control/loadtestcontrol.js
new file mode 100644
index 0000000..2a4e3f7
--- /dev/null
+++ b/etherpad/src/etherpad/control/loadtestcontrol.js
@@ -0,0 +1,93 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.dbwriter");
+import("etherpad.pad.activepads");
+import("etherpad.control.pad.pad_control");
+import("etherpad.collab.collab_server");
+
+// NOTE: we need to talk before enabling this again, for potential security vulnerabilities.
+var LOADTEST_ENABLED = false;
+
+function onRequest() {
+ if (!LOADTEST_ENABLED) {
+ response.forbid();
+ }
+}
+
+function render_createpad() {
+ var padId = request.params.padId;
+
+ padutils.accessPadLocal(padId, function(pad) {
+ if (! pad.exists()) {
+ pad.create(pad_control.getDefaultPadText());
+ }
+ });
+
+ activepads.touch(padId);
+ response.write("OK");
+}
+
+function render_readpad() {
+ var padId = request.params.padId;
+
+ padutils.accessPadLocal(padId, function(pad) {
+ /* nothing */
+ });
+
+ activepads.touch(padId);
+ response.write("OK");
+}
+
+function render_appendtopad() {
+ var padId = request.params.padId;
+ var text = request.params.text;
+
+ padutils.accessPadLocal(padId, function(pad) {
+ collab_server.appendPadText(pad, text);
+ });
+
+ activepads.touch(padId);
+ response.write("OK");
+}
+
+function render_flushpad() {
+ var padId = request.params.padId;
+
+ padutils.accessPadLocal(padId, function(pad) {
+ dbwriter.writePadNow(pad, true);
+ });
+
+ activepads.touch(padId);
+ response.write("OK");
+}
+
+function render_setpadtext() {
+ var padId = request.params.padId;
+ var text = request.params.text;
+
+ padutils.accessPadLocal(padId, function(pad) {
+ collab_server.setPadText(pad, text);
+ });
+
+ activepads.touch(padId);
+ response.write("OK");
+}
+
+
+
diff --git a/etherpad/src/etherpad/control/maincontrol.js b/etherpad/src/etherpad/control/maincontrol.js
new file mode 100644
index 0000000..261ddaf
--- /dev/null
+++ b/etherpad/src/etherpad/control/maincontrol.js
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("funhtml.*");
+import("stringutils.toHTML");
+
+import("etherpad.globals.*");
+import("etherpad.helpers.*");
+import("etherpad.licensing");
+import("etherpad.log");
+import("etherpad.utils.*");
+
+import("etherpad.control.blogcontrol");
+
+import("etherpad.pad.model");
+import("etherpad.collab.collab_server");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+
+function render_main() {
+ if (request.path == '/ep/') {
+ response.redirect('/');
+ }
+ renderFramed('main/home.ejs', {
+ newFromEtherpad: blogcontrol.render_new_from_etherpad()
+ });
+ return true;
+}
+
+function render_support() {
+ renderFramed("main/support_body.ejs");
+}
+
+function render_changelog_get() {
+ renderFramed("main/changelog.ejs");
+}
+
+
diff --git a/etherpad/src/etherpad/control/pad/pad_changeset_control.js b/etherpad/src/etherpad/control/pad/pad_changeset_control.js
new file mode 100644
index 0000000..5af7ed0
--- /dev/null
+++ b/etherpad/src/etherpad/control/pad/pad_changeset_control.js
@@ -0,0 +1,280 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.helpers");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.utils.*");
+import("fastJSON");
+import("etherpad.collab.server_utils.*");
+import("etherpad.collab.ace.easysync2.{AttribPool,Changeset}");
+import("cache_utils.syncedWithCache");
+import("etherpad.log");
+jimport("net.appjet.common.util.LimitedSizeMapping");
+
+import("stringutils");
+import("stringutils.sprintf");
+
+var _JSON_CACHE_SIZE = 10000;
+
+// to clear: appjet.cache.pad_changeset_control.jsoncache.map.clear()
+function _getJSONCache() {
+ return syncedWithCache('pad_changeset_control.jsoncache', function(cache) {
+ if (! cache.map) {
+ cache.map = new LimitedSizeMapping(_JSON_CACHE_SIZE);
+ }
+ return cache.map;
+ });
+}
+
+var _profiler = {
+ t: 0,
+ laps: [],
+ active: false,
+ start: function() {
+ _profiler.t = +new Date;
+ _profiler.laps = [];
+ //_profiler.active = true;
+ },
+ lap: function(name) {
+ if (! _profiler.active) return;
+ var t2 = +new Date;
+ _profiler.laps.push([name, t2 - _profiler.t]);
+ },
+ dump: function(info) {
+ if (! _profiler.active) return;
+ function padright(s, len) {
+ s = String(s);
+ return s + new Array(Math.max(0,len-s.length+1)).join(' ');
+ }
+ var str = padright(info,20)+": ";
+ _profiler.laps.forEach(function(e) {
+ str += padright(e.join(':'), 8);
+ });
+ java.lang.System.out.println(str);
+ },
+ stop: function() {
+ _profiler.active = false;
+ }
+};
+
+function onRequest() {
+ _profiler.start();
+
+ var parts = request.path.split('/');
+ // TODO(kroo): create a mapping between padId and read-only id
+ var urlId = parts[4];
+ var padId = parseUrlId(urlId).localPadId;
+ // var revisionId = parts[5];
+
+ padutils.accessPadLocal(padId, function(pad) {
+ if (! pad.exists() && pad.getSupportsTimeSlider()) {
+ response.forbid();
+ }
+ }, 'r');
+
+ // use the query string to specify start and end revision numbers
+ var startRev = parseInt(request.params["s"]);
+ var endRev = startRev + 100 * parseInt(request.params["g"]);
+ var granularity = parseInt(request.params["g"]);
+
+ _profiler.lap('A');
+ var changesetsJson =
+ getCacheableChangesetInfoJSON(padId, startRev, endRev, granularity);
+ _profiler.lap('X');
+
+ //TODO(kroo): set content-type to javascript
+ response.write(changesetsJson);
+ _profiler.lap('J');
+ if (request.acceptsGzip) {
+ response.setGzip(true);
+ }
+
+ _profiler.lap('Z');
+ _profiler.dump(startRev+'/'+granularity+'/'+endRev);
+ _profiler.stop();
+
+ return true;
+}
+
+function getCacheableChangesetInfoJSON(padId, startNum, endNum, granularity) {
+ padutils.accessPadLocal(padId, function(pad) {
+ var lastRev = pad.getHeadRevisionNumber();
+ if (endNum > lastRev+1) {
+ endNum = lastRev+1;
+ }
+ endNum = Math.floor(endNum / granularity)*granularity;
+ }, 'r');
+
+ var cacheKey = "C/"+startNum+"/"+endNum+"/"+granularity+"/"+
+ padutils.getGlobalPadId(padId);
+
+ var cache = _getJSONCache();
+
+ var cachedJson = cache.get(cacheKey);
+ if (cachedJson) {
+ cache.touch(cacheKey);
+ //java.lang.System.out.println("HIT! "+cacheKey);
+ return cachedJson;
+ }
+ else {
+ var result = getChangesetInfo(padId, startNum, endNum, granularity);
+ var json = fastJSON.stringify(result);
+ cache.put(cacheKey, json);
+ //java.lang.System.out.println("MISS! "+cacheKey);
+ return json;
+ }
+}
+
+// uses changesets whose numbers are between startRev (inclusive)
+// and endRev (exclusive); 0 <= startNum < endNum
+function getChangesetInfo(padId, startNum, endNum, granularity) {
+ var forwardsChangesets = [];
+ var backwardsChangesets = [];
+ var timeDeltas = [];
+ var apool = new AttribPool();
+
+ var callId = stringutils.randomString(10);
+
+ log.custom("getchangesetinfo", {event: "start", callId:callId,
+ padId:padId, startNum:startNum,
+ endNum:endNum, granularity:granularity});
+
+ // This function may take a while and avoids holding a lock on the pad.
+ // Though the pad may change during execution of this function,
+ // after we retrieve the HEAD revision number, all other accesses
+ // are unaffected by new revisions being added to the pad.
+
+ var lastRev;
+ padutils.accessPadLocal(padId, function(pad) {
+ lastRev = pad.getHeadRevisionNumber();
+ }, 'r');
+
+ if (endNum > lastRev+1) {
+ endNum = lastRev+1;
+ }
+ endNum = Math.floor(endNum / granularity)*granularity;
+
+ var lines;
+ padutils.accessPadLocal(padId, function(pad) {
+ lines = _getPadLines(pad, startNum-1);
+ }, 'r');
+ _profiler.lap('L');
+
+ var compositeStart = startNum;
+ while (compositeStart < endNum) {
+ var whileBodyResult = padutils.accessPadLocal(padId, function(pad) {
+ _profiler.lap('c0');
+ if (compositeStart + granularity > endNum) {
+ return "break";
+ }
+ var compositeEnd = compositeStart + granularity;
+ var forwards = _composePadChangesets(pad, compositeStart, compositeEnd);
+ _profiler.lap('c1');
+ var backwards = Changeset.inverse(forwards, lines.textlines,
+ lines.alines, pad.pool());
+
+ _profiler.lap('c2');
+ Changeset.mutateAttributionLines(forwards, lines.alines, pad.pool());
+ _profiler.lap('c3');
+ Changeset.mutateTextLines(forwards, lines.textlines);
+ _profiler.lap('c4');
+
+ var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.pool(), apool);
+ _profiler.lap('c5');
+ var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.pool(), apool);
+ _profiler.lap('c6');
+ function revTime(r) {
+ var date = pad.getRevisionDate(r);
+ var s = Math.floor((+date)/1000);
+ //java.lang.System.out.println("time "+r+": "+s);
+ return s;
+ }
+
+ var t1, t2;
+ if (compositeStart == 0) {
+ t1 = revTime(0);
+ }
+ else {
+ t1 = revTime(compositeStart - 1);
+ }
+ t2 = revTime(compositeEnd - 1);
+ timeDeltas.push(t2 - t1);
+
+ _profiler.lap('c7');
+ forwardsChangesets.push(forwards2);
+ backwardsChangesets.push(backwards2);
+
+ compositeStart += granularity;
+ }, 'r');
+ if (whileBodyResult == "break") {
+ break;
+ }
+ }
+
+ log.custom("getchangesetinfo", {event: "finish", callId:callId,
+ padId:padId, startNum:startNum,
+ endNum:endNum, granularity:granularity});
+
+ return { forwardsChangesets:forwardsChangesets,
+ backwardsChangesets:backwardsChangesets,
+ apool: apool.toJsonable(),
+ actualEndNum: endNum,
+ timeDeltas: timeDeltas };
+}
+
+// Compose a series of consecutive changesets from a pad.
+// precond: startNum < endNum
+function _composePadChangesets(pad, startNum, endNum) {
+ if (endNum - startNum > 1) {
+ var csFromPad = pad.getCoarseChangeset(startNum, endNum - startNum);
+ if (csFromPad) {
+ //java.lang.System.out.println("HIT! "+startNum+"-"+endNum);
+ return csFromPad;
+ }
+ else {
+ //java.lang.System.out.println("MISS! "+startNum+"-"+endNum);
+ }
+ //java.lang.System.out.println("composePadChangesets: "+startNum+','+endNum);
+ }
+ var changeset = pad.getRevisionChangeset(startNum);
+ for(var r=startNum+1; r<endNum; r++) {
+ var cs = pad.getRevisionChangeset(r);
+ changeset = Changeset.compose(changeset, cs, pad.pool());
+ }
+ return changeset;
+}
+
+// Get arrays of text lines and attribute lines for a revision
+// of a pad.
+function _getPadLines(pad, revNum) {
+ var atext;
+ _profiler.lap('PL0');
+ if (revNum >= 0) {
+ atext = pad.getInternalRevisionAText(revNum);
+ }
+ else {
+ atext = Changeset.makeAText("\n");
+ }
+ _profiler.lap('PL1');
+ var result = {};
+ result.textlines = Changeset.splitTextLines(atext.text);
+ _profiler.lap('PL2');
+ result.alines = Changeset.splitAttributionLines(atext.attribs,
+ atext.text);
+ _profiler.lap('PL3');
+ return result;
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/control/pad/pad_control.js b/etherpad/src/etherpad/control/pad/pad_control.js
new file mode 100644
index 0000000..32ff8a3
--- /dev/null
+++ b/etherpad/src/etherpad/control/pad/pad_control.js
@@ -0,0 +1,754 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("comet");
+import("email.sendEmail");
+import("fastJSON");
+import("jsutils.eachProperty");
+import("sqlbase.sqlbase");
+import("stringutils.{toHTML,md5}");
+import("stringutils");
+
+import("etherpad.collab.collab_server");
+import("etherpad.debug.dmesg");
+import("etherpad.globals.*");
+import("etherpad.helpers");
+import("etherpad.licensing");
+import("etherpad.quotas");
+import("etherpad.log");
+import("etherpad.log.{logRequest,logException}");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pro.pro_pad_db");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_config");
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_quotas");
+
+import("etherpad.pad.revisions");
+import("etherpad.pad.chatarchive");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padusers");
+import("etherpad.control.pad.pad_view_control");
+import("etherpad.control.pad.pad_changeset_control");
+import("etherpad.control.pad.pad_importexport_control");
+import("etherpad.collab.readonly_server");
+
+import("dispatch.{Dispatcher,PrefixMatcher,DirMatcher,forward}");
+
+jimport("java.lang.System.out.println");
+
+var DISABLE_PAD_CREATION = false;
+
+function onStartup() {
+ sqlbase.createJSONTable("PAD_DIAGNOSTIC");
+}
+
+function onRequest() {
+
+ // TODO: take a hard look at /ep/pad/FOO/BAR/ dispatching.
+ // Perhaps standardize on /ep/pad/<pad-id>/foo
+ if (request.path.indexOf('/ep/pad/auth/') == 0) {
+ if (request.isGet) {
+ return render_auth_get();
+ }
+ if (request.isPost) {
+ return render_auth_post();
+ }
+ }
+
+ if (pro_utils.isProDomainRequest()) {
+ pro_quotas.perRequestBillingCheck();
+ }
+
+ var disp = new Dispatcher();
+ disp.addLocations([
+ [PrefixMatcher('/ep/pad/view/'), forward(pad_view_control)],
+ [PrefixMatcher('/ep/pad/changes/'), forward(pad_changeset_control)],
+ [PrefixMatcher('/ep/pad/impexp/'), forward(pad_importexport_control)],
+ [PrefixMatcher('/ep/pad/export/'), pad_importexport_control.renderExport]
+ ]);
+ return disp.dispatch();
+}
+
+//----------------------------------------------------------------
+// utils
+//----------------------------------------------------------------
+
+function getDefaultPadText() {
+ if (pro_utils.isProDomainRequest()) {
+ return pro_config.getConfig().defaultPadText;
+ }
+ return renderTemplateAsString("misc/pad_default.ejs", {padUrl: request.url.split("?", 1)[0]});
+}
+
+function assignName(pad, userId) {
+ if (padusers.isGuest(userId)) {
+ // use pad-specific name if possible
+ var userData = pad.getAuthorData(userId);
+ var nm = (userData && userData.name) || padusers.getUserName() || null;
+
+ // don't let name guest typed in last once we've assigned a name
+ // for this pad, so the user can change it
+ delete getSession().guestDisplayName;
+
+ return nm;
+ }
+ else {
+ return padusers.getUserName();
+ }
+}
+
+function assignColorId(pad, userId) {
+ // use pad-specific color if possible
+ var userData = pad.getAuthorData(userId);
+ if (userData && ('colorId' in userData)) {
+ return userData.colorId;
+ }
+
+ // assign random unique color
+ function r(n) {
+ return Math.floor(Math.random() * n);
+ }
+ var colorsUsed = {};
+ var users = collab_server.getConnectedUsers(pad);
+ var availableColors = [];
+ users.forEach(function(u) {
+ colorsUsed[u.colorId] = true;
+ });
+ for (var i = 0; i < COLOR_PALETTE.length; i++) {
+ if (!colorsUsed[i]) {
+ availableColors.push(i);
+ }
+ }
+ if (availableColors.length > 0) {
+ return availableColors[r(availableColors.length)];
+ } else {
+ return r(COLOR_PALETTE.length);
+ }
+}
+
+function _getPrivs() {
+ return {
+ maxRevisions: quotas.getMaxSavedRevisionsPerPad()
+ };
+}
+
+//----------------------------------------------------------------
+// linkfile (a file that users can save that redirects them to
+// a particular pad; auto-download)
+//----------------------------------------------------------------
+function render_linkfile() {
+ var padId = request.params.padId;
+
+ renderHtml("pad/pad_download_link.ejs", {
+ padId: padId
+ });
+
+ response.setHeader("Content-Disposition", "attachment; filename=\""+padId+".html\"");
+}
+
+//----------------------------------------------------------------
+// newpad
+//----------------------------------------------------------------
+
+function render_newpad() {
+ var session = getSession();
+ var padId;
+
+ if (pro_utils.isProDomainRequest()) {
+ padId = pro_pad_db.getNextPadId();
+ } else {
+ padId = randomUniquePadId();
+ }
+
+ session.instantCreate = padId;
+ response.redirect("/"+padId);
+}
+
+// Tokbox
+function render_newpad_xml_post() {
+ var localPadId;
+ if (pro_utils.isProDomainRequest()) {
+ localPadId = pro_pad_db.getNextPadId();
+ } else {
+ localPadId = randomUniquePadId();
+ }
+ // <RAFTER>
+ if (DISABLE_PAD_CREATION) {
+ if (! pro_utils.isProDomainRequest()) {
+ utils.render500();
+ return;
+ }
+ }
+ // </RAFTER>
+
+ padutils.accessPadLocal(localPadId, function(pad) {
+ if (!pad.exists()) {
+ pad.create(getDefaultPadText());
+ }
+ });
+ response.setContentType('text/plain; charset=utf-8');
+ response.write([
+ '<newpad>',
+ '<url>http://'+request.host+'/'+localPadId+'</url>',
+ '</newpad>'
+ ].join('\n'));
+}
+
+//----------------------------------------------------------------
+// pad
+//----------------------------------------------------------------
+
+function _createIfNecessary(localPadId, pad) {
+ if (pad.exists()) {
+ delete getSession().instantCreate;
+ return;
+ }
+ // make sure localPadId is valid.
+ var validPadId = padutils.makeValidLocalPadId(localPadId);
+ if (localPadId != validPadId) {
+ response.redirect('/'+validPadId);
+ }
+ // <RAFTER>
+ if (DISABLE_PAD_CREATION) {
+ if (! pro_utils.isProDomainRequest()) {
+ response.redirect("/ep/pad/create?padId="+encodeURIComponent(localPadId));
+ return;
+ }
+ }
+ // </RAFTER>
+ // tokbox may use createImmediately
+ if (request.params.createImmediately || getSession().instantCreate == localPadId) {
+ pad.create(getDefaultPadText());
+ delete getSession().instantCreate;
+ return;
+ }
+ response.redirect("/ep/pad/create?padId="+encodeURIComponent(localPadId));
+}
+
+function _promptForMobileDevices(pad) {
+ // TODO: also work with blackbery and windows mobile and others
+ if (request.userAgent.isIPhone() && (!request.params.skipIphoneCheck)) {
+ renderHtml("pad/pad_iphone_body.ejs", {padId: pad.getLocalId()});
+ response.stop();
+ }
+}
+
+function _checkPadQuota(pad) {
+ var numConnectedUsers = collab_server.getNumConnections(pad);
+ var maxUsersPerPad = quotas.getMaxSimultaneousPadEditors(pad.getId());
+
+ if (numConnectedUsers >= maxUsersPerPad) {
+ log.info("rendered-padfull");
+ renderFramed('pad/padfull_body.ejs',
+ {maxUsersPerPad: maxUsersPerPad, padId: pad.getLocalId()});
+ response.stop();
+ }
+
+ if (pne_utils.isPNE()) {
+ if (!licensing.canSessionUserJoin()) {
+ renderFramed('pad/total_users_exceeded.ejs', {
+ userQuota: licensing.getActiveUserQuota(),
+ activeUserWindowHours: licensing.getActiveUserWindowHours()
+ });
+ response.stop();
+ }
+ }
+}
+
+function _checkIfDeleted(pad) {
+ // TODO: move to access control check on access?
+ if (pro_utils.isProDomainRequest()) {
+ pro_padmeta.accessProPad(pad.getId(), function(propad) {
+ if (propad.exists() && propad.isDeleted()) {
+ renderNoticeString("This pad has been deleted.");
+ response.stop();
+ }
+ });
+ }
+}
+
+function render_pad(localPadId) {
+ var proTitle = null, documentBarTitle, initialPassword = null;
+ var isPro = isProDomainRequest();
+ var userId = padusers.getUserId();
+
+ var opts = {};
+ var globalPadId;
+
+ if (isPro) {
+ pro_quotas.perRequestBillingCheck();
+ }
+
+ padutils.accessPadLocal(localPadId, function(pad) {
+ globalPadId = pad.getId();
+ request.cache.globalPadId = globalPadId;
+ _createIfNecessary(localPadId, pad);
+ _promptForMobileDevices(pad);
+ _checkPadQuota(pad);
+ _checkIfDeleted(pad);
+
+ if (request.params.inviteTo) {
+ getSession().nameGuess = request.params.inviteTo;
+ response.redirect('/'+localPadId);
+ }
+ var displayName;
+ if (request.params.displayName) { // tokbox
+ displayName = String(request.params.displayName);
+ }
+ else {
+ displayName = assignName(pad, userId);
+ }
+
+ if (isProDomainRequest()) {
+ pro_padmeta.accessProPadLocal(localPadId, function(propad) {
+ proTitle = propad.getDisplayTitle();
+ initialPassword = propad.getPassword();
+ });
+ }
+ documentBarTitle = (proTitle || "Public Pad");
+
+ var specialKey = request.params.specialKey ||
+ (sessions.isAnEtherpadAdmin() ? collab_server.getSpecialKey('invisible') :
+ null);
+
+ helpers.addClientVars({
+ padId: localPadId,
+ globalPadId: globalPadId,
+ userAgent: request.headers["User-Agent"],
+ collab_client_vars: collab_server.getCollabClientVars(pad),
+ debugEnabled: request.params.djs,
+ clientIp: request.clientAddr,
+ colorPalette: COLOR_PALETTE,
+ nameGuess: (getSession().nameGuess || null),
+ initialRevisionList: revisions.getRevisionList(pad),
+ serverTimestamp: +(new Date),
+ accountPrivs: _getPrivs(),
+ chatHistory: chatarchive.getRecentChatBlock(pad, 30),
+ numConnectedUsers: collab_server.getNumConnections(pad),
+ isProPad: isPro,
+ initialTitle: documentBarTitle,
+ initialPassword: initialPassword,
+ initialOptions: pad.getPadOptionsObj(),
+ userIsGuest: padusers.isGuest(userId),
+ userId: userId,
+ userName: displayName,
+ userColor: assignColorId(pad, userId),
+ specialKey: specialKey,
+ specialKeyTranslation: collab_server.translateSpecialKey(specialKey),
+ });
+ });
+
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ padutils.setOptsAndCookiePrefs(request);
+ var prefs = helpers.getClientVar('cookiePrefsToSet');
+ var bodyClass = (prefs.isFullWidth ? "fullwidth" : "limwidth") +
+ " "+(isPro ? "propad" : "nonpropad")+" "+
+ (isProUser ? "prouser" : "nonprouser");
+
+
+ renderHtml("pad/pad_body2.ejs",
+ {localPadId:localPadId,
+ pageTitle:toHTML(proTitle || localPadId),
+ initialTitle:toHTML(documentBarTitle),
+ bodyClass: bodyClass,
+ hasOffice: hasOffice(),
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: getSessionProAccount(), // may be falsy
+ toHTML: toHTML,
+ prefs: prefs,
+ signinUrl: '/ep/account/sign-in?cont='+
+ encodeURIComponent(request.url),
+ fullSuperdomain: pro_utils.getFullSuperdomainHost()
+ });
+ return true;
+}
+
+function render_create_get() {
+ var padId = request.params.padId;
+ // <RAFTER>
+ var template = (DISABLE_PAD_CREATION && ! pro_utils.isProDomainRequest()) ?
+ "pad/create_body_rafter.ejs" :
+ "pad/create_body.ejs";
+ // </RAFTER>
+ renderFramed(template, {padId: padId,
+ fullSuperdomain: pro_utils.getFullSuperdomainHost()});
+}
+
+function render_create_post() {
+ var padId = request.params.padId;
+ getSession().instantCreate = padId;
+ response.redirect("/"+padId);
+}
+
+//----------------------------------------------------------------
+// saverevision
+//----------------------------------------------------------------
+
+function render_saverevision_post() {
+ var padId = request.params.padId;
+ var savedBy = request.params.savedBy;
+ var savedById = request.params.savedById;
+ var revNum = request.params.revNum;
+ var privs = _getPrivs();
+ padutils.accessPadLocal(padId, function(pad) {
+ if (! pad.exists()) { response.notFound(); }
+ var currentRevs = revisions.getRevisionList(pad);
+ if (currentRevs.length >= privs.maxRevisions) {
+ response.forbid();
+ }
+ var savedRev = revisions.saveNewRevision(pad, savedBy, savedById,
+ revNum);
+ readonly_server.broadcastNewRevision(pad, savedRev);
+ response.setContentType('text/x-json');
+ response.write(fastJSON.stringify(revisions.getRevisionList(pad)));
+ });
+}
+
+function render_saverevisionlabel_post() {
+ var userId = request.params.userId;
+ var padId = request.params.padId;
+ var revId = request.params.revId;
+ var newLabel = request.params.newLabel;
+ padutils.accessPadLocal(padId, function(pad) {
+ revisions.setLabel(pad, revId, userId, newLabel);
+ response.setContentType('text/x-json');
+ response.write(fastJSON.stringify(revisions.getRevisionList(pad)));
+ });
+}
+
+function render_getrevisionatext_get() {
+ var padId = request.params.padId;
+ var revId = request.params.revId;
+ var result = null;
+
+ var rev = padutils.accessPadLocal(padId, function(pad) {
+ var r = revisions.getStoredRevision(pad, revId);
+ var forWire = collab_server.getATextForWire(pad, r.revNum);
+ result = {atext:forWire.atext, apool:forWire.apool,
+ historicalAuthorData:forWire.historicalAuthorData};
+ return r;
+ }, "r");
+
+ response.setContentType('text/plain; charset=utf-8');
+ response.write(fastJSON.stringify(result));
+}
+
+//----------------------------------------------------------------
+// reconnect
+//----------------------------------------------------------------
+
+function _recordDiagnosticInfo(padId, diagnosticInfoJson) {
+
+ var diagnosticInfo = {};
+ try {
+ diagnosticInfo = fastJSON.parse(diagnosticInfoJson);
+ } catch (ex) {
+ log.warn("Error parsing diagnosticInfoJson: "+ex);
+ diagnosticInfo = {error: "error parsing JSON"};
+ }
+
+ // ignore userdups, unauth
+ if (diagnosticInfo.disconnectedMessage == "userdup" ||
+ diagnosticInfo.disconnectedMessage == "unauth") {
+ return;
+ }
+
+ var d = new Date();
+
+ diagnosticInfo.date = +d;
+ diagnosticInfo.strDate = String(d);
+ diagnosticInfo.clientAddr = request.clientAddr;
+ diagnosticInfo.padId = padId;
+ diagnosticInfo.headers = {};
+ eachProperty(request.headers, function(k,v) {
+ diagnosticInfo.headers[k] = v;
+ });
+
+ var uid = diagnosticInfo.uniqueId;
+
+ sqlbase.putJSON("PAD_DIAGNOSTIC", (diagnosticInfo.date)+"-"+uid, diagnosticInfo);
+
+}
+
+function recordMigratedDiagnosticInfo(objArray) {
+ objArray.forEach(function(obj) {
+ sqlbase.putJSON("PAD_DIAGNOSTIC", (obj.date)+"-"+obj.uniqueId, obj);
+ });
+}
+
+function render_reconnect() {
+ var localPadId = request.params.padId;
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ var userId = (padutils.getPrefsCookieUserId() || undefined);
+ var hasClientErrors = false;
+ var uniqueId;
+ try {
+ var obj = fastJSON.parse(request.params.diagnosticInfo);
+ uniqueId = obj.uniqueId;
+ errorMessage = obj.disconnectedMessage;
+ hasClientErrors = obj.collabDiagnosticInfo.errors.length > 0;
+ } catch (e) {
+ // guess it doesn't have errors.
+ }
+
+ log.custom("reconnect", {globalPadId: globalPadId, userId: userId,
+ uniqueId: uniqueId,
+ hasClientErrors: hasClientErrors,
+ errorMessage: errorMessage });
+
+ try {
+ _recordDiagnosticInfo(globalPadId, request.params.diagnosticInfo);
+ } catch (ex) {
+ log.warn("Error recording diagnostic info: "+ex+" / "+request.params.diagnosticInfo);
+ }
+
+ try {
+ _applyMissedChanges(localPadId, request.params.missedChanges);
+ } catch (ex) {
+ log.warn("Error applying missed changes: "+ex+" / "+request.params.missedChanges);
+ }
+
+ response.redirect('/'+localPadId);
+}
+
+/* posted asynchronously by the client as soon as reconnect dialogue appears. */
+function render_connection_diagnostic_info_post() {
+ var localPadId = request.params.padId;
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ var userId = (padutils.getPrefsCookieUserId() || undefined);
+ var hasClientErrors = false;
+ var uniqueId;
+ var errorMessage;
+ try {
+ var obj = fastJSON.parse(request.params.diagnosticInfo);
+ uniqueId = obj.uniqueId;
+ errorMessage = obj.disconnectedMessage;
+ hasClientErrors = obj.collabDiagnosticInfo.errors.length > 0;
+ } catch (e) {
+ // guess it doesn't have errors.
+ }
+ log.custom("disconnected_autopost", {globalPadId: globalPadId, userId: userId,
+ uniqueId: uniqueId,
+ hasClientErrors: hasClientErrors,
+ errorMessage: errorMessage});
+
+ try {
+ _recordDiagnosticInfo(globalPadId, request.params.diagnosticInfo);
+ } catch (ex) {
+ log.warn("Error recording diagnostic info: "+ex+" / "+request.params.diagnosticInfo);
+ }
+ response.setContentType('text/plain; charset=utf-8');
+ response.write("OK");
+}
+
+function _applyMissedChanges(localPadId, missedChangesJson) {
+ var missedChanges;
+ try {
+ missedChanges = fastJSON.parse(missedChangesJson);
+ } catch (ex) {
+ log.warn("Error parsing missedChangesJson: "+ex);
+ return;
+ }
+
+ padutils.accessPadLocal(localPadId, function(pad) {
+ if (pad.exists()) {
+ collab_server.applyMissedChanges(pad, missedChanges);
+ }
+ });
+}
+
+//----------------------------------------------------------------
+// feedback
+//----------------------------------------------------------------
+
+function render_feedback_post() {
+ var feedback = request.params.feedback;
+ var localPadId = request.params.padId;
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ var username = request.params.username;
+ var email = request.params.email;
+ var subject = 'EtherPad Feedback from '+request.clientAddr+' / '+globalPadId+' / '+username;
+
+ if (feedback.indexOf("@") > 0) {
+ subject = "@ "+subject;
+ }
+
+ feedback += "\n\n--\n";
+ feedback += ("User Agent: "+request.headers['User-Agent'] + "\n");
+ feedback += ("Session Referer: "+getSession().initialReferer + "\n");
+ feedback += ("Email: "+email+"\n");
+
+ // log feedback
+ var userId = padutils.getPrefsCookieUserId();
+ log.custom("feedback", {
+ globalPadId: globalPadId,
+ userId: userId,
+ email: email,
+ username: username,
+ feedback: request.params.feedback});
+
+ sendEmail(
+ 'feedback@pad.spline.inf.fu-berlin.de',
+ 'feedback@pad.spline.inf.fu-berlin.de',
+ subject,
+ {},
+ feedback
+ );
+ response.write("OK");
+}
+
+//----------------------------------------------------------------
+// emailinvite
+//----------------------------------------------------------------
+
+function render_emailinvite_post() {
+ var toEmails = String(request.params.toEmails).split(',');
+ var padId = String(request.params.padId);
+ var username = String(request.params.username);
+ var subject = String(request.params.subject);
+ var message = String(request.params.message);
+
+ log.custom("padinvite",
+ {toEmails: toEmails, padId: padId, username: username,
+ subject: subject, message: message});
+
+ var fromAddr = '"EtherPad" <noreply@pad.spline.inf.fu-berlin.de>';
+ // client enforces non-empty subject and message
+ var subj = '[EtherPad] '+subject;
+ var body = renderTemplateAsString('email/padinvite.ejs',
+ {body: message});
+ var headers = {};
+ var proAccount = getSessionProAccount();
+ if (proAccount) {
+ headers['Reply-To'] = proAccount.email;
+ }
+
+ response.setContentType('text/plain; charset=utf-8');
+ try {
+ sendEmail(toEmails, fromAddr, subj, headers, body);
+ response.write("OK");
+ } catch (e) {
+ logException(e);
+ response.setStatusCode(500);
+ response.write("Error");
+ }
+}
+
+//----------------------------------------------------------------
+// time-slider
+//----------------------------------------------------------------
+function render_slider() {
+ var parts = request.path.split('/');
+ var padOpaqueRef = parts[4];
+
+ helpers.addClientVars({padOpaqueRef:padOpaqueRef});
+
+ renderHtml("pad/padslider_body.ejs", {
+ // properties go here
+ });
+
+ return true;
+}
+
+//----------------------------------------------------------------
+// auth
+//----------------------------------------------------------------
+
+function render_auth_get() {
+ var parts = request.path.split('/');
+ var localPadId = parts[4];
+ var errDiv;
+ if (getSession().padPassErr) {
+ errDiv = DIV({style: "border: 1px solid #fcc; background: #ffeeee; padding: 1em; margin: 1em 0;"},
+ B(getSession().padPassErr));
+ delete getSession().padPassErr;
+ } else {
+ errDiv = DIV();
+ }
+ renderFramedHtml(function() {
+ return DIV({className: "fpcontent"},
+ DIV({style: "margin: 1em;"},
+ errDiv,
+ FORM({style: "border: 1px solid #ccc; padding: 1em; background: #fff6cc;",
+ action: request.path+'?'+request.query,
+ method: "post"},
+ LABEL(B("Please enter the password required to access this pad:")),
+ BR(), BR(),
+ INPUT({type: "text", name: "password"}), INPUT({type: "submit", value: "Submit"})
+ /*DIV(BR(), "Or ", A({href: '/ep/account/sign-in'}, "sign in"), ".")*/
+ )),
+ DIV({style: "padding: 0 1em;"},
+ P({style: "color: #444;"},
+ "If you have forgotten a pad's password, contact your site administrator.",
+ " Site administrators can recover lost pad text through the \"Admin\" tab.")
+ )
+ );
+ });
+ return true;
+}
+
+function render_auth_post() {
+ var parts = request.path.split('/');
+ var localPadId = parts[4];
+ var domainId = domains.getRequestDomainId();
+ if (!getSession().padPasswordAuth) {
+ getSession().padPasswordAuth = {};
+ }
+ var currentPassword = pro_padmeta.accessProPadLocal(localPadId, function(propad) {
+ return propad.getPassword();
+ });
+ if (request.params.password == currentPassword) {
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ getSession().padPasswordAuth[globalPadId] = true;
+ } else {
+ getSession().padPasswordAuth[globalPadId] = false;
+ getSession().padPassErr = "Incorrect password.";
+ }
+ var cont = request.params.cont;
+ if (!cont) {
+ cont = '/'+localPadId;
+ }
+ response.redirect(cont);
+}
+
+//----------------------------------------------------------------
+// chathistory
+//----------------------------------------------------------------
+
+function render_chathistory_get() {
+ var padId = request.params.padId;
+ var start = Number(request.params.start || 0);
+ var end = Number(request.params.end || 0);
+ var result = null;
+
+ var rev = padutils.accessPadLocal(padId, function(pad) {
+ result = chatarchive.getChatBlock(pad, start, end);
+ }, "r");
+
+ response.setContentType('text/plain; charset=utf-8');
+ response.write(fastJSON.stringify(result));
+}
+
diff --git a/etherpad/src/etherpad/control/pad/pad_importexport_control.js b/etherpad/src/etherpad/control/pad/pad_importexport_control.js
new file mode 100644
index 0000000..b7e5f4d
--- /dev/null
+++ b/etherpad/src/etherpad/control/pad/pad_importexport_control.js
@@ -0,0 +1,319 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("jsutils.arrayToSet");
+import("stringutils.{toHTML,md5}");
+import("stringutils");
+import("sync");
+import("varz");
+
+import("etherpad.control.pad.pad_view_control.getRevisionInfo");
+import("etherpad.helpers");
+import("etherpad.importexport.importexport");
+import("etherpad.log");
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.importhtml");
+import("etherpad.pad.exporthtml");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.{render404,renderFramedError}");
+import("etherpad.collab.server_utils");
+
+function _log(obj) {
+ log.custom("import-export", obj);
+}
+
+//---------------------------------------
+// utilities
+//---------------------------------------
+
+function _getPadTextBytes(padId, revNum) {
+ if (revNum === undefined) {
+ return null;
+ }
+ return padutils.accessPadLocal(padId, function(pad) {
+ if (pad.exists()) {
+ var txt = exporthtml.getPadPlainText(pad, revNum);
+ return (new java.lang.String(txt)).getBytes("UTF-8");
+ } else {
+ return null;
+ }
+ }, 'r');
+}
+
+function _getPadHtmlBytes(padId, revNum, noDocType) {
+ if (revNum === undefined) {
+ return null;
+ }
+ var html = padutils.accessPadLocal(padId, function(pad) {
+ if (pad.exists()) {
+ return exporthtml.getPadHTMLDocument(pad, revNum, noDocType);
+ }
+ });
+ if (html) {
+ return (new java.lang.String(html)).getBytes("UTF-8");
+ } else {
+ return null;
+ }
+}
+
+function _getFileExtension(fileName, def) {
+ if (fileName.lastIndexOf('.') > 0) {
+ return fileName.substr(fileName.lastIndexOf('.')+1);
+ } else {
+ return def;
+ }
+}
+
+function _guessFileType(contentType, fileName) {
+ function _f(str) { return function() { return str; }}
+ var unchangedExtensions =
+ arrayToSet(['txt', 'htm', 'html', 'doc', 'docx', 'rtf', 'pdf', 'odt']);
+ var textExtensions =
+ arrayToSet(['js', 'scala', 'java', 'c', 'cpp', 'log', 'h', 'htm', 'html', 'css', 'php', 'xhtml',
+ 'dhtml', 'jsp', 'asp', 'sh', 'bat', 'pl', 'py']);
+ var contentTypes = {
+ 'text/plain': 'txt',
+ 'text/html': 'html',
+ 'application/msword': 'doc',
+ 'application/vnd.oasis.opendocument.text': 'odt',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
+ 'text/rtf': 'rtf',
+ 'application/pdf': 'pdf'
+ }
+
+ var ext = _getFileExtension(fileName);
+ if (ext) {
+ if (unchangedExtensions[ext]) {
+ return ext;
+ } else if (textExtensions[ext]) {
+ return 'txt';
+ }
+ }
+ if (contentType in contentTypes) {
+ return contentTypes[contentType]
+ }
+ // unknown type, nothing to return.
+ _log({type: "warning", error: "unknown-type", contentType: contentType, fileName: fileName});
+}
+
+function _noteExportFailure() {
+ varz.incrementInt("export-failed");
+}
+
+function _noteImportFailure() {
+ varz.incrementInt("import-failed");
+}
+
+//---------------------------------------
+// export
+//---------------------------------------
+
+// handles /ep/pad/export/*
+function renderExport() {
+ var parts = request.path.split('/');
+ var padId = server_utils.parseUrlId(parts[4]).localPadId;
+ var revisionId = parts[5];
+ var rev = null;
+ var format = request.params.format || 'txt';
+
+ if (! request.cache.skipAccess) {
+ _log({type: "request", direction: "export", format: format});
+ rev = getRevisionInfo(padId, revisionId);
+ if (! rev) {
+ render404();
+ }
+ request.cache.skipAccess = true;
+ }
+
+ var result = _exportToFormat(padId, revisionId, (rev || {}).revNum, format);
+ if (result === true) {
+ response.stop();
+ } else {
+ renderFramedError(result);
+ }
+ return true;
+}
+
+function _exportToFormat(padId, revisionId, revNum, format) {
+ var bytes = _doExportConversion(format,
+ function() { return _getPadTextBytes(padId, revNum); },
+ function(noDocType) { return _getPadHtmlBytes(padId, revNum, noDocType); });
+ if (! bytes) {
+ return "Unable to convert file for export... try a different format?"
+ } else if (typeof(bytes) == 'string') {
+ return bytes
+ } else {
+ response.setContentType(importexport.formats[format]);
+ response.setHeader("Content-Disposition", "attachment; filename=\""+padId+"-"+revisionId+"."+format+"\"");
+ response.writeBytes(bytes);
+ return true;
+ }
+}
+
+
+function _doExportConversion(format, getTextBytes, getHtmlBytes) {
+ if (! (format in importexport.formats)) {
+ return false;
+ }
+ var bytes;
+ var srcFormat;
+
+ if (format == 'txt') {
+ bytes = getTextBytes();
+ srcFormat = 'txt';
+ } else {
+ bytes = getHtmlBytes(format == 'doc' || format == 'odt');
+ srcFormat = 'html';
+ }
+ if (bytes == null) {
+ bytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 0);
+ }
+
+ try {
+ var ret = importexport.convertFile(srcFormat, format, bytes);
+ if (typeof(ret) == 'string') {
+ _log({type: "error", error: "export-failed", format: format, message: ret});
+ _noteExportFailure();
+ return ret;
+ }
+ bytes = ret;
+ } catch (e) {
+ if (e.javaException instanceof org.mortbay.jetty.RetryRequest) {
+ throw e.javaException
+ }
+ if (e.javaException || e.rhinoException) {
+ net.appjet.oui.exceptionlog.apply(e.javaException || e.rhinoException);
+ }
+ bytes = null;
+ }
+ if (bytes == null || bytes.length == 0) {
+ _log({type: "error", error: "export-failed", format: format, message: ret});
+ _noteExportFailure();
+ return false;
+ }
+ return bytes;
+}
+
+//---------------------------------------
+// import
+//---------------------------------------
+
+function _getImportInfo(key) {
+ var session = getSession();
+ sync.callsyncIfTrue(session, function() { return ! ('importexport' in session) },
+ function() {
+ session.importexport = {};
+ });
+ var tokens = session.importexport;
+ sync.callsyncIfTrue(tokens, function() { return ! (key in tokens) },
+ function() {
+ tokens[key] = {};
+ });
+ return tokens[key];
+}
+
+function render_import() {
+ function _r(code) {
+ response.setContentType("text/html");
+ response.write("<html><body><script>try{parent.document.domain}catch(e){document.domain=document.domain}\n"+code+"</script></body></html>");
+ response.stop();
+ }
+
+ if (! request.isPost) {
+ response.stop();
+ }
+
+ var padId = decodeURIComponent(request.params.padId);
+ if (! padId) {
+ response.stop();
+ }
+
+ var file = request.files.file;
+ if (! file) {
+ _r('parent.pad.handleImportExportFrameCall("importFailed", "Please select a file to import.")');
+ }
+
+ var bytes = file.bytes;
+ var type = _guessFileType(file.contentType, file.filesystemName);
+
+ _log({type: "request", direction: "import", format: type});
+
+ if (! type) {
+ type = _getFileExtension(file.filesystemName, "no file extension found");
+ _r('parent.pad.handleImportExportFrameCall("importFailed", "'+importexport.errorUnsupported(type)+'")');
+ }
+
+ var token = md5(bytes);
+ var state = _getImportInfo(token);
+ state.bytes = bytes;
+ state.type = type;
+
+ _r("parent.pad.handleImportExportFrameCall('importSuccessful', '"+token+"')");
+}
+
+
+function render_import2() {
+ var token = request.params.token;
+
+ function _r(txt) {
+ response.write(txt);
+ response.stop();
+ }
+
+ if (! token) { _r("fail"); }
+
+ var state = _getImportInfo(token);
+ if (! state.type || ! state.bytes) { _r("fail"); }
+
+ var newBytes;
+ try {
+ newBytes = importexport.convertFile(state.type, "html", state.bytes);
+ } catch (e) {
+ if (e.javaException instanceof org.mortbay.jetty.RetryRequest) {
+ throw e.javaException;
+ }
+ net.appjet.oui.exceptionlog.apply(e);
+ throw e;
+ }
+
+ if (typeof(newBytes) == 'string') {
+ _log({type: "error", error: "import-failed", format: state.type, message: newBytes});
+ _noteImportFailure();
+ _r("msg:"+newBytes);
+ }
+
+ if (! newBytes || newBytes.length == 0) {
+ _r("fail");
+ }
+
+ var newHTML;
+ try {
+ newHTML = String(new java.lang.String(newBytes, "UTF-8"));
+ } catch (e) {
+ _r("fail");
+ }
+
+ if (! request.params.padId) { _r("fail"); }
+ padutils.accessPadLocal(request.params.padId, function(pad) {
+ if (! pad.exists()) {
+ _r("fail");
+ }
+ importhtml.setPadHTML(pad, newHTML);
+ });
+ _r("ok");
+}
diff --git a/etherpad/src/etherpad/control/pad/pad_view_control.js b/etherpad/src/etherpad/control/pad/pad_view_control.js
new file mode 100644
index 0000000..0606d2c
--- /dev/null
+++ b/etherpad/src/etherpad/control/pad/pad_view_control.js
@@ -0,0 +1,287 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.helpers");
+import("etherpad.pad.model");
+import("etherpad.pad.padusers");
+import("etherpad.pad.padutils");
+import("etherpad.pad.exporthtml");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.utils.*");
+import("etherpad.pad.revisions");
+import("stringutils.toHTML");
+import("etherpad.collab.server_utils.*");
+import("etherpad.collab.collab_server.buildHistoricalAuthorDataMapForPadHistory");
+import("etherpad.collab.collab_server.getATextForWire");
+import("etherpad.control.pad.pad_changeset_control.getChangesetInfo");
+import("etherpad.globals");
+import("fastJSON");
+import("etherpad.collab.ace.easysync2.Changeset");
+import("etherpad.collab.ace.linestylefilter.linestylefilter");
+import("etherpad.collab.ace.domline.domline");
+
+//----------------------------------------------------------------
+// view (viewing a static revision of a pad)
+//----------------------------------------------------------------
+
+function onRequest() {
+ var parts = request.path.split('/');
+ // TODO(kroo): create a mapping between padId and read-only id
+ var readOnlyIdOrLocalPadId = parts[4];
+ var parseResult = parseUrlId(readOnlyIdOrLocalPadId);
+ var isReadOnly = parseResult.isReadOnly;
+ var viewId = parseResult.viewId;
+ var localPadId = parseResult.localPadId;
+ var globalPadId = parseResult.globalPadId;
+ var roPadId = parseResult.roPadId;
+ var revisionId = parts[5];
+
+ var rev = getRevisionInfo(localPadId, revisionId);
+ if (! rev) {
+ return false;
+ }
+
+ if (request.params.pt == 1) {
+ var padText = padutils.accessPadLocal(localPadId, function(pad) {
+ return pad.getRevisionText(rev.revNum);
+ }, 'r');
+
+ response.setContentType('text/plain; charset=utf-8');
+ response.write(padText);
+ } else {
+ var padContents, totalRevs, atextForWire, savedRevisions;
+ var supportsSlider;
+ padutils.accessPadLocal(localPadId, function(pad) {
+ padContents = [_getPadHTML(pad, rev.revNum),
+ pad.getRevisionText(rev.revNum)];
+ totalRevs = pad.getHeadRevisionNumber();
+ atextForWire = getATextForWire(pad, rev.revNum);
+ savedRevisions = revisions.getRevisionList(pad);
+ supportsSlider = pad.getSupportsTimeSlider();
+ }, 'r');
+
+ var _add = function(dict, anotherdict) {
+ for(var key in anotherdict) {
+ dict[key] = anotherdict[key];
+ }
+ return dict;
+ }
+
+ var getAdaptiveChangesetsArray = function(array, start, granularity) {
+ array = array || [];
+ start = start || 0;
+ granularity = granularity || Math.pow(10, Math.floor(Math.log(totalRevs+1) / Math.log(10)));
+ var changeset = _add(getChangesetInfo(localPadId, start, totalRevs+1, granularity), {
+ start: start,
+ granularity: Math.floor(granularity)
+ });
+ array.push(changeset);
+ if(changeset.actualEndNum != totalRevs+1 && granularity > 1)
+ getAdaptiveChangesetsArray(array, changeset.actualEndNum, Math.floor(granularity / 10));
+ return array;
+ }
+ var initialChangesets = [];
+ if (supportsSlider) {
+ initialChangesets = getAdaptiveChangesetsArray(
+ [
+ _add(getChangesetInfo(localPadId, Math.floor(rev.revNum / 1000)*1000, Math.floor(rev.revNum / 1000)*1000+1000, 100), {
+ start: Math.floor(rev.revNum / 1000)*1000,
+ granularity: 100
+ }),
+ _add(getChangesetInfo(localPadId, Math.floor(rev.revNum / 100)*100, Math.floor(rev.revNum / 100)*100+100, 10), {
+ start: Math.floor(rev.revNum / 100)*100,
+ granularity: 10
+ }),
+ _add(getChangesetInfo(localPadId, Math.floor(rev.revNum / 10)*10, Math.floor(rev.revNum / 10)*10+10, 1), {
+ start: Math.floor(rev.revNum / 10)*10,
+ granularity: 1
+ })]
+ );
+ }
+
+ var zpad = function(str, length) {
+ str = str+"";
+ while(str.length < length)
+ str = '0'+str;
+ return str;
+ };
+ var dateFormat = function(savedWhen) {
+ var date = new Date(savedWhen);
+ var month = zpad(date.getMonth()+1,2);
+ var day = zpad(date.getDate(),2);
+ var year = (date.getFullYear());
+ var hours = zpad(date.getHours(),2);
+ var minutes = zpad(date.getMinutes(),2);
+ var seconds = zpad(date.getSeconds(),2);
+ return ([month,'/',day,'/',year,' ',hours,':',minutes,':',seconds].join(""));
+ };
+
+ var proTitle = null;
+ var initialPassword = null;
+ if (isProDomainRequest()) {
+ pro_padmeta.accessProPadLocal(localPadId, function(propad) {
+ proTitle = propad.getDisplayTitle();
+ initialPassword = propad.getPassword();
+ });
+ }
+ var documentBarTitle = (proTitle || "Public Pad");
+
+ var padHTML = padContents[0];
+ var padText = padContents[1];
+
+ var historicalAuthorData = padutils.accessPadLocal(localPadId, function(pad) {
+ return buildHistoricalAuthorDataMapForPadHistory(pad);
+ }, 'r');
+
+ helpers.addClientVars({
+ viewId: viewId,
+ initialPadContents: padText,
+ revNum: rev.revNum,
+ totalRevs: totalRevs,
+ initialChangesets: initialChangesets,
+ initialStyledContents: atextForWire,
+ savedRevisions: savedRevisions,
+ currentTime: rev.timestamp,
+ sliderEnabled: (!appjet.cache.killSlider) && request.params.slider != 0,
+ supportsSlider: supportsSlider,
+ historicalAuthorData: historicalAuthorData,
+ colorPalette: globals.COLOR_PALETTE,
+ padIdForUrl: readOnlyIdOrLocalPadId,
+ fullWidth: request.params.fullScreen == 1,
+ disableRightBar: request.params.sidebar == 0,
+ });
+
+ var userId = padusers.getUserId();
+ var isPro = isProDomainRequest();
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ var bodyClass = ["limwidth",
+ (isPro ? "propad" : "nonpropad"),
+ (isProUser ? "prouser" : "nonprouser")].join(" ");
+
+ renderHtml("pad/padview_body.ejs", {
+ bodyClass: bodyClass,
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: pro_accounts.getSessionProAccount(),
+ signinUrl: '/ep/account/sign-in?cont='+
+ encodeURIComponent(request.url),
+ padId: readOnlyIdOrLocalPadId,
+ padTitle: documentBarTitle,
+ rlabel: rev.label,
+ padHTML: padHTML,
+ padText: padText,
+ savedBy: rev.savedBy,
+ savedIp: rev.ip,
+ savedWhen: rev.timestamp,
+ toHTML: toHTML,
+ revisionId: revisionId,
+ dateFormat: dateFormat(rev.timestamp),
+ readOnly: isReadOnly,
+ roPadId: roPadId,
+ hasOffice: hasOffice()
+ });
+ }
+
+ return true;
+}
+
+function getRevisionInfo(localPadId, revisionId) {
+ var rev = padutils.accessPadLocal(localPadId, function(pad) {
+ if (!pad.exists()) {
+ return null;
+ }
+ var r;
+ if (revisionId == "latest") {
+ // a "fake" revision for HEAD
+ var headRevNum = pad.getHeadRevisionNumber();
+ r = {
+ revNum: headRevNum,
+ label: "Latest text of pad "+localPadId,
+ savedBy: null,
+ savedIp: null,
+ timestamp: +pad.getRevisionDate(headRevNum)
+ };
+ } else if (revisionId == "autorecover") {
+ var revNum = _findLastGoodRevisionInPad(pad);
+ r = {
+ revNum: revNum,
+ label: "Auto-recovered text of pad "+localPadId,
+ savedBy: null,
+ savedIp: null,
+ timestamp: +pad.getRevisionDate(revNum)
+ };
+ } else if(revisionId.indexOf("rev.") === 0) {
+ var revNum = parseInt(revisionId.split(".")[1]);
+ var latest = pad.getHeadRevisionNumber();
+ if(revNum > latest)
+ revNum = latest;
+ r = {
+ revNum: revNum,
+ label: "Version " + revNum,
+ savedBy: null,
+ savedIp: null,
+ timestamp: +pad.getRevisionDate(revNum)
+ }
+
+ } else {
+ r = revisions.getStoredRevision(pad, revisionId);
+ }
+ if (!r) {
+ return null;
+ }
+ return r;
+ }, "r");
+ return rev;
+}
+
+function _findLastGoodRevisionInPad(pad) {
+ var revNum = pad.getHeadRevisionNumber();
+ function valueOrNullOnError(f) {
+ try { return f(); } catch (e) { return null; }
+ }
+ function isAcceptable(strOrNull) {
+ return (strOrNull && strOrNull.length > 20);
+ }
+ while (revNum > 0 &&
+ ! isAcceptable(valueOrNullOnError(function() { return pad.getRevisionText(revNum); }))) {
+ revNum--;
+ }
+ return revNum;
+}
+
+function _getPadHTML(pad, revNum) {
+ var atext = pad.getInternalRevisionAText(revNum);
+ var textlines = Changeset.splitTextLines(atext.text);
+ var alines = Changeset.splitAttributionLines(atext.attribs,
+ atext.text);
+
+ var pieces = [];
+ var apool = pad.pool();
+ for(var i=0;i<textlines.length;i++) {
+ var line = textlines[i];
+ var aline = alines[i];
+ var emptyLine = (line == '\n');
+ var domInfo = domline.createDomLine(! emptyLine, true);
+ linestylefilter.populateDomLine(line, aline, apool, domInfo);
+ domInfo.prepareForAdd();
+ var node = domInfo.node;
+ pieces.push('<div class="', node.className, '">',
+ node.innerHTML, '</div>\n');
+ }
+ return pieces.join('');
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/control/pne_manual_control.js b/etherpad/src/etherpad/control/pne_manual_control.js
new file mode 100644
index 0000000..0dd65f8
--- /dev/null
+++ b/etherpad/src/etherpad/control/pne_manual_control.js
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+
+function onRequest() {
+ var p = request.path.split('/')[3];
+ if (!p) {
+ p = "main";
+ }
+ if (_getTitle(p)) {
+ _renderManualPage(p);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+function _getTitle(t) {
+ var titles = {
+ 'main': " ",
+ 'installation-guide': "Installation Guide",
+ 'upgrade-guide': "Upgrade Guide",
+ 'configuration-guide': "Configuration Guide",
+ 'troubleshooting': "Troubleshooting",
+ 'faq': "FAQ",
+ 'changelog': "ChangeLog"
+ };
+ return titles[t];
+}
+
+function _renderTopnav(p) {
+ var d = DIV({className: "pne-manual-topnav"});
+ if (p != "main") {
+ d.push(A({href: '/ep/pne-manual/'}, "PNE Manual"),
+ " > ",
+ _getTitle(p));
+ }
+ return d;
+}
+
+function _renderManualPage(p, data) {
+ data = (data || {});
+ data.pneVersion = PNE_RELEASE_VERSION;
+
+ function getContent() {
+ return renderTemplateAsString('pne-manual/'+p+'.ejs', data);
+ }
+ renderFramed('pne-manual/manual-template.ejs', {
+ getContent: getContent,
+ renderTopnav: function() { return _renderTopnav(p); },
+ title: _getTitle(p),
+ id: p,
+ });
+ return true;
+}
+
+
+
diff --git a/etherpad/src/etherpad/control/pne_tracker_control.js b/etherpad/src/etherpad/control/pne_tracker_control.js
new file mode 100644
index 0000000..ee36645
--- /dev/null
+++ b/etherpad/src/etherpad/control/pne_tracker_control.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("image");
+import("blob");
+import("sqlbase.sqlobj");
+import("jsutils.*");
+
+function render_t() {
+ var data = {
+ date: new Date(),
+ remoteIp: request.clientAddr
+ };
+ if (request.params.k) {
+ data.keyHash = request.params.k;
+ }
+ var found = false;
+ eachProperty(request.params, function(name, value) {
+ if (name != "k") {
+ data.name = name;
+ data.value = value;
+ found = true;
+ }
+ });
+ if (found) {
+ sqlobj.insert('pne_tracking_data', data);
+ }
+
+ // serve a 1x1 white image
+ if (!appjet.cache.pneTrackingImage) {
+ appjet.cache.pneTrackingImage = image.solidColorImageBlob(1, 1, "ffffff");
+ }
+ blob.serveBlob(appjet.cache.pneTrackingImage);
+}
+
diff --git a/etherpad/src/etherpad/control/pro/account_control.js b/etherpad/src/etherpad/control/pro/account_control.js
new file mode 100644
index 0000000..031dbe6
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/account_control.js
@@ -0,0 +1,369 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("stringutils");
+import("stringutils.*");
+import("funhtml.*");
+import("email.sendEmail");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.helpers");
+import("etherpad.utils.*");
+import("etherpad.sessions.getSession");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_account_auto_signin");
+import("etherpad.pro.pro_config");
+import("etherpad.pad.pad_security");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padusers");
+import("etherpad.collab.collab_server");
+
+function onRequest() {
+ if (!getSession().tempFormData) {
+ getSession().tempFormData = {};
+ }
+
+ return false; // path not handled here
+}
+
+//--------------------------------------------------------------------------------
+// helpers
+//--------------------------------------------------------------------------------
+
+function _redirOnError(m, clearQuery) {
+ if (m) {
+ getSession().accountFormError = m;
+
+ var dest = request.url;
+ if (clearQuery) {
+ dest = request.path;
+ }
+ response.redirect(dest);
+ }
+}
+
+function setSigninNotice(m) {
+ getSession().accountSigninNotice = m;
+}
+
+function setSessionError(m) {
+ getSession().accountFormError = m;
+}
+
+function _topDiv(id, name) {
+ var m = getSession()[name];
+ if (m) {
+ delete getSession()[name];
+ return DIV({id: id}, m);
+ } else {
+ return '';
+ }
+}
+
+function _messageDiv() { return _topDiv('account-message', 'accountMessage'); }
+function _errorDiv() { return _topDiv('account-error', 'accountFormError'); }
+function _signinNoticeDiv() { return _topDiv('signin-notice', 'accountSigninNotice'); }
+
+function _renderTemplate(name, data) {
+ data.messageDiv = _messageDiv;
+ data.errorDiv = _errorDiv;
+ data.signinNotice = _signinNoticeDiv;
+ data.tempFormData = getSession().tempFormData;
+ renderFramed('pro/account/'+name+'.ejs', data);
+}
+
+//----------------------------------------------------------------
+// /ep/account/
+//----------------------------------------------------------------
+
+function render_main_get() {
+ _renderTemplate('my-account', {
+ account: getSessionProAccount(),
+ changePass: getSession().changePass
+ });
+}
+
+function render_update_info_get() {
+ response.redirect('/ep/account/');
+}
+
+function render_update_info_post() {
+ var fullName = request.params.fullName;
+ var email = trim(request.params.email);
+
+ getSession().tempFormData.email = email;
+ getSession().tempFormData.fullName = fullName;
+
+ _redirOnError(pro_accounts.validateEmail(email));
+ _redirOnError(pro_accounts.validateFullName(fullName));
+
+ pro_accounts.setEmail(getSessionProAccount(), email);
+ pro_accounts.setFullName(getSessionProAccount(), fullName);
+
+ getSession().accountMessage = "Info updated.";
+ response.redirect('/ep/account/');
+}
+
+function render_update_password_get() {
+ response.redirect('/ep/account/');
+}
+
+function render_update_password_post() {
+ var password = request.params.password;
+ var passwordConfirm = request.params.passwordConfirm;
+
+ if (password != passwordConfirm) { _redirOnError('Passwords did not match.'); }
+
+ _redirOnError(pro_accounts.validatePassword(password));
+
+ pro_accounts.setPassword(getSessionProAccount(), password);
+
+ if (getSession().changePass) {
+ delete getSession().changePass;
+ response.redirect('/');
+ }
+
+ getSession().accountMessage = "Password updated.";
+ response.redirect('/ep/account/');
+}
+
+//--------------------------------------------------------------------------------
+// signin/signout
+//--------------------------------------------------------------------------------
+
+function render_sign_in_get() {
+ if (request.params.uid && request.params.tp) {
+ var m = pro_accounts.authenticateTempSignIn(Number(request.params.uid), request.params.tp);
+ if (m) {
+ getSession().accountFormError = m;
+ response.redirect('/ep/account/');
+ }
+ }
+ if (request.params.instantSigninKey) {
+ _attemptInstantSignin(request.params.instantSigninKey);
+ }
+ if (getSession().recentlySignedOut && getSession().accountFormError) {
+ delete getSession().accountFormError;
+ delete getSession().recentlySignedOut;
+ }
+ // Note: must check isAccountSignedIn before calling checkAutoSignin()!
+ if (pro_accounts.isAccountSignedIn()) {
+ _redirectToPostSigninDestination();
+ }
+ pro_account_auto_signin.checkAutoSignin();
+ var domainRecord = domains.getRequestDomainRecord();
+ var showGuestBox = false;
+ if (request.params.guest && request.params.padId) {
+ showGuestBox = true;
+ }
+ _renderTemplate('signin', {
+ domain: pro_utils.getFullProDomain(),
+ siteName: toHTML(pro_config.getConfig().siteName),
+ email: getSession().tempFormData.email || "",
+ password: getSession().tempFormData.password || "",
+ rememberMe: getSession().tempFormData.rememberMe || false,
+ showGuestBox: showGuestBox,
+ localPadId: request.params.padId
+ });
+}
+
+function _attemptInstantSignin(key) {
+ // See src/etherpad/control/global_pro_account_control.js
+ var email = null;
+ var password = null;
+ syncedWithCache('global_signin_passwords', function(c) {
+ if (c[key]) {
+ email = c[key].email;
+ password = c[key].password;
+ }
+ delete c[key];
+ });
+ getSession().tempFormData.email = email;
+ _redirOnError(pro_accounts.authenticateSignIn(email, password), true);
+}
+
+function render_sign_in_post() {
+ var email = trim(request.params.email);
+ var password = request.params.password;
+
+ getSession().tempFormData.email = email;
+ getSession().tempFormData.rememberMe = request.params.rememberMe;
+
+ _redirOnError(pro_accounts.authenticateSignIn(email, password));
+ pro_account_auto_signin.setAutoSigninCookie(request.params.rememberMe);
+ _redirectToPostSigninDestination();
+}
+
+function render_guest_sign_in_get() {
+ var localPadId = request.params.padId;
+ var domainId = domains.getRequestDomainId();
+ var globalPadId = padutils.makeGlobalId(domainId, localPadId);
+ var userId = padusers.getUserId();
+
+ pro_account_auto_signin.checkAutoSignin();
+ pad_security.clearKnockStatus(userId, globalPadId);
+
+ _renderTemplate('signin-guest', {
+ localPadId: localPadId,
+ errorMessage: getSession().guestAccessError,
+ siteName: toHTML(pro_config.getConfig().siteName),
+ guestName: padusers.getUserName() || ""
+ });
+}
+
+function render_guest_sign_in_post() {
+ function _err(m) {
+ if (m) {
+ getSession().guestAccessError = m;
+ response.redirect(request.url);
+ }
+ }
+ var displayName = request.params.guestDisplayName;
+ var localPadId = request.params.localPadId;
+ if (!(displayName && displayName.length > 0)) {
+ _err("Please enter a display name");
+ }
+ getSession().guestDisplayName = displayName;
+ response.redirect('/ep/account/guest-knock?padId='+encodeURIComponent(localPadId)+
+ "&guestDisplayName="+encodeURIComponent(displayName));
+}
+
+function render_guest_knock_get() {
+ var localPadId = request.params.padId;
+ helpers.addClientVars({
+ localPadId: localPadId,
+ guestDisplayName: request.params.guestDisplayName,
+ padUrl: "http://"+httpHost(request.host)+"/"+localPadId
+ });
+ _renderTemplate('guest-knock', {});
+}
+
+function render_guest_knock_post() {
+ var localPadId = request.params.padId;
+ var displayName = request.params.guestDisplayName;
+ var domainId = domains.getRequestDomainId();
+ var globalPadId = padutils.makeGlobalId(domainId, localPadId);
+ var userId = padusers.getUserId();
+
+ response.setContentType("text/plain; charset=utf-8");
+ // has the knock already been answsered?
+ var currentAnswer = pad_security.getKnockAnswer(userId, globalPadId);
+ if (currentAnswer) {
+ response.write(currentAnswer);
+ } else {
+ collab_server.guestKnock(globalPadId, userId, displayName);
+ response.write("wait");
+ }
+}
+
+function _redirectToPostSigninDestination() {
+ var cont = request.params.cont;
+ if (!cont) { cont = '/'; }
+ response.redirect(cont);
+}
+
+function render_sign_out() {
+ pro_account_auto_signin.setAutoSigninCookie(false);
+ pro_accounts.signOut();
+ delete getSession().padPasswordAuth;
+ getSession().recentlySignedOut = true;
+ response.redirect("/");
+}
+
+//--------------------------------------------------------------------------------
+// create-admin-account (eepnet only)
+//--------------------------------------------------------------------------------
+
+function render_create_admin_account_get() {
+ if (pro_accounts.doesAdminExist()) {
+ renderFramedError("An admin account already exists on this domain.");
+ response.stop();
+ }
+ _renderTemplate('create-admin-account', {});
+}
+
+function render_create_admin_account_post() {
+ var email = trim(request.params.email);
+ var password = request.params.password;
+ var passwordConfirm = request.params.passwordConfirm;
+ var fullName = request.params.fullName;
+
+ getSession().tempFormData.email = email;
+ getSession().tempFormData.fullName = fullName;
+
+ if (password != passwordConfirm) { _redirOnError('Passwords did not match.'); }
+
+ _redirOnError(pro_accounts.validateEmail(email));
+ _redirOnError(pro_accounts.validateFullName(fullName));
+ _redirOnError(pro_accounts.validatePassword(password));
+
+ pro_accounts.createNewAccount(null, fullName, email, password, true);
+
+ var u = pro_accounts.getAccountByEmail(email, null);
+
+ // TODO: should we send a welcome email here?
+ //pro_accounts.sendWelcomeEmail(u);
+
+ _redirOnError(pro_accounts.authenticateSignIn(email, password));
+
+ response.redirect("/");
+}
+
+
+//--------------------------------------------------------------------------------
+// forgot password
+//--------------------------------------------------------------------------------
+
+function render_forgot_password_get() {
+ if (request.params.instantSubmit && request.params.email) {
+ render_forgot_password_post();
+ } else {
+ _renderTemplate('forgot-password', {
+ email: getSession().tempFormData.email || ""
+ });
+ }
+}
+
+function render_forgot_password_post() {
+ var email = trim(request.params.email);
+
+ getSession().tempFormData.email = email;
+
+ var u = pro_accounts.getAccountByEmail(email, null);
+ if (!u) {
+ _redirOnError("Account not found: "+email);
+ }
+
+ var tempPass = stringutils.randomString(10);
+ pro_accounts.setTempPassword(u, tempPass);
+
+ var subj = "EtherPad: Request to reset your password on "+request.domain;
+ var body = renderTemplateAsString('pro/account/forgot-password-email.ejs', {
+ account: u,
+ recoverUrl: pro_accounts.getTempSigninUrl(u, tempPass)
+ });
+ var fromAddr = pro_utils.getEmailFromAddr();
+ sendEmail(u.email, fromAddr, subj, {}, body);
+
+ getSession().accountMessage = "An email has been sent to "+u.email+" with instructions to reset the password.";
+ response.redirect(request.path);
+}
+
+
+
diff --git a/etherpad/src/etherpad/control/pro/admin/account_manager_control.js b/etherpad/src/etherpad/control/pro/admin/account_manager_control.js
new file mode 100644
index 0000000..8f93b2e
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/admin/account_manager_control.js
@@ -0,0 +1,260 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("stringutils");
+import("stringutils.*");
+import("email.sendEmail");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.sessions.getSession");
+
+import("etherpad.control.pro.admin.pro_admin_control");
+
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_config");
+import("etherpad.pro.domains");
+import("etherpad.billing.team_billing");
+
+jimport("java.lang.System.out.println");
+
+function _err(m) {
+ if (m) {
+ getSession().accountManagerError = m;
+ response.redirect(request.path);
+ }
+}
+
+function _renderTopDiv(mid, htmlId) {
+ var m = getSession()[mid];
+ if (m) {
+ delete getSession()[mid];
+ return DIV({id: htmlId}, m);
+ } else {
+ return '';
+ }
+}
+
+function _errorDiv() { return _renderTopDiv('accountManagerError', 'error-message'); }
+function _messageDiv() { return _renderTopDiv('accountManagerMessage', 'message'); }
+function _warningDiv() { return _renderTopDiv('accountManagerWarning', 'warning'); }
+
+function onRequest() {
+ var parts = request.path.split('/');
+
+ function dispatchAccountAction(action, handlerGet, handlerPost) {
+ if ((parts[4] == action) && (isNumeric(parts[5]))) {
+ if (request.isGet) { handlerGet(+parts[5]); }
+ if (request.isPost) { handlerPost(+parts[5]); }
+ return true;
+ }
+ return false;
+ }
+
+ if (dispatchAccountAction('account', render_account_get, render_account_post)) {
+ return true;
+ }
+ if (dispatchAccountAction('delete-account', render_delete_account_get, render_delete_account_post)) {
+ return true;
+ };
+
+ return false;
+}
+
+function render_main() {
+ var accountList = pro_accounts.listAllDomainAccounts();
+ pro_admin_control.renderAdminPage('account-manager', {
+ accountList: accountList,
+ messageDiv: _messageDiv,
+ warningDiv: _warningDiv
+ });
+}
+
+function render_new_get() {
+ pro_admin_control.renderAdminPage('new-account', {
+ oldData: getSession().accountManagerFormData || {},
+ stringutils: stringutils,
+ errorDiv: _errorDiv
+ });
+}
+
+function _ensureBillingOK() {
+ var activeAccounts = pro_accounts.getCachedActiveCount(domains.getRequestDomainId());
+ if (activeAccounts < PRO_FREE_ACCOUNTS) {
+ return;
+ }
+
+ var status = team_billing.getDomainStatus(domains.getRequestDomainId());
+ if (!((status == team_billing.CURRENT)
+ || (status == team_billing.PAST_DUE))) {
+ _err(SPAN(
+ "A payment profile is required to create more than ", PRO_FREE_ACCOUNTS,
+ " accounts. ",
+ A({href: "/ep/admin/billing/", id: "billinglink"}, "Manage billing")));
+ }
+}
+
+function render_new_post() {
+ if (request.params.cancel) {
+ response.redirect('/ep/admin/account-manager/');
+ }
+
+ _ensureBillingOK();
+
+ var fullName = request.params.fullName;
+ var email = trim(request.params.email);
+ var tempPass = request.params.tempPass;
+ var makeAdmin = !!request.params.makeAdmin;
+
+ getSession().accountManagerFormData = {
+ fullName: fullName,
+ email: email,
+ tempPass: tempPass,
+ makeAdmin: makeAdmin
+ };
+
+ // validation
+ if (!tempPass) {
+ tempPass = stringutils.randomString(6);
+ }
+
+ _err(pro_accounts.validateEmail(email));
+ _err(pro_accounts.validateFullName(fullName));
+ _err(pro_accounts.validatePassword(tempPass));
+
+ var existingAccount = pro_accounts.getAccountByEmail(email, null);
+ if (existingAccount) {
+ _err("There is already a account with that email address.");
+ }
+
+ pro_accounts.createNewAccount(null, fullName, email, tempPass, makeAdmin);
+ var account = pro_accounts.getAccountByEmail(email, null);
+
+ pro_accounts.setTempPassword(account, tempPass);
+ sendWelcomeEmail(account, tempPass);
+
+ delete getSession().accountManagerFormData;
+ getSession().accountManagerMessage = "Account "+fullName+" ("+email+") created successfully.";
+ response.redirect('/ep/admin/account-manager/');
+}
+
+function sendWelcomeEmail(account, tempPass) {
+ var subj = "Welcome to EtherPad on "+pro_utils.getFullProDomain()+"!";
+ var toAddr = account.email;
+ var fromAddr = pro_utils.getEmailFromAddr();
+
+ var body = renderTemplateAsString('pro/account/account-welcome-email.ejs', {
+ account: account,
+ adminAccount: getSessionProAccount(),
+ signinLink: pro_accounts.getTempSigninUrl(account, tempPass),
+ toEmail: toAddr,
+ siteName: pro_config.getConfig().siteName
+ });
+ try {
+ sendEmail(toAddr, fromAddr, subj, {}, body);
+ } catch (ex) {
+ var d = DIV();
+ d.push(P("Warning: unable to send welcome email."));
+ if (pne_utils.isPNE()) {
+ d.push(P("Perhaps you have not ",
+ A({href: '/ep/admin/pne-config'}, "Configured SMTP on this server", "?")));
+ }
+ getSession().accountManagerWarning = d;
+ }
+}
+
+// Managing a single account.
+function render_account_get(accountId) {
+ var account = pro_accounts.getAccountById(accountId);
+ if (!account) {
+ response.write("Account not found.");
+ return true;
+ }
+ pro_admin_control.renderAdminPage('manage-account', {
+ account: account,
+ errorDiv: _errorDiv,
+ warningDiv: _warningDiv
+ });
+}
+
+function render_account_post(accountId) {
+ if (request.params.cancel) {
+ response.redirect('/ep/admin/account-manager/');
+ }
+ var newFullName = request.params.newFullName;
+ var newEmail = request.params.newEmail;
+ var newIsAdmin = !!request.params.newIsAdmin;
+
+ _err(pro_accounts.validateEmail(newEmail));
+ _err(pro_accounts.validateFullName(newFullName));
+
+ if ((!newIsAdmin) && (accountId == getSessionProAccount().id)) {
+ _err("You cannot remove your own administrator privileges.");
+ }
+
+ var account = pro_accounts.getAccountById(accountId);
+ if (!account) {
+ response.write("Account not found.");
+ return true;
+ }
+
+ pro_accounts.setEmail(account, newEmail);
+ pro_accounts.setFullName(account, newFullName);
+ pro_accounts.setIsAdmin(account, newIsAdmin);
+
+ getSession().accountManageMessage = "Info updated.";
+ response.redirect('/ep/admin/account-manager/');
+}
+
+function render_delete_account_get(accountId) {
+ var account = pro_accounts.getAccountById(accountId);
+ if (!account) {
+ response.write("Account not found.");
+ return true;
+ }
+ pro_admin_control.renderAdminPage('delete-account', {
+ account: account,
+ errorDiv: _errorDiv
+ });
+}
+
+function render_delete_account_post(accountId) {
+ if (request.params.cancel) {
+ response.redirect("/ep/admin/account-manager/account/"+accountId);
+ }
+
+ if (accountId == getSessionProAccount().id) {
+ getSession().accountManagerError = "You cannot delete your own account.";
+ response.redirect("/ep/admin/account-manager/account/"+accountId);
+ }
+
+ var account = pro_accounts.getAccountById(accountId);
+ if (!account) {
+ response.write("Account not found.");
+ return true;
+ }
+
+ pro_accounts.setDeleted(account);
+ getSession().accountManagerMessage = "The account "+account.fullName+" <"+account.email+"> has been deleted.";
+ response.redirect("/ep/admin/account-manager/");
+}
+
+
+
diff --git a/etherpad/src/etherpad/control/pro/admin/license_manager_control.js b/etherpad/src/etherpad/control/pro/admin/license_manager_control.js
new file mode 100644
index 0000000..ca6d6a6
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/admin/license_manager_control.js
@@ -0,0 +1,128 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fileutils.writeRealFile");
+import("stringutils");
+
+import("etherpad.licensing");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+import("etherpad.pne.pne_utils");
+
+import("etherpad.control.pro.admin.pro_admin_control");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+// license manager
+//----------------------------------------------------------------
+
+function getPath() {
+ return '/ep/admin/pne-license-manager/';
+}
+
+function _getTemplateData(data) {
+ var licenseInfo = licensing.getLicense();
+ data.licenseInfo = licenseInfo;
+ data.isUnlicensed = !licenseInfo;
+ data.isEvaluation = licensing.isEvaluation();
+ data.isExpired = licensing.isExpired();
+ data.isTooOld = licensing.isVersionTooOld();
+ data.errorMessage = (getSession().errorMessage || null);
+ data.runningVersionString = pne_utils.getVersionString();
+ data.licenseVersionString = licensing.getVersionString();
+ return data;
+}
+
+function render_main_get() {
+ licensing.reloadLicense();
+ var licenseInfo = licensing.getLicense();
+ if (!licenseInfo || licensing.isExpired()) {
+ response.redirect(getPath()+'edit');
+ }
+
+ pro_admin_control.renderAdminPage('pne-license-manager',
+ _getTemplateData({edit: false}));
+}
+
+function render_edit_get() {
+ licensing.reloadLicense();
+
+ if (request.params.btn) { response.redirect(request.path); }
+
+ var licenseInfo = licensing.getLicense();
+ var oldData = getSession().oldLicenseData;
+ if (!oldData) {
+ oldData = {};
+ if (licenseInfo) {
+ oldData.orgName = licenseInfo.organizationName;
+ oldData.personName = licenseInfo.personName;
+ }
+ }
+
+ pro_admin_control.renderAdminPage('pne-license-manager',
+ _getTemplateData({edit: true, oldData: oldData}));
+
+ delete getSession().errorMessage;
+}
+
+function render_edit_post() {
+ pne_utils.enableTrackingAgain();
+
+ function _trim(s) {
+ if (!s) { return ''; }
+ return stringutils.trim(s);
+ }
+ function _clean(s) {
+ s = s.replace(/\W/g, '');
+ s = s.replace(/\+/g, '');
+ return s;
+ }
+
+ if (request.params.cancel) {
+ delete getSession().oldLicenseData;
+ response.redirect(getPath());
+ }
+
+ var personName = _trim(request.params.personName);
+ var orgName = _trim(request.params.orgName);
+ var licenseString = _clean(request.params.licenseString);
+
+ getSession().oldLicenseData = {
+ personName: personName, orgName: orgName, licenseString: licenseString};
+
+ var key = [personName,orgName,licenseString].join(":");
+ println("validating key [ "+key+" ]");
+
+ if (!licensing.isValidKey(key)) {
+ getSession().errorMessage = "Invalid License Key";
+ response.redirect(request.path);
+ }
+
+ // valid key. write to disk.
+ var writeSuccess = false;
+ try {
+ println("writing key file: ./data/license.key");
+ writeRealFile("./data/license.key", key);
+ writeSuccess = true;
+ } catch (ex) {
+ println("exception: "+ex);
+ getSession().errorMessage = "Failed to write key to disk. (Do you have permission to write ./data/license.key ?).";
+ }
+ response.redirect(getPath());
+}
+
+
diff --git a/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js b/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js
new file mode 100644
index 0000000..51d6ba3
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js
@@ -0,0 +1,280 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("stringutils");
+import("funhtml.*");
+import("dispatch.{Dispatcher,DirMatcher,forward}");
+
+import("etherpad.licensing");
+import("etherpad.control.admincontrol");
+import("etherpad.control.pro.admin.license_manager_control");
+import("etherpad.control.pro.admin.account_manager_control");
+import("etherpad.control.pro.admin.pro_config_control");
+import("etherpad.control.pro.admin.team_billing_control");
+
+import("etherpad.pad.padutils");
+
+import("etherpad.admin.shell");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_accounts");
+import("etherpad.utils.*");
+
+//----------------------------------------------------------------
+
+var _pathPrefix = '/ep/admin/';
+
+var _PRO = 1;
+var _PNE_ONLY = 2;
+var _ONDEMAND_ONLY = 3;
+
+function _getLeftnavItems() {
+ var nav = [
+ _PRO, [
+ [_PRO, null, "Admin"],
+ [_PNE_ONLY, "pne-dashboard", "Server Dashboard"],
+ [_PRO, "account-manager/", "Manage Accounts"],
+ [_PRO, "recover-padtext", "Recover Pad Text"],
+ [_PRO, null, "Configuration"],
+ [_PRO, [[_PNE_ONLY, "pne-config", "Private Server Configuration"],
+ [_PRO, "pro-config", "Application Configuration"]]],
+ ]
+ ];
+ return nav;
+}
+
+function renderAdminLeftNav() {
+ function _make(x) {
+ if ((x[0] == _PNE_ONLY) && !pne_utils.isPNE()) {
+ return null;
+ }
+ if ((x[0] == _ONDEMAND_ONLY) && pne_utils.isPNE()) {
+ return null;
+ }
+
+ if (x[1] instanceof Array) {
+ return _makelist(x[1]);
+ } else {
+ return _makeitem(x);
+ }
+ }
+ var selected;
+ function _makeitem(x) {
+ if (x[1]) {
+ var p = x[1];
+ if (x[1].charAt(0) != '/') {
+ p = _pathPrefix+p;
+ }
+ var li = LI(A({href: p}, x[2]));
+ if (stringutils.startsWith(request.path, p)) {
+ // select the longest prefix match.
+ if (! selected || p.length > selected.path.length) {
+ selected = {path: p, li: li};
+ }
+ }
+ return li;
+ } else {
+ return LI(DIV({className: 'leftnav-title'}, x[2]));
+ }
+ }
+ function _makelist(x) {
+ var ul = UL();
+ x.forEach(function(y) {
+ var t = _make(y);
+ if (t) { ul.push(t); }
+ });
+ return ul;
+ }
+ var d = DIV(_make(_getLeftnavItems()));
+ if (selected) {
+ selected.li.attribs.className = "selected";
+ }
+ // leftnav looks stupid when it's not very tall.
+ for (var i = 0; i < 10; i++) { d.push(BR()); }
+ return d;
+}
+
+function renderAdminPage(p, data) {
+ appjet.requestCache.proTopNavSelection = 'admin';
+ function getAdminContent() {
+ if (typeof(p) == 'function') {
+ return p();
+ } else {
+ return renderTemplateAsString('pro/admin/'+p+'.ejs', data);
+ }
+ }
+ renderFramed('pro/admin/admin-template.ejs', {
+ getAdminContent: getAdminContent,
+ renderAdminLeftNav: renderAdminLeftNav,
+ validLicense: pne_utils.isServerLicensed(),
+ });
+}
+
+//----------------------------------------------------------------
+
+function onRequest() {
+ var disp = new Dispatcher();
+ disp.addLocations([
+ [DirMatcher(license_manager_control.getPath()), forward(license_manager_control)],
+ [DirMatcher('/ep/admin/account-manager/'), forward(account_manager_control)],
+ [DirMatcher('/ep/admin/pro-config/'), forward(pro_config_control)],
+ [DirMatcher('/ep/admin/billing/'), forward(team_billing_control)],
+ ]);
+
+ if (disp.dispatch()) {
+ return true;
+ }
+
+ // request will be handled by this module.
+ pro_accounts.requireAdminAccount();
+}
+
+function render_main() {
+// renderAdminPage('admin');
+ response.redirect('/ep/admin/account-manager/')
+}
+
+function render_pne_dashboard() {
+ renderAdminPage('pne-dashboard', {
+ renderUptime: admincontrol.renderServerUptime,
+ renderResponseCodes: admincontrol.renderResponseCodes,
+ renderPadConnections: admincontrol.renderPadConnections,
+ renderTransportStats: admincontrol.renderCometStats,
+ todayActiveUsers: licensing.getActiveUserCount(),
+ userQuota: licensing.getActiveUserQuota()
+ });
+}
+
+var _documentedServerOptions = [
+ 'listen',
+ 'listenSecure',
+ 'transportUseWildcardSubdomains',
+ 'sslKeyStore',
+ 'sslKeyPassword',
+ 'etherpad.soffice',
+ 'etherpad.adminPass',
+ 'etherpad.SQL_JDBC_DRIVER',
+ 'etherpad.SQL_JDBC_URL',
+ 'etherpad.SQL_USERNAME',
+ 'etherpad.SQL_PASSWORD',
+ 'smtpServer',
+ 'smtpUser',
+ 'smtpPass',
+ 'configFile',
+ 'etherpad.licenseKey',
+ 'verbose'
+];
+
+function render_pne_config_get() {
+ renderAdminPage('pne-config', {
+ propKeys: _documentedServerOptions,
+ appjetConfig: appjet.config
+ });
+}
+
+function render_pne_advanced_get() {
+ response.redirect("/ep/admin/shell");
+}
+
+function render_shell_get() {
+ if (!(pne_utils.isPNE() || sessions.isAnEtherpadAdmin())) {
+ return false;
+ }
+ appjet.requestCache.proTopNavSelection = 'admin';
+ renderAdminPage('pne-shell', {
+ oldCmd: getSession().pneAdminShellCmd,
+ result: getSession().pneAdminShellResult,
+ elapsedMs: getSession().pneAdminShellElapsed
+ });
+ delete getSession().pneAdminShellResult;
+ delete getSession().pneAdminShellElapsed;
+}
+
+function render_shell_post() {
+ if (!(pne_utils.isPNE() || sessions.isAnEtherpadAdmin())) {
+ return false;
+ }
+ var cmd = request.params.cmd;
+ var start = +(new Date);
+ getSession().pneAdminShellCmd = cmd;
+ getSession().pneAdminShellResult = shell.getResult(cmd);
+ getSession().pneAdminShellElapsed = +(new Date) - start;
+ response.redirect(request.path);
+}
+
+function render_recover_padtext_get() {
+ function getNumRevisions(localPadId) {
+ return padutils.accessPadLocal(localPadId, function(pad) {
+ if (!pad.exists()) { return null; }
+ return 1+pad.getHeadRevisionNumber();
+ });
+ }
+ function getPadText(localPadId, revNum) {
+ return padutils.accessPadLocal(localPadId, function(pad) {
+ if (!pad.exists()) { return null; }
+ return pad.getRevisionText(revNum);
+ });
+ }
+
+ var localPadId = request.params.localPadId;
+ var revNum = request.params.revNum;
+
+ var d = DIV({style: "font-size: .8em;"});
+
+ d.push(FORM({action: request.path, method: "get"},
+ P({style: "margin-top: 0;"}, LABEL("Pad ID: "),
+ INPUT({type: "text", name: "localPadId", value: localPadId || ""}),
+ INPUT({type: "submit", value: "Submit"}))));
+
+ var showPadHelp = false;
+ var revisions = null;
+
+ if (!localPadId) {
+ showPadHelp = true;
+ } else {
+ revisions = getNumRevisions(localPadId);
+ if (!revisions) {
+ d.push(P("Pad not found: "+localPadId));
+ } else {
+ d.push(P(B(localPadId), " has ", revisions, " revisions."));
+ d.push(P("Enter a revision number (0-"+revisions+") to recover the pad text for that revision:"));
+ d.push(FORM({action: request.path, method: "get"},
+ P(LABEL("Revision number:"),
+ INPUT({type: "hidden", name: "localPadId", value: localPadId}),
+ INPUT({type: "text", name: "revNum", value: revNum || (revisions - 1)}),
+ INPUT({type: "submit", value: "Submit"}))));
+ }
+ }
+
+ if (showPadHelp) {
+ d.push(P({style: "font-size: 1em; color: #555;"},
+ 'The pad ID is the same as the URL to the pad, without the leading "/".',
+ ' For example, if the pad lives at http://pad.spline.inf.fu-berlin.de/foobar,',
+ ' then the pad ID is "foobar" (without the quotes).'))
+ }
+
+ if (revisions && revNum && (revNum < revisions)) {
+ var padText = getPadText(localPadId, revNum);
+ d.push(P(B("Pad text for ["+localPadId+"] revision #"+revNum)));
+ d.push(DIV({style: "font-family: monospace; border: 1px solid #ccc; background: #ffe; padding: 1em;"}, padText));
+ }
+
+ renderAdminPage(function() { return d; });
+}
+
+
diff --git a/etherpad/src/etherpad/control/pro/admin/pro_config_control.js b/etherpad/src/etherpad/control/pro/admin/pro_config_control.js
new file mode 100644
index 0000000..b03da45
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/admin/pro_config_control.js
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+
+import("etherpad.sessions.getSession");
+import("etherpad.control.pro.admin.pro_admin_control");
+import("etherpad.pro.pro_config");
+
+function _renderTopDiv(mid, htmlId) {
+ var m = getSession()[mid];
+ if (m) {
+ delete getSession()[mid];
+ return DIV({id: htmlId}, m);
+ } else {
+ return '';
+ }
+}
+
+function _messageDiv() {
+ return _renderTopDiv('proConfigMessage', 'pro-config-message');
+}
+
+function render_main_get() {
+ pro_config.reloadConfig();
+ var config = pro_config.getConfig();
+ pro_admin_control.renderAdminPage('pro-config', {
+ config: config,
+ messageDiv: _messageDiv
+ });
+}
+
+function render_main_post() {
+ pro_config.setConfigVal('siteName', request.params.siteName);
+ pro_config.setConfigVal('alwaysHttps', !!request.params.alwaysHttps);
+ pro_config.setConfigVal('defaultPadText', request.params.defaultPadText);
+ getSession().proConfigMessage = "New settings applied.";
+ response.redirect(request.path);
+}
+
+
diff --git a/etherpad/src/etherpad/control/pro/admin/team_billing_control.js b/etherpad/src/etherpad/control/pro/admin/team_billing_control.js
new file mode 100644
index 0000000..5be6a0e
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/admin/team_billing_control.js
@@ -0,0 +1,447 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("dateutils");
+import("email.sendEmail");
+import("fastJSON");
+import("funhtml.*");
+import("jsutils.*");
+import("sqlbase.sqlcommon.inTransaction");
+import("stringutils.*");
+
+import("etherpad.billing.billing");
+import("etherpad.billing.fields");
+import("etherpad.billing.team_billing");
+import("etherpad.control.pro.admin.pro_admin_control");
+import("etherpad.globals");
+import("etherpad.helpers");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_utils");
+import("etherpad.sessions");
+import("etherpad.store.checkout");
+import("etherpad.utils.*");
+
+import("static.js.billing_shared.{billing=>billingJS}");
+
+var billingButtonName = "Confirm"
+
+function _cart() {
+ var s = sessions.getSession();
+ if (! s.proBillingCart) {
+ s.proBillingCart = {};
+ }
+ return s.proBillingCart;
+}
+
+function _billingForm() {
+ return renderTemplateAsString('store/eepnet-checkout/billing-info.ejs', {
+ cart: _cart(),
+ billingButtonName: billingButtonName,
+ billingFinalPhrase: "",
+ helpers: helpers,
+ errorIfInvalid: _errorIfInvalid,
+ billing: billingJS,
+ obfuscateCC: checkout.obfuscateCC,
+ dollars: checkout.dollars,
+ countryList: fields.countryList,
+ usaStateList: fields.usaStateList,
+ getFullSuperdomainHost: pro_utils.getFullSuperdomainHost,
+ showCouponCode: true,
+ });
+}
+
+function _plural(num) {
+ return (num == 1 ? "" : "s");
+}
+
+function _billingSummary(domainId, subscription) {
+ var paymentInfo = team_billing.getRecurringBillingInfo(domainId);
+ if (! paymentInfo) {
+ return;
+ }
+ var latestInvoice = team_billing.getLatestPaidInvoice(subscription.id);
+ var usersSoFar = team_billing.getMaxUsers(domainId);
+ var costSoFar = team_billing.calculateSubscriptionCost(usersSoFar, subscription.coupon);
+
+ var lastPaymentString =
+ (latestInvoice ?
+ "US $"+checkout.dollars(billing.centsToDollars(latestInvoice.amt))+
+ " ("+latestInvoice.users+" account"+_plural(latestInvoice.users)+")"+
+ ", on "+checkout.formatDate(latestInvoice.time) :
+ "None");
+
+ var coupon = false;
+ if (subscription.coupon) {
+ println("has a coupon: "+subscription.coupon);
+ var cval = team_billing.getCouponValue(subscription.coupon);
+ coupon = [];
+ if (cval.freeUsers) {
+ coupon.push(cval.freeUsers+" free user"+(cval.freeUsers == 1 ? "" : "s"));
+ }
+ if (cval.pctDiscount) {
+ coupon.push(cval.pctDiscount+"% savings");
+ }
+ coupon = coupon.join(", ");
+ }
+
+ return {
+ fullName: paymentInfo.fullname,
+ paymentSummary:
+ paymentInfo.paymentsummary +
+ (paymentInfo.expiration ?
+ ", expires "+checkout.formatExpiration(paymentInfo.expiration) :
+ ""),
+ lastPayment: lastPaymentString,
+ nextPayment: checkout.formatDate(subscription.paidThrough),
+ maxUsers: usersSoFar,
+ estimatedPayment: "US $"+checkout.dollars(costSoFar),
+ coupon: coupon
+ }
+}
+
+function _statusMessage() {
+ if (_cart().statusMessage) {
+ return toHTML(P({style: "color: green;"}, _cart().statusMessage));
+ } else {
+ return '';
+ }
+}
+
+function renderMainPage(doEdit) {
+ var cart = _cart();
+ var domainId = domains.getRequestDomainId();
+ var subscription = team_billing.getSubscriptionForCustomer(domainId);
+ var pendingInvoice = team_billing.getLatestPendingInvoice(domainId)
+ var usersSoFar = team_billing.getMaxUsers(domainId);
+ var costSoFar = team_billing.calculateSubscriptionCost(usersSoFar, subscription && subscription.coupon);
+
+ checkout.guessBillingNames(cart, pro_accounts.getSessionProAccount().fullName);
+ if (! cart.billingReferralCode) {
+ if (subscription && subscription.coupon) {
+ cart.billingReferralCode = subscription.coupon;
+ }
+ }
+
+ var summary = _billingSummary(domainId, subscription);
+ if (! summary) {
+ doEdit = true;
+ }
+
+ pro_admin_control.renderAdminPage('manage-billing', {
+ billingForm: _billingForm,
+ doEdit: doEdit,
+ paymentInfo: summary,
+ getFullSuperdomainHost: pro_utils.getFullSuperdomainHost,
+ firstCharge: checkout.formatDate(subscription ? subscription.paidThrough : dateutils.nextMonth(new Date)),
+ billingButtonName: billingButtonName,
+ errorDiv: _errorDiv,
+ showBackButton: (summary != undefined),
+ statusMessage: _statusMessage,
+ isBehind: (subscription ? subscription.paidThrough < Date.now() - 86400*1000 : false),
+ amountDue: "US $"+checkout.dollars(billing.centsToDollars(pendingInvoice ? pendingInvoice.amt : costSoFar*100)),
+ cart: _cart()
+ });
+
+ delete _cart().errorId;
+ delete _cart().errorMsg;
+ delete _cart().statusMessage;
+}
+
+function render_main() {
+ renderMainPage(false);
+}
+
+function render_edit() {
+ renderMainPage(true);
+}
+
+function _errorDiv() {
+ var m = _cart().errorMsg;
+ if (m) {
+ return DIV({className: 'errormsg', id: 'errormsg'}, m);
+ } else {
+ return '';
+ }
+}
+
+function _validationError(id, errorMessage) {
+ var cart = _cart();
+ cart.errorMsg = errorMessage;
+ cart.errorId = {};
+ if (id instanceof Array) {
+ id.forEach(function(k) {
+ cart.errorId[k] = true;
+ });
+ } else {
+ cart.errorId[id] = true;
+ }
+ response.redirect('/ep/admin/billing/edit');
+}
+
+function _errorIfInvalid(id) {
+ var cart = _cart();
+ if (cart.errorId && cart.errorId[id]) {
+ return 'error';
+ } else {
+ return '';
+ }
+}
+
+function paypalNotifyUrl() {
+ return request.scheme+"://"+pro_utils.getFullSuperdomainHost()+"/ep/store/paypalnotify";
+}
+
+function _paymentSummary(payInfo) {
+ return payInfo.cardType + " ending in " + payInfo.cardNumber.substr(-4);
+}
+
+function _expiration(payInfo) {
+ return payInfo.cardExpiration;
+}
+
+function _attemptAuthorization(success_f) {
+ var cart = _cart();
+ var domain = domains.getRequestDomainRecord();
+ var domainId = domain.id;
+ var domainName = domain.subDomain;
+ var payInfo = checkout.generatePayInfo(cart);
+ var proAccount = pro_accounts.getSessionProAccount();
+ var fullName = cart.billingFirstName+" "+cart.billingLastName;
+ var email = proAccount.email;
+
+ // PCI rules require that we not store the CVV longer than necessary to complete the transaction
+ var savedCvv = payInfo.cardCvv;
+ delete payInfo.cardCvv;
+ checkout.writeToEncryptedLog(fastJSON.stringify({date: String(new Date()), domain: domain, payInfo: payInfo}));
+ payInfo.cardCvv = savedCvv;
+
+ var result = billing.authorizePurchase(payInfo, paypalNotifyUrl());
+ if (result.status == 'success') {
+ billing.log({type: 'new-subscription',
+ name: fullName,
+ domainId: domainId,
+ domainName: domainName});
+ success_f(result);
+ } else if (result.status == 'pending') {
+ _validationError('', "Your authorization is pending. When it clears, your account will be activated. "+
+ "You may choose to pay by different means now, or wait until your authorization clears.");
+ } else if (result.status == 'failure') {
+ var paypalResult = result.debug;
+ billing.log({'type': 'FATAL', value: "Direct purchase failed on paypal.", cart: cart, paypal: paypalResult});
+ checkout.validateErrorFields(_validationError, "There seems to be an error in your billing information."+
+ " Please verify and correct your ",
+ result.errorField.userErrors);
+ checkout.validateErrorFields(_validationError, "The bank declined your billing information. Please try a different ",
+ result.errorField.permanentErrors);
+ _validationError('', "A temporary error has prevented processing of your payment. Please try again later.");
+ } else {
+ billing.log({'type': 'FATAL', value: "Unknown error: "+result.status+" - debug: "+result.debug});
+ sendEmail('support@pad.spline.inf.fu-berlin.de', 'urgent@pad.spline.inf.fu-berlin.de', 'UNKNOWN ERROR WARNING!', {},
+ "Hey,\n\nThis is a billing system error. Some unknown error occurred. "+
+ "This shouldn't ever happen. Probably good to let J.D. know. <grin>\n\n"+
+ fastJSON.stringify(cart));
+ _validationError('', "An unknown error occurred. We're looking into it!")
+ }
+}
+
+function _processNewSubscription() {
+ _attemptAuthorization(function(result) {
+ var domain = domains.getRequestDomainRecord();
+ var domainId = domain.id;
+ var domainName = domain.subDomain;
+
+ var cart = _cart();
+ var payInfo = checkout.generatePayInfo(cart);
+ var proAccount = pro_accounts.getSessionProAccount();
+ var fullName = cart.billingFirstName+" "+cart.billingLastName;
+ var email = proAccount.email;
+
+ inTransaction(function() {
+
+ var subscriptionId = team_billing.createSubscription(domainId, cart.billingReferralCode);
+
+ team_billing.setRecurringBillingInfo(
+ domainId,
+ fullName,
+ email,
+ _paymentSummary(payInfo),
+ _expiration(payInfo),
+ result.purchaseInfo.paypalId);
+ });
+
+ if (globals.isProduction()) {
+ sendEmail('sales@pad.spline.inf.fu-berlin.de', 'sales@pad.spline.inf.fu-berlin.de', "EtherPad: New paid pro account for "+fullName, {},
+ "This is an automatic notification.\n\n"+fullName+" ("+email+") successfully set up "+
+ "a billing profile for domain: "+domainName+".");
+ }
+ });
+}
+
+function _updateExistingSubscription(subscription) {
+ var cart = _cart();
+
+ _attemptAuthorization(function(result) {
+ inTransaction(function() {
+ var cart = _cart();
+ var domain = domains.getRequestDomainId();
+ var payInfo = checkout.generatePayInfo(cart);
+ var proAccount = pro_accounts.getSessionProAccount();
+ var fullName = cart.billingFirstName+" "+cart.billingLastName;
+ var email = proAccount.email;
+
+ var subscriptionId = subscription.id;
+
+ team_billing.setRecurringBillingInfo(
+ domain,
+ fullName,
+ email,
+ _paymentSummary(payInfo),
+ _expiration(payInfo),
+ result.purchaseInfo.paypalId);
+ });
+ });
+
+ if (subscription.paidThrough < new Date) {
+ // if they're behind, do the purchase!
+ if (team_billing.processSubscription(subscription)) {
+ cart.statusMessage = "Your payment was successful, and your account is now up to date! You will receive a receipt by email."
+ } else {
+ cart.statusMessage = "Your payment failed; you will receive further instructions by email.";
+ }
+ }
+}
+
+function _processBillingInfo() {
+ var cart = _cart();
+ var domain = domains.getRequestDomainId();
+
+ var subscription = team_billing.getSubscriptionForCustomer(domain);
+ if (! subscription) {
+ _processNewSubscription();
+ response.redirect('/ep/admin/billing/');
+ } else {
+ team_billing.updateSubscriptionCouponCode(subscription.id, cart.billingReferralCode);
+ if (cart.billingCCNumber.length > 0) {
+ _updateExistingSubscription(subscription);
+ }
+ response.redirect('/ep/admin/billing')
+ }
+}
+
+function _processPaypalPurchase() {
+ var domain = domains.getRequestDomainId();
+ billing.log({type: "paypal-attempt",
+ domain: domain,
+ message: "Someone tried to use paypal to pay for on-demand."+
+ " They got an error message. If this happens a lot, we should implement paypal."})
+ java.lang.Thread.sleep(5000);
+ _validationError('billingPurchaseType', "There was an error contacting PayPal. Please try another payment type.")
+}
+
+function _processInvoicePurchase() {
+ var output = [
+ "Name: "+cart.billingFirstName+" "+cart.billingLastName,
+ "\nAddress: ",
+ cart.billingAddressLine1+(cart.billingAddressLine2.length > 0 ? "\n"+cart.billingAddressLine2 : ""),
+ cart.billingCity + ", " + (cart.billingState.length > 0 ? cart.billingState : cart.billingProvince),
+ cart.billingZipCode.length > 0 ? cart.billingZipCode : cart.billingPostalCode,
+ cart.billingCountry,
+ "\nEmail: ",
+ pro_accounts.getSessionProAccount().email
+ ].join("\n");
+ var recipient = (globals.isProduction() ? 'sales@pad.spline.inf.fu-berlin.de' : 'jd@appjet.com');
+ sendEmail(
+ recipient,
+ 'sales@pad.spline.inf.fu-berlin.de',
+ 'Invoice payment request - '+pro_utils.getProRequestSubdomain(),
+ {},
+ "Hi there,\n\nA pro user tried to pay by invoice. Their information follows."+
+ "\n\nThanks!\n\n"+output);
+ _validationError('', "Your information has been sent to our sales department; a salesperson will contact you shortly regarding your invoice request.")
+}
+
+function render_apply() {
+ var cart = _cart();
+ eachProperty(request.params, function(k, v) {
+ if (startsWith(k, "billing")) {
+ if (k == "billingCCNumber" && v.charAt(0) == 'X') { return; }
+ cart[k] = toHTML(v);
+ }
+ });
+
+ if (! request.params.backbutton) {
+ var allPaymentFields = ["billingCCNumber", "billingExpirationMonth", "billingExpirationYear", "billingCSC", "billingAddressLine1", "billingAddressLine2", "billingCity", "billingState", "billingZipCode", "billingProvince", "billingPostalCode"];
+ var allBlank = true;
+ allPaymentFields.forEach(function(field) { if (cart[field].length > 0) { allBlank = false; }});
+ if (! allBlank) {
+ checkout.validateBillingCart(_validationError, cart);
+ }
+ } else {
+ response.redirect("/ep/admin/billing/");
+ }
+
+ var couponCode = cart.billingReferralCode;
+
+ if (couponCode.length != 0 && (couponCode.length != 8 || ! team_billing.getCouponValue(couponCode))) {
+ _validationError('billingReferralCode', 'Invalid referral code entered. Please verify your code and try again.');
+ }
+
+ if (cart.billingPurchaseType == 'paypal') {
+ _processPaypalPurchase();
+ } else if (cart.billingPurchaseType == 'invoice') {
+ _processInvoicePurchase();
+ }
+
+ _processBillingInfo();
+}
+
+function handlePaypalNotify() {
+ // XXX: handle delayed paypal authorization
+}
+
+function render_invoices() {
+ if (request.params.id) {
+ var purchaseId = team_billing.getSubscriptionForCustomer(domains.getRequestDomainId()).id;
+ var invoice = billing.getInvoice(request.params.id);
+ if (invoice.purchase != purchaseId) {
+ response.redirect(request.path);
+ }
+
+ var transaction;
+ var adjustments = billing.getAdjustments(invoice.id);
+ if (adjustments.length == 1) {
+ transaction = billing.getTransaction(adjustments[0].transaction);
+ }
+
+ pro_admin_control.renderAdminPage('single-invoice', {
+ formatDate: checkout.formatDate,
+ dollars: checkout.dollars,
+ centsToDollars: billing.centsToDollars,
+ invoice: invoice,
+ transaction: transaction
+ });
+ } else {
+ var invoices = team_billing.getAllInvoices(domains.getRequestDomainId());
+
+ pro_admin_control.renderAdminPage('billing-invoices', {
+ invoices: invoices,
+ formatDate: checkout.formatDate,
+ dollars: checkout.dollars,
+ centsToDollars: billing.centsToDollars
+ });
+ }
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/control/pro/pro_main_control.js b/etherpad/src/etherpad/control/pro/pro_main_control.js
new file mode 100644
index 0000000..b4e3bc4
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/pro_main_control.js
@@ -0,0 +1,150 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("stringutils");
+import("dispatch.{Dispatcher,DirMatcher,forward}");
+import("funhtml.*");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.helpers");
+import("etherpad.utils.*");
+import("etherpad.sessions.getSession");
+import("etherpad.licensing");
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_pad_db");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.pro_padlist");
+
+import("etherpad.control.pro.account_control");
+import("etherpad.control.pro.pro_padlist_control");
+import("etherpad.control.pro.admin.pro_admin_control");
+import("etherpad.control.pro.admin.account_manager_control");
+
+import("etherpad.pad.activepads");
+import("etherpad.pad.model");
+
+
+function onRequest() {
+ var disp = new Dispatcher();
+ disp.addLocations([
+ [DirMatcher('/ep/account/'), forward(account_control)],
+ [DirMatcher('/ep/admin/'), forward(pro_admin_control)],
+ [DirMatcher('/ep/padlist/'), forward(pro_padlist_control)],
+ ]);
+ return disp.dispatch();
+}
+
+function render_main() {
+ if (request.path == '/ep/') {
+ response.redirect('/');
+ }
+
+ // recent pad list
+ var livePads = pro_pad_db.listLiveDomainPads();
+ var recentPads = pro_pad_db.listAllDomainPads();
+
+ var renderLivePads = function() {
+ return pro_padlist.renderPadList(livePads, ['title', 'connectedUsers'], 10);
+ }
+
+ var renderRecentPads = function() {
+ return pro_padlist.renderPadList(recentPads, ['title'], 10);
+ };
+
+ var r = domains.getRequestDomainRecord();
+
+ renderFramed('pro/pro_home.ejs', {
+ isEvaluation: licensing.isEvaluation(),
+ account: getSessionProAccount(),
+ isPNE: pne_utils.isPNE(),
+ pneVersion: pne_utils.getVersionString(),
+ livePads: livePads,
+ recentPads: recentPads,
+ renderRecentPads: renderRecentPads,
+ renderLivePads: renderLivePads,
+ orgName: r.orgName
+ });
+ return true;
+}
+
+function render_finish_activation_get() {
+ if (!isActivationAllowed()) {
+ response.redirect('/');
+ }
+
+ var accountList = pro_accounts.listAllDomainAccounts();
+ if (accountList.length > 1) {
+ response.redirect('/');
+ }
+ if (accountList.length == 0) {
+ throw Error("accountList.length should never be 0.");
+ }
+
+ var acct = accountList[0];
+ var tempPass = stringutils.randomString(10);
+ pro_accounts.setTempPassword(acct, tempPass);
+ account_manager_control.sendWelcomeEmail(acct, tempPass);
+
+ var domainId = domains.getRequestDomainId();
+
+ syncedWithCache('pro-activations', function(c) {
+ delete c[domainId];
+ });
+
+ renderNoticeString(
+ DIV({style: "font-size: 16pt; border: 1px solid green; background: #eeffee; margin: 2em 4em; padding: 1em;"},
+ P("Success! You will receive an email shortly with instructions."),
+ DIV({style: "display: none;", id: "reference"}, acct.id, ":", tempPass)));
+}
+
+function isActivationAllowed() {
+ if (request.path != '/ep/finish-activation') {
+ return false;
+ }
+ var allowed = false;
+ var domainId = domains.getRequestDomainId();
+ return syncedWithCache('pro-activations', function(c) {
+ if (c[domainId]) {
+ return true;
+ }
+ return false;
+ });
+}
+
+function render_payment_required_get() {
+ // Users get to this page when there is a problem with billing:
+ // possibilities:
+ // * they try to create a new account but they have not entered
+ // payment information
+ //
+ // * their credit card lapses and any pro request fails.
+ //
+ // * others?
+
+ var message = getSession().billingProblem || "A payment is required to proceed.";
+ var adminList = pro_accounts.listAllDomainAdmins();
+
+ renderFramed("pro/pro-payment-required.ejs", {
+ message: message,
+ isAdmin: pro_accounts.isAdminSignedIn(),
+ adminList: adminList
+ });
+}
+
+
+
diff --git a/etherpad/src/etherpad/control/pro/pro_padlist_control.js b/etherpad/src/etherpad/control/pro/pro_padlist_control.js
new file mode 100644
index 0000000..9a90c67
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro/pro_padlist_control.js
@@ -0,0 +1,200 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("jsutils.*");
+import("stringutils");
+
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+import("etherpad.helpers");
+import("etherpad.pad.exporthtml");
+import("etherpad.pad.padutils");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pro.pro_pad_db");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_padlist");
+
+jimport("java.lang.System.out.println");
+
+function onRequest(name) {
+ if (name == "all_pads.zip") {
+ render_all_pads_zip_get();
+ return true;
+ } else {
+ return false;
+ }
+}
+
+function _getBaseUrl() { return "/ep/padlist/"; }
+
+function _renderPadNav() {
+ var d = DIV({id: "padlist-nav"});
+ var ul = UL();
+ var items = [
+ ['allpads', 'all-pads', "All Pads"],
+ ['mypads', 'my-pads', "My Pads"],
+ ['archivedpads', 'archived-pads', "Archived Pads"]
+ ];
+ for (var i = 0; i < items.length; i++) {
+ var item = items[i];
+ var cn = "";
+ if (request.path.split("/").slice(-1)[0] == item[1]) {
+ cn = "selected";
+ }
+ ul.push(LI(A({id: "nav-"+item[1], href: _getBaseUrl()+item[1], className: cn}, item[2])));
+ }
+ ul.push(html(helpers.clearFloats()));
+ d.push(ul);
+ d.push(FORM({id: "newpadform", method: "get", action: "/ep/pad/newpad"},
+ INPUT({type: "submit", value: "New Pad"})));
+ d.push(html(helpers.clearFloats()));
+ return d;
+}
+
+function _renderPage(name, data) {
+ getSession().latestPadlistView = request.path + "?" + request.query;
+ var r = domains.getRequestDomainRecord();
+ appjet.requestCache.proTopNavSelection = 'padlist';
+ data.renderPadNav = _renderPadNav;
+ data.orgName = r.orgName;
+ data.renderNotice = function() {
+ var m = getSession().padlistMessage;
+ if (m) {
+ delete getSession().padlistMessage;
+ return DIV({className: "padlist-notice"}, m);
+ } else {
+ return "";
+ }
+ };
+
+ renderFramed("pro/padlist/"+name+".ejs", data);
+}
+
+function _renderListPage(padList, showingDesc, columns) {
+ _renderPage("pro-padlist", {
+ padList: padList,
+ renderPadList: function() {
+ return pro_padlist.renderPadList(padList, columns);
+ },
+ renderShowingDesc: function(count) {
+ return DIV({id: "showing-desc"},
+ "Showing "+showingDesc+" ("+count+").");
+ },
+ isAdmin: pro_accounts.isAdminSignedIn()
+ });
+}
+
+function render_main() {
+ if (!getSession().latestPadlistView) {
+ getSession().latestPadlistView = "/ep/padlist/all-pads";
+ }
+ response.redirect(getSession().latestPadlistView);
+}
+
+function render_all_pads_get() {
+ _renderListPage(
+ pro_pad_db.listAllDomainPads(),
+ "all pads",
+ ['secure', 'title', 'lastEditedDate', 'editors', 'actions']);
+}
+
+function render_all_pads_zip_get() {
+ if (! pro_accounts.isAdminSignedIn()) {
+ response.redirect(_getBaseUrl()+"all-pads");
+ }
+ var bytes = new java.io.ByteArrayOutputStream();
+ var zos = new java.util.zip.ZipOutputStream(bytes);
+
+ var pads = pro_pad_db.listAllDomainPads();
+ pads.forEach(function(pad) {
+ var padHtml;
+ var title;
+ padutils.accessPadLocal(pad.localPadId, function(p) {
+ title = padutils.getProDisplayTitle(pad.localPadId, pad.title);
+ padHtml = exporthtml.getPadHTML(p);
+ }, "r");
+
+ title = title.replace(/[^\w\s]/g, "-") + ".html";
+ zos.putNextEntry(new java.util.zip.ZipEntry(title));
+ var padBytes = (new java.lang.String(renderTemplateAsString('pad/exporthtml.ejs', {
+ content: padHtml,
+ pre: false
+ }))).getBytes("UTF-8");
+
+ zos.write(padBytes, 0, padBytes.length);
+ zos.closeEntry();
+ });
+ zos.close();
+ response.setContentType("application/zip");
+ response.writeBytes(bytes.toByteArray());
+}
+
+function render_my_pads_get() {
+ _renderListPage(
+ pro_pad_db.listMyPads(),
+ "pads created by me",
+ ['secure', 'title', 'lastEditedDate', 'editors', 'actions']);
+}
+
+function render_archived_pads_get() {
+ helpers.addClientVars({
+ showingArchivedPads: true
+ });
+ _renderListPage(
+ pro_pad_db.listArchivedPads(),
+ "archived pads",
+ ['secure', 'title', 'lastEditedDate', 'actions']);
+}
+
+function render_edited_by_get() {
+ var editorId = request.params.editorId;
+ var editorName = pro_accounts.getFullNameById(editorId);
+ _renderListPage(
+ pro_pad_db.listPadsByEditor(editorId),
+ "pads edited by "+editorName,
+ ['secure', 'title', 'lastEditedDate', 'editors', 'actions']);
+}
+
+function render_delete_post() {
+ var localPadId = request.params.padIdToDelete;
+
+ pro_padmeta.accessProPadLocal(localPadId, function(propad) {
+ propad.markDeleted();
+ getSession().padlistMessage = 'Pad "'+propad.getDisplayTitle()+'" has been deleted.';
+ });
+
+ response.redirect(request.params.returnPath);
+}
+
+function render_toggle_archive_post() {
+ var localPadId = request.params.padIdToToggleArchive;
+
+ pro_padmeta.accessProPadLocal(localPadId, function(propad) {
+ if (propad.isArchived()) {
+ propad.unmarkArchived();
+ getSession().padlistMessage = 'Pad "'+propad.getDisplayTitle()+'" has been un-archived.';
+ } else {
+ propad.markArchived();
+ getSession().padlistMessage = 'Pad "'+propad.getDisplayTitle()+'" has been archived. You can view archived pads by clicking on the "Archived" tab at the top of the pad list.';
+ }
+ });
+
+ response.redirect(request.params.returnPath);
+}
+
+
diff --git a/etherpad/src/etherpad/control/pro_beta_control.js b/etherpad/src/etherpad/control/pro_beta_control.js
new file mode 100644
index 0000000..ec99b43
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro_beta_control.js
@@ -0,0 +1,136 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*", "stringutils.*");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("stringutils");
+import("email.sendEmail");
+
+import("etherpad.utils.*");
+import("etherpad.log");
+import("etherpad.sessions.getSession");
+
+jimport("java.lang.System.out.println");
+
+function render_main_get() {
+ if (isValveOpen()) {
+ response.redirect("/ep/pro-signup/");
+ }
+ renderFramed("beta/signup.ejs", {
+ errorMsg: getSession().betaSignupError
+ });
+ delete getSession().betaSignupError;
+}
+
+function render_signup_post() {
+ // record in sql: [id, email, activated=false, activationCode]
+ // log to disk
+
+ var email = request.params.email;
+ if (!isValidEmail(email)) {
+ getSession().betaSignupError = "Invalid email address.";
+ response.redirect('/ep/beta-account/');
+ }
+
+ // does email already exist?
+ if (sqlobj.selectSingle('pro_beta_signups', {email: email})) {
+ getSession().betaSignupError = "Email already signed up.";
+ response.redirect('/ep/beta-account/');
+ }
+
+ sqlobj.insert('pro_beta_signups', {
+ email: email,
+ isActivated: false,
+ signupDate: new Date()
+ });
+
+ response.redirect('/ep/beta-account/signup-ok');
+}
+
+function render_signup_ok() {
+ renderNoticeString(
+ DIV({style: "font-size: 16pt; border: 1px solid green; background: #eeffee; margin: 2em 4em; padding: 1em;"},
+ P("Great! We'll be in touch."),
+ P("In the meantime, you can ", A({href: '/ep/pad/newpad', style: 'text-decoration: underline;'},
+ "create a public pad"), " right now.")));
+}
+
+// return string if not valid, falsy otherwise.
+function isValidCode(code) {
+ if (isValveOpen()) {
+ return undefined;
+ }
+
+ function wr(m) {
+ return DIV(P(m), P("You can sign up for the beta ",
+ A({href: "/ep/beta-account/"}, "here")));
+ }
+
+ if (!code) {
+ return wr("Invalid activation code.");
+ }
+ var record = sqlobj.selectSingle('pro_beta_signups', { activationCode: code });
+ if (!record) {
+ return wr("Invalid activation code.");
+ }
+ if (record.isActivated) {
+ return wr("That activation code has already been used.");
+ }
+ return undefined;
+}
+
+function isValveOpen() {
+ if (appjet.cache.proBetaValveIsOpen === undefined) {
+ appjet.cache.proBetaValveIsOpen = true;
+ }
+ return appjet.cache.proBetaValveIsOpen;
+}
+
+function toggleValve() {
+ appjet.cache.proBetaValveIsOpen = !appjet.cache.proBetaValveIsOpen;
+}
+
+function sendInvite(recordId) {
+ var record = sqlobj.selectSingle('pro_beta_signups', {id: recordId});
+ if (record.activationCode) {
+ getSession().betaAdminMessage = "Already active";
+ return;
+ }
+
+ // create activation code
+ var code = stringutils.randomString(10);
+ sqlcommon.inTransaction(function() {
+ sqlobj.update('pro_beta_signups', {id: recordId}, {activationCode: code});
+ var body = renderTemplateAsString('email/pro_beta_invite.ejs', {
+ toAddr: record.email,
+ signupAgo: timeAgo(record.signupDate),
+ signupCode: code,
+ activationUrl: "http://"+httpHost(request.host)+"/ep/pro-signup/?sc="+code
+ });
+ sendEmail(record.email, "EtherPad <support@pad.spline.inf.fu-berlin.de>",
+ "Your EtherPad Professional Beta Account", {}, body);
+ });
+
+ getSession().betaAdminMessage = "Invite sent.";
+}
+
+function notifyActivated(code) {
+ println("updating: "+code);
+ sqlobj.update('pro_beta_signups', {activationCode: code},
+ {isActivated: true, activationDate: new Date()});
+}
+
diff --git a/etherpad/src/etherpad/control/pro_signup_control.js b/etherpad/src/etherpad/control/pro_signup_control.js
new file mode 100644
index 0000000..6bf7cc3
--- /dev/null
+++ b/etherpad/src/etherpad/control/pro_signup_control.js
@@ -0,0 +1,173 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("jsutils.*");
+import("cache_utils.syncedWithCache");
+import("funhtml.*");
+import("stringutils");
+import("stringutils.*");
+import("sqlbase.sqlcommon");
+
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.domains");
+
+import("etherpad.control.pro_beta_control");
+import("etherpad.control.pro.admin.account_manager_control");
+
+import("etherpad.helpers");
+
+function onRequest() {
+ if (!getSession().ods) {
+ getSession().ods = {};
+ }
+ if (request.method == "POST") {
+ // add params to cart
+ eachProperty(request.params, function(k,v) {
+ getSession().ods[k] = stringutils.toHTML(v);
+ });
+ }
+}
+
+function _errorDiv() {
+ var m = getSession().errorMessage;
+ if (m) {
+ delete getSession().errorMessage;
+ return DIV({className: 'err'}, m);
+ }
+ return "";
+}
+
+function _input(id, type) {
+ return INPUT({type: type ? type : 'text', name: id, id: id,
+ value: getSession().ods[id] || ""});
+}
+
+function _inf(id, label, type) {
+ return DIV(
+ DIV({style: "width: 100px; text-align: right; float: left; padding-top: 3px;"}, label, ": "),
+ DIV({style: "text-align: left; float: left;"},
+ _input(id, type)),
+ DIV({style: "height: 6px; clear: both;"}, " "));
+}
+
+function render_main_get() {
+ // observe activation code
+ if (request.params.sc) {
+ getSession().betaActivationCode = request.params.sc;
+ response.redirect(request.path);
+ }
+
+ // validate activation code
+ var activationCode = getSession().betaActivationCode;
+ var err = pro_beta_control.isValidCode(activationCode);
+ if (err) {
+ renderNoticeString(DIV({style: "border: 1px solid red; background: #fdd; font-weight: bold; padding: 1em;"},
+ err));
+ response.stop();
+ }
+
+ // serve activation page
+ renderFramed('main/pro_signup_body.ejs', {
+ errorDiv: _errorDiv,
+ input: _input,
+ inf: _inf
+ });
+}
+
+function _err(m) {
+ if (m) {
+ getSession().errorMessage = m;
+ response.redirect(request.path);
+ }
+}
+
+function render_main_post() {
+ var subdomain = trim(String(request.params.subdomain).toLowerCase());
+ var fullName = request.params.fullName;
+ var email = trim(request.params.email);
+
+ // validate activation code
+ var activationCode = getSession().betaActivationCode;
+ var err = pro_beta_control.isValidCode(activationCode);
+ if (err) {
+ resonse.write(err);
+ }
+
+ /*
+ var password = request.params.password;
+ var passwordConfirm = request.params.passwordConfirm;
+ */
+ var orgName = subdomain;
+
+ //---- basic validation ----
+ if (!/^\w[\w\d\-]*$/.test(subdomain)) {
+ _err("Invalid domain: "+subdomain);
+ }
+ if (subdomain.length < 2) {
+ _err("Subdomain must be at least 2 characters.");
+ }
+ if (subdomain.length > 60) {
+ _err("Subdomain must be <= 60 characters.");
+ }
+
+/*
+ if (password != passwordConfirm) {
+ _err("Passwords do not match.");
+ }
+ */
+
+ _err(pro_accounts.validateFullName(fullName));
+ _err(pro_accounts.validateEmail(email));
+
+ if (!(email.match(/[Ff][Uu]-[Bb][Ee][Rr][Ll][Ii][Nn].[Dd][Ee]$/))) { _err("Please use your *.fu-berlin.de email address."); }
+// _err(pro_accounts.validatePassword(password));
+
+ //---- database validation ----
+
+ if (domains.doesSubdomainExist(subdomain)) {
+ _err("The domain "+subdomain+" is already in use.");
+ }
+
+ //---- looks good. create records! ----
+
+ // TODO: log a bunch of stuff, and request IP address, etc.
+
+ var ok = false;
+ sqlcommon.inTransaction(function() {
+ var tempPass = stringutils.randomString(10);
+ // TODO: move validation code into domains.createNewSubdomain...
+ var domainId = domains.createNewSubdomain(subdomain, orgName);
+ var accountId = pro_accounts.createNewAccount(domainId, fullName, email, tempPass, true);
+ // send welcome email
+ syncedWithCache('pro-activations', function(c) {
+ c[domainId] = true;
+ });
+ ok = true;
+ if (activationCode) {
+ pro_beta_control.notifyActivated(activationCode);
+ }
+ });
+
+ if (ok) {
+ response.redirect('http://'+subdomain+"."+request.host+'/ep/finish-activation');
+ } else {
+ response.write("There was an error processing your request.");
+ }
+}
+
diff --git a/etherpad/src/etherpad/control/scriptcontrol.js b/etherpad/src/etherpad/control/scriptcontrol.js
new file mode 100644
index 0000000..16efc60
--- /dev/null
+++ b/etherpad/src/etherpad/control/scriptcontrol.js
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.pad.dbwriter");
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+
+function onRequest() {
+ if (!isProduction()) {
+ return;
+ }
+ if (request.params.auth != 'f83kg840d12jk') {
+ response.forbid();
+ }
+}
+
+function render_setdbwritable() {
+ var dbwritable = (String(request.params.value).toLowerCase() != 'false'); // default to true
+
+ dbwriter.setWritableState({constant: dbwritable});
+
+ response.write("OK, set to "+dbwritable);
+}
+
+function render_getdbwritable() {
+ var state = dbwriter.getWritableState();
+
+ response.write(String(dbwriter.getWritableStateDescription(state)));
+}
+
+function render_pausedbwriter() {
+ var seconds = request.params.seconds;
+ var seconds = Number(seconds || 0);
+ if (isNaN(seconds)) seconds = 0;
+
+ var finishTime = (+new Date())+(1000*seconds);
+ dbwriter.setWritableState({trueAfter: finishTime});
+
+ response.write("Paused dbwriter for "+seconds+" seconds.");
+}
+
+function render_fake_pne_on() {
+ if (isProduction()) {
+ response.write("has no effect in production.");
+ } else {
+ appjet.cache.fakePNE = true;
+ response.write("OK");
+ }
+}
+
+function render_fake_pne_off() {
+ if (isProduction()) {
+ response.write("has no effect in production.");
+ } else {
+ appjet.cache.fakePNE = false;
+ response.write("OK");
+ }
+}
+
+
+
+
diff --git a/etherpad/src/etherpad/control/static_control.js b/etherpad/src/etherpad/control/static_control.js
new file mode 100644
index 0000000..d938b26
--- /dev/null
+++ b/etherpad/src/etherpad/control/static_control.js
@@ -0,0 +1,76 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+import("etherpad.admin.plugins");
+
+function onRequest() {
+ var staticBase = '/static';
+
+ var opts = {cache: isProduction()};
+
+ var disp = new Dispatcher();
+
+ /* FIXME: Is there a more effective way to do this? */
+ for (plugin in plugins.plugins) {
+ disp.addLocations([
+ [PrefixMatcher('/static/js/plugins/'+plugin+'/'), faststatic.directoryServer('/plugins/' + plugin + '/static/js/', opts)],
+ [PrefixMatcher('/static/css/plugins/'+plugin+'/'), faststatic.directoryServer('/plugins/' + plugin + '/static/css/', opts)],
+ [PrefixMatcher('/static/swf/plugins/'+plugin+'/'), faststatic.directoryServer('/plugins/' + plugin + '/static/swf/', opts)],
+ [PrefixMatcher('/static/html/plugins/'+plugin+'/'), faststatic.directoryServer('/plugins/' + plugin + '/static/html/', opts)],
+ [PrefixMatcher('/static/zip/plugins/'+plugin+'/'), faststatic.directoryServer('/plugins/' + plugin + '/static/zip/', opts)]]);
+ }
+
+ var serveFavicon = faststatic.singleFileServer(staticBase + '/favicon.ico', opts);
+ var serveCrossDomain = faststatic.singleFileServer(staticBase + '/crossdomain.xml', opts);
+ var serveStaticDir = faststatic.directoryServer(staticBase, opts);
+ var serveCompressed = faststatic.compressedFileServer(opts);
+ var serveJs = faststatic.directoryServer(staticBase+'/js/', opts);
+ var serveCss = faststatic.directoryServer(staticBase+'/css/', opts);
+ var serveSwf = faststatic.directoryServer(staticBase+'/swf/', opts);
+ var serveHtml = faststatic.directoryServer(staticBase+'/html/', opts);
+ var serveZip = faststatic.directoryServer(staticBase+'/zip/', opts);
+
+ disp.addLocations([
+ ['/favicon.ico', serveFavicon],
+ ['/robots.txt', serveRobotsTxt],
+ ['/crossdomain.xml', serveCrossDomain],
+ [PrefixMatcher('/static/html/'), serveHtml],
+ [PrefixMatcher('/static/js/'), serveJs],
+ [PrefixMatcher('/static/css/'), serveCss],
+ [PrefixMatcher('/static/swf/'), serveSwf],
+ [PrefixMatcher('/static/zip/'), serveZip],
+ [PrefixMatcher('/static/compressed/'), serveCompressed],
+ [PrefixMatcher('/static/'), serveStaticDir]
+ ]);
+
+ return disp.dispatch();
+}
+
+function serveRobotsTxt(name) {
+ response.neverCache();
+ response.setContentType('text/plain');
+ response.write('User-agent: *\n');
+ if (!isProduction()) {
+ response.write('Disallow: /\n');
+ }
+ response.stop();
+ return true;
+}
diff --git a/etherpad/src/etherpad/control/statscontrol.js b/etherpad/src/etherpad/control/statscontrol.js
new file mode 100644
index 0000000..3659107
--- /dev/null
+++ b/etherpad/src/etherpad/control/statscontrol.js
@@ -0,0 +1,1214 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("netutils");
+import("funhtml.*");
+import("stringutils.{html,sprintf,startsWith,md5}");
+import("jsutils.*");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("dispatch.{Dispatcher,PrefixMatcher,DirMatcher,forward}");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.sessions.getSession");
+import("etherpad.sessions");
+import("etherpad.statistics.statistics");
+import("etherpad.log");
+import("etherpad.usage_stats.usage_stats");
+import("etherpad.helpers");
+
+//----------------------------------------------------------------
+// Usagestats
+//----------------------------------------------------------------
+
+var _defaultPrefs = {
+ topNCount: 5,
+ granularity: 1440
+}
+
+function onRequest() {
+ keys(_defaultPrefs).forEach(function(prefName) {
+ if (request.params[prefName]) {
+ _prefs()[prefName] = request.params[prefName];
+ }
+ });
+ if (request.isPost) {
+ response.redirect(
+ request.path+
+ (request.query ? "?"+request.query : "")+
+ (request.params.fragment ? "#"+request.params.fragment : ""));
+ }
+}
+
+function _prefs() {
+ if (! sessions.getSession().statsPrefs) {
+ sessions.getSession().statsPrefs = {}
+ }
+ return sessions.getSession().statsPrefs;
+}
+
+function _pref(pname) {
+ return _prefs()[pname] || _defaultPrefs[pname];
+}
+
+function _topN() {
+ return _pref('topNCount');
+}
+function _showLiveStats() {
+ return _timescale() < 1440;
+ // return _pref('granularity') == 'live';
+}
+function _showHistStats() {
+ return _timescale() >= 1440
+ // return _pref('showLiveOrHistorical') == 'hist';
+}
+function _timescale() {
+ return Number(_pref('granularity')) || 1;
+}
+
+// types:
+// compare - compare one or more single-value stats
+// top - show top values over time
+// histogram - show histogram over time
+
+var statDisplays = {
+ users: [
+ { name: "visitors",
+ description: "User visits, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "site_pageviews",
+ description: "Page views",
+ color: "FFA928" },
+ {stat: "site_unique_ips",
+ description: "Unique IPs",
+ color: "00FF00" } ] },
+
+ // free pad usage
+ { name: "free pad usage, 1 day",
+ description: "Free pad.spline.inf.fu-berlin.de users, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "active_user_ids",
+ description: "All users",
+ color: "FFA928" },
+ {stat: "users_1day_returning_7days",
+ description: "Users returning after 7 days",
+ color: "00FF00"},
+ {stat: "users_1day_returning_30days",
+ description: "Users returning after 30 days",
+ color: "FF0000"} ] },
+ { name: "free pad usage, 7 day",
+ description: "Free pad.spline.inf.fu-berlin.de users over the last 7 days",
+ type: "compare",
+ options: { hideLive: true, latestUseHistorical: true},
+ stats: [ {stat: "active_user_ids_7days",
+ description: "All users",
+ color: "FFA928" },
+ {stat: "users_7day_returning_7days",
+ description: "Users returning after 7 days",
+ color: "00FF00"},
+ {stat: "users_7day_returning_30days",
+ description: "Users returning after 30 days",
+ color: "FF0000"} ] },
+ { name: "free pad usage, 30 day",
+ description: "Free pad.spline.inf.fu-berlin.de users over the last 30 days",
+ type: "compare",
+ options: { hideLive: true, latestUseHistorical: true},
+ stats: [ {stat: "active_user_ids_30days",
+ description: "All users",
+ color: "FFA928" },
+ {stat: "users_30day_returning_7days",
+ description: "Users returning after 7 days",
+ color: "00FF00"},
+ {stat: "users_30day_returning_30days",
+ description: "Users returning after 30 days",
+ color: "FF0000"} ] },
+
+ // pro pad usage
+ { name: "active pro accounts, 1 day",
+ description: "Active pro accounts, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "active_pro_accounts",
+ description: "All accounts",
+ color: "FFA928" },
+ {stat: "pro_accounts_1day_returning_7days",
+ description: "Accounts older than 7 days",
+ color: "00FF00"},
+ {stat: "pro_accounts_1day_returning_30days",
+ description: "Accounts older than 30 days",
+ color: "FF0000"} ] },
+ { name: "active pro accounts, 7 day",
+ description: "Active pro accounts over the last 7 days",
+ type: "compare",
+ options: { hideLive: true, latestUseHistorical: true},
+ stats: [ {stat: "active_pro_accounts_7days",
+ description: "All accounts",
+ color: "FFA928" },
+ {stat: "pro_accounts_7day_returning_7days",
+ description: "Accounts older than 7 days",
+ color: "00FF00"},
+ {stat: "pro_accounts_7day_returning_30days",
+ description: "Accounts older than 30 days",
+ color: "FF0000"} ] },
+ { name: "active pro accounts, 30 day",
+ description: "Active pro accounts over the last 30 days",
+ type: "compare",
+ options: { hideLive: true, latestUseHistorical: true},
+ stats: [ {stat: "active_pro_accounts_30days",
+ description: "All accounts",
+ color: "FFA928" },
+ {stat: "pro_accounts_30day_returning_7days",
+ description: "Accounts older than 7 days",
+ color: "00FF00"},
+ {stat: "pro_accounts_30day_returning_30days",
+ description: "Accounts older than 30 days",
+ color: "FF0000"} ] },
+
+ // other stats
+ { name: "pad connections",
+ description: "Number of active comet connections, mean over a %t period",
+ type: "top",
+ options: {showOthers: false},
+ stats: ["streaming_connections"] },
+ { name: "referers",
+ description: "Referers, number of hits over a %t period",
+ type: "top",
+ options: {showOthers: false},
+ stats: ["top_referers"] },
+ ],
+ product: [
+ { name: "pads",
+ description: "Newly-created and active pads, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "active_pads",
+ description: "Active pads",
+ color: "FFA928" },
+ {stat: "new_pads",
+ description: "New pads",
+ color: "FF0000" }] },
+ { name: "chats",
+ description: "Chat messages and active chatters, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "chat_messages",
+ description: "Messages",
+ color: "FFA928" },
+ {stat: "active_chatters",
+ description: "Chatters",
+ color: "FF0000" }] },
+ { name: "import/export",
+ description: "Imports and Exports, total over a %t period",
+ type: "compare",
+ stats: [ {stat: {f: '+', args: ["imports_exports_counts:export", "imports_exports_counts:import"]},
+ description: "Total",
+ color: "FFA928" },
+ {stat: "imports_exports_counts:export",
+ description: "Exports",
+ color: "FF0000"},
+ {stat: "imports_exports_counts:import",
+ description: "Imports",
+ color: "00FF00"}] },
+ { name: "revenue",
+ description: "Revenue, total over a %t period",
+ type: "compare",
+ stats: [ {stat: "revenue",
+ description: "Revenue",
+ color: "FFA928"}] }
+ ],
+ performance: [
+ { name: "dynamic page latencies",
+ description: "Slowest dynamic pages: mean load time in milliseconds over a %t period",
+ type: "top",
+ options: {showOthers: false},
+ stats: ["execution_latencies"] },
+ { name: "pad startup latencies",
+ description: "Pad startup times: percent load time in milliseconds over a %t period",
+ type: "histogram",
+ stats: ["pad_startup_times"] },
+ { name: "stream post latencies",
+ description: "Comet post latencies, percentiles in milliseconds over a %t period",
+ type: "histogram",
+ stats: ["streaming_latencies"] },
+ ],
+ health: [
+ { name: "disconnect causes",
+ description: "Causes of disconnects, total over a %t period",
+ type: "top",
+ stats: ["disconnect_causes"] },
+ { name: "paths with 404s",
+ description: "'Not found' responses, by path, number served over a %t period",
+ type: "top",
+ stats: ["paths_404"] },
+ { name: "exceptions",
+ description: "Total number of server exceptions over a %t period",
+ type: "compare",
+ stats: [ {stat: "exceptions",
+ description: "Exceptions",
+ color: "FF1928" } ] },
+ { name: "paths with 500s",
+ type: "top",
+ description: "'500' responses, by path, number served over a %t period",
+ type: "top",
+ stats: ["paths_500"] },
+ { name: "paths with exceptions",
+ description: "responses with exceptions, by path, number served over a %t period",
+ type: "top",
+ stats: ["paths_exception"] },
+ { name: "disconnects with client-side errors",
+ description: "user disconnects with an error on the client side, number over a %t period",
+ type: "compare",
+ stats: [ { stat: "disconnects_with_clientside_errors",
+ description: "Disconnects with errors",
+ color: "FFA928" } ] },
+ { name: "unnecessary disconnects",
+ description: "disconnects that were avoidable, number over a %t period",
+ type: "compare",
+ stats: [ { stat: "streaming_disconnects:disconnected_userids",
+ description: "Number of unique users disconnected",
+ color: "FFA928" },
+ { stat: "streaming_disconnects:total_disconnects",
+ description: "Total number of disconnects",
+ color: "FF0000" } ] },
+ ]
+}
+
+function getUsedStats(statStructure) {
+ var stats = {};
+ function getStructureValues(statStructure) {
+ if (typeof(statStructure) == 'string') {
+ stats[statStructure] = true;
+ } else {
+ statStructure.args.forEach(getStructureValues);
+ }
+ }
+ getStructureValues(statStructure);
+ return keys(stats);
+}
+
+function getStatData(statStructure, values_f) {
+ function getStructureValues(statStructure) {
+ if (typeof(statStructure) == 'string') {
+ return values_f(statStructure);
+ } else if (typeof(statStructure) == 'number') {
+ return statStructure;
+ } else {
+ var args = statStructure.args.map(getStructureValues);
+ return {
+ f: statStructure.f,
+ args: args
+ }
+ }
+ }
+
+ var mappedStructure = getStructureValues(statStructure);
+
+ function evalStructure(statStructure) {
+ if ((typeof(statStructure) == 'number') || (statStructure instanceof Array)) {
+ return statStructure;
+ } else {
+ var merge_f = statStructure.f;
+ if (typeof(merge_f) == 'string') {
+ switch (merge_f) {
+ case '+':
+ merge_f = function() {
+ var sum = 0;
+ for (var i = 0; i < arguments.length; ++i) {
+ sum += arguments[i];
+ }
+ return sum;
+ }
+ break;
+ case '*':
+ merge_f = function() {
+ var product = 0;
+ for (var i = 0; i < arguments.length; ++i) {
+ product *= arguments[i];
+ }
+ return product;
+ }
+ break;
+ case '/':
+ merge_f = function(a, b) { return a / b; }
+ break;
+ case '-':
+ merge_f = function(a, b) { return a - b; }
+ break;
+ }
+ }
+ var evaluatedArguments = statStructure.args.map(evalStructure);
+ var length = -1;
+ evaluatedArguments.forEach(function(arg) {
+ if (typeof(arg) == 'object' && (arg instanceof Array)) {
+ length = arg.length;
+ }
+ });
+ evaluatedArguments = evaluatedArguments.map(function(arg) {
+ if (typeof(arg) == 'number') {
+ var newArg = new Array(length);
+ for (var i = 0; i < newArg.length; ++i) {
+ newArg[i] = arg;
+ }
+ return newArg
+ } else {
+ return arg;
+ }
+ });
+ return mergeArrays.apply(this, [merge_f].concat(evaluatedArguments));
+ }
+ }
+ return evalStructure(mappedStructure);
+}
+
+var googleChartSimpleEncoding = "ABCDEFGHIJLKMNOPQRSTUVQXYZabcdefghijklmnopqrstuvwxyz0123456789-.";
+function _enc(value) {
+ return googleChartSimpleEncoding[Math.floor(value/64)] + googleChartSimpleEncoding[value%64];
+}
+
+function drawSparkline(dataSets, labels, colors, minutes) {
+ var max = 1;
+ var maxLength = 0;
+ dataSets.forEach(function(dataSet, i) {
+ if (dataSet.length > maxLength) {
+ maxLength = dataSet.length;
+ }
+ dataSet.forEach(function(point) {
+ if (point > max) {
+ max = point;
+ }
+ });
+ });
+ var data = dataSets.map(function(dataSet) {
+ var chars = dataSet.map(function(x) {
+ if (x !== undefined) {
+ return _enc(Math.round(x/max*4095));
+ } else {
+ return "__";
+ }
+ }).join("");
+ while (chars.length < maxLength*2) {
+ chars = "__"+chars;
+ }
+ return chars;
+ }).join(",");
+ var timeLabels;
+ if (minutes < 60*24) {
+ timeLabels = [4,3,2,1,0].map(function(t) {
+ var minutesPerTick = minutes/4;
+ var d = new Date(Date.now() - minutesPerTick*60000*t);
+ return (d.getHours()%12 || 12)+":"+(d.getMinutes() < 10 ? "0" : "")+d.getMinutes()+(d.getHours() < 12 ? "am":"pm");
+ }).join("|");
+ } else {
+ timeLabels = [4,3,2,1,0].map(function(t) {
+ var daysPerTick = (minutes/(60*24))/4;
+ var d = new Date(Date.now() - t*daysPerTick*24*60*60*1000);
+ return (d.getMonth()+1)+"/"+d.getDate();
+ }).join("|");
+ }
+ var pointLabels = dataSets.map(function(dataSet, i) {
+ return ["t"+dataSet[dataSet.length-1],colors[i],i,maxLength-1,12,0].join(",");
+ }).join("|");
+ labels = labels.map(function(label) {
+ return encodeURIComponent((label.length > 73) ? label.slice(0, 70) + "..." : label);
+ });
+ var step = Math.round(max/10);
+ step = Math.round(step/Math.pow(10, String(step).length-1))*Math.pow(10, String(step).length-1);
+ var srcUrl =
+ "http://chart.apis.google.com/chart?chs=600x300&cht=lc&chd=e:"+data+
+ "&chxt=y,x&chco="+colors.join(",")+"&chxr=0,0,"+max+","+step+"&chxl=1:|"+timeLabels+
+ "&chdl="+labels.join("|")+"&chdlp=b&chm="+pointLabels;
+ return toHTML(IMG({src: srcUrl}));
+}
+
+var liveDataNumSamples = 20;
+
+function extractStatValuesFunction(nameToValues_f) {
+ return function(statName) {
+ var value;
+ if (statName.indexOf(":") >= 0) {
+ [statName, value] = statName.split(":");
+ }
+ var h = nameToValues_f(statName);
+ if (value) {
+ h = h.map(function(topValues) {
+ if (! topValues) { return; }
+ var tv = topValues.topValues;
+ for (var i = 0; i < tv.length; ++i) {
+ if (tv[i].value == value) {
+ return tv[i].count;
+ }
+ }
+ return 0;
+ });
+ }
+ return h;
+ }
+}
+
+function sparkline_compare(history_f, minutesPerSample, stat) {
+ var histories = stat.stats.map(function(stat) {
+ var samples = getStatData(stat.stat, extractStatValuesFunction(history_f));
+ return [samples, stat.description, stat.color];
+ });
+ return drawSparkline(histories.map(function(history) { return history[0] }),
+ histories.map(function(history) { return history[1] }),
+ histories.map(function(history) { return history[2] }),
+ minutesPerSample*histories[0][0].length);
+}
+
+function sparkline_top(history_f, minutesPerSample, stat) {
+ var showOthers = ! stat.options || stat.options.showOthers != false;
+ var history = stat.stats.map(history_f)[0];
+
+ if (history.length == 0) {
+ return "<b>no data</b>";
+ }
+ var topRecents = {};
+ var topRecents_arr = [];
+ history.forEach(function(tv) {
+ if (! tv) { return; }
+ if (tv.topValues.length > 0) {
+ topRecents_arr = tv.topValues.map(function(x) { return x.value; });
+ }
+ });
+
+ if (topRecents_arr.length == 0) {
+ return "<b>no data</b>";
+ }
+ topRecents_arr = topRecents_arr.slice(0, _topN());
+ topRecents_arr.forEach(function(value, i) {
+ topRecents[value] = i;
+ });
+
+ if (showOthers) {
+ topRecents_arr.push("Other");
+ }
+ var max = 1;
+ var values = topRecents_arr.map(function() { return history.map(function() { return 0 }); });
+
+ history.forEach(function(tv, i) {
+ if (! tv) { return; }
+ tv.topValues.forEach(function(entry) {
+ if (entry.count > max) {
+ max = entry.count;
+ }
+ if (entry.value in topRecents) {
+ values[topRecents[entry.value]][i] = entry.count;
+ } else if (showOthers) {
+ values[values.length-1][i] += entry.count;
+ }
+ });
+ });
+ return drawSparkline(
+ values,
+ topRecents_arr,
+ ["FF0000", "00FF00", "0000FF", "FF00FF", "00FFFF"].slice(0, topRecents_arr.length-1).concat("FFA928"),
+ minutesPerSample*history.length);
+}
+
+function sparkline_histogram(history_f, minutesPerSample, stat) {
+ var history = stat.stats.map(history_f)[0];
+
+ if (history.length == 0) {
+ return "<b>no data</b>";
+ }
+ var percentiles = [50, 90, 95, 99];
+ var data = percentiles.map(function() { return []; })
+ history.forEach(function(hist) {
+ percentiles.forEach(function(pct, i) {
+ data[i].push((hist ? hist[""+pct] : undefined));
+ });
+ });
+ return drawSparkline(
+ data,
+ percentiles.map(function(pct) { return ""+pct+"%"; }),
+ ["FF0000","FF00FF","FFA928","00FF00"].reverse(),
+ minutesPerSample*history.length);
+}
+
+function liveHistoryFunction(minutesPerSample) {
+ return function(statName) {
+ return statistics.liveSnapshot(statName).history(minutesPerSample, liveDataNumSamples);
+ }
+}
+
+function _listStats(statName, count) {
+ var options = { orderBy: '-timestamp,id' };
+ if (count !== undefined) {
+ options.limit = count;
+ }
+ return sqlobj.selectMulti('statistics', {name: statName}, options);
+}
+
+function ancientHistoryFunction(time) {
+ return function(statName) {
+ var seenDates = {};
+ var samples = _listStats(statName);
+
+ samples = samples.reverse().map(function(json) {
+ if (seenDates[""+json.timestamp]) { return; }
+ seenDates[""+json.timestamp] = true;
+ return {timestamp: json.timestamp, json: json.value};
+ }).filter(function(x) { return x !== undefined });
+
+ samples = samples.reverse().slice(0, Math.round(time/(24*60)));
+ var samplesWithEmptyValues = [];
+ for (var i = 0; i < samples.length-1; ++i) {
+ var current = samples[i];
+ var next = samples[i+1];
+ samplesWithEmptyValues.push(current.json);
+ for (var j = current.timestamp+86400*1000; j < next.timestamp; j += 86400*1000) {
+ samplesWithEmptyValues.push(undefined);
+ }
+ }
+ if (samples.length > 0) {
+ samplesWithEmptyValues.push(samples[samples.length-1].json);
+ }
+ samplesWithEmptyValues = samplesWithEmptyValues.map(function(json) {
+ if (! json) { return; }
+ var obj = fastJSON.parse(json);
+ if (keys(obj).length == 1 && 'value' in obj) {
+ obj = obj.value;
+ }
+ return obj;
+ });
+
+ return samplesWithEmptyValues.reverse();
+ }
+}
+
+function sparkline(history_f, minutesPerSample, stat) {
+ if (this["sparkline_"+stat.type]) {
+ return this["sparkline_"+stat.type](history_f, minutesPerSample, stat);
+ } else {
+ return "<b>No sparkline handler!</b>";
+ }
+}
+
+function liveLatestFunction(minutesPerSample) {
+ return function(statName) {
+ return [statistics.liveSnapshot(statName).latest(minutesPerSample)];
+ }
+}
+
+function liveTotal(statName) {
+ return [statistics.liveSnapshot(statName).total];
+}
+
+function historyLatest(statName) {
+ return _listStats(statName, 1).map(function(x) {
+ var value = fastJSON.parse(x.value);
+ if (keys(value).length == 1 && 'value' in value) {
+ value = value.value;
+ }
+ return value;
+ });
+}
+
+function latest_compare(latest_f, stat) {
+ return stat.stats.map(function(stat) {
+ var sample = getStatData(stat.stat, extractStatValuesFunction(latest_f))[0];
+ return { value: sample, description: stat.description };
+ });
+}
+
+function latest_top(latest_f, stat) {
+ var showOthers = ! stat.options || stat.options.showOthers != false;
+
+ var sample = stat.stats.map(latest_f)[0][0];
+ if (! sample) {
+ return [];
+ }
+ var total = sample.count;
+
+ var values = sample.topValues.slice(0, _topN()).map(function(v) {
+ total -= v.count;
+ return { value: v.count, description: v.value };
+ });
+ if (showOthers) {
+ values.push({value: total, description: "Other"});
+ }
+ return values;
+}
+
+function latest_histogram(latest_f, stat) {
+ var sample = stat.stats.map(latest_f)[0][0];
+
+ if (! sample) {
+ return "<b>no data</b>";
+ }
+
+ var percentiles = [0, 1, 5, 10, 25, 50, 75, 90, 95, 99, 100].filter(function(pct) { return ((""+pct) in sample) });
+
+ var xpos = percentiles.map(function(x, i) { return sample[x] });
+ var xMax = 0;
+ var xMin = 1e12;
+ xpos.forEach(function(x) { xMax = (x > xMax ? x : xMax); xMin = (x < xMin ? x : xMin); });
+ xposNormalized = xpos.map(function(x) { return Math.round((x-xMin)/(xMax-xMin || 1)*100); });
+
+ var ypos = percentiles.slice(1).map(function(y, i) { return (y-percentiles[i])/(xpos[i+1] || 1); });
+ var yMax = 0;
+ ypos.forEach(function(y) { yMax = (y > yMax ? y : yMax); });
+ yposNormalized = ypos.map(function(y) { return Math.round(y/yMax*100); });
+
+ // var proposedLabels = mergeArrays(function(x, y) { return {pos: x, label: y}; }, xposNormalized, xpos);
+ // var keepLabels = [{pos: 0, label: 0}];
+ // proposedLabels.forEach(function(label) {
+ // if (label.pos - keepLabels[keepLabels.length-1].pos > 10) {
+ // keepLabels.push(label);
+ // }
+ // });
+ //
+ // var labelPos = keepLabels.map(function(x) { return x.pos });
+ // var labels = keepLabels.map(function(x) { return x.label });
+
+ return toHTML(IMG({src:
+ "http://chart.apis.google.com/chart?chs=340x100&cht=lxy&chd=t:"+xposNormalized.join(",")+"|0,"+yposNormalized.join(",")+
+ "&chxt=x&chxr=0,"+xMin+","+xMax+","+Math.floor((xMax-xMin)/5) // "l=0:|"+labels.join("|")+"&chxp=0,"+labelPos.join(",")
+ }));
+}
+
+function latest(latest_f, stat) {
+ if (this["latest_"+stat.type]) {
+ return this["latest_"+stat.type](latest_f, stat);
+ } else {
+ return "<b>No latest handler!</b>";
+ }
+}
+
+function dropdown(name, options, selected) {
+ var select;
+ if (typeof(name) == 'string') {
+ select = SELECT({name: name});
+ } else {
+ select = SELECT(name);
+ }
+
+ function addOption(value, content) {
+ var opt = OPTION({value: value}, content || value);
+ if (value == selected) {
+ opt.attribs.selected = "selected";
+ }
+ select.push(opt);
+ }
+
+ if (options instanceof Array) {
+ options.forEach(f_limitArgs(this, addOption, 1));
+ } else {
+ eachProperty(options, addOption);
+ }
+ return select;
+}
+
+function render_main() {
+ var categoriesToStats = {};
+
+ eachProperty(statDisplays, function(catName, statArray) {
+ categoriesToStats[catName] = statArray.map(_renderableStat);
+ });
+
+ renderHtml('statistics/stat_page.ejs',
+ {eachProperty: eachProperty,
+ statCategoryNames: keys(categoriesToStats),
+ categoriesToStats: categoriesToStats,
+ optionsForm: _optionsForm() });
+}
+
+function _optionsForm() {
+ return FORM({id: "statprefs", method: "POST"}, "Show data with granularity: ",
+ // dropdown({name: 'showLiveOrHistorical', onchange: 'formChanged();'},
+ // {live: 'live', hist: 'historical'},
+ // _pref('showLiveOrHistorical')),
+ // (_showLiveStats() ?
+ // SPAN(" with granularity ",
+ dropdown({name: 'granularity', onchange: 'formChanged();'},
+ {"1": '1 minute', "5": '5 minutes', "60": '1 hour', "1440": '1 day'},
+ _pref('granularity')), // ),
+ // : ""),
+ " top N:",
+ INPUT({type: "text", name: "topNCount", value: _topN()}),
+ INPUT({type: "submit", name: "Set", value: "set N"}),
+ INPUT({type: "hidden", name: "fragment", id: "fragment", value: "health"}));
+}
+
+// function render_main() {
+// var body = BODY();
+//
+// var cat = request.params.cat;
+// if (!cat) {
+// cat = 'health';
+// }
+//
+// body.push(A({id: "backtoadmin", href: "/ep/admin/"}, html("&laquo;"), " back to admin"));
+// body.push(_renderTopnav(cat));
+//
+// body.push(form);
+//
+// if (request.params.stat) {
+// body.push(A({className: "viewall",
+// href: qpath({stat: null})}, html("&laquo;"), " view all"));
+// }
+//
+// var statNames = statDisplays[cat];
+// statNames.forEach(function(sn) {
+// if (!request.params.stat || (request.params.stat == sn)) {
+// body.push(_renderableStat(sn));
+// }
+// });
+//
+// helpers.includeCss('admin/admin-stats.css');
+// response.write(HTML(HEAD(html(helpers.cssIncludes())), body));
+// }
+
+function _getLatest(stat) {
+ var minutesPerSample = _timescale();
+
+ if (_showLiveStats()) {
+ return latest(liveLatestFunction(minutesPerSample), stat);
+ } else {
+ return latest(liveTotal, stat);
+ }
+}
+
+function _getGraph(stat) {
+ var minutesPerSample = _timescale();
+
+ if (_showLiveStats()) {
+ return html(sparkline(liveHistoryFunction(minutesPerSample), minutesPerSample, stat));
+ } else {
+ return html(sparkline(ancientHistoryFunction(60*24*60), 24*60, stat));
+ }
+}
+
+function _getDataLinks(stat) {
+ if (_showLiveStats()) {
+ return;
+ }
+
+ function listToLinks(list) {
+ var links = []; //SPAN({className: "datalink"}, "(data for ");
+ list.forEach(function(statName) {
+ links.push(toHTML(A({href: "/ep/admin/usagestats/data?statName="+statName}, statName)));
+ });
+// links.push(")");
+ return links;
+ }
+
+ switch (stat.type) {
+ case 'compare':
+ var stats = [];
+ stat.stats.map(function(stat) { return getUsedStats(stat.stat); }).forEach(function(list) {
+ stats = stats.concat(list);
+ });
+ return listToLinks(stats);
+ case 'top':
+ return listToLinks(stat.stats);
+ case 'histogram':
+ return listToLinks(stat.stats);
+ }
+}
+
+function _renderableStat(stat) {
+ var minutesPerSample = _timescale();
+
+ var period = (_showLiveStats() ? minutesPerSample : 24*60);
+
+ if (period < 24*60 && stat.hideLive) {
+ return;
+ }
+
+ if (period < 60) {
+ period = ""+period+"-minute";
+ } else if (period < 24*60) {
+ period = ""+period/(60)+"-hour";
+ } else if (period >= 24*60) {
+ period = ""+period/(24*60)+"-day";
+ }
+ var graph = _getGraph(stat);
+ var id = stat.name.replace(/[^a-zA-Z0-9]/g, "");
+
+ var displayName = stat.description.replace("%t", period);
+ var latest = _getLatest(stat);
+ var dataLinks = _getDataLinks(stat);
+
+ return {
+ id: id,
+ specialState: "",
+ displayName: displayName,
+ name: stat.name,
+ graph: graph,
+ latest: latest,
+ dataLinks: dataLinks
+ }
+}
+
+function render_data() {
+ var sn = request.params.statName;
+ var t = TABLE({border: 1, cellpadding: 2, style: "font-family: monospace;"});
+ _listStats(sn).forEach(function(s) {
+ var tr = TR();
+ tr.push(TD((s.id)));
+ tr.push(TD((new Date(s.timestamp * 1000)).toString()));
+ tr.push(TD(s.value));
+ t.push(tr);
+ });
+ response.write(HTML(BODY(t)));
+}
+
+
+// function renderStat(body, statName) {
+// var div = DIV({className: 'statbox'});
+// div.push(A({className: "stat-title", href: qpath({stat: statName})},
+// statName, descriptions[statName] || ""));
+// if (_showHistStats()) {
+// div.push(
+// DIV({className: "stat-graph"},
+// A({href: '/ep/admin/usagestats/graph?size=1080x420&statName='+statName},
+// IMG({src: '/ep/admin/usagestats/graph?size=400x200&statName='+statName,
+// style: 'border: 1px solid #ccc; margin: 10px 0 0 20px;'})),
+// BR(),
+// DIV({style: 'text-align: right;'},
+// A({style: 'text-decoration: none; font-size: .8em;',
+// href: '/ep/admin/usagestats/data?statName='+statName}, "(data)")))
+// );
+// }
+// if (_showLiveStats()) {
+// var data = statistics.getStatData(statName);
+// var displayData = statistics.liveSnapshot(data);
+// var t = TABLE({border: 0});
+// var tcount = 0;
+// ["minute", "hour", "fourHour", "day"].forEach(function(timescale) {
+// if (! _showTimescale(timescale)) { return; }
+// var tr = TR();
+// t.push(tr);
+// tr.push(TD({valign: "top"}, B("Last ", timescale)));
+// var td = TD();
+// var cell = SPAN();
+// tr.push(td);
+// td.push(cell);
+// switch (data.plotType) {
+// case 'line':
+// cell.push(B(displayData[timescale])); break;
+// case 'topValues':
+// var top = displayData[timescale].topValues;
+// if (tcount != 0) {
+// tr[0].attribs.style = cell.attribs.style = "border-top: 2px solid black;";
+// }
+// // println(statName+" / top length: "+top.length);
+// for (var i = 0; i < Math.min(_topN(), top.length); ++i) {
+// cell.push(B(top[i].count), ": ", top[i].value, BR());
+// }
+// break;
+// case 'histogram':
+// var percentiles = displayData[timescale];
+// var pcts = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
+// var max = percentiles["100"] || 1000;
+// cell.push(IMG({src: "http://chart.apis.google.com/chart?chs=340x100&cht=bvs&chd=t:"+
+// pcts.map(function(pct) { return Math.round(percentiles[""+pct]/max*100); }).join(",")+
+// "&chxt=x,y&chxl=0:|"+
+// pcts.map(function(pct) { return ""+pct+"%"; }).join("|")+
+// "&chxr=0,0,100|1,0,"+max+""}))
+// // td.push("50%: ", B(percentiles["50"]), " ",
+// // "90%: ", B(percentiles["90"]), " ",
+// // "max: ", B(percentiles["100"]));
+// break;
+// }
+// tcount++;
+// });
+// div.push(DIV({className: "stat-table"}, t));
+// div.push(html(helpers.clearFloats()));
+// }
+// body.push(div);
+// }
+// =======
+// >>>>>>> Stashed changes:etherpad/src/etherpad/control/statscontrol.js
+
+
+// old output.
+
+//
+// function getStatsForCategory(category) {
+// var statnames = statistics.getAllStatNames();
+//
+// var matchingStatNames = [];
+// statnames.forEach(function(sn) {
+// if (statistics.getStatData(sn).category == category) {
+// matchingStatNames.push(sn);
+// }
+// });
+//
+// return matchingStatNames;
+// }
+//
+// function renderCategoryList() {
+// var body = BODY();
+//
+// catNames = getCategoryNames();
+// body.push(P("Please select a statistics category:"));
+// catNames.sort().forEach(function(catname) {
+// body.push(P(A({href: "/ep/admin/usagestats/?cat="+catname}, catname)));
+// });
+// response.write(body);
+// }
+//
+// function getCategoryNames() {
+// var statnames = statistics.getAllStatNames();
+// var catNames = {};
+// statnames.forEach(function(sn) {
+// catNames[statistics.getStatData(sn).category] = true;
+// });
+// return keys(catNames);
+// }
+//
+// function dropdown(name, options, selected) {
+// var select;
+// if (typeof(name) == 'string') {
+// select = SELECT({name: name});
+// } else {
+// select = SELECT(name);
+// }
+//
+// function addOption(value, content) {
+// var opt = OPTION({value: value}, content || value);
+// if (value == selected) {
+// opt.attribs.selected = "selected";
+// }
+// select.push(opt);
+// }
+//
+// if (options instanceof Array) {
+// options.forEach(f_limitArgs(this, addOption, 1));
+// } else {
+// eachProperty(options, addOption);
+// }
+// return select;
+// }
+//
+// function getCategorizedStats() {
+// var statnames = statistics.getAllStatNames();
+// var categories = {}
+// statnames.forEach(function(sn) {
+// var category = statistics.getStatData(sn).category
+// if (! categories[category]) {
+// categories[category] = [];
+// }
+// categories[category].push(statistics.getStatData(sn));
+// });
+// return categories;
+// }
+//
+// function render_ajax() {
+// var categoriesToStats = getCategorizedStats();
+//
+// eachProperty(categoriesToStats, function(catName, statArray) {
+// categoriesToStats[catName] = statArray.map(function(statObject) {
+// return {
+// specialState: "",
+// displayName: statObject.name,
+// name: statObject.name,
+// data: liveStatDisplayHtml(statObject)
+// }
+// })
+// });
+//
+// renderHtml('statistics/stat_page.ejs',
+// {eachProperty: eachProperty,
+// statCategoryNames: keys(categoriesToStats),
+// categoriesToStats: categoriesToStats });
+// }
+
+// function render_main() {
+// var body = BODY();
+//
+// var statNames = statistics.getAllStatNames(); //getStatsForCategory(request.params.cat);
+// statNames.forEach(function(sn) {
+// renderStat(body, sn);
+// });
+// response.write(body);
+// }
+//
+// var descriptions = {
+// execution_latencies: ", mean response time in milliseconds",
+// static_file_latencies: ", mean response time in milliseconds",
+// pad_startup_times: ", max response time in milliseconds of fastest N% of requests"
+// };
+//
+// function liveStatDisplayHtml(statObject) {
+// var displayData = statistics.liveSnapshot(statObject);
+// switch (statObject.plotType) {
+// case 'line':
+// return displayData;
+// case 'topValues':
+// var data = {}
+// eachProperty(displayData, function(timescale, tsdata) {
+// data[timescale] = ""
+// var top = tsdata.topValues;
+// for (var i = 0; i < Math.min(_topN(), top.length); ++i) {
+// data[timescale] += [B(top[i].count), ": ", top[i].value, BR()].map(toHTML).join("");
+// }
+// if (data[timescale] == "") {
+// data[timescale] = "(no data)";
+// }
+// });
+// return data;
+// case 'histogram':
+// var imgs = {}
+// eachProperty(displayData, function(timescale, tsdata) {
+// var percentiles = tsdata;
+// var pcts = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
+// var max = percentiles["100"] || 1000;
+// imgs[timescale] =
+// toHTML(IMG({src: "http://chart.apis.google.com/chart?chs=400x100&cht=bvs&chd=t:"+
+// pcts.map(function(pct) { return Math.round(percentiles[""+pct]/max*100); }).join(",")+
+// "&chxt=x,y&chxl=0:|"+
+// pcts.map(function(pct) { return ""+pct+"%"; }).join("|")+
+// "&chxr=0,0,100|1,0,"+max+""}));
+// });
+// return imgs;
+// }
+// }
+//
+// function renderStat(body, statName) {
+// var div = DIV({style: 'float: left; text-align: center; margin: 3px; border: 1px solid black;'})
+// div.push(P(statName, descriptions[statName] || ""));
+// if (_showLiveStats()) {
+// var data = statistics.getStatData(statName);
+// var displayData = statistics.liveSnapshot(data);
+// var t = TABLE();
+// var tcount = 0;
+// ["minute", "hour", "fourHour", "day"].forEach(function(timescale) {
+// if (! _showTimescale(timescale)) { return; }
+// var tr = TR();
+// t.push(tr);
+// tr.push(TD("last ", timescale));
+// var td = TD();
+// tr.push(td);
+// switch (data.plotType) {
+// case 'line':
+// td.push(B(displayData[timescale])); break;
+// case 'topValues':
+// var top = displayData[timescale].topValues;
+// if (tcount != 0) {
+// tr[0].attribs.style = td.attribs.style = "border-top: 1px solid gray;";
+// }
+// // println(statName+" / top length: "+top.length);
+// for (var i = 0; i < Math.min(_topN(), top.length); ++i) {
+// td.push(B(top[i].count), ": ", top[i].value, BR());
+// }
+// break;
+// case 'histogram':
+// var percentiles = displayData[timescale];
+// var pcts = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
+// var max = percentiles["100"] || 1000;
+// td.push(IMG({src: "http://chart.apis.google.com/chart?chs=340x100&cht=bvs&chd=t:"+
+// pcts.map(function(pct) { return Math.round(percentiles[""+pct]/max*100); }).join(",")+
+// "&chxt=x,y&chxl=0:|"+
+// pcts.map(function(pct) { return ""+pct+"%"; }).join("|")+
+// "&chxr=0,0,100|1,0,"+max+""}))
+// // td.push("50%: ", B(percentiles["50"]), " ",
+// // "90%: ", B(percentiles["90"]), " ",
+// // "max: ", B(percentiles["100"]));
+// break;
+// }
+// tcount++;
+// });
+// div.push(t)
+// }
+// if (_showHistStats()) {
+// div.push(A({href: '/ep/admin/usagestats/graph?size=1080x420&statName='+statName},
+// IMG({src: '/ep/admin/usagestats/graph?size=400x200&statName='+statName,
+// style: 'border: 1px solid #ccc; margin: 10px 0 0 20px;'})),
+// BR(),
+// DIV({style: 'text-align: right;'},
+// A({style: 'text-decoration: none; font-size: .8em;',
+// href: '/ep/admin/usagestats/data?statName='+statName}, "(data)")));
+// }
+// body.push(div);
+// }
+//
+// function render_graph() {
+// var sn = request.params.statName;
+// if (!sn) {
+// render404();
+// }
+// usage_stats.respondWithGraph(sn);
+// }
+//
+//
+// function render_exceptions() {
+// var logNames = ["frontend/exception", "backend/exceptions"];
+// }
+
+// function render_updatehistory() {
+//
+// sqlcommon.withConnection(function(conn) {
+// var stmnt = "delete from statistics;";
+// var s = conn.createStatement();
+// sqlcommon.closing(s, function() {
+// s.execute(stmnt);
+// });
+// });
+//
+// var processed = {};
+//
+// function _domonth(y, m) {
+// for (var i = 0; i < 32; i++) {
+// _processStatsDay(y, m, i, processed);
+// }
+// }
+//
+// _domonth(2008, 10);
+// _domonth(2008, 11);
+// _domonth(2008, 12);
+// _domonth(2009, 1);
+// _domonth(2009, 2);
+// _domonth(2009, 3);
+// _domonth(2009, 4);
+// _domonth(2009, 5);
+// _domonth(2009, 6);
+// _domonth(2009, 7);
+//
+// response.redirect('/ep/admin/usagestats');
+// }
+
+// function _processStatsDay(year, month, date, processed) {
+// var now = new Date();
+// var day = new Date();
+//
+// for (var i = 0; i < 10; i++) {
+// day.setFullYear(year);
+// day.setDate(date);
+// day.setMonth(month-1);
+// }
+//
+// if ((+day < +now) &&
+// (!((day.getFullYear() == now.getFullYear()) &&
+// (day.getMonth() == now.getMonth()) &&
+// (day.getDate() == now.getDate())))) {
+//
+// var dayNoon = statistics.noon(day);
+//
+// if (processed[dayNoon]) {
+// return;
+// } else {
+// statistics.processLogDay(new Date(dayNoon));
+// processed[dayNoon] = true;
+// }
+// } else {
+// /* nothing */
+// }
+// }
+
diff --git a/etherpad/src/etherpad/control/store/eepnet_checkout_control.js b/etherpad/src/etherpad/control/store/eepnet_checkout_control.js
new file mode 100644
index 0000000..ddd4973
--- /dev/null
+++ b/etherpad/src/etherpad/control/store/eepnet_checkout_control.js
@@ -0,0 +1,757 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("email.sendEmail");
+import("fastJSON");
+import("funhtml.*");
+import("jsutils.*");
+import("sqlbase.sqlobj");
+import("stringutils");
+import("sync");
+
+import("etherpad.billing.billing");
+import("etherpad.billing.fields");
+import("etherpad.globals");
+import("etherpad.globals.*");
+import("etherpad.helpers");
+import("etherpad.licensing");
+import("etherpad.pro.pro_utils");
+import("etherpad.sessions.{getSession,getTrackingId,getSessionId}");
+import("etherpad.store.checkout");
+import("etherpad.store.eepnet_checkout");
+import("etherpad.utils.*");
+
+import("static.js.billing_shared.{billing=>billingJS}");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+
+var STORE_URL = '/ep/store/eepnet-checkout/';
+
+var _pageSequence = [
+ ['purchase', "Number of Users", true],
+ ['support-contract', "Support Contract", true],
+ ['license-info', "License Information", true],
+ ['billing-info', "Billing Information", true],
+ ['confirmation', "Confirmation", false]
+];
+
+var _specialPages = {
+ 'receipt': ['receipt', "Receipt", false]
+}
+
+//----------------------------------------------------------------
+
+function _cart() {
+ return getSession().eepnetCart;
+}
+
+function _currentPageSegment() {
+ return request.path.split('/')[4];
+}
+
+function _currentPageId() {
+ return _applyToCurrentPageSequenceEntry(function(ps) { return ps[0]; });
+}
+
+function _applyToCurrentPageSequenceEntry(f) {
+ for (var i = 0; i < _pageSequence.length; i++) {
+ if (_pageSequence[i][0] == _currentPageSegment()) {
+ return f(_pageSequence[i], i, true);
+ }
+ }
+ if (_specialPages[_currentPageSegment()]) {
+ return f(_specialPages[_currentPageSegment()], -1, false);
+ }
+ return undefined;
+}
+
+function _currentPageIndex() {
+ return _applyToCurrentPageSequenceEntry(function(ps, i) { return i; });
+}
+
+function _currentPageTitle() {
+ return _applyToCurrentPageSequenceEntry(function(ps) { return ps[1]; });
+}
+
+function _currentPageShowCart() {
+ return _applyToCurrentPageSequenceEntry(function(ps) { return ps[2]; });
+}
+
+function _currentPageInFlow() {
+ return _applyToCurrentPageSequenceEntry(function(ps, i, isSpecial) { return isSpecial });
+}
+
+function _pageId(d) {
+ return _applyToCurrentPageSequenceEntry(function(ps, i) {
+ if (_pageSequence[i+d]) {
+ return _pageSequence[i+d][0];
+ }
+ });
+}
+
+function _nextPageId() { return _pageId(+1); }
+function _prevPageId() { return _pageId(-1); }
+
+function _advancePage() {
+ response.redirect(_pathTo(_nextPageId()));
+}
+
+function _pathTo(id) {
+ return STORE_URL+id;
+}
+
+// anything starting with 'billing' is also ok.
+function _isAutomaticallySetParam(p) {
+ var _automaticallySetParams = arrayToSet([
+ 'numUsers', 'couponCode', 'supportContract',
+ 'email', 'ownerName', 'orgName', 'licenseAgreement'
+ ]);
+
+ return _automaticallySetParams[p] || stringutils.startsWith(p, "billing");
+}
+
+function _lastSubmittedPage() {
+ var cart = _cart();
+ return isNaN(cart.lastSubmittedPage) ? -1 : Number(cart.lastSubmittedPage);
+}
+
+function _shallowSafeCopy(obj) {
+ return billing.clearKeys(obj, [
+ {name: 'billingCCNumber',
+ valueTest: function(s) { return /^\d{15,16}$/.test(s) },
+ valueReplace: billing.replaceWithX },
+ {name: 'billingCSC',
+ valueTest: function(s) { return /^\d{3,4}$/.test(s) },
+ valueReplace: billing.replaceWithX }]);
+}
+
+function onRequest() {
+ billing.log({
+ 'type': "billing-request",
+ 'date': +(new Date),
+ 'method': request.method,
+ 'path': request.path,
+ 'query': request.query,
+ 'host': request.host,
+ 'scheme': request.scheme,
+ 'params': _shallowSafeCopy(request.params),
+ 'cart': _shallowSafeCopy(_cart())
+ });
+ if (request.path == STORE_URL+"paypalnotify") {
+ _handlePaypalNotification();
+ }
+ if (request.path == STORE_URL+"paypalredirect") {
+ _handlePayPalRedirect();
+ }
+ var cart = _cart();
+ if (!cart || request.params.clearcart) {
+ getSession().eepnetCart = {
+ lastSubmittedPage: -1,
+ invoiceId: billing.createInvoice()
+ };
+ if (request.params.clearcart) {
+ response.redirect(request.path);
+ }
+ if (_currentPageId() != 'purchase') {
+ response.redirect(_pathTo('purchase'));
+ }
+ cart = _cart();
+ }
+ if (request.params.invoice) {
+ cart.billingPurchaseType = 'invoice';
+ }
+ if (cart.purchaseComplete && _currentPageId() != 'receipt') {
+ cart.showStartOverMessage = true;
+ response.redirect(_pathTo('receipt'));
+ }
+ // somehow user got too far?
+ if (_currentPageIndex() > _lastSubmittedPage() + 1) {
+ response.redirect(_pathTo(_pageSequence[_lastSubmittedPage()+1][0]));
+ }
+ if (request.isGet) {
+ // see if this is a standard cart-page get
+ if (_currentPageId()) {
+ _renderCartPage();
+ return true;
+ }
+ }
+ if (request.isPost) {
+ // add params to cart
+ eachProperty(request.params, function(k,v) {
+ if (! _isAutomaticallySetParam(k)) { return; }
+ if (k == "billingCCNumber" && v.charAt(0) == 'X') { return; }
+ cart[k] = stringutils.toHTML(v);
+ });
+ if (_currentPageId() == 'license-info' && ! request.params.licenseAgreement) {
+ delete cart.licenseAgreement;
+ }
+ if (_currentPageIndex() > cart.lastSubmittedPage) {
+ cart.lastSubmittedPage = _currentPageIndex();
+ }
+ }
+ if (request.params.backbutton) {
+ _updateCosts();
+ response.redirect(_pathTo(_prevPageId()));
+ }
+ return false; // commence auto-dispatch
+}
+
+function _getCoupon(code) {
+ return sqlobj.selectSingle('checkout_referral', {id: code});
+}
+
+function _supportCost() {
+ var cart = _cart();
+ return Math.max(eepnet_checkout.SUPPORT_MIN_COST, eepnet_checkout.SUPPORT_COST_PCT/100*cart.baseCost);
+}
+
+function _discountedSupportCost() {
+ var cart = _cart();
+ if ('couponSupportPctDiscount' in cart) {
+ return _supportCost() -
+ (cart.couponSupportPctDiscount ?
+ cart.couponSupportPctDiscount/100 * _supportCost() :
+ 0);
+ }
+}
+
+function _updateCosts() {
+ var cart = _cart();
+
+ if (cart.numUsers) {
+ cart.numUsers = Number(cart.numUsers);
+
+ cart.baseCost = cart.numUsers * eepnet_checkout.COST_PER_USER;
+
+ if (cart.supportContract == "true") {
+ cart.supportCost = _supportCost();
+ } else {
+ delete cart.supportCost;
+ }
+
+ var coupon = _getCoupon(cart.couponCode);
+ if (coupon) {
+ for (i in coupon) {
+ cart["coupon"+stringutils.makeTitle(i)] = coupon[i];
+ }
+ cart.coupon = coupon;
+ } else {
+ for (i in cart.coupon) {
+ delete cart["coupon"+stringutils.makeTitle(i)];
+ }
+ delete cart.coupon;
+ }
+
+ if (cart.couponProductPctDiscount) {
+ cart.productReferralDiscount =
+ cart.couponProductPctDiscount/100 * cart.baseCost;
+ } else {
+ delete cart.productReferralDiscount;
+ }
+ if (cart.couponSupportPctDiscount) {
+ cart.supportReferralDiscount =
+ cart.couponSupportPctDiscount/100 * (cart.supportCost || 0);
+ } else {
+ delete cart.supportReferralDiscount;
+ }
+ cart.subTotal =
+ cart.baseCost - (cart.productReferralDiscount || 0) +
+ (cart.supportCost || 0) - (cart.supportReferralDiscount || 0);
+
+ if (cart.couponTotalPctDiscount) {
+ cart.totalReferralDiscount =
+ cart.couponTotalPctDiscount/100 * cart.subTotal;
+ } else {
+ delete cart.totalReferralDiscount;
+ }
+
+ if (cart.couponFreeUsersCount || cart.couponFreeUsersPct) {
+ cart.freeUserCount =
+ Math.round(cart.couponFreeUsersCount +
+ cart.couponFreeUsersPct/100 * cart.numUsers);
+ } else {
+ delete cart.freeUserCount;
+ }
+ cart.userCount = Number(cart.numUsers) + Number(cart.freeUserCount || 0);
+
+ cart.total =
+ cart.subTotal - (cart.totalReferralDiscount || 0);
+ }
+}
+
+//----------------------------------------------------------------
+// template helper functions
+//----------------------------------------------------------------
+
+function _cartDebug() {
+ if (globals.isProduction()) {
+ return '';
+ }
+
+ var d = DIV({style: 'font-family: monospace; font-size: 1em; border: 1px solid #ccc; padding: 1em; margin: 1em;'});
+ d.push(H3({style: "font-size: 1.5em; font-weight: bold;"}, "Debug Info:"));
+ var t = TABLE({border: 1, cellspacing: 0, cellpadding: 4});
+ keys(_cart()).sort().forEach(function(k) {
+ var v = _cart()[k];
+ if (typeof(v) == 'object' && v != null) {
+ v = v.toSource();
+ }
+ t.push(TR(TD({style: 'padding: 2px 6px;', align: 'right'}, k),
+ TD({style: 'padding: 2px 6px;', align: 'left'}, v)));
+ });
+ d.push(t);
+ return d;
+}
+
+var billingButtonName = "Review Order";
+
+function _templateContext(extra) {
+ var cart = _cart();
+
+ var pageId = _currentPageId();
+
+ var ret = {
+ cart: cart,
+ costPerUser: eepnet_checkout.COST_PER_USER,
+ supportCostPct: eepnet_checkout.SUPPORT_COST_PCT,
+ supportMinCost: eepnet_checkout.SUPPORT_MIN_COST,
+ errorIfInvalid: _errorIfInvalid,
+ dollars: checkout.dollars,
+ countryList: fields.countryList,
+ usaStateList: fields.usaStateList,
+ obfuscateCC: checkout.obfuscateCC,
+ helpers: helpers,
+ inFlow: _currentPageInFlow(),
+ displayCart: _displayCart,
+ displaySummary: _displaySummary,
+ pathTo: _pathTo,
+ billing: billingJS,
+ handlePayPalRedirect: _handlePayPalRedirect,
+ supportCost: _supportCost,
+ discountedSupportCost: _discountedSupportCost,
+ billingButtonName: billingButtonName,
+ billingFinalPhrase: "<p>You will not be charged until you review"+
+ " and confirm your order on the next page.</p>",
+ getFullSuperdomainHost: pro_utils.getFullSuperdomainHost,
+ showCouponCode: false
+ };
+ eachProperty(extra, function(k, v) {
+ ret[k] = v;
+ });
+ return ret;
+}
+
+function _displayCart(cartid, editable) {
+ return renderTemplateAsString('store/eepnet-checkout/cart.ejs', _templateContext({
+ shoppingcartid: cartid || "shoppingcart",
+ editable: editable
+ }));
+}
+
+function _displaySummary(editable) {
+ return renderTemplateAsString('store/eepnet-checkout/summary.ejs', _templateContext({
+ editable: editable
+ }));
+}
+
+function _renderCartPage() {
+ var cart = _cart();
+
+ var pageId = _currentPageId();
+ var title = _currentPageTitle();
+
+ function _getContent() {
+ return renderTemplateAsString('store/eepnet-checkout/'+pageId+'.ejs', _templateContext());
+ }
+
+ renderFramed('store/eepnet-checkout/checkout-template.ejs', {
+ cartDebug: _cartDebug,
+ errorDiv: _errorDiv,
+ pageId: pageId,
+ getContent: _getContent,
+ title: title,
+ inFlow: _currentPageInFlow(),
+ displayCart: _displayCart,
+ showCart: _currentPageShowCart(),
+ cart: cart,
+ billingButtonName: billingButtonName
+ });
+
+ // clear errors
+ delete cart.errorMsg;
+ delete cart.errorId;
+}
+
+function _errorDiv() {
+ var m = _cart().errorMsg;
+ if (m) {
+ return DIV({className: 'errormsg', id: 'errormsg'}, m);
+ } else {
+ return '';
+ }
+}
+
+function _errorIfInvalid(id) {
+ var e = _cart().errorId
+ if (e && e[id]) {
+ return 'error';
+ } else {
+ return '';
+ }
+}
+
+function _validationError(id, msg, pageId) {
+ var cart = _cart();
+ cart.errorMsg = msg;
+ cart.errorId = {};
+ if (id instanceof Array) {
+ id.forEach(function(k) {
+ cart.errorId[k] = true;
+ });
+ } else {
+ cart.errorId[id] = true;
+ }
+ if (pageId) {
+ response.redirect(_pathTo(pageId));
+ }
+ response.redirect(request.path);
+}
+
+//--------------------------------------------------------------------------------
+// main
+//--------------------------------------------------------------------------------
+
+function render_main() {
+ response.redirect(STORE_URL+'purchase');
+}
+
+//--------------------------------------------------------------------------------
+// cart
+//--------------------------------------------------------------------------------
+
+function render_purchase_post() {
+ var cart = _cart();
+
+ // validate numUsers and couponCode
+ if (! checkout.isOnlyDigits(cart.numUsers)) {
+ _validationError("numUsers", "Please enter a valid number of users.");
+ }
+ if (Number(cart.numUsers) < 1) {
+ _validationError("numUsers", "Please specify at least one user.");
+ }
+
+ if (cart.couponCode && (cart.couponCode.length != 8 || ! _getCoupon(cart.couponCode))) {
+ _validationError("couponCode", "That coupon code does not appear to be valid.");
+ }
+
+ _updateCosts();
+ _advancePage();
+}
+
+//--------------------------------------------------------------------------------
+// support-contract
+//--------------------------------------------------------------------------------
+
+function render_support_contract_post() {
+ var cart = _cart();
+
+ if (cart.supportContract != "true" && cart.supportContract != "false") {
+ _validationError("supportContract", "Please select one of the options.");
+ }
+
+ _updateCosts();
+ _advancePage();
+}
+
+//--------------------------------------------------------------------------------
+// license-info
+//--------------------------------------------------------------------------------
+
+function render_license_info_post() {
+ var cart = _cart();
+
+ if (!isValidEmail(cart.email)) {
+ _validationError("email", "That email address does not look valid.");
+ }
+ if (!cart.ownerName) {
+ _validationError("ownerName", "Please enter a license owner name.");
+ }
+ if (!cart.orgName) {
+ _validationError("orgName", "Please enter an organization name.");
+ }
+ if (!cart.licenseAgreement) {
+ _validationError("licenseAgreement", "You must agree to the terms of the license to purchase EtherPad PNE.");
+ }
+
+ if ((! cart.billingFirstName) && ! (cart.billingLastName)) {
+ var nameParts = cart.ownerName.split(/\s+/);
+ if (nameParts.length == 1) {
+ cart.billingFirstName = nameParts[0];
+ } else {
+ cart.billingLastName = nameParts[nameParts.length-1];
+ cart.billingFirstName = nameParts.slice(0, nameParts.length-1).join(' ');
+ }
+ }
+
+ _updateCosts();
+ _advancePage();
+}
+
+//--------------------------------------------------------------------------------
+// billing-info
+//--------------------------------------------------------------------------------
+
+function render_billing_info_post() {
+ var cart = _cart();
+
+ checkout.validateBillingCart(_validationError, cart);
+ if (cart.billingPurchaseType == 'paypal') {
+ _beginPaypalPurchase();
+ }
+
+ _updateCosts();
+ _advancePage();
+}
+
+function _absoluteUrl(id) {
+ return request.scheme+"://"+request.host+_pathTo(id);
+}
+
+function _beginPaypalPurchase() {
+ _updateCosts();
+
+ var cart = _cart();
+
+ var purchase = _generatePurchaseRecord();
+ var result =
+ billing.beginExpressPurchase(cart.invoiceId, cart.customerId,
+ "EEPNET", cart.total || 0.01, cart.couponCode || "",
+ _absoluteUrl('paypalredirect?status=ok'),
+ _absoluteUrl('paypalredirect?status=fail'),
+ _absoluteUrl('paypalnotify'));
+ if (result.status != 'success') {
+ _validationError("billingPurchaseType",
+ "PayPal purchase not available at the moment. "+
+ "Please try again later, or try using a different payment option.");
+ }
+ cart.paypalPurchaseInfo = result.purchaseInfo;
+ response.redirect(billing.paypalPurchaseUrl(result.purchaseInfo.token));
+}
+
+//--------------------------------------------------------------------------------
+// confirmation
+//--------------------------------------------------------------------------------
+
+function _handlePaypalNotification() {
+ var ret = billing.handlePaypalNotification();
+ if (ret.status == 'completion') {
+ var purchaseInfo = ret.purchaseInfo;
+ var eepnetPurchase = eepnet_checkout.getPurchaseByInvoiceId(purchaseInfo.invoiceId);
+ var fakeCart = {
+ ownerName: eepnetPurchase.owner,
+ orgName: eepnetPurchase.organization,
+ email: eepnetPurchase.emails,
+ customerId: eepnetPurchase.id,
+ userCount: eepnetPurchase.numUsers,
+ receiptEmail: eepnetPurchase.receiptEmail,
+ }
+ eepnet_checkout.generateLicenseKey(fakeCart);
+ eepnet_checkout.sendReceiptEmail(fakeCart);
+ eepnet_checkout.sendLicenseEmail(fakeCart);
+ billing.log({type: 'purchase-complete', dollars: purchaseInfo.cost});
+ }
+}
+
+function _handlePayPalRedirect() {
+ var cart = _cart();
+
+ if (request.params.status == 'ok' && cart.paypalPurchaseInfo) {
+ var result = billing.continueExpressPurchase(cart.paypalPurchaseInfo);
+ if (result.status == 'success') {
+ cart.paypalPayerInfo = result.payerInfo;
+ response.redirect(_pathTo('confirmation'));
+ } else {
+ _validationError("billingPurchaseType",
+ "There was an error processing your payment through PayPal. "+
+ "Please try again later, or use a different payment option.",
+ 'billing-info');
+ }
+ } else {
+ _validationError("billingPurchaseType",
+ "PayPal payment didn't go through. "+
+ "Please try again later, or use a different payment option.",
+ 'billing-info');
+ }
+}
+
+function _recordPurchase(p) {
+ return sqlobj.insert("checkout_purchase", p);
+}
+
+function _generatePurchaseRecord() {
+ var cart = _cart();
+
+ if (! cart.invoiceId) {
+ throw Error("No invoice id!");
+ }
+
+ var purchase = {
+ invoiceId: cart.invoiceId,
+ email: cart.email,
+ firstName: cart.billingFirstName,
+ lastName: cart.billingLastName,
+ owner: cart.ownerName || "",
+ organization: cart.orgName || "",
+ addressLine1: cart.billingAddressLine1 || "",
+ addressLine2: cart.billingAddressLine2 || "",
+ city: cart.billingCity || "",
+ state: cart.billingState || "",
+ zip: cart.billingZipCode || "",
+ referral: cart.couponCode,
+ cents: cart.total*100, // cents here.
+ numUsers: cart.userCount,
+ purchaseType: cart.billingPurchaseType,
+ }
+ cart.customerId = _recordPurchase(purchase);
+ return purchase;
+}
+
+function _performCreditCardPurchase() {
+ var cart = _cart();
+ var purchase = _generatePurchaseRecord();
+ var payInfo = checkout.generatePayInfo(cart);
+
+ // log everything but the CVV, which we're not allowed to store
+ // any longer than it takes to process this transaction.
+ var savedCvv = payInfo.cardCvv;
+ delete payInfo.cardCvv;
+ checkout.writeToEncryptedLog(fastJSON.stringify({date: String(new Date()), purchase: purchase, customerId: cart.customerId, payInfo: payInfo}));
+ payInfo.cardCvv = savedCvv;
+
+ var result =
+ billing.directPurchase(cart.invoiceId, cart.customerId,
+ "EEPNET", cart.total || 0.01,
+ cart.couponCode || "",
+ payInfo, _absoluteUrl('paypalnotify'));
+
+ if (result.status == 'success') {
+ cart.status = 'success';
+ cart.purchaseComplete = true;
+ eepnet_checkout.generateLicenseKey(cart);
+ eepnet_checkout.sendReceiptEmail(cart);
+ eepnet_checkout.sendLicenseEmail(cart);
+ billing.log({type: 'purchase-complete', dollars: cart.total,
+ email: cart.email, user: cart.ownerName,
+ org: cart.organization});
+ // TODO: generate key and include in receipt page, and add to purchase table.
+ } else if (result.status == 'pending') {
+ cart.status = 'pending';
+ cart.purchaseComplete = true;
+ eepnet_checkout.sendReceiptEmail(cart);
+ // save the receipt email text to resend later.
+ eepnet_checkout.updatePurchaseWithReceipt(cart.customerId,
+ eepnet_checkout.receiptEmailText(cart));
+ } else if (result.status == 'failure') {
+ var paypalResult = result.debug;
+ billing.log({'type': 'FATAL', value: "Direct purchase failed on paypal.", cart: cart, paypal: paypalResult});
+ if (result.errorField.permanentErrors[0] == 'invoiceId') {
+ // repeat invoice id. damnit, this is bad.
+ sendEmail('support@pad.spline.inf.fu-berlin.de', 'urgent@pad.spline.inf.fu-berlin.de', 'DUPLICATE INVOICE WARNING!', {},
+ "Hey,\n\nThis is a billing system error. The EEPNET checkout tried to make a "+
+ "purchase with PayPal and got a duplicate invoice error on invoice ID "+cart.invoiceId+
+ ".\n\nUnless you're expecting this (or recently ran a selenium test, or have reason to "+
+ "believe this isn't an exceptional condition, please look into this "+
+ "and get back to the user ASAP!\n\n"+fastJSON.stringify(cart));
+ _validationError('', "Your payment was processed, but we cannot proceed. "+
+ "You will hear from us shortly via email. (If you don't hear from us "+
+ "within 24 hours, please email <a href='mailto:sales@pad.spline.inf.fu-berlin.de'>"+
+ "sales@pad.spline.inf.fu-berlin.de</a>.)");
+ }
+ checkout.validateErrorFields(function(x, y) { _validationError(x, y, 'billing-info') }, "There seems to be an error in your billing information."+
+ " Please verify and correct your ",
+ result.errorField.userErrors);
+ checkout.validateErrorFields(function(x, y) { _validationError(x, y, 'billing-info') }, "The bank declined your billing information. Please try a different ",
+ result.errorField.permanentErrors);
+ _validationError('', "A temporary error has prevented processing of your payment. Please try again later.");
+ } else {
+ billing.log({'type': 'FATAL', value: "Unknown error: "+result.status+" - debug: "+result.debug});
+ sendEmail('support@pad.spline.inf.fu-berlin.de', 'urgent@pad.spline.inf.fu-berlin.de', 'UNKNOWN ERROR WARNING!', {},
+ "Hey,\n\nThis is a billing system error. Some unknown error occurred. "+
+ "This shouldn't ever happen. Probably good to let J.D. know. <grin>\n\n"+
+ fastJSON.stringify(cart));
+ _validationError('', "An unknown error occurred. We're looking into it!")
+ }
+}
+
+function _completePaypalPurchase() {
+ var cart = _cart();
+ var purchaseInfo = cart.paypalPurchaseInfo;
+ var payerInfo = cart.paypalPayerInfo;
+
+ var result = billing.completeExpressPurchase(purchaseInfo, payerInfo, _absoluteUrl('paypalnotify'));
+ if (result.status == 'success') {
+ cart.status = 'success';
+ cart.purchaseComplete = true;
+ eepnet_checkout.generateLicenseKey(cart);
+ eepnet_checkout.sendReceiptEmail(cart);
+ eepnet_checkout.sendLicenseEmail(cart);
+ billing.log({type: 'purchase-complete', dollars: cart.total,
+ email: cart.email, user: cart.ownerName,
+ org: cart.organization});
+
+ } else if (result.status == 'pending') {
+ cart.status = 'pending';
+ cart.purchaseComplete = true;
+ eepnet_checkout.sendReceiptEmail(cart);
+ // save the receipt email text to resend later.
+ eepnet_checkout.updatePurchaseWithReceipt(cart.customerId,
+ eepnet_checkout.receiptEmailText(cart));
+ } else {
+ billing.log({'type': 'FATAL', value: "Paypal failed.", cart: cart, paypal: paypalResult});
+ _validationError("billingPurchaseType",
+ "There was an error processing your payment through PayPal. "+
+ "Please try again later, or use a different payment option.",
+ 'billing-info');
+ }
+}
+
+function _showReceipt() {
+ response.redirect(_pathTo('receipt'));
+}
+
+function render_confirmation_post() {
+ var cart = _cart();
+
+ _updateCosts(); // no fishy business, please.
+
+ if (cart.billingPurchaseType == 'creditcard') {
+ _performCreditCardPurchase();
+ _showReceipt();
+ } else if (cart.billingPurchaseType == 'paypal') {
+ _completePaypalPurchase();
+ _showReceipt();
+ }
+}
+
+//--------------------------------------------------------------------------------
+// receipt
+//--------------------------------------------------------------------------------
+
+function render_receipt_post() {
+ response.redirect(request.path);
+}
diff --git a/etherpad/src/etherpad/control/store/storecontrol.js b/etherpad/src/etherpad/control/store/storecontrol.js
new file mode 100644
index 0000000..43569e4
--- /dev/null
+++ b/etherpad/src/etherpad/control/store/storecontrol.js
@@ -0,0 +1,201 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("dispatch.{Dispatcher,DirMatcher,forward}");
+import("fastJSON");
+import("funhtml.*");
+
+import('etherpad.globals.*');
+import("etherpad.store.eepnet_trial");
+import("etherpad.store.eepnet_checkout");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+
+import("etherpad.control.store.eepnet_checkout_control");
+import("etherpad.control.pro.admin.team_billing_control");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+
+function onStartup() {}
+
+function onRequest() {
+ var disp = new Dispatcher();
+ disp.addLocations([
+ [DirMatcher('/ep/store/eepnet-checkout/'), forward(eepnet_checkout_control)],
+ ]);
+ return disp.dispatch();
+}
+
+//----------------------------------------------------------------
+
+function render_main() {
+ response.redirect("/ep/about/pricing");
+}
+
+//----------------------------------------------------------------
+// Flow goes through these 4 pages in order:
+//----------------------------------------------------------------
+
+function render_eepnet_eval_signup_get() {
+ renderFramed("store/eepnet_eval_signup.ejs", {
+ trialDays: eepnet_trial.getTrialDays(),
+ oldData: (getSession().pricingContactData || {}),
+ sfIndustryList: eepnet_trial.getSalesforceIndustryList()
+ });
+ delete getSession().errorMsg;
+}
+
+// function render_eepnet_eval_signup_post() {
+// response.setContentType("text/plain; charset=utf-8");
+// var data = {};
+// var fields = ['firstName', 'lastName', 'email', 'orgName',
+// 'jobTitle', 'phone', 'estUsers', 'industry'];
+//
+// if (!getSession().pricingContactData) {
+// getSession().pricingContactData = {};
+// }
+//
+// function _redirectErr(msg) {
+// response.write(fastJSON.stringify({error: msg}));
+// response.stop();
+// }
+//
+// fields.forEach(function(f) {
+// getSession().pricingContactData[f] = request.params[f];
+// });
+//
+// fields.forEach(function(f) {
+// data[f] = request.params[f];
+// if (!(data[f] && (data[f].length > 0))) {
+// _redirectErr("All fields are required.");
+// }
+// });
+//
+// // validate email
+// if (!isValidEmail(data.email)) {
+// _redirectErr("That email address doesn't look valid.");
+// }
+//
+// // check that email not already registered.
+// if (eepnet_trial.hasEmailAlreadyDownloaded(data.email)) {
+// _redirectErr("That email has already downloaded a free trial."+
+// ' <a href="/ep/store/eepnet-recover-license">Recover a lost license key here</a>.');
+// }
+//
+// // Looks good! Create and email license key...
+// eepnet_trial.createAndMailNewLicense(data);
+// getSession().message = "A license key has been sent to "+data.email;
+//
+// // Generate web2lead info and return it
+// var web2leadData = eepnet_trial.getWeb2LeadData(data, request.clientAddr, getSession().initialReferer);
+// response.write(fastJSON.stringify(web2leadData));
+// }
+//
+// function render_salesforce_web2lead_ok() {
+// renderFramedHtml([
+// '<script>',
+// 'top.location.href = "'+request.scheme+'://'+request.host+'/ep/store/eepnet-download";',
+// '</script>'
+// ].join('\n'));
+// }
+//
+// function render_eepnet_eval_download() {
+// // NOTE: keep this URL around for historical reasons?
+// response.redirect("/ep/store/eepnet-download");
+// }
+//
+// function render_eepnet_download() {
+// renderFramed("store/eepnet_download.ejs", {
+// message: (getSession().message || null),
+// versionString: (PNE_RELEASE_VERSION+"&nbsp;("+PNE_RELEASE_DATE +")")
+// });
+// delete getSession().message;
+// }
+//
+// function render_eepnet_download_zip() {
+// response.redirect("/static/zip/pne-release/etherpad-pne-"+PNE_RELEASE_VERSION+".zip");
+// }
+//
+// function render_eepnet_download_nextsteps() {
+// renderFramed("store/eepnet_eval_nextsteps.ejs");
+// }
+
+//----------------------------------------------------------------
+// recover a lost license
+//----------------------------------------------------------------
+function render_eepnet_recover_license_get() {
+ var d = DIV({className: "fpcontent"});
+
+ d.push(P("Recover your lost license key."));
+
+ if (getSession().message) {
+ d.push(DIV({id: "resultmsg",
+ style: "border: 1px solid #333; padding: 0 1em; background: #efe; margin: 1em 0;"}, getSession().message));
+ delete getSession().message;
+ }
+ if (getSession().error) {
+ d.push(DIV({id: "errormsg",
+ style: "border: 1px solid red; padding: 0 1em; background: #fee; margin: 1em 0;"}, getSession().error));
+ delete getSession().error;
+ }
+
+ d.push(FORM({style: "border: 1px solid #222; padding: 2em; background: #eee;",
+ action: request.path, method: "post"},
+ LABEL({htmlFor: "email"},
+ "Your email address:"),
+ INPUT({type: "text", name: "email", id: "email"}),
+ INPUT({type: "submit", id: "submit", value: "Submit"})));
+
+ renderFramedHtml(d);
+}
+
+function render_eepnet_recover_license_post() {
+ var email = request.params.email;
+ if (!eepnet_trial.hasEmailAlreadyDownloaded(email) && !eepnet_trialhasEmailAlreadyPurchased(email)) {
+ getSession().error = P("License not found for email: \"", email, "\".");
+ response.redirect(request.path);
+ }
+ if (eepnet_checkout.hasEmailAlreadyPurchased(email)) {
+ eepnet_checkout.mailLostLicense(email);
+ } else if (eepnet_trial.hasEmailAlreadyDownloaded(email)) {
+ eepnet_trial.mailLostLicense(email);
+ }
+ getSession().message = P("Your license information has been sent to ", email, ".");
+ response.redirect(request.path);
+}
+
+//----------------------------------------------------------------
+function render_eepnet_purchase_get() {
+ renderFramed("store/eepnet_purchase.ejs", {});
+}
+
+//--------------------------------------------------------------------------------
+// csc-help page
+//--------------------------------------------------------------------------------
+
+function render_csc_help_get() {
+ response.write(renderTemplateAsString("store/csc-help.ejs"));
+}
+
+//--------------------------------------------------------------------------------
+// paypal notifications for pro
+//--------------------------------------------------------------------------------
+
+function render_paypalnotify() {
+ team_billing_control.handlePaypalNotify();
+}
diff --git a/etherpad/src/etherpad/control/testcontrol.js b/etherpad/src/etherpad/control/testcontrol.js
new file mode 100644
index 0000000..ed13006
--- /dev/null
+++ b/etherpad/src/etherpad/control/testcontrol.js
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+var tests = [
+ "t0000_test",
+ "t0001_sqlbase_transaction_rollback",
+ "t0002_license_generation",
+ "t0003_persistent_vars",
+ "t0004_sqlobj",
+ "t0005_easysync"
+];
+
+var tscope = this;
+tests.forEach(function(t) {
+ import.call(tscope, 'etherpad.testing.unit_tests.'+t);
+});
+//----------------------------------------------------------------
+
+function _testName(x) {
+ x = x.replace(/^t\d+\_/, '');
+ return x;
+}
+
+function render_run() {
+ response.setContentType("text/plain; charset=utf-8");
+ if (isProduction() && (request.params.p != "waverunner")) {
+ response.write("access denied");
+ response.stop();
+ }
+
+ var singleTest = request.params.t;
+ var numRun = 0;
+
+ println("----------------------------------------------------------------");
+ println("running tests");
+ println("----------------------------------------------------------------");
+ tests.forEach(function(t) {
+ var testName = _testName(t);
+ if (singleTest && (singleTest != testName)) {
+ return;
+ }
+ println("running test: "+testName);
+ numRun++;
+ tscope[t].run();
+ println("|| pass ||");
+ });
+ println("----------------------------------------------------------------");
+
+ if (numRun == 0) {
+ response.write("Error: no tests found");
+ } else {
+ response.write("OK");
+ }
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0000_test.js b/etherpad/src/etherpad/db_migrations/m0000_test.js
new file mode 100644
index 0000000..7df9bfd
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0000_test.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+function run() {
+ // nothing
+}
+
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js b/etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js
new file mode 100644
index 0000000..0e65779
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.createTable('eepnet_signups', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ email: 'VARCHAR(128) NOT NULL UNIQUE',
+ date: 'TIMESTAMP',
+ signupIp: 'VARCHAR(16)',
+ fullName: 'VARCHAR(255) NOT NULL',
+ orgName: 'VARCHAR(255) NOT NULL',
+ jobTitle: 'VARCHAR(255) NOT NULL',
+ estUsers: 'VARCHAR(255) NOT NULL',
+ licenseKey: 'VARCHAR(1024) NOT NULL'
+ });
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js b/etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js
new file mode 100644
index 0000000..786e4e9
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ // add new columns.
+ sqlobj.addColumns('eepnet_signups', {
+ firstName: 'VARCHAR(128) NOT NULL DEFAULT \'\'',
+ lastName: 'VARCHAR(128) NOT NULL DEFAULT \'\'',
+ phone: 'VARCHAR(128) NOT NULL DEFAULT \'\''
+ });
+
+ // split name into first/last
+ var rows = sqlobj.selectMulti('eepnet_signups', {}, {});
+ rows.forEach(function(r) {
+ var name = r.fullName;
+ r.firstName = (r.fullName.split(' ')[0]) || "?";
+ r.lastName = (r.fullName.split(' ').slice(1).join(' ')) || "?";
+ r.phone = "?";
+ sqlobj.updateSingle('eepnet_signups', {id: r.id}, r);
+ });
+
+ // drop column fullName
+ sqlobj.dropColumn('eepnet_signups', 'fullName');
+}
+
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js b/etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js
new file mode 100644
index 0000000..f121145
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+function run() {
+ if (sqlcommon.doesTableExist('just_a_test')) {
+ sqlobj.dropTable('just_a_test');
+ }
+ sqlobj.createTable('just_a_test', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ x: 'VARCHAR(128)'
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js b/etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js
new file mode 100644
index 0000000..959865d
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+import('etherpad.db_migrations.migration_runner.dmesg');
+
+function run() {
+ // This migration only applies to MySQL
+ if (!sqlcommon.isMysql()) {
+ return;
+ }
+
+ var tables = sqlobj.listTables();
+ tables.forEach(function(t) {
+ if (sqlobj.getTableEngine(t) != "InnoDB") {
+ dmesg("Converting table "+t+" to InnoDB...");
+ sqlobj.setTableEngine(t, "InnoDB");
+ }
+ });
+
+};
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js b/etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js
new file mode 100644
index 0000000..0dfd37e
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ var idColspec = "INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY";
+
+ sqlobj.createTable('billing_purchase', {
+ id: idColspec,
+ type: "ENUM('onetimepurchase', 'subscription')",
+ customer: "INT(11) NOT NULL",
+ product: "VARCHAR(128) NOT NULL",
+ cost: "INT(11) NOT NULL",
+ coupon: "VARCHAR(128) NOT NULL",
+ time: "DATETIME",
+ paidThrough: "DATETIME",
+ status: "ENUM('active', 'inactive')"
+ }, {
+ type: true,
+ customer: true,
+ product: true
+ });
+
+ sqlobj.createTable('billing_invoice', {
+ id: idColspec,
+ time: "DATETIME",
+ purchase: "INT(11) NOT NULL",
+ amt: "INT(11) NOT NULL",
+ status: "ENUM('pending', 'paid', 'void', 'refunded')"
+ }, {
+ status: true
+ });
+
+ sqlobj.createTable('billing_transaction', {
+ id: idColspec,
+ customer: "INT(11)",
+ time: "DATETIME",
+ amt: "INT(11)",
+ payInfo: "VARCHAR(128)",
+ txnId: "VARCHAR(128)", // depends on gateway used?
+ status: "ENUM('new', 'success', 'failure', 'pending')"
+ }, {
+ customer: true,
+ txnId: true
+ });
+
+ sqlobj.createTable('billing_adjustment', {
+ id: idColspec,
+ transaction: "INT(11)",
+ invoice: "INT(11)",
+ time: "DATETIME",
+ amt: "INT(11)"
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js b/etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js
new file mode 100644
index 0000000..349b27a
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ // add new columns.
+ sqlobj.addColumns('eepnet_signups', {
+ industry: 'VARCHAR(128)',
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js b/etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js
new file mode 100644
index 0000000..bda5853
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js
@@ -0,0 +1,67 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function run() {
+ ['pro_domains', 'pro_users', 'pro_padmeta'].forEach(function(t) {
+ if (sqlcommon.doesTableExist(t)) {
+ sqlobj.dropTable(t);
+ }
+ });
+
+ sqlobj.createTable('pro_domains', {
+ id: sqlobj.getIdColspec(),
+ subDomain: 'VARCHAR(128) UNIQUE NOT NULL',
+ extDomain: 'VARCHAR(128) DEFAULT NULL',
+ orgName: 'VARCHAR(128)'
+ });
+
+ sqlobj.createIndex('pro_domains', ['subDomain']);
+ sqlobj.createIndex('pro_domains', ['extDomain']);
+
+ sqlobj.createTable('pro_users', {
+ id: sqlobj.getIdColspec(),
+ domainId: 'INT NOT NULL',
+ fullName: 'VARCHAR(128) NOT NULL',
+ email: 'VARCHAR(128) NOT NULL', // not unique because same
+ // email can be on multiple domains.
+ passwordHash: 'VARCHAR(128) NOT NULL',
+ createdDate: sqlobj.getDateColspec("NOT NULL"),
+ lastLoginDate: sqlobj.getDateColspec("DEFAULT NULL"),
+ isAdmin: sqlobj.getBoolColspec("DEFAULT 0")
+ });
+
+ sqlobj.createTable('pro_padmeta', {
+ id: sqlobj.getIdColspec(),
+ domainId: 'INT NOT NULL',
+ localPadId: 'VARCHAR(128) NOT NULL',
+ title: 'VARCHAR(128)',
+ creatorId: 'INT DEFAULT NULL',
+ createdDate: sqlobj.getDateColspec("NOT NULL"),
+ lastEditorId: 'INT DEFAULT NULL',
+ lastEditedDate: sqlobj.getDateColspec("DEFAULT NULL")
+ });
+
+ sqlobj.createIndex('pro_padmeta', ['domainId', 'localPadId']);
+
+ var pneDomain = "<<private-network>>";
+ if (!sqlobj.selectSingle('pro_domains', {subDomain: pneDomain})) {
+ sqlobj.insert('pro_domains', {subDomain: pneDomain});
+ }
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js b/etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js
new file mode 100644
index 0000000..30e379a
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function run() {
+
+ var idColspec = 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY';
+
+ sqlobj.createTable('persistent_vars', {
+ id: idColspec,
+ name: 'VARCHAR(128) UNIQUE NOT NULL',
+ stringVal: 'VARCHAR(1024)'
+ });
+
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0009_pad_tables.js b/etherpad/src/etherpad/db_migrations/m0009_pad_tables.js
new file mode 100644
index 0000000..93f5a62
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0009_pad_tables.js
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlbase");
+
+function run() {
+
+ // These table creations used to be in etherpad.pad.model.onStartup, but
+ // they make more sense here because later migrations access these tables.
+ sqlbase.createJSONTable("PAD_META");
+ sqlbase.createJSONTable("PAD_APOOL");
+ sqlbase.createStringArrayTable("PAD_REVS");
+ sqlbase.createStringArrayTable("PAD_CHAT");
+ sqlbase.createStringArrayTable("PAD_REVMETA");
+ sqlbase.createStringArrayTable("PAD_AUTHORS");
+
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js b/etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js
new file mode 100644
index 0000000..36150b1
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlbase");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("etherpad.utils.startConsoleProgressBar");
+
+
+function run() {
+
+ sqlobj.dropAndCreateTable('PAD_SQLMETA', {
+ id: 'VARCHAR(128) PRIMARY KEY NOT NULL',
+ version: 'INT NOT NULL',
+ creationTime: sqlobj.getDateColspec('NOT NULL'),
+ lastWriteTime: sqlobj.getDateColspec('NOT NULL'),
+ headRev: 'INT NOT NULL'
+ });
+
+ sqlobj.createIndex('PAD_SQLMETA', ['version']);
+
+ var allPadIds = sqlbase.getAllJSONKeys("PAD_META");
+
+ // If this is a new database, there are no pads; else
+ // it is an old database with version 1 pads.
+ if (allPadIds.length == 0) {
+ return;
+ }
+
+ var numPadsTotal = allPadIds.length;
+ var numPadsSoFar = 0;
+ var progressBar = startConsoleProgressBar();
+
+ allPadIds.forEach(function(padId) {
+ var meta = sqlbase.getJSON("PAD_META", padId);
+
+ sqlobj.insert("PAD_SQLMETA", {
+ id: padId,
+ version: 1,
+ creationTime: new Date(meta.creationTime || 0),
+ lastWriteTime: new Date(),
+ headRev: meta.head
+ });
+
+ delete meta.creationTime; // now stored in SQLMETA
+ delete meta.version; // just in case (was used during development)
+ delete meta.dirty; // no longer stored in DB
+ delete meta.lastAccess; // no longer stored in DB
+
+ sqlbase.putJSON("PAD_META", padId, meta);
+
+ numPadsSoFar++;
+ progressBar.update(numPadsSoFar/numPadsTotal, numPadsSoFar+"/"+numPadsTotal+" pads");
+ });
+
+ progressBar.finish();
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js b/etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js
new file mode 100644
index 0000000..5ac8b26
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function run() {
+ // allow null values in passwordHash
+ if (sqlcommon.isDerby()) {
+ sqlobj.alterColumn('pro_users', 'passwordHash', 'NULL');
+ } else {
+ sqlobj.modifyColumn('pro_users', 'passwordHash', 'VARCHAR(128)');
+ }
+ sqlobj.addColumns('pro_users', {
+ tempPassHash: 'VARCHAR(128)'
+ });
+}
+
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js b/etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js
new file mode 100644
index 0000000..ddd4cf6
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.createTable('pro_users_auto_signin', {
+ id: sqlobj.getIdColspec(),
+ cookie: 'VARCHAR(128) UNIQUE NOT NULL',
+ userId: 'INT UNIQUE NOT NULL',
+ expires: sqlobj.getDateColspec('NOT NULL')
+ });
+ sqlobj.createIndex('pro_users_auto_signin', ['cookie']);
+ sqlobj.createIndex('pro_users_auto_signin', ['userId']);
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js b/etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js
new file mode 100644
index 0000000..146923a
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.startConsoleProgressBar");
+import("etherpad.pad.easysync2migration");
+import("etherpad.pne.pne_utils");
+import("sqlbase.sqlobj");
+import("etherpad.log");
+
+function run() {
+
+ // this is a PNE-only migration
+ if (! pne_utils.isPNE()) {
+ return;
+ }
+
+ var migrationsNeeded = sqlobj.selectMulti("PAD_SQLMETA", {version: 1});
+
+ if (migrationsNeeded.length == 0) {
+ return;
+ }
+
+ var migrationsTotal = migrationsNeeded.length;
+ var migrationsSoFar = 0;
+ var progressBar = startConsoleProgressBar();
+
+ migrationsNeeded.forEach(function(obj) {
+ var padId = String(obj.id);
+
+ log.info("Migrating pad "+padId+" from version 1 to version 2...");
+ easysync2migration.migratePad(padId);
+ sqlobj.update("PAD_SQLMETA", {id: padId}, {version: 2});
+ log.info("Migrated pad "+padId+".");
+
+ migrationsSoFar++;
+ progressBar.update(migrationsSoFar/migrationsTotal, migrationsSoFar+"/"+migrationsTotal+" pads");
+ });
+
+ progressBar.finish();
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js b/etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js
new file mode 100644
index 0000000..445b32d
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js
@@ -0,0 +1,102 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.startConsoleProgressBar");
+import("etherpad.pne.pne_utils");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlbase");
+import("etherpad.log");
+import("sqlbase.sqlcommon.*");
+import("etherpad.pad.padutils");
+
+function run() {
+
+ // this is a PNE-only migration
+ if (! pne_utils.isPNE()) {
+ return;
+ }
+
+ var renamesNeeded = sqlobj.selectMulti("PAD_SQLMETA", {});
+
+ if (renamesNeeded.length == 0) {
+ return;
+ }
+
+ var renamesTotal = renamesNeeded.length;
+ var renamesSoFar = 0;
+ var progressBar = startConsoleProgressBar();
+
+ renamesNeeded.forEach(function(obj) {
+ var oldPadId = String(obj.id);
+ var newPadId;
+ if (/^1\$[a-zA-Z0-9\-]+$/.test(oldPadId)) {
+ // not expecting a user pad beginning with "1$";
+ // this case is to avoid trashing dev databases
+ newPadId = oldPadId;
+ }
+ else {
+ var localPadId = padutils.makeValidLocalPadId(oldPadId);
+ newPadId = "1$"+localPadId;
+
+ // PAD_SQLMETA
+ obj.id = newPadId;
+ sqlobj.deleteRows("PAD_SQLMETA", {id:oldPadId});
+ sqlobj.insert("PAD_SQLMETA", obj);
+
+ // PAD_META
+ var meta = sqlbase.getJSON("PAD_META", oldPadId);
+ meta.padId = newPadId;
+ sqlbase.deleteJSON("PAD_META", oldPadId);
+ sqlbase.putJSON("PAD_META", newPadId, meta);
+
+ // PAD_APOOL
+ var apool = sqlbase.getJSON("PAD_APOOL", oldPadId);
+ sqlbase.deleteJSON("PAD_APOOL", oldPadId);
+ sqlbase.putJSON("PAD_APOOL", newPadId, apool);
+
+ function renamePadInStringArrayTable(arrayName) {
+ var stmnt = "UPDATE "+btquote("PAD_"+arrayName.toUpperCase()+"_META")+
+ " SET "+btquote("ID")+" = ? WHERE "+btquote("ID")+" = ?";
+ return withConnection(function(conn) {
+ var pstmnt = conn.prepareStatement(stmnt);
+ return closing(pstmnt, function() {
+ pstmnt.setString(1, newPadId);
+ pstmnt.setString(2, oldPadId);
+ pstmnt.executeUpdate();
+ });
+ });
+ }
+
+ renamePadInStringArrayTable("revs");
+ renamePadInStringArrayTable("chat");
+ renamePadInStringArrayTable("revmeta");
+ renamePadInStringArrayTable("authors");
+
+ sqlobj.insert('pro_padmeta', {
+ localPadId: localPadId,
+ title: localPadId,
+ createdDate: obj.creationTime,
+ domainId: 1 // PNE
+ });
+ }
+
+ renamesSoFar++;
+ progressBar.update(renamesSoFar/renamesTotal, renamesSoFar+"/"+renamesTotal+" pads");
+ });
+
+ progressBar.finish();
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js b/etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js
new file mode 100644
index 0000000..8fa98bb
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.addColumns('pro_padmeta', {
+ password: 'VARCHAR(128) DEFAULT NULL'
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js b/etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js
new file mode 100644
index 0000000..abcc93f
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js
@@ -0,0 +1,35 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.createTable('pne_tracking_data', {
+ id: sqlobj.getIdColspec(),
+ date: sqlobj.getDateColspec("NOT NULL"),
+ keyHash: 'VARCHAR(128) DEFAULT NULL',
+ name: 'VARCHAR(128) NOT NULL',
+ value: 'VARCHAR(1024) NOT NULL'
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js b/etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js
new file mode 100644
index 0000000..1067840
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.addColumns('pne_tracking_data', {
+ remoteIp: 'VARCHAR(128) NOT NULL'
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js b/etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js
new file mode 100644
index 0000000..6e10000
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js
@@ -0,0 +1,82 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ var idColspec = "INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY";
+
+ sqlobj.createTable('checkout_purchase', {
+ id: idColspec,
+ invoiceId: "INT NOT NULL",
+ owner: "VARCHAR(128) NOT NULL",
+ email: "VARCHAR(128) NOT NULL",
+ organization: "VARCHAR(128) NOT NULL",
+ firstName: "VARCHAR(100) NOT NULL",
+ lastName: "VARCHAR(100) NOT NULL",
+ addressLine1: "VARCHAR(100) NOT NULL",
+ addressLine2: "VARCHAR(100) NOT NULL",
+ city: "VARCHAR(40) NOT NULL",
+ state: "VARCHAR(2) NOT NULL",
+ zip: "VARCHAR(10) NOT NULL",
+ numUsers: "INT NOT NULL",
+ date: "TIMESTAMP NOT NULL",
+ cents: "INT NOT NULL",
+ referral: "VARCHAR(8)",
+ receiptEmail: "TEXT",
+ purchaseType: "ENUM('creditcard', 'invoice', 'paypal') NOT NULL",
+ licenseKey: "VARCHAR(1024)"
+ }, {
+ email: true,
+ invoiceId: true
+ });
+
+ sqlobj.createTable('checkout_referral', {
+ id: "VARCHAR(8) NOT NULL PRIMARY KEY",
+ productPctDiscount: "INT",
+ supportPctDiscount: "INT",
+ totalPctDiscount: "INT",
+ freeUsersCount: "INT",
+ freeUsersPct: "INT"
+ });
+
+ // add a sample referral code.
+ sqlobj.insert('checkout_referral', {
+ id: 'EPCO6128',
+ productPctDiscount: 50,
+ supportPctDiscount: 25,
+ totalPctDiscount: 15,
+ freeUsersCount: 20,
+ freeUsersPct: 10
+ });
+
+ // add a "free" referral code.
+ sqlobj.insert('checkout_referral', {
+ id: 'EP99FREE',
+ totalPctDiscount: 99
+ });
+
+ sqlobj.insert('checkout_referral', {
+ id: 'EPFREE68',
+ totalPctDiscount: 100
+ });
+
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js b/etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js
new file mode 100644
index 0000000..1f9ecbb
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.addColumns('pro_padmeta', {
+ isDeleted: sqlobj.getBoolColspec("NOT NULL DEFAULT 0")
+ });
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js b/etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js
new file mode 100644
index 0000000..a776622
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.addColumns('pro_padmeta', {
+ isArchived: sqlobj.getBoolColspec("NOT NULL DEFAULT 0")
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js b/etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js
new file mode 100644
index 0000000..9f357b7
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function run() {
+ sqlobj.addColumns('pro_padmeta', {
+ proAttrsJson: sqlobj.getLongtextColspec("")
+ });
+
+ // convert all existing columns into metaJSON
+
+ sqlcommon.inTransaction(function() {
+ var records = sqlobj.selectMulti('pro_padmeta', {}, {});
+ records.forEach(function(r) {
+ migrateRecord(r);
+ });
+ });
+}
+
+function migrateRecord(r) {
+ var editors = [];
+ if (r.creatorId) {
+ editors.push(r.creatorId);
+ }
+ if (r.lastEditorId) {
+ if (editors.indexOf(r.lastEditorId) < 0) {
+ editors.push(r.lastEditorId);
+ }
+ }
+ editors.sort();
+
+ var proAttrs = {
+ editors: editors,
+ };
+
+ var proAttrsJson = fastJSON.stringify(proAttrs);
+
+ sqlobj.update('pro_padmeta', {id: r.id}, {proAttrsJson: proAttrsJson});
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js b/etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js
new file mode 100644
index 0000000..23ca8d3
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.createTable('pad_cookie_userids', {
+ id: "VARCHAR(40) NOT NULL PRIMARY KEY",
+ createdDate: sqlobj.getDateColspec("NOT NULL"),
+ lastActiveDate: sqlobj.getDateColspec("NOT NULL")
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js b/etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js
new file mode 100644
index 0000000..927cdc9
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.createTable('usage_stats', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ name: 'VARCHAR(128) NOT NULL',
+ timestamp: 'INT NOT NULL',
+ value: 'INT NOT NULL'
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0024_statistics_table.js b/etherpad/src/etherpad/db_migrations/m0024_statistics_table.js
new file mode 100644
index 0000000..9d6e58c
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0024_statistics_table.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("fastJSON");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.createTable('statistics', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ name: 'VARCHAR(128) NOT NULL',
+ timestamp: 'INT NOT NULL',
+ value: 'TEXT NOT NULL'
+ });
+
+ var oldStats = sqlobj.selectMulti('usage_stats', {});
+ oldStats.forEach(function(stat) {
+ sqlobj.insert('statistics', {
+ timestamp: stat.timestamp,
+ name: stat.name,
+ value: fastJSON.stringify({value: stat.value})
+ });
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js b/etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js
new file mode 100644
index 0000000..a429f41
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function run() {
+ sqlobj.renameTable('pro_users', 'pro_accounts');
+ sqlobj.renameTable('pro_users_auto_signin', 'pro_accounts_auto_signin');
+ sqlobj.changeColumn('pro_accounts_auto_signin', 'userId', 'accountId INT UNIQUE NOT NULL');
+ sqlobj.createIndex('pro_accounts_auto_signin', ['accountId']);
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js b/etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js
new file mode 100644
index 0000000..7c41309
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (sqlcommon.doesTableExist("pad_guests")) {
+ sqlobj.dropTable("pad_guests");
+ }
+
+ sqlobj.createTable('pad_guests', {
+ id: sqlobj.getIdColspec(),
+ privateKey: 'VARCHAR(63) UNIQUE NOT NULL',
+ userId: 'VARCHAR(63) UNIQUE NOT NULL',
+ createdDate: sqlobj.getDateColspec("NOT NULL"),
+ lastActiveDate: sqlobj.getDateColspec("NOT NULL"),
+ data: sqlobj.getLongtextColspec("")
+ });
+
+ sqlobj.createIndex('pad_guests', ['privateKey']);
+ sqlobj.createIndex('pad_guests', ['userId']);
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0027_pro_config.js b/etherpad/src/etherpad/db_migrations/m0027_pro_config.js
new file mode 100644
index 0000000..9cbb629
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0027_pro_config.js
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.createTable('pro_config', {
+ id: sqlobj.getIdColspec(),
+ domainId: 'INT',
+ name: 'VARCHAR(128)',
+ jsonVal: sqlobj.getLongtextColspec("")
+ });
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js b/etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js
new file mode 100644
index 0000000..f708363
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.createTable('pro_beta_signups', {
+ id: sqlobj.getIdColspec(),
+ email: 'VARCHAR(256)',
+ activationCode: 'VARCHAR(128)',
+ isActivated: sqlobj.getBoolColspec(),
+ signupDate: sqlobj.getDateColspec(),
+ activationDate: sqlobj.getDateColspec()
+ });
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js b/etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js
new file mode 100644
index 0000000..36b76ab
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ var recordList = sqlobj.selectMulti('pro_domains', {});
+ recordList.forEach(function(r) {
+ var subDomain = r.subDomain;
+ if (subDomain != subDomain.toLowerCase()) {
+ // delete this domain record and all accounts associated with it.
+ sqlobj.deleteRows('pro_domains', {id: r.id});
+ sqlobj.deleteRows('pro_accounts', {domainId: r.id});
+ }
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js b/etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js
new file mode 100644
index 0000000..aeaa40f
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.utils.isPrivateNetworkEdition");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.modifyColumn('statistics', 'value', 'MEDIUMTEXT NOT NULL');
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js b/etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js
new file mode 100644
index 0000000..b9744a3
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.addColumns('pro_accounts', {
+ isDeleted: sqlobj.getBoolColspec("NOT NULL DEFAULT 0")
+ });
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js b/etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js
new file mode 100644
index 0000000..5e748f5
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.utils.isPrivateNetworkEdition");
+import("fastJSON");
+
+import("etherpad.statistics.statistics");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ statistics.getAllStatNames().forEach(function(statName) {
+ if (statistics.getStatData(statName).dataType == 'topValues') {
+ var entries = sqlobj.selectMulti('statistics', {name: statName});
+ entries.forEach(function(statEntry) {
+ var value = fastJSON.parse(statEntry.value);
+ value.topValues = value.topValues.slice(0, 50);
+ statEntry.value = fastJSON.stringify(value);
+ sqlobj.update('statistics', {id: statEntry.id}, statEntry);
+ });
+ }
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js b/etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js
new file mode 100644
index 0000000..4b33f52
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+
+function run() {
+ sqlobj.createTable('pro_account_usage', {
+ id: sqlobj.getIdColspec(),
+ domainId: 'INT NOT NULL UNIQUE',
+ count: 'INT NOT NULL DEFAULT 0',
+ lastReset: sqlobj.getDateColspec(),
+ lastUpdated: sqlobj.getDateColspec()
+ });
+ sqlobj.createIndex('pro_account_usage', ['domainId']);
+}
+
+
diff --git a/etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js b/etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js
new file mode 100644
index 0000000..491581b
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ var idColspec = "INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY";
+
+ sqlobj.createTable('billing_payment_info', {
+ customer: "INT(11) NOT NULL PRIMARY KEY",
+ fullname: "VARCHAR(128)",
+ paymentsummary: "VARCHAR(128)",
+ expiration: "VARCHAR(6)", // MMYYYY
+ transaction: "VARCHAR(128)"
+ });
+
+ sqlobj.addColumns('billing_purchase', {
+ error: "TEXT"
+ });
+
+ sqlobj.addColumns('billing_invoice', {
+ users: "INT(11)"
+ })
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js b/etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js
new file mode 100644
index 0000000..a49e9f9
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js
@@ -0,0 +1,28 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ sqlobj.addColumns('billing_payment_info', {
+ email: "VARCHAR(255)"
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js b/etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js
new file mode 100644
index 0000000..ce77734
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("dateutils");
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ var allDomains = sqlobj.selectMulti('pro_domains', {}, {});
+
+ allDomains.forEach(function(domain) {
+ var domainId = domain.id;
+ var accounts = sqlobj.selectMulti('pro_accounts', {domainId: domainId}, {});
+ if (accounts.length > 3) {
+ if (! sqlobj.selectSingle('billing_purchase', {product: "ONDEMAND", customer: domainId}, {})) {
+ sqlobj.insert('billing_purchase', {
+ product: "ONDEMAND",
+ paidThrough: dateutils.noon(new Date(Date.now()-1000*86400)),
+ type: 'subscription',
+ customer: domainId,
+ status: 'inactive',
+ cost: 0,
+ coupon: ""
+ });
+ }
+ }
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js b/etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js
new file mode 100644
index 0000000..7a9982c
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+
+function run() {
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ var idColspec = "INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY";
+
+ sqlobj.createTable('checkout_pro_referral', {
+ id: "VARCHAR(8) NOT NULL PRIMARY KEY",
+ pctDiscount: "INT",
+ freeUsers: "INT",
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js b/etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js
new file mode 100644
index 0000000..1e9a53c
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlbase");
+
+function run() {
+
+ sqlbase.createStringArrayTable("PAD_REVS10");
+ sqlbase.createStringArrayTable("PAD_REVS100");
+ sqlbase.createStringArrayTable("PAD_REVS1000");
+
+}
+
diff --git a/etherpad/src/etherpad/db_migrations/m0040_create_plugin_tables.js b/etherpad/src/etherpad/db_migrations/m0040_create_plugin_tables.js
new file mode 100644
index 0000000..62e8ff7
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/m0040_create_plugin_tables.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.utils.isPrivateNetworkEdition");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function run() {
+ sqlobj.createTable('plugin', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ name: 'VARCHAR(128) character set utf8 collate utf8_bin UNIQUE NOT NULL'
+ });
+ sqlobj.createTable('hook_type', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ name: 'VARCHAR(128) character set utf8 collate utf8_bin UNIQUE NOT NULL'
+ });
+ sqlobj.createTable('hook', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ type_id: 'INT NOT NULL REFERENCES hook_type(id)',
+ name: 'VARCHAR(128) character set utf8 collate utf8_bin NOT NULL'
+ });
+ sqlobj.createTable('plugin_hook', {
+ plugin_id: 'INT NOT NULL REFERENCES plugin(id)',
+ hook_id: 'INT NOT NULL REFERENCES hook(id)',
+ original_name: 'VARCHAR(128) character set utf8 collate utf8_bin'
+ });
+}
diff --git a/etherpad/src/etherpad/db_migrations/migration_runner.js b/etherpad/src/etherpad/db_migrations/migration_runner.js
new file mode 100644
index 0000000..f4fa861
--- /dev/null
+++ b/etherpad/src/etherpad/db_migrations/migration_runner.js
@@ -0,0 +1,148 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Database migrations.
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.log");
+import("etherpad.pne.pne_utils");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+// 1 migration per file
+//----------------------------------------------------------------
+
+var migrations = [
+ "m0000_test",
+ "m0001_eepnet_signups_init",
+ "m0002_eepnet_signups_2",
+ "m0003_create_tests_table_v2",
+ "m0004_convert_all_tables_to_innodb",
+ "m0005_create_billing_tables",
+ "m0006_eepnet_signups_3",
+ "m0007_create_pro_tables_v4",
+ "m0008_persistent_vars",
+ "m0009_pad_tables",
+ "m0010_pad_sqlmeta",
+ "m0011_pro_users_temppass",
+ "m0012_pro_users_auto_signin",
+ "m0013_pne_padv2_upgrade",
+ "m0014_pne_globalpadids",
+ "m0015_padmeta_passwords",
+ "m0016_pne_tracking_data",
+ "m0017_pne_tracking_data_v2",
+ "m0018_eepnet_checkout_tables",
+ "m0019_padmeta_deleted",
+ "m0020_padmeta_archived",
+ "m0021_pro_padmeta_json",
+ "m0022_create_userids_table",
+ "m0023_create_usagestats_table",
+ "m0024_statistics_table",
+ "m0025_rename_pro_users_table",
+ "m0026_create_guests_table",
+ "m0027_pro_config",
+ "m0028_ondemand_beta_emails",
+ "m0029_lowercase_subdomains",
+ "m0030_fix_statistics_values",
+ "m0031_deleted_pro_users",
+ "m0032_reduce_topvalues_counts",
+ "m0033_pro_account_usage",
+ "m0034_create_recurring_billing_table",
+ "m0035_add_email_to_paymentinfo",
+ "m0036_create_missing_subscription_records",
+ "m0037_create_pro_referral_table",
+ "m0038_pad_coarse_revs",
+ "m0040_create_plugin_tables"
+];
+
+var mscope = this;
+migrations.forEach(function(m) {
+ import.call(mscope, "etherpad.db_migrations."+m);
+});
+
+//----------------------------------------------------------------
+
+function dmesg(m) {
+ if ((!isProduction()) || appjet.cache.db_migrations_print_debug) {
+ log.info(m);
+ println(m);
+ }
+}
+
+function onStartup() {
+ appjet.cache.db_migrations_print_debug = true;
+ if (!sqlcommon.doesTableExist("db_migrations")) {
+ appjet.cache.db_migrations_print_debug = false;
+ sqlobj.createTable('db_migrations', {
+ id: 'INT NOT NULL '+sqlcommon.autoIncrementClause()+' PRIMARY KEY',
+ name: 'VARCHAR(255) NOT NULL UNIQUE',
+ completed: 'TIMESTAMP'
+ });
+ }
+
+ if (pne_utils.isPNE()) { pne_utils.checkDbVersionUpgrade(); }
+ runMigrations();
+ if (pne_utils.isPNE()) { pne_utils.saveDbVersion(); }
+}
+
+function _migrationName(m) {
+ m = m.replace(/^m\d+\_/, '');
+ m = m.replace(/\_/g, '-');
+ return m;
+}
+
+function getCompletedMigrations() {
+ var completedMigrationsList = sqlobj.selectMulti('db_migrations', {}, {});
+ var completedMigrations = {};
+
+ completedMigrationsList.forEach(function(c) {
+ completedMigrations[c.name] = true;
+ });
+
+ return completedMigrations;
+}
+
+function runMigrations() {
+ var completedMigrations = getCompletedMigrations();
+
+ dmesg("Checking for database migrations...");
+ migrations.forEach(function(m) {
+ var name = _migrationName(m);
+ if (!completedMigrations[name]) {
+ sqlcommon.inTransaction(function() {
+ dmesg("performing database migration: ["+name+"]");
+ var startTime = +(new Date);
+
+ mscope[m].run();
+
+ var elapsedMs = +(new Date) - startTime;
+ dmesg("migration completed in "+elapsedMs+"ms");
+
+ sqlobj.insert('db_migrations', {
+ name: name,
+ completed: new Date()
+ });
+ });
+ }
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/debug.js b/etherpad/src/etherpad/debug.js
new file mode 100644
index 0000000..069ad14
--- /dev/null
+++ b/etherpad/src/etherpad/debug.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.globals.*");
+
+jimport("java.lang.System.out.println");
+
+function dmesg(m) {
+ if (!isProduction()) {
+ println(m);
+ }
+}
+
diff --git a/etherpad/src/etherpad/globals.js b/etherpad/src/etherpad/globals.js
new file mode 100644
index 0000000..fcd5519
--- /dev/null
+++ b/etherpad/src/etherpad/globals.js
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2009 Google Inc.
+ * Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+ *
+ * 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.
+ */
+
+//----------------------------------------------------------------
+// global variabls
+//----------------------------------------------------------------
+
+var COMETPATH = "/comet";
+
+var COLOR_PALETTE = ['#ffc7c7','#fff1c7','#e3ffc7','#c7ffd5','#c7ffff','#c7d5ff','#e3c7ff','#ffc7f1','#ff8f8f','#ffe38f','#c7ff8f','#8fffab','#8fffff','#8fabff','#c78fff','#ff8fe3','#d97979','#d9c179','#a9d979','#79d991','#79d9d9','#7991d9','#a979d9','#d979c1','#d9a9a9','#d9cda9','#c1d9a9','#a9d9b5','#a9d9d9','#a9b5d9','#c1a9d9','#d9a9cd'];
+
+var trueRegex = /\s*true\s*/i;
+
+function isProduction() {
+ return (trueRegex.test(appjet.config['etherpad.isProduction']));
+}
+
+function isProAccountEnabled() {
+ return (appjet.config['etherpad.proAccounts'] == "true");
+}
+
+function domainEnabled(domain) {
+ var enabled = appjet.config.topdomains.split(',');
+ for (var i = 0; i < enabled.length; i++)
+ if (domain == enabled[i])
+ return true;
+ return false;
+}
+
+var PNE_RELEASE_VERSION = "1.1.3";
+var PNE_RELEASE_DATE = "June 15, 2009";
+
+var PRO_FREE_ACCOUNTS = 1e9;
+
+
diff --git a/etherpad/src/etherpad/helpers.js b/etherpad/src/etherpad/helpers.js
new file mode 100644
index 0000000..3996a3b
--- /dev/null
+++ b/etherpad/src/etherpad/helpers.js
@@ -0,0 +1,306 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("jsutils.eachProperty");
+import("faststatic");
+import("comet");
+import("funhtml.META");
+
+import("etherpad.globals.*");
+import("etherpad.debug.dmesg");
+
+import("etherpad.pro.pro_utils");
+
+jimport("java.lang.System.out.println");
+
+//----------------------------------------------------------------
+// array that supports contains() in O(1)
+
+var _UniqueArray = function() {
+ this._a = [];
+ this._m = {};
+};
+_UniqueArray.prototype.add = function(x) {
+ if (!this._m[x]) {
+ this._a.push(x);
+ this._m[x] = true;
+ }
+};
+_UniqueArray.prototype.asArray = function() {
+ return this._a;
+};
+
+//----------------------------------------------------------------
+// EJS template helpers
+//----------------------------------------------------------------
+
+function _hd() {
+ if (!appjet.requestCache.helperData) {
+ appjet.requestCache.helperData = {
+ clientVars: {},
+ htmlTitle: "",
+ headExtra: "",
+ bodyId: "",
+ bodyClasses: new _UniqueArray(),
+ cssIncludes: new _UniqueArray(),
+ jsIncludes: new _UniqueArray(),
+ includeCometJs: false,
+ suppressGA: false,
+ showHeader: true,
+ robotsPolicy: null
+ };
+ }
+ return appjet.requestCache.helperData;
+}
+
+function addBodyClass(c) {
+ _hd().bodyClasses.add(c);
+}
+
+function addClientVars(vars) {
+ eachProperty(vars, function(k,v) {
+ _hd().clientVars[k] = v;
+ });
+}
+
+function getClientVar(name) {
+ return _hd().clientVars[name];
+}
+
+function addToHead(stuff) {
+ _hd().headExtra += stuff;
+}
+
+function setHtmlTitle(t) {
+ _hd().htmlTitle = t;
+}
+
+function setBodyId(id) {
+ _hd().bodyId = id;
+}
+
+function includeJs(relpath) {
+ _hd().jsIncludes.add(relpath);
+}
+
+function includeJQuery() {
+ includeJs("jquery-1.3.2.js");
+}
+
+function includeCss(relpath) {
+ _hd().cssIncludes.add(relpath);
+}
+
+function includeCometJs() {
+ _hd().includeCometJs = true;
+}
+
+function suppressGA() {
+ _hd().suppressGA = true;
+}
+
+function hideHeader() {
+ _hd().showHeader = false;
+}
+
+//----------------------------------------------------------------
+// for rendering HTML
+//----------------------------------------------------------------
+
+function bodyClasses() {
+ return _hd().bodyClasses.asArray().join(' ');
+}
+
+function clientVarsScript() {
+ var x = _hd().clientVars;
+ x = fastJSON.stringify(x);
+ if (x == '{}') {
+ return '<!-- no client vars -->';
+ }
+ x = x.replace(/</g, '\\x3c');
+ return [
+ '<script type="text/javascript">',
+ ' // <![CDATA[',
+ 'var clientVars = '+x+';',
+ ' // ]]>',
+ '</script>'
+ ].join('\n');
+}
+
+function htmlTitle() {
+ return _hd().htmlTitle;
+}
+
+function bodyId() {
+ return _hd().bodyId;
+}
+
+function baseHref() {
+ return request.scheme + "://"+ request.host + "/";
+}
+
+function headExtra() {
+ return _hd().headExtra;
+}
+
+function jsIncludes() {
+ if (isProduction()) {
+ var jsincludes = _hd().jsIncludes.asArray();
+ if (_hd().includeCometJs) {
+ jsincludes.splice(0, 0, {
+ getPath: function() { return 'comet-client.js'; },
+ getContents: function() { return comet.clientCode(); },
+ getMTime: function() { return comet.clientMTime(); }
+ });
+ }
+ if (jsincludes.length < 1) { return ''; }
+ var key = faststatic.getCompressedFilesKey('js', '/static/js', jsincludes);
+ return '<script type="text/javascript" src="/static/compressed/'+key+'"></script>';
+ } else {
+ var ts = +(new Date);
+ var r = [];
+ if (_hd().includeCometJs) {
+ r.push('<script type="text/javascript" src="'+COMETPATH+'/js/client.js?'+ts+'"></script>');
+ }
+ _hd().jsIncludes.asArray().forEach(function(relpath) {
+ r.push('<script type="text/javascript" src="/static/js/'+relpath+'?'+ts+'"></script>');
+ });
+ return r.join('\n');
+ }
+}
+
+function cssIncludes() {
+ if (isProduction()) {
+ var key = faststatic.getCompressedFilesKey('css', '/static/css', _hd().cssIncludes.asArray());
+ return '<link href="/static/compressed/'+key+'" rel="stylesheet" type="text/css" />';
+ } else {
+ var ts = +(new Date);
+ var r = [];
+ _hd().cssIncludes.asArray().forEach(function(relpath) {
+ r.push('<link href="/static/css/'+relpath+'?'+ts+'" rel="stylesheet" type="text/css" />');
+ });
+ return r.join('\n');
+ }
+}
+
+function oemail(username) {
+ return '&lt;<a class="obfuscemail" href="mailto:'+username+'@p*d.sp***e.inf.fu-berlin.de">'+
+ username+'@p*d.sp***e.inf.fu-berlin.de</a>&gt;';
+}
+
+function googleAnalytics() {
+ // GA disabled always now.
+ return '';
+
+ if (!isProduction()) { return ''; }
+ if (_hd().suppressGA) { return ''; }
+ return [
+ '<script type="text/javascript">',
+ ' var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");',
+ ' document.write(unescape("%3Cscript src=\'" + gaJsHost + "google-analytics.com/ga.js\' type=\'text/javascript\'%3E%3C/script%3E"));',
+ '</script>',
+ '<script type="text/javascript">',
+ 'try {',
+ ' var pageTracker = _gat._getTracker("UA-6236278-1");',
+ ' pageTracker._trackPageview();',
+ '} catch(err) {}</script>'
+ ].join('\n');
+}
+
+function isHeaderVisible() {
+ return _hd().showHeader;
+}
+
+function setRobotsPolicy(policy) {
+ _hd().robotsPolicy = policy;
+}
+function robotsMeta() {
+ if (!_hd().robotsPolicy) { return ''; }
+ var content = "";
+ content += (_hd().robotsPolicy.index ? 'INDEX' : 'NOINDEX');
+ content += ", ";
+ content += (_hd().robotsPolicy.follow ? 'FOLLOW' : 'NOFOLLOW');
+ return META({name: "ROBOTS", content: content});
+}
+
+function thawteSiteSeal() {
+ return [
+ '<div>',
+ '<table width="10" border="0" cellspacing="0" align="center">',
+ '<tr>',
+ '<td>',
+ '<script src="https://siteseal.thawte.com/cgi/server/thawte_seal_generator.exe"></script>',
+ '</td>',
+ '</tr>',
+ '<tr>',
+ '<td height="0" align="center">',
+ '<a style="color:#AD0034" target="_new"',
+ 'href="http://www.thawte.com/digital-certificates/">',
+ '<span style="font-family:arial; font-size:8px; color:#AD0034">',
+ 'ABOUT SSL CERTIFICATES</span>',
+ '</a>',
+ '</td>',
+ '</tr>',
+ '</table>',
+ '</div>'
+ ].join('\n');
+}
+
+function clearFloats() {
+ return '<div style="clear: both;"><!-- --></div>';
+}
+
+function rafterBlogUrl() {
+ return '/ep/blog/posts/google-acquires-appjet';
+}
+
+function rafterNote() {
+ return """<div style='border: 1px solid #ccc; background: #fee; padding: 1em; margin: 1em 0;'>
+ <b>Note: </b>We are no longer accepting new accounts. <a href='"""+rafterBlogUrl()+"""'>Read more</a>.
+ </div>""";
+}
+
+function rafterTerminationDate() {
+ return "March 31, 2010";
+}
+
+function updateToUrl(setParams, deleteParams, setPath) {
+ var params = {};
+
+ for (param in request.params)
+ if (deleteParams === undefined || deleteParams.indexOf(param) == -1)
+ params[param] = request.params[param];
+
+ if (setParams !== undefined)
+ for (param in setParams)
+ params[param] = setParams[param];
+
+ var path = request.path;
+ if (setPath !== undefined)
+ path = setPath;
+
+ var paramStr = '';
+ for (param in params) {
+ if (paramStr == '')
+ paramStr += '?';
+ else
+ paramStr += '&';
+ paramStr += param + '=' + params[param];
+ }
+
+ return path + paramStr;
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/importexport/importexport.js b/etherpad/src/etherpad/importexport/importexport.js
new file mode 100644
index 0000000..304a1f4
--- /dev/null
+++ b/etherpad/src/etherpad/importexport/importexport.js
@@ -0,0 +1,241 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+jimport("java.io.File");
+jimport("java.io.FileOutputStream");
+jimport("java.lang.System.out.println");
+jimport("java.io.ByteArrayInputStream");
+jimport("java.io.ByteArrayOutputStream");
+jimport("java.io.DataInputStream");
+jimport("java.io.DataOutputStream");
+jimport("net.appjet.common.sars.SarsClient");
+jimport("com.etherpad.openofficeservice.OpenOfficeService");
+jimport("com.etherpad.openofficeservice.UnsupportedFormatException");
+jimport("com.etherpad.openofficeservice.TemporaryFailure");
+
+import("etherpad.log");
+import("etherpad.utils");
+import("sync");
+import("execution");
+import("varz");
+import("exceptionutils");
+
+function _log(obj) {
+ log.custom("import-export", obj);
+}
+
+function onStartup() {
+ execution.initTaskThreadPool("importexport", 1);
+}
+
+var formats = {
+ pdf: 'application/pdf',
+ doc: 'application/msword',
+ html: 'text/html; charset=utf-8',
+ odt: 'application/vnd.oasis.opendocument.text',
+ txt: 'text/plain; charset=utf-8'
+}
+
+function _createTempFile(bytes, type) {
+ var f = File.createTempFile("ooconvert-", (type === null ? null : (type == "" ? "" : "."+type)));
+ if (bytes) {
+ var fos = new FileOutputStream(f);
+ fos.write(bytes);
+ }
+ return f;
+}
+
+function _initConverterClient(convertServer) {
+ if (convertServer) {
+ var convertHost = convertServer.split(":")[0];
+ var convertPort = Number(convertServer.split(":")[1]);
+ if (! appjet.scopeCache.converter) {
+ var converter = new SarsClient("ooffice-password", convertHost, convertPort);
+ appjet.scopeCache.converter = converter;
+ converter.setConnectTimeout(5000);
+ converter.setReadTimeout(40000);
+ appjet.scopeCache.converter.connect();
+ }
+ return appjet.scopeCache.converter;
+ } else {
+ return null;
+ }
+}
+
+function _conversionSarsFailure() {
+ delete appjet.scopeCache.converter;
+}
+
+function errorUnsupported(from) {
+ return "Unsupported file type"+(from ? ": <strong>"+from+"</strong>." : ".")+" Etherpad can only import <strong>txt</strong>, <strong>html</strong>, <strong>rtf</strong>, <strong>doc</strong>, and <strong>docx</strong> files.";
+}
+var errorTemporary = "A temporary failure occurred; please try again later.";
+
+function doSlowFileConversion(from, to, bytes, continuation) {
+ var bytes = convertFileSlowly(from, to, bytes);
+ continuation.resume();
+ return bytes;
+}
+
+function _convertOverNetwork(convertServer, from, to, bytes) {
+ var c = _initConverterClient(convertServer);
+ var reqBytes = new ByteArrayOutputStream();
+ var req = new DataOutputStream(reqBytes);
+ req.writeUTF(from);
+ req.writeUTF(to);
+ req.writeInt(bytes.length);
+ req.write(bytes, 0, bytes.length);
+
+ var retBtyes;
+ try {
+ retBytes = c.message(reqBytes.toByteArray());
+ } catch (e) {
+ if (e.javaException) {
+ net.appjet.oui.exceptionlog.apply(e.javaException)
+ }
+ _conversionSarsFailure();
+ return "A communications failure occurred; please try again later.";
+ }
+
+ if (retBytes.length == 0) {
+ return "An unknown failure occurred; please try again later. (#5)";
+ }
+ var res = new DataInputStream(new ByteArrayInputStream(retBytes));
+ var status = res.readInt();
+ if (status == 0) { // success
+ var len = res.readInt();
+ var resBytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, len);
+ res.readFully(resBytes);
+ return resBytes;
+ } else if (status == 1) {
+ return errorTemporary;
+ } else if (status == 2) {
+ var permFailureCode = res.readInt();
+ if (permFailureCode == 0) {
+ return "An unknown failure occurred. (#1)";
+ } else if (permFailureCode == 1) {
+ return errorUnsupported(from);
+ }
+ } else {
+ return "An unknown failure occurred. (#2)";
+ }
+}
+
+function convertFileSlowly(from, to, bytes) {
+ var convertServer = appjet.config["etherpad.sofficeConversionServer"];
+ if (convertServer) {
+ return _convertOverNetwork(convertServer, from, to, bytes);
+ }
+
+ if (! utils.hasOffice()) {
+ return "EtherPad is not configured to import or export formats other than <strong>txt</strong> and <strong>html</strong>. Please contact your system administrator for details.";
+ }
+ OpenOfficeService.setExecutable(appjet.config["etherpad.soffice"]);
+ try {
+ return OpenOfficeService.convertFile(from, to, bytes);
+ } catch (e) {
+ if (e.javaException instanceof TemporaryFailure) {
+ return errorTemporary;
+ } else if (e.javaException instanceof UnsupportedFormatException) {
+ return errorUnsupported(from);
+ } else {
+ return "An unknown failure occurred. (#3)";
+ }
+ }
+}
+
+function _noteConversionAttempt() {
+ varz.incrementInt("importexport-conversions-attempted");
+}
+
+function _noteConversionSuccess() {
+ varz.incrementInt("importexport-conversions-successful");
+}
+
+function _noteConversionFailure() {
+ varz.incrementInt("importexport-conversions-failed");
+}
+
+function _noteConversionTimeout() {
+ varz.incrementInt("importexport-conversions-timeout");
+}
+
+function _noteConversionImpossible() {
+ varz.incrementInt("importexport-conversions-impossible");
+}
+
+function precomputedConversionResult(from, to, bytes) {
+ try {
+ var retBytes = request.cache.conversionCallable.get(500, java.util.concurrent.TimeUnit.MILLISECONDS);
+ var delay = Date.now() - request.cache.startTime;
+ _log({type: "conversion-latency", from: from, to: to,
+ numBytes: request.cache.conversionByteLength,
+ delay: delay});
+ varz.addToInt("importexport-total-conversion-millis", delay);
+ if (typeof(retBytes) == 'string') {
+ _log({type: "error", error: "conversion-failed", from: from, to: to,
+ numBytes: request.cache.conversionByteLength,
+ delay: delay});
+ _noteConversionFailure();
+ } else {
+ _noteConversionSuccess();
+ }
+ return retBytes;
+ } catch (e) {
+ if (e.javaException instanceof java.util.concurrent.TimeoutException) {
+ _noteConversionTimeout();
+ request.cache.conversionCallable.cancel(false);
+ _log({type: "error", error: "conversion-failed", from: from, to: to,
+ numBytes: request.cache.conversionByteLength,
+ delay: -1});
+ return "Conversion timed out. Please try again later.";
+ }
+ _log({type: "error", error: "conversion-failed", from: from, to: to,
+ numBytes: request.cache.conversionByteLength,
+ trace: exceptionutils.getStackTracePlain(e)});
+ _noteConversionFailure();
+ return "An unknown failure occurred. (#4)";
+ }
+}
+
+function convertFile(from, to, bytes) {
+ if (request.cache.conversionCallable) {
+ return precomputedConversionResult(from, to, bytes);
+ }
+
+ _noteConversionAttempt();
+ if (from == to) {
+ _noteConversionSuccess();
+ return bytes;
+ }
+ if (from == "txt" && to == "html") {
+ _noteConversionSuccess();
+ return (new java.lang.String(utils.renderTemplateAsString('pad/exporthtml.ejs', {
+ content: String(new java.lang.String(bytes, "UTF-8")).replace(/&/g, "&amp;").replace(/</g, "&lt;"),
+ pre: true
+ }))).getBytes("UTF-8");
+ }
+
+ request.cache.conversionByteLength = bytes.length;
+ request.cache.conversionCallable =
+ execution.scheduleTask("importexport", "doSlowFileConversion", 0, [
+ from, to, bytes, request.continuation
+ ]);
+ request.cache.startTime = Date.now();
+ request.continuation.suspend(45000);
+ _noteConversionImpossible();
+ return "An unexpected error occurred."; // Shouldn't ever get here.
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/legacy_urls.js b/etherpad/src/etherpad/legacy_urls.js
new file mode 100644
index 0000000..d8aa629
--- /dev/null
+++ b/etherpad/src/etherpad/legacy_urls.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* legacy URLs only apply to the public main site. (not Pro or PNE). */
+
+var _legacyURLs = {
+ '/ep/beta-signup': '/',
+ '/ep/talktostrangers': '/',
+ '/ep/about/pricing-eepod': '/ep/about/pricing-pro',
+ '/static/html/enterprise-etherpad-installguide.html': '/ep/pne-manual/',
+ '/static/html/eepnet/eepnet-changelog.html': '/ep/pne-manual/changelog',
+ '/static/html/eepnet/eepnet-installguide.html': '/ep/pne-manual/',
+ '/ep/blog/posts/back-online-until-open-sourced': '/ep/blog/posts/etherpad-back-online-until-open-sourced'
+};
+
+function checkPath() {
+ var p = request.path;
+ var match = _legacyURLs[p];
+
+ if (match) {
+ response.redirect(match);
+ }
+}
+
diff --git a/etherpad/src/etherpad/licensing.js b/etherpad/src/etherpad/licensing.js
new file mode 100644
index 0000000..2337456
--- /dev/null
+++ b/etherpad/src/etherpad/licensing.js
@@ -0,0 +1,163 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * This file used to control access restrictions for various sites like
+ * pad.spline.inf.fu-berlin.de or on-prem installations of etherpad, or evaluation
+ * editions. For the open-source effort, I have gutted out the
+ * restrictions. --aiba
+ */
+
+import("sync.callsync");
+import("stringutils");
+import("fileutils.readRealFile");
+import("jsutils.*");
+
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padutils");
+import("etherpad.pne.pne_utils");
+
+jimport("com.etherpad.Licensing");
+jimport("java.lang.System.out.println");
+
+var _editionNames = {
+ 0: 'ETHERPAD.COM',
+ 1: 'PRIVATE_NETWORK_EVALUATION',
+ 2: 'PRIVATE_NETWORK'
+};
+
+function onStartup() { }
+
+//----------------------------------------------------------------
+
+/**
+ * expires is a long timestamp (set to null for never expiring).
+ * maxUsers is also a long (set to -1 for infinite users).
+ */
+function generateNewKey(personName, orgName, expires, editionId, maxUsers) {
+ return null;
+}
+
+function decodeLicenseInfoFromKey(key) {
+ return null;
+}
+
+//----------------------------------------------------------------
+
+function _getCache() {
+ return {};
+}
+
+function _readKeyFile(f) {
+ return null;
+}
+
+function _readLicenseKey() {
+ return null;
+}
+
+function reloadLicense() {
+}
+
+function getLicense() {
+ return null;
+}
+
+function isPrivateNetworkEdition() {
+ return false;
+}
+
+// should really only be called for PNE requests.
+// see etherpad.quotas module
+function getMaxUsersPerPad() {
+ return 1e9;
+}
+
+function getEditionId(editionName) {
+ return _editionNames[0];
+}
+
+function getEditionName(editionId) {
+ return _editionNames[editionId];
+}
+
+function isEvaluation() {
+ return false;
+}
+
+function isExpired() {
+ return false;
+}
+
+function isValidKey(key) {
+ return true;
+}
+
+function getVersionString() {
+ return "0";
+}
+
+function isVersionTooOld() {
+ return false;
+}
+
+//----------------------------------------------------------------
+// counting active users
+//----------------------------------------------------------------
+
+function getActiveUserQuota() {
+ return 1e9;
+}
+
+function _previousMidnight() {
+ // return midnight of today.
+ var d = new Date();
+ d.setHours(0);
+ d.setMinutes(0);
+ d.setSeconds(0);
+ d.setMilliseconds(1); // just north of midnight
+ return d;
+}
+
+function _resetActiveUserStats() {
+}
+
+function getActiveUserWindowStart() {
+ return null;
+}
+
+function getActiveUserWindowHours() {
+ return null;
+}
+
+function getActiveUserCount() {
+ return 0;
+}
+
+function canSessionUserJoin() {
+ return true;
+}
+
+function onUserJoin(userInfo) {
+}
+
+function onUserLeave() {
+ // do nothing.
+}
+
+
diff --git a/etherpad/src/etherpad/log.js b/etherpad/src/etherpad/log.js
new file mode 100644
index 0000000..cfc82de
--- /dev/null
+++ b/etherpad/src/etherpad/log.js
@@ -0,0 +1,255 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("stringutils.startsWith");
+import("sync.{callsync,callsyncIfTrue}");
+import("jsutils.*");
+import("exceptionutils");
+
+import("etherpad.globals.*");
+import("etherpad.pad.padutils");
+import("etherpad.sessions");
+import("etherpad.utils.*");
+
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+
+jimport("java.io.FileWriter");
+jimport("java.lang.System.out.println");
+jimport("java.io.File");
+jimport("net.appjet.ajstdlib.execution");
+
+
+function getReadableTime() {
+ return (new Date()).toString().split(' ').slice(0, 5).join('-');
+}
+
+serverhandlers.tasks.trackerAndSessionIds = function() {
+ var m = new Packages.scala.collection.mutable.HashMap();
+ if (request.isDefined) {
+ try {
+ if (sessions.getTrackingId()) {
+ m.update("tracker", sessions.getTrackingId());
+ }
+ if (sessions.getSessionId()) {
+ m.update("session", sessions.getSessionId());
+ }
+ if (request.path) {
+ m.update("path", request.path);
+ }
+ if (request.clientAddr) {
+ m.update("clientAddr", request.clientAddr);
+ }
+ if (request.host) {
+ m.update("host", request.host);
+ }
+ if (getSessionProAccount()) {
+ m.update("proAccountId", getSessionProAccount().id);
+ }
+ } catch (e) {
+ // do nothing.
+ }
+ }
+ return m;
+}
+
+function onStartup() {
+ var f = execution.wrapRunTask("trackerAndSessionIds", null,
+ java.lang.Class.forName("scala.collection.mutable.HashMap"));
+ net.appjet.oui.GenericLoggerUtils.setExtraPropertiesFunction(f);
+}
+
+//----------------------------------------------------------------
+// Logfile parsing
+//----------------------------------------------------------------
+
+function _n(x) {
+ if (x < 10) { return "0"+x; }
+ else { return x; }
+}
+
+function logFileName(prefix, logName, day) {
+ var fmt = [day.getFullYear(), _n(day.getMonth()+1), _n(day.getDate())].join('-');
+ var fname = (appjet.config['logDir'] + '/'+prefix+'/' + logName + '/' +
+ logName + '-' + fmt + '.jslog');
+
+ // make sure file exists
+ if (!(new File(fname)).exists()) {
+ //log.warn("WARNING: file does not exist: "+fname);
+ return null;
+ }
+
+ return fname;
+}
+
+function frontendLogFileName(logName, day) {
+ return logFileName('frontend', logName, day);
+}
+
+function backendLogFileName(logName, day) {
+ return logFileName('backend', logName, day);
+}
+
+//----------------------------------------------------------------
+function _getRequestLogEntry() {
+ if (request.isDefined) {
+ var logEntry = {
+ clientAddr: request.clientAddr,
+ method: request.method.toLowerCase(),
+ scheme: request.scheme,
+ host: request.host,
+ path: request.path,
+ query: request.query,
+ referer: request.headers['Referer'],
+ userAgent: request.headers['User-Agent'],
+ statusCode: response.getStatusCode(),
+ }
+ if ('globalPadId' in request.cache) {
+ logEntry.padId = request.cache.globalPadId;
+ }
+ return logEntry;
+ } else {
+ return {};
+ }
+}
+
+function logRequest() {
+ if ((! request.isDefined) ||
+ startsWith(request.path, COMETPATH) ||
+ isStaticRequest()) {
+ return;
+ }
+
+ _log("request", _getRequestLogEntry());
+}
+
+function _log(name, m) {
+ var cache = appjet.cache;
+
+ callsyncIfTrue(
+ cache,
+ function() { return ! ('logWriters' in cache)},
+ function() { cache.logWriters = {}; }
+ );
+
+ callsyncIfTrue(
+ cache.logWriters,
+ function() { return !(name in cache.logWriters) },
+ function() {
+ lw = new net.appjet.oui.GenericLogger('frontend', name, true);
+ if (! isProduction()) {
+ lw.setEchoToStdOut(true);
+ }
+ lw.start();
+ cache.logWriters[name] = lw;
+ });
+
+ var lw = cache.logWriters[name];
+ if (typeof(m) == 'object') {
+ lw.logObject(m);
+ } else {
+ lw.log(m);
+ }
+}
+
+function custom(name, m) {
+ _log(name, m);
+}
+
+function _stampedMessage(m) {
+ var obj = {};
+ if (typeof(m) == 'string') {
+ obj.message = m;
+ } else {
+ eachProperty(m, function(k, v) {
+ obj[k] = v;
+ });
+ }
+ // stamp message with pad and path
+ if (request.isDefined) {
+ obj.path = request.path;
+ }
+
+ var currentPad = padutils.getCurrentPad();
+ if (currentPad) {
+ obj.currentPad = currentPad;
+ }
+
+ return obj;
+}
+
+//----------------------------------------------------------------
+// logException
+//----------------------------------------------------------------
+
+function logException(ex) {
+ if (typeof(ex) != 'object' || ! (ex instanceof java.lang.Throwable)) {
+ ex = new java.lang.RuntimeException(String(ex));
+ }
+ // NOTE: ex is always a java.lang.Throwable
+ var m = _getRequestLogEntry();
+ m.jsTrace = exceptionutils.getStackTracePlain(ex);
+ var s = new java.io.StringWriter();
+ ex.printStackTrace(new java.io.PrintWriter(s));
+ m.trace = s.toString();
+ _log("exception", m);
+}
+
+function callCatchingExceptions(func) {
+ try {
+ return func();
+ }
+ catch (e) {
+ logException(toJavaException(e));
+ }
+ return undefined;
+}
+
+//----------------------------------------------------------------
+// warning
+//----------------------------------------------------------------
+function warn(m) {
+ _log("warn", _stampedMessage(m));
+}
+
+//----------------------------------------------------------------
+// info
+//----------------------------------------------------------------
+function info(m) {
+ _log("info", _stampedMessage(m));
+}
+
+function onUserJoin(userId) {
+ function doUpdate() {
+ sqlobj.update('pad_cookie_userids', {id: userId}, {lastActiveDate: new Date()});
+ }
+ try {
+ sqlcommon.inTransaction(function() {
+ if (sqlobj.selectSingle('pad_cookie_userids', {id: userId})) {
+ doUpdate();
+ } else {
+ sqlobj.insert('pad_cookie_userids',
+ {id: userId, createdDate: new Date(), lastActiveDate: new Date()});
+ }
+ });
+ }
+ catch (e) {
+ sqlcommon.inTransaction(function() {
+ doUpdate();
+ });
+ }
+}
diff --git a/etherpad/src/etherpad/metrics/metrics.js b/etherpad/src/etherpad/metrics/metrics.js
new file mode 100644
index 0000000..435a5be
--- /dev/null
+++ b/etherpad/src/etherpad/metrics/metrics.js
@@ -0,0 +1,438 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.log.frontendLogFileName");
+import("jsutils.eachProperty");
+import("stringutils.startsWith");
+import("fileutils.eachFileLine");
+
+jimport("java.lang.System.out.println");
+
+var _idleTime = 5*60*1000 // 5 minutes?
+
+function _isPadUrl(url) {
+ return url != '/' && ! startsWith(url, '/ep/');
+}
+
+function VisitData(url, referer) {
+ this.url = url;
+ this.referer = referer;
+ this.__defineGetter__('isPadVisit', function() {
+ return _isPadUrl(this.url);
+ });
+}
+VisitData.prototype.toString = function() {
+ var re = new RegExp("^https?://"+request.host);
+ if (this.referer && ! re.test(this.referer)) {
+ return this.url+", from "+this.referer;
+ } else {
+ return this.url;
+ }
+}
+
+function Event(time, type, data) {
+ this.time = time;
+ this.type = type;
+ this.data = data;
+}
+Event.prototype.toString = function() {
+ return "("+this.type+" "+this.data+" @ "+this.time.getTime()+")";
+}
+
+function Flow(sessionKey, startEvent) {
+ this.sessionKey = sessionKey;
+ this.events = [];
+ this.visitedPaths = {};
+ var visitCount = 0;
+ var visitsCache;
+ this._updateVisitedPaths = function(url) {
+ if (! this.visitedPaths[url]) {
+ this.visitedPaths[url] = [visitCount];
+ } else {
+ this.visitedPaths[url].push(visitCount);
+ }
+ }
+ var isInPad = 0;
+ this.push = function(evt) {
+ evt.flow = this;
+ this.events.push(evt);
+ if (evt.type == 'visit') {
+ this._updateVisitedPaths(evt.data.url);
+ if (_isPadUrl(evt.data.url)) {
+ this._updateVisitedPaths("(pad)");
+ }
+ visitCount++;
+ visitsCache = undefined;
+ } else if (evt.type == 'userjoin') {
+ isInPad++;
+ } else if (evt.type == 'userleave') {
+ isInPad--;
+ }
+ }
+ this.__defineGetter__("isInPad", function() { return isInPad > 0; });
+ this.__defineGetter__("lastEvent", function() {
+ return this.events[this.events.length-1];
+ });
+ this.__defineGetter__("visits", function() {
+ if (! visitsCache) {
+ visitsCache = this.events.filter(function(x) { return x.type == "visit" });
+ }
+ return visitsCache;
+ });
+ startEvent.flow = this;
+ this.push(startEvent);
+}
+Flow.prototype.toString = function() {
+ return "["+this.events.map(function(x) { return x.toString(); }).join(", ")+"]";
+}
+Flow.prototype.includesVisit = function(path, index, useExactIndexMatch) {
+ if (! this.visitedPaths[path]) return false;
+ if (useExactIndexMatch) {
+ return this.visitedPaths[path].some(function(x) { return x == index });
+ } else {
+ if (index) {
+ for (var i = 0; i < this.visitedPaths[path].length; ++i) {
+ if (this.visitedPaths[path][i] >= index)
+ return this.visitedPaths[path][i];
+ }
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
+Flow.prototype.visitIndices = function(path) {
+ return this.visitedPaths[path] || [];
+}
+
+function getKeyForDate(date) {
+ return date.getYear()+":"+date.getMonth()+":"+date.getDay();
+}
+
+function parseEvents(dates) {
+ if (! appjet.cache["metrics-events"]) {
+ appjet.cache["metrics-events"] = {};
+ }
+ var events = {};
+ function eventArray(key) {
+ if (! events[key]) {
+ events[key] = [];
+ }
+ return events[key];
+ }
+
+ dates.sort(function(a, b) { return a.getTime() - b.getTime(); });
+ dates.forEach(function(day) {
+ if (! appjet.cache["metrics-events"][getKeyForDate(day)]) {
+ var daysEvents = {};
+ function daysEventArray(key) {
+ if (! daysEvents[key]) {
+ daysEvents[key] = [];
+ }
+ return daysEvents[key];
+ }
+ var requestLog = frontendLogFileName("request", day);
+ if (requestLog) {
+ eachFileLine(requestLog, function(line) {
+ var s = line.split("\t");
+ var sessionKey = s[3];
+ if (sessionKey == "-") { return; }
+ var time = new Date(Number(s[1]));
+ var path = s[7];
+ var referer = (s[9] == "-" ? null : s[9]);
+ var userAgent = s[10];
+ var statusCode = s[5];
+ // Remove bots and other automatic or irrelevant requests.
+ // There's got to be something better than a whitelist.
+ if (userAgent.indexOf("Mozilla") < 0 &&
+ userAgent.indexOf("Opera") < 0) {
+ return;
+ }
+ if (path == "/favicon.ico") { return; }
+ daysEventArray(sessionKey).push(new Event(time, "visit", new VisitData(path, referer)));
+ });
+ }
+ var padEventLog = frontendLogFileName("padevents", day);
+ if (padEventLog) {
+ eachFileLine(padEventLog, function(line) {
+ var s = line.split("\t");
+ var sessionKey = s[7];
+ if (sessionKey == "-") { return; }
+ var time = new Date(Number(s[1]));
+ var padId = s[3];
+ var evt = s[2];
+ daysEventArray(sessionKey).push(new Event(time, evt, padId));
+ });
+ }
+ var chatLog = frontendLogFileName("chat", day);
+ if (chatLog) {
+ eachFileLine(chatLog, function(line) {
+ var s = line.split("\t");
+ var sessionKey = s[4];
+ if (sessionKey == "-") { return; }
+ var time = new Date(Number(s[1]));
+ var padId = s[2];
+ daysEventArray(sessionKey).push(new Event(time, "chat", padId));
+ });
+ }
+ eachProperty(daysEvents, function(k, v) {
+ v.sort(function(a, b) { return a.time.getTime() - b.time.getTime()});
+ });
+ appjet.cache["metrics-events"][getKeyForDate(day)] = daysEvents;
+ }
+ eachProperty(appjet.cache["metrics-events"][getKeyForDate(day)], function(k, v) {
+ Array.prototype.push.apply(eventArray(k), v);
+ });
+ });
+
+ return events;
+}
+
+function getFlows(startDate, endDate) {
+ if (! endDate) { endDate = startDate; }
+ if (! appjet.cache.flows || request.params.clearCache == "1") {
+ appjet.cache.flows = {};
+ }
+ if (appjet.cache.flows[getKeyForDate(startDate)+"-"+getKeyForDate(endDate)]) {
+ return appjet.cache.flows[getKeyForDate(startDate)+"-"+getKeyForDate(endDate)];
+ }
+
+ var datesForEvents = [];
+ for (var i = startDate; i.getTime() <= endDate.getTime(); i = new Date(i.getTime()+86400*1000)) {
+ datesForEvents.push(i);
+ }
+
+ var events = parseEvents(datesForEvents);
+ var flows = {};
+
+ eachProperty(events, function(k, eventArray) {
+ flows[k] = [];
+ function lastFlow() {
+ var f = flows[k];
+ if (f.length > 0) {
+ return f[f.length-1];
+ }
+ }
+ var lastTime = 0;
+ eventArray.forEach(function(evt) {
+ var l = lastFlow();
+
+ if (l && (l.lastEvent.time.getTime() + _idleTime > evt.time.getTime() || l.isInPad)) {
+ l.push(evt);
+ } else {
+ flows[k].push(new Flow(k, evt));
+ }
+ });
+ });
+ appjet.cache.flows[getKeyForDate(startDate)+"-"+getKeyForDate(endDate)] = flows;
+ return flows;
+}
+
+function _uniq(array) {
+ var seen = {};
+ return array.filter(function(x) {
+ if (seen[x]) {
+ return false;
+ }
+ seen[x] = true;
+ return true;
+ });
+}
+
+function getFunnel(startDate, endDate, pathsArray, useConsecutivePaths) {
+ var flows = getFlows(startDate, endDate)
+
+ var flowsAtStep = pathsArray.map(function() { return []; });
+ eachProperty(flows, function(k, flowArray) {
+ flowArray.forEach(function(flow) {
+ if (flow.includesVisit(pathsArray[0])) {
+ flowsAtStep[0].push({f: flow, i: flow.visitIndices(pathsArray[0])});
+ }
+ });
+ });
+ for (var i = 0; i < pathsArray.length-1; ++i) {
+ flowsAtStep[i].forEach(function(fobj) {
+ var newIndices = fobj.i.map(function(index) {
+ var nextIndex =
+ fobj.f.includesVisit(pathsArray[i+1], index+1, useConsecutivePaths);
+ if (nextIndex !== false) {
+ return (useConsecutivePaths ? index+1 : nextIndex);
+ }
+ }).filter(function(x) { return x !== undefined; });
+ if (newIndices.length > 0) {
+ flowsAtStep[i+1].push({f: fobj.f, i: newIndices});
+ }
+ });
+ }
+ return {
+ flows: flowsAtStep.map(function(x) { return x.map(function(y) { return y.f; }); }),
+ visitCounts: flowsAtStep.map(function(x) { return x.length; }),
+ visitorCounts: flowsAtStep.map(function(x) {
+ return _uniq(x.map(function(y) { return y.f.sessionKey; })).length
+ })
+ };
+}
+
+function makeHistogram(array) {
+ var counts = {};
+ for (var i = 0; i < array.length; ++i) {
+ var value = array[i]
+ if (! counts[value]) {
+ counts[value] = 0;
+ }
+ counts[value]++;
+ }
+ var histogram = [];
+ eachProperty(counts, function(k, v) {
+ histogram.push({value: k, count: v, fraction: (v / array.length)});
+ });
+ histogram.sort(function(a, b) { return b.count - a.count; });
+ return histogram;
+}
+
+function getOrigins(startDate, endDate, useReferer, shouldAggregatePads) {
+ var key = (useReferer ? "referer" : "url");
+ var flows = getFlows(startDate, endDate);
+
+ var sessionKeyFirsts = [];
+ var flowFirsts = [];
+ eachProperty(flows, function(k, flowArray) {
+ if (flowArray[0].visits[0] && flowArray[0].visits[0].data &&
+ flowArray[0].visits[0].data[key]) {
+ var path = flowArray[0].visits[0].data[key];
+ sessionKeyFirsts.push(
+ (shouldAggregatePads && ! useReferer && _isPadUrl(path) ?
+ "(pad)" : path));
+ }
+ flowArray.forEach(function(flow) {
+ if (flow.visits[0] && flow.visits[0].data &&
+ flow.visits[0].data[key]) {
+ var path = flow.visits[0].data[key];
+ flowFirsts.push(
+ (shouldAggregatePads && ! useReferer && _isPadUrl(path) ?
+ "(pad)" : path));
+ }
+ });
+ });
+
+ if (useReferer) {
+ flowFirsts = flowFirsts.filter(function(x) { return ! startsWith(x, "http://pad.spline.inf.fu-berlin.de"); });
+ sessionKeyFirsts = sessionKeyFirsts.filter(function(x) { return ! startsWith(x, "http://pad.spline.inf.fu-berlin.de"); });
+ }
+
+ return {
+ flowFirsts: makeHistogram(flowFirsts),
+ sessionKeyFirsts: makeHistogram(sessionKeyFirsts)
+ }
+}
+
+function getExits(startDate, endDate, src, shouldAggregatePads) {
+ var flows = getFlows(startDate, endDate);
+
+ var exits = [];
+
+ eachProperty(flows, function(k, flowArray) {
+ flowArray.forEach(function(flow) {
+ var indices = flow.visitIndices(src);
+ for (var i = 0; i < indices.length; ++i) {
+ if (indices[i]+1 < flow.visits.length) {
+ if (src != flow.visits[indices[i]+1].data.url) {
+ exits.push(flow.visits[indices[i]+1]);
+ }
+ } else {
+ exits.push("(nothing)");
+ }
+ }
+ });
+ });
+ return {
+ nextVisits: exits,
+ histogram: makeHistogram(exits.map(function(x) {
+ if (typeof(x) == 'string') return x;
+ return ((! shouldAggregatePads) || ! _isPadUrl(x.data.url) ?
+ x.data.url : "(pad)" )
+ }))
+ }
+}
+
+jimport("org.jfree.data.general.DefaultPieDataset");
+jimport("org.jfree.chart.plot.PiePlot");
+jimport("org.jfree.chart.ChartUtilities");
+jimport("org.jfree.chart.JFreeChart");
+
+function _fToPct(f) {
+ return Math.round(f*10000)/100;
+}
+
+function _shorten(str) {
+ if (startsWith(str, "http://")) {
+ str = str.substring("http://".length);
+ }
+ var len = 35;
+ if (str.length > len) {
+ return str.substring(0, len-3)+"..."
+ } else {
+ return str;
+ }
+}
+
+function respondWithPieChart(name, histogram) {
+ var width = 900;
+ var height = 300;
+
+ var ds = new DefaultPieDataset();
+
+ var cumulative = 0;
+ var other = 0;
+ var otherCount = 0;
+ histogram.forEach(function(x, i) {
+ cumulative += x.fraction;
+ if (cumulative < 0.98 && x.fraction > .01) {
+ ds.setValue(_shorten(x.value)+"\n ("+x.count+" visits - "+_fToPct(x.fraction)+"%)", x.fraction);
+ } else {
+ other += x.fraction;
+ otherCount += x.count;
+ }
+ });
+ if (other > 0) {
+ ds.setValue("Other ("+otherCount + " visits - "+_fToPct(other)+"%)", other);
+ }
+
+ var piePlot = new PiePlot(ds);
+
+ var chart = new JFreeChart(piePlot);
+ chart.setTitle(name);
+ chart.removeLegend();
+
+ var jos = new java.io.ByteArrayOutputStream();
+ ChartUtilities.writeChartAsJPEG(
+ jos, 1.0, chart, width, height);
+
+ response.setContentType('image/jpeg');
+ response.writeBytes(jos.toByteArray());
+}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/etherpad/src/etherpad/pad/activepads.js b/etherpad/src/etherpad/pad/activepads.js
new file mode 100644
index 0000000..07f5e2e
--- /dev/null
+++ b/etherpad/src/etherpad/pad/activepads.js
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("jsutils.cmp");
+
+jimport("net.appjet.common.util.LimitedSizeMapping");
+
+var HISTORY_SIZE = 100;
+
+function _getMap() {
+ if (!appjet.cache['activepads']) {
+ appjet.cache['activepads'] = {
+ map: new LimitedSizeMapping(HISTORY_SIZE)
+ };
+ }
+ return appjet.cache['activepads'].map;
+}
+
+function touch(padId) {
+ _getMap().put(padId, +(new Date));
+}
+
+function getActivePads() {
+ var m = _getMap();
+ var a = m.listAllKeys().toArray();
+ var activePads = [];
+ for (var i = 0; i < a.length; i++) {
+ activePads.push({
+ padId: a[i],
+ timestamp: m.get(a[i])
+ });
+ }
+
+ activePads.sort(function(a,b) { return cmp(b.timestamp,a.timestamp); });
+ return activePads;
+}
+
+
+
diff --git a/etherpad/src/etherpad/pad/chatarchive.js b/etherpad/src/etherpad/pad/chatarchive.js
new file mode 100644
index 0000000..2f8e33a
--- /dev/null
+++ b/etherpad/src/etherpad/pad/chatarchive.js
@@ -0,0 +1,67 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("stringutils");
+import("etherpad.log");
+
+jimport("java.lang.System.out.println");
+
+function onChatMessage(pad, senderUserInfo, msg) {
+ pad.appendChatMessage({
+ name: senderUserInfo.name,
+ userId: senderUserInfo.userId,
+ time: +(new Date),
+ lineText: msg.lineText
+ });
+}
+
+function getRecentChatBlock(pad, howMany) {
+ var numMessages = pad.getNumChatMessages();
+ var firstToGet = Math.max(0, numMessages - howMany);
+
+ return getChatBlock(pad, firstToGet, numMessages);
+}
+
+function getChatBlock(pad, start, end) {
+ if (start < 0) {
+ start = 0;
+ }
+ if (end > pad.getNumChatMessages()) {
+ end = pad.getNumChatMessages();
+ }
+
+ var historicalAuthorData = {};
+ var lines = [];
+ var block = {start: start, end: end,
+ historicalAuthorData: historicalAuthorData,
+ lines: lines};
+
+ for(var i=start; i<end; i++) {
+ var x = pad.getChatMessage(i);
+ var userId = x.userId;
+ if (! historicalAuthorData[userId]) {
+ historicalAuthorData[userId] = (pad.getAuthorData(userId) || {});
+ }
+ lines.push({
+ name: x.name,
+ time: x.time,
+ userId: x.userId,
+ lineText: x.lineText
+ });
+ }
+
+ return block;
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/pad/dbwriter.js b/etherpad/src/etherpad/pad/dbwriter.js
new file mode 100644
index 0000000..233622b
--- /dev/null
+++ b/etherpad/src/etherpad/pad/dbwriter.js
@@ -0,0 +1,338 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("execution");
+import("profiler");
+
+import("etherpad.pad.model");
+import("etherpad.pad.model.accessPadGlobal");
+import("etherpad.log");
+import("etherpad.utils");
+
+jimport("net.appjet.oui.exceptionlog");
+jimport("java.util.concurrent.ConcurrentHashMap");
+jimport("java.lang.System.out.println");
+
+var MIN_WRITE_INTERVAL_MS = 2000; // 2 seconds
+var MIN_WRITE_DELAY_NOTIFY_MS = 2000; // 2 seconds
+var AGE_FOR_PAD_FLUSH_MS = 5*60*1000; // 5 minutes
+var DBUNWRITABLE_WRITE_DELAY_MS = 30*1000; // 30 seconds
+
+// state is { constant: true }, { constant: false }, { trueAfter: timeInMs }
+function setWritableState(state) {
+ _dbwriter().dbWritable = state;
+}
+
+function getWritableState() {
+ return _dbwriter().dbWritable;
+}
+
+function isDBWritable() {
+ return _isDBWritable();
+}
+
+function _isDBWritable() {
+ var state = _dbwriter().dbWritable;
+ if (typeof state != "object") {
+ return true;
+ }
+ else if (state.constant !== undefined) {
+ return !! state.constant;
+ }
+ else if (state.trueAfter !== undefined) {
+ return (+new Date()) > state.trueAfter;
+ }
+ else return true;
+}
+
+function getWritableStateDescription(state) {
+ var v = _isDBWritable();
+ var restOfMessage = "";
+ if (state.trueAfter !== undefined) {
+ var now = +new Date();
+ var then = state.trueAfter;
+ var diffSeconds = java.lang.String.format("%.1f", Math.abs(now - then)/1000);
+ if (now < then) {
+ restOfMessage = " until "+diffSeconds+" seconds from now";
+ }
+ else {
+ restOfMessage = " since "+diffSeconds+" seconds ago";
+ }
+ }
+ return v+restOfMessage;
+}
+
+function _dbwriter() {
+ return appjet.cache.dbwriter;
+}
+
+function onStartup() {
+ appjet.cache.dbwriter = {};
+ var dbwriter = _dbwriter();
+ dbwriter.pendingWrites = new ConcurrentHashMap();
+ dbwriter.scheduledFor = new ConcurrentHashMap(); // padId --> long
+ dbwriter.dbWritable = { constant: true };
+
+ execution.initTaskThreadPool("dbwriter", 4);
+ // we don't wait for scheduled tasks in the infreq pool to run and complete
+ execution.initTaskThreadPool("dbwriter_infreq", 1);
+
+ _scheduleCheckForStalePads();
+}
+
+function _scheduleCheckForStalePads() {
+ execution.scheduleTask("dbwriter_infreq", "checkForStalePads", AGE_FOR_PAD_FLUSH_MS, []);
+}
+
+function onShutdown() {
+ log.info("Doing final DB writes before shutdown...");
+ var success = execution.shutdownAndWaitOnTaskThreadPool("dbwriter", 10000);
+ if (! success) {
+ log.warn("ERROR! DB WRITER COULD NOT SHUTDOWN THREAD POOL!");
+ }
+}
+
+function _logException(e) {
+ var exc = utils.toJavaException(e);
+ log.warn("writeAllToDB: Error writing to SQL! Written to exceptions.log: "+exc);
+ log.logException(exc);
+ exceptionlog.apply(exc);
+}
+
+function taskFlushPad(padId, reason) {
+ var dbwriter = _dbwriter();
+ if (! _isDBWritable()) {
+ // DB is unwritable, delay
+ execution.scheduleTask("dbwriter_infreq", "flushPad", DBUNWRITABLE_WRITE_DELAY_MS, [padId, reason]);
+ return;
+ }
+
+ model.accessPadGlobal(padId, function(pad) {
+ writePadNow(pad, true);
+ }, "r");
+
+ log.info("taskFlushPad: flushed "+padId+(reason?(" (reason: "+reason+")"):''));
+}
+
+function taskWritePad(padId) {
+ var dbwriter = _dbwriter();
+ if (! _isDBWritable()) {
+ // DB is unwritable, delay
+ dbwriter.scheduledFor.put(padId, (+(new Date)+DBUNWRITABLE_WRITE_DELAY_MS));
+ execution.scheduleTask("dbwriter", "writePad", DBUNWRITABLE_WRITE_DELAY_MS, [padId]);
+ return;
+ }
+
+ profiler.reset();
+ var t1 = profiler.rcb("lock wait");
+ model.accessPadGlobal(padId, function(pad) {
+ t1();
+ _dbwriter().pendingWrites.remove(padId); // do this first
+
+ var success = false;
+ try {
+ var t2 = profiler.rcb("write");
+ writePadNow(pad);
+ t2();
+
+ success = true;
+ }
+ finally {
+ if (! success) {
+ log.warn("DB WRITER FAILED TO WRITE PAD: "+padId);
+ }
+ profiler.print();
+ }
+ }, "r");
+}
+
+function taskCheckForStalePads() {
+ // do this first
+ _scheduleCheckForStalePads();
+
+ if (! _isDBWritable()) return;
+
+ // get "active" pads into an array
+ var padIter = appjet.cache.pads.meta.keySet().iterator();
+ var padList = [];
+ while (padIter.hasNext()) { padList.push(padIter.next()); }
+
+ var numStale = 0;
+
+ for (var i = 0; i < padList.length; i++) {
+ if (! _isDBWritable()) break;
+ var p = padList[i];
+ if (model.isPadLockHeld(p)) {
+ // skip it, don't want to lock up stale pad flusher
+ }
+ else {
+ accessPadGlobal(p, function(pad) {
+ if (pad.exists()) {
+ var padAge = (+new Date()) - pad._meta.status.lastAccess;
+ if (padAge > AGE_FOR_PAD_FLUSH_MS) {
+ writePadNow(pad, true);
+ numStale++;
+ }
+ }
+ }, "r");
+ }
+ }
+
+ log.info("taskCheckForStalePads: flushed "+numStale+" stale pads");
+}
+
+function notifyPadDirty(padId) {
+ var dbwriter = _dbwriter();
+ if (! dbwriter.pendingWrites.containsKey(padId)) {
+ dbwriter.pendingWrites.put(padId, "pending");
+ dbwriter.scheduledFor.put(padId, (+(new Date)+MIN_WRITE_INTERVAL_MS));
+ execution.scheduleTask("dbwriter", "writePad", MIN_WRITE_INTERVAL_MS, [padId]);
+ }
+}
+
+function scheduleFlushPad(padId, reason) {
+ execution.scheduleTask("dbwriter_infreq", "flushPad", 0, [padId, reason]);
+}
+
+/*function _dbwriterLoopBody(executor) {
+ try {
+ var info = writeAllToDB(executor);
+ if (!info.boring) {
+ log.info("DB writer: "+info.toSource());
+ }
+ java.lang.Thread.sleep(Math.max(0, MIN_WRITE_INTERVAL_MS - info.elapsed));
+ }
+ catch (e) {
+ _logException(e);
+ java.lang.Thread.sleep(MIN_WRITE_INTERVAL_MS);
+ }
+}
+
+function _startInThread(name, func) {
+ (new Thread(new Runnable({
+ run: function() {
+ func();
+ }
+ }), name)).start();
+}
+
+function killDBWriterThreadAndWait() {
+ appjet.cache.abortDBWriter = true;
+ while (appjet.cache.runningDBWriter) {
+ java.lang.Thread.sleep(100);
+ }
+}*/
+
+/*function writeAllToDB(executor, andFlush) {
+ if (!executor) {
+ executor = new ScheduledThreadPoolExecutor(NUM_WRITER_THREADS);
+ }
+
+ profiler.reset();
+ var startWriteTime = profiler.time();
+ var padCount = new AtomicInteger(0);
+ var writeCount = new AtomicInteger(0);
+ var removeCount = new AtomicInteger(0);
+
+ // get pads into an array
+ var padIter = appjet.cache.pads.meta.keySet().iterator();
+ var padList = [];
+ while (padIter.hasNext()) { padList.push(padIter.next()); }
+
+ var latch = new CountDownLatch(padList.length);
+
+ for (var i = 0; i < padList.length; i++) {
+ _spawnCall(executor, function(p) {
+ try {
+ var padWriteResult = {};
+ accessPadGlobal(p, function(pad) {
+ if (pad.exists()) {
+ padCount.getAndIncrement();
+ padWriteResult = writePad(pad, andFlush);
+ if (padWriteResult.didWrite) writeCount.getAndIncrement();
+ if (padWriteResult.didRemove) removeCount.getAndIncrement();
+ }
+ }, "r");
+ } catch (e) {
+ _logException(e);
+ } finally {
+ latch.countDown();
+ }
+ }, padList[i]);
+ }
+
+ // wait for them all to finish
+ latch.await();
+
+ var endWriteTime = profiler.time();
+ var elapsed = Math.round((endWriteTime - startWriteTime)/1000)/1000;
+ var interesting = (writeCount.get() > 0 || removeCount.get() > 0);
+
+ var obj = {padCount:padCount.get(), writeCount:writeCount.get(), elapsed:elapsed, removeCount:removeCount.get()};
+ if (! interesting) obj.boring = true;
+ if (interesting) {
+ profiler.record("writeAll", profiler.time()-startWriteTime);
+ profiler.print();
+ }
+
+ return obj;
+}*/
+
+function writePadNow(pad, andFlush) {
+ var didWrite = false;
+ var didRemove = false;
+
+ if (pad.exists()) {
+ var dbUpToDate = false;
+ if (pad._meta.status.dirty) {
+ /*log.info("Writing pad "+pad.getId());*/
+ pad._meta.status.dirty = false;
+ //var t1 = +new Date();
+ pad.writeToDB();
+ //var t2 = +new Date();
+ didWrite = true;
+
+ //log.info("Wrote pad "+pad.getId()+" in "+(t2-t1)+" ms.");
+
+ var now = +(new Date);
+ var sched = _dbwriter().scheduledFor.get(pad.getId());
+ if (sched) {
+ var delay = now - sched;
+ if (delay > MIN_WRITE_DELAY_NOTIFY_MS) {
+ log.warn("dbwriter["+pad.getId()+"] behind schedule by "+delay+"ms");
+ }
+ _dbwriter().scheduledFor.remove(pad.getId());
+ }
+ }
+ if (andFlush) {
+ // remove from cache
+ model.removeFromMemory(pad);
+ didRemove = true;
+ }
+ }
+ return {didWrite:didWrite, didRemove:didRemove};
+}
+
+/*function _spawnCall(executor, func, varargs) {
+ var args = Array.prototype.slice.call(arguments, 2);
+ var that = this;
+ executor.schedule(new Runnable({
+ run: function() {
+ func.apply(that, args);
+ }
+ }), 0, TimeUnit.MICROSECONDS);
+}*/
+
diff --git a/etherpad/src/etherpad/pad/easysync2migration.js b/etherpad/src/etherpad/pad/easysync2migration.js
new file mode 100644
index 0000000..c2a1523
--- /dev/null
+++ b/etherpad/src/etherpad/pad/easysync2migration.js
@@ -0,0 +1,675 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import("etherpad.collab.ace.easysync1");
+import("etherpad.collab.ace.easysync2");
+import("sqlbase.sqlbase");
+import("fastJSON");
+import("sqlbase.sqlcommon.*");
+import("etherpad.collab.ace.contentcollector.sanitizeUnicode");
+
+function _getPadStringArrayNumId(padId, arrayName) {
+ var stmnt = "SELECT NUMID FROM "+btquote("PAD_"+arrayName.toUpperCase()+"_META")+
+ " WHERE ("+btquote("ID")+" = ?)";
+
+ return withConnection(function(conn) {
+ var pstmnt = conn.prepareStatement(stmnt);
+ return closing(pstmnt, function() {
+ pstmnt.setString(1, padId);
+ var resultSet = pstmnt.executeQuery();
+ return closing(resultSet, function() {
+ if (! resultSet.next()) {
+ return -1;
+ }
+ return resultSet.getInt(1);
+ });
+ });
+ });
+}
+
+function _getEntirePadStringArray(padId, arrayName) {
+ var numId = _getPadStringArrayNumId(padId, arrayName);
+ if (numId < 0) {
+ return [];
+ }
+
+ var stmnt = "SELECT PAGESTART, OFFSETS, DATA FROM "+btquote("PAD_"+arrayName.toUpperCase()+"_TEXT")+
+ " WHERE ("+btquote("NUMID")+" = ?)";
+
+ return withConnection(function(conn) {
+ var pstmnt = conn.prepareStatement(stmnt);
+ return closing(pstmnt, function() {
+ pstmnt.setInt(1, numId);
+ var resultSet = pstmnt.executeQuery();
+ return closing(resultSet, function() {
+ var array = [];
+ while (resultSet.next()) {
+ var pageStart = resultSet.getInt(1);
+ var lengthsString = resultSet.getString(2);
+ var dataString = resultSet.getString(3);
+ var dataIndex = 0;
+ var arrayIndex = pageStart;
+ lengthsString.split(',').forEach(function(len) {
+ if (len) {
+ len = Number(len);
+ array[arrayIndex] = dataString.substr(dataIndex, len);
+ dataIndex += len;
+ }
+ arrayIndex++;
+ });
+ }
+ return array;
+ });
+ });
+ });
+}
+
+function _overwriteEntirePadStringArray(padId, arrayName, array) {
+ var numId = _getPadStringArrayNumId(padId, arrayName);
+ if (numId < 0) {
+ // generate numId
+ withConnection(function(conn) {
+ var ps = conn.prepareStatement("INSERT INTO "+btquote("PAD_"+arrayName.toUpperCase()+"_META")+
+ " ("+btquote("ID")+") VALUES (?)",
+ java.sql.Statement.RETURN_GENERATED_KEYS);
+ closing(ps, function() {
+ ps.setString(1, padId);
+ ps.executeUpdate();
+ var keys = ps.getGeneratedKeys();
+ if ((! keys) || (! keys.next())) {
+ throw new Error("Couldn't generate key for "+arrayName+" table for pad "+padId);
+ }
+ closing(keys, function() {
+ numId = keys.getInt(1);
+ });
+ });
+ });
+ }
+
+ withConnection(function(conn) {
+
+ var stmnt1 = "DELETE FROM "+btquote("PAD_"+arrayName.toUpperCase()+"_TEXT")+
+ " WHERE ("+btquote("NUMID")+" = ?)";
+ var pstmnt1 = conn.prepareStatement(stmnt1);
+ closing(pstmnt1, function() {
+ pstmnt1.setInt(1, numId);
+ pstmnt1.executeUpdate();
+ });
+
+ var PAGE_SIZE = 20;
+ var numPages = Math.floor((array.length-1) / PAGE_SIZE + 1);
+
+ var PAGES_PER_BATCH = 20;
+ var curPage = 0;
+
+ while (curPage < numPages) {
+ var stmnt2 = "INSERT INTO "+btquote("PAD_"+arrayName.toUpperCase()+"_TEXT")+
+ " ("+btquote("NUMID")+", "+btquote("PAGESTART")+", "+btquote("OFFSETS")+
+ ", "+btquote("DATA")+") VALUES (?, ?, ?, ?)";
+ var pstmnt2 = conn.prepareStatement(stmnt2);
+ closing(pstmnt2, function() {
+ for(var n=0;n<PAGES_PER_BATCH && curPage < numPages;n++) {
+ var pageStart = curPage*PAGE_SIZE;
+ var r = pageStart;
+ var lengthPieces = [];
+ var dataPieces = [];
+ for(var i=0;i<PAGE_SIZE;i++) {
+ var str = (array[r] || '');
+ dataPieces.push(str);
+ lengthPieces.push(String(str.length || ''));
+ r++;
+ }
+ var lengthsString = lengthPieces.join(',');
+ var dataString = dataPieces.join('');
+ pstmnt2.setInt(1, numId);
+ pstmnt2.setInt(2, pageStart);
+ pstmnt2.setString(3, lengthsString);
+ pstmnt2.setString(4, dataString);
+ pstmnt2.addBatch();
+
+ curPage++;
+ }
+ pstmnt2.executeBatch();
+ });
+ }
+ });
+
+}
+
+function _getEntirePadJSONArray(padId, arrayName) {
+ var array = _getEntirePadStringArray(padId, arrayName);
+ for(var k in array) {
+ if (array[k]) {
+ array[k] = fastJSON.parse(array[k]);
+ }
+ }
+ return array;
+}
+
+function _overwriteEntirePadJSONArray(padId, arrayName, objArray) {
+ var array = [];
+ for(var k in objArray) {
+ if (objArray[k]) {
+ array[k] = fastJSON.stringify(objArray[k]);
+ }
+ }
+ _overwriteEntirePadStringArray(padId, arrayName, array);
+}
+
+function _getMigrationPad(padId) {
+ var oldRevs = _getEntirePadStringArray(padId, "revs");
+ var oldRevMeta = _getEntirePadJSONArray(padId, "revmeta");
+ var oldAuthors = _getEntirePadJSONArray(padId, "authors");
+ var oldMeta = sqlbase.getJSON("PAD_META", padId);
+
+ var oldPad = {
+ getHeadRevisionNumber: function() {
+ return oldMeta.head;
+ },
+ getRevisionChangesetString: function(r) {
+ return oldRevs[r];
+ },
+ getRevisionAuthor: function(r) {
+ return oldMeta.numToAuthor[oldRevMeta[r].a];
+ },
+ getId: function() { return padId; },
+ getKeyRevisionNumber: function(r) {
+ return Math.floor(r / oldMeta.keyRevInterval) * oldMeta.keyRevInterval;
+ },
+ getInternalRevisionText: function(r) {
+ if (r != oldPad.getKeyRevisionNumber(r)) {
+ throw new Error("Assertion error: "+r+" != "+oldPad.getKeyRevisionNumber(r));
+ }
+ return oldRevMeta[r].atext.text;
+ },
+ _meta: oldMeta,
+ getAuthorArrayEntry: function(n) {
+ return oldAuthors[n];
+ },
+ getRevMetaArrayEntry: function(r) {
+ return oldRevMeta[r];
+ }
+ };
+
+ var apool = new easysync2.AttribPool();
+ var newRevMeta = [];
+ var newAuthors = [];
+ var newRevs = [];
+ var metaPropsToDelete = [];
+
+ var newPad = {
+ pool: function() { return apool; },
+ setAuthorArrayEntry: function(n, obj) {
+ newAuthors[n] = obj;
+ },
+ setRevMetaArrayEntry: function(r, obj) {
+ newRevMeta[r] = obj;
+ },
+ setRevsArrayEntry: function(r, cs) {
+ newRevs[r] = cs;
+ },
+ deleteMetaProp: function(propName) {
+ metaPropsToDelete.push(propName);
+ }
+ };
+
+ function writeToDB() {
+ var newMeta = {};
+ for(var k in oldMeta) {
+ newMeta[k] = oldMeta[k];
+ }
+ metaPropsToDelete.forEach(function(p) {
+ delete newMeta[p];
+ });
+
+ sqlbase.putJSON("PAD_META", padId, newMeta);
+ sqlbase.putJSON("PAD_APOOL", padId, apool.toJsonable());
+
+ _overwriteEntirePadStringArray(padId, "revs", newRevs);
+ _overwriteEntirePadJSONArray(padId, "revmeta", newRevMeta);
+ _overwriteEntirePadJSONArray(padId, "authors", newAuthors);
+ }
+
+ return {oldPad:oldPad, newPad:newPad, writeToDB:writeToDB};
+}
+
+function migratePad(padId) {
+
+ var mpad = _getMigrationPad(padId);
+ var oldPad = mpad.oldPad;
+ var newPad = mpad.newPad;
+
+ var headRev = oldPad.getHeadRevisionNumber();
+ var txt = "\n";
+ var newChangesets = [];
+ var newChangesetAuthorNums = [];
+ var cumCs = easysync2.Changeset.identity(1);
+
+ var pool = newPad.pool();
+
+ var isExtraFinalNewline = false;
+
+ function authorToNewNum(author) {
+ return pool.putAttrib(['author',author||'']);
+ }
+
+ //S var oldTotalChangesetSize = 0;
+ //S var newTotalChangesetSize = 0;
+ //S function stringSize(str) {
+ //S return new java.lang.String(str).getBytes("UTF-8").length;
+ //S }
+
+ //P var diffTotals = [];
+ for(var r=0;r<=headRev;r++) {
+ //P var times = [];
+ //P times.push(+new Date);
+ var author = oldPad.getRevisionAuthor(r);
+ //P times.push(+new Date);
+ newChangesetAuthorNums.push(authorToNewNum(author));
+
+ var newCs, newText;
+ if (r == 0) {
+ newText = oldPad.getInternalRevisionText(0);
+ newCs = getInitialChangeset(newText, pool, author);
+ //S oldTotalChangesetSize += stringSize(pad.getRevisionChangesetString(0));
+ }
+ else {
+ var oldCsStr = oldPad.getRevisionChangesetString(r);
+ //S oldTotalChangesetSize += stringSize(oldCsStr);
+ //P times.push(+new Date);
+ var oldCs = easysync1.Changeset.decodeFromString(oldCsStr);
+ //P times.push(+new Date);
+
+ /*var newTextFromOldCs = oldCs.applyToText(txt);
+ if (newTextFromOldCs.charAt(newTextFromOldCs.length-1) != '\n') {
+ var e = new Error("Violation of final newline property at revision "+r);
+ e.finalNewlineMissing = true;
+ throw e;
+ }*/
+ //var newCsNewTxt1 = upgradeChangeset(oldCs, txt, pool, author);
+ var oldIsExtraFinalNewline = isExtraFinalNewline;
+ var newCsNewTxt2 = upgradeChangeset(oldCs, txt, pool, author, isExtraFinalNewline);
+ //P times.push(+new Date);
+ /*if (newCsNewTxt1[1] != newCsNewTxt2[1]) {
+ _putFile(newCsNewTxt1[1], "/tmp/file1");
+ _putFile(newCsNewTxt2[1], "/tmp/file2");
+ throw new Error("MISMATCH 1");
+ }
+ if (newCsNewTxt1[0] != newCsNewTxt2[0]) {
+ _putFile(newCsNewTxt1[0], "/tmp/file1");
+ _putFile(newCsNewTxt2[0], "/tmp/file2");
+ throw new Error("MISMATCH 0");
+ }*/
+ newCs = newCsNewTxt2[0];
+ newText = newCsNewTxt2[1];
+ isExtraFinalNewline = newCsNewTxt2[2];
+
+ /*if (oldIsExtraFinalNewline || isExtraFinalNewline) {
+ System.out.print("\nnewline fix for rev "+r+"/"+headRev+"... ");
+ }*/
+ }
+
+ var oldText = txt;
+ newChangesets.push(newCs);
+ txt = newText;
+ //System.out.println(easysync2.Changeset.toBaseTen(cumCs)+" * "+
+ //easysync2.Changeset.toBaseTen(newCs));
+ /*cumCs = easysync2.Changeset.checkRep(easysync2.Changeset.compose(cumCs, newCs));
+ if (easysync2.Changeset.applyToText(cumCs, "\n") != txt) {
+ throw new Error("cumCs mismatch");
+ }*/
+
+ //P times.push(+new Date);
+
+ easysync2.Changeset.checkRep(newCs);
+ //P times.push(+new Date);
+ var origText = txt;
+ if (isExtraFinalNewline) {
+ origText = origText.slice(0, -1);
+ }
+ if (r == oldPad.getKeyRevisionNumber(r)) {
+ // only check key revisions (and final outcome), for speed
+ if (oldPad.getInternalRevisionText(r) != origText) {
+ var expected = oldPad.getInternalRevisionText(r);
+ var actual = origText;
+ //_putFile(expected, "/tmp/file1");
+ //_putFile(actual, "/tmp/file2");
+ //_putFile(oldText, "/tmp/file3");
+ //java.lang.System.out.println(String(oldCs));
+ //java.lang.System.out.println(easysync2.Changeset.toBaseTen(newCs));
+ throw new Error("Migration mismatch, pad "+padId+", revision "+r);
+ }
+ }
+
+ //S newTotalChangesetSize += stringSize(newCs);
+
+ //P if (r > 0) {
+ //P var diffs = [];
+ //P for(var i=0;i<times.length-1;i++) {
+ //P diffs[i] = times[i+1] - times[i];
+ //P }
+ //P for(var i=0;i<diffs.length;i++) {
+ //P diffTotals[i] = (diffTotals[i] || 0) + diffs[i]*1000/headRev;
+ //P }
+ //P }
+ }
+ //P System.out.println(String(diffTotals));
+
+ //S System.out.println("New data is "+(newTotalChangesetSize/oldTotalChangesetSize*100)+
+ //S "% size of old data (average "+(newTotalChangesetSize/(headRev+1))+
+ //S " bytes instead of "+(oldTotalChangesetSize/(headRev+1))+")");
+
+ var atext = easysync2.Changeset.makeAText("\n");
+ for(var r=0; r<=headRev; r++) {
+ newPad.setRevsArrayEntry(r, newChangesets[r]);
+
+ atext = easysync2.Changeset.applyToAText(newChangesets[r], atext, pool);
+
+ var rm = oldPad.getRevMetaArrayEntry(r);
+ rm.a = newChangesetAuthorNums[r];
+ if (rm.atext) {
+ rm.atext = easysync2.Changeset.cloneAText(atext);
+ }
+ newPad.setRevMetaArrayEntry(r, rm);
+ }
+
+ var newAuthors = [];
+ var newAuthorDatas = [];
+ for(var k in oldPad._meta.numToAuthor) {
+ var n = Number(k);
+ var authorData = oldPad.getAuthorArrayEntry(n) || {};
+ var authorName = oldPad._meta.numToAuthor[n];
+ var newAuthorNum = pool.putAttrib(['author',authorName]);
+ newPad.setAuthorArrayEntry(newAuthorNum, authorData);
+ }
+
+ newPad.deleteMetaProp('numToAuthor');
+ newPad.deleteMetaProp('authorToNum');
+
+ mpad.writeToDB();
+}
+
+function getInitialChangeset(txt, pool, author) {
+ var txt2 = txt.substring(0, txt.length-1); // strip off final newline
+
+ var assem = easysync2.Changeset.smartOpAssembler();
+ assem.appendOpWithText('+', txt2, pool && author && [['author',author]], pool);
+ assem.endDocument();
+ return easysync2.Changeset.pack(1, txt2.length+1, assem.toString(), txt2);
+}
+
+function upgradeChangeset(cs, inputText, pool, author, isExtraNewlineInSource) {
+ var attribs = '';
+ if (pool && author) {
+ attribs = '*'+easysync2.Changeset.numToString(pool.putAttrib(['author', author]));
+ }
+
+ function keepLastCharacter(c) {
+ if (! c[c.length-1] && c[c.length-3] + c[c.length-2] >= (c.oldLen() - 1)) {
+ c[c.length-2] = c.oldLen() - c[c.length-3];
+ }
+ else {
+ c.push(c.oldLen() - 1, 1, "");
+ }
+ }
+
+ var isExtraNewlineInOutput = false;
+ if (isExtraNewlineInSource) {
+ cs[1] += 1; // oldLen ++
+ }
+ if ((cs[cs.length-1] && cs[cs.length-1].slice(-1) != '\n') ||
+ ((! cs[cs.length-1]) && inputText.charAt(cs[cs.length-3] + cs[cs.length-2] - 1) != '\n')) {
+ // new text won't end with newline!
+ if (isExtraNewlineInSource) {
+ keepLastCharacter(cs);
+ }
+ else {
+ cs[cs.length-1] += "\n";
+ }
+ cs[2] += 1; // newLen ++
+ isExtraNewlineInOutput = true;
+ }
+
+ var oldLen = cs.oldLen();
+ var newLen = cs.newLen();
+
+ // final-newline-preserving modifications to changeset {{{
+ // These fixes are required for changesets that don't respect the
+ // new rule that the final newline of the document not be touched,
+ // and also for changesets tweaked above. It is important that the
+ // fixed changesets obey all the constraints on version 1 changesets
+ // so that they may become valid version 2 changesets.
+ {
+ function collapsePotentialEmptyLastTake(c) {
+ if (c[c.length-2] == 0 && c.length > 6) {
+ if (! c[c.length-1]) {
+ // last strip doesn't take or insert now
+ c.length -= 3;
+ }
+ else {
+ // the last two strips should be merged
+ // e.g. fo\n -> rock\nbar\n: then in this block,
+ // "Changeset,3,9,0,0,r,1,1,ck,2,0,\nbar" becomes
+ // "Changeset,3,9,0,0,r,1,1,ck\nbar"
+ c[c.length-4] += c[c.length-1];
+ c.length -= 3;
+ }
+ }
+ }
+ var lastStripStart = cs[cs.length-3];
+ var lastStripTake = cs[cs.length-2];
+ var lastStripInsert = cs[cs.length-1];
+ if (lastStripStart + lastStripTake == oldLen && lastStripInsert) {
+ // an insert at end
+ // e.g. foo\n -> foo\nbar\n:
+ // "Changeset,4,8,0,4,bar\n" becomes "Changeset,4,8,0,3,\nbar,3,1,"
+ // first make the previous newline part of the insertion
+ cs[cs.length-2] -= 1;
+ cs[cs.length-1] = '\n'+cs[cs.length-1].slice(0,-1);
+ collapsePotentialEmptyLastTake(cs);
+ keepLastCharacter(cs);
+ }
+ else if (lastStripStart + lastStripTake < oldLen && ! lastStripInsert) {
+ // ends with pure deletion
+ cs[cs.length-2] -= 1;
+ collapsePotentialEmptyLastTake(cs);
+ keepLastCharacter(cs);
+ }
+ else if (lastStripStart + lastStripTake < oldLen) {
+ // ends with replacement
+ cs[cs.length-1] = cs[cs.length-1].slice(0,-1);
+ keepLastCharacter(cs);
+ }
+ }
+ // }}}
+
+ var ops = [];
+ var lastOpcode = '';
+ function appendOp(opcode, text, startChar, endChar) {
+ function num(n) {
+ return easysync2.Changeset.numToString(n);
+ }
+ var lines = 0;
+ var lastNewlineEnd = startChar;
+ for (;;) {
+ var index = text.indexOf('\n', lastNewlineEnd);
+ if (index < 0 || index >= endChar) {
+ break;
+ }
+ lines++;
+ lastNewlineEnd = index+1;
+ }
+ var a = (opcode == '+' ? attribs : '');
+ var multilineChars = (lastNewlineEnd - startChar);
+ var seqLength = endChar - startChar;
+ var op = '';
+ if (lines > 0) {
+ op = [a, '|', num(lines), opcode, num(multilineChars)].join('');
+ }
+ if (multilineChars < seqLength) {
+ op += [a, opcode, num(seqLength - multilineChars)].join('');
+ }
+ if (op) {
+ // we reorder a single - and a single +
+ if (opcode == '-' && lastOpcode == '+') {
+ ops.splice(ops.length-1, 0, op);
+ }
+ else {
+ ops.push(op);
+ lastOpcode = opcode;
+ }
+ }
+ }
+
+ var oldPos = 0;
+
+ var textPieces = [];
+ var charBankPieces = [];
+ cs.eachStrip(function(start, take, insert) {
+ if (start > oldPos) {
+ appendOp('-', inputText, oldPos, start);
+ }
+ if (take) {
+ if (start+take < oldLen || insert) {
+ appendOp('=', inputText, start, start+take);
+ }
+ textPieces.push(inputText.substring(start, start+take));
+ }
+ if (insert) {
+ appendOp('+', insert, 0, insert.length);
+ textPieces.push(insert);
+ charBankPieces.push(insert);
+ }
+ oldPos = start+take;
+ });
+ // ... and no final deletions after the newline fixing.
+
+ var newCs = easysync2.Changeset.pack(oldLen, newLen, ops.join(''),
+ sanitizeUnicode(charBankPieces.join('')));
+ var newText = textPieces.join('');
+
+ return [newCs, newText, isExtraNewlineInOutput];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// unicode issues: 5SaYQp7cKV
+
+// // hard-coded just for testing; any pad is allowed to have corruption.
+// var newlineCorruptedPads = [
+// '0OCGFKkjDv', '14dWjOiOxP', '1LL8XQCBjC', '1jMnjEEK6e', '21',
+// '23DytOPN7d', '32YzfdT2xS', '3E6GB7l7FZ', '3Un8qaCfJh', '3YAj3rC9em',
+// '3vY2eaHSw5', '4834RRTLlg', '4Fm1iVSTWI', '5NpTNqWHGC', '7FYNSdYQVa',
+// '7RZCbvgw1z', '8EVpyN6HyY', '8P5mPRxPVr', '8aHFRmLxKR', '8dsj9eGQfP',
+// 'BSoGobOJZZ', 'Bf0uVghKy0', 'C2f3umStKd', 'CHlu2CA8F3', 'D2WEwgvg1W',
+// 'DNLTpuP2wl', 'DwNpm2TDgu', 'EKPByZ3EGZ', 'FwQxu6UKQx', 'HUn9O34rFl',
+// 'JKZhxMo20E', 'JVjuukL42N', 'JVuBlWxaxL', 'Jmw5lPNYcl', 'KnZHz6jE2P',
+// 'Luyp6ylbgR', 'MB6lPoN1eI', 'McsCrQUM6c', 'NWIuVobIw9', 'OKERTLQCCn',
+// 'OchiOchi', 'OfhKHCB8jJ', 'OkM3Jv3XY9', 'PX5Z89mx29', 'PdmKQIvOEd',
+// 'R9NQNB66qt', 'RvULFSvCbV', 'RyLJC6Qo1x', 'SBlKLwr2Ag', 'SavD72Q9P7',
+// 'SfXyxseAeF', 'TTGZ4yO2PI', 'U3U7rT3d6w', 'UFmqpQIDAi', 'V7Or0QQk4m',
+// 'VPCM5ReAQm', 'VvIYHzIJUY', 'W0Ccc3BVGb', 'Wv3cGgSgjg', 'WwVPgaZUK5',
+// 'WyIFUJXfm5', 'XxESEsgQ6R', 'Yc5Yq3WCuU', 'ZRqCFaRx6h', 'ZepX6TLFbD',
+// 'bSeImT5po4', 'bqIlTkFDiH', 'btt9vNPSQ9', 'c97YJj8PSN', 'd9YV3sypKF',
+// 'eDzzkrwDRU', 'eFQJZWclzo', 'eaz44OhFDu', 'ehKkx1YpLA', 'ep',
+// 'foNq3v3e9T', 'form6rooma', 'fqhtIHG0Ii', 'fvZyCRZjv2', 'gZnadICPYV',
+// 'gvGXtMKhQk', 'h7AYuTxUOd', 'hc1UZSti3J', 'hrFQtae2jW', 'i8rENUZUMu',
+// 'iFW9dceEmh', 'iRNEc8SlOc', 'jEDsDgDlaK', 'jo8ngXlSJh', 'kgJrB9Gh2M',
+// 'klassennetz76da2661f8ceccfe74faf97d25a4b418',
+// 'klassennetzf06d4d8176d0804697d9650f836cb1f7', 'lDHgmfyiSu',
+// 'mA1cbvxFwA', 'mSJpW1th29', 'mXHAqv1Emu', 'monocles12', 'n0NhU3FxxT',
+// 'ng7AlzPb5b', 'ntbErnnuyz', 'oVnMO0dX80', 'omOTPVY3Gl', 'p5aNFCfYG9',
+// 'pYxjVCILuL', 'phylab', 'pjVBFmnhf1', 'qGohFW3Lbr', 'qYlbjeIHDs',
+// 'qgf4OwkFI6', 'qsi', 'rJQ09pRexM', 'snNjlS1aLC', 'tYKC53TDF9',
+// 'u1vZmL8Yjv', 'ur4sb7DBJB', 'vesti', 'w9NJegEAZt', 'wDwlSCby2s',
+// 'wGFJJRT514', 'wTgEoQGqng', 'xomMZGhius', 'yFEFYWBSvr', 'z7tGFKsGk6',
+// 'zIJWNK8Z4i', 'zNMGJYI7hq'];
+
+// function _time(f) {
+// var t1 = +(new Date);
+// f();
+// var t2 = +(new Date);
+// return t2 - t1;
+// }
+
+// function listAllRevisionCounts() {
+// var padList = sqlbase.getAllJSONKeys("PAD_META");
+// //padList.length = 10;
+// padList = padList.slice(68000, 68100);
+// padList.forEach(function(id) {
+// model.accessPadGlobal(id, function(pad) {
+// System.out.println((new java.lang.Integer(pad.getHeadRevisionNumber()).toString())+
+// " "+id);
+// dbwriter.writePadNow(pad, true);
+// }, 'r');
+// });
+// }
+
+// function verifyAllPads() {
+// //var padList = sqlbase.getAllJSONKeys("PAD_META");
+// //padList = newlineCorruptedPads;
+// var padList = ['0OCGFKkjDv'];
+// //padList = ['form6rooma'];
+// //padList.length = 10;
+// var numOks = 0;
+// var numErrors = 0;
+// var numNewlineBugs = 0;
+// var longestPad;
+// var longestPadTime = -1;
+// System.out.println(padList.length+" pads.");
+// var totalTime = _time(function() {
+// padList.forEach(function(id) {
+// model.accessPadGlobal(id, function(pad) {
+// var padTime = _time(function() {
+// System.out.print(id+"... ");
+// try {
+// verifyMigration(pad);
+// System.out.println("OK ("+(++numOks)+")");
+// }
+// catch (e) {
+// System.out.println("ERROR ("+(++numErrors)+")"+(e.finalNewlineMissing?" [newline]":""));
+// System.out.println(e.toString());
+// if (e.finalNewlineMissing) {
+// numNewlineBugs++;
+// }
+// }
+// });
+// if (padTime > longestPadTime) {
+// longestPadTime = padTime;
+// longestPad = id;
+// }
+// }, 'r');
+// });
+// });
+// System.out.println("finished verifyAllPads in "+(totalTime/1000)+" seconds.");
+// System.out.println(numOks+" OK");
+// System.out.println(numErrors+" ERROR");
+// System.out.println("Most time-consuming pad: "+longestPad+" / "+longestPadTime+" ms");
+// }
+
+// function _literal(v) {
+// if ((typeof v) == "string") {
+// return '"'+v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n')+'"';
+// }
+// else return v.toSource();
+// }
+
+// function _putFile(str, path) {
+// var writer = new java.io.FileWriter(path);
+// writer.write(str);
+// writer.close();
+// }
diff --git a/etherpad/src/etherpad/pad/exporthtml.js b/etherpad/src/etherpad/pad/exporthtml.js
new file mode 100644
index 0000000..2512603
--- /dev/null
+++ b/etherpad/src/etherpad/pad/exporthtml.js
@@ -0,0 +1,383 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("etherpad.collab.ace.easysync2.Changeset");
+
+function getPadPlainText(pad, revNum) {
+ var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) :
+ pad.atext());
+ var textLines = atext.text.slice(0,-1).split('\n');
+ var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
+ var apool = pad.pool();
+
+ var pieces = [];
+ for(var i=0;i<textLines.length;i++) {
+ var line = _analyzeLine(textLines[i], attribLines[i], apool);
+ if (line.listLevel) {
+ var numSpaces = line.listLevel*2-1;
+ var bullet = '*';
+ pieces.push(new Array(numSpaces+1).join(' '), bullet, ' ', line.text, '\n');
+ }
+ else {
+ pieces.push(line.text, '\n');
+ }
+ }
+
+ return pieces.join('');
+}
+
+function getPadHTML(pad, revNum) {
+ var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) :
+ pad.atext());
+ var textLines = atext.text.slice(0,-1).split('\n');
+ var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
+
+ var apool = pad.pool();
+
+ var tags = ['b','i','u','s','h1','h2','h3','h4','h5','h6'];
+ var props = ['bold','italic','underline','strikethrough','h1','h2','h3','h4','h5','h6'];
+ var anumMap = {};
+ props.forEach(function(propName, i) {
+ var propTrueNum = apool.putAttrib([propName,true], true);
+ if (propTrueNum >= 0) {
+ anumMap[propTrueNum] = i;
+ }
+ });
+
+ function getLineHTML(text, attribs) {
+ var propVals = [false, false, false];
+ var ENTER = 1;
+ var STAY = 2;
+ var LEAVE = 0;
+
+ // Use order of tags (b/i/u) as order of nesting, for simplicity
+ // and decent nesting. For example,
+ // <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
+ // becomes
+ // <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
+
+ var taker = Changeset.stringIterator(text);
+ var assem = Changeset.stringAssembler();
+
+ function emitOpenTag(i) {
+ assem.append('<');
+ assem.append(tags[i]);
+ assem.append('>');
+ }
+ function emitCloseTag(i) {
+ assem.append('</');
+ assem.append(tags[i]);
+ assem.append('>');
+ }
+
+ var urls = _findURLs(text);
+
+ var idx = 0;
+ function processNextChars(numChars) {
+ if (numChars <= 0) {
+ return;
+ }
+
+ var iter = Changeset.opIterator(Changeset.subattribution(attribs,
+ idx, idx+numChars));
+ idx += numChars;
+
+ while (iter.hasNext()) {
+ var o = iter.next();
+ var propChanged = false;
+ Changeset.eachAttribNumber(o.attribs, function(a) {
+ if (a in anumMap) {
+ var i = anumMap[a]; // i = 0 => bold, etc.
+ if (! propVals[i]) {
+ propVals[i] = ENTER;
+ propChanged = true;
+ }
+ else {
+ propVals[i] = STAY;
+ }
+ }
+ });
+ for(var i=0;i<propVals.length;i++) {
+ if (propVals[i] === true) {
+ propVals[i] = LEAVE;
+ propChanged = true;
+ }
+ else if (propVals[i] === STAY) {
+ propVals[i] = true; // set it back
+ }
+ }
+ // now each member of propVal is in {false,LEAVE,ENTER,true}
+ // according to what happens at start of span
+
+ if (propChanged) {
+ // leaving bold (e.g.) also leaves italics, etc.
+ var left = false;
+ for(var i=0;i<propVals.length;i++) {
+ var v = propVals[i];
+ if (! left) {
+ if (v === LEAVE) {
+ left = true;
+ }
+ }
+ else {
+ if (v === true) {
+ propVals[i] = STAY; // tag will be closed and re-opened
+ }
+ }
+ }
+
+ for(var i=propVals.length-1; i>=0; i--) {
+ if (propVals[i] === LEAVE) {
+ emitCloseTag(i);
+ propVals[i] = false;
+ }
+ else if (propVals[i] === STAY) {
+ emitCloseTag(i);
+ }
+ }
+ for(var i=0; i<propVals.length; i++) {
+ if (propVals[i] === ENTER || propVals[i] === STAY) {
+ emitOpenTag(i);
+ propVals[i] = true;
+ }
+ }
+ // propVals is now all {true,false} again
+ } // end if (propChanged)
+
+ var chars = o.chars;
+ if (o.lines) {
+ chars--; // exclude newline at end of line, if present
+ }
+ var s = taker.take(chars);
+
+ assem.append(_escapeHTML(s));
+ } // end iteration over spans in line
+
+ for(var i=propVals.length-1; i>=0; i--) {
+ if (propVals[i]) {
+ emitCloseTag(i);
+ propVals[i] = false;
+ }
+ }
+ } // end processNextChars
+
+ if (urls) {
+ urls.forEach(function(urlData) {
+ var startIndex = urlData[0];
+ var url = urlData[1];
+ var urlLength = url.length;
+ processNextChars(startIndex - idx);
+ assem.append('<a href="'+url.replace(/\"/g, '&quot;')+'">');
+ processNextChars(urlLength);
+ assem.append('</a>');
+ });
+ }
+ processNextChars(text.length - idx);
+
+ return _processSpaces(assem.toString());
+ } // end getLineHTML
+
+ var pieces = [];
+
+ // Need to deal with constraints imposed on HTML lists; can
+ // only gain one level of nesting at once, can't change type
+ // mid-list, etc.
+ // People might use weird indenting, e.g. skip a level,
+ // so we want to do something reasonable there. We also
+ // want to deal gracefully with blank lines.
+ var lists = []; // e.g. [[1,'bullet'], [3,'bullet'], ...]
+ for(var i=0;i<textLines.length;i++) {
+ var line = _analyzeLine(textLines[i], attribLines[i], apool);
+ var lineContent = getLineHTML(line.text, line.aline);
+
+ if (line.listLevel || lists.length > 0) {
+ // do list stuff
+ var whichList = -1; // index into lists or -1
+ if (line.listLevel) {
+ whichList = lists.length;
+ for(var j=lists.length-1;j>=0;j--) {
+ if (line.listLevel <= lists[j][0]) {
+ whichList = j;
+ }
+ }
+ }
+
+ if (whichList >= lists.length) {
+ lists.push([line.listLevel, line.listTypeName]);
+ pieces.push('<ul><li>', lineContent || '<br/>');
+ }
+ else if (whichList == -1) {
+ if (line.text) {
+ // non-blank line, end all lists
+ pieces.push(new Array(lists.length+1).join('</li></ul\n>'));
+ lists.length = 0;
+ pieces.push(lineContent, '<br\n/>');
+ }
+ else {
+ pieces.push('<br/><br\n/>');
+ }
+ }
+ else {
+ while (whichList < lists.length-1) {
+ pieces.push('</li></ul\n>');
+ lists.length--;
+ }
+ pieces.push('</li\n><li>', lineContent || '<br/>');
+ }
+ }
+ else {
+ pieces.push(lineContent, '<br\n/>');
+ }
+ }
+ pieces.push(new Array(lists.length+1).join('</li></ul\n>'));
+
+ return pieces.join('');
+}
+
+function _analyzeLine(text, aline, apool) {
+ var line = {};
+
+ // identify list
+ var lineMarker = 0;
+ line.listLevel = 0;
+ if (aline) {
+ var opIter = Changeset.opIterator(aline);
+ if (opIter.hasNext()) {
+ var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool);
+ if (listType) {
+ lineMarker = 1;
+ listType = /([a-z]+)([12345678])/.exec(listType);
+ if (listType) {
+ line.listTypeName = listType[1];
+ line.listLevel = Number(listType[2]);
+ }
+ }
+ }
+ }
+ if (lineMarker) {
+ line.text = text.substring(1);
+ line.aline = Changeset.subattribution(aline, 1);
+ }
+ else {
+ line.text = text;
+ line.aline = aline;
+ }
+
+ return line;
+}
+
+function getPadHTMLDocument(pad, revNum, noDocType) {
+ var head = (noDocType?'':'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '+
+ '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')+
+ '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">\n'+
+ (noDocType?'':
+ '<head>\n'+
+ '<meta http-equiv="Content-type" content="text/html; charset=utf-8" />\n'+
+ '<meta http-equiv="Content-Language" content="en-us" />\n'+
+ '<title>'+'/'+pad.getId()+'</title>\n'+
+ '<style type="text/css">h1,h2,h3,h4,h5,h6 { display: inline; }</style>\n' +
+ '</head>\n')+
+ '<body>';
+
+ var foot = '</body>\n</html>\n';
+
+ return head + getPadHTML(pad, revNum) + foot;
+}
+
+function _escapeHTML(s) {
+ var re = /[&<>]/g;
+ if (! re.MAP) {
+ // persisted across function calls!
+ re.MAP = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ };
+ }
+ return s.replace(re, function(c) { return re.MAP[c]; });
+}
+
+// copied from ACE
+function _processSpaces(s) {
+ var doesWrap = true;
+ if (s.indexOf("<") < 0 && ! doesWrap) {
+ // short-cut
+ return s.replace(/ /g, '&nbsp;');
+ }
+ var parts = [];
+ s.replace(/<[^>]*>?| |[^ <]+/g, function(m) { parts.push(m); });
+ if (doesWrap) {
+ var endOfLine = true;
+ var beforeSpace = false;
+ // last space in a run is normal, others are nbsp,
+ // end of line is nbsp
+ for(var i=parts.length-1;i>=0;i--) {
+ var p = parts[i];
+ if (p == " ") {
+ if (endOfLine || beforeSpace)
+ parts[i] = '&nbsp;';
+ endOfLine = false;
+ beforeSpace = true;
+ }
+ else if (p.charAt(0) != "<") {
+ endOfLine = false;
+ beforeSpace = false;
+ }
+ }
+ // beginning of line is nbsp
+ for(var i=0;i<parts.length;i++) {
+ var p = parts[i];
+ if (p == " ") {
+ parts[i] = '&nbsp;';
+ break;
+ }
+ else if (p.charAt(0) != "<") {
+ break;
+ }
+ }
+ }
+ else {
+ for(var i=0;i<parts.length;i++) {
+ var p = parts[i];
+ if (p == " ") {
+ parts[i] = '&nbsp;';
+ }
+ }
+ }
+ return parts.join('');
+}
+
+
+// copied from ACE
+var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
+var _REGEX_SPACE = /\s/;
+var _REGEX_URLCHAR = new RegExp('('+/[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source+'|'+_REGEX_WORDCHAR.source+')');
+var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source+_REGEX_URLCHAR.source+'*(?![:.,;])'+_REGEX_URLCHAR.source, 'g');
+
+// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
+function _findURLs(text) {
+ _REGEX_URL.lastIndex = 0;
+ var urls = null;
+ var execResult;
+ while ((execResult = _REGEX_URL.exec(text))) {
+ urls = (urls || []);
+ var startIndex = execResult.index;
+ var url = execResult[0];
+ urls.push([startIndex, url]);
+ }
+
+ return urls;
+}
diff --git a/etherpad/src/etherpad/pad/importhtml.js b/etherpad/src/etherpad/pad/importhtml.js
new file mode 100644
index 0000000..4a48c6f
--- /dev/null
+++ b/etherpad/src/etherpad/pad/importhtml.js
@@ -0,0 +1,230 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+jimport("org.ccil.cowan.tagsoup.Parser");
+jimport("org.ccil.cowan.tagsoup.PYXWriter");
+jimport("java.io.StringReader");
+jimport("java.io.StringWriter");
+jimport("org.xml.sax.InputSource");
+
+import("etherpad.collab.ace.easysync2.{Changeset,AttribPool}");
+import("etherpad.collab.ace.contentcollector.makeContentCollector");
+import("etherpad.collab.collab_server");
+
+function setPadHTML(pad, html) {
+ var atext = htmlToAText(html, pad.pool());
+ collab_server.setPadAText(pad, atext);
+}
+
+function _html2pyx(html) {
+ var p = new Parser();
+ var w = new StringWriter();
+ var h = new PYXWriter(w);
+ p.setContentHandler(h);
+ var s = new InputSource();
+ s.setCharacterStream(new StringReader(html));
+ p.parse(s);
+ return w.toString().replace(/\r\n|\r|\n/g, '\n');
+}
+
+function _htmlBody2js(html) {
+ var pyx = _html2pyx(html);
+ var plines = pyx.split("\n");
+
+ function pyxUnescape(s) {
+ return s.replace(/\\t/g, '\t').replace(/\\/g, '\\');
+ }
+ var inAttrs = false;
+
+ var nodeStack = [];
+ var topNode = {};
+
+ var bodyNode = {name:"body"};
+
+ plines.forEach(function(pline) {
+ var t = pline.charAt(0);
+ var v = pline.substring(1);
+ if (inAttrs && t != 'A') {
+ inAttrs = false;
+ }
+ if (t == '?') { /* ignore */ }
+ else if (t == '(') {
+ var newNode = {name: v};
+ if (v.toLowerCase() == "body") {
+ bodyNode = newNode;
+ }
+ topNode.children = (topNode.children || []);
+ topNode.children.push(newNode);
+ nodeStack.push(topNode);
+ topNode = newNode;
+ inAttrs = true;
+ }
+ else if (t == 'A') {
+ var spaceIndex = v.indexOf(' ');
+ var key = v.substring(0, spaceIndex);
+ var value = pyxUnescape(v.substring(spaceIndex+1));
+ topNode.attrs = (topNode.attrs || {});
+ topNode.attrs['$'+key] = value;
+ }
+ else if (t == '-') {
+ if (v == "\\n") {
+ v = '\n';
+ }
+ else {
+ v = pyxUnescape(v);
+ }
+ if (v) {
+ topNode.children = (topNode.children || []);
+ if (topNode.children.length > 0 &&
+ ((typeof topNode.children[topNode.children.length-1]) == "string")) {
+ // coallesce
+ topNode.children.push(topNode.children.pop() + v);
+ }
+ else {
+ topNode.children.push(v);
+ }
+ }
+ }
+ else if (t == ')') {
+ topNode = nodeStack.pop();
+ }
+ });
+
+ return bodyNode;
+}
+
+function _trimDomNode(n) {
+ function isWhitespace(str) {
+ return /^\s*$/.test(str);
+ }
+ function trimBeginningOrEnd(n, endNotBeginning) {
+ var cc = n.children;
+ var backwards = endNotBeginning;
+ if (cc) {
+ var i = (backwards ? cc.length-1 : 0);
+ var done = false;
+ var hitActualText = false;
+ while (! done) {
+ if (! (backwards ? (i >= 0) : (i < cc.length-1))) {
+ done = true;
+ }
+ else {
+ var c = cc[i];
+ if ((typeof c) == "string") {
+ if (! isWhitespace(c)) {
+ // actual text
+ hitActualText = true;
+ break;
+ }
+ else {
+ // whitespace
+ cc[i] = '';
+ }
+ }
+ else {
+ // recurse
+ if (trimBeginningOrEnd(cc[i], endNotBeginning)) {
+ hitActualText = true;
+ break;
+ }
+ }
+ i += (backwards ? -1 : 1);
+ }
+ }
+ n.children = n.children.filter(function(x) { return !!x; });
+ return hitActualText;
+ }
+ return false;
+ }
+ trimBeginningOrEnd(n, false);
+ trimBeginningOrEnd(n, true);
+}
+
+function htmlToAText(html, apool) {
+ var body = _htmlBody2js(html);
+ _trimDomNode(body);
+
+ var dom = {
+ isNodeText: function(n) {
+ return (typeof n) == "string";
+ },
+ nodeTagName: function(n) {
+ return ((typeof n) == "object") && n.name;
+ },
+ nodeValue: function(n) {
+ return String(n);
+ },
+ nodeNumChildren: function(n) {
+ return (((typeof n) == "object") && n.children && n.children.length) || 0;
+ },
+ nodeChild: function(n, i) {
+ return (((typeof n) == "object") && n.children && n.children[i]) || null;
+ },
+ nodeProp: function(n, p) {
+ return (((typeof n) == "object") && n.attrs && n.attrs[p]) || null;
+ },
+ nodeAttr: function(n, a) {
+ return (((typeof n) == "object") && n.attrs && n.attrs[a]) || null;
+ },
+ optNodeInnerHTML: function(n) {
+ return null;
+ }
+ }
+
+ var cc = makeContentCollector(true, null, apool, dom);
+ for(var i=0; i<dom.nodeNumChildren(body); i++) {
+ var n = dom.nodeChild(body, i);
+ cc.collectContent(n);
+ }
+ cc.notifyNextNode(null);
+ var ccData = cc.finish();
+
+ var textLines = ccData.lines;
+ var attLines = ccData.lineAttribs;
+ for(var i=0;i<textLines.length;i++) {
+ var txt = textLines[i];
+ if (txt == " " || txt == "\xa0") {
+ // space or nbsp all alone on a line, remove
+ textLines[i] = "";
+ attLines[i] = "";
+ }
+ }
+
+ var text = textLines.join('\n')+'\n';
+ var attribs = _joinLineAttribs(attLines);
+ var atext = Changeset.makeAText(text, attribs);
+
+ return atext;
+}
+
+function _joinLineAttribs(lineAttribs) {
+ var assem = Changeset.smartOpAssembler();
+
+ var newline = Changeset.newOp('+');
+ newline.chars = 1;
+ newline.lines = 1;
+
+ lineAttribs.forEach(function(aline) {
+ var iter = Changeset.opIterator(aline);
+ while (iter.hasNext()) {
+ assem.append(iter.next());
+ }
+ assem.append(newline);
+ });
+
+ return assem.toString();
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/pad/model.js b/etherpad/src/etherpad/pad/model.js
new file mode 100644
index 0000000..3f44dfa
--- /dev/null
+++ b/etherpad/src/etherpad/pad/model.js
@@ -0,0 +1,655 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("timer");
+import("sync");
+
+import("etherpad.collab.ace.easysync2.{Changeset,AttribPool}");
+import("etherpad.log");
+import("etherpad.pad.padevents");
+import("etherpad.pad.padutils");
+import("etherpad.pad.dbwriter");
+import("etherpad.pad.pad_migrations");
+import("etherpad.pad.pad_security");
+import("etherpad.collab.collab_server");
+import("cache_utils.syncedWithCache");
+import("etherpad.admin.plugins");
+
+jimport("net.appjet.common.util.LimitedSizeMapping");
+
+jimport("java.lang.System.out.println");
+
+jimport("java.util.concurrent.ConcurrentHashMap");
+jimport("net.appjet.oui.GlobalSynchronizer");
+jimport("net.appjet.oui.exceptionlog");
+
+function onStartup() {
+ appjet.cache.pads = {};
+ appjet.cache.pads.meta = new ConcurrentHashMap();
+ appjet.cache.pads.temp = new ConcurrentHashMap();
+ appjet.cache.pads.revs = new ConcurrentHashMap();
+ appjet.cache.pads.revs10 = new ConcurrentHashMap();
+ appjet.cache.pads.revs100 = new ConcurrentHashMap();
+ appjet.cache.pads.revs1000 = new ConcurrentHashMap();
+ appjet.cache.pads.chat = new ConcurrentHashMap();
+ appjet.cache.pads.revmeta = new ConcurrentHashMap();
+ appjet.cache.pads.authors = new ConcurrentHashMap();
+ appjet.cache.pads.apool = new ConcurrentHashMap();
+}
+
+var _JSON_CACHE_SIZE = 10000;
+
+// to clear: appjet.cache.padmodel.modelcache.map.clear()
+function _getModelCache() {
+ return syncedWithCache('padmodel.modelcache', function(cache) {
+ if (! cache.map) {
+ cache.map = new LimitedSizeMapping(_JSON_CACHE_SIZE);
+ }
+ return cache.map;
+ });
+}
+
+function cleanText(txt) {
+ return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' ');
+}
+
+/**
+ * Access a pad object, which is passed as an argument to
+ * the given padFunc, which is executed inside an exclusive lock,
+ * and return the result. If the pad doesn't exist, a wrapper
+ * object is still created and passed to padFunc, and it can
+ * be used to check whether the pad exists and create it.
+ *
+ * Note: padId is a GLOBAL id.
+ */
+function accessPadGlobal(padId, padFunc, rwMode) {
+ // this may make a nested call to accessPadGlobal, so do it first
+ pad_security.checkAccessControl(padId, rwMode);
+
+ // pad is never loaded into memory (made "active") unless it has been migrated.
+ // Migrations do not use accessPad, but instead access the database directly.
+ pad_migrations.ensureMigrated(padId);
+
+ var mode = (rwMode || "rw").toLowerCase();
+
+ if (! appjet.requestCache.padsAccessing) {
+ appjet.requestCache.padsAccessing = {};
+ }
+ if (appjet.requestCache.padsAccessing[padId]) {
+ // nested access to same pad
+ var p = appjet.requestCache.padsAccessing[padId];
+ var m = p._meta;
+ if (m && mode != "r") {
+ m.status.lastAccess = +new Date();
+ m.status.dirty = true;
+ }
+ return padFunc(p);
+ }
+
+ return doWithPadLock(padId, function() {
+ return sqlcommon.inTransaction(function() {
+ var meta = _getPadMetaData(padId); // null if pad doesn't exist yet
+
+ if (meta && ! meta.status) {
+ meta.status = { validated: false };
+ }
+
+ if (meta && mode != "r") {
+ meta.status.lastAccess = +new Date();
+ }
+
+ function getCurrentAText() {
+ var tempObj = pad.tempObj();
+ if (! tempObj.atext) {
+ tempObj.atext = pad.getInternalRevisionAText(meta.head);
+ }
+ return tempObj.atext;
+ }
+ function addRevision(theChangeset, author, optDatestamp) {
+ var atext = getCurrentAText();
+ var newAText = Changeset.applyToAText(theChangeset, atext, pad.pool());
+ Changeset.copyAText(newAText, atext); // updates pad.tempObj().atext!
+
+ var newRev = ++meta.head;
+
+ var revs = _getPadStringArray(padId, "revs");
+ revs.setEntry(newRev, theChangeset);
+
+ var revmeta = _getPadStringArray(padId, "revmeta");
+ var thisRevMeta = {t: (optDatestamp || (+new Date())),
+ a: getNumForAuthor(author)};
+ if ((newRev % meta.keyRevInterval) == 0) {
+ thisRevMeta.atext = atext;
+ }
+ revmeta.setJSONEntry(newRev, thisRevMeta);
+
+ updateCoarseChangesets(true);
+ }
+ function getNumForAuthor(author, dontAddIfAbsent) {
+ return pad.pool().putAttrib(['author',author||''], dontAddIfAbsent);
+ }
+ function getAuthorForNum(n) {
+ // must return null if n is an attrib number that isn't an author
+ var pair = pad.pool().getAttrib(n);
+ if (pair && pair[0] == 'author') {
+ return pair[1];
+ }
+ return null;
+ }
+
+ function updateCoarseChangesets(onlyIfPresent) {
+ // this is fast to run if the coarse changesets
+ // are up-to-date or almost up-to-date;
+ // if there's no coarse changeset data,
+ // it may take a while.
+
+ if (! meta.coarseHeads) {
+ if (onlyIfPresent) {
+ return;
+ }
+ else {
+ meta.coarseHeads = {10:-1, 100:-1, 1000:-1};
+ }
+ }
+ var head = meta.head;
+ // once we reach head==9, coarseHeads[10] moves
+ // from -1 up to 0; at head==19 it moves up to 1
+ var desiredCoarseHeads = {
+ 10: Math.floor((head-9)/10),
+ 100: Math.floor((head-99)/100),
+ 1000: Math.floor((head-999)/1000)
+ };
+ var revs = _getPadStringArray(padId, "revs");
+ var revs10 = _getPadStringArray(padId, "revs10");
+ var revs100 = _getPadStringArray(padId, "revs100");
+ var revs1000 = _getPadStringArray(padId, "revs1000");
+ var fineArrays = [revs, revs10, revs100];
+ var coarseArrays = [revs10, revs100, revs1000];
+ var levels = [10, 100, 1000];
+ var dirty = false;
+ for(var z=0;z<3;z++) {
+ var level = levels[z];
+ var coarseArray = coarseArrays[z];
+ var fineArray = fineArrays[z];
+ while (meta.coarseHeads[level] < desiredCoarseHeads[level]) {
+ dirty = true;
+ // for example, if the current coarse head is -1,
+ // compose 0-9 inclusive of the finer level and call it 0
+ var x = meta.coarseHeads[level] + 1;
+ var cs = fineArray.getEntry(10 * x);
+ for(var i=1;i<=9;i++) {
+ cs = Changeset.compose(cs, fineArray.getEntry(10*x + i),
+ pad.pool());
+ }
+ coarseArray.setEntry(x, cs);
+ meta.coarseHeads[level] = x;
+ }
+ }
+ if (dirty) {
+ meta.status.dirty = true;
+ }
+ }
+
+ /////////////////// "Public" API starts here (functions used by collab_server or other modules)
+ var pad = {
+ // Operations that write to the data structure should
+ // set meta.dirty = true. Any pad access that isn't
+ // done in "read" mode also sets dirty = true.
+ getId: function() { return padId; },
+ exists: function() { return !!meta; },
+ create: function(optText) {
+ meta = {};
+ meta.head = -1; // incremented below by addRevision
+ pad.tempObj().atext = Changeset.makeAText("\n");
+ meta.padId = padId,
+ meta.keyRevInterval = 100;
+ meta.numChatMessages = 0;
+ var t = +new Date();
+ meta.status = { validated: true };
+ meta.status.lastAccess = t;
+ meta.status.dirty = true;
+ meta.supportsTimeSlider = true;
+
+ var firstChangeset = Changeset.makeSplice("\n", 0, 0,
+ cleanText(optText || ''));
+ addRevision(firstChangeset, '');
+
+ _insertPadMetaData(padId, meta);
+
+ sqlobj.insert("PAD_SQLMETA", {
+ id: padId, version: 2, creationTime: new Date(t), lastWriteTime: new Date(),
+ headRev: meta.head }); // headRev is not authoritative, just for info
+
+ padevents.onNewPad(pad);
+ },
+ destroy: function() { // you may want to collab_server.bootAllUsers first
+ padevents.onDestroyPad(pad);
+
+ _destroyPadStringArray(padId, "revs");
+ _destroyPadStringArray(padId, "revs10");
+ _destroyPadStringArray(padId, "revs100");
+ _destroyPadStringArray(padId, "revs1000");
+ _destroyPadStringArray(padId, "revmeta");
+ _destroyPadStringArray(padId, "chat");
+ _destroyPadStringArray(padId, "authors");
+ _removePadMetaData(padId);
+ _removePadAPool(padId);
+ sqlobj.deleteRows("PAD_SQLMETA", { id: padId });
+ meta = null;
+ },
+ writeToDB: function() {
+ var meta2 = {};
+ for(var k in meta) meta2[k] = meta[k];
+ delete meta2.status;
+ sqlbase.putJSON("PAD_META", padId, meta2);
+
+ plugins.callHook("padModelWriteToDB", {pad:pad, padId:padId});
+
+ _getPadStringArray(padId, "revs").writeToDB();
+ _getPadStringArray(padId, "revs10").writeToDB();
+ _getPadStringArray(padId, "revs100").writeToDB();
+ _getPadStringArray(padId, "revs1000").writeToDB();
+ _getPadStringArray(padId, "revmeta").writeToDB();
+ _getPadStringArray(padId, "chat").writeToDB();
+ _getPadStringArray(padId, "authors").writeToDB();
+ sqlbase.putJSON("PAD_APOOL", padId, pad.pool().toJsonable());
+
+ var props = { headRev: meta.head, lastWriteTime: new Date() };
+ _writePadSqlMeta(padId, props);
+ },
+ pool: function() {
+ return _getPadAPool(padId);
+ },
+ getHeadRevisionNumber: function() { return meta.head; },
+ getRevisionAuthor: function(r) {
+ var n = _getPadStringArray(padId, "revmeta").getJSONEntry(r).a;
+ return getAuthorForNum(Number(n));
+ },
+ getRevisionChangeset: function(r) {
+ return _getPadStringArray(padId, "revs").getEntry(r);
+ },
+ tempObj: function() { return _getPadTemp(padId); },
+ getKeyRevisionNumber: function(r) {
+ return Math.floor(r / meta.keyRevInterval) * meta.keyRevInterval;
+ },
+ getInternalRevisionAText: function(r) {
+ var cacheKey = "atext/C/"+r+"/"+padId;
+ var modelCache = _getModelCache();
+ var cachedValue = modelCache.get(cacheKey);
+ if (cachedValue) {
+ modelCache.touch(cacheKey);
+ //java.lang.System.out.println("HIT! "+cacheKey);
+ return Changeset.cloneAText(cachedValue);
+ }
+ //java.lang.System.out.println("MISS! "+cacheKey);
+
+ var revs = _getPadStringArray(padId, "revs");
+ var keyRev = pad.getKeyRevisionNumber(r);
+ var revmeta = _getPadStringArray(padId, "revmeta");
+ var atext = revmeta.getJSONEntry(keyRev).atext;
+ var curRev = keyRev;
+ var targetRev = r;
+ var apool = pad.pool();
+ while (curRev < targetRev) {
+ curRev++;
+ var cs = pad.getRevisionChangeset(curRev);
+ atext = Changeset.applyToAText(cs, atext, apool);
+ }
+ modelCache.put(cacheKey, Changeset.cloneAText(atext));
+ return atext;
+ },
+ getInternalRevisionText: function(r, optInfoObj) {
+ var atext = pad.getInternalRevisionAText(r);
+ var text = atext.text;
+ if (optInfoObj) {
+ if (text.slice(-1) != "\n") {
+ optInfoObj.badLastChar = text.slice(-1);
+ }
+ }
+ return text;
+ },
+ getRevisionText: function(r, optInfoObj) {
+ var internalText = pad.getInternalRevisionText(r, optInfoObj);
+ return internalText.slice(0, -1);
+ },
+ atext: function() { return Changeset.cloneAText(getCurrentAText()); },
+ text: function() { return pad.atext().text; },
+ getRevisionDate: function(r) {
+ var revmeta = _getPadStringArray(padId, "revmeta");
+ return new Date(revmeta.getJSONEntry(r).t);
+ },
+ // note: calls like appendRevision will NOT notify clients of the change!
+ // you must go through collab_server.
+ // Also, be sure to run cleanText() on any text to strip out carriage returns
+ // and other stuff.
+ appendRevision: function(theChangeset, author, optDatestamp) {
+ addRevision(theChangeset, author || '', optDatestamp);
+ },
+ appendChatMessage: function(obj) {
+ var index = meta.numChatMessages;
+ meta.numChatMessages++;
+ var chat = _getPadStringArray(padId, "chat");
+ chat.setJSONEntry(index, obj);
+ },
+ getNumChatMessages: function() {
+ return meta.numChatMessages;
+ },
+ getChatMessage: function(i) {
+ var chat = _getPadStringArray(padId, "chat");
+ return chat.getJSONEntry(i);
+ },
+ getPadOptionsObj: function() {
+ var data = pad.getDataRoot();
+ if (! data.padOptions) {
+ data.padOptions = {};
+ }
+ if ((! data.padOptions.guestPolicy) ||
+ (data.padOptions.guestPolicy == 'ask')) {
+ data.padOptions.guestPolicy = 'deny';
+ }
+ return data.padOptions;
+ },
+ getGuestPolicy: function() {
+ // allow/ask/deny
+ return pad.getPadOptionsObj().guestPolicy;
+ },
+ setGuestPolicy: function(policy) {
+ pad.getPadOptionsObj().guestPolicy = policy;
+ },
+ getDataRoot: function() {
+ var dataRoot = meta.dataRoot;
+ if (! dataRoot) {
+ dataRoot = {};
+ meta.dataRoot = dataRoot;
+ }
+ return dataRoot;
+ },
+ // returns an object, changes to which are not reflected
+ // in the DB; use setAuthorData for mutation
+ getAuthorData: function(author) {
+ var authors = _getPadStringArray(padId, "authors");
+ var n = getNumForAuthor(author, true);
+ if (n < 0) {
+ return null;
+ }
+ else {
+ return authors.getJSONEntry(n);
+ }
+ },
+ setAuthorData: function(author, data) {
+ var authors = _getPadStringArray(padId, "authors");
+ var n = getNumForAuthor(author);
+ authors.setJSONEntry(n, data);
+ },
+ adoptChangesetAttribs: function(cs, oldPool) {
+ return Changeset.moveOpsToNewPool(cs, oldPool, pad.pool());
+ },
+ eachATextAuthor: function(atext, func) {
+ var seenNums = {};
+ Changeset.eachAttribNumber(atext.attribs, function(n) {
+ if (! seenNums[n]) {
+ seenNums[n] = true;
+ var author = getAuthorForNum(n);
+ if (author) {
+ func(author, n);
+ }
+ }
+ });
+ },
+ getCoarseChangeset: function(start, numChangesets) {
+ updateCoarseChangesets();
+
+ if (!(numChangesets == 10 || numChangesets == 100 ||
+ numChangesets == 1000)) {
+ return null;
+ }
+ var level = numChangesets;
+ var x = Math.floor(start / level);
+ if (!(x >= 0 && x*level == start)) {
+ return null;
+ }
+
+ var cs = _getPadStringArray(padId, "revs"+level).getEntry(x);
+
+ if (! cs) {
+ return null;
+ }
+
+ return cs;
+ },
+ getSupportsTimeSlider: function() {
+ if (! ('supportsTimeSlider' in meta)) {
+ if (padutils.isProPadId(padId)) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+ else {
+ return !! meta.supportsTimeSlider;
+ }
+ },
+ setSupportsTimeSlider: function(v) {
+ meta.supportsTimeSlider = v;
+ },
+ get _meta() { return meta; }
+ };
+
+ try {
+ padutils.setCurrentPad(padId);
+ appjet.requestCache.padsAccessing[padId] = pad;
+ return padFunc(pad);
+ }
+ finally {
+ padutils.clearCurrentPad();
+ delete appjet.requestCache.padsAccessing[padId];
+ if (meta) {
+ if (mode != "r") {
+ meta.status.dirty = true;
+ }
+ if (meta.status.dirty) {
+ dbwriter.notifyPadDirty(padId);
+ }
+ }
+ }
+ });
+ });
+}
+
+/**
+ * Call an arbitrary function with no arguments inside an exclusive
+ * lock on a padId, and return the result.
+ */
+function doWithPadLock(padId, func) {
+ var lockName = "document/"+padId;
+ return sync.doWithStringLock(lockName, func);
+}
+
+function isPadLockHeld(padId) {
+ var lockName = "document/"+padId;
+ return GlobalSynchronizer.isHeld(lockName);
+}
+
+/**
+ * Get pad meta-data object, which is stored in SQL as JSON
+ * but cached in appjet.cache. Returns null if pad doesn't
+ * exist at all (does NOT create it). Requires pad lock.
+ */
+function _getPadMetaData(padId) {
+ var padMeta = appjet.cache.pads.meta.get(padId);
+ if (! padMeta) {
+ // not in cache
+ padMeta = sqlbase.getJSON("PAD_META", padId);
+ if (! padMeta) {
+ // not in SQL
+ padMeta = null;
+ }
+ else {
+ appjet.cache.pads.meta.put(padId, padMeta);
+ }
+ }
+ return padMeta;
+}
+
+/**
+ * Sets a pad's meta-data object, such as when creating
+ * a pad for the first time. Requires pad lock.
+ */
+function _insertPadMetaData(padId, obj) {
+ appjet.cache.pads.meta.put(padId, obj);
+}
+
+/**
+ * Removes a pad's meta data, writing through to the database.
+ * Used for the rare case of deleting a pad.
+ */
+function _removePadMetaData(padId) {
+ appjet.cache.pads.meta.remove(padId);
+ sqlbase.deleteJSON("PAD_META", padId);
+}
+
+function _getPadAPool(padId) {
+ var padAPool = appjet.cache.pads.apool.get(padId);
+ if (! padAPool) {
+ // not in cache
+ padAPool = new AttribPool();
+ padAPoolJson = sqlbase.getJSON("PAD_APOOL", padId);
+ if (padAPoolJson) {
+ // in SQL
+ padAPool.fromJsonable(padAPoolJson);
+ }
+ appjet.cache.pads.apool.put(padId, padAPool);
+ }
+ return padAPool;
+}
+
+/**
+ * Removes a pad's apool data, writing through to the database.
+ * Used for the rare case of deleting a pad.
+ */
+function _removePadAPool(padId) {
+ appjet.cache.pads.apool.remove(padId);
+ sqlbase.deleteJSON("PAD_APOOL", padId);
+}
+
+/**
+ * Get an object for a pad that's not persisted in storage,
+ * e.g. for tracking open connections. Creates object
+ * if necessary. Requires pad lock.
+ */
+function _getPadTemp(padId) {
+ var padTemp = appjet.cache.pads.temp.get(padId);
+ if (! padTemp) {
+ padTemp = {};
+ appjet.cache.pads.temp.put(padId, padTemp);
+ }
+ return padTemp;
+}
+
+/**
+ * Returns an object with methods for manipulating a string array, where name
+ * is something like "revs" or "chat". The object must be acquired and used
+ * all within a pad lock.
+ */
+function _getPadStringArray(padId, name) {
+ var padFoo = appjet.cache.pads[name].get(padId);
+ if (! padFoo) {
+ padFoo = {};
+ // writes go into writeCache, which is authoritative for reads;
+ // reads cause pages to be read into readCache
+ padFoo.readCache = {};
+ padFoo.writeCache = {};
+ appjet.cache.pads[name].put(padId, padFoo);
+ }
+ var tableName = "PAD_"+name.toUpperCase();
+ var self = {
+ getEntry: function(idx) {
+ var n = Number(idx);
+ if (padFoo.writeCache[n]) return padFoo.writeCache[n];
+ if (padFoo.readCache[n]) return padFoo.readCache[n];
+ sqlbase.getPageStringArrayElements(tableName, padId, n, padFoo.readCache);
+ return padFoo.readCache[n]; // null if not present in SQL
+ },
+ setEntry: function(idx, value) {
+ var n = Number(idx);
+ var v = String(value);
+ padFoo.writeCache[n] = v;
+ },
+ getJSONEntry: function(idx) {
+ var result = self.getEntry(idx);
+ if (! result) return result;
+ return fastJSON.parse(String(result));
+ },
+ setJSONEntry: function(idx, valueObj) {
+ self.setEntry(idx, fastJSON.stringify(valueObj));
+ },
+ writeToDB: function() {
+ sqlbase.putDictStringArrayElements(tableName, padId, padFoo.writeCache);
+ // copy key-vals of writeCache into readCache
+ var readCache = padFoo.readCache;
+ var writeCache = padFoo.writeCache;
+ for(var p in writeCache) {
+ readCache[p] = writeCache[p];
+ }
+ padFoo.writeCache = {};
+ }
+ };
+ return self;
+}
+
+/**
+ * Destroy a string array; writes through to the database. Must be
+ * called within a pad lock.
+ */
+function _destroyPadStringArray(padId, name) {
+ appjet.cache.pads[name].remove(padId);
+ var tableName = "PAD_"+name.toUpperCase();
+ sqlbase.clearStringArray(tableName, padId);
+}
+
+/**
+ * SELECT the row of PAD_SQLMETA for the given pad. Requires pad lock.
+ */
+function _getPadSqlMeta(padId) {
+ return sqlobj.selectSingle("PAD_SQLMETA", { id: padId });
+}
+
+function _writePadSqlMeta(padId, updates) {
+ sqlobj.update("PAD_SQLMETA", { id: padId }, updates);
+}
+
+
+// called from dbwriter
+function removeFromMemory(pad) {
+ // safe to call if all data is written to SQL, otherwise will lose data;
+ var padId = pad.getId();
+ appjet.cache.pads.meta.remove(padId);
+ appjet.cache.pads.revs.remove(padId);
+ appjet.cache.pads.revs10.remove(padId);
+ appjet.cache.pads.revs100.remove(padId);
+ appjet.cache.pads.revs1000.remove(padId);
+ appjet.cache.pads.chat.remove(padId);
+ appjet.cache.pads.revmeta.remove(padId);
+ appjet.cache.pads.apool.remove(padId);
+ collab_server.removeFromMemory(pad);
+}
+
+
diff --git a/etherpad/src/etherpad/pad/noprowatcher.js b/etherpad/src/etherpad/pad/noprowatcher.js
new file mode 100644
index 0000000..8eb2a92
--- /dev/null
+++ b/etherpad/src/etherpad/pad/noprowatcher.js
@@ -0,0 +1,110 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * noprowatcher keeps track of when a pad has had no pro user
+ * in it for a certain period of time, after which all guests
+ * are booted.
+ */
+
+import("etherpad.pad.padutils");
+import("etherpad.collab.collab_server");
+import("etherpad.pad.padusers");
+import("etherpad.pad.pad_security");
+import("etherpad.pad.model");
+import("cache_utils.syncedWithCache");
+import("execution");
+import("etherpad.sessions");
+
+function onStartup() {
+ execution.initTaskThreadPool("noprowatcher", 1);
+}
+
+function getNumProUsers(pad) {
+ var n = 0;
+ collab_server.getConnectedUsers(pad).forEach(function(info) {
+ if (! padusers.isGuest(info.userId)) {
+ n++; // found a non-guest
+ }
+ });
+ return n;
+}
+
+var _EMPTY_TIME = 60000;
+
+function checkPad(padOrPadId) {
+ if ((typeof padOrPadId) == "string") {
+ return model.accessPadGlobal(padOrPadId, function(pad) {
+ return checkPad(pad);
+ });
+ }
+ var pad = padOrPadId;
+
+ if (! padutils.isProPad(pad)) {
+ return; // public pad
+ }
+
+ if (pad.getGuestPolicy() == 'allow') {
+ return; // public access
+ }
+
+ if (sessions.isAnEtherpadAdmin()) {
+ return;
+ }
+
+ var globalPadId = pad.getId();
+
+ var numConnections = collab_server.getNumConnections(pad);
+ var numProUsers = getNumProUsers(pad);
+ syncedWithCache('noprowatcher.no_pros_since', function(noProsSince) {
+ if (! numConnections) {
+ // no connections, clear state and we're done
+ delete noProsSince[globalPadId];
+ }
+ else if (numProUsers) {
+ // pro users in pad, so we're not in a span of time with
+ // no pro users
+ delete noProsSince[globalPadId];
+ }
+ else {
+ // no pro users in pad
+ var since = noProsSince[globalPadId];
+ if (! since) {
+ // no entry in cache, that means last time we checked
+ // there were still pro users, but now there aren't
+ noProsSince[globalPadId] = +new Date;
+ execution.scheduleTask("noprowatcher", "noProWatcherCheckPad",
+ _EMPTY_TIME+1000, [globalPadId]);
+ }
+ else {
+ // already in a span of time with no pro users
+ if ((+new Date) - since > _EMPTY_TIME) {
+ // _EMPTY_TIME milliseconds since we first noticed no pro users
+ collab_server.bootAllUsersFromPad(pad, "unauth");
+ pad_security.revokeAllPadAccess(globalPadId);
+ }
+ }
+ }
+ });
+}
+
+function onUserJoin(pad, userInfo) {
+ checkPad(pad);
+}
+
+function onUserLeave(pad, userInfo) {
+ checkPad(pad);
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/pad/pad_migrations.js b/etherpad/src/etherpad/pad/pad_migrations.js
new file mode 100644
index 0000000..e81cf63
--- /dev/null
+++ b/etherpad/src/etherpad/pad/pad_migrations.js
@@ -0,0 +1,206 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("etherpad.pad.model");
+import("etherpad.pad.easysync2migration");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("etherpad.log");
+import("etherpad.pne.pne_utils");
+jimport("java.util.concurrent.ConcurrentHashMap");
+jimport("java.lang.System");
+jimport("java.util.ArrayList");
+jimport("java.util.Collections");
+
+function onStartup() {
+ if (! appjet.cache.pad_migrations) {
+ appjet.cache.pad_migrations = {};
+ }
+
+ // this part can be removed when all pads are migrated on pad.spline.inf.fu-berlin.de
+ //if (! pne_utils.isPNE()) {
+ // System.out.println("Building cache for live migrations...");
+ // initLiveMigration();
+ //}
+}
+
+function initLiveMigration() {
+
+ if (! appjet.cache.pad_migrations) {
+ appjet.cache.pad_migrations = {};
+ }
+ appjet.cache.pad_migrations.doingAnyLiveMigrations = true;
+ appjet.cache.pad_migrations.doingBackgroundLiveMigrations = true;
+ appjet.cache.pad_migrations.padMap = new ConcurrentHashMap();
+
+ // presence of a pad in padMap indicates migration is needed
+ var padMap = _padMap();
+ var migrationsNeeded = sqlobj.selectMulti("PAD_SQLMETA", {version: 1});
+ migrationsNeeded.forEach(function(obj) {
+ padMap.put(String(obj.id), {from: obj.version});
+ });
+}
+
+function _padMap() {
+ return appjet.cache.pad_migrations.padMap;
+}
+
+function _doingItLive() {
+ return !! appjet.cache.pad_migrations.doingAnyLiveMigrations;
+}
+
+function checkPadStatus(padId) {
+ if (! _doingItLive()) {
+ return "ready";
+ }
+ var info = _padMap().get(padId);
+ if (! info) {
+ return "ready";
+ }
+ else if (info.migrating) {
+ return "migrating";
+ }
+ else {
+ return "oldversion";
+ }
+}
+
+function ensureMigrated(padId, async) {
+ if (! _doingItLive()) {
+ return false;
+ }
+
+ var info = _padMap().get(padId);
+ if (! info) {
+ // pad is up-to-date
+ return false;
+ }
+ else if (async && info.migrating) {
+ // pad is already being migrated, don't wait on the lock
+ return false;
+ }
+
+ return model.doWithPadLock(padId, function() {
+ // inside pad lock...
+ var info = _padMap().get(padId);
+ if (!info) {
+ return false;
+ }
+ // migrate from version 1 to version 2 in a transaction
+ var migrateSucceeded = false;
+ try {
+ info.migrating = true;
+ log.info("Migrating pad "+padId+" from version 1 to version 2...");
+
+ var success = false;
+ var whichTry = 1;
+ while ((! success) && whichTry <= 3) {
+ success = sqlcommon.inTransaction(function() {
+ try {
+ easysync2migration.migratePad(padId);
+ sqlobj.update("PAD_SQLMETA", {id: padId}, {version: 2});
+ return true;
+ }
+ catch (e if (e.toString().indexOf("try restarting transaction") >= 0)) {
+ whichTry++;
+ return false;
+ }
+ });
+ if (! success) {
+ java.lang.Thread.sleep(Math.floor(Math.random()*200));
+ }
+ }
+ if (! success) {
+ throw new Error("too many retries");
+ }
+
+ migrateSucceeded = true;
+ log.info("Migrated pad "+padId+".");
+ _padMap().remove(padId);
+ }
+ finally {
+ info.migrating = false;
+ if (! migrateSucceeded) {
+ log.info("Migration failed for pad "+padId+".");
+ throw new Error("Migration failed for pad "+padId+".");
+ }
+ }
+ return true;
+ });
+}
+
+function numUnmigratedPads() {
+ if (! _doingItLive()) {
+ return 0;
+ }
+
+ return _padMap().size();
+}
+
+////////// BACKGROUND MIGRATIONS
+
+function _logPadMigration(runnerId, padNumber, padTotal, timeMs, fourCharResult, padId) {
+ log.custom("pad_migrations", {
+ runnerId: runnerId,
+ padNumber: Math.round(padNumber+1),
+ padTotal: Math.round(padTotal),
+ timeMs: Math.round(timeMs),
+ fourCharResult: fourCharResult,
+ padId: padId});
+}
+
+function _getNeededMigrationsArrayList(filter) {
+ var L = new ArrayList(_padMap().keySet());
+ for(var i=L.size()-1; i>=0; i--) {
+ if (! filter(String(L.get(i)))) {
+ L.remove(i);
+ }
+ }
+ return L;
+}
+
+function runBackgroundMigration(residue, modulus, runnerId) {
+ var L = _getNeededMigrationsArrayList(function(padId) {
+ return (padId.charCodeAt(0) % modulus) == residue;
+ });
+ Collections.shuffle(L);
+
+ var totalPads = L.size();
+ for(var i=0;i<totalPads;i++) {
+ if (! appjet.cache.pad_migrations.doingBackgroundLiveMigrations) {
+ break;
+ }
+ var padId = L.get(i);
+ var result = "FAIL";
+ var t1 = System.currentTimeMillis();
+ try {
+ if (ensureMigrated(padId, true)) {
+ result = " OK "; // migrated successfully
+ }
+ else {
+ result = " -- "; // no migration needed after all
+ }
+ }
+ catch (e) {
+ // e just says "migration failed", but presumably
+ // inTransaction() printed a stack trace.
+ // result == "FAIL", do nothing.
+ }
+ var t2 = System.currentTimeMillis();
+ _logPadMigration(runnerId, i, totalPads, t2 - t1, result, padId);
+ }
+}
diff --git a/etherpad/src/etherpad/pad/pad_security.js b/etherpad/src/etherpad/pad/pad_security.js
new file mode 100644
index 0000000..0ff8783
--- /dev/null
+++ b/etherpad/src/etherpad/pad/pad_security.js
@@ -0,0 +1,237 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("stringutils");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.sessions.getSession");
+import("etherpad.sessions");
+
+import("etherpad.pad.model");
+import("etherpad.pad.padutils");
+import("etherpad.pad.padusers");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_utils.isProDomainRequest");
+import("etherpad.pad.noprowatcher");
+
+//--------------------------------------------------------------------------------
+// granting session permanent access to pads (for the session)
+//--------------------------------------------------------------------------------
+
+function _grantSessionAccessTo(globalPadId) {
+ var userId = padusers.getUserId();
+ syncedWithCache("pad-auth."+globalPadId, function(c) {
+ c[userId] = true;
+ });
+}
+
+function _doesSessionHaveAccessTo(globalPadId) {
+ var userId = padusers.getUserId();
+ return syncedWithCache("pad-auth."+globalPadId, function(c) {
+ return c[userId];
+ });
+}
+
+function revokePadUserAccess(globalPadId, userId) {
+ syncedWithCache("pad-auth."+globalPadId, function(c) {
+ delete c[userId];
+ });
+}
+
+function revokeAllPadAccess(globalPadId) {
+ syncedWithCache("pad-auth."+globalPadId, function(c) {
+ for (k in c) {
+ delete c[k];
+ }
+ });
+}
+
+//--------------------------------------------------------------------------------
+// knock/answer
+//--------------------------------------------------------------------------------
+
+function clearKnockStatus(userId, globalPadId) {
+ syncedWithCache("pad-guest-knocks."+globalPadId, function(c) {
+ delete c[userId];
+ });
+}
+
+// called by collab_server when accountholders approve or deny
+function answerKnock(userId, globalPadId, status) {
+ // status is either "approved" or "denied"
+ syncedWithCache("pad-guest-knocks."+globalPadId, function(c) {
+ // If two account-holders respond to the knock, keep the first one.
+ if (!c[userId]) {
+ c[userId] = status;
+ }
+ });
+}
+
+// returns "approved", "denied", or undefined
+function getKnockAnswer(userId, globalPadId) {
+ return syncedWithCache("pad-guest-knocks."+globalPadId, function(c) {
+ return c[userId];
+ });
+}
+
+//--------------------------------------------------------------------------------
+// main entrypoint called for every accessPad()
+//--------------------------------------------------------------------------------
+
+var _insideCheckAccessControl = false;
+
+function checkAccessControl(globalPadId, rwMode) {
+ if (!request.isDefined) {
+ return; // TODO: is this the right thing to do here?
+ // Empirical evidence indicates request.isDefined during comet requests,
+ // but not during tasks, which is the behavior we want.
+ }
+
+ if (_insideCheckAccessControl) {
+ // checkAccessControl is always allowed to access pads itself
+ return;
+ }
+ if (isProDomainRequest() && (request.path == "/ep/account/guest-knock")) {
+ return;
+ }
+ if (!isProDomainRequest() && (request.path == "/ep/admin/padinspector")) {
+ return;
+ }
+ if (isProDomainRequest() && (request.path == "/ep/padlist/all-pads.zip")) {
+ return;
+ }
+ try {
+ _insideCheckAccessControl = true;
+
+ if (!padutils.isProPadId(globalPadId)) {
+ // no access control on non-pro pads yet.
+ return;
+ }
+
+ if (sessions.isAnEtherpadAdmin()) {
+ return;
+ }
+ if (_doesSessionHaveAccessTo(globalPadId)) {
+ return;
+ }
+ _checkDomainSecurity(globalPadId);
+ _checkGuestSecurity(globalPadId);
+ _checkPasswordSecurity(globalPadId);
+
+ // remember that this user has access
+ _grantSessionAccessTo(globalPadId);
+ }
+ finally {
+ // this always runs, even on error or stop
+ _insideCheckAccessControl = false;
+ }
+}
+
+function _checkDomainSecurity(globalPadId) {
+ var padDomainId = padutils.getDomainId(globalPadId);
+ if (!padDomainId) {
+ return; // global pad
+ }
+ if (pro_utils.isProDomainRequest()) {
+ var requestDomainId = domains.getRequestDomainId();
+ if (requestDomainId != padDomainId) {
+ throw Error("Request cross-domain pad access not allowed.");
+ }
+ }
+}
+
+function _checkGuestSecurity(globalPadId) {
+ if (!getSession().guestPadAccess) {
+ getSession().guestPadAccess = {};
+ }
+
+ var padDomainId = padutils.getDomainId(globalPadId);
+ var isAccountHolder = pro_accounts.isAccountSignedIn();
+ if (isAccountHolder) {
+ if (getSessionProAccount().domainId != padDomainId) {
+ throw Error("Account cross-domain pad access not allowed.");
+ }
+ return; // OK
+ }
+
+ // Not an account holder ==> Guest
+
+ // returns either "allow", "ask", or "deny"
+ var guestPolicy = model.accessPadGlobal(globalPadId, function(p) {
+ if (!p.exists()) {
+ return "deny";
+ } else {
+ return p.getGuestPolicy();
+ }
+ });
+
+ var numProUsers = model.accessPadGlobal(globalPadId, function(pad) {
+ return noprowatcher.getNumProUsers(pad);
+ });
+
+ if (guestPolicy == "allow") {
+ return;
+ }
+ if (guestPolicy == "deny") {
+ pro_accounts.requireAccount("Guests are not allowed to join that pad. Please sign in.");
+ }
+ if (guestPolicy == "ask") {
+ if (numProUsers < 1) {
+ pro_accounts.requireAccount("This pad's security policy does not allow guests to join unless an account-holder is connected to the pad.");
+ }
+ var userId = padusers.getUserId();
+
+ // one of {"approved", "denied", undefined}
+ var knockAnswer = getKnockAnswer(userId, globalPadId);
+ if (knockAnswer == "approved") {
+ return;
+ } else {
+ var localPadId = padutils.globalToLocalId(globalPadId);
+ response.redirect('/ep/account/guest-sign-in?padId='+encodeURIComponent(localPadId));
+ }
+ }
+}
+
+function _checkPasswordSecurity(globalPadId) {
+ if (!getSession().padPasswordAuth) {
+ getSession().padPasswordAuth = {};
+ }
+ if (getSession().padPasswordAuth[globalPadId] == true) {
+ return;
+ }
+ var domainId = padutils.getDomainId(globalPadId);
+ var localPadId = globalPadId.split("$")[1];
+
+ if (stringutils.startsWith(request.path, "/ep/admin/recover-padtext")) {
+ return;
+ }
+
+ var p = pro_padmeta.accessProPad(globalPadId, function(propad) {
+ if (propad.exists()) {
+ return propad.getPassword();
+ } else {
+ return null;
+ }
+ });
+ if (p) {
+ response.redirect('/ep/pad/auth/'+localPadId+'?cont='+encodeURIComponent(request.url));
+ }
+}
+
diff --git a/etherpad/src/etherpad/pad/padevents.js b/etherpad/src/etherpad/pad/padevents.js
new file mode 100644
index 0000000..52b303c
--- /dev/null
+++ b/etherpad/src/etherpad/pad/padevents.js
@@ -0,0 +1,170 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// src/etherpad/events.js
+
+import("etherpad.licensing");
+import("etherpad.log");
+import("etherpad.pad.chatarchive");
+import("etherpad.pad.activepads");
+import("etherpad.pad.padutils");
+import("etherpad.sessions");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pro.pro_pad_db");
+import("etherpad.pad.padusers");
+import("etherpad.pad.pad_security");
+import("etherpad.pad.noprowatcher");
+import("etherpad.collab.collab_server");
+jimport("java.lang.System.out.println");
+
+function onNewPad(pad) {
+ log.custom("padevents", {
+ type: "newpad",
+ padId: pad.getId()
+ });
+ pro_pad_db.onCreatePad(pad);
+}
+
+function onDestroyPad(pad) {
+ log.custom("padevents", {
+ type: "destroypad",
+ padId: pad.getId()
+ });
+ pro_pad_db.onDestroyPad(pad);
+}
+
+function onUserJoin(pad, userInfo) {
+ log.callCatchingExceptions(function() {
+
+ var name = userInfo.name || "unnamed";
+ log.custom("padevents", {
+ type: "userjoin",
+ padId: pad.getId(),
+ username: name,
+ ip: userInfo.ip,
+ userId: userInfo.userId
+ });
+ activepads.touch(pad.getId());
+ licensing.onUserJoin(userInfo);
+ log.onUserJoin(userInfo.userId);
+ padusers.notifyActive();
+ noprowatcher.onUserJoin(pad, userInfo);
+
+ });
+}
+
+function onUserLeave(pad, userInfo) {
+ log.callCatchingExceptions(function() {
+
+ var name = userInfo.name || "unnamed";
+ log.custom("padevents", {
+ type: "userleave",
+ padId: pad.getId(),
+ username: name,
+ ip: userInfo.ip,
+ userId: userInfo.userId
+ });
+ activepads.touch(pad.getId());
+ licensing.onUserLeave(userInfo);
+ noprowatcher.onUserLeave(pad, userInfo);
+
+ });
+}
+
+function onUserInfoChange(pad, userInfo) {
+ log.callCatchingExceptions(function() {
+
+ activepads.touch(pad.getId());
+
+ });
+}
+
+function onClientMessage(pad, senderUserInfo, msg) {
+ var padId = pad.getId();
+ activepads.touch(padId);
+
+ if (msg.type == "chat") {
+
+ chatarchive.onChatMessage(pad, senderUserInfo, msg);
+
+ var name = "unnamed";
+ if (senderUserInfo.name) {
+ name = senderUserInfo.name;
+ }
+
+ log.custom("chat", {
+ padId: padId,
+ userId: senderUserInfo.userId,
+ username: name,
+ text: msg.lineText
+ });
+ }
+ else if (msg.type == "padtitle") {
+ if (msg.title && padutils.isProPadId(pad.getId())) {
+ pro_padmeta.accessProPad(pad.getId(), function(propad) {
+ propad.setTitle(String(msg.title).substring(0, 80));
+ });
+ }
+ }
+ else if (msg.type == "padpassword") {
+ if (padutils.isProPadId(pad.getId())) {
+ pro_padmeta.accessProPad(pad.getId(), function(propad) {
+ propad.setPassword(msg.password || null);
+ });
+ }
+ }
+ else if (msg.type == "padoptions") {
+ // options object is a full set of options or just
+ // some options to change
+ var opts = msg.options;
+ var padOptions = pad.getPadOptionsObj();
+ if (opts.view) {
+ if (! padOptions.view) {
+ padOptions.view = {};
+ }
+ for(var k in opts.view) {
+ padOptions.view[k] = opts.view[k];
+ }
+ }
+ if (opts.guestPolicy) {
+ padOptions.guestPolicy = opts.guestPolicy;
+ if (opts.guestPolicy == 'deny') {
+ // boot guests!
+ collab_server.bootUsersFromPad(pad, "unauth", function(userInfo) {
+ return padusers.isGuest(userInfo.userId); }).forEach(function(userInfo) {
+ pad_security.revokePadUserAccess(padId, userInfo.userId); });
+ }
+ }
+ }
+ else if (msg.type == "guestanswer") {
+ if ((! msg.authId) || padusers.isGuest(msg.authId)) {
+ // not a pro user, forbid.
+ }
+ else {
+ pad_security.answerKnock(msg.guestId, padId, msg.answer);
+ }
+ }
+}
+
+function onEditPad(pad, authorId) {
+ log.callCatchingExceptions(function() {
+
+ pro_pad_db.onEditPad(pad, authorId);
+
+ });
+}
+
+
diff --git a/etherpad/src/etherpad/pad/padusers.js b/etherpad/src/etherpad/pad/padusers.js
new file mode 100644
index 0000000..f04f0eb
--- /dev/null
+++ b/etherpad/src/etherpad/pad/padusers.js
@@ -0,0 +1,397 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("fastJSON");
+import("stringutils");
+import("jsutils.eachProperty");
+import("sync");
+import("etherpad.sessions");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.domains");
+import("stringutils.randomHash");
+
+var _table = cachedSqlTable('pad_guests', 'pad_guests',
+ ['id', 'privateKey', 'userId'], processGuestRow);
+function processGuestRow(row) {
+ row.data = fastJSON.parse(row.data);
+}
+
+function notifySignIn() {
+ /*if (pro_accounts.isAccountSignedIn()) {
+ var proId = getUserId();
+ var guestId = _getGuestUserId();
+
+ var guestUser = _getGuestByKey('userId', guestId);
+ if (guestUser) {
+ var mods = {};
+ mods.data = guestUser.data;
+ // associate guest with proId
+ mods.data.replacement = proId;
+ // de-associate ET cookie with guest, otherwise
+ // the ET cookie would provide a semi-permanent way
+ // to effect changes under the pro account's name!
+ mods.privateKey = "replaced$"+_randomString(20);
+ _updateGuest('userId', guestId, mods);
+ }
+ }*/
+}
+
+function notifyActive() {
+ if (isGuest(getUserId())) {
+ _updateGuest('userId', getUserId(), {});
+ }
+}
+
+function notifyUserData(userData) {
+ var uid = getUserId();
+ if (isGuest(uid)) {
+ var data = _getGuestByKey('userId', uid).data;
+ if (userData.name) {
+ data.name = userData.name;
+ }
+ _updateGuest('userId', uid, {data: data});
+ }
+}
+
+function getUserId() {
+ if (pro_accounts.isAccountSignedIn()) {
+ return "p."+(getSessionProAccount().id);
+ }
+ else {
+ return getGuestUserId();
+ }
+}
+
+function getUserName() {
+ var uid = getUserId();
+ if (isGuest(uid)) {
+ var fromSession = sessions.getSession().guestDisplayName;
+ return fromSession || _getGuestByKey('userId', uid).data.name || null;
+ }
+ else {
+ return getSessionProAccount().fullName;
+ }
+}
+
+function getAccountIdForProAuthor(uid) {
+ if (uid.indexOf("p.") == 0) {
+ return Number(uid.substring(2));
+ }
+ else {
+ return -1;
+ }
+}
+
+function getNameForUserId(uid) {
+ if (isGuest(uid)) {
+ return _getGuestByKey('userId', uid).data.name || null;
+ }
+ else {
+ var accountNum = getAccountIdForProAuthor(uid);
+ if (accountNum < 0) {
+ return null;
+ }
+ else {
+ return pro_accounts.getAccountById(accountNum).fullName;
+ }
+ }
+}
+
+function isGuest(userId) {
+ return /^g/.test(userId);
+}
+
+function getGuestUserId() {
+ // cache the userId in the requestCache,
+ // for efficiency and consistency
+ var c = appjet.requestCache;
+ if (c.padGuestUserId === undefined) {
+ c.padGuestUserId = _computeGuestUserId();
+ }
+ return c.padGuestUserId;
+}
+
+function _getGuestTrackerId() {
+ // get ET cookie
+ var tid = sessions.getTrackingId();
+ if (tid == '-') {
+ // no tracking cookie? not a normal request?
+ return null;
+ }
+
+ // get domain ID
+ var domain = "-";
+ if (pro_utils.isProDomainRequest()) {
+ // e.g. "3"
+ domain = String(domains.getRequestDomainId());
+ }
+
+ // combine them
+ return domain+"$"+tid;
+}
+
+function _insertGuest(obj) {
+ // only requires 'userId' in obj
+
+ obj.createdDate = new Date;
+ obj.lastActiveDate = new Date;
+ if (! obj.data) {
+ obj.data = {};
+ }
+ if ((typeof obj.data) == "object") {
+ obj.data = fastJSON.stringify(obj.data);
+ }
+ if (! obj.privateKey) {
+ // private keys must be unique
+ obj.privateKey = "notracker$"+_randomString(20);
+ }
+
+ return _table.insert(obj);
+}
+
+function _getGuestByKey(keyColumn, value) {
+ return _table.getByKey(keyColumn, value);
+}
+
+function _updateGuest(keyColumn, value, obj) {
+ var obj2 = {};
+ eachProperty(obj, function(k,v) {
+ if (k == "data" && (typeof v) == "object") {
+ obj2.data = fastJSON.stringify(v);
+ }
+ else {
+ obj2[k] = v;
+ }
+ });
+
+ obj2.lastActiveDate = new Date;
+
+ _table.updateByKey(keyColumn, value, obj2);
+}
+
+function _newGuestUserId() {
+ return "g."+_randomString(16);
+}
+
+function _computeGuestUserId() {
+ // always returns some userId
+
+ var privateKey = _getGuestTrackerId();
+
+ if (! privateKey) {
+ // no tracking cookie, pretend there is one
+ privateKey = randomHash(16);
+ }
+
+ var userFromTracker = _table.getByKey('privateKey', privateKey);
+ if (userFromTracker) {
+ // we know this guy
+ return userFromTracker.userId;
+ }
+
+ // generate userId
+ var userId = _newGuestUserId();
+ var guest = {userId:userId, privateKey:privateKey};
+ var data = {};
+ guest.data = data;
+
+ var prefsCookieData = _getPrefsCookieData();
+ if (prefsCookieData) {
+ // found an old prefs cookie with an old userId
+ var oldUserId = prefsCookieData.userId;
+ // take the name and preferences
+ if ('name' in prefsCookieData) {
+ data.name = prefsCookieData.name;
+ }
+ /*['fullWidth','viewZoom'].forEach(function(pref) {
+ if (pref in prefsCookieData) {
+ data.prefs[pref] = prefsCookieData[pref];
+ }
+ });*/
+ }
+
+ _insertGuest(guest);
+ return userId;
+}
+
+function _getPrefsCookieData() {
+ // get userId from old prefs cookie if possible,
+ // but don't allow modern usernames
+
+ var prefsCookie = request.cookies['prefs'];
+ if (! prefsCookie) {
+ return null;
+ }
+ if (prefsCookie.charAt(0) != '%') {
+ return null;
+ }
+ try {
+ var cookieData = fastJSON.parse(unescape(prefsCookie));
+ // require one to three digits followed by dot at beginning of userId
+ if (/^[0-9]{1,3}\./.test(String(cookieData.userId))) {
+ return cookieData;
+ }
+ }
+ catch (e) {
+ return null;
+ }
+
+ return null;
+}
+
+function _randomString(len) {
+ // use only numbers and lowercase letters
+ var pieces = [];
+ for(var i=0;i<len;i++) {
+ pieces.push(Math.floor(Math.random()*36).toString(36).slice(-1));
+ }
+ return pieces.join('');
+}
+
+
+function cachedSqlTable(cacheName, tableName, keyColumns, processFetched) {
+ // Keeps a cache of sqlobj rows for the case where
+ // you want to select one row at a time by a single column
+ // at a time, taken from some set of key columns.
+ // The cache maps (keyColumn, value), e.g. ("id", 4) or
+ // ("secondaryKey", "foo123"), to an object, and each
+ // object is either present for all keyColumns
+ // (e.g. "id", "secondaryKey") or none.
+
+ if ((typeof keyColumns) == "string") {
+ keyColumns = [keyColumns];
+ }
+ processFetched = processFetched || (function(o) {});
+
+ function getCache() {
+ // this function is normally fast, only slow when cache
+ // needs to be created for the first time
+ var cache = appjet.cache[cacheName];
+ if (cache) {
+ return cache;
+ }
+ else {
+ // initialize in a synchronized block (double-checked locking);
+ // uses same lock as cache_utils.syncedWithCache would use.
+ sync.doWithStringLock("cache/"+cacheName, function() {
+ if (! appjet.cache[cacheName]) {
+ // values expire after 10 minutes
+ appjet.cache[cacheName] =
+ new net.appjet.common.util.ExpiringMapping(10*60*1000);
+ }
+ });
+ return appjet.cache[cacheName];
+ }
+ }
+
+ function cacheKey(keyColumn, value) {
+ // e.g. "id$4"
+ return keyColumn+"$"+String(value);
+ }
+
+ function getFromCache(keyColumn, value) {
+ return getCache().get(cacheKey(keyColumn, value));
+ }
+ function putInCache(obj) {
+ var cache = getCache();
+ // put in cache, keyed on all keyColumns we care about
+ keyColumns.forEach(function(keyColumn) {
+ cache.put(cacheKey(keyColumn, obj[keyColumn]), obj);
+ });
+ }
+ function touchInCache(obj) {
+ var cache = getCache();
+ keyColumns.forEach(function(keyColumn) {
+ cache.touch(cacheKey(keyColumn, obj[keyColumn]));
+ });
+ }
+ function removeObjFromCache(obj) {
+ var cache = getCache();
+ keyColumns.forEach(function(keyColumn) {
+ cache.remove(cacheKey(keyColumn, obj[keyColumn]));
+ });
+ }
+ function removeFromCache(keyColumn, value) {
+ var cached = getFromCache(keyColumn, value);
+ if (cached) {
+ removeObjFromCache(cached);
+ }
+ }
+
+ var self = {
+ clearCache: function() {
+ getCache().clear();
+ },
+ getByKey: function(keyColumn, value) {
+ // get cached object, if any
+ var cached = getFromCache(keyColumn, value);
+ if (! cached) {
+ // nothing in cache for this query, fetch from SQL
+ var keyToValue = {};
+ keyToValue[keyColumn] = value;
+ var fetched = sqlobj.selectSingle(tableName, keyToValue);
+ if (fetched) {
+ processFetched(fetched);
+ // fetched something, stick it in the cache
+ putInCache(fetched);
+ }
+ return fetched;
+ }
+ else {
+ // touch cached object and return
+ touchInCache(cached);
+ return cached;
+ }
+ },
+ updateByKey: function(keyColumn, value, obj) {
+ var keyToValue = {};
+ keyToValue[keyColumn] = value;
+ sqlobj.updateSingle(tableName, keyToValue, obj);
+ // remove old object from caches but
+ // don't put obj in cache, because it
+ // is likely a partial object
+ removeFromCache(keyColumn, value);
+ },
+ insert: function(obj) {
+ var returnVal = sqlobj.insert(tableName, obj);
+ // remove old object from caches but
+ // don't put obj in the cache; it doesn't
+ // have all values, e.g. for auto-generated ids
+ removeObjFromCache(obj);
+ return returnVal;
+ },
+ deleteByKey: function(keyColumn, value) {
+ var keyToValue = {};
+ keyToValue[keyColumn] = value;
+ sqlobj.deleteRows(tableName, keyToValue);
+ removeFromCache(keyColumn, value);
+ }
+ };
+ return self;
+}
+
+function _getClientIp() {
+ return (request.isDefined && request.clientIp) || '';
+}
+
+function getUserIdCreatedDate(userId) {
+ var record = sqlobj.selectSingle('pad_cookie_userids', {id: userId});
+ if (! record) { return; } // hm. weird case.
+ return record.createdDate;
+}
diff --git a/etherpad/src/etherpad/pad/padutils.js b/etherpad/src/etherpad/pad/padutils.js
new file mode 100644
index 0000000..b53de11
--- /dev/null
+++ b/etherpad/src/etherpad/pad/padutils.js
@@ -0,0 +1,191 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("stringutils");
+
+import("etherpad.control.pro.account_control");
+
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.pad.model");
+import("etherpad.sessions.getSession");
+import("etherpad.helpers");
+
+jimport("java.lang.System.out.println");
+
+
+function setCurrentPad(p) {
+ appjet.context.attributes().update("currentPadId", p);
+}
+
+function clearCurrentPad() {
+ appjet.context.attributes()['$minus$eq']("currentPadId");
+}
+
+function getCurrentPad() {
+ var padOpt = appjet.context.attributes().get("currentPadId");
+ if (padOpt.isEmpty()) return null;
+ return padOpt.get();
+}
+
+function _parseCookie(text) {
+ try {
+ var cookieData = fastJSON.parse(unescape(text));
+ return cookieData;
+ }
+ catch (e) {
+ return null;
+ }
+}
+
+function getPrefsCookieData() {
+ var prefsCookie = request.cookies['prefs'];
+ if (!prefsCookie) {
+ return null;
+ }
+
+ return _parseCookie(prefsCookie);
+}
+
+function getPrefsCookieUserId() {
+ var cookieData = getPrefsCookieData();
+ if (! cookieData) {
+ return null;
+ }
+ return cookieData.userId || null;
+}
+
+/**
+ * Not valid to call this function outisde a HTTP request.
+ */
+function accessPadLocal(localPadId, fn, rwMode) {
+ if (!request.isDefined) {
+ throw Error("accessPadLocal() cannot run outside an HTTP request.");
+ }
+ var globalPadId = getGlobalPadId(localPadId);
+ var fnwrap = function(pad) {
+ pad.getLocalId = function() {
+ return getLocalPadId(pad);
+ };
+ return fn(pad);
+ }
+ return model.accessPadGlobal(globalPadId, fnwrap, rwMode);
+}
+
+/**
+ * Not valid to call this function outisde a HTTP request.
+ */
+function getGlobalPadId(localPadId) {
+ if (!request.isDefined) {
+ throw Error("getGlobalPadId() cannot run outside an HTTP request.");
+ }
+ if (pro_utils.isProDomainRequest()) {
+ return makeGlobalId(domains.getRequestDomainId(), localPadId);
+ } else {
+ // pad.spline.inf.fu-berlin.de pads
+ return localPadId;
+ }
+}
+
+function makeGlobalId(domainId, localPadId) {
+ return [domainId, localPadId].map(String).join('$');
+}
+
+function globalToLocalId(globalId) {
+ var parts = globalId.split('$');
+ if (parts.length == 1) {
+ return parts[0];
+ } else {
+ return parts[1];
+ }
+}
+
+function getLocalPadId(pad) {
+ var globalId = pad.getId();
+ return globalToLocalId(globalId);
+}
+
+function isProPadId(globalPadId) {
+ return (globalPadId.indexOf("$") > 0);
+}
+
+function isProPad(pad) {
+ return isProPadId(pad.getId());
+}
+
+function getDomainId(globalPadId) {
+ var parts = globalPadId.split("$");
+ if (parts.length < 2) {
+ return null;
+ } else {
+ return Number(parts[0]);
+ }
+}
+
+function makeValidLocalPadId(str) {
+ return str.replace(/[^a-zA-Z0-9\-]/g, '-');
+}
+
+function getProDisplayTitle(localPadId, title) {
+ if (title) {
+ return title;
+ }
+ if (stringutils.isNumeric(localPadId)) {
+ return ("Untitled "+localPadId);
+ } else {
+ return (localPadId);
+ }
+}
+
+
+function setOptsAndCookiePrefs(request) {
+ opts = {};
+ if (request.params.fullScreen) { // tokbox, embedding
+ opts.fullScreen = true;
+ }
+ if (request.params.tokbox) {
+ opts.tokbox = true;
+ }
+ if (request.params.sidebar) {
+ opts.sidebar = Boolean(Number(request.params.sidebar));
+ }
+ helpers.addClientVars({opts: opts});
+
+
+ var prefs = getPrefsCookieData();
+
+ var prefsToSet = {
+ fullWidth:false,
+ hideSidebar:false
+ };
+ if (prefs) {
+ prefsToSet.isFullWidth = !! prefs.fullWidth;
+ prefsToSet.hideSidebar = !! prefs.hideSidebar;
+ }
+ if (opts.fullScreen) {
+ prefsToSet.isFullWidth = true;
+ if (opts.tokbox) {
+ prefsToSet.hideSidebar = true;
+ }
+ }
+ if ('sidebar' in opts) {
+ prefsToSet.hideSidebar = ! opts.sidebar;
+ }
+ helpers.addClientVars({cookiePrefsToSet: prefsToSet});
+}
diff --git a/etherpad/src/etherpad/pad/revisions.js b/etherpad/src/etherpad/pad/revisions.js
new file mode 100644
index 0000000..c7c84e8
--- /dev/null
+++ b/etherpad/src/etherpad/pad/revisions.js
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("jsutils.cmp");
+import("stringutils");
+
+import("etherpad.utils.*");
+
+jimport("java.lang.System.out.println");
+
+/* revisionList is an array of revisionInfo structures.
+ *
+ * Each revisionInfo structure looks like:
+ *
+ * {
+ * timestamp: a number unix timestamp
+ * label: string
+ * savedBy: string of author name
+ * savedById: string id of the author
+ * revNum: revision number in the edit history
+ * id: the view id of the (formerly the id of the StorableObject)
+ * }
+ */
+
+/* returns array */
+function _getRevisionsArray(pad) {
+ var dataRoot = pad.getDataRoot();
+ if (!dataRoot.savedRevisions) {
+ dataRoot.savedRevisions = [];
+ }
+ dataRoot.savedRevisions.sort(function(a,b) {
+ return cmp(b.timestamp, a.timestamp);
+ });
+ return dataRoot.savedRevisions;
+}
+
+function _getPadRevisionById(pad, savedRevId) {
+ var revs = _getRevisionsArray(pad);
+ var rev;
+ for(var i=0;i<revs.length;i++) {
+ if (revs[i].id == savedRevId) {
+ rev = revs[i];
+ break;
+ }
+ }
+ return rev || null;
+}
+
+/*----------------------------------------------------------------*/
+/* public functions */
+/*----------------------------------------------------------------*/
+
+function getRevisionList(pad) {
+ return _getRevisionsArray(pad);
+}
+
+function saveNewRevision(pad, savedBy, savedById, revisionNumber, optIP, optTimestamp, optId) {
+ var revArray = _getRevisionsArray(pad);
+ var rev = {
+ timestamp: (optTimestamp || (+(new Date))),
+ label: null,
+ savedBy: savedBy,
+ savedById: savedById,
+ revNum: revisionNumber,
+ ip: (optIP || request.clientAddr),
+ id: (optId || stringutils.randomString(10)) // *probably* unique
+ };
+ revArray.push(rev);
+ rev.label = "Revision "+revArray.length;
+ return rev;
+}
+
+function setLabel(pad, savedRevId, userId, newLabel) {
+ var rev = _getPadRevisionById(pad, savedRevId);
+ if (!rev) {
+ throw new Error("revision does not exist: "+savedRevId);
+ }
+ /*if (rev.savedById != userId) {
+ throw new Error("cannot label someone else's revision.");
+ }
+ if (((+new Date) - rev.timestamp) > (24*60*60*1000)) {
+ throw new Error("revision is too old to label: "+savedRevId);
+ }*/
+ rev.label = newLabel;
+}
+
+function getStoredRevision(pad, savedRevId) {
+ return _getPadRevisionById(pad, savedRevId);
+}
+
diff --git a/etherpad/src/etherpad/pne/pne_utils.js b/etherpad/src/etherpad/pne/pne_utils.js
new file mode 100644
index 0000000..073ad2a
--- /dev/null
+++ b/etherpad/src/etherpad/pne/pne_utils.js
@@ -0,0 +1,149 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("stringutils.md5");
+import("sqlbase.persistent_vars");
+
+import("etherpad.licensing");
+
+jimport("java.lang.System.out.println");
+jimport("java.lang.System");
+
+
+function isPNE() {
+ if (appjet.cache.fakePNE || appjet.config['etherpad.fakePNE']) {
+ return true;
+ }
+ if (getVersionString()) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Versioning scheme: we basically just use the apache scheme of MAJOR.MINOR.PATCH:
+ *
+ * Versions are denoted using a standard triplet of integers: MAJOR.MINOR.PATCH. The
+ * basic intent is that MAJOR versions are incompatible, large-scale upgrades of the API.
+ * MINOR versions retain source and binary compatibility with older minor versions, and
+ * changes in the PATCH level are perfectly compatible, forwards and backwards.
+ */
+
+function getVersionString() {
+ return appjet.config['etherpad.pneVersion'];
+}
+
+function parseVersionString(x) {
+ var parts = x.split('.');
+ return {
+ major: Number(parts[0] || 0),
+ minor: Number(parts[1] || 0),
+ patch: Number(parts[2] || 0)
+ };
+}
+
+/* returns {major: int, minor: int, patch: int} */
+function getVersionNumbers() {
+ return parseVersionString(getVersionString());
+}
+
+function checkDbVersionUpgrade() {
+ var dbVersionString = persistent_vars.get("db_pne_version");
+ var runningVersionString = getVersionString();
+
+ if (!dbVersionString) {
+ println("Upgrading to Private Network Edition, version: "+runningVersionString);
+ return;
+ }
+
+ var dbVersion = parseVersionString(dbVersionString);
+ var runningVersion = getVersionNumbers();
+ var trueRegex = /\s*true\s*/i;
+ var force = trueRegex.test(appjet.config['etherpad.forceDbUpgrade']);
+
+ if (!force && (runningVersion.major != dbVersion.major)) {
+ println("Error: you are attempting to update an EtherPad["+dbVersionString+
+ "] database to version ["+runningVersionString+"]. This is not possible.");
+ println("Exiting...");
+ System.exit(1);
+ }
+ if (!force && (runningVersion.minor < dbVersion.minor)) {
+ println("Error: your etherpad database is at a newer version ["+dbVersionString+"] than"+
+ " the current running etherpad ["+runningVersionString+"]. Please upgrade to the "+
+ " latest version.");
+ println("Exiting...");
+ System.exit(1);
+ }
+ if (!force && (runningVersion.minor > (dbVersion.minor + 1))) {
+ println("\n\nWARNING: you are attempting to upgrade from version "+dbVersionString+" to version "+
+ runningVersionString+". It is recommended that you upgrade one minor version at a time."+
+ " (The \"minor\" version number is the second number separated by dots. For example,"+
+ " if you are running version 1.2, it is recommended that you upgrade to 1.3 and then 1.4 "+
+ " instead of going directly from 1.2 to 1.4.");
+ println("\n\nIf you really want to do this, you can force us to attempt the upgrade with "+
+ " the --etherpad.forceDbUpgrade=true flag.");
+ println("\n\nExiting...");
+ System.exit(1);
+ }
+ if (runningVersion.minor > dbVersion.minor) {
+ println("Upgrading database to version "+runningVersionString);
+ }
+}
+
+function saveDbVersion() {
+ var dbVersionString = persistent_vars.get("db_pne_version");
+ if (getVersionString() != dbVersionString) {
+ persistent_vars.put('db_pne_version', getVersionString());
+ println("Upgraded Private Network Edition version to ["+getVersionString()+"]");
+ }
+}
+
+// These are a list of some of the config vars documented in the PNE manual. They are here
+// temporarily, until we move them to the PNE config UI.
+
+var _eepneAllowedConfigVars = [
+ 'configFile',
+ 'etherpad.soffice',
+ 'etherpad.useMySQL',
+ 'etherpad.SQL_JDBC_DRIVER',
+ 'etherpad.SQL_JDBC_URL',
+ 'etherpad.SQL_PASSWORD',
+ 'etherpad.SQL_USERNAME',
+ 'etherpad.adminPass',
+ 'etherpad.licenseKey',
+ 'listen',
+ 'listenSecure',
+ 'smtpPass',
+ 'smtpServer',
+ 'smtpUser',
+ 'sslKeyPassword',
+ 'sslKeyStore'
+];
+
+function isServerLicensed() {
+ return true;
+}
+
+function enableTrackingAgain() {
+}
+
+function pneTrackerHtml() {
+ appjet.cache.noMorePneTracking = true;
+}
+
+
+
diff --git a/etherpad/src/etherpad/pro/domains.js b/etherpad/src/etherpad/pro/domains.js
new file mode 100644
index 0000000..e56a408
--- /dev/null
+++ b/etherpad/src/etherpad/pro/domains.js
@@ -0,0 +1,141 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Library for managing subDomains
+
+import("jsutils.*");
+import("sqlbase.sqlobj");
+
+import("etherpad.pro.pro_utils");
+import("etherpad.pne.pne_utils");
+import("etherpad.licensing");
+
+jimport("java.lang.System.out.println");
+
+// reserved domains
+var reservedSubdomains = {
+ 'alpha': 1,
+ 'beta': 1,
+ 'blog': 1,
+ 'comet': 1,
+ 'diagnostic': 1,
+ 'forums': 1,
+ 'forumsdev': 1,
+ 'staging': 1,
+ 'web': 1,
+ 'www': 1
+};
+
+function _getCache() {
+ if (!appjet.cache.pro_domains) {
+ appjet.cache.pro_domains = {
+ records: {id: {}, subDomain: {}}
+ };
+ }
+ return appjet.cache.pro_domains;
+}
+
+function doesSubdomainExist(subDomain) {
+ if (reservedSubdomains[subDomain]) {
+ return true;
+ }
+ if (getDomainRecordFromSubdomain(subDomain) != null) {
+ return true;
+ }
+ return false;
+}
+
+function _updateCache(locator) {
+ var record = sqlobj.selectSingle('pro_domains', locator);
+ var recordCache = _getCache().records;
+
+ if (record) {
+ // update both maps: recordCache.id, recordCache.subDomain
+ keys(recordCache).forEach(function(key) {
+ recordCache[key][record[key]] = record;
+ });
+ } else {
+ // write false for whatever hit with this locator
+ keys(locator).forEach(function(key) {
+ recordCache[key][locator[key]] = false;
+ });
+ }
+}
+
+function getDomainRecord(domainId) {
+ if (!(domainId in _getCache().records.id)) {
+ _updateCache({id: domainId});
+ }
+ var record = _getCache().records.id[domainId];
+ return (record ? record : null);
+}
+
+function getDomainRecordFromSubdomain(subDomain) {
+ subDomain = subDomain.toLowerCase();
+ if (!(subDomain in _getCache().records.subDomain)) {
+ _updateCache({subDomain: subDomain});
+ }
+ var record = _getCache().records.subDomain[subDomain];
+ return (record ? record : null);
+}
+
+/** returns id of newly created subDomain */
+function createNewSubdomain(subDomain, orgName) {
+ var id = sqlobj.insert('pro_domains', {subDomain: subDomain, orgName: orgName});
+ _updateCache({id: id});
+ return id;
+}
+
+function getPrivateNetworkDomainId() {
+ var r = getDomainRecordFromSubdomain('<<private-network>>');
+ if (!r) {
+ throw Error("<<private-network>> does not exist in the domains table!");
+ }
+ return r.id;
+}
+
+/** returns null if not found. */
+function getRequestDomainRecord() {
+ if (pne_utils.isPNE()) {
+ var r = getDomainRecord(getPrivateNetworkDomainId());
+ if (appjet.cache.fakePNE) {
+ r.orgName = "fake";
+ } else {
+ var licenseInfo = licensing.getLicense();
+ if (licenseInfo) {
+ r.orgName = licenseInfo.organizationName;
+ } else {
+ r.orgName = "Private Network Edition TRIAL";
+ }
+ }
+ return r;
+ } else {
+ var subDomain = pro_utils.getProRequestSubdomain();
+ var r = getDomainRecordFromSubdomain(subDomain);
+ return r;
+ }
+}
+
+/* throws exception if not pro domain request. */
+function getRequestDomainId() {
+ var r = getRequestDomainRecord();
+ if (!r) {
+ throw Error("Error getting request domain id.");
+ }
+ return r.id;
+}
+
+
diff --git a/etherpad/src/etherpad/pro/pro_account_auto_signin.js b/etherpad/src/etherpad/pro/pro_account_auto_signin.js
new file mode 100644
index 0000000..ebcd227
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_account_auto_signin.js
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlobj");
+import("stringutils");
+
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+
+jimport("java.lang.System.out.println");
+
+var _COOKIE_NAME = "PUAS";
+
+function dmesg(m) {
+ if (false) {
+ println("[pro-account-auto-sign-in]: "+m);
+ }
+}
+
+function checkAutoSignin() {
+ dmesg("checking auto sign-in...");
+ if (pro_accounts.isAccountSignedIn()) {
+ dmesg("account already signed in...");
+ // don't mess with already signed-in account
+ return;
+ }
+ var cookie = request.cookies[_COOKIE_NAME];
+ if (!cookie) {
+ dmesg("no auto-sign-in cookie found...");
+ return;
+ }
+ var record = sqlobj.selectSingle('pro_accounts_auto_signin', {cookie: cookie}, {});
+ if (!record) {
+ return;
+ }
+
+ var now = +(new Date);
+ if (+record.expires < now) {
+ sqlobj.deleteRows('pro_accounts_auto_signin', {id: record.id});
+ response.deleteCookie(_COOKIE_NAME);
+ dmesg("deleted expired record...");
+ return;
+ }
+ // do auto-signin (bypasses normal security)
+ dmesg("Doing auto sign in...");
+ var account = pro_accounts.getAccountById(record.accountId);
+ pro_accounts.signInSession(account);
+ response.redirect('/ep/account/sign-in?cont='+encodeURIComponent(request.url));
+}
+
+function setAutoSigninCookie(rememberMe) {
+ if (!pro_accounts.isAccountSignedIn()) {
+ return; // only call this function after account is already signed in.
+ }
+
+ var accountId = getSessionProAccount().id;
+ // delete any existing auto-signins for this account.
+ sqlobj.deleteRows('pro_accounts_auto_signin', {accountId: accountId});
+
+ // set this insecure cookie just to indicate that account is auto-sign-in-able
+ response.setCookie({
+ name: "ASIE",
+ value: (rememberMe ? "T" : "F"),
+ path: "/",
+ domain: request.domain,
+ expires: new Date(32503708800000), // year 3000
+ });
+
+ if (!rememberMe) {
+ return;
+ }
+
+ var cookie = stringutils.randomHash(16);
+ var now = +(new Date);
+ var expires = new Date(now + 1000*60*60*24*30); // 30 days
+ //var expires = new Date(now + 1000 * 60 * 5); // 2 minutes
+
+ sqlobj.insert('pro_accounts_auto_signin', {cookie: cookie, accountId: accountId, expires: expires});
+ response.setCookie({
+ name: _COOKIE_NAME,
+ value: cookie,
+ path: "/ep/account/",
+ domain: request.domain,
+ expires: new Date(32503708800000), // year 3000
+ secure: true
+ });
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_accounts.js b/etherpad/src/etherpad/pro/pro_accounts.js
new file mode 100644
index 0000000..98df6bb
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_accounts.js
@@ -0,0 +1,592 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// library for pro accounts
+
+import("funhtml.*");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon.inTransaction");
+import("email.sendEmail");
+import("cache_utils.syncedWithCache");
+import("stringutils.*");
+
+import("etherpad.globals.*");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+import("etherpad.utils.*");
+import("etherpad.pro.domains");
+import("etherpad.control.pro.account_control");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_ldap_support.*");
+import("etherpad.pro.pro_quotas");
+import("etherpad.pad.padusers");
+import("etherpad.log");
+import("etherpad.billing.team_billing");
+
+import("process.*");
+import("fastJSON")
+
+jimport("org.mindrot.BCrypt");
+jimport("java.lang.System.out.println");
+
+function _dmesg(m) {
+ if (!isProduction()) {
+ println(m);
+ }
+}
+
+function _computePasswordHash(p) {
+ var pwh;
+ pwh = BCrypt.hashpw(p, BCrypt.gensalt(10));
+ return pwh;
+}
+
+function _withCache(name, fn) {
+ return syncedWithCache('pro_accounts.'+name, fn);
+}
+
+//----------------------------------------------------------------
+// validation
+//----------------------------------------------------------------
+
+function validateEmail(email) {
+ if (!email) { return "Email is required."; }
+ if (!isValidEmail(email)) { return "\""+email+"\" does not look like a valid email address."; }
+ return null;
+}
+
+function validateFullName(name) {
+ if (!name) { return "Full name is required."; }
+ if (name.length < 2) { return "Full name must be at least 2 characters."; }
+ return null;
+}
+
+function validatePassword(p) {
+ if (!p) { return "Password is required."; }
+ if (p.length < 6) { return "Passwords must be at least 6 characters."; }
+ return null;
+}
+
+function validateEmailDomainPair(email, domainId) {
+ // TODO: make sure the same email address cannot exist more than once within
+ // the same domainid.
+}
+
+/* if domainId is null, then use domainId of current request. */
+function createNewAccount(domainId, fullName, email, password, isAdmin, skipValidation) {
+ if (!domainId) {
+ domainId = domains.getRequestDomainId();
+ }
+ if (!skipValidation) {
+ skipValidation = false;
+ }
+ email = trim(email);
+ isAdmin = !!isAdmin; // convert to bool
+
+ // validation
+ if (!skipValidation) {
+ var e;
+ e = validateEmail(email); if (e) { throw Error(e); }
+ e = validateFullName(fullName); if (e) { throw Error(e); }
+ e = validatePassword(password); if (e) { throw Error(e); }
+ }
+
+ // xss normalization
+ fullName = toHTML(fullName);
+
+ // make sure account does not already exist on this domain.
+ var ret = inTransaction(function() {
+ var existingAccount = getAccountByEmail(email, domainId);
+ if (existingAccount) {
+ throw Error("There is already an account with that email address.");
+ }
+ // No existing account. Proceed.
+ var now = new Date();
+ var account = {
+ domainId: domainId,
+ fullName: fullName,
+ email: email,
+ passwordHash: _computePasswordHash(password),
+ createdDate: now,
+ isAdmin: isAdmin
+ };
+ return sqlobj.insert('pro_accounts', account);
+ });
+
+ _withCache('does-domain-admin-exist', function(cache) {
+ delete cache[domainId];
+ });
+
+ pro_quotas.updateAccountUsageCount(domainId);
+ updateCachedActiveCount(domainId);
+
+ if (ret) {
+ log.custom('pro-accounts',
+ {type: "account-created",
+ accountId: ret,
+ domainId: domainId,
+ name: fullName,
+ email: email,
+ admin: isAdmin});
+ }
+
+ return ret;
+}
+
+function _checkAccess(account) {
+ if (sessions.isAnEtherpadAdmin()) {
+ return;
+ }
+ if (account.domainId != domains.getRequestDomainId()) {
+ throw Error("access denied");
+ }
+}
+
+function setPassword(account, newPass) {
+ _checkAccess(account);
+ var passHash = _computePasswordHash(newPass);
+ sqlobj.update('pro_accounts', {id: account.id}, {passwordHash: passHash});
+ markDirtySessionAccount(account.id);
+}
+
+function setTempPassword(account, tempPass) {
+ _checkAccess(account);
+ var tempPassHash = _computePasswordHash(tempPass);
+ sqlobj.update('pro_accounts', {id: account.id}, {tempPassHash: tempPassHash});
+ markDirtySessionAccount(account.id);
+}
+
+function setEmail(account, newEmail) {
+ _checkAccess(account);
+ sqlobj.update('pro_accounts', {id: account.id}, {email: newEmail});
+ markDirtySessionAccount(account.id);
+}
+
+function setFullName(account, newName) {
+ _checkAccess(account);
+ sqlobj.update('pro_accounts', {id: account.id}, {fullName: newName});
+ markDirtySessionAccount(account.id);
+}
+
+function setIsAdmin(account, newVal) {
+ _checkAccess(account);
+ sqlobj.update('pro_accounts', {id: account.id}, {isAdmin: newVal});
+ markDirtySessionAccount(account.id);
+}
+
+function setDeleted(account) {
+ _checkAccess(account);
+ if (!isNumeric(account.id)) {
+ throw new Error("Invalid account id: "+account.id);
+ }
+ sqlobj.update('pro_accounts', {id: account.id}, {isDeleted: true});
+ markDirtySessionAccount(account.id);
+ pro_quotas.updateAccountUsageCount(account.domainId);
+ updateCachedActiveCount(account.domainId);
+
+ log.custom('pro-accounts',
+ {type: "account-deleted",
+ accountId: account.id,
+ domainId: account.domainId,
+ name: account.fullName,
+ email: account.email,
+ admin: account.isAdmin,
+ createdDate: account.createdDate.getTime()});
+}
+
+//----------------------------------------------------------------
+
+function doesAdminExist() {
+ var domainId = domains.getRequestDomainId();
+ return _withCache('does-domain-admin-exist', function(cache) {
+ if (cache[domainId] === undefined) {
+ _dmesg("cache miss for doesAdminExist (domainId="+domainId+")");
+ var admins = sqlobj.selectMulti('pro_accounts', {domainId: domainId, isAdmin: true}, {});
+ cache[domainId] = (admins.length > 0);
+ }
+ return cache[domainId]
+ });
+}
+
+function attemptSingleSignOn() {
+ if(!appjet.config['etherpad.SSOScript']) return null;
+
+ // pass request.cookies to a small user script
+ var file = appjet.config['etherpad.SSOScript'];
+
+ var cmd = exec(file);
+
+ // note that this will block until script execution returns
+ var result = cmd.write(fastJSON.stringify(request.cookies)).result();
+ var val = false;
+
+ // we try to parse the result as a JSON string, if not, return null.
+ try {
+ if(!!(val=fastJSON.parse(result))) {
+ return val;
+ }
+ } catch(e) {}
+ return null;
+}
+
+function getSessionProAccount() {
+ if (sessions.isAnEtherpadAdmin()) {
+ return getEtherpadAdminAccount();
+ }
+ var account = getSession().proAccount;
+ if (!account) {
+ return null;
+ }
+ if (account.isDeleted) {
+ delete getSession().proAccount;
+ return null;
+ }
+ return account;
+}
+
+function isAccountSignedIn() {
+ if (getSessionProAccount()) {
+ return true;
+ } else {
+ // if the user is not signed in, check to see if he should be signed in
+ // by calling an external script.
+ if(appjet.config['etherpad.SSOScript']) {
+ var ssoResult = attemptSingleSignOn();
+ if(ssoResult && ('email' in ssoResult)) {
+ var user = getAccountByEmail(ssoResult['email']);
+ if (!user) {
+ var email = ssoResult['email'];
+ var pass = ssoResult['password'] || "";
+ var name = ssoResult['fullname'] || "unnamed";
+ createNewAccount(null, name, email, pass, false, true);
+ user = getAccountByEmail(email, null);
+ }
+
+ signInSession(user);
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+
+function isAdminSignedIn() {
+ return isAccountSignedIn() && getSessionProAccount().isAdmin;
+}
+
+function requireAccount(message) {
+ if ((request.path == "/ep/account/sign-in") ||
+ (request.path == "/ep/account/sign-out") ||
+ (request.path == "/ep/account/guest-sign-in") ||
+ (request.path == "/ep/account/guest-knock") ||
+ (request.path == "/ep/account/forgot-password")) {
+ return;
+ }
+
+ function checkSessionAccount() {
+ if (!getSessionProAccount()) {
+ if (message) {
+ account_control.setSigninNotice(message);
+ }
+ response.redirect('/ep/account/sign-in?cont='+encodeURIComponent(request.url));
+ }
+ }
+
+ checkSessionAccount();
+
+ if (getSessionProAccount().domainId != domains.getRequestDomainId()) {
+ // This should theoretically never happen unless the account is spoofing cookies / trying to
+ // hack the site.
+ pro_utils.renderFramedMessage("Permission denied.");
+ response.stop();
+ }
+ // update dirty session account if necessary
+ _withCache('dirty-session-accounts', function(cache) {
+ var uid = getSessionProAccount().id;
+ if (cache[uid]) {
+ reloadSessionAccountData(uid);
+ cache[uid] = false;
+ }
+ });
+
+ // need to check again in case dirty update caused account to be marked
+ // deleted.
+ checkSessionAccount();
+}
+
+function requireAdminAccount() {
+ requireAccount();
+ if (!getSessionProAccount().isAdmin) {
+ pro_utils.renderFramedMessage("Permission denied.");
+ response.stop();
+ }
+}
+
+/* returns undefined on success, error string otherise. */
+function authenticateSignIn(email, password) {
+ // blank passwords are not allowed to sign in.
+ if (password == "") return "Please provide a password.";
+
+ // If the email ends with our ldap suffix...
+ var isLdapSuffix = getLDAP() && getLDAP().isLDAPSuffix(email);
+
+ if(isLdapSuffix && !getLDAP()) {
+ return "LDAP not yet configured. Please contact your system admininstrator.";
+ }
+
+ // if there is an error in the LDAP configuration, return the error message
+ if(getLDAP() && getLDAP().error) {
+ return getLDAP().error + " Please contact your system administrator.";
+ }
+
+ if(isLdapSuffix && getLDAP()) {
+ var ldapuser = email.substr(0, email.indexOf(getLDAP().getLDAPSuffix()));
+ var ldapResult = getLDAP().login(ldapuser, password);
+
+ if (ldapResult.error == true) {
+ return ldapResult.message + "";
+ }
+
+ var accountRecord = getAccountByEmail(email, null);
+
+ // if this is the first time this user has logged in, create a user
+ // for him/her
+ if (!accountRecord) {
+ // password to store in database -- a blank password means the user
+ // cannot authenticate normally (e.g. must go through SSO or LDAP)
+ var ldapPass = "";
+
+ // create a new user (skipping validation of email/users/passes)
+ createNewAccount(null, ldapResult.getFullName(), email, ldapPass, false, true);
+ accountRecord = getAccountByEmail(email, null);
+ }
+
+ signInSession(accountRecord);
+ return undefined; // success
+ }
+
+ var accountRecord = getAccountByEmail(email, null);
+ if (!accountRecord) {
+ return "Account not found: "+email;
+ }
+
+ if (BCrypt.checkpw(password, accountRecord.passwordHash) != true) {
+ return "Incorrect password. Please try again.";
+ }
+
+ signInSession(accountRecord);
+
+ return undefined; // success
+}
+
+function signOut() {
+ delete getSession().proAccount;
+}
+
+function authenticateTempSignIn(uid, tempPass) {
+ var emsg = "That password reset link that is no longer valid.";
+
+ var account = getAccountById(uid);
+ if (!account) {
+ return emsg+" (Account not found.)";
+ }
+ if (account.domainId != domains.getRequestDomainId()) {
+ return emsg+" (Wrong domain.)";
+ }
+ if (!account.tempPassHash) {
+ return emsg+" (Expired.)";
+ }
+ if (BCrypt.checkpw(tempPass, account.tempPassHash) != true) {
+ return emsg+" (Bad temp pass.)";
+ }
+
+ signInSession(account);
+
+ getSession().accountMessage = "Please choose a new password";
+ getSession().changePass = true;
+
+ response.redirect("/ep/account/");
+}
+
+function signInSession(account) {
+ account.lastLoginDate = new Date();
+ account.tempPassHash = null;
+ sqlobj.updateSingle('pro_accounts', {id: account.id}, account);
+ reloadSessionAccountData(account.id);
+ padusers.notifySignIn();
+}
+
+function listAllDomainAccounts(domainId) {
+ if (domainId === undefined) {
+ domainId = domains.getRequestDomainId();
+ }
+ var records = sqlobj.selectMulti('pro_accounts',
+ {domainId: domainId, isDeleted: false}, {});
+ return records;
+}
+
+function listAllDomainAdmins(domainId) {
+ if (domainId === undefined) {
+ domainId = domains.getRequestDomainId();
+ }
+ var records = sqlobj.selectMulti('pro_accounts',
+ {domainId: domainId, isDeleted: false, isAdmin: true},
+ {});
+ return records;
+}
+
+function getActiveCount(domainId) {
+ var records = sqlobj.selectMulti('pro_accounts',
+ {domainId: domainId, isDeleted: false}, {});
+ return records.length;
+}
+
+/* getAccountById works for deleted and non-deleted accounts.
+ * The assumption is that cases whewre you look up an account by ID, you
+ * want the account info even if the account has been deleted. For
+ * example, when asking who created a pad.
+ */
+function getAccountById(accountId) {
+ var r = sqlobj.selectSingle('pro_accounts', {id: accountId});
+ if (r) {
+ return r;
+ } else {
+ return undefined;
+ }
+}
+
+/* getting an account by email only returns the account if it is
+ * not deleted. The assumption is that when you look up an account by
+ * email address, you only want active accounts. Furthermore, some
+ * deleted accounts may match a given email, but only one non-deleted
+ * account should ever match a single (email,domainId) pair.
+ */
+function getAccountByEmail(email, domainId) {
+ if (!domainId) {
+ domainId = domains.getRequestDomainId();
+ }
+ var r = sqlobj.selectSingle('pro_accounts', {domainId: domainId, email: email, isDeleted: false});
+ if (r) {
+ return r;
+ } else {
+ return undefined;
+ }
+}
+
+function getFullNameById(id) {
+ if (!id) {
+ return null;
+ }
+
+ return _withCache('names-by-id', function(cache) {
+ if (cache[id] === undefined) {
+ _dmesg("cache miss for getFullNameById (accountId="+id+")");
+ var r = getAccountById(id);
+ if (r) {
+ cache[id] = r.fullName;
+ } else {
+ cache[id] = false;
+ }
+ }
+ if (cache[id]) {
+ return cache[id];
+ } else {
+ return null;
+ }
+ });
+}
+
+function getTempSigninUrl(account, tempPass) {
+ if(appjet.config.listenSecurePort != 0 || appjet.config.useHttpsUrls)
+ return [
+ 'https://', httpsHost(pro_utils.getFullProHost()), '/ep/account/sign-in?',
+ 'uid=', account.id, '&tp=', tempPass
+ ].join('');
+ else
+ return [
+ 'http://', httpHost(pro_utils.getFullProHost()), '/ep/account/sign-in?',
+ 'uid=', account.id, '&tp=', tempPass
+ ].join('');
+}
+
+
+// TODO: this session account object storage / dirty cache is a
+// ridiculous hack. What we should really do is have a caching/access
+// layer for accounts similar to accessPad() and accessProPadMeta(), and
+// have that abstraction take care of caching and marking accounts as
+// dirty. This can be incorporated into getSessionProAccount(), and we
+// should actually refactor that into accessSessionProAccount().
+
+/* will force session data for this account to be updated next time that
+ * account requests a page. */
+function markDirtySessionAccount(uid) {
+ var domainId = domains.getRequestDomainId();
+
+ _withCache('dirty-session-accounts', function(cache) {
+ cache[uid] = true;
+ });
+ _withCache('names-by-id', function(cache) {
+ delete cache[uid];
+ });
+ _withCache('does-domain-admin-exist', function(cache) {
+ delete cache[domainId];
+ });
+}
+
+function reloadSessionAccountData(uid) {
+ if (!uid) {
+ uid = getSessionProAccount().id;
+ }
+ getSession().proAccount = getAccountById(uid);
+}
+
+function getAllAccountsWithEmail(email) {
+ var accountRecords = sqlobj.selectMulti('pro_accounts', {email: email, isDeleted: false}, {});
+ return accountRecords;
+}
+
+function getEtherpadAdminAccount() {
+ return {
+ id: 0,
+ isAdmin: true,
+ fullName: "ETHERPAD ADMIN",
+ email: "support@pad.spline.inf.fu-berlin.de",
+ domainId: domains.getRequestDomainId(),
+ isDeleted: false
+ };
+}
+
+function getCachedActiveCount(domainId) {
+ return _withCache('user-counts.'+domainId, function(c) {
+ if (!c.count) {
+ c.count = getActiveCount(domainId);
+ }
+ return c.count;
+ });
+}
+
+function updateCachedActiveCount(domainId) {
+ _withCache('user-counts.'+domainId, function(c) {
+ c.count = getActiveCount(domainId);
+ });
+}
+
+
+
+
+
+
diff --git a/etherpad/src/etherpad/pro/pro_config.js b/etherpad/src/etherpad/pro/pro_config.js
new file mode 100644
index 0000000..d2d119f
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_config.js
@@ -0,0 +1,92 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("sqlbase.sqlobj");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.globals.*");
+import("etherpad.utils.*");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_utils");
+
+function _guessSiteName() {
+ var x = request.host.split('.')[0];
+ x = (x.charAt(0).toUpperCase() + x.slice(1));
+ return x;
+}
+
+function _getDefaultConfig() {
+ return {
+ siteName: _guessSiteName(),
+ alwaysHttps: false,
+ defaultPadText: renderTemplateAsString("misc/pad_default.ejs")
+ };
+}
+
+// must be fast! gets called per request, on every request.
+function getConfig() {
+ if (!pro_utils.isProDomainRequest()) {
+ return null;
+ }
+
+ if (!appjet.cache.pro_config) {
+ appjet.cache.pro_config = {};
+ }
+
+ var domainId = domains.getRequestDomainId();
+ if (!appjet.cache.pro_config[domainId]) {
+ reloadConfig();
+ }
+
+ return appjet.cache.pro_config[domainId];
+}
+
+function reloadConfig() {
+ var domainId = domains.getRequestDomainId();
+ var config = _getDefaultConfig();
+ var records = sqlobj.selectMulti('pro_config', {domainId: domainId}, {});
+
+ records.forEach(function(r) {
+ var name = r.name;
+ var val = fastJSON.parse(r.jsonVal).x;
+ config[name] = val;
+ });
+
+ if (!appjet.cache.pro_config) {
+ appjet.cache.pro_config = {};
+ }
+
+ appjet.cache.pro_config[domainId] = config;
+}
+
+function setConfigVal(name, val) {
+ var domainId = domains.getRequestDomainId();
+ var jsonVal = fastJSON.stringify({x: val});
+
+ var r = sqlobj.selectSingle('pro_config', {domainId: domainId, name: name});
+ if (!r) {
+ sqlobj.insert('pro_config',
+ {domainId: domainId, name: name, jsonVal: jsonVal});
+ } else {
+ sqlobj.update('pro_config',
+ {name: name, domainId: domainId},
+ {jsonVal: jsonVal});
+ }
+
+ reloadConfig();
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_ldap_support.js b/etherpad/src/etherpad/pro/pro_ldap_support.js
new file mode 100644
index 0000000..a657af1
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_ldap_support.js
@@ -0,0 +1,217 @@
+import("fastJSON");
+
+jimport("net.appjet.common.util.BetterFile")
+
+jimport("java.lang.System.out.println");
+jimport("javax.naming.directory.DirContext");
+jimport("javax.naming.directory.SearchControls");
+jimport("javax.naming.directory.InitialDirContext");
+jimport("javax.naming.directory.SearchResult");
+jimport("javax.naming.NamingEnumeration");
+jimport("javax.naming.Context");
+jimport("java.util.Hashtable");
+
+function LDAP(config, errortext) {
+ if(!config)
+ this.error = errortext;
+ else
+ this.error = false;
+
+ this.ldapConfig = config;
+}
+
+function _dmesg(m) {
+ // if (!isProduction()) {
+ println(new String(m));
+ // }
+}
+
+/**
+ * an ldap result object
+ *
+ * will either have error = true, with a corrisponding error message,
+ * or will have error = false, with a corrisponding results object message
+ */
+function LDAPResult(msg, error, ldap) {
+ if(!ldap) ldap = getLDAP();
+ if(!error) error = false;
+ this.message = msg;
+ this.ldap = ldap;
+ this.error = error;
+}
+
+/**
+ * returns the full name attribute, as specified by the 'nameAttribute' config
+ * value.
+ */
+LDAPResult.prototype.getFullName = function() {
+ return this.message[this.ldap.ldapConfig['nameAttribute']][0];
+}
+
+/**
+ * Handy function for creating an LDAPResult object
+ */
+function ldapMessage(success, msg) {
+ var message = msg;
+ if(typeof(msg) == String) {
+ message = "LDAP " +
+ (success ? "Success" : "Error") + ": " + msg;
+ }
+
+ var result = new LDAPResult(message);
+ result.error = !success;
+ return result;
+}
+
+// returns the associated ldap results object, with an error flag of false
+var ldapSuccess =
+ function(msg) { return ldapMessage.apply(this, [true, msg]); };
+
+// returns a helpful error message
+var ldapError =
+ function(msg) { return ldapMessage.apply(this, [false, msg]); };
+
+/* build an LDAP Query (searches for an objectClass and uid) */
+LDAP.prototype.buildLDAPQuery = function(queryUser) {
+ if(queryUser && queryUser.match(/[\w_-]+/)) {
+ return "(&(objectClass=" +
+ this.ldapConfig['userClass'] + ")(uid=" +
+ queryUser + "))"
+ } else return null;
+}
+
+LDAP.prototype.login = function(queryUser, queryPass) {
+ var query = this.buildLDAPQuery(queryUser);
+ if(!query) { return ldapError("invalid LDAP username"); }
+
+ try {
+ var context = LDAP.authenticate(this.ldapConfig['url'],
+ this.ldapConfig['principal'],
+ this.ldapConfig['password']);
+
+ if(!context) {
+ return ldapError("could not authenticate principle user.");
+ }
+
+ var ctrl = new SearchControls();
+ ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE);
+ var results = context.search(this.ldapConfig['rootPath'], query, ctrl);
+
+ // if the user is found
+ if(results.hasMore()) {
+ var result = results.next();
+
+ // grab the absolute path to the user
+ var userResult = result.getNameInNamespace();
+ var authed = !!LDAP.authenticate(this.ldapConfig['url'],
+ userResult,
+ queryPass)
+
+ // return the LDAP info on the user upon success
+ return authed ?
+ ldapSuccess(LDAP.parse(result)) :
+ ldapError("Incorrect password. Please try again.");
+ } else {
+ return ldapError("User "+queryUser+" not found in LDAP.");
+ }
+
+ // if there are errors in the search, log them and return "unknown error"
+ } catch (e) {
+ _dmesg(e);
+ return ldapError(new String(e))
+ }
+};
+
+LDAP.prototype.isLDAPSuffix = function(email) {
+ return email.indexOf(this.ldapConfig['ldapSuffix']) ==
+ (email.length-this.ldapConfig['ldapSuffix'].length);
+}
+
+LDAP.prototype.getLDAPSuffix = function() {
+ return this.ldapConfig['ldapSuffix'];
+}
+
+/* static function returns a DirContext, or undefined upon authentation err */
+LDAP.authenticate = function(url, user, pass) {
+ var context = null;
+ try {
+ var env = new Hashtable();
+ env.put(Context.INITIAL_CONTEXT_FACTORY,
+ "com.sun.jndi.ldap.LdapCtxFactory");
+ env.put( Context.SECURITY_PRINCIPAL, user );
+ env.put( Context.SECURITY_CREDENTIALS, pass );
+ env.put(Context.PROVIDER_URL, url);
+ context = new InitialDirContext(env);
+ } catch (e) {
+ // bind failed.
+ }
+ return context;
+}
+
+/* turn a res */
+LDAP.parse = function(result) {
+ var resultobj = {};
+ try {
+ var attrs = result.getAttributes();
+ var ids = attrs.getIDs();
+
+ while(ids.hasMore()) {
+ var id = ids.next().toString();
+ resultobj[id] = [];
+
+ var attr = attrs.get(id);
+
+ for(var i=0; i<attr.size(); i++) {
+ resultobj[id].push(attr.get(i).toString());
+ }
+ }
+ } catch (e) {
+ // naming error
+ return {'keys': e}
+ }
+
+ return resultobj;
+}
+
+LDAP.ldapSingleton = false;
+
+// load in ldap configuration from a file...
+function readLdapConfig(file) {
+ var fileContents = BetterFile.getFileContents(file);
+
+ if(fileContents == null)
+ return "File not found.";
+
+ var configObject = fastJSON.parse(fileContents);
+ if(configObject['ldapSuffix']) {
+ LDAP.ldapSuffix = configObject['ldapSuffix'];
+ }
+ return configObject;
+}
+
+// Sample Configuration file:
+// {
+// "userClass" : "person",
+// "url" : "ldap://localhost:10389",
+// "principal" : "uid=admin,ou=system",
+// "password" : "secret",
+// "rootPath" : "ou=users,ou=system",
+// "nameAttribute": "displayname",
+// "ldapSuffix" : "@ldap"
+// }
+
+// appjet.config['etherpad.useLdapConfiguration'] = "/Users/kroo/Documents/Projects/active/AppJet/ldapConfig.json";
+function getLDAP() {
+ if (! LDAP.ldapSingleton &&
+ appjet.config['etherpad.useLdapConfiguration']) {
+ var config = readLdapConfig(appjet.config['etherpad.useLdapConfiguration']);
+ var error = null;
+ if(!config) {
+ config = null;
+ error = "Error reading LDAP configuration file."
+ }
+ LDAP.ldapSingleton = new LDAP(config, error);
+ }
+
+ return LDAP.ldapSingleton;
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/pro/pro_pad_db.js b/etherpad/src/etherpad/pro/pro_pad_db.js
new file mode 100644
index 0000000..dbb412c
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_pad_db.js
@@ -0,0 +1,232 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("fastJSON");
+import("sqlbase.sqlobj");
+import("cache_utils.syncedWithCache");
+import("stringutils");
+
+import("etherpad.pad.padutils");
+import("etherpad.collab.collab_server");
+
+import("etherpad.pro.pro_pad_editors");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+
+jimport("java.lang.System.out.println");
+
+
+// TODO: actually implement the cache part
+
+// NOTE: must return a deep-CLONE of the actual record, because caller
+// may proceed to mutate the returned record.
+
+function _makeRecord(r) {
+ if (!r) {
+ return null;
+ }
+ r.proAttrs = {};
+ if (r.proAttrsJson) {
+ r.proAttrs = fastJSON.parse(r.proAttrsJson);
+ }
+ if (!r.proAttrs.editors) {
+ r.proAttrs.editors = [];
+ }
+ r.proAttrs.editors.sort();
+ return r;
+}
+
+function getSingleRecord(domainId, localPadId) {
+ // TODO: make clone
+ // TODO: use cache
+ var record = sqlobj.selectSingle('pro_padmeta', {domainId: domainId, localPadId: localPadId});
+ return _makeRecord(record);
+}
+
+function update(padRecord) {
+ // TODO: use cache
+
+ padRecord.proAttrsJson = fastJSON.stringify(padRecord.proAttrs);
+ delete padRecord.proAttrs;
+
+ sqlobj.update('pro_padmeta', {id: padRecord.id}, padRecord);
+}
+
+
+//--------------------------------------------------------------------------------
+// create/edit/destory events
+//--------------------------------------------------------------------------------
+
+function onCreatePad(pad) {
+ if (!padutils.isProPad(pad)) { return; }
+
+ var data = {
+ domainId: padutils.getDomainId(pad.getId()),
+ localPadId: padutils.getLocalPadId(pad),
+ createdDate: new Date(),
+ };
+
+ if (getSessionProAccount()) {
+ data.creatorId = getSessionProAccount().id;
+ }
+
+ sqlobj.insert('pro_padmeta', data);
+}
+
+// Not a normal part of the UI. This is only called from admin interface,
+// and thus should actually destroy all record of the pad.
+function onDestroyPad(pad) {
+ if (!padutils.isProPad(pad)) { return; }
+
+ sqlobj.deleteRows('pro_padmeta', {
+ domainId: padutils.getDomainId(pad.getId()),
+ localPadId: padutils.getLocalPadId(pad)
+ });
+}
+
+// Called within the context of a comet post.
+function onEditPad(pad, padAuthorId) {
+ if (!padutils.isProPad(pad)) { return; }
+
+ var editorId = undefined;
+ if (getSessionProAccount()) {
+ editorId = getSessionProAccount().id;
+ }
+
+ if (!(editorId && (editorId > 0))) {
+ return; // etherpad admins
+ }
+
+ pro_pad_editors.notifyEdit(
+ padutils.getDomainId(pad.getId()),
+ padutils.getLocalPadId(pad),
+ editorId,
+ new Date()
+ );
+}
+
+//--------------------------------------------------------------------------------
+// accessing the pad list.
+//--------------------------------------------------------------------------------
+
+function _makeRecordList(lis) {
+ lis.forEach(function(r) {
+ r = _makeRecord(r);
+ });
+ return lis;
+}
+
+function listMyPads() {
+ var domainId = domains.getRequestDomainId();
+ var accountId = getSessionProAccount().id;
+
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, creatorId: accountId, isDeleted: false, isArchived: false});
+ return _makeRecordList(padlist);
+}
+
+function listAllDomainPads() {
+ var domainId = domains.getRequestDomainId();
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, isDeleted: false, isArchived: false});
+ return _makeRecordList(padlist);
+}
+
+function listArchivedPads() {
+ var domainId = domains.getRequestDomainId();
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, isDeleted: false, isArchived: true});
+ return _makeRecordList(padlist);
+}
+
+function listPadsByEditor(editorId) {
+ editorId = Number(editorId);
+ var domainId = domains.getRequestDomainId();
+ var padlist = sqlobj.selectMulti('pro_padmeta', {domainId: domainId, isDeleted: false, isArchived: false});
+ padlist = _makeRecordList(padlist);
+ padlist = padlist.filter(function(p) {
+ // NOTE: could replace with binary search to speed things up,
+ // since we know that editors array is sorted.
+ return (p.proAttrs.editors.indexOf(editorId) >= 0);
+ });
+ return padlist;
+}
+
+function listLiveDomainPads() {
+ var thisDomainId = domains.getRequestDomainId();
+ var allLivePadIds = collab_server.getAllPadsWithConnections();
+ var livePadMap = {};
+
+ allLivePadIds.forEach(function(globalId) {
+ if (padutils.isProPadId(globalId)) {
+ var domainId = padutils.getDomainId(globalId);
+ var localId = padutils.globalToLocalId(globalId);
+ if (domainId == thisDomainId) {
+ livePadMap[localId] = true;
+ }
+ }
+ });
+
+ var padList = listAllDomainPads();
+ padList = padList.filter(function(p) {
+ return (!!livePadMap[p.localPadId]);
+ });
+
+ return padList;
+}
+
+//--------------------------------------------------------------------------------
+// misc utils
+//--------------------------------------------------------------------------------
+
+
+function _withCache(name, fn) {
+ return syncedWithCache('pro-padmeta.'+name, fn);
+}
+
+function _withDomainCache(domainId, name, fn) {
+ return _withCache(name+"."+domainId, fn);
+}
+
+
+
+// returns the next pad ID to use for a newly-created pad on this domain.
+function getNextPadId() {
+ var domainId = domains.getRequestDomainId();
+ return _withDomainCache(domainId, 'padcounters', function(c) {
+ var ret;
+ if (c.x === undefined) {
+ c.x = _getLargestNumericPadId(domainId) + 1;
+ }
+ while (sqlobj.selectSingle('pro_padmeta', {domainId: domainId, localPadId: String(c.x)})) {
+ c.x++;
+ }
+ ret = c.x;
+ c.x++;
+ return ret;
+ });
+}
+
+function _getLargestNumericPadId(domainId) {
+ var max = 0;
+ var allPads = listAllDomainPads();
+ allPads.forEach(function(p) {
+ if (stringutils.isNumeric(p.localPadId)) {
+ max = Math.max(max, Number(p.localPadId));
+ }
+ });
+ return max;
+}
+
+
+
diff --git a/etherpad/src/etherpad/pro/pro_pad_editors.js b/etherpad/src/etherpad/pro/pro_pad_editors.js
new file mode 100644
index 0000000..a90f05b
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_pad_editors.js
@@ -0,0 +1,104 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("execution");
+import("jsutils.*");
+import("cache_utils.syncedWithCache");
+
+import("etherpad.pad.padutils");
+import("etherpad.pro.pro_padmeta");
+import("etherpad.log");
+
+var _DOMAIN_EDIT_WRITE_INTERVAL = 2000; // 2 seconds
+
+function _withCache(name, fn) {
+ return syncedWithCache('pro-padmeta.'+name, fn);
+}
+
+function _withDomainCache(domainId, name, fn) {
+ return _withCache(name+"."+domainId, fn);
+}
+
+
+function onStartup() {
+ execution.initTaskThreadPool("pro-padmeta-edits", 1);
+}
+
+function onShutdown() {
+ var success = execution.shutdownAndWaitOnTaskThreadPool("pro-padmeta-edits", 4000);
+ if (!success) {
+ log.warn("Warning: pro.padmeta failed to flush pad edits on shutdown.");
+ }
+}
+
+function notifyEdit(domainId, localPadId, editorId, editTime) {
+ if (!editorId) {
+ // guest editors
+ return;
+ }
+ _withDomainCache(domainId, "edits", function(c) {
+ if (!c[localPadId]) {
+ c[localPadId] = {
+ lastEditorId: editorId,
+ lastEditTime: editTime,
+ recentEditors: []
+ };
+ }
+ var info = c[localPadId];
+ if (info.recentEditors.indexOf(editorId) < 0) {
+ info.recentEditors.push(editorId);
+ }
+ });
+ _flushPadEditsEventually(domainId);
+}
+
+
+function _flushPadEditsEventually(domainId) {
+ // Make sure there is a recurring edit-writer for this domain
+ _withDomainCache(domainId, "recurring-edit-writers", function(c) {
+ if (!c[domainId]) {
+ flushEditsNow(domainId);
+ c[domainId] = true;
+ }
+ });
+}
+
+function flushEditsNow(domainId) {
+ if (!appjet.cache.shutdownHandlerIsRunning) {
+ execution.scheduleTask("pro-padmeta-edits", "proPadmetaFlushEdits",
+ _DOMAIN_EDIT_WRITE_INTERVAL, [domainId]);
+ }
+
+ _withDomainCache(domainId, "edits", function(edits) {
+ var padIdList = keys(edits);
+ padIdList.forEach(function(localPadId) {
+ _writePadEditsToDbNow(domainId, localPadId, edits[localPadId]);
+ delete edits[localPadId];
+ });
+ });
+}
+
+function _writePadEditsToDbNow(domainId, localPadId, editInfo) {
+ var globalPadId = padutils.makeGlobalId(domainId, localPadId);
+ pro_padmeta.accessProPad(globalPadId, function(propad) {
+ propad.setLastEditedDate(editInfo.lastEditTime);
+ propad.setLastEditor(editInfo.lastEditorId);
+ editInfo.recentEditors.forEach(function(eid) {
+ propad.addEditor(eid);
+ });
+ });
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_padlist.js b/etherpad/src/etherpad/pro/pro_padlist.js
new file mode 100644
index 0000000..73b179c
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_padlist.js
@@ -0,0 +1,289 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("jsutils.*");
+import("stringutils");
+
+import("etherpad.utils.*");
+import("etherpad.helpers");
+import("etherpad.pad.padutils");
+import("etherpad.collab.collab_server");
+
+import("etherpad.pro.pro_accounts");
+
+function _getColumnMeta() {
+ // returns map of {id --> {
+ // title,
+ // sortFn(a,b),
+ // render(p)
+ // }
+
+ function _dateNum(d) {
+ if (!d) {
+ return 0;
+ }
+ return -1 * (+d);
+ }
+
+
+ var cols = {};
+
+ function addAvailableColumn(id, cdata) {
+ if (!cdata.render) {
+ cdata.render = function(p) {
+ return p[id];
+ };
+ }
+ if (!cdata.cmpFn) {
+ cdata.cmpFn = function(a,b) {
+ return cmp(a[id], b[id]);
+ };
+ }
+ cdata.id = id;
+ cols[id] = cdata;
+ }
+
+ addAvailableColumn('public', {
+ title: "",
+ render: function(p) {
+ // TODO: implement an icon with hover text that says public vs.
+ // private
+ return "";
+ },
+ cmpFn: function(a,b) {
+ return 0; // not sort-able
+ }
+ });
+ addAvailableColumn('secure', {
+ title: "",
+ render: function(p) {
+ if (p.password) {
+ return IMG({src: '/static/img/may09/padlock.gif'});
+ } else {
+ return "";
+ }
+ },
+ cmpFn: function(a,b) {
+ return cmp(a.password, b.password);
+ }
+ });
+ addAvailableColumn('title', {
+ title: "Title",
+ render: function(p) {
+ var t = padutils.getProDisplayTitle(p.localPadId, p.title);
+ return A({href: "/"+p.localPadId}, t);
+ },
+ sortFn: function(a, b) {
+ return cmp(padutils.getProDisplayTitle(a.localPadId, a.title),
+ padutils.getProDisplayTitle(b.localPadId, b.title));
+ }
+ });
+ addAvailableColumn('creatorId', {
+ title: "Creator",
+ render: function(p) {
+ return pro_accounts.getFullNameById(p.creatorId);
+ },
+ sortFn: function(a, b) {
+ return cmp(pro_accounts.getFullNameById(a.creatorId),
+ pro_accounts.getFullNameById(b.creatorId));
+ }
+ });
+ addAvailableColumn('createdDate', {
+ title: "Created",
+ render: function(p) {
+ return timeAgo(p.createdDate);
+ },
+ sortFn: function(a, b) {
+ return cmp(_dateNum(a.createdDate), _dateNum(b.createdDate));
+ }
+ });
+ addAvailableColumn('lastEditorId', {
+ title: "Last Editor",
+ render: function(p) {
+ if (p.lastEditorId) {
+ return pro_accounts.getFullNameById(p.lastEditorId);
+ } else {
+ return "";
+ }
+ },
+ sortFn: function(a, b) {
+ var a_ = a.lastEditorId ? pro_accounts.getFullNameById(a.lastEditorId) : "ZZZZZZZZZZ";
+ var b_ = b.lastEditorId ? pro_accounts.getFullNameById(b.lastEditorId) : "ZZZZZZZZZZ";
+ return cmp(a_, b_);
+ }
+ });
+
+ addAvailableColumn('editors', {
+ title: "Editors",
+ render: function(p) {
+ var editors = [];
+ p.proAttrs.editors.forEach(function(editorId) {
+ editors.push([editorId, pro_accounts.getFullNameById(editorId)]);
+ });
+ editors.sort(function(a,b) { return cmp(a[1], b[1]); });
+ var sp = SPAN();
+ for (var i = 0; i < editors.length; i++) {
+ if (i > 0) {
+ sp.push(", ");
+ }
+ sp.push(A({href: "/ep/padlist/edited-by?editorId="+editors[i][0]}, editors[i][1]));
+ }
+ return sp;
+ }
+ });
+
+ addAvailableColumn('lastEditedDate', {
+ title: "Last Edited",
+ render: function(p) {
+ if (p.lastEditedDate) {
+ return timeAgo(p.lastEditedDate);
+ } else {
+ return "never";
+ }
+ },
+ sortFn: function(a,b) {
+ return cmp(_dateNum(a.lastEditedDate), _dateNum(b.lastEditedDate));
+ }
+ });
+ addAvailableColumn('localPadId', {
+ title: "Path",
+ });
+ addAvailableColumn('actions', {
+ title: "",
+ render: function(p) {
+ return DIV({className: "gear-drop", id: "pad-gear-"+p.id}, " ");
+ }
+ });
+
+ addAvailableColumn('connectedUsers', {
+ title: "Connected Users",
+ render: function(p) {
+ var names = [];
+ padutils.accessPadLocal(p.localPadId, function(pad) {
+ var userList = collab_server.getConnectedUsers(pad);
+ userList.forEach(function(u) {
+ if (collab_server.translateSpecialKey(u.specialKey) != 'invisible') {
+ // excludes etherpad admin user
+ names.push(u.name);
+ }
+ });
+ });
+ return names.join(", ");
+ }
+ });
+
+ return cols;
+}
+
+function _sortPads(padList) {
+ var meta = _getColumnMeta();
+ var sortId = _getCurrentSortId();
+ var reverse = false;
+ if (sortId.charAt(0) == '-') {
+ reverse = true;
+ sortId = sortId.slice(1);
+ }
+ padList.sort(function(a,b) { return cmp(a.localPadId, b.localPadId); });
+ padList.sort(function(a,b) { return meta[sortId].sortFn(a, b); });
+ if (reverse) { padList.reverse(); }
+}
+
+function _addClientVars(padList) {
+ var padTitles = {}; // maps localPadId -> title
+ var localPadIds = {}; // maps padmetaId -> localPadId
+ padList.forEach(function(p) {
+ padTitles[p.localPadId] = stringutils.toHTML(padutils.getProDisplayTitle(p.localPadId, p.title));
+ localPadIds[p.id] = p.localPadId;
+ });
+ helpers.addClientVars({
+ padTitles: padTitles,
+ localPadIds: localPadIds
+ });
+}
+
+function _getCurrentSortId() {
+ return request.params.sortBy || "lastEditedDate";
+}
+
+function _renderColumnHeader(m) {
+ var sp = SPAN();
+ var sortBy = _getCurrentSortId();
+ if (m.sortFn) {
+ var d = {sortBy: m.id};
+ var arrow = "";
+ if (sortBy == m.id) {
+ d.sortBy = ("-"+m.id);
+ arrow = html("&#8595;");
+ }
+ if (sortBy == ("-"+m.id)) {
+ arrow = html("&#8593;");
+ }
+ sp.push(arrow, " ", A({href: qpath(d)}, m.title));
+ } else {
+ sp.push(m.title);
+ }
+ return sp;
+}
+
+function renderPadList(padList, columnIds, limit) {
+ _sortPads(padList);
+ _addClientVars(padList);
+
+ if (limit && (limit < padList.length)) {
+ padList = padList.slice(0,limit);
+ }
+
+ var showSecurityInfo = false;
+ padList.forEach(function(p) {
+ if (p.password && p.password.length > 0) { showSecurityInfo = true; }
+ });
+ if (!showSecurityInfo && (columnIds[0] == 'secure')) {
+ columnIds.shift();
+ }
+
+ var columnMeta = _getColumnMeta();
+
+ var t = TABLE({id: "padtable", cellspacing:"0", cellpadding:"0"});
+ var toprow = TR({className: "toprow"});
+ columnIds.forEach(function(cid) {
+ toprow.push(TH(_renderColumnHeader(columnMeta[cid])));
+ });
+ t.push(toprow);
+
+ padList.forEach(function(p) {
+ // Note that this id is always numeric, and is the actual
+ // canonical padmeta id.
+ var row = TR({id: 'padmeta-'+p.id});
+ var first = true;
+ for (var i = 0; i < columnIds.length; i++) {
+ var cid = columnIds[i];
+ var m = columnMeta[cid];
+ var classes = cid;
+ if (i == 0) {
+ classes += (" first");
+ }
+ if (i == (columnIds.length - 1)) {
+ classes += (" last");
+ }
+ row.push(TD({className: classes}, m.render(p)));
+ }
+ t.push(row);
+ });
+
+ return t;
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_padmeta.js b/etherpad/src/etherpad/pro/pro_padmeta.js
new file mode 100644
index 0000000..6f911b2
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_padmeta.js
@@ -0,0 +1,111 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("stringutils");
+import("cache_utils.syncedWithCache");
+import("sync");
+
+import("etherpad.pad.padutils");
+import("etherpad.pro.pro_pad_db");
+
+function _doWithProPadLock(domainId, localPadId, func) {
+ var lockName = ["pro-pad", domainId, localPadId].join("/");
+ return sync.doWithStringLock(lockName, func);
+}
+
+function accessProPad(globalPadId, fn) {
+ // retrieve pad from cache
+ var domainId = padutils.getDomainId(globalPadId);
+ if (!domainId) {
+ throw Error("not a pro pad: "+globalPadId);
+ }
+ var localPadId = padutils.globalToLocalId(globalPadId);
+ var padRecord = pro_pad_db.getSingleRecord(domainId, localPadId);
+
+ return _doWithProPadLock(domainId, localPadId, function() {
+ var isDirty = false;
+
+ var proPad = {
+ exists: function() { return !!padRecord; },
+ getDomainId: function() { return domainId; },
+ getLocalPadId: function() { return localPadId; },
+ getGlobalId: function() { return globalPadId; },
+ getDisplayTitle: function() { return padutils.getProDisplayTitle(localPadId, padRecord.title); },
+ setTitle: function(newTitle) {
+ padRecord.title = newTitle;
+ isDirty = true;
+ },
+ isDeleted: function() { return padRecord.isDeleted; },
+ markDeleted: function() {
+ padRecord.isDeleted = true;
+ isDirty = true;
+ },
+ getPassword: function() { return padRecord.password; },
+ setPassword: function(newPass) {
+ if (newPass == "") {
+ newPass = null;
+ }
+ padRecord.password = newPass;
+ isDirty = true;
+ },
+ isArchived: function() { return padRecord.isArchived; },
+ markArchived: function() {
+ padRecord.isArchived = true;
+ isDirty = true;
+ },
+ unmarkArchived: function() {
+ padRecord.isArchived = false;
+ isDirty = true;
+ },
+ setLastEditedDate: function(d) {
+ padRecord.lastEditedDate = d;
+ isDirty = true;
+ },
+ addEditor: function(editorId) {
+ var es = String(editorId);
+ if (es && es.length > 0 && stringutils.isNumeric(editorId)) {
+ if (padRecord.proAttrs.editors.indexOf(editorId) < 0) {
+ padRecord.proAttrs.editors.push(editorId);
+ padRecord.proAttrs.editors.sort();
+ }
+ isDirty = true;
+ }
+ },
+ setLastEditor: function(editorId) {
+ var es = String(editorId);
+ if (es && es.length > 0 && stringutils.isNumeric(editorId)) {
+ padRecord.lastEditorId = editorId;
+ this.addEditor(editorId);
+ isDirty = true;
+ }
+ }
+ };
+
+ var ret = fn(proPad);
+
+ if (isDirty) {
+ pro_pad_db.update(padRecord);
+ }
+
+ return ret;
+ });
+}
+
+function accessProPadLocal(localPadId, fn) {
+ var globalPadId = padutils.getGlobalPadId(localPadId);
+ return accessProPad(globalPadId, fn);
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_quotas.js b/etherpad/src/etherpad/pro/pro_quotas.js
new file mode 100644
index 0000000..ed69e1c
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_quotas.js
@@ -0,0 +1,141 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("funhtml.*");
+import("stringutils.startsWith");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon.inTransaction");
+
+import("etherpad.billing.team_billing");
+import("etherpad.globals.*");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.domains");
+import("etherpad.sessions.getSession");
+import("etherpad.store.checkout");
+
+function _createRecordIfNecessary(domainId) {
+ inTransaction(function() {
+ var r = sqlobj.selectSingle('pro_account_usage', {domainId: domainId});
+ if (!r) {
+ var count = pro_accounts.getActiveCount(domainId);
+ sqlobj.insert('pro_account_usage', {
+ domainId: domainId,
+ count: count,
+ lastReset: (new Date),
+ lastUpdated: (new Date)
+ });
+ }
+ });
+}
+
+/**
+ * Called after a successful payment has been made.
+ * Effect: counts the current number of domain accounts and stores that
+ * as the current account usage count.
+ */
+function resetAccountUsageCount(domainId) {
+ _createRecordIfNecessary(domainId);
+ var newCount = pro_accounts.getActiveCount(domainId);
+ sqlobj.update(
+ 'pro_account_usage',
+ {domainId: domainId},
+ {count: newCount, lastUpdated: (new Date), lastReset: (new Date)}
+ );
+}
+
+/**
+ * Returns the max number of accounts that have existed simultaneously
+ * since the last reset.
+ */
+function getAccountUsageCount(domainId) {
+ _createRecordIfNecessary(domainId);
+ var record = sqlobj.selectSingle('pro_account_usage', {domainId: domainId});
+ return record.count;
+}
+
+
+/**
+ * Updates the current account usage count by computing:
+ * usage_count = max(current_accounts, usage_count)
+ */
+function updateAccountUsageCount(domainId) {
+ _createRecordIfNecessary(domainId);
+ var record = sqlobj.selectSingle('pro_account_usage', {domainId: domainId});
+ var currentCount = pro_accounts.getActiveCount(domainId);
+ var newCount = Math.max(record.count, currentCount);
+ sqlobj.update(
+ 'pro_account_usage',
+ {domainId: domainId},
+ {count: newCount, lastUpdated: (new Date)}
+ );
+}
+
+// called per request
+
+function _generateGlobalBillingNotice(status) {
+ if (status == team_billing.CURRENT) {
+ return;
+ }
+ var notice = SPAN();
+ if (status == team_billing.PAST_DUE) {
+ var suspensionDate = checkout.formatDate(team_billing.getDomainSuspensionDate(domains.getRequestDomainId()));
+ notice.push(
+ "Warning: your account is past due and will be suspended on ",
+ suspensionDate, ".");
+ }
+ if (status == team_billing.SUSPENDED) {
+ notice.push(
+ "Warning: your account is suspended because it is more than ",
+ team_billing.GRACE_PERIOD_DAYS, " days past due.");
+ }
+
+ if (pro_accounts.isAdminSignedIn()) {
+ notice.push(" ", A({href: "/ep/admin/billing/"}, "Manage billing"), ".");
+ } else {
+ getSession().billingProblem = "Payment is required for sites with more than "+PRO_FREE_ACCOUNTS+" accounts.";
+ notice.push(" ", "Please ",
+ A({href: "/ep/payment-required"}, "contact a site administrator"), ".");
+ }
+ request.cache.globalProNotice = notice;
+}
+
+function perRequestBillingCheck() {
+ // Do nothing if under the free account limit.
+ var activeAccounts = pro_accounts.getCachedActiveCount(domains.getRequestDomainId());
+ if (activeAccounts <= PRO_FREE_ACCOUNTS) {
+ return;
+ }
+
+ var status = team_billing.getDomainStatus(domains.getRequestDomainId());
+ _generateGlobalBillingNotice(status);
+
+ // now see if we need to block the request because of account
+ // suspension
+ if (status != team_billing.SUSPENDED) {
+ return;
+ }
+ // These path sare still OK if a suspension is on.
+ if ((startsWith(request.path, "/ep/account/") ||
+ startsWith(request.path, "/ep/admin/") ||
+ startsWith(request.path, "/ep/pro-help/") ||
+ startsWith(request.path, "/ep/payment-required"))) {
+ return;
+ }
+
+ getSession().billingProblem = "Payment is required for sites with more than "+PRO_FREE_ACCOUNTS+" accounts.";
+ response.redirect('/ep/payment-required');
+}
+
diff --git a/etherpad/src/etherpad/pro/pro_utils.js b/etherpad/src/etherpad/pro/pro_utils.js
new file mode 100644
index 0000000..d3098d7
--- /dev/null
+++ b/etherpad/src/etherpad/pro/pro_utils.js
@@ -0,0 +1,169 @@
+/**
+ * Copyright 2009 Google Inc.
+ * Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+ *
+ * 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("funhtml.*");
+import("stringutils.startsWith");
+
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.pro.domains");
+import("etherpad.pro.pro_quotas");
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+
+import("etherpad.control.pro.pro_main_control");
+
+jimport("java.lang.System.out.println");
+
+function _stripComet(x) {
+ if (x.indexOf('.comet.') > 0) {
+ x = x.split('.comet.')[1];
+ }
+ return x;
+}
+
+function getProRequestSubdomain() {
+ var d = _stripComet(request.domain);
+ return d.split('.')[0];
+}
+
+function getRequestSuperdomain() {
+ var parts = request.domain.split('.');
+ while (parts.length > 0) {
+ var domain = parts.join('.');
+ if (domainEnabled(domain)) {
+ return domain;
+ }
+ parts.shift();
+ }
+ return false;
+}
+
+function isProDomainRequest() {
+ if(!isProAccountEnabled())
+ return false;
+ // the result of this function never changes within the same request.
+ var c = appjet.requestCache;
+ if (c.isProDomainRequest === undefined) {
+ c.isProDomainRequest = _computeIsProDomainRequest();
+ }
+ return c.isProDomainRequest;
+}
+
+function _computeIsProDomainRequest() {
+ if (pne_utils.isPNE()) {
+ return true;
+ }
+
+ var domain = _stripComet(request.domain);
+
+ if (domainEnabled(domain)) {
+ return false;
+ }
+
+ var requestSuperdomain = getRequestSuperdomain();
+
+ if (domainEnabled(requestSuperdomain)) {
+ // now see if this subdomain is actually in our database.
+ if (domains.getRequestDomainRecord()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ return false;
+}
+
+function preDispatchAccountCheck() {
+ // if account is not logged in, redirect to /ep/account/login
+ //
+ // if it's PNE and there is no admin account, allow them to create an admin
+ // account.
+
+ if (pro_main_control.isActivationAllowed()) {
+ return;
+ }
+
+ if (!pro_accounts.doesAdminExist()) {
+ if (request.path != '/ep/account/create-admin-account') {
+ // should only happen for eepnet installs
+ response.redirect('/ep/account/create-admin-account');
+ }
+ } else {
+ pro_accounts.requireAccount();
+ }
+
+ pro_quotas.perRequestBillingCheck();
+}
+
+function renderFramedMessage(m) {
+ renderFramedHtml(
+ DIV(
+ {style: "font-size: 2em; padding: 2em; margin: 4em; border: 1px solid #ccc; background: #e6e6e6;"},
+ m));
+}
+
+function getFullProDomain() {
+ // TODO: have a special config param for this? --etherpad.canonicalDomain
+ return request.domain;
+}
+
+// domain, including port if necessary
+function getFullProHost() {
+ var h = getFullProDomain();
+ var parts = request.host.split(':');
+ if (parts.length > 1) {
+ h += (':' + parts[1]);
+ }
+ return h;
+}
+
+function getFullSuperdomainHost() {
+ if (isProDomainRequest()) {
+ var h = getRequestSuperdomain()
+ var parts = request.host.split(':');
+ if (parts.length > 1) {
+ h += (':' + parts[1]);
+ }
+ return h;
+ } else {
+ return request.host;
+ }
+}
+
+function getEmailFromAddr() {
+ var fromDomain = 'pad.spline.inf.fu-berlin.de';
+ if (pne_utils.isPNE()) {
+ fromDomain = getFullProDomain();
+ }
+ return ('"EtherPad" <noreply@'+fromDomain+'>');
+}
+
+function renderGlobalProNotice() {
+ if (request.cache.globalProNotice) {
+ return DIV({className: 'global-pro-notice'},
+ request.cache.globalProNotice);
+ } else {
+ return "";
+ }
+}
+
diff --git a/etherpad/src/etherpad/quotas.js b/etherpad/src/etherpad/quotas.js
new file mode 100644
index 0000000..7e939ec
--- /dev/null
+++ b/etherpad/src/etherpad/quotas.js
@@ -0,0 +1,50 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("stringutils");
+import("etherpad.licensing");
+import("etherpad.utils.*");
+import("etherpad.pne.pne_utils");
+
+// TODO: hook into PNE?
+
+function getMaxSimultaneousPadEditors(globalPadId) {
+ if (isProDomainRequest()) {
+ if (pne_utils.isPNE()) {
+ return licensing.getMaxUsersPerPad();
+ } else {
+ return 1e6;
+ }
+ } else {
+ // pad.spline.inf.fu-berlin.de public pads
+ if (globalPadId && stringutils.startsWith(globalPadId, "conf-")) {
+ return 64;
+ } else {
+ return 16;
+ }
+ }
+ return 1e6;
+}
+
+function getMaxSavedRevisionsPerPad() {
+ if (isProDomainRequest()) {
+ return 1e3;
+ } else {
+ // free public pad.spline.inf.fu-berlin.de
+ return 100;
+ }
+}
+
diff --git a/etherpad/src/etherpad/sessions.js b/etherpad/src/etherpad/sessions.js
new file mode 100644
index 0000000..f430ddd
--- /dev/null
+++ b/etherpad/src/etherpad/sessions.js
@@ -0,0 +1,203 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sessions");
+import("stringutils.randomHash");
+import("funhtml.*");
+
+import("etherpad.log");
+import("etherpad.globals.*");
+import("etherpad.pro.pro_utils");
+import("etherpad.utils.*");
+import("cache_utils.syncedWithCache");
+
+jimport("java.lang.System.out.println");
+
+var _TRACKING_COOKIE_NAME = "ET";
+var _SESSION_COOKIE_NAME = "ES";
+
+function _updateInitialReferrer(data) {
+
+ if (data.initialReferer) {
+ return;
+ }
+
+ var ref = request.headers["Referer"];
+
+ if (!ref) {
+ return;
+ }
+ if (ref.indexOf('http://'+request.host) == 0) {
+ return;
+ }
+ if (ref.indexOf('https://'+request.host) == 0) {
+ return;
+ }
+
+ data.initialReferer = ref;
+ log.custom("referers", {referer: ref});
+}
+
+function _getScopedDomain(subDomain) {
+ var d = request.domain;
+ if (d.indexOf(".") == -1) {
+ // special case for "localhost". For some reason, firefox does not like cookie domains
+ // to be ".localhost".
+ return undefined;
+ }
+ if (subDomain) {
+ d = subDomain + "." + d;
+ }
+ return "." + d;
+}
+//--------------------------------------------------------------------------------
+
+// pass in subDomain to get the session data for a particular subdomain --
+// intended for debugging.
+function getSession(subDomain) {
+ var sessionData = sessions.getSession({
+ cookieName: _SESSION_COOKIE_NAME,
+ domain: _getScopedDomain(subDomain)
+ });
+ _updateInitialReferrer(sessionData);
+ return sessionData;
+}
+
+function getSessionId() {
+ return sessions.getSessionId(_SESSION_COOKIE_NAME, false, _getScopedDomain());
+}
+
+function _getGlobalSessionId() {
+ return (request.isDefined && request.cookies[_SESSION_COOKIE_NAME]) || null;
+}
+
+function isAnEtherpadAdmin() {
+ var sessionId = _getGlobalSessionId();
+ if (! sessionId) {
+ return false;
+ }
+
+ return syncedWithCache("isAnEtherpadAdmin", function(c) {
+ return !! c[sessionId];
+ });
+}
+
+function setIsAnEtherpadAdmin(v) {
+ var sessionId = _getGlobalSessionId();
+ if (! sessionId) {
+ return;
+ }
+
+ syncedWithCache("isAnEtherpadAdmin", function(c) {
+ if (v) {
+ c[sessionId] = true;
+ }
+ else {
+ delete c[sessionId];
+ }
+ });
+}
+
+//--------------------------------------------------------------------------------
+
+function setTrackingCookie() {
+ if (request.cookies[_TRACKING_COOKIE_NAME]) {
+ return;
+ }
+
+ var trackingVal = randomHash(16);
+ var expires = new Date(32503708800000); // year 3000
+
+ response.setCookie({
+ name: _TRACKING_COOKIE_NAME,
+ value: trackingVal,
+ path: "/",
+ domain: _getScopedDomain(),
+ expires: expires
+ });
+}
+
+function getTrackingId() {
+ // returns '-' if no tracking ID (caller can assume)
+ return (request.cookies[_TRACKING_COOKIE_NAME] || response.getCookie(_TRACKING_COOKIE_NAME) || '-');
+}
+
+//--------------------------------------------------------------------------------
+
+function preRequestCookieCheck() {
+ if (isStaticRequest()) {
+ return;
+ }
+
+ // If this function completes without redirecting, then it means
+ // there is a valid session cookie and tracking cookie.
+
+ if (request.cookies[_SESSION_COOKIE_NAME] &&
+ request.cookies[_TRACKING_COOKIE_NAME]) {
+
+ if (request.params.cookieShouldBeSet) {
+ response.redirect(qpath({cookieShouldBeSet: null}));
+ }
+ return;
+ }
+
+ // Only superdomains can set cookies.
+ var isSuperdomain = domainEnabled(request.domain);
+
+ if (isSuperdomain) {
+ // superdomain without cookies
+
+ getSession();
+ setTrackingCookie();
+
+ // check if we need to redirect back to a subdomain.
+ if ((request.path == "/") &&
+ (request.params.setCookie) &&
+ (request.params.contUrl)) {
+
+ var contUrl = request.params.contUrl;
+ if (contUrl.indexOf("?") == -1) {
+ contUrl += "?";
+ }
+ contUrl += "&cookieShouldBeSet=1";
+ response.redirect(contUrl);
+ }
+ } else {
+ var parts = request.domain.split(".");
+ if (parts.length < 3) {
+ // invalid superdomain
+ response.write("invalid superdomain");
+ response.stop();
+ }
+ // subdomain without cookies
+ if (request.params.cookieShouldBeSet) {
+ log.warn("Cookie failure!");
+ renderFramedHtml(DIV({style: "border: 1px solid #ccc; padding: 1em; width: 600px; margin: 1em auto; font-size: 1.4em;"},
+ P("Please enable cookies in your browser in order to access this site."),
+ BR(),
+ P(A({href: "/"}, "Continue"))));
+ response.stop();
+ } else {
+ var contUrl = request.url;
+ var p = request.host.split(':')[1];
+ p = (p ? (":"+p) : "");
+ response.redirect(request.scheme+"://"+pro_utils.getRequestSuperdomain()+p+
+ "/?setCookie=1&contUrl="+encodeURIComponent(contUrl));
+ }
+ }
+}
+
+
diff --git a/etherpad/src/etherpad/statistics/exceptions.js b/etherpad/src/etherpad/statistics/exceptions.js
new file mode 100644
index 0000000..723085d
--- /dev/null
+++ b/etherpad/src/etherpad/statistics/exceptions.js
@@ -0,0 +1,231 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import("fastJSON");
+import("etherpad.log");
+import("cache_utils.syncedWithCache");
+import("funhtml.*");
+import("jsutils.{eachProperty,keys}");
+
+function _dayKey(date) {
+ return [date.getFullYear(), date.getMonth()+1, date.getDate()].join(',');
+}
+
+function _dateAddDays(date, numDays) {
+ return new Date((+date) + numDays*1000*60*60*24);
+}
+
+function _loadDay(date) {
+ var fileName = log.frontendLogFileName('exception', date);
+ if (! fileName) {
+ return [];
+ }
+ var reader = new java.io.BufferedReader(new java.io.FileReader(fileName));
+ var line = null;
+ var array = [];
+ while ((line = reader.readLine()) !== null) {
+ array.push(fastJSON.parse(line));
+ }
+ return array;
+}
+
+function _accessLatestLogs(func) {
+ syncedWithCache("etherpad.statistics.exceptions", function(exc) {
+ if (! exc.byDay) {
+ exc.byDay = {};
+ }
+ // always reload today from disk
+ var now = new Date();
+ var today = now;
+ var todayKey = _dayKey(today);
+ exc.byDay[todayKey] = _loadDay(today);
+ var activeKeys = {};
+ activeKeys[todayKey] = true;
+ // load any of 7 previous days that aren't loaded or
+ // were not loaded as a historical day
+ for(var i=1;i<=7;i++) {
+ var pastDay = _dateAddDays(today, -i);
+ var pastDayKey = _dayKey(pastDay);
+ activeKeys[pastDayKey] = true;
+ if ((! exc.byDay[pastDayKey]) || (! exc.byDay[pastDayKey].sealed)) {
+ exc.byDay[pastDayKey] = _loadDay(pastDay);
+ exc.byDay[pastDayKey].sealed = true; // in the past, won't change
+ }
+ }
+ // clear old days
+ for(var k in exc.byDay) {
+ if (! (k in activeKeys)) {
+ delete exc.byDay[k];
+ }
+ }
+
+ var logs = {
+ getDay: function(daysAgo) {
+ return exc.byDay[_dayKey(_dateAddDays(today, -daysAgo))];
+ },
+ eachLineInLastNDays: function(n, func) {
+ var oldest = _dateAddDays(now, -n);
+ var oldestNum = +oldest;
+ for(var i=n;i>=0;i--) {
+ var lines = logs.getDay(i);
+ lines.forEach(function(line) {
+ if (line.date > oldestNum) {
+ func(line);
+ }
+ });
+ }
+ }
+ };
+
+ func(logs);
+ });
+}
+
+function _exceptionHash(line) {
+ // skip the first line of jsTrace, take hashCode of rest
+ var trace = line.jsTrace;
+ var stack = trace.substring(trace.indexOf('\n') + 1);
+ return new java.lang.String(stack).hashCode();
+}
+
+// Used to take a series of strings and produce an array of
+// [common prefix, example middle, common suffix], or
+// [string] if the strings are the same. Takes oldInfo
+// and returns newInfo; each is either null or an array
+// of length 1 or 3.
+function _accumCommonPrefixSuffix(oldInfo, newString) {
+ function _commonPrefixLength(a, b) {
+ var x = 0;
+ while (x < a.length && x < b.length && a.charAt(x) == b.charAt(x)) {
+ x++;
+ }
+ return x;
+ }
+
+ function _commonSuffixLength(a, b) {
+ var x = 0;
+ while (x < a.length && x < b.length &&
+ a.charAt(a.length-1-x) == b.charAt(b.length-1-x)) {
+ x++;
+ }
+ return x;
+ }
+
+ if (! oldInfo) {
+ return [newString];
+ }
+ else if (oldInfo.length == 1) {
+ var oldString = oldInfo[0];
+ if (oldString == newString) {
+ return oldInfo;
+ }
+ var newInfo = [];
+ var a = _commonPrefixLength(oldString, newString);
+ newInfo[0] = newString.substring(0, a);
+ oldString = oldString.substring(a);
+ newString = newString.substring(a);
+ var b = _commonSuffixLength(oldString, newString);
+ newInfo[2] = newString.slice(-b);
+ oldString = oldString.slice(0, -b);
+ newString = newString.slice(0, -b);
+ newInfo[1] = newString;
+ return newInfo;
+ }
+ else {
+ // oldInfo.length == 3
+ var a = _commonPrefixLength(oldInfo[0], newString);
+ var b = _commonSuffixLength(oldInfo[2], newString);
+ return [newString.slice(0, a), newString.slice(a, -b),
+ newString.slice(-b)];
+ }
+}
+
+function render() {
+
+ _accessLatestLogs(function(logs) {
+ var weekCounts = {};
+ var totalWeekCount = 0;
+
+ // count exceptions of each kind in last week
+ logs.eachLineInLastNDays(7, function(line) {
+ var hash = _exceptionHash(line);
+ weekCounts[hash] = (weekCounts[hash] || 0) + 1;
+ totalWeekCount++;
+ });
+
+ var dayData = {};
+ var totalDayCount = 0;
+
+ // accumulate data about each exception in last 24 hours
+ logs.eachLineInLastNDays(1, function(line) {
+ var hash = _exceptionHash(line);
+ var oldData = dayData[hash];
+ var data = (oldData || {});
+ if (! oldData) {
+ data.hash = hash;
+ data.trace = line.jsTrace.substring(line.jsTrace.indexOf('\n')+1);
+ data.trackers = {};
+ }
+ var msg = line.jsTrace.substring(0, line.jsTrace.indexOf('\n'));
+ data.message = _accumCommonPrefixSuffix(data.message, msg);
+ data.count = (data.count || 0)+1;
+ data.trackers[line.tracker] = true;
+ totalDayCount++;
+ dayData[hash] = data;
+ });
+
+ // put day datas in an array and sort
+ var dayDatas = [];
+ eachProperty(dayData, function(k,v) {
+ dayDatas.push(v);
+ });
+ dayDatas.sort(function(a, b) {
+ return b.count - a.count;
+ });
+
+ // process
+ dayDatas.forEach(function(data) {
+ data.weekCount = (weekCounts[data.hash] || 0);
+ data.numTrackers = keys(data.trackers).length;
+ });
+
+ // gen HTML
+ function num(n) { return SPAN({className:'num'}, n); }
+
+ response.write(STYLE(html(".trace { height: 300px; overflow: auto; background: #eee; margin-left: 1em; font-family: monospace; border: 1px solid #833; padding: 4px; }\n"+
+ ".exc { margin: 1em 0; }\n"+
+ ".num { font-size: 150%; }")));
+
+ response.write(P("Total exceptions in past day: ", num(totalDayCount),
+ ", past week: ", totalWeekCount));
+
+ response.write(P(SMALL(EM("Data on this page is live."))));
+
+ response.write(H2("Exceptions grouped by stack trace:"));
+
+ dayDatas.forEach(function(data) {
+ response.write(DIV({className:'exc'},
+ 'Past day: ',num(data.count),', Past week: ',
+ data.weekCount,', Different tracker cookies today: ',
+ data.numTrackers,
+ '\n',data.message[0],
+ (data.message[1] && I(data.message[1])) || '',
+ (data.message[2] || ''),'\n',
+ DIV({className:'trace'}, data.trace)));
+ });
+ });
+}
diff --git a/etherpad/src/etherpad/statistics/statistics.js b/etherpad/src/etherpad/statistics/statistics.js
new file mode 100644
index 0000000..8174405
--- /dev/null
+++ b/etherpad/src/etherpad/statistics/statistics.js
@@ -0,0 +1,1248 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("dateutils.noon");
+import("execution");
+import("exceptionutils");
+import("fastJSON");
+import("fileutils.fileLineIterator");
+import("jsutils.*");
+import("sqlbase.sqlobj");
+
+import("etherpad.log");
+
+jimport("net.appjet.oui.GenericLoggerUtils");
+jimport("net.appjet.oui.LoggableFromJson");
+jimport("net.appjet.oui.FilterWrangler");
+jimport("java.lang.System.out.println");
+jimport("net.appjet.common.util.ExpiringMapping");
+
+var millisInDay = 86400*1000;
+
+function _stats() {
+ if (! appjet.cache.statistics) {
+ appjet.cache.statistics = {};
+ }
+ return appjet.cache.statistics;
+}
+
+function onStartup() {
+ execution.initTaskThreadPool("statistics", 1);
+ _scheduleNextDailyUpdate();
+
+ onReset();
+}
+
+function _info(m) {
+ log.info({type: 'statistics', message: m});
+}
+
+function _warn(m) {
+ log.info({type: 'statistics', message: m});
+}
+
+function _statData() {
+ return _stats().stats;
+}
+
+function getAllStatNames() {
+ return keys(_statData());
+}
+
+function getStatData(statName) {
+ return _statData()[statName];
+}
+
+function _setStatData(statName, data) {
+ _statData()[statName] = data;
+}
+
+function liveSnapshot(stat) {
+ var statObject;
+ if (typeof(stat) == 'string') {
+ // "stat" is the stat name.
+ statObject = getStatData(stat);
+ } else if (typeof(stat) == 'object') {
+ statObject = stat;
+ } else {
+ return;
+ }
+ return _callFunction(statObject.snapshot_f,
+ statObject.name, statObject.options, statObject.data);
+}
+
+// ------------------------------------------------------------------
+// stats processing
+// ------------------------------------------------------------------
+
+// some useful constants
+var LIVE = 'live';
+var HIST = 'historical';
+var HITS = 'hits';
+var UNIQ = 'uniques';
+var VALS = 'values';
+var HGRM = 'histogram';
+
+// helpers
+
+function _date(d) {
+ return new Date(d);
+}
+
+function _saveStat(day, name, value) {
+ var timestamp = Math.floor(day.valueOf() / 1000);
+ _info({statistic: name,
+ timestamp: timestamp,
+ value: value});
+ try {
+ sqlobj.insert('statistics', {
+ name: name,
+ timestamp: timestamp,
+ value: fastJSON.stringify(value)
+ });
+ } catch (e) {
+ var msg;
+ try {
+ msg = e.getMessage();
+ } catch (e2) {
+ try {
+ msg = e.toSource();
+ } catch (e3) {
+ msg = "(none)";
+ }
+ }
+ _warn("failed to save stat "+name+": "+msg);
+ }
+}
+
+function _convertScalaTopValuesToJs(topValues) {
+ var totalValue = topValues._1();
+ var countsMap = topValues._2();
+ countsObj = {};
+ countsMap.foreach(scalaF1(function(pair) { countsObj[pair._1()] = pair._2(); }));
+ return {total: totalValue, counts: countsObj};
+}
+
+function _fakeMap() {
+ var map = {}
+ return {
+ get: function(k) { return map[k]; },
+ put: function(k, v) { map[k] = v; },
+ remove: function(k) { delete map[k]; }
+ }
+}
+
+function _withinSecondsOf(numSeconds, t1, t2) {
+ return (t1 > t2-numSeconds*1000) && (t1 < t2+numSeconds*1000);
+}
+
+function _callFunction(functionName, arg1, arg2, etc) {
+ var f = this[functionName];
+ var args = Array.prototype.slice.call(arguments, 1);
+ return f.apply(this, args);
+}
+
+// trackers and other init functions
+
+function _hitTracker(trackerType, timescaleType) {
+ var className;
+ switch (trackerType) {
+ case HITS: className = "BucketedLastHits"; break;
+ case UNIQ: className = "BucketedUniques"; break;
+ case VALS: className = "BucketedValueCounts"; break;
+ case HGRM: className = "BucketedLastHitsHistogram"; break;
+ }
+ var tracker;
+ switch (timescaleType) {
+ case LIVE:
+ tracker = new net.appjet.oui[className](24*60*60*1000);
+ break;
+ case HIST:
+ // timescale just needs to be longer than a day.
+ tracker = new net.appjet.oui[className](365*24*60*60*1000, true);
+ break;
+ }
+
+ var conversionData = {
+ total_f: "count",
+ history_f: "history",
+ latest_f: "latest",
+ };
+ switch (trackerType) {
+ case HITS: case UNIQ:
+ conversionData.conversionFunction =
+ function(x) { return x; } // no conversion necessary.
+ break;
+ case VALS:
+ conversionData.conversionFunction = _convertScalaTopValuesToJs
+ break;
+ case HGRM:
+ conversionData.conversionFunction =
+ function(hFunc) { return function(pct) { return hFunc.apply(pct); } }
+ break;
+ }
+
+
+ return {
+ tracker: tracker,
+ conversionData: conversionData,
+ hit: function(d, n1, n2) {
+ d = _date(d);
+ if (n2 === undefined) {
+ this.tracker.hit(d, n1);
+ } else {
+ this.tracker.hit(d, n1, n2);
+ }
+ },
+ get total() {
+ return this.conversionData.conversionFunction(this.tracker[this.conversionData.total_f]());
+ },
+ history: function(bucketsPerSample, numSamples) {
+ var scalaArray = this.tracker[this.conversionData.history_f](bucketsPerSample, numSamples);
+ var jsArray = [];
+ for (var i = 0; i < scalaArray.length(); ++i) {
+ jsArray.push(this.conversionData.conversionFunction(scalaArray.apply(i)));
+ }
+ return jsArray;
+ },
+ latest: function(bucketsPerSample) {
+ return this.conversionData.conversionFunction(this.tracker[this.conversionData.latest_f](bucketsPerSample));
+ }
+ }
+}
+
+function _initCount(statName, options, timescaleType) {
+ return _hitTracker(HITS, timescaleType);
+}
+function _initUniques(statName, options, timescaleType) {
+ return _hitTracker(UNIQ, timescaleType);
+}
+function _initTopValues(statName, options, timescaleType) {
+ return _hitTracker(VALS, timescaleType);
+}
+function _initHistogram(statName, options, timescaleType) {
+ return _hitTracker(HGRM, timescaleType);
+}
+
+function _initLatencies(statName, options, type) {
+ var hits = _initTopValues(statName, options, type);
+ var latencies = _initTopValues(statName, options, type);
+
+ return {
+ hit: function(d, value, latency) {
+ hits.hit(d, value);
+ latencies.hit(d, value, latency);
+ },
+ hits: hits,
+ latencies: latencies
+ }
+}
+
+function _initDisconnectTracker(statName, options, timescaleType) {
+ return {
+ map: (timescaleType == LIVE ? new ExpiringMapping(60*1000) : _fakeMap()),
+ counter: _initCount(statName, options, timescaleType),
+ uniques: _initUniques(statName, options, timescaleType),
+ isLive: timescaleType == LIVE
+ }
+}
+
+// update functions
+
+function _updateCount(statName, options, logName, data, logObject) {
+ // println("update count: "+statName+" on log "+logName+", with data: "+data.toSource()+" with log entry: "+logObject.toSource());
+ if (options.filter == null || options.filter(logObject)) {
+ data.hit(logObject.date, 1);
+ }
+}
+
+function _updateSum(statName, options, logName, data, logObject) {
+ // println("update sum: "+statName+" on log "+logName+", with data: "+data.toSource()+" with log entry: "+logObject.toSource());
+ if (options.filter == null || options.filter(logObject)) {
+ data.hit(logObject.date, Math.round(Number(logObject[options.fieldName])));
+ }
+}
+
+function _updateUniquenessCount(statName, options, logName, data, logObject) {
+ // println("update uniqueness: "+statName+" on log "+logName+", with data: "+data.toSource()+" with log entry: "+logObject.toSource());
+ if (options.filter == null || options.filter(logObject)) {
+ var value = logObject[options.fieldName];
+ if (value === undefined) { return; }
+ data.hit(logObject.date, value);
+ }
+}
+
+function _updateTopValues(statName, options, logName, data, logObject) {
+ // println("update topvalues: "+statName+" on log "+logName+", with data: "+data.toSource()+" with log entry: "+logObject.toSource());
+
+ if (options.filter == null || options.filter(logObject)) {
+ var value = logObject[options.fieldName];
+ if (value === undefined) { return; }
+ if (options.canonicalizer) {
+ value = options.canonicalizer(value);
+ }
+ data.hit(logObject.date, value);
+ }
+}
+
+function _updateLatencies(statName, options, logName, data, logObject) {
+ // println("update latencies: "+statName+" on log "+logName+", with data: "+data.toSource()+" with log entry: "+logObject.toSource());
+
+ if (options.filter == null || options.filter(logObject)) {
+ var value = logObject[options.fieldName];
+ var latency = logObject[options.latencyFieldName];
+ if (value === undefined) { return; }
+ data.hit(logObject.date, value, latency);
+ }
+}
+
+function _updateDisconnectTracker(statName, options, logName, data, logObject) {
+ if (logName == "frontend/padevents" && logObject.type != "userleave") {
+ // we only care about userleaves from the padevents log.
+ return;
+ }
+
+ var [evtPrefix, otherPrefix] =
+ (logName == "frontend/padevents" ? ["l-", "d-"] : ["d-", "l-"]);
+ var dateLong = logObject.date;
+ var userId = logObject.session;
+
+ var lastOtherEvent = data.map.get(otherPrefix+userId);
+ if (lastOtherEvent != null && _withinSecondsOf(60, dateLong, lastOtherEvent.date)) {
+ data.counter.hit(logObject.date, 1);
+ data.uniques.hit(logObject.date, userId);
+ data.map.remove(otherPrefix+userId);
+ if (data.isLive) {
+ log.custom("avoidable_disconnects",
+ {userId: userId,
+ errorMessage: lastOtherEvent.errorMessage || logObject.errorMessage});
+ }
+ } else {
+ data.map.put(evtPrefix+userId, {date: dateLong, message: logObject.errorMessage});
+ }
+}
+
+// snapshot functions
+
+function _lazySnapshot(snapshot) {
+ var total;
+ var history = {};
+ var latest = {};
+ return {
+ get total() {
+ if (total === undefined) {
+ total = snapshot.total;
+ }
+ return total;
+ },
+ history: function(bucketsPerSample, numSamples) {
+ if (history[""+bucketsPerSample+":"+numSamples] === undefined) {
+ history[""+bucketsPerSample+":"+numSamples] = snapshot.history(bucketsPerSample, numSamples);
+ }
+ return history[""+bucketsPerSample+":"+numSamples];
+ },
+ latest: function(bucketsPerSample) {
+ if (latest[""+bucketsPerSample] === undefined) {
+ latest[""+bucketsPerSample] = snapshot.latest(bucketsPerSample);
+ }
+ return latest[""+bucketsPerSample];
+ }
+ }
+}
+
+function _snapshotTotal(statName, options, data) {
+ return _lazySnapshot(data);
+}
+
+function _convertTopValue(topValue) {
+ var counts = topValue.counts;
+ var sortedValues = keys(counts).sort(function(x, y) {
+ return counts[y] - counts[x];
+ }).map(function(key) {
+ return { value: key, count: counts[key] };
+ });
+ return {count: topValue.total, topValues: sortedValues.slice(0, 50) };
+}
+
+function _snapshotTopValues(statName, options, data) {
+ var convertedData = {};
+
+ return _lazySnapshot({
+ get total() {
+ return _convertTopValue(data.total);
+ },
+ history: function(bucketsPerSample, numSamples) {
+ return data.history(bucketsPerSample, numSamples).map(_convertTopValue);
+ },
+ latest: function(bucketsPerSample) {
+ return _convertTopValue(data.latest(bucketsPerSample));
+ }
+ });
+}
+
+function _snapshotLatencies(statName, options, data) {
+ // convert the hits + total latencies into a topValues-style data object.
+ var hits = data.hits;
+ var totalLatencies = data.latencies;
+
+ function convertCountsObjects(latencyCounts, hitCounts) {
+ var mergedCounts = {}
+ keys(latencyCounts.counts).forEach(function(value) {
+ mergedCounts[value] =
+ Math.round(latencyCounts.counts[value] / (hitCounts.counts[value] || 1));
+ });
+ return {counts: mergedCounts, total: latencyCounts.total / (hitCounts.total || 1)};
+ }
+
+ // ...and then convert that object into a snapshot.
+ return _snapshotTopValues(statName, options, {
+ get total() {
+ return convertCountsObjects(totalLatencies.total, hits.total);
+ },
+ history: function(bucketsPerSample, numSamples) {
+ return mergeArrays(
+ convertCountsObjects,
+ totalLatencies.history(bucketsPerSample, numSamples),
+ hits.history(bucketsPerSample, numSamples));
+ },
+ latest: function(bucketsPerSample) {
+ return convertCountsObjects(totalLatencies.latest(bucketsPerSample), hits.latest(bucketsPerSample));
+ }
+ });
+}
+
+function _snapshotDisconnectTracker(statName, options, data) {
+ var topValues = {};
+ var counts = data.counter;
+ var uniques = data.uniques;
+ function topValue(counts, uniques) {
+ return {
+ count: counts,
+ topValues: [{value: "total_disconnects", count: counts},
+ {value: "disconnected_userids", count: uniques}]
+ }
+ }
+ return _lazySnapshot({
+ get total() {
+ return topValue(counts.total, uniques.total);
+ },
+ history: function(bucketsPerSample, numSamples) {
+ return mergeArrays(
+ topValue,
+ counts.history(bucketsPerSample, numSamples),
+ uniques.history(bucketsPerSample, numSamples));
+ },
+ latest: function(bucketsPerSample) {
+ return topValue(counts.latest(bucketsPerSample), uniques.latest(bucketsPerSample));
+ }
+ });
+}
+
+function _generateLogInterestMap(statNames) {
+ var interests = {};
+ statNames.forEach(function(statName) {
+ var logs = getStatData(statName).logNames;
+ logs.forEach(function(logName) {
+ if (! interests[logName]) {
+ interests[logName] = {};
+ }
+ interests[logName][statName] = true;
+ });
+ });
+ return interests;
+}
+
+
+// ------------------------------------------------------------------
+// stat generators
+// ------------------------------------------------------------------
+
+// statSpec has these properties
+// name
+// dataType - line, topvalues, histogram, etc.
+// logNames
+// init_f
+// update_f
+// snapshot_f
+// options - object containing any additional data, passed in to to the various functions.
+
+// init_f gets (statName, options, "live"|"historical")
+// update_f gets (statName, options, logName, data, logObject)
+// snapshot_f gets (statName, options, data)
+function addStat(statSpec) {
+ var statName = statSpec.name;
+ if (! getStatData(statName)) {
+ var initialData =
+ _callFunction(statSpec.init_f, statName, statSpec.options, LIVE);
+ _setStatData(statName, {
+ data: initialData,
+ });
+ }
+
+ var s = getStatData(statName);
+
+ s.options = statSpec.options;
+ s.name = statName;
+ s.logNames = statSpec.logNames;
+ s.dataType = statSpec.dataType;
+ s.historicalDays = ("historicalDays" in statSpec ? statSpec.historicalDays : 1);
+
+ s.init_f = statSpec.init_f;
+ s.update_f = statSpec.update_f;
+ s.snapshot_f = statSpec.snapshot_f;
+
+ function registerInterest(logName) {
+ if (! _stats().logNamesToInterestedStatNames[logName]) {
+ _stats().logNamesToInterestedStatNames[logName] = {};
+ }
+ _stats().logNamesToInterestedStatNames[logName][statName] = true;
+ }
+ statSpec.logNames.forEach(registerInterest);
+}
+
+function addSimpleCount(statName, historicalDays, logName, filter) {
+ addStat({
+ name: statName,
+ dataType: "line",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initCount",
+ update_f: "_updateCount",
+ snapshot_f: "_snapshotTotal",
+ options: { filter: filter },
+ historicalDays: historicalDays || 1
+ });
+}
+
+function addSimpleSum(statName, historicalDays, logName, filter, fieldName) {
+ addStat({
+ name: statName,
+ dataType: "line",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initCount",
+ update_f: "_updateSum",
+ snapshot_f: "_snapshotTotal",
+ options: { filter: filter, fieldName: fieldName },
+ historicalDays: historicalDays || 1
+ });
+}
+
+function addUniquenessCount(statName, historicalDays, logName, filter, fieldName) {
+ addStat({
+ name: statName,
+ dataType: "line",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initUniques",
+ update_f: "_updateUniquenessCount",
+ snapshot_f: "_snapshotTotal",
+ options: { filter: filter, fieldName: fieldName },
+ historicalDays: historicalDays || 1
+ })
+}
+
+function addTopValuesStat(statName, historicalDays, logName, filter, fieldName, canonicalizer) {
+ addStat({
+ name: statName,
+ dataType: "topValues",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initTopValues",
+ update_f: "_updateTopValues",
+ snapshot_f: "_snapshotTopValues",
+ options: { filter: filter, fieldName: fieldName, canonicalizer: canonicalizer },
+ historicalDays: historicalDays || 1
+ });
+}
+
+function addLatenciesStat(statName, historicalDays, logName, filter, fieldName, latencyFieldName) {
+ addStat({
+ name: statName,
+ dataType: "topValues",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initLatencies",
+ update_f: "_updateLatencies",
+ snapshot_f: "_snapshotLatencies",
+ options: { filter: filter, fieldName: fieldName, latencyFieldName: latencyFieldName },
+ historicalDays: historicalDays || 1
+ });
+}
+
+
+// RETURNING USERS
+
+function _initReturningUsers(statName, options, timescaleType) {
+ return { cache: {}, uniques: _initUniques(statName, options, timescaleType) };
+}
+
+function _returningUsersUserId(logObject) {
+ if (logObject.type == "userjoin") {
+ return logObject.userId;
+ }
+}
+
+function _returningUsersUserCreationDate(userId) {
+ var record = sqlobj.selectSingle('pad_cookie_userids', {id: userId});
+ if (record) {
+ return record.createdDate.getTime();
+ }
+}
+
+function _returningUsersAccountId(logObject) {
+ return logObject.proAccountId;
+}
+
+function _returningUsersAccountCreationDate(accountId) {
+ var record = sqlobj.selectSingle('pro_accounts', {id: accountId});
+ if (record) {
+ return record.createdDate.getTime();
+ }
+}
+
+
+function _updateReturningUsers(statName, options, logName, data, logObject) {
+ var userId = (options.useProAccountId ? _returningUsersAccountId(logObject) : _returningUsersUserId(logObject));
+ if (! userId) { return; }
+ var date = logObject.date;
+ if (! data.cache[""+userId]) {
+ var creationTime = (options.useProAccountId ? _returningUsersAccountCreationDate(userId) : _returningUsersUserCreationDate(userId));
+ if (! creationTime) { return; } // hm. weird case.
+ data.cache[""+userId] = creationTime;
+ }
+ if (data.cache[""+userId] < date - options.registeredNDaysAgo*24*60*60*1000) {
+ data.uniques.hit(logObject.date, ""+userId);
+ }
+}
+function _snapshotReturningUsers(statName, options, data) {
+ return _lazySnapshot(data.uniques);
+}
+
+function addReturningUserStat(statName, pastNDays, registeredNDaysAgo) {
+ addStat({
+ name: statName,
+ dataType: "line",
+ logNames: ["frontend/padevents"],
+ init_f: "_initReturningUsers",
+ update_f: "_updateReturningUsers",
+ snapshot_f: "_snapshotReturningUsers",
+ options: { registeredNDaysAgo: registeredNDaysAgo },
+ historicalDays: pastNDays
+ });
+}
+
+function addReturningProAccountStat(statName, pastNDays, registeredNDaysAgo) {
+ addStat({
+ name: statName,
+ dataType: "line",
+ logNames: ["frontend/request"],
+ init_f: "_initReturningUsers",
+ update_f: "_updateReturningUsers",
+ snapshot_f: "_snapshotReturningUsers",
+ options: { registeredNDaysAgo: registeredNDaysAgo, useProAccountId: true },
+ historicalDays: pastNDays
+ });
+}
+
+
+function addDisconnectStat() {
+ addStat({
+ name: "streaming_disconnects",
+ dataType: "topValues",
+ logNames: ["frontend/padevents", "frontend/reconnect", "frontend/disconnected_autopost"],
+ init_f: "_initDisconnectTracker",
+ update_f: "_updateDisconnectTracker",
+ snapshot_f: "_snapshotDisconnectTracker",
+ options: {}
+ });
+}
+
+// PAD STARTUP LATENCY
+function _initPadStartupLatency(statName, options, timescaleType) {
+ return {
+ recentGets: (timescaleType == LIVE ? new ExpiringMapping(60*1000) : _fakeMap()),
+ latencies: _initHistogram(statName, options, timescaleType),
+ }
+}
+
+function _updatePadStartupLatency(statName, options, logName, data, logObject) {
+ var session = logObject.session;
+ if (logName == "frontend/request") {
+ if (! ('padId' in logObject)) { return; }
+ var padId = logObject.padId;
+ if (! data.recentGets.get(session)) {
+ data.recentGets.put(session, {});
+ }
+ data.recentGets.get(session)[padId] = logObject.date;
+ }
+ if (logName == "frontend/padevents") {
+ if (logObject.type != 'userjoin') { return; }
+ if (! data.recentGets.get(session)) { return; }
+ var padId = logObject.padId;
+ var getTime = data.recentGets.get(session)[padId];
+ if (! getTime) { return; }
+ delete data.recentGets.get(session)[padId];
+ var latency = logObject.date - getTime;
+ if (latency < 60*1000) {
+ // latencies longer than 60 seconds don't represent data we care about for this stat.
+ data.latencies.hit(logObject.date, latency);
+ }
+ }
+}
+
+function _snapshotPadStartupLatency(statName, options, data) {
+ var latencies = data.latencies;
+ function convertHistogram(histogram_f) {
+ var deciles = {};
+ [0, 1, 5, 10, 25, 50, 75, 90, 95, 99, 100].forEach(function(pct) {
+ deciles[""+pct] = histogram_f(pct);
+ });
+ return deciles;
+ }
+ return _lazySnapshot({
+ latencies: latencies,
+ get total() {
+ return convertHistogram(this.latencies.total);
+ },
+ history: function(bucketsPerSample, numSamples) {
+ return this.latencies.history(bucketsPerSample, numSamples).map(convertHistogram);
+ },
+ latest: function(bucketsPerSample) {
+ return convertHistogram(this.latencies.latest(bucketsPerSample));
+ }
+ });
+}
+
+function addPadStartupLatencyStat() {
+ addStat({
+ name: "pad_startup_times",
+ dataType: "histogram",
+ logNames: ["frontend/padevents", "frontend/request"],
+ init_f: "_initPadStartupLatency",
+ update_f: "_updatePadStartupLatency",
+ snapshot_f: "_snapshotPadStartupLatency",
+ options: {}
+ });
+}
+
+
+function _initSampleTracker(statName, options, timescaleType) {
+ return {
+ samples: Array(1440), // 1 hour at 1 sample/minute
+ nextSample: 0,
+ numSamples: 0
+ }
+}
+
+function _updateSampleTracker(statName, options, logName, data, logObject) {
+ if (options.filter && ! options.filter(logObject)) {
+ return;
+ }
+ if (options.fieldName && ! (options.fieldName in logObject)) {
+ return;
+ }
+ data.samples[data.nextSample] = (options.fieldName ? logObject[fieldName] : logObject);
+ data.nextSample++;
+ data.nextSample %= data.samples.length;
+ data.numSamples = Math.min(data.samples.length, data.numSamples+1);
+}
+
+function _snapshotSampleTracker(statName, options, data) {
+ function indexTransform(i) {
+ return (data.nextSample-data.numSamples+i + data.samples.length) % data.samples.length;
+ }
+ var merge_f = options.mergeFunction || function(a, b) { return a+b; }
+ var process_f = options.processFunction || function(a) { return a; }
+ function mergeValues(values) {
+ if (values.length <= 1) { return values[0]; }
+ var t = values[0];
+ for (var i = 1; i < values.length; ++i) {
+ t = merge_f(values[i], t);
+ }
+ return t;
+ }
+ return _lazySnapshot({
+ get total() {
+ var t = [];
+ for (var i = 0; i < data.numSamples; ++i) {
+ t.push(data.samples[indexTransform(i)]);
+ }
+ return process_f(mergeValues(t), t.length);
+ },
+ history: function(bucketsPerSample, numSamples) {
+ var allSamples = [];
+ for (var i = data.numSamples-1; i >= Math.max(0, data.numSamples - bucketsPerSample*numSamples); --i) {
+ allSamples.push(data.samples[indexTransform(i)]);
+ }
+ var out = [];
+ for (var i = 0; i < numSamples && i*bucketsPerSample < allSamples.length; ++i) {
+ var subArray = [];
+ for (var j = 0; j < bucketsPerSample && i*bucketsPerSample+j < allSamples.length; ++j) {
+ subArray.push(allSamples[i*bucketsPerSample+j]);
+ }
+ out.push(process_f(mergeValues(subArray), subArray.length));
+ }
+ return out.reverse();
+ },
+ latest: function(bucketsPerSample) {
+ var t = [];
+ for (var i = data.numSamples-1; i >= Math.max(0, data.numSamples-bucketsPerSample); --i) {
+ t.push(data.samples[indexTransform(i)]);
+ }
+ return process_f(mergeValues(t), t.length);
+ }
+ });
+}
+
+function addSampleTracker(statName, logName, filter, fieldName, mergeFunction, processFunction) {
+ addStat({
+ name: statName,
+ dataType: "histogram",
+ logNames: (logName instanceof Array ? logName : [logName]),
+ init_f: "_initSampleTracker",
+ update_f: "_updateSampleTracker",
+ snapshot_f: "_snapshotSampleTracker",
+ options: { filter: filter, fieldName: fieldName,
+ mergeFunction: mergeFunction, processFunction: processFunction }
+ });
+}
+
+function addCometLatencySampleTracker(statName) {
+ addSampleTracker(statName, "backend/server-events", typeMatcher("streaming-message-latencies"), null,
+ function(a, b) {
+ var ret = {};
+ ["count", "p50", "p90", "p95", "p99", "max"].forEach(function(key) {
+ ret[key] = (Number(a[key]) || 0) + (Number(b[key]) || 0);
+ });
+ return ret;
+ },
+ function(v, count) {
+ if (count == 0) {
+ return {
+ "50": 0, "90": 0, "95": 0, "99": 0, "100": 0
+ }
+ }
+ var ret = {count: v.count};
+ ["p50", "p90", "p95", "p99", "max"].forEach(function(key) {
+ ret[key] = (Number(v[key]) || 0)/(Number(count) || 1);
+ });
+ return {"50": Math.round(ret.p50/1000),
+ "90": Math.round(ret.p90/1000),
+ "95": Math.round(ret.p95/1000),
+ "99": Math.round(ret.p99/1000),
+ "100": Math.round(ret.max/1000)};
+ });
+}
+
+function addConnectionTypeSampleTracker(statName) {
+ var caredAboutFields = ["streaming", "longpolling", "shortpolling", "(unconnected)"];
+
+ addSampleTracker(statName, "backend/server-events", typeMatcher("streaming-connection-count"), null,
+ function(a, b) {
+ var ret = {};
+ caredAboutFields.forEach(function(k) {
+ ret[k] = (Number(a[k]) || 0) + (Number(b[k]) || 0);
+ });
+ return ret;
+ },
+ function(v, count) {
+ if (count == 0) {
+ return _convertTopValue({total: 0, counts: {}});
+ }
+ var values = {};
+ var total = 0;
+ caredAboutFields.forEach(function(k) {
+ values[k] = Math.round((Number(v[k]) || 0)/count);
+ total += values[k];
+ });
+ values["Total"] = total;
+ return _convertTopValue({
+ total: Math.round(total),
+ counts: values
+ });
+ });
+}
+
+// helpers for filter functions
+
+function expectedHostnames() {
+ var hostPart = appjet.config.listenHost || "localhost";
+ if (appjet.config.listenSecureHost != hostPart) {
+ hostPart = "("+hostPart+"|"+(appjet.config.listenSecureHost || "localhost")+")";
+ }
+ var ports = [];
+ if (appjet.config.listenPort != 80) {
+ ports.push(""+appjet.config.listenPort);
+ }
+ if (appjet.config.listenSecurePort != 443) {
+ ports.push(""+appjet.config.listenSecurePort);
+ }
+ var portPart = (ports.length > 0 ? ":("+ports.join("|")+")" : "");
+ return hostPart + portPart;
+}
+
+function fieldMatcher(fieldName, fieldValue) {
+ if (fieldValue instanceof RegExp) {
+ return function(logObject) {
+ return fieldValue.test(logObject[fieldName]);
+ }
+ } else {
+ return function(logObject) {
+ return logObject[fieldName] == fieldValue;
+ }
+ }
+}
+
+function typeMatcher(type) {
+ return fieldMatcher("type", type);
+}
+
+function invertMatcher(f) {
+ return function(logObject) {
+ return ! f(logObject);
+ }
+}
+
+function setupStatsCollector() {
+ var c;
+
+ function unwatchLog(logName) {
+ GenericLoggerUtils.clearWrangler(logName.split('/')[1], c.wranglers[logName]);
+ }
+ function watchLog(logName) {
+ c.wranglers[logName] = new Packages.net.appjet.oui.LogWrangler({
+ tell: function(lpb) {
+ c.queue.add({logName: logName, json: lpb.json()});
+ }
+ });
+ c.wranglers[logName].watch(logName.split('/')[1]);
+ }
+
+ c = _stats().liveCollector;
+ if (c) {
+ c.watchedLogs.forEach(unwatchLog);
+ delete c.wrangler;
+ } else {
+ c = _stats().liveCollector = {};
+ }
+ c.watchedLogs = keys(_stats().logNamesToInterestedStatNames);
+ c.queue = new java.util.concurrent.ConcurrentLinkedQueue();
+ c.wranglers = {};
+ c.watchedLogs.forEach(watchLog);
+
+ if (! c.updateTask || c.updateTask.isDone()) {
+ c.updateTask = execution.scheduleTask('statistics', "statisticsLiveUpdate", 2000, []);
+ }
+}
+
+serverhandlers.tasks.statisticsLiveUpdate = function() {
+ var c = _stats().liveCollector;
+ try {
+ while (true) {
+ var obj = c.queue.poll();
+ if (obj != null) {
+ var statNames =
+ keys(_stats().logNamesToInterestedStatNames[obj.logName]);
+ var logObject = fastJSON.parse(obj.json);
+ statNames.forEach(function(statName) {
+ var statObject = getStatData(statName);
+ _callFunction(statObject.update_f,
+ statName, statObject.options, obj.logName, statObject.data, logObject);
+ });
+ } else {
+ break;
+ }
+ }
+ } catch (e) {
+ println("EXCEPTION IN LIVE UPDATE: "+e+" / "+e.fileName+":"+e.lineNumber)
+ println(exceptionutils.getStackTracePlain(new net.appjet.bodylock.JSRuntimeException(String(e), e.javaException || e.rhinoException)));
+ } finally {
+ c.updateTask = execution.scheduleTask('statistics', "statisticsLiveUpdate", 2000, []);
+ }
+}
+
+function onReset() {
+ // this gets refilled every reset.
+ _stats().logNamesToInterestedStatNames = {};
+
+ // we'll want to keep around the live data, though, so this is conditionally set.
+ if (! _stats().stats) {
+ _stats().stats = {};
+ }
+
+ addSimpleCount("site_pageviews", 1, "frontend/request", null);
+ addUniquenessCount("site_unique_ips", 1, "frontend/request", null, "clientAddr");
+
+ addUniquenessCount("active_user_ids", 1, "frontend/padevents", typeMatcher("userjoin"), "userId");
+ addUniquenessCount("active_user_ids_7days", 7, "frontend/padevents", typeMatcher("userjoin"), "userId");
+ addUniquenessCount("active_user_ids_30days", 30, "frontend/padevents", typeMatcher("userjoin"), "userId");
+
+ addUniquenessCount("active_pro_accounts", 1, "frontend/request", invertMatcher(fieldMatcher("proAccountId", undefined)),
+ "proAccountId");
+ addUniquenessCount("active_pro_accounts_7days", 7, "frontend/request", invertMatcher(fieldMatcher("proAccountId", undefined)),
+ "proAccountId");
+ addUniquenessCount("active_pro_accounts_30days", 30, "frontend/request", invertMatcher(fieldMatcher("proAccountId", undefined)),
+ "proAccountId");
+
+
+ addUniquenessCount("active_pads", 1, "frontend/padevents", typeMatcher("userjoin"), "padId");
+ addSimpleCount("new_pads", 1, "frontend/padevents", typeMatcher("newpad"));
+
+ addSimpleCount("chat_messages", 1, "frontend/chat", null);
+ addUniquenessCount("active_chatters", 1, "frontend/chat", null, "userId");
+
+ addSimpleCount("exceptions", 1, "frontend/exception", null);
+
+ addSimpleCount("eepnet_trial_downloads", 1, "frontend/eepnet_download_info", null);
+
+ addSimpleSum("revenue", 1, "frontend/billing", typeMatcher("purchase-complete"), "dollars")
+
+ var hostRegExp = new RegExp("^https?:\\/\\/([-a-zA-Z0-9]+.)?"+expectedHostnames()+"\\/");
+ addTopValuesStat("top_referers", 1, "frontend/request",
+ invertMatcher(fieldMatcher(
+ "referer", hostRegExp)),
+ "referer");
+
+ addTopValuesStat("paths_404", 1, "frontend/request", fieldMatcher("statusCode", 404), "path");
+ addTopValuesStat("paths_500", 1, "frontend/request", fieldMatcher("statusCode", 500), "path");
+ addTopValuesStat("paths_exception", 1, "frontend/exception", null, "path");
+
+ addTopValuesStat("top_exceptions", 1, ["frontend/exception", "backend/exceptions"],
+ invertMatcher(fieldMatcher("trace", undefined)),
+ "trace", function(trace) {
+ var jstrace = trace.split("\n").filter(function(line) {
+ return /^\tat JS\$.*?\.js:\d+\)$/.test(line);
+ });
+ if (jstrace.length > 3) {
+ return "JS Exception:\n"+jstrace.slice(0, 10).join("\n").replace(/\t[^\(]*/g, "");
+ }
+ return trace.split("\n").slice(1, 10).join("\n").replace(/\t/g, "");
+ });
+
+ addReturningUserStat("users_1day_returning_7days", 1, 7);
+ addReturningUserStat("users_7day_returning_7days", 7, 7);
+ addReturningUserStat("users_30day_returning_7days", 30, 7);
+
+ addReturningUserStat("users_1day_returning_30days", 1, 30);
+ addReturningUserStat("users_7day_returning_30days", 7, 30);
+ addReturningUserStat("users_30day_returning_30days", 30, 30);
+
+ addReturningProAccountStat("pro_accounts_1day_returning_7days", 1, 7);
+ addReturningProAccountStat("pro_accounts_7day_returning_7days", 7, 7);
+ addReturningProAccountStat("pro_accounts_30day_returning_7days", 30, 7);
+
+ addReturningProAccountStat("pro_accounts_1day_returning_30days", 1, 30);
+ addReturningProAccountStat("pro_accounts_7day_returning_30days", 7, 30);
+ addReturningProAccountStat("pro_accounts_30day_returning_30days", 30, 30);
+
+
+ addDisconnectStat();
+ addTopValuesStat("disconnect_causes", 1, "frontend/avoidable_disconnects", null, "errorMessage");
+
+ var staticFileRegExp = /^\/static\/|^\/favicon.ico/;
+ addLatenciesStat("execution_latencies", 1, "backend/latency",
+ invertMatcher(fieldMatcher('path', staticFileRegExp)),
+ "path", "time");
+ addLatenciesStat("static_file_latencies", 1, "backend/latency",
+ fieldMatcher('path', staticFileRegExp),
+ "path", "time");
+
+ addUniquenessCount("disconnects_with_clientside_errors", 1,
+ ["frontend/reconnect", "frontend/disconnected_autopost"],
+ fieldMatcher("hasClientErrors", true), "uniqueId");
+
+ addTopValuesStat("imports_exports_counts", 1, "frontend/import-export",
+ typeMatcher("request"), "direction");
+
+ addPadStartupLatencyStat();
+
+ addCometLatencySampleTracker("streaming_latencies");
+ addConnectionTypeSampleTracker("streaming_connections");
+ // TODO: add more stats here.
+
+ setupStatsCollector();
+}
+
+//----------------------------------------------------------------
+// Log processing
+//----------------------------------------------------------------
+
+function _whichStats(statNames) {
+ var whichStats = _statData();
+ var logNamesToInterestedStatNames = _stats().logNamesToInterestedStatNames;
+
+ if (statNames) {
+ whichStats = {};
+ statNames.forEach(function(statName) { whichStats[statName] = getStatData(statName) });
+ logNamesToInterestedStatNames = _generateLogInterestMap(statNames);
+ }
+
+ return [whichStats, logNamesToInterestedStatNames];
+}
+
+function _initStatDataMap(statNames) {
+ var [whichStats, logNamesToInterestedStatNames] = _whichStats(statNames);
+
+ var statDataMap = {};
+
+ function initStat(statName, statObject) {
+ statDataMap[statName] =
+ _callFunction(statObject.init_f, statName, statObject.options, HIST);
+ }
+ eachProperty(whichStats, initStat);
+
+ return statDataMap;
+}
+
+function _saveStats(day, statDataMap, statNames) {
+ var [whichStats, logNamesToInterestedStatNames] = _whichStats(statNames);
+
+ function saveStat(statName, statObject) {
+ var value = _callFunction(statObject.snapshot_f,
+ statName, statObject.options, statDataMap[statName]).total;
+ if (typeof(value) != 'object') {
+ value = {value: value};
+ }
+ _saveStat(day, statName, value);
+ }
+ eachProperty(whichStats, saveStat);
+}
+
+function _processSingleDayLogs(day, logNamesToInterestedStatNames, statDataMap) {
+ var iterators = {};
+ keys(logNamesToInterestedStatNames).forEach(function(logName) {
+ var [prefix, logId] = logName.split("/");
+ var fileName = log.logFileName(prefix, logId, day);
+ if (! fileName) {
+ _info("No such file: "+logName+" on day "+day);
+ return;
+ }
+ iterators[logName] = fileLineIterator(fileName);
+ });
+
+ var numIterators = keys(iterators).length;
+ if (numIterators == 0) {
+ _info("No logs to process on day "+day);
+ return;
+ }
+ var sortedLogObjects = new java.util.PriorityQueue(numIterators,
+ new java.util.Comparator({
+ compare: function(o1, o2) { return o1.logObject.date - o2.logObject.date }
+ }));
+
+ function lineToLogObject(logName, json) {
+ return {logName: logName, logObject: fastJSON.parse(json)};
+ }
+
+ // begin by filling the queue with one object from each log.
+ eachProperty(iterators, function(logName, iterator) {
+ if (iterator.hasNext) {
+ sortedLogObjects.add(lineToLogObject(logName, iterator.next));
+ }
+ });
+
+ // update with all log objects, in date order (enforced by priority queue).
+ while (! sortedLogObjects.isEmpty()) {
+ var nextObject = sortedLogObjects.poll();
+ var logName = nextObject.logName;
+
+ keys(logNamesToInterestedStatNames[logName]).forEach(function(statName) {
+ var statObject = getStatData(statName);
+ _callFunction(statObject.update_f,
+ statName, statObject.options, logName, statDataMap[statName], nextObject.logObject);
+ });
+
+ // get next entry from this log, if there is one.
+ if (iterators[logName].hasNext) {
+ sortedLogObjects.add(lineToLogObject(logName, iterators[logName].next));
+ }
+ }
+}
+
+function processStatsForDay(day, statNames, statDataMap) {
+ var [whichStats, logNamesToInterestedStatNames] = _whichStats(statNames);
+
+ // process the logs, notifying the right statistics updaters.
+ _processSingleDayLogs(day, logNamesToInterestedStatNames, statDataMap);
+}
+
+//----------------------------------------------------------------
+// Daily update
+//----------------------------------------------------------------
+serverhandlers.tasks.statisticsDailyUpdate = function() {
+// do nothing for now.
+
+// dailyUpdate();
+};
+
+function _scheduleNextDailyUpdate() {
+ // Run at 1:11am every day
+ var now = +(new Date);
+ var tomorrow = new Date(now + 1000*60*60*24);
+ tomorrow.setHours(1);
+ tomorrow.setMinutes(11);
+ tomorrow.setMilliseconds(111);
+ log.info("Scheduling next daily statistics update for: "+tomorrow.toString());
+ var delay = +tomorrow - (+(new Date));
+ execution.scheduleTask("statistics", "statisticsDailyUpdate", delay, []);
+}
+
+function processStatsAsOfDay(date, statNames) {
+ var latestDay = noon(new Date(date - 1000*60*60*24));
+
+ _processLogsForNeededDays(latestDay, statNames);
+}
+
+function _processLogsForNeededDays(latestDay, statNames) {
+ if (! statNames) {
+ statNames = getAllStatNames();
+ }
+ var statDataMap = _initStatDataMap(statNames);
+
+ var agesToStats = [];
+ var atLeastOneStat = true;
+ for (var i = 0; atLeastOneStat; ++i) {
+ atLeastOneStat = false;
+ agesToStats[i] = [];
+ statNames.forEach(function(statName) {
+ var statData = getStatData(statName);
+ if (statData.historicalDays > i) {
+ atLeastOneStat = true;
+ agesToStats[i].push(statName);
+ }
+ });
+ }
+ agesToStats.pop();
+
+ for (var i = agesToStats.length-1; i >= 0; --i) {
+ var day = new Date(+latestDay - i*24*60*60*1000);
+ processStatsForDay(day, agesToStats[i], statDataMap);
+ }
+ _saveStats(latestDay, statDataMap, statNames);
+}
+
+function doDailyUpdate(date) {
+ var now = (date === undefined ? new Date() : date);
+ var yesterdayNoon = noon(new Date(+now - 1000*60*60*24));
+
+ _processLogsForNeededDays(yesterdayNoon);
+}
+
+function dailyUpdate() {
+ try {
+ doDailyUpdate();
+ } catch (ex) {
+ log.warn("statistics.dailyUpdate() failed: "+ex.toString());
+ } finally {
+ _scheduleNextDailyUpdate();
+ }
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/store/checkout.js b/etherpad/src/etherpad/store/checkout.js
new file mode 100644
index 0000000..2a4d7e7
--- /dev/null
+++ b/etherpad/src/etherpad/store/checkout.js
@@ -0,0 +1,300 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("dateutils");
+import("email.sendEmail");
+import("jsutils.*");
+import("sqlbase.sqlobj");
+import("stringutils");
+import("sync");
+
+import("etherpad.globals");
+import("etherpad.globals.*");
+import("etherpad.licensing");
+import("etherpad.utils.*");
+
+import("static.js.billing_shared.{billing=>billingJS}");
+
+function dollars(x, nocommas) {
+ if (! x) { return "0.00"; }
+ var s = String(x);
+ var dollars = s.split('.')[0];
+ var pennies = s.split('.')[1];
+
+ if (!dollars) {
+ dollars = "0";
+ }
+
+ if (!nocommas && dollars.length > 3) {
+ var newDollars = [];
+ newDollars.push(dollars[dollars.length-1]);
+
+ for (var i = 1; i < dollars.length; ++i) {
+ if (i % 3 == 0) {
+ newDollars.push(",");
+ }
+ newDollars.push(dollars[dollars.length-1-i]);
+ }
+ dollars = newDollars.reverse().join('');
+ }
+
+ if (!pennies) {
+ pennies = "00";
+ }
+
+ if (pennies.length == 1) {
+ pennies = pennies + "0";
+ }
+
+ if (pennies.length > 2) {
+ pennies = pennies.substr(0,2);
+ }
+
+ return [dollars,pennies].join('.');
+}
+
+function obfuscateCC(x) {
+ if (x.length == 16 || x.length == 15) {
+ return stringutils.repeat("X", x.length-4) + x.substr(-4);
+ } else {
+ return x;
+ }
+}
+
+
+// validation functions
+
+function isOnlyDigits(s) {
+ return /^[0-9]+$/.test(s);
+}
+
+function isOnlyLettersAndSpaces(s) {
+ return /^[a-zA-Z ]+$/.test(s);
+}
+
+function isLength(s, minLen, maxLen) {
+ if (maxLen === undefined) {
+ return (typeof(s) == 'string' && s.length == minLen);
+ } else {
+ return (typeof(s) == 'string' && s.length >= minLen && s.length <= maxLen);
+ }
+}
+
+function errorMissing(validationError, name, description) {
+ validationError(name, "Please enter a "+description+".");
+}
+
+function errorTooSomething(validationError, name, description, max, tooWhat, betterAdjective) {
+ validationError(name, "Your "+description+" is too " + tooWhat + "; please provide a "+description+
+ " that is "+max+" characters or "+betterAdjective);
+}
+
+function validateString(validationError, s, name, description, mustExist, maxLength, minLength) {
+ if (mustExist && ! s) {
+ errorMissing(validationError, name, description);
+ }
+ if (s && s.length > maxLength) {
+ errorTooSomething(validationError, name, description, maxLength, "long", "shorter");
+ }
+ if (minLength > 0 && s.length < minLength) {
+ errorTooSomething(validationError, name, description, minLength, "short", "longer");
+ }
+}
+
+function validateZip(validationError, s) {
+ if (! s) {
+ errorMissing(validationError, 'billingZipCode', "ZIP code");
+ }
+ if (! (/^\d{5}(-\d{4})?$/.test(s))) {
+ validationError('billingZipCode', "Please enter a valid ZIP code");
+ }
+}
+
+function validateBillingCart(validationError, cart) {
+ var p = cart;
+
+ if (! isOnlyLettersAndSpaces(p.billingFirstName)) {
+ validationError("billingFirstName", "Name fields may only contain alphanumeric characters.");
+ }
+
+ if (! isOnlyLettersAndSpaces(p.billingLastName)) {
+ validationError("billingLastName", "Name fields may only contain alphanumeric characters.");
+ }
+
+ var validPurchaseTypes = arrayToSet(['creditcard', 'invoice', 'paypal']);
+ if (! p.billingPurchaseType in validPurchaseTypes) {
+ validationError("billingPurchaseType", "Please select a valid purchase type.")
+ }
+
+ switch (p.billingPurchaseType) {
+ case 'creditcard':
+ if (! billingJS.validateCcNumber(p.billingCCNumber)) {
+ validationError("billingCCNumber", "Your card number doesn't appear to be valid.");
+ }
+ if (! isOnlyDigits(p.billingExpirationMonth) ||
+ ! isLength(p.billingExpirationMonth, 1, 2)) {
+ validationError("billingMeta", "Invalid expiration month.");
+ }
+ if (! isOnlyDigits(p.billingExpirationYear) ||
+ ! isLength(p.billingExpirationYear, 1, 2)) {
+ validationError("billingMeta", "Invalid expiration year.");
+ }
+ if (Number("20"+p.billingExpirationYear) <= (new Date()).getFullYear() &&
+ Number(p.billingExpirationMonth) < (new Date()).getMonth()+1) {
+ validationError("billingMeta", "Invalid expiration date.");
+ }
+ var ccType = billingJS.getCcType(p.billingCCNumber);
+ if (! isOnlyDigits(p.billingCSC) ||
+ ! isLength(p.billingCSC, (ccType == 'amex' ? 4 : 3))) {
+ validationError("billingMeta", "Invalid CSC.");
+ }
+ // falling through here!
+ case 'invoice':
+ validateString(validationError, p.billingCountry, "billingCountry", "country name", true, 2);
+ validateString(validationError, p.billingAddressLine1, "billingAddressLine1", "billing address", true, 100);
+ validateString(validationError, p.billingAddressLine2, "billingAddressLine2", "billing address", false, 100);
+ validateString(validationError, p.billingCity, "billingCity", "city name", true, 40);
+ if (p.billingCountry == "US") {
+ validateString(validationError, p.billingState, "billingState", "state name", true, 2);
+ validateZip(validationError, p.billingZipCode);
+ } else {
+ validateString(validationError, p.billingProvince, "billingProvince", "province name", true, 40, 1);
+ validateString(validationError, p.billingPostalCode, "billingPostalCode", "postal code", true, 20, 5);
+ }
+ }
+}
+
+function _cardType(number) {
+ var cardType = billingJS.getCcType(number);
+ switch (cardType) {
+ case 'visa':
+ return "Visa";
+ case 'amex':
+ return "Amex";
+ case 'disc':
+ return "Discover";
+ case 'mc':
+ return "MasterCard";
+ }
+}
+
+function generatePayInfo(cart) {
+ var isUs = cart.billingCountry == "US";
+
+ var payInfo = {
+ cardType: _cardType(cart.billingCCNumber),
+ cardNumber: cart.billingCCNumber,
+ cardExpiration: ""+cart.billingExpirationMonth+"20"+cart.billingExpirationYear,
+ cardCvv: cart.billingCSC,
+
+ nameSalutation: "",
+ nameFirst: cart.billingFirstName,
+ nameMiddle: "",
+ nameLast: cart.billingLastName,
+ nameSuffix: "",
+
+ addressStreet: cart.billingAddressLine1,
+ addressStreet2: cart.billingAddressLine2,
+ addressCity: cart.billingCity,
+ addressState: (isUs ? cart.billingState : cart.billingProvince),
+ addressZip: (isUs ? cart.billingZipCode : cart.billingPostalCode),
+ addressCountry: cart.billingCountry
+ }
+
+ return payInfo;
+}
+
+var billingCartFieldMap = {
+ cardType: {f: ["billingCCNumber"], d: "credit card number"},
+ cardNumber: { f: ["billingCCNumber"], d: "credit card number"},
+ cardExpiration: { f: ["billingMeta", "billingMeta"], d: "expiration date" },
+ cardCvv: { f: ["billingMeta"], d: "card security code" },
+ card: { f: ["billingCCNumber", "billingMeta"], d: "credit card"},
+ nameFirst: { f: ["billingFirstName"], d: "first name" },
+ nameLast: {f: ["billingLastName"], d: "last name" },
+ addressStreet: { f: ["billingAddressLine1"], d: "billing address" },
+ addressStreet2: { f: ["billingAddressLine2"], d: "billing address" },
+ addressCity: { f: ["billingCity"], d: "city" },
+ addressState: { f: ["billingState", "billingProvince"], d: "state or province" },
+ addressCountry: { f: ["billingCountry"], d: "country" },
+ addressZip: { f: ["billingZipCode", "billingPostalCode"], d: "ZIP or postal code" },
+ address: { f: ["billingAddressLine1", "billingAddressLine2", "billingCity", "billingState", "billingCountry", "billingZipCode"], d: "address" }
+}
+
+function validateErrorFields(validationError, errorPrefix, fieldList) {
+ if (fieldList.length > 0) {
+ var errorMsg;
+ var errorFields;
+ errorMsg = errorPrefix +
+ fieldList.map(function(field) { return billingCartFieldMap[field].d }).join(", ") +
+ ".";
+ errorFields = [];
+ fieldList.forEach(function(field) {
+ errorFields = errorFields.concat(billingCartFieldMap[field].f);
+ });
+ validationError(errorFields, errorMsg);
+ }
+}
+
+function guessBillingNames(cart, name) {
+ if (! cart.billingFirstName && ! cart.billingLastName) {
+ var nameParts = name.split(/\s+/);
+ if (nameParts.length == 1) {
+ cart.billingFirstName = nameParts[0];
+ } else {
+ cart.billingLastName = nameParts[nameParts.length-1];
+ cart.billingFirstName = nameParts.slice(0, nameParts.length-1).join(' ');
+ }
+ }
+}
+
+function writeToEncryptedLog(s) {
+ if (! appjet.config["etherpad.billingEncryptedLog"]) {
+ // no need to log, this probably isn't the live server.
+ return;
+ }
+ var e = net.appjet.oui.Encryptomatic;
+ sync.callsyncIfTrue(appjet.cache,
+ function() { return ! appjet.cache.billingEncryptedLog },
+ function() {
+ appjet.cache.billingEncryptedLog = {
+ writer: new java.io.FileWriter(appjet.config["etherpad.billingEncryptedLog"], true),
+ key: e.readPublicKey("RSA", new java.io.FileInputStream(appjet.config["etherpad.billingPublicKey"]))
+ }
+ });
+ var l = appjet.cache.billingEncryptedLog;
+ sync.callsync(l, function() {
+ l.writer.write(e.bytesToAscii(e.encrypt(
+ new java.io.ByteArrayInputStream((new java.lang.String(s)).getBytes("UTF-8")),
+ l.key))+"\n");
+ l.writer.flush();
+ })
+}
+
+function formatExpiration(expiration) {
+ return dateutils.shortMonths[Number(expiration.substr(0, 2))-1]+" "+expiration.substr(2);
+}
+
+function formatDate(date) {
+ return dateutils.months[date.getMonth()]+" "+date.getDate()+", "+date.getFullYear();
+}
+
+function salesEmail(to, from, subject, headers, body) {
+ sendEmail(to, from, subject, headers, body);
+ if (globals.isProduction()) {
+ sendEmail("sales@pad.spline.inf.fu-berlin.de", from, subject, headers, body);
+ }
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/store/eepnet_checkout.js b/etherpad/src/etherpad/store/eepnet_checkout.js
new file mode 100644
index 0000000..62137d3
--- /dev/null
+++ b/etherpad/src/etherpad/store/eepnet_checkout.js
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("email.sendEmail");
+import("sqlbase.sqlobj");
+import("stringutils");
+
+import("etherpad.globals");
+import("etherpad.globals.*");
+import("etherpad.licensing");
+import("etherpad.utils.*");
+import("etherpad.store.checkout.*");
+
+var COST_PER_USER = 99;
+var SUPPORT_COST_PCT = 20;
+var SUPPORT_MIN_COST = 50;
+
+function getPurchaseByEmail(email) {
+ return sqlobj.selectSingle('checkout_purchase', {email: email});
+}
+
+function hasEmailAlreadyPurchased(email) {
+ var purchase = getPurchaseByEmail(email);
+ return purchase && purchase.licenseKey ? true : false;
+}
+
+function mailLostLicense(email) {
+ var purchase = getPurchaseByEmail(email);
+ if (purchase && purchase.licenseKey) {
+ sendLicenseEmail({
+ email: email,
+ ownerName: purchase.owner,
+ orgName: purchase.organization,
+ licenseKey: purchase.licenseKey
+ });
+ }
+}
+
+function _updatePurchaseWithKey(id, key) {
+ sqlobj.updateSingle('checkout_purchase', {id: id}, {licenseKey: key});
+}
+
+function updatePurchaseWithReceipt(id, text) {
+ sqlobj.updateSingle('checkout_purchase', {id: id}, {receiptEmail: text});
+}
+
+function getPurchaseByInvoiceId(id) {
+ sqlobj.selectSingle('checkout_purchase', {invoiceId: id});
+}
+
+function generateLicenseKey(cart) {
+ var licenseKey = licensing.generateNewKey(cart.ownerName, cart.orgName, null, 2, cart.userCount);
+ cart.licenseKey = licenseKey;
+ _updatePurchaseWithKey(cart.customerId, cart.licenseKey);
+ return licenseKey;
+}
+
+function receiptEmailText(cart) {
+ return renderTemplateAsString('email/eepnet_purchase_receipt.ejs', {
+ cart: cart,
+ dollars: dollars,
+ obfuscateCC: obfuscateCC
+ });
+}
+
+function licenseEmailText(userName, licenseKey) {
+ return renderTemplateAsString('email/eepnet_license_info.ejs', {
+ userName: userName,
+ licenseKey: licenseKey,
+ isEvaluation: false
+ });
+}
+
+function sendReceiptEmail(cart) {
+ var receipt = cart.receiptEmail || receiptEmailText(cart);
+
+ salesEmail(cart.email, "sales@pad.spline.inf.fu-berlin.de",
+ "EtherPad: Receipt for "+cart.ownerName+" ("+cart.orgName+")",
+ {}, receipt);
+}
+
+function sendLicenseEmail(cart) {
+ var licenseEmail = licenseEmailText(cart.ownerName, cart.licenseKey);
+
+ salesEmail(cart.email, "sales@pad.spline.inf.fu-berlin.de",
+ "EtherPad: License Key for "+cart.ownerName+" ("+cart.orgName+")",
+ {}, licenseEmail);
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/store/eepnet_trial.js b/etherpad/src/etherpad/store/eepnet_trial.js
new file mode 100644
index 0000000..570d351
--- /dev/null
+++ b/etherpad/src/etherpad/store/eepnet_trial.js
@@ -0,0 +1,241 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("email.sendEmail");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("execution");
+
+import("etherpad.sessions.getSession");
+import("etherpad.log");
+import("etherpad.licensing");
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+
+//----------------------------------------------------------------
+
+function getTrialDays() {
+ return 30;
+}
+
+function getTrialUserQuota() {
+ return 100;
+}
+
+function mailLicense(data, licenseKey, expiresDate) {
+ var toAddr = data.email;
+ if (isTestEmail(toAddr)) {
+ toAddr = "blackhole@appjet.com";
+ }
+ var subject = ('EtherPad: Trial License Information for '+
+ data.firstName+' '+data.lastName+' ('+data.orgName+')');
+
+ var emailBody = renderTemplateAsString("email/eepnet_license_info.ejs", {
+ userName: data.firstName+" "+data.lastName,
+ licenseKey: licenseKey,
+ expiresDate: expiresDate,
+ isEvaluation: true
+ });
+
+ sendEmail(
+ toAddr,
+ 'sales@pad.spline.inf.fu-berlin.de',
+ subject,
+ {},
+ emailBody
+ );
+}
+
+function mailLostLicense(email) {
+ var data = sqlobj.selectSingle('eepnet_signups', {email: email});
+ var keyInfo = licensing.decodeLicenseInfoFromKey(data.licenseKey);
+ var expiresDate = keyInfo.expiresDate;
+
+ mailLicense(data, data.licenseKey, expiresDate);
+}
+
+function hasEmailAlreadyDownloaded(email) {
+ var existingRecord = sqlobj.selectSingle('eepnet_signups', {email: email});
+ if (existingRecord) {
+ return true;
+ } else {
+ return false
+ }
+}
+
+function createAndMailNewLicense(data) {
+ sqlcommon.inTransaction(function() {
+ var expiresDate = new Date(+(new Date)+(1000*60*60*24*getTrialDays()));
+ var licenseKey = licensing.generateNewKey(
+ data.firstName + ' ' + data.lastName,
+ data.orgName,
+ +expiresDate,
+ licensing.getEditionId('PRIVATE_NETWORK_EVALUATION'),
+ getTrialUserQuota()
+ );
+
+ // confirm key
+ if (!licensing.isValidKey(licenseKey)) {
+ throw Error("License key I just created is not valid: "+l);
+ }
+
+ // Log all this precious info
+ _logDownloadData(data, licenseKey);
+
+ // Store in database
+ sqlobj.insert("eepnet_signups", {
+ firstName: data.firstName,
+ lastName: data.lastName,
+ email: data.email,
+ orgName: data.orgName,
+ jobTitle: data.jobTitle,
+ date: new Date(),
+ signupIp: String(request.clientAddr).substr(0,16),
+ estUsers: data.estUsers,
+ licenseKey: licenseKey,
+ phone: data.phone,
+ industry: data.industry
+ });
+
+ mailLicense(data, licenseKey, expiresDate);
+
+ // Send sales notification
+ var clientAddr = request.clientAddr;
+ var initialReferer = getSession().initialReferer;
+ execution.async(function() {
+ _sendSalesNotification(data, clientAddr, initialReferer);
+ });
+
+ }); // end transaction
+}
+
+function _logDownloadData(data, licenseKey) {
+ log.custom("eepnet_download_info", {
+ email: data.email,
+ firstName: data.firstName,
+ lastName: data.lastName,
+ org: data.orgName,
+ jobTitle: data.jobTitle,
+ phone: data.phone,
+ estUsers: data.estUsers,
+ licenseKey: licenseKey,
+ ip: request.clientAddr,
+ industry: data.industry,
+ referer: getSession().initialReferer
+ });
+}
+
+function getWeb2LeadData(data, ip, ref) {
+ var googleQuery = extractGoogleQuery(ref);
+ var w2ldata = {
+ oid: "00D80000000b7ey",
+ first_name: data.firstName,
+ last_name: data.lastName,
+ email: data.email,
+ company: data.orgName,
+ title: data.jobTitle,
+ phone: data.phone,
+ '00N80000003FYtG': data.estUsers,
+ '00N80000003FYto': ref,
+ '00N80000003FYuI': googleQuery,
+ lead_source: 'EEPNET Download',
+ industry: data.industry
+ };
+
+ if (!isProduction()) {
+// w2ldata.debug = "1";
+// w2ldata.debugEmail = "aaron@appjet.com";
+ }
+
+ return w2ldata;
+}
+
+function _sendSalesNotification(data, ip, ref) {
+ var hostname = ipToHostname(ip) || "unknown";
+
+ var subject = "EEPNET Trial Download: "+[data.orgName, data.firstName + ' ' + data.lastName, data.email].join(" / ");
+
+ var body = [
+ "",
+ "This is an automated message.",
+ "",
+ "Somebody downloaded a "+getTrialDays()+"-day trial of EEPNET.",
+ "",
+ "This lead should be automatically added to the AppJet salesforce account.",
+ "",
+ "Organization: "+data.orgName,
+ "Industry: "+data.industry,
+ "Full Name: "+data.firstName + ' ' + data.lastName,
+ "Job Title: "+data.jobTitle,
+ "Email: "+data.email,
+ 'Phone: '+data.phone,
+ "Est. Users: "+data.estUsers,
+ "IP Address: "+ip+" ("+hostname+")",
+ "Session Referer: "+ref,
+ ""
+ ].join("\n");
+
+ var toAddr = 'sales@pad.spline.inf.fu-berlin.de';
+ if (isTestEmail(data.email)) {
+ toAddr = 'blackhole@appjet.com';
+ }
+ sendEmail(
+ toAddr,
+ 'sales@pad.spline.inf.fu-berlin.de',
+ subject,
+ {'Reply-To': data.email},
+ body
+ );
+}
+
+function getSalesforceIndustryList() {
+ return [
+ '--None--',
+ 'Agriculture',
+ 'Apparel',
+ 'Banking',
+ 'Biotechnology',
+ 'Chemicals',
+ 'Communications',
+ 'Construction',
+ 'Consulting',
+ 'Education',
+ 'Electronics',
+ 'Energy',
+ 'Engineering',
+ 'Entertainment',
+ 'Environmental',
+ 'Finance',
+ 'Food & Beverage',
+ 'Government',
+ 'Healthcare',
+ 'Hospitality',
+ 'Insurance',
+ 'Machinery',
+ 'Manufacturing',
+ 'Media',
+ 'Not For Profit',
+ 'Other',
+ 'Recreation',
+ 'Retail',
+ 'Shipping',
+ 'Technology',
+ 'Telecommunications',
+ 'Transportation',
+ 'Utilities'
+ ];
+}
+
diff --git a/etherpad/src/etherpad/testing/testutils.js b/etherpad/src/etherpad/testing/testutils.js
new file mode 100644
index 0000000..eac7840
--- /dev/null
+++ b/etherpad/src/etherpad/testing/testutils.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function assertTruthy(x) {
+ if (!x) {
+ throw new Error("assertTruthy failure: "+x);
+ }
+}
+
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0000_test.js b/etherpad/src/etherpad/testing/unit_tests/t0000_test.js
new file mode 100644
index 0000000..9e0e78b
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0000_test.js
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+function run() {
+ return "This is a test test.";
+}
+
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js b/etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js
new file mode 100644
index 0000000..96a74e4
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.sqlcommon.{withConnection,inTransaction,closing}");
+import("sqlbase.sqlobj");
+
+import("etherpad.testing.testutils.*");
+
+function run() {
+
+ withConnection(function(conn) {
+ var s = conn.createStatement();
+ closing(s, function() {
+ s.execute("delete from just_a_test");
+ });
+ });
+
+ sqlobj.insert("just_a_test", {id: 1, x: "a"});
+
+ try { // this should fail
+ inTransaction(function(conn) {
+ sqlobj.updateSingle("just_a_test", {id: 1}, {id: 1, x: "b"});
+ // note: this will be pritned to the console, but that's OK
+ throw Error();
+ });
+ } catch (e) {}
+
+ var testRecord = sqlobj.selectSingle("just_a_test", {id: 1});
+
+ assertTruthy(testRecord.x == "a");
+}
+
+
+
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js b/etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js
new file mode 100644
index 0000000..67c79d8
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("stringutils");
+import("sqlbase.sqlobj");
+
+import("etherpad.licensing");
+
+jimport("java.util.Random");
+
+function run() {
+ var r = new Random(0);
+
+ function testLicense(name, org, expires, editionId, userQuota) {
+ function keydataString() {
+ return "{name: "+name+", org: "+org+", expires: "+expires+", editionId: "+editionId+", userQuota: "+userQuota+"}";
+ }
+ var key = licensing.generateNewKey(name, org, expires, editionId, userQuota);
+ var info = licensing.decodeLicenseInfoFromKey(key);
+ if (!info) {
+ println("Generated key does not decode at all: "+keydataString());
+ println(" generated key: "+key);
+ throw new Error("Generated key does not decode at all. See stdout.");
+ }
+ function testMatch(name, x, y) {
+ if (x != y) {
+ println("key match error ("+name+"): ["+x+"] != ["+y+"]");
+ println(" key data: "+keydataString());
+ println(" generated key: "+key);
+ println(" decoded key: "+info.toSource());
+ throw new Error(name+" mismatch. see stdout.");
+ }
+ }
+ testMatch("personName", info.personName, name);
+ testMatch("orgName", info.organizationName, org);
+ testMatch("expires", +info.expiresDate, +expires);
+ testMatch("editionName", info.editionName, licensing.getEditionName(editionId));
+ testMatch("userQuota", +info.userQuota, +userQuota);
+ }
+
+ testLicense("aaron", "test", +(new Date)+1000*60*60*24*30, licensing.getEditionId('PRIVATE_NETWORK_EVALUATION'), 1001);
+
+ for (var editionId = 0; editionId < 3; editionId++) {
+ for (var unlimitedUsers = 0; unlimitedUsers <= 1; unlimitedUsers++) {
+ for (var noExpiry = 0; noExpiry <= 1; noExpiry++) {
+ for (var j = 0; j < 100; j++) {
+ var name = stringutils.randomString(1+r.nextInt(39));
+ var org = stringutils.randomString(1+r.nextInt(39));
+ var expires = null;
+ if (noExpiry == 0) {
+ expires = +(new Date)+(1000*60*60*24*r.nextInt(100));
+ }
+ var userQuota = -1;
+ if (unlimitedUsers == 1) {
+ userQuota = r.nextInt(1e6);
+ }
+
+ testLicense(name, org, expires, editionId, userQuota);
+ }
+ }
+ }
+ }
+
+ // test that all previously generated keys continue to decode.
+ var historicalKeys = sqlobj.selectMulti('eepnet_signups', {}, {});
+ historicalKeys.forEach(function(d) {
+ var key = d.licenseKey;
+ if (key && !licensing.isValidKey(key)) {
+ throw new Error("Historical license key no longer validates: "+key);
+ }
+ });
+
+}
+
+
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js b/etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js
new file mode 100644
index 0000000..0898fbe
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("sqlbase.persistent_vars");
+
+import("stringutils");
+
+import("etherpad.testing.testutils.*");
+
+function run() {
+ var varname = stringutils.randomString(50);
+ var varval = stringutils.randomString(50);
+
+ var x = persistent_vars.get(varname);
+ assertTruthy(!x);
+
+ persistent_vars.put(varname, varval);
+
+ for (var i = 0; i < 3; i++) {
+ x = persistent_vars.get(varname);
+ assertTruthy(x == varval);
+ }
+
+ persistent_vars.remove(varname);
+
+ var x = persistent_vars.get(varname);
+ assertTruthy(!x);
+}
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js b/etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js
new file mode 100644
index 0000000..7f8c996
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js
@@ -0,0 +1,214 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("jsutils.*");
+import("stringutils");
+
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+import("etherpad.globals.*");
+import("etherpad.testing.testutils.*");
+
+function run() {
+ cleanUpTables();
+ testGeneral();
+ testAlterColumn();
+ cleanUpTables();
+}
+
+function _getTestTableName() {
+ return 'sqlobj_unit_test_'+stringutils.randomString(10);
+}
+
+function testGeneral() {
+
+ if (isProduction()) {
+ return; // we dont run this in productin!
+ }
+
+ // create a test table
+ var tableName = _getTestTableName();
+
+ sqlobj.createTable(tableName, {
+ id: sqlobj.getIdColspec(),
+ varChar: 'VARCHAR(128)',
+ dateTime: sqlobj.getDateColspec("NOT NULL"),
+ int11: 'INT',
+ tinyInt: sqlobj.getBoolColspec("DEFAULT 0")
+ });
+
+ // add some columns
+ sqlobj.addColumns(tableName, {
+ a: 'VARCHAR(256)',
+ b: 'VARCHAR(256)',
+ c: 'VARCHAR(256)',
+ d: 'VARCHAR(256)'
+ });
+
+ // drop columns
+ sqlobj.dropColumn(tableName, 'c');
+ sqlobj.dropColumn(tableName, 'd');
+
+ // list tables and make sure it contains tableName
+ var l = sqlobj.listTables();
+ var found = false;
+ l.forEach(function(x) {
+ if (x == tableName) { found = true; }
+ });
+ assertTruthy(found);
+
+ if (sqlcommon.isMysql()) {
+ for (var i = 0; i < 3; i++) {
+ ['MyISAM', 'InnoDB'].forEach(function(e) {
+ sqlobj.setTableEngine(tableName, e);
+ assertTruthy(e == sqlobj.getTableEngine(tableName));
+ });
+ }
+ }
+
+ sqlobj.createIndex(tableName, ['a', 'b']);
+ sqlobj.createIndex(tableName, ['int11', 'a', 'b']);
+
+ // test null columns
+ for (var i = 0; i < 10; i++) {
+ var id = sqlobj.insert(tableName, {dateTime: new Date(), a: null, b: null});
+ sqlobj.deleteRows(tableName, {id: id});
+ }
+
+ //----------------------------------------------------------------
+ // data management
+ //----------------------------------------------------------------
+
+ // insert + selectSingle
+ function _randomDate() {
+ // millisecond accuracy is lost in DB.
+ var d = +(new Date);
+ d = Math.round(d / 1000) * 1000;
+ return new Date(d);
+ }
+ var obj_data_list = [];
+ for (var i = 0; i < 40; i++) {
+ var obj_data = {
+ varChar: stringutils.randomString(20),
+ dateTime: _randomDate(),
+ int11: +(new Date) % 10000,
+ tinyInt: !!(+(new Date) % 2),
+ a: "foo",
+ b: "bar"
+ };
+ obj_data_list.push(obj_data);
+
+ var obj_id = sqlobj.insert(tableName, obj_data);
+ var obj_result = sqlobj.selectSingle(tableName, {id: obj_id});
+
+ assertTruthy(obj_result.id == obj_id);
+ keys(obj_data).forEach(function(k) {
+ var d1 = obj_data[k];
+ var d2 = obj_result[k];
+ if (k == "dateTime") {
+ d1 = +d1;
+ d2 = +d2;
+ }
+ if (d1 != d2) {
+ throw Error("result mismatch ["+k+"]: "+d1+" != "+d2);
+ }
+ });
+ }
+
+ // selectMulti: no constraints, no options
+ var obj_result_list = sqlobj.selectMulti(tableName, {}, {});
+ assertTruthy(obj_result_list.length == obj_data_list.length);
+ // orderBy
+ ['int11', 'a', 'b'].forEach(function(colName) {
+ obj_result_list = sqlobj.selectMulti(tableName, {}, {orderBy: colName});
+ assertTruthy(obj_result_list.length == obj_data_list.length);
+ for (var i = 1; i < obj_result_list.length; i++) {
+ assertTruthy(obj_result_list[i-1][colName] <= obj_result_list[i][colName]);
+ }
+
+ obj_result_list = sqlobj.selectMulti(tableName, {}, {orderBy: "-"+colName});
+ assertTruthy(obj_result_list.length == obj_data_list.length);
+ for (var i = 1; i < obj_result_list.length; i++) {
+ assertTruthy(obj_result_list[i-1][colName] >= obj_result_list[i][colName]);
+ }
+ });
+
+ // selectMulti: with constraints
+ var obj_result_list1 = sqlobj.selectMulti(tableName, {tinyInt: true}, {});
+ var obj_result_list2 = sqlobj.selectMulti(tableName, {tinyInt: false}, {});
+ assertTruthy((obj_result_list1.length + obj_result_list2.length) == obj_data_list.length);
+ obj_result_list1.forEach(function(o) {
+ assertTruthy(o.tinyInt == true);
+ });
+ obj_result_list2.forEach(function(o) {
+ assertTruthy(o.tinyInt == false);
+ });
+
+ // updateSingle
+ obj_result_list1.forEach(function(o) {
+ o.a = "ttt";
+ sqlobj.updateSingle(tableName, {id: o.id}, o);
+ });
+ // update
+ sqlobj.update(tableName, {tinyInt: false}, {a: "fff"});
+ // verify
+ obj_result_list = sqlobj.selectMulti(tableName, {}, {});
+ obj_result_list.forEach(function(o) {
+ if (o.tinyInt) {
+ assertTruthy(o.a == "ttt");
+ } else {
+ assertTruthy(o.a == "fff");
+ }
+ });
+
+ // deleteRows
+ sqlobj.deleteRows(tableName, {a: "ttt"});
+ sqlobj.deleteRows(tableName, {a: "fff"});
+ // verify
+ obj_result_list = sqlobj.selectMulti(tableName, {}, {});
+ assertTruthy(obj_result_list.length == 0);
+}
+
+function cleanUpTables() {
+ // delete testing table (and any other old testing tables)
+ sqlobj.listTables().forEach(function(t) {
+ if (t.indexOf("sqlobj_unit_test") == 0) {
+ sqlobj.dropTable(t);
+ }
+ });
+}
+
+function testAlterColumn() {
+ var tableName = _getTestTableName();
+
+ sqlobj.createTable(tableName, {
+ x: 'INT',
+ a: 'INT NOT NULL',
+ b: 'INT NOT NULL'
+ });
+
+ if (sqlcommon.isMysql()) {
+ sqlobj.modifyColumn(tableName, 'a', 'INT');
+ sqlobj.modifyColumn(tableName, 'b', 'INT');
+ } else {
+ sqlobj.alterColumn(tableName, 'a', 'NULL');
+ sqlobj.alterColumn(tableName, 'b', 'NULL');
+ }
+
+ sqlobj.insert(tableName, {a: 5});
+}
+
diff --git a/etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js b/etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js
new file mode 100644
index 0000000..9cd3f21
--- /dev/null
+++ b/etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import("etherpad.collab.ace.easysync2_tests");
+
+function run() {
+ easysync2_tests.runTests();
+} \ No newline at end of file
diff --git a/etherpad/src/etherpad/usage_stats/usage_stats.js b/etherpad/src/etherpad/usage_stats/usage_stats.js
new file mode 100644
index 0000000..59074ed
--- /dev/null
+++ b/etherpad/src/etherpad/usage_stats/usage_stats.js
@@ -0,0 +1,162 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("execution");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+import("jsutils.*");
+import("fastJSON");
+
+import("etherpad.log");
+import("etherpad.log.frontendLogFileName");
+import("etherpad.statistics.statistics");
+import("fileutils.eachFileLine");
+
+jimport("java.lang.System.out.println");
+jimport("java.io.BufferedReader");
+jimport("java.io.FileReader");
+jimport("java.io.File");
+jimport("java.awt.Color");
+
+jimport("org.jfree.chart.ChartFactory");
+jimport("org.jfree.chart.ChartUtilities");
+jimport("org.jfree.chart.JFreeChart");
+jimport("org.jfree.chart.axis.DateAxis");
+jimport("org.jfree.chart.axis.NumberAxis");
+jimport("org.jfree.chart.plot.XYPlot");
+jimport("org.jfree.chart.renderer.xy.XYLineAndShapeRenderer");
+jimport("org.jfree.data.time.Day");
+jimport("org.jfree.data.time.TimeSeries");
+jimport("org.jfree.data.time.TimeSeriesCollection");
+
+//----------------------------------------------------------------
+// Database reading/writing
+//----------------------------------------------------------------
+
+
+function _listStats(statName) {
+ return sqlobj.selectMulti('statistics', {name: statName}, {orderBy: '-timestamp'});
+}
+
+// public accessor
+function getStatData(statName) {
+ return _listStats(statName);
+}
+
+//----------------------------------------------------------------
+// HTML & Graph generating
+//----------------------------------------------------------------
+
+function respondWithGraph(statName) {
+ var width = 500;
+ var height = 300;
+ if (request.params.size) {
+ var parts = request.params.size.split('x');
+ width = +parts[0];
+ height = +parts[1];
+ }
+
+ var dataset = new TimeSeriesCollection();
+ var hideLegend = true;
+
+ switch (statistics.getStatData(statName).plotType) {
+ case 'line':
+ var ts = new TimeSeries(statName);
+
+ _listStats(statName).forEach(function(stat) {
+ var day = new Day(new java.util.Date(stat.timestamp * 1000));
+ ts.addOrUpdate(day, fastJSON.parse(stat.value).value);
+ });
+ dataset.addSeries(ts);
+ break;
+ case 'topValues':
+ hideLegend = false;
+ var stats = _listStats(statName);
+ if (stats.length == 0) break;
+ var latestStat = fastJSON.parse(stats[0].value);
+ var valuesToWatch = [];
+ var series = {};
+ var nLines = 5;
+ function forEachFirstN(n, stat, f) {
+ for (var i = 0; i < Math.min(n, stat.topValues.length); i++) {
+ f(stat.topValues[i].value, stat.topValues[i].count);
+ }
+ }
+ forEachFirstN(nLines, latestStat, function(value, count) {
+ valuesToWatch.push(value);
+ series[value] = new TimeSeries(value);
+ });
+ stats.forEach(function(stat) {
+ var day = new Day(new java.util.Date(stat.timestamp*1000));
+ var statData = fastJSON.parse(stat.value);
+ valuesToWatch.forEach(function(value) { series[value].addOrUpdate(day, 0); })
+ forEachFirstN(nLines, statData, function(value, count) {
+ if (series[value]) {
+ series[value].addOrUpdate(day, count);
+ }
+ });
+ });
+ valuesToWatch.forEach(function(value) {
+ dataset.addSeries(series[value]);
+ });
+ break;
+ case 'histogram':
+ hideLegend = false;
+ var stats = _listStats(statName);
+ percentagesToGraph = ["50", "90", "100"];
+ series = {};
+ percentagesToGraph.forEach(function(pct) {
+ series[pct] = new TimeSeries(pct+"%");
+ dataset.addSeries(series[pct]);
+ });
+ if (stats.length == 0) break;
+ stats.forEach(function(stat) {
+ var day = new Day(new java.util.Date(stat.timestamp*1000));
+ var statData = fastJSON.parse(stat.value);
+ eachProperty(series, function(pct, timeseries) {
+ timeseries.addOrUpdate(day, statData[pct] || 0);
+ });
+ });
+ break;
+ }
+
+ var domainAxis = new DateAxis("");
+ var rangeAxis = new NumberAxis();
+ var renderer = new XYLineAndShapeRenderer();
+
+ var numSeries = dataset.getSeriesCount();
+ var colors = [Color.blue, Color.red, Color.green, Color.orange, Color.pink, Color.magenta];
+ for (var i = 0; i < numSeries; ++i) {
+ renderer.setSeriesPaint(i, colors[i]);
+ renderer.setSeriesShapesVisible(i, false);
+ }
+
+ var plot = new XYPlot(dataset, domainAxis, rangeAxis, renderer);
+
+ var chart = new JFreeChart(plot);
+ chart.setTitle(statName);
+ if (hideLegend) {
+ chart.removeLegend();
+ }
+
+ var jos = new java.io.ByteArrayOutputStream();
+ ChartUtilities.writeChartAsJPEG(
+ jos, 1.0, chart, width, height);
+
+ response.setContentType('image/jpeg');
+ response.writeBytes(jos.toByteArray());
+}
+
diff --git a/etherpad/src/etherpad/utils.js b/etherpad/src/etherpad/utils.js
new file mode 100644
index 0000000..65ebe1f
--- /dev/null
+++ b/etherpad/src/etherpad/utils.js
@@ -0,0 +1,464 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("exceptionutils");
+import("fileutils.{readFile,fileLastModified}");
+import("ejs.EJS");
+import("funhtml.*");
+import("stringutils");
+import("stringutils.startsWith");
+import("jsutils.*");
+
+import("etherpad.sessions");
+import("etherpad.sessions.getSession");
+import("etherpad.globals.*");
+import("etherpad.helpers");
+import("etherpad.collab.collab_server");
+import("etherpad.pad.model");
+import("etherpad.pro.domains");
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_config");
+import("etherpad.pro.pro_accounts");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("etherpad.log");
+import("etherpad.admin.plugins");
+
+jimport("java.lang.System.out.print");
+jimport("java.lang.System.out.println");
+
+jimport("java.io.File");
+
+//----------------------------------------------------------------
+// utilities
+//----------------------------------------------------------------
+
+// returns globally-unique padId
+function randomUniquePadId() {
+ var id = stringutils.randomString(10);
+ while (model.accessPadGlobal(id, function(p) { return p.exists(); }, "r")) {
+ id = stringutils.randomString(10);
+ }
+ return id;
+}
+
+//----------------------------------------------------------------
+// template rendering
+//----------------------------------------------------------------
+
+function findExistsingFile(files) {
+ for (var i = 0; i < files.length; i++) {
+ var f = new File('./src' + files[i]);
+ if (f.exists())
+ return files[i];
+ }
+}
+
+function findTemplate(filename, plugin) {
+ var files = [];
+
+ var pluginList = [plugin];
+ try {
+ if (plugin.forEach !== undefined)
+ pluginList = plugin;
+ else
+ pluginList = [plugin];
+ } catch (e) {}
+
+ pluginList.forEach(function (plugin) {
+ if (plugin != undefined) {
+ files.push('/plugins/' + plugin + '/templates/' + filename);
+ files.push('/themes/' + appjet.config.theme + '/plugins/' + plugin + '/templates/' + filename);
+ files.push('/themes/default/plugins/' + plugin + '/templates/' + filename);
+ }
+ });
+ files.push('/themes/' + appjet.config.theme + '/templates/' + filename);
+ files.push('/themes/default/templates/' + filename);
+
+ return findExistsingFile(files);
+}
+
+function Template(params, plugin) {
+ this._defines = {}
+ this._params = params;
+ this._params.template = this;
+ this._plugin = plugin;
+}
+
+Template.prototype.define = function(name, fn) {
+ this._defines[name] = fn;
+ return '';
+}
+
+Template.prototype.use = function (name, fn, arg) {
+ if (this._defines[name] != undefined)
+ return this._defines[name](arg);
+ else if (fn != undefined)
+ return fn(arg);
+ else
+ return '';
+}
+
+Template.prototype.inherit = function (template) {
+ return renderTemplateAsString(template, this._params, this._plugin);
+}
+
+function renderTemplateAsString(filename, data, plugin) {
+ data = data || {};
+ data.helpers = helpers; // global helpers
+ data.plugins = plugins; // Access callHook and the like...
+ if (data.template == undefined)
+ new Template(data, plugin);
+
+ var f = findTemplate(filename, plugin); //"/templates/"+filename;
+ if (! appjet.scopeCache.ejs) {
+ appjet.scopeCache.ejs = {};
+ }
+ var cacheObj = appjet.scopeCache.ejs[filename];
+ if (cacheObj === undefined || fileLastModified(f) > cacheObj.mtime) {
+ var templateText = readFile(f);
+ templateText += "<%: template.use('body', function () { return ''; }); %> ";
+ cacheObj = {};
+ cacheObj.tmpl = new EJS({text: templateText, name: filename});
+ cacheObj.mtime = fileLastModified(f);
+ appjet.scopeCache.ejs[filename] = cacheObj;
+ }
+ var html = cacheObj.tmpl.render(data);
+ return html;
+}
+
+function renderTemplate(filename, data, plugin) {
+ response.write(renderTemplateAsString(filename, data, plugin));
+ if (request.acceptsGzip) {
+ response.setGzip(true);
+ }
+}
+
+function renderHtml(bodyFileName, data, plugin) {
+ var bodyHtml = renderTemplateAsString(bodyFileName, data, plugin);
+ bodyHtml = plugins.callHookStr("renderPageBodyPre", {bodyFileName:bodyFileName, data:data, plugin:plugin}) +
+ bodyHtml +
+ plugins.callHookStr("renderPageBodyPost", {bodyFileName:bodyFileName, data:data, plugin:plugin});
+ response.write(renderTemplateAsString("html.ejs", {bodyHtml: bodyHtml}));
+ if (request.acceptsGzip) {
+ response.setGzip(true);
+ }
+}
+
+function renderFramedHtml(contentHtml, plugin) {
+ var getContentHtml;
+ if (typeof(contentHtml) == 'function') {
+ getContentHtml = contentHtml;
+ } else {
+ getContentHtml = function() { return contentHtml; }
+ }
+
+ var template = "framed/framedpage.ejs";
+ if (isProDomainRequest()) {
+ template = "framed/framedpage-pro.ejs";
+ }
+
+ renderHtml(template, {
+ renderHeader: renderMainHeader,
+ renderFooter: renderMainFooter,
+ getContentHtml: getContentHtml,
+ isProDomainRequest: isProDomainRequest(),
+ renderGlobalProNotice: pro_utils.renderGlobalProNotice
+ }, plugin);
+}
+
+function renderFramed(bodyFileName, data, plugin) {
+ function _getContentHtml() {
+ return renderTemplateAsString(bodyFileName, data, plugin);
+ }
+ renderFramedHtml(_getContentHtml);
+}
+
+function renderFramedError(error, plugin) {
+ var content = DIV({className: 'fpcontent'},
+ DIV({style: "padding: 2em 1em;"},
+ DIV({style: "padding: 1em; border: 1px solid #faa; background: #fdd;"},
+ B("Error: "), error)));
+ renderFramedHtml(content, plugin);
+}
+
+function renderNotice(bodyFileName, data, plugin) {
+ renderNoticeString(renderTemplateAsString(bodyFileName, data, plugin), plugin);
+}
+
+function renderNoticeString(contentHtml, plugin) {
+ renderFramed("notice.ejs", {content: contentHtml}, plugin);
+}
+
+function render404(noStop, plugin) {
+ response.reset();
+ response.setStatusCode(404);
+ renderFramedHtml(DIV({className: "fpcontent"},
+ DIV({style: "padding: 2em 1em;"},
+ DIV({style: "border: 1px solid #aaf; background: #def; padding: 1em; font-size: 150%;"},
+ "404 not found: "+request.path))), plugin);
+ if (! noStop) {
+ response.stop();
+ }
+}
+
+function render500(ex, plugin) {
+ response.reset();
+ response.setStatusCode(500);
+ var trace = null;
+ if (ex && (!isProduction())) {
+ trace = exceptionutils.getStackTracePlain(ex);
+ }
+ renderFramed("500_body.ejs", {trace: trace}, plugin);
+}
+
+function _renderEtherpadDotComHeader(data) {
+ if (!data) {
+ data = {selected: ''};
+ }
+ data.html = stringutils.html;
+ data.UL = UL;
+ data.LI = LI;
+ data.A = A;
+ data.isPNE = isPrivateNetworkEdition();
+ return renderTemplateAsString("framed/framedheader.ejs", data);
+}
+
+function _renderProHeader(data) {
+ if (!pro_accounts.isAccountSignedIn()) {
+ return '<div style="height: 140px;">&nbsp;</div>';
+ }
+
+ var r = domains.getRequestDomainRecord();
+ if (!data) { data = {}; }
+ data.navSelection = (data.navSelection || appjet.requestCache.proTopNavSelection || '');
+ data.proDomainOrgName = pro_config.getConfig().siteName;
+ data.isPNE = isPrivateNetworkEdition();
+ data.account = getSessionProAccount();
+ data.validLicense = pne_utils.isServerLicensed();
+ data.pneTrackerHtml = pne_utils.pneTrackerHtml();
+ data.isAnEtherpadAdmin = sessions.isAnEtherpadAdmin();
+ data.fullSuperdomain = pro_utils.getFullSuperdomainHost();
+ return renderTemplateAsString("framed/framedheader-pro.ejs", data);
+}
+
+function renderMainHeader(data) {
+ if (isProDomainRequest()) {
+ return _renderProHeader(data);
+ } else {
+ return _renderEtherpadDotComHeader(data);
+ }
+}
+
+function renderMainFooter() {
+ return renderTemplateAsString("framed/framedfooter.ejs", {
+ isProDomainRequest: isProDomainRequest()
+ });
+}
+
+//----------------------------------------------------------------
+// isValidEmail
+//----------------------------------------------------------------
+
+// TODO: make better and use the better version on the client in
+// various places as well (pad.js and etherpad.js)
+function isValidEmail(x) {
+ return (x &&
+ ((x.length > 0) &&
+ (x.match(/^[\w\.\_\+\-]+\@[\w\_\-]+\.[\w\_\-\.]+$/))));
+}
+
+//----------------------------------------------------------------
+
+function timeAgo(d, now) {
+ if (!now) { now = new Date(); }
+
+ function format(n, word) {
+ n = Math.round(n);
+ return ('' + n + ' ' + word + (n != 1 ? 's' : '') + ' ago');
+ }
+
+ d = (+now - (+d)) / 1000;
+ if (d < 60) { return format(d, 'second'); }
+ d /= 60;
+ if (d < 60) { return format(d, 'minute'); }
+ d /= 60;
+ if (d < 24) { return format(d, 'hour'); }
+ d /= 24;
+ return format(d, 'day');
+};
+
+
+//----------------------------------------------------------------
+// linking to a set of new CGI parameters
+//----------------------------------------------------------------
+function qpath(m) {
+ var q = {};
+ if (request.query) {
+ request.query.split('&').forEach(function(kv) {
+ if (kv) {
+ var parts = kv.split('=');
+ q[parts[0]] = parts[1];
+ }
+ });
+ }
+ eachProperty(m, function(k,v) {
+ q[k] = v;
+ });
+ var r = request.path + '?';
+ eachProperty(q, function(k,v) {
+ if (v !== undefined && v !== null) {
+ r += ('&' + k + '=' + v);
+ }
+ });
+ return r;
+}
+
+//----------------------------------------------------------------
+
+function ipToHostname(ip) {
+ var DNS = Packages.org.xbill.DNS;
+
+ if (!DNS.Address.isDottedQuad(ip)) {
+ return null
+ }
+
+ try {
+ var addr = DNS.Address.getByAddress(ip);
+ return DNS.Address.getHostName(addr);
+ } catch (ex) {
+ return null;
+ }
+}
+
+function extractGoogleQuery(ref) {
+ ref = String(ref);
+ ref = ref.toLowerCase();
+ if (!(ref.indexOf("google") >= 0)) {
+ return "";
+ }
+
+ ref = ref.split('?')[1];
+
+ var q = "";
+ ref.split("&").forEach(function(x) {
+ var parts = x.split("=");
+ if (parts[0] == "q") {
+ q = parts[1];
+ }
+ });
+
+ q = decodeURIComponent(q);
+ q = q.replace(/\+/g, " ");
+
+ return q;
+}
+
+function isTestEmail(x) {
+ return (x.indexOf("+appjetseleniumtest+") >= 0);
+}
+
+function isPrivateNetworkEdition() {
+ return pne_utils.isPNE();
+}
+
+function isProDomainRequest() {
+ return pro_utils.isProDomainRequest();
+}
+
+function hasOffice() {
+ return appjet.config["etherpad.soffice"] || appjet.config["etherpad.sofficeConversionServer"];
+}
+
+////////// console progress bar
+
+function startConsoleProgressBar(barWidth, updateIntervalSeconds) {
+ barWidth = barWidth || 40;
+ updateIntervalSeconds = ((typeof updateIntervalSeconds) == "number" ? updateIntervalSeconds : 1.0);
+
+ var unseenStatus = null;
+ var lastPrintTime = 0;
+ var column = 0;
+
+ function replaceLineWith(str) {
+ //print((new Array(column+1)).join('\b')+str);
+ print('\r'+str);
+ column = str.length;
+ }
+
+ var bar = {
+ update: function(frac, msg, force) {
+ var t = +new Date();
+ if ((!force) && ((t - lastPrintTime)/1000 < updateIntervalSeconds)) {
+ unseenStatus = {frac:frac, msg:msg};
+ }
+ else {
+ var pieces = [];
+ pieces.push(' ', (' '+Math.round(frac*100)).slice(-3), '%', ' [');
+ var barEndLoc = Math.max(0, Math.min(barWidth-1, Math.floor(frac*barWidth)));
+ for(var i=0;i<barWidth;i++) {
+ if (i < barEndLoc) pieces.push('=');
+ else if (i == barEndLoc) pieces.push('>');
+ else pieces.push(' ');
+ }
+ pieces.push('] ', msg || '');
+ replaceLineWith(pieces.join(''));
+
+ unseenStatus = null;
+ lastPrintTime = t;
+ }
+ },
+ finish: function() {
+ if (unseenStatus) {
+ bar.update(unseenStatus.frac, unseenStatus.msg, true);
+ }
+ println();
+ }
+ };
+
+ println();
+ bar.update(0, null, true);
+
+ return bar;
+}
+
+function isStaticRequest() {
+ return (startsWith(request.path, '/static/') ||
+ startsWith(request.path, '/favicon.ico') ||
+ startsWith(request.path, '/robots.txt'));
+}
+
+function httpsHost(h) {
+ h = h.split(":")[0]; // strip any existing port
+ if (appjet.config.listenSecurePort != "443" && !appjet.config.hidePorts) {
+ h = (h + ":" + appjet.config.listenSecurePort);
+ }
+ return h;
+}
+
+function httpHost(h) {
+ h = h.split(":")[0]; // strip any existing port
+ if (appjet.config.listenPort != "80" && !appjet.config.hidePorts) {
+ h = (h + ":" + appjet.config.listenPort);
+ }
+ return h;
+}
+
+function toJavaException(e) {
+ var exc = ((e instanceof java.lang.Throwable) && e) || e.rhinoException || e.javaException ||
+ new java.lang.Throwable(e.message+"/"+e.fileName+"/"+e.lineNumber);
+ return exc;
+}
diff --git a/etherpad/src/main.js b/etherpad/src/main.js
new file mode 100644
index 0000000..53671cf
--- /dev/null
+++ b/etherpad/src/main.js
@@ -0,0 +1,434 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import("dispatch.{Dispatcher,PrefixMatcher,DirMatcher,forward}");
+import("exceptionutils");
+import("fastJSON");
+import("jsutils.*");
+import("sqlbase.sqlcommon");
+import("stringutils");
+import("sessions.{readLatestSessionsFromDisk,writeSessionsToDisk}");
+
+import("etherpad.billing.team_billing");
+import("etherpad.globals.*");
+import("etherpad.log.{logRequest,logException}");
+import("etherpad.log");
+import("etherpad.utils.*");
+import("etherpad.statistics.statistics");
+import("etherpad.sessions");
+import("etherpad.db_migrations.migration_runner");
+import("etherpad.importexport.importexport");
+import("etherpad.legacy_urls");
+
+import("etherpad.control.aboutcontrol");
+import("etherpad.control.admincontrol");
+import("etherpad.control.blogcontrol");
+import("etherpad.control.connection_diagnostics_control");
+import("etherpad.control.global_pro_account_control");
+import("etherpad.control.historycontrol");
+import("etherpad.control.loadtestcontrol");
+import("etherpad.control.maincontrol");
+import("etherpad.control.pad.pad_control");
+import("etherpad.control.pne_manual_control");
+import("etherpad.control.pne_tracker_control");
+import("etherpad.control.pro.admin.license_manager_control");
+import("etherpad.control.pro_beta_control");
+import("etherpad.control.pro.pro_main_control");
+import("etherpad.control.pro_signup_control");
+import("etherpad.control.scriptcontrol");
+import("etherpad.control.static_control");
+import("etherpad.control.store.storecontrol");
+import("etherpad.control.testcontrol");
+
+import("etherpad.pne.pne_utils");
+import("etherpad.pro.pro_pad_editors");
+import("etherpad.pro.pro_utils");
+import("etherpad.pro.pro_config");
+
+import("etherpad.collab.collabroom_server");
+import("etherpad.collab.collab_server");
+import("etherpad.collab.readonly_server");
+import("etherpad.collab.genimg");
+import("etherpad.pad.model");
+import("etherpad.pad.dbwriter");
+import("etherpad.pad.pad_migrations");
+import("etherpad.pad.noprowatcher");
+
+import("etherpad.admin.plugins");
+
+jimport("java.lang.System.out.println");
+
+serverhandlers.startupHandler = function() {
+ // Order matters.
+ checkSystemRequirements();
+
+ var sp = function(k) { return appjet.config['etherpad.SQL_'+k] || null; };
+ sqlcommon.init(sp('JDBC_DRIVER'), sp('JDBC_URL'), sp('USERNAME'), sp('PASSWORD'));
+
+ log.onStartup();
+ statistics.onStartup();
+ migration_runner.onStartup();
+ pad_migrations.onStartup();
+ model.onStartup();
+ collab_server.onStartup();
+ pad_control.onStartup();
+ dbwriter.onStartup();
+ blogcontrol.onStartup();
+ importexport.onStartup();
+ pro_pad_editors.onStartup();
+ noprowatcher.onStartup();
+ team_billing.onStartup();
+ collabroom_server.onStartup();
+ readLatestSessionsFromDisk();
+
+ plugins.callHook('serverStartup');
+};
+
+serverhandlers.resetHandler = function() {
+ statistics.onReset();
+}
+
+serverhandlers.shutdownHandler = function() {
+ plugins.callHook('serverShutdown');
+
+ appjet.cache.shutdownHandlerIsRunning = true;
+
+ log.callCatchingExceptions(writeSessionsToDisk);
+ log.callCatchingExceptions(dbwriter.onShutdown);
+ log.callCatchingExceptions(sqlcommon.onShutdown);
+ log.callCatchingExceptions(pro_pad_editors.onShutdown);
+};
+
+//----------------------------------------------------------------
+// request handling
+//----------------------------------------------------------------
+
+serverhandlers.requestHandler = function() {
+ checkRequestIsWellFormed();
+ sessions.preRequestCookieCheck();
+ checkHost();
+ checkHTTPS();
+ handlePath();
+};
+
+// In theory, this should never get called.
+// Exceptions that are thrown in frontend etherpad javascript should
+// always be caught and treated specially.
+// If serverhandlers.errorHandler gets called, then it's a bug in the frontend.
+serverhandlers.errorHandler = function(ex) {
+ logException(ex);
+ response.setStatusCode(500);
+ if (request.isDefined) {
+ render500(ex);
+ } else {
+ if (! isProduction()) {
+ response.write(exceptionutils.getStackTracePlain(ex));
+ } else {
+ response.write(ex.getMessage());
+ }
+ }
+};
+
+serverhandlers.postRequestHandler = function() {
+ logRequest();
+};
+
+//----------------------------------------------------------------
+// Scheduled tasks
+//----------------------------------------------------------------
+
+serverhandlers.tasks.writePad = function(globalPadId) {
+ dbwriter.taskWritePad(globalPadId);
+};
+serverhandlers.tasks.flushPad = function(globalPadId, reason) {
+ dbwriter.taskFlushPad(globalPadId, reason);
+};
+serverhandlers.tasks.checkForStalePads = function() {
+ dbwriter.taskCheckForStalePads();
+};
+serverhandlers.tasks.statisticsDailyUpdate = function() {
+ //statistics.dailyUpdate();
+};
+serverhandlers.tasks.doSlowFileConversion = function(from, to, bytes, cont) {
+ return importexport.doSlowFileConversion(from, to, bytes, cont);
+};
+serverhandlers.tasks.proPadmetaFlushEdits = function(domainId) {
+ pro_pad_editors.flushEditsNow(domainId);
+};
+serverhandlers.tasks.noProWatcherCheckPad = function(globalPadId) {
+ noprowatcher.checkPad(globalPadId);
+};
+serverhandlers.tasks.collabRoomDisconnectSocket = function(connectionId, socketId) {
+ collabroom_server.disconnectDefunctSocket(connectionId, socketId);
+};
+
+//----------------------------------------------------------------
+// cometHandler()
+//----------------------------------------------------------------
+
+serverhandlers.cometHandler = function(op, id, data) {
+ checkRequestIsWellFormed();
+ if (!data) {
+ // connect/disconnect message, notify all comet receivers
+ collabroom_server.handleComet(op, id, data);
+ return;
+ }
+
+ while (data[data.length-1] == '\u0000') {
+ data = data.substr(0, data.length-1);
+ }
+
+ var wrapper;
+ try {
+ wrapper = fastJSON.parse(data);
+ } catch (err) {
+ try {
+ // after removing \u0000 might have to add '}'
+ wrapper = fastJSON.parse(data+'}');
+ }
+ catch (err) {
+ log.custom("invalid-json", {data: data});
+ throw err;
+ }
+ }
+ if(wrapper.type == "COLLABROOM") {
+ collabroom_server.handleComet(op, id, wrapper.data);
+ } else {
+ //println("incorrectly wrapped data: " + wrapper['type']);
+ }
+};
+
+//----------------------------------------------------------------
+// sarsHandler()
+//----------------------------------------------------------------
+
+serverhandlers.sarsHandler = function(str) {
+ str = String(str);
+ println("sarsHandler: parsing JSON string (length="+str.length+")");
+ var message = fastJSON.parse(str);
+ println("dispatching SARS message of type "+message.type);
+ if (message.type == "migrateDiagnosticRecords") {
+ pad_control.recordMigratedDiagnosticInfo(message.records);
+ return 'OK';
+ }
+ return 'UNKNOWN_MESSAGE_TYPE';
+};
+
+//----------------------------------------------------------------
+// checkSystemRequirements()
+//----------------------------------------------------------------
+function checkSystemRequirements() {
+ var jv = Packages.java.lang.System.getProperty("java.version");
+ jv = +(String(jv).split(".").slice(0,2).join("."));
+ if (jv < 1.6) {
+ println("Error: EtherPad requires JVM 1.6 or greater.");
+ println("Your version of the JVM is: "+jv);
+ println("Aborting...");
+ Packages.java.lang.System.exit(1);
+ }
+}
+
+function checkRequestIsWellFormed() {
+ // We require the "host" field to be present.
+ // This should always be true, as long as the protocl is HTTP/1.1
+ // TODO: check (request.protocol != "HTTP/1.1")
+ if (request.isDefined && !request.host) {
+ response.setStatusCode(505);
+ response.setContentType('text/plain');
+ response.write('Protocol not supported. HTTP/1.1 required.');
+ response.stop();
+ }
+}
+
+//----------------------------------------------------------------
+// checkHost()
+//----------------------------------------------------------------
+function checkHost() {
+ var trueRegex = /\s*true\s*/i;
+ if (trueRegex.test(appjet.config['etherpad.skipHostnameCheck'])) {
+ return;
+ }
+
+ if (isPrivateNetworkEdition()) {
+ return;
+ }
+
+ // we require the domain to either be <superdomain> or a pro domain request.
+ if (domainEnabled(request.domain)) {
+ return;
+ }
+ if (pro_utils.isProDomainRequest()) {
+ return;
+ }
+
+ // redirect to main site
+ var newurl = "http://pad.spline.inf.fu-berlin.de/"+request.path;
+ if (request.query) { newurl += "?"+request.query; }
+ response.redirect(newurl);
+}
+
+//----------------------------------------------------------------
+// checkHTTPS()
+//----------------------------------------------------------------
+
+// Check for HTTPS
+function checkHTTPS() {
+ /* Open-source note: this function used to check the protocol and make
+ * sure that pages that needed to be secure went over HTTPS, and pages
+ * that didn't go over HTTP. However, when we open-sourced the code,
+ * we disabled HTTPS because we didn't want to ship the pad.spline.inf.fu-berlin.de
+ * private crypto keys. --aiba */
+ return;
+
+
+ if (stringutils.startsWith(request.path, "/static/")) { return; }
+
+ if (sessions.getSession().disableHttps || request.params.disableHttps) {
+ sessions.getSession().disableHttps = true;
+ println("setting session diableHttps");
+ return;
+ }
+
+ var _ports = {
+ http: appjet.config.listenPort,
+ https: appjet.config.listenSecurePort
+ };
+ var _defaultPorts = {
+ http: 80,
+ https: 443
+ };
+
+ // Require HTTPS for the following paths:
+ var _requiredHttpsPrefixes = [
+ '/ep/admin', // pro and main site
+ '/ep/account', // pro only
+ ];
+
+ var httpsRequired = false;
+ _requiredHttpsPrefixes.forEach(function(p) {
+ if (stringutils.startsWith(request.path, p)) {
+ httpsRequired = true;
+ }
+ });
+
+ if (isProDomainRequest() && pro_config.getConfig().alwaysHttps) {
+ httpsRequired = true;
+ }
+
+ if (httpsRequired && !request.isSSL) {
+ _redirectToScheme("https");
+ }
+ if (!httpsRequired && request.isSSL) {
+ _redirectToScheme("http");
+ }
+
+ function _redirectToScheme(scheme) {
+ var url = scheme + "://";
+ url += request.host.split(':')[0]; // server
+
+ if (_ports[scheme] != _defaultPorts[scheme]) {
+ url += ':'+_ports[scheme];
+ }
+
+ url += request.path;
+ if (request.query) {
+ url += "?"+request.query;
+ }
+ response.redirect(url);
+ }
+}
+
+//----------------------------------------------------------------
+// dispatching
+//----------------------------------------------------------------
+
+function handlePath() {
+ // Default. Can be overridden in case of static files.
+ response.neverCache();
+
+ plugins.registerClientHandlerJS();
+
+ // these paths are handled identically on all sites/subdomains.
+ var commonDispatcher = new Dispatcher();
+
+ commonDispatcher.addLocations(plugins.callHook('handlePath'));
+
+ commonDispatcher.addLocations([
+ ['/favicon.ico', forward(static_control)],
+ ['/robots.txt', forward(static_control)],
+ ['/crossdomain.xml', forward(static_control)],
+ [PrefixMatcher('/static/'), forward(static_control)],
+ [PrefixMatcher('/ep/genimg/'), genimg.renderPath],
+ [PrefixMatcher('/ep/pad/'), forward(pad_control)],
+ [PrefixMatcher('/ep/script/'), forward(scriptcontrol)],
+ [/^\/([^\/]+)$/, pad_control.render_pad],
+ [DirMatcher('/ep/unit-tests/'), forward(testcontrol)],
+ [DirMatcher('/ep/pne-manual/'), forward(pne_manual_control)],
+ ]);
+
+ // these paths are main site only
+ var mainsiteDispatcher = new Dispatcher();
+ mainsiteDispatcher.addLocations([
+ ['/', maincontrol.render_main],
+ [DirMatcher('/ep/beta-account/'), forward(pro_beta_control)],
+ [DirMatcher('/ep/pro-signup/'), forward(pro_signup_control)],
+ [DirMatcher('/ep/about/'), forward(aboutcontrol)],
+ [DirMatcher('/ep/admin/'), forward(admincontrol)],
+ [DirMatcher('/ep/blog/posts/'), blogcontrol.render_post],
+ [DirMatcher('/ep/blog/'), forward(blogcontrol)],
+ [DirMatcher('/ep/connection-diagnostics/'), forward(connection_diagnostics_control)],
+ [DirMatcher('/ep/loadtest/'), forward(loadtestcontrol)],
+ [DirMatcher('/ep/tpne/'), forward(pne_tracker_control)],
+ [DirMatcher('/ep/pro-account/'), forward(global_pro_account_control)],
+ [/^\/ep\/pad\/history\/(\w+)\/(.*)$/, historycontrol.render_history],
+ [PrefixMatcher('/ep/pad/slider/'), pad_control.render_slider],
+ [DirMatcher('/ep/store/'), forward(storecontrol)],
+ [PrefixMatcher('/ep/'), forward(maincontrol)]
+ ]);
+
+ // these paths are pro only
+ var proDispatcher = new Dispatcher();
+ proDispatcher.addLocations([
+ ['/', pro_main_control.render_main],
+ [PrefixMatcher('/ep/'), forward(pro_main_control)],
+ ]);
+
+ // dispatching logic: first try common, then dispatch to
+ // main site or pro.
+
+ if (commonDispatcher.dispatch()) {
+ return;
+ }
+
+ // Check if there is a pro domain associated with this request.
+ if (isProDomainRequest()) {
+ pro_utils.preDispatchAccountCheck();
+ if (proDispatcher.dispatch()) {
+ return;
+ }
+ } else {
+ if (mainsiteDispatcher.dispatch()) {
+ return;
+ }
+ }
+
+ if (!isProDomainRequest()) {
+ legacy_urls.checkPath();
+ }
+
+ render404();
+}
+
diff --git a/etherpad/src/plugins/fileUpload/controllers/fileUpload.js b/etherpad/src/plugins/fileUpload/controllers/fileUpload.js
new file mode 100644
index 0000000..d6585f1
--- /dev/null
+++ b/etherpad/src/plugins/fileUpload/controllers/fileUpload.js
@@ -0,0 +1,87 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ *
+ * 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("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.collab.server_utils");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padusers");
+import("etherpad.pro.pro_utils");
+import("etherpad.helpers");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("plugins.fileUpload.models");
+jimport("org.apache.commons.fileupload");
+
+function onRequest() {
+ var isPro = pro_utils.isProDomainRequest();
+ var userId = padusers.getUserId();
+
+
+ helpers.addClientVars({
+ userAgent: request.headers["User-Agent"],
+ debugEnabled: request.params.djs,
+ clientIp: request.clientAddr,
+ colorPalette: COLOR_PALETTE,
+ serverTimestamp: +(new Date),
+ isProPad: isPro,
+ userIsGuest: padusers.isGuest(userId),
+ userId: userId,
+ });
+
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ if (request.isPost) {
+ var uploads = [];
+ var itemFactory = new fileupload.disk.DiskFileItemFactory();
+ var handler = new fileupload.servlet.ServletFileUpload(itemFactory);
+ var items = handler.parseRequest(request.underlying).toArray();
+ for (var i = 0; i < items.length; i++){
+ if (!items[i].isFormField())
+ uploads.push('/up/' + models.storeFile(items[i]));
+ }
+
+ response.setContentType("text/json; charset=utf-8");
+ response.write(
+ renderTemplateAsString(
+ "fileUploaded.ejs",
+ {
+ uploads: uploads,
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: getSessionProAccount(), // may be falsy
+ },
+ 'fileUpload'));
+ if (request.acceptsGzip) {
+ response.setGzip(true);
+ }
+ } else {
+ renderHtml(
+ "fileUpload.ejs",
+ {
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: getSessionProAccount(), // may be falsy
+ },
+ 'fileUpload');
+ }
+ return true;
+}
diff --git a/etherpad/src/plugins/fileUpload/hooks.js b/etherpad/src/plugins/fileUpload/hooks.js
new file mode 100644
index 0000000..0948c17
--- /dev/null
+++ b/etherpad/src/plugins/fileUpload/hooks.js
@@ -0,0 +1,11 @@
+import("etherpad.log");
+import("faststatic");
+import("etherpad.utils.*");
+import("etherpad.globals.*");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+import("plugins.fileUpload.controllers.fileUpload");
+
+function handlePath() {
+ return [[PrefixMatcher('/ep/fileUpload/'), forward(fileUpload)],
+ [PrefixMatcher('/up/'), faststatic.directoryServer('/plugins/fileUpload/upload/', {cache: isProduction()})]];
+}
diff --git a/etherpad/src/plugins/fileUpload/main.js b/etherpad/src/plugins/fileUpload/main.js
new file mode 100644
index 0000000..71bd819
--- /dev/null
+++ b/etherpad/src/plugins/fileUpload/main.js
@@ -0,0 +1,19 @@
+import("etherpad.log");
+import("plugins.fileUpload.hooks");
+
+function init() {
+ this.hooks = ['handlePath'];
+ this.description = 'File upload manager adds a button to pads to upload a file. A URL to the uploaded file is then inserted into the pad.';
+ this.handlePath = hooks.handlePath;
+ this.install = install;
+ this.uninstall = uninstall;
+}
+
+function install() {
+ log.info("Installing fileUpload");
+}
+
+function uninstall() {
+ log.info("Uninstalling fileUpload");
+}
+
diff --git a/etherpad/src/plugins/fileUpload/models.js b/etherpad/src/plugins/fileUpload/models.js
new file mode 100644
index 0000000..f8fb563
--- /dev/null
+++ b/etherpad/src/plugins/fileUpload/models.js
@@ -0,0 +1,95 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ *
+ * 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("etherpad.utils.*");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+jimport("java.io.File",
+ "java.io.DataInputStream",
+ "java.io.FileInputStream",
+ "java.lang.Byte",
+ "java.io.FileReader",
+ "java.io.BufferedReader",
+ "java.security.MessageDigest",
+ "java.lang.Runtime");
+
+
+/* Normal base64 encoding, except we don't care about adding newlines */
+function base64Encode(stringArray) {
+ base64code = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "+/";
+
+ /* Pad array to nearest three byte multiple */
+ var padding = (3 - (stringArray.length % 3)) % 3;
+ var padded = java.lang.reflect.Array.newInstance(Byte.TYPE, stringArray.length + padding);
+ java.lang.System.arraycopy(stringArray, 0, padded, 0, stringArray.length);
+ stringArray = padded;
+
+ var encoded = "";
+ for (var i = 0; i < stringArray.length; i += 3) {
+ var j = (((stringArray[i] & 0xff) << 16) +
+ ((stringArray[i + 1] & 0xff) << 8) +
+ (stringArray[i + 2] & 0xff));
+ encoded = (encoded +
+ base64code.charAt((j >> 18) & 0x3f) +
+ base64code.charAt((j >> 12) & 0x3f) +
+ base64code.charAt((j >> 6) & 0x3f) +
+ base64code.charAt(j & 0x3f));
+ }
+ /* replace padding with "=" */
+ return encoded.substring(0, encoded.length - padding) + "==".substring(0, padding);
+}
+
+
+function makeSymlink(destination, source) {
+ return Runtime.getRuntime().exec(['ln', '-s', source.getPath(), destination.getPath()]).waitFor();
+}
+
+
+/* Reads a File and updates a digest with its content */
+function updateDigestFromFile(digest, file) {
+ var handle = new java.io.FileInputStream(file);
+
+ var bytes = java.lang.reflect.Array.newInstance(Byte.TYPE, 512);
+ var nbytes = 0;
+
+ while ((nbytes = handle.read(bytes, 0, 512)) != -1)
+ digest.update(bytes, 0, nbytes);
+
+ handle.close();
+}
+
+
+/* Stores a org.apache.commons.fileupload.disk.DiskFileItem permanently and returns a filename. */
+function storeFile(fileItem) {
+ var nameParts = fileItem.name.split('.');
+ var extension = nameParts[nameParts.length-1];
+
+ var digest = MessageDigest.getInstance("SHA1");
+ updateDigestFromFile(digest, fileItem.getStoreLocation());
+ var checksum = base64Encode(digest.digest());
+
+ fileItem.write(File("src/plugins/fileUpload/upload/" + checksum));
+
+ makeSymlink(
+ File("src/plugins/fileUpload/upload/" + checksum + '.' + extension),
+ File(checksum));
+
+ return checksum + '.' + extension;
+}
diff --git a/etherpad/src/plugins/fileUpload/templates/fileUpload.ejs b/etherpad/src/plugins/fileUpload/templates/fileUpload.ejs
new file mode 100644
index 0000000..57f33e6
--- /dev/null
+++ b/etherpad/src/plugins/fileUpload/templates/fileUpload.ejs
@@ -0,0 +1,32 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<%
+ helpers.setHtmlTitle("Test plugin");
+ helpers.setBodyId("padbody");
+ helpers.addBodyClass("limwidth nonpropad nonprouser");
+ helpers.includeCss("pad2_ejs.css");
+ helpers.setRobotsPolicy({index: false, follow: false})
+ helpers.includeJQuery();
+ helpers.includeCometJs();
+ helpers.includeJs("json2.js");
+ helpers.addToHead('\n<style type="text/css" title="dynamicsyntax"></style>\n');
+%>
+
+<div id="padpage">
+ <form method="POST" enctype="multipart/form-data">
+ <input type="file" name="fileParam">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
diff --git a/etherpad/src/plugins/fileUpload/templates/fileUploaded.ejs b/etherpad/src/plugins/fileUpload/templates/fileUploaded.ejs
new file mode 100644
index 0000000..5a62f7e
--- /dev/null
+++ b/etherpad/src/plugins/fileUpload/templates/fileUploaded.ejs
@@ -0,0 +1,5 @@
+[
+<% for (var i = 0; i < uploads.length; i++) { %>
+ '<%= uploads[i] %>',
+<% } %>
+]
diff --git a/etherpad/src/plugins/kafoo/main.js b/etherpad/src/plugins/kafoo/main.js
new file mode 100644
index 0000000..f645576
--- /dev/null
+++ b/etherpad/src/plugins/kafoo/main.js
@@ -0,0 +1,16 @@
+import("etherpad.log");
+
+function init() {
+ this.hooks = [];
+ this.description = 'KaBar plugin';
+ this.install = install;
+ this.uninstall = uninstall;
+}
+
+function install() {
+ log.info("Installing testplugin");
+}
+
+function uninstall() {
+ log.info("Uninstalling testplugin");
+}
diff --git a/etherpad/src/plugins/testplugin/controllers/testplugin.js b/etherpad/src/plugins/testplugin/controllers/testplugin.js
new file mode 100644
index 0000000..da74ade
--- /dev/null
+++ b/etherpad/src/plugins/testplugin/controllers/testplugin.js
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ *
+ * 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("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.collab.server_utils");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padusers");
+import("etherpad.pro.pro_utils");
+import("etherpad.helpers");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+
+function onRequest() {
+ var isPro = pro_utils.isProDomainRequest();
+ var userId = padusers.getUserId();
+
+ helpers.addClientVars({
+ userAgent: request.headers["User-Agent"],
+ debugEnabled: request.params.djs,
+ clientIp: request.clientAddr,
+ colorPalette: COLOR_PALETTE,
+ serverTimestamp: +(new Date),
+ isProPad: isPro,
+ userIsGuest: padusers.isGuest(userId),
+ userId: userId,
+ });
+
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ renderHtml(
+ "testplugin.ejs",
+ {
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: getSessionProAccount(), // may be falsy
+ },
+ 'testplugin');
+ return true;
+}
diff --git a/etherpad/src/plugins/testplugin/hooks.js b/etherpad/src/plugins/testplugin/hooks.js
new file mode 100644
index 0000000..493a2c2
--- /dev/null
+++ b/etherpad/src/plugins/testplugin/hooks.js
@@ -0,0 +1,15 @@
+import("etherpad.log");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+import("plugins.testplugin.controllers.testplugin");
+
+function serverStartup() {
+ log.info("Server startup for testplugin");
+}
+
+function serverShutdown() {
+ log.info("Server shutdown for testplugin");
+}
+
+function handlePath() {
+ return [[PrefixMatcher('/ep/testplugin/'), forward(testplugin)]];
+}
diff --git a/etherpad/src/plugins/testplugin/main.js b/etherpad/src/plugins/testplugin/main.js
new file mode 100644
index 0000000..49b447c
--- /dev/null
+++ b/etherpad/src/plugins/testplugin/main.js
@@ -0,0 +1,23 @@
+import("etherpad.log");
+import("plugins.testplugin.hooks");
+import("plugins.testplugin.static.js.main");
+
+function init() {
+ this.hooks = ['serverStartup', 'serverShutdown', 'handlePath'];
+ this.client = new main.init();
+ this.description = 'Test Plugin';
+ this.serverStartup = hooks.serverStartup;
+ this.serverShutdown = hooks.serverShutdown;
+ this.handlePath = hooks.handlePath;
+ this.install = install;
+ this.uninstall = uninstall;
+}
+
+function install() {
+ log.info("Installing testplugin");
+}
+
+function uninstall() {
+ log.info("Uninstalling testplugin");
+}
+
diff --git a/etherpad/src/plugins/testplugin/static/js/main.js b/etherpad/src/plugins/testplugin/static/js/main.js
new file mode 100644
index 0000000..f08b8f7
--- /dev/null
+++ b/etherpad/src/plugins/testplugin/static/js/main.js
@@ -0,0 +1,11 @@
+function init() {
+ this.hooks = ['kafoo'];
+ this.kafoo = kafoo;
+}
+
+function kafoo() {
+ alert('hej');
+}
+
+/* used on the client side only */
+testplugin = new init();
diff --git a/etherpad/src/plugins/testplugin/static/js/test.js b/etherpad/src/plugins/testplugin/static/js/test.js
new file mode 100644
index 0000000..83fb40c
--- /dev/null
+++ b/etherpad/src/plugins/testplugin/static/js/test.js
@@ -0,0 +1 @@
+plugins.callHook("kafoo");
diff --git a/etherpad/src/plugins/testplugin/templates/page.ejs b/etherpad/src/plugins/testplugin/templates/page.ejs
new file mode 100644
index 0000000..71633c0
--- /dev/null
+++ b/etherpad/src/plugins/testplugin/templates/page.ejs
@@ -0,0 +1,23 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+
+<% template.define('body', function() { var ejs_data=''; %>
+ <div id="blabla">
+ <h1>Page header</h1>
+ <%: template.use('content', function() { var ejs_data=''; %>
+ Original content
+ <% return ejs_data; }); %>
+ <div>footer</div>
+ </div>
+<% return ejs_data; }); %>
diff --git a/etherpad/src/plugins/testplugin/templates/testplugin.ejs b/etherpad/src/plugins/testplugin/templates/testplugin.ejs
new file mode 100644
index 0000000..69c4453
--- /dev/null
+++ b/etherpad/src/plugins/testplugin/templates/testplugin.ejs
@@ -0,0 +1,33 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<%
+ helpers.setHtmlTitle("Test plugin");
+ helpers.setBodyId("padbody");
+ helpers.addBodyClass("limwidth nonpropad nonprouser");
+ helpers.includeCss("pad2_ejs.css");
+ helpers.setRobotsPolicy({index: false, follow: false})
+ helpers.includeJQuery();
+ helpers.includeCometJs();
+ helpers.includeJs("json2.js");
+ helpers.includeJs("plugins/testplugin/test.js");
+ helpers.addToHead('\n<style type="text/css" title="dynamicsyntax"></style>\n');
+%>
+
+<% template.inherit('page.ejs') %>
+
+<% template.define('content', function() { var ejs_data=''; %>
+ <div id="padpage">
+ Welcome to the test plugin
+ </div>
+<% return ejs_data; }); %>
diff --git a/etherpad/src/plugins/twitterStyleTags/controllers/tagBrowser.js b/etherpad/src/plugins/twitterStyleTags/controllers/tagBrowser.js
new file mode 100644
index 0000000..5335ab7
--- /dev/null
+++ b/etherpad/src/plugins/twitterStyleTags/controllers/tagBrowser.js
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ * Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+ *
+ * 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("plugins.twitterStyleTags.models.tagQuery");
+
+import("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.collab.server_utils");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padusers");
+import("etherpad.pro.pro_utils");
+import("etherpad.helpers");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("etherpad.pad.padutils");
+
+
+function onRequest() {
+ var tags = tagQuery.queryToTags(request.params.query);
+
+ /* Create the pad filter sql */
+ var querySql = tagQuery.getQueryToSql(tags.tags.concat(['public']), tags.antiTags);
+
+ /* Use the pad filter sql to figure out which tags to show in the tag browser this time. */
+ var queryNewTagsSql = tagQuery.newTagsSql(querySql);
+ var newTags = sqlobj.executeRaw(queryNewTagsSql.sql, queryNewTagsSql.params);
+
+ padSql = tagQuery.padInfoSql(querySql, 10);
+ var matchingPads = sqlobj.executeRaw(padSql.sql, padSql.params);
+
+ for (i = 0; i < matchingPads.length; i++) {
+ matchingPads[i].TAGS = matchingPads[i].TAGS.split('#');
+ }
+
+ var isPro = pro_utils.isProDomainRequest();
+ var userId = padusers.getUserId();
+
+ helpers.addClientVars({
+ userAgent: request.headers["User-Agent"],
+ debugEnabled: request.params.djs,
+ clientIp: request.clientAddr,
+ colorPalette: COLOR_PALETTE,
+ serverTimestamp: +(new Date),
+ isProPad: isPro,
+ userIsGuest: padusers.isGuest(userId),
+ userId: userId,
+ });
+
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ padutils.setOptsAndCookiePrefs(request);
+ var prefs = helpers.getClientVar('cookiePrefsToSet');
+ var bodyClass = (prefs.isFullWidth ? "fullwidth" : "limwidth")
+
+ var info = {
+ prefs: prefs,
+ config: appjet.config,
+ tagQuery: tagQuery,
+ padIdToReadonly: server_utils.padIdToReadonly,
+ tags: tags.tags,
+ antiTags: tags.antiTags,
+ newTags: newTags,
+ matchingPads: matchingPads,
+ bodyClass: 'nonpropad',
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: getSessionProAccount(), // may be falsy
+ };
+
+ var format = "html";
+ if (request.params.format != undefined)
+ format = request.params.format;
+
+ if (format == "html")
+ renderHtml("tagBrowser.ejs", info, 'twitterStyleTags');
+ else if (format == "rss") {
+ response.setContentType("application/xml; charset=utf-8");
+ response.write(renderTemplateAsString("tagRss.ejs", info, 'twitterStyleTags'));
+ if (request.acceptsGzip) {
+ response.setGzip(true);
+ }
+ }
+ return true;
+}
diff --git a/etherpad/src/plugins/twitterStyleTags/hooks.js b/etherpad/src/plugins/twitterStyleTags/hooks.js
new file mode 100644
index 0000000..1072579
--- /dev/null
+++ b/etherpad/src/plugins/twitterStyleTags/hooks.js
@@ -0,0 +1,56 @@
+import("etherpad.log");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+import("plugins.twitterStyleTags.controllers.tagBrowser");
+import("sqlbase.sqlobj");
+
+function handlePath() {
+ return [[PrefixMatcher('/ep/tag/'), forward(tagBrowser)]];
+}
+
+function padModelWriteToDB(args) {
+ /* Update tags for the pad */
+
+ var new_tags = args.pad.text().match(new RegExp("#[^,#=!\\s][^,#=!\\s]*", "g"));
+ if (new_tags == null) new_tags = new Array();
+ for (i = 0; i < new_tags.length; i++)
+ new_tags[i] = new_tags[i].substring(1);
+ var new_tags_str = new_tags.join('#')
+
+ var old_tags_row = sqlobj.selectSingle("PAD_TAG_CACHE", { PAD_ID: args.padId });
+ var old_tags_str;
+ if (old_tags_row !== null)
+ old_tags_str = old_tags_row['TAGS'];
+ else
+ old_tags_str = '';
+
+ // var old_tags = old_tags_str != '' ? old_tags_str.split('#') : new Array();
+
+ if (new_tags_str != old_tags_str) {
+ // log.info({message: 'Updating tags', new_tags:new_tags, old_tags:old_tags});
+
+ if (old_tags_row)
+ sqlobj.update("PAD_TAG_CACHE", {PAD_ID: args.padId }, {TAGS: new_tags.join('#')});
+ else
+ sqlobj.insert("PAD_TAG_CACHE", {PAD_ID: args.padId, TAGS: new_tags.join('#')});
+
+ sqlobj.deleteRows("PAD_TAG", {PAD_ID: args.padId});
+
+ for (i = 0; i < new_tags.length; i++) {
+ var tag_row = sqlobj.selectSingle("TAG", { NAME: new_tags[i] });
+ if (tag_row === null) {
+ sqlobj.insert("TAG", {NAME: new_tags[i]});
+ tag_row = sqlobj.selectSingle("TAG", { NAME: new_tags[i] });
+ }
+ sqlobj.insert("PAD_TAG", {PAD_ID: args.padId, TAG_ID: tag_row['ID']});
+ }
+ }
+}
+
+function docbarItemsAll() {
+ return ["<td class='docbarbutton'><a href='/ep/tag/'>Home</a></td>"];
+}
+
+function docbarItemsTagBrowser() {
+ return ["<td class='docbarbutton'><a href='/ep/tag/'>Pads</a></td>"];
+}
+
diff --git a/etherpad/src/plugins/twitterStyleTags/main.js b/etherpad/src/plugins/twitterStyleTags/main.js
new file mode 100644
index 0000000..d64abff
--- /dev/null
+++ b/etherpad/src/plugins/twitterStyleTags/main.js
@@ -0,0 +1,45 @@
+import("etherpad.log");
+import("plugins.twitterStyleTags.hooks");
+import("plugins.twitterStyleTags.static.js.main");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function init() {
+ this.hooks = ['handlePath', 'aceGetFilterStack', 'aceCreateDomLine', 'padModelWriteToDB', 'docbarItemsAll', 'docbarItemsTagBrowser'];
+ this.client = new main.init();
+ this.description = 'Twitter-style tags allows the user to tag pads by writing #tagname anywhere in the pad text. Tags are automatically linked to searches for that tag in other pads. This plugin also provides an alternative home-page for Etherpad with a display of the last changed public pads as well as that information available as an RSS stream.';
+ this.handlePath = hooks.handlePath;
+ this.aceGetFilterStack = main.aceGetFilterStack;
+ this.aceCreateDomLine = main.aceCreateDomLine;
+ this.padModelWriteToDB = hooks.padModelWriteToDB;
+ this.docbarItemsAll = hooks.docbarItemsAll;
+ this.docbarItemsTagBrowser = hooks.docbarItemsTagBrowser;
+
+ this.install = install;
+ this.uninstall = uninstall;
+}
+
+function install() {
+ log.info("Installing Twitter-style tags");
+
+ sqlobj.createTable('TAG', {
+ ID: 'int not null '+sqlcommon.autoIncrementClause()+' primary key',
+ NAME: 'varchar(128) character set utf8 collate utf8_bin not null',
+ });
+
+ sqlobj.createTable('PAD_TAG', {
+ PAD_ID: 'varchar(128) character set utf8 collate utf8_bin not null references PAD_META(ID)',
+ TAG_ID: 'int default NULL references TAG(ID)',
+ });
+
+ sqlobj.createTable('PAD_TAG_CACHE', {
+ PAD_ID: 'varchar(128) character set utf8 collate utf8_bin unique not null references PAD_META(ID)',
+ TAGS: 'varchar(1024) collate utf8_bin not null',
+ });
+
+}
+
+function uninstall() {
+ log.info("Uninstalling Twitter-style tags");
+}
+
diff --git a/etherpad/src/plugins/twitterStyleTags/models/tagQuery.js b/etherpad/src/plugins/twitterStyleTags/models/tagQuery.js
new file mode 100644
index 0000000..8a32ef7
--- /dev/null
+++ b/etherpad/src/plugins/twitterStyleTags/models/tagQuery.js
@@ -0,0 +1,227 @@
+/**
+ * Copyright 2010 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ * Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+ *
+ * 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("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("etherpad.log");
+
+function tagsToQuery(tags, antiTags) {
+ var prefixed = [];
+ for (i = 0; i < antiTags.length; i++)
+ prefixed[i] = '!' + antiTags[i];
+ return tags.concat(prefixed).join(',');
+}
+
+function queryToTags(query) {
+ var tags = {
+ tags: new Array(),
+ antiTags: new Array()
+ };
+
+ if (query != undefined && query != '') {
+ var query = query.split(',');
+ for (i = 0; i < query.length; i++)
+ if (query[i][0] == '!')
+ tags.antiTags.push(query[i].substring(1));
+ else
+ tags.tags.push(query[i]);
+ }
+ return tags;
+}
+
+function stringFormat(text, obj) {
+ var name;
+ for (name in obj) {
+ //iterate through the params and replace their placeholders from the original text
+ text = text.replace(new RegExp('%\\(' + name + '\\)s', 'gi' ), obj[name]);
+ }
+ return text;
+}
+
+/* All these sql query functions both takes a querySql object as
+ * parameter and returns one. This object has two members - sql and
+ * params. Sql is a string of an sql table name or a subqyery in
+ * parens. The table pr subquery should have an ID column containing a
+ * PAD_ID.
+ */
+
+/* Filters pads by tags and anti-tags */
+function getQueryToSql(tags, antiTags, querySql) {
+ var queryTable;
+ var queryParams;
+
+ if (querySql == null) {
+ queryTable = 'PAD_META';
+ queryParams = [];
+ } else {
+ queryTable = querySql.sql;
+ queryParams = querySql.params;
+ }
+
+ var exceptArray = [];
+ var joinArray = [];
+ var whereArray = [];
+ var exceptParamArray = [];
+ var joinParamArray = [];
+
+ var info = new Object();
+ info.queryTable = queryTable;
+ info.n = 0;
+ var i;
+
+ for (i = 0; i < antiTags.length; i++) {
+ tag = antiTags[i];
+ exceptArray.push(
+ stringFormat(
+ 'left join (PAD_TAG as pt%(n)s ' +
+ ' join TAG AS t%(n)s on ' +
+ ' t%(n)s.NAME = ? ' +
+ ' and t%(n)s.ID = pt%(n)s.TAG_ID) on ' +
+ ' pt%(n)s.PAD_ID = p.ID ',
+ info));
+ whereArray.push(stringFormat('pt%(n)s.TAG_ID is null', info));
+ exceptParamArray.push(tag);
+ info.n += 1;
+ }
+ for (i = 0; i < tags.length; i++) {
+ tag = tags[i];
+ joinArray.push(
+ stringFormat(
+ 'join PAD_TAG as pt%(n)s on ' +
+ ' pt%(n)s.PAD_ID = p.ID ' +
+ 'join TAG as t%(n)s on ' +
+ ' t%(n)s.ID = pt%(n)s.TAG_ID ' +
+ ' and t%(n)s.NAME = ? ',
+ info));
+ joinParamArray.push(tag);
+ info.n += 1;
+ }
+
+ info["joins"] = joinArray.join(' ');
+ info["excepts"] = exceptArray.join(' ');
+ info["wheres"] = whereArray.length > 0 ? ' where ' + whereArray.join(' and ') : '';
+
+ /* Create a subselect from all the joins */
+ return {
+ sql: stringFormat(
+ '(select distinct ' +
+ ' p.ID ' +
+ ' from ' +
+ ' %(queryTable)s as p ' +
+ ' %(joins)s ' +
+ ' %(excepts)s ' +
+ ' %(wheres)s ' +
+ ') ',
+ info),
+ params: queryParams.concat(joinParamArray).concat(exceptParamArray)};
+}
+
+/* Returns the sql to count the number of results from some other
+ * query. */
+function nrSql(querySql) {
+ var queryTable;
+ var queryParams;
+
+ if (querySql == null) {
+ queryTable = 'PAD_META';
+ queryParams = [];
+ } else {
+ queryTable = querySql.sql;
+ queryParams = querySql.params;
+ }
+
+ var info = [];
+ info['query_sql'] = queryTable
+ return {
+ sql: stringFormat('(select count(*) as total from %(query_sql)s as q)', info),
+ params: queryParams};
+}
+
+/* Returns the sql to select the 10 best new tags to tack on to a
+ * query, that is, the tags that are closest to halving the result-set
+ * if tacked on. */
+function newTagsSql(querySql) {
+ var queryTable;
+ var queryParams;
+
+ if (querySql == null) {
+ queryTable = 'PAD_META';
+ queryParams = [];
+ } else {
+ queryTable = querySql.sql;
+ queryParams = querySql.params;
+ }
+
+ var info = [];
+ info["query_post_table"] = queryTable;
+ var queryNrSql = nrSql(querySql);
+ info["query_nr_sql"] = queryNrSql.sql;
+ queryNrParams = queryNrSql.params;
+
+ return {
+ sql: stringFormat('' +
+ 'select ' +
+ ' t.NAME tagname, ' +
+ ' count(tp.PAD_ID) as matches, ' +
+ ' tn.total - count(tp.PAD_ID) as antimatches, ' +
+ ' abs(count(tp.PAD_ID) - (tn.total / 2)) as weight ' +
+ 'from ' +
+ ' TAG as t, ' +
+ ' PAD_TAG as tp, ' +
+ ' %(query_nr_sql)s as tn ' +
+ 'where ' +
+ ' tp.TAG_ID = t.ID ' +
+ ' and tp.PAD_ID in %(query_post_table)s ' +
+ ' and tp.PAD_ID NOT LIKE \'%$%\'' +
+ 'group by t.NAME, tn.total ' +
+ 'having ' +
+ ' count(tp.PAD_ID) > 0 and count(tp.PAD_ID) < tn.total ' +
+ 'order by ' +
+ ' abs(count(tp.PAD_ID) - (tn.total / 2)) asc ' +
+ 'limit 10 ' +
+ '', info),
+ params: queryNrParams.concat(queryParams)};
+}
+
+/* Select the X last changed matching pads and some extra information
+ * on them. Except the Pro Pads*/
+function padInfoSql(querySql, limit, offset) {
+ var sql = '' +
+ 'select ' +
+ ' m.id as ID, ' +
+ ' DATE_FORMAT(m.lastWriteTime, \'%a, %d %b %Y %H:%i:%s GMT\') as lastWriteTime, ' +
+ ' c.TAGS ' +
+ 'from ' +
+ querySql.sql + ' as q ' +
+ ' join PAD_SQLMETA as m on ' +
+ ' m.id = q.ID ' +
+ ' join PAD_TAG_CACHE as c on ' +
+ ' c.PAD_ID = q.ID ' +
+ 'where ' +
+ ' m.id NOT LIKE \'%$%\'' +
+ 'order by ' +
+ ' m.lastWriteTime desc ';
+ if (limit != undefined)
+ sql += 'limit ' + limit + " ";
+ if (offset != undefined)
+ sql += 'offset ' + offset + " ";
+ return {
+ sql: sql,
+ params: querySql.params
+ };
+}
diff --git a/etherpad/src/plugins/twitterStyleTags/static/css/pad.css b/etherpad/src/plugins/twitterStyleTags/static/css/pad.css
new file mode 100644
index 0000000..e144de5
--- /dev/null
+++ b/etherpad/src/plugins/twitterStyleTags/static/css/pad.css
@@ -0,0 +1,70 @@
+.padtag a,
+.padtag a:visited,
+a.padtag,
+a.padtag:visited,
+a.anti_padtag,
+a.anti_padtag:visited {
+ text-decoration: none !important;
+ color: #2e2eaa !important;
+
+ border-style: solid;
+ border-width: 1px;
+
+ border-left-color: #8c8c8c;
+ border-right-color: #707070;
+ border-top-color: #9c9c9c;
+ border-bottom-color: #606060;
+
+ -moz-border-radius-topleft: 3pt;
+ -moz-border-radius-topright: 3pt;
+ -moz-border-radius-bottomleft: 3pt;
+ -moz-border-radius-bottomright: 3pt;
+ -webkit-border-top-left-radius: 3pt;
+ -webkit-border-top-right-radius: 3pt;
+ -webkit-border-bottom-left-radius: 3pt;
+ -webkit-border-bottom-right-radius: 3pt;
+
+ padding-left: 2pt;
+ padding-right: 2pt
+}
+
+a.anti_padtag,
+a.anti_padtag:visited {
+ color: #aa2e2e !important;
+ border-left-color: #aa8c8c;
+ border-right-color: #aa7070;
+ border-top-color: #aa9c9c;
+ border-bottom-color: #aa6060;
+}
+
+.padtag_public a,
+.padtag_public a:visited,
+a.padtag_public,
+a.padtag_public:visited {
+ color: #2e772e !important;
+ background-color: #99ff99 !important;
+
+ border-style: solid;
+ border-width: 1px;
+
+ border-left-color: #8caa8c;
+ border-right-color: #70aa70;
+ border-top-color: #9caa9c;
+ border-bottom-color: #60aa60;
+}
+
+.padtag_writable a,
+.padtag_writable a:visited,
+a.padtag_writable,
+a.padtag_writable:visited {
+ color: #2e2e77 !important;
+ background-color: #9999ff !important;
+
+ border-style: solid;
+ border-width: 1px;
+
+ border-left-color: #8c8caa;
+ border-right-color: #7070aa;
+ border-top-color: #9c9caa;
+ border-bottom-color: #6060aa;
+}
diff --git a/etherpad/src/plugins/twitterStyleTags/static/css/tagBrowser.css b/etherpad/src/plugins/twitterStyleTags/static/css/tagBrowser.css
new file mode 100644
index 0000000..55fcda2
--- /dev/null
+++ b/etherpad/src/plugins/twitterStyleTags/static/css/tagBrowser.css
@@ -0,0 +1,90 @@
+.padtag a,
+.padtag a:visited,
+a.padtag,
+a.padtag:visited,
+a.anti_padtag,
+a.anti_padtag:visited {
+ line-height: 1.7em;
+}
+
+dt {
+ padding-bottom: 2pt;
+}
+
+dd {
+ padding-left: 20pt;
+ padding-bottom: 10pt;
+}
+
+h1 {
+ font-size: 12pt;
+ margin-top: 5pt;
+}
+
+.label {
+ font-size: 14pt;
+}
+
+#editorcontainer {
+ padding: 5pt;
+}
+
+#editorcontainerbox {
+ overflow: auto;
+ height: auto;
+}
+
+
+.query-refiner {
+ float: right;
+ padding: 10pt;
+ margin-left: 5pt;
+ margin-bottom: 5pt;
+ border: 1px solid #9C9C9C;
+ width: 40%;
+}
+
+.query-refiner h1 {
+ margin-bottom: 2pt;
+ margin-top: 0;
+}
+
+#home-newpad, #home-newteam {
+ display: block;
+ background-color: #a3bde0;
+ color: #555555;
+ border-style: solid;
+ border-width: 2px;
+ border-left-color: #d6e2f1;
+ border-right-color: #86aee1;
+ border-top-color: #d6e2f1;
+ border-bottom-color: #86aee1;
+ margin: 10pt;
+ text-align: center;
+ text-decoration: none;
+ padding-top: 50pt;
+ padding-bottom: 50pt;
+ font-size: 20pt;
+ -moz-border-radius-topleft: 3pt;
+ -moz-border-radius-topright: 3pt;
+ -moz-border-radius-bottomleft: 3pt;
+ -moz-border-radius-bottomright: 3pt;
+ -webkit-border-top-left-radius: 3pt;
+ -webkit-border-top-right-radius: 3pt;
+ -webkit-border-bottom-left-radius: 3pt;
+ -webkit-border-bottom-right-radius: 3pt;
+}
+
+#editbarinner {
+ line-height: 29px;
+ font-size: 16px;
+ padding-left: 6pt;
+}
+
+#editbarinner a {
+ font-size: 12px;
+}
+
+#padchat iframe {
+ border: none;
+}
diff --git a/etherpad/src/plugins/twitterStyleTags/static/js/main.js b/etherpad/src/plugins/twitterStyleTags/static/js/main.js
new file mode 100644
index 0000000..a83e3e8
--- /dev/null
+++ b/etherpad/src/plugins/twitterStyleTags/static/js/main.js
@@ -0,0 +1,48 @@
+function init() {
+ this.hooks = ['aceInitInnerdocbodyHead', 'aceGetFilterStack', 'aceCreateDomLine'];
+ this.aceInitInnerdocbodyHead = aceInitInnerdocbodyHead;
+ this.aceGetFilterStack = aceGetFilterStack;
+ this.aceCreateDomLine = aceCreateDomLine;
+}
+
+function aceInitInnerdocbodyHead(args) {
+ args.iframeHTML.push('\'<link rel="stylesheet" type="text/css" href="/static/css/plugins/twitterStyleTags/pad.css"/>\'');
+}
+
+function aceGetFilterStack(args) {
+ return [
+ args.linestylefilter.getRegexpFilter(
+ new RegExp("#[^,#=!\\s][^,#=!\\s]*", "g"), 'padtag'),
+ args.linestylefilter.getRegexpFilter(
+ new RegExp("=[^#=\\s][^#=\\s]*", "g"), 'padtagsearch')
+ ];
+}
+
+function aceCreateDomLine(args) {
+ if (args.cls.indexOf('padtagsearch') >= 0) {
+ var href;
+ cls = args.cls.replace(/(^| )padtagsearch:(\S+)/g, function(x0, space, padtagsearch) {
+ href = '/ep/tag/?query=' + padtagsearch.substring(1);
+ return space + "padtagsearch padtagsearch_" + padtagsearch.substring(1);
+ });
+
+ return [{
+ cls: cls,
+ extraOpenTags: '<a href="' + href.replace(/\"/g, '&quot;') + '">',
+ extraCloseTags: '</a>'}];
+ } else if (args.cls.indexOf('padtag') >= 0) {
+ var href;
+ cls = args.cls.replace(/(^| )padtag:(\S+)/g, function(x0, space, padtag) {
+ href = '/ep/tag/?query=' + padtag.substring(1);
+ return space + "padtag padtag_" + padtag.substring(1);
+ });
+
+ return [{
+ cls: cls,
+ extraOpenTags: '<a href="' + href.replace(/\"/g, '&quot;') + '">',
+ extraCloseTags: '</a>'}];
+ }
+}
+
+/* used on the client side only */
+twitterStyleTags = new init();
diff --git a/etherpad/src/plugins/twitterStyleTags/templates/tagBrowser.ejs b/etherpad/src/plugins/twitterStyleTags/templates/tagBrowser.ejs
new file mode 100644
index 0000000..e101196
--- /dev/null
+++ b/etherpad/src/plugins/twitterStyleTags/templates/tagBrowser.ejs
@@ -0,0 +1,115 @@
+<% /*
+Copyright 2009 Google Inc.
+Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+
+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. */ %>
+<%
+ template.inherit('page.ejs');
+ helpers.setHtmlTitle("EtherPad: Browse tags");
+ helpers.includeCss("plugins/twitterStyleTags/tagBrowser.css");
+ helpers.includeCss("plugins/twitterStyleTags/pad.css");
+ helpers.addToHead('\n<link rel="alternate" href="' + helpers.updateToUrl({format:'rss'}) + '" type="application/rss+xml" title="Query results as RSS" />\n');
+
+ function inArray(item, arr) {
+ for (var i = 0; i < arr.length; i++)
+ if (arr[i] == item)
+ return true;
+ return false;
+ }
+%>
+<% template.define('docBarTitle', function() { var ejs_data=''; %>
+ <td id="docbarpadtitle"><span>Browse Tags</span></td>
+<% return ejs_data; }); %>
+
+<%
+ template.define('docBarItems', function() {
+ return plugins.callHookStr('docbarItemsTagBrowserPad', {}, '', '', '') +
+ plugins.callHookStr('docbarItemsTagBrowser', {}, '', '', '');
+ });
+%>
+
+<% template.define('sideBar', function() { var ejs_data=''; %>
+ <div id="padusers">
+ <% if (isProAccountEnabled()) { %>
+ <a href="/ep/pad/newpad" style="padding: 25px 0" id="home-newpad">
+ Create new pad
+ </a>
+ <a href="/ep/pro-signup/" style="padding: 25px 0" id="home-newteam">
+ Create new team
+ </a>
+ <% } else { %>
+ <a href="/ep/pad/newpad" id="home-newpad">
+ Create new pad
+ </a>
+ <% } %>
+ </div>
+
+ <div id="hdraggie"><!-- --></div>
+
+ <div id="padchat"><iframe src="<%= config['motdPage'] %>" width="100%" height="100%"></iframe></div>
+<% return ejs_data; }); %>
+
+<% template.define('editBarItemsLeft', function() { var ejs_data=''; %>
+ <td>
+ Query:
+ <% if (tags.length == 0 && antiTags.length == 0) { %>
+ Latest changed pads
+ <% } else { %>
+ <% for (i = 0; i < tags.length; i++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags.filter(function (tag) { return tag != tags[i]}), antiTags)}) %>" class="padtag" title="<%= tags[i] %> matches">#<%= tags[i] %></a>
+ <% } %>
+ <% for (i = 0; i < antiTags.length; i++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags, antiTags.filter(function (tag) { return tag != antiTags[i]}))}) %>" class="anti_padtag" title="<%= antiTags[i] %> matches">!#<%= antiTags[i] %></a>
+ <% } %>
+ <% } %>
+ </td>
+<% return ejs_data; }); %>
+
+<% template.define('contentArea', function() { var ejs_data=''; %>
+ <div id="editorcontainer">
+ <div class="query-refiner">
+ <%: template.use('queryRefiner', function() { var ejs_data=''; %>
+ <h1>Search for pads that have the tag</h1>
+ <% for (i = 0; i < newTags.length; i++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags.concat([newTags[i].tagname]),antiTags)}) %>" class="padtag" title="<%= newTags[i].matches %> matches">#<%= newTags[i].tagname %></a>
+ <% } %>
+
+ <h1>Search for pads that <em>don't</em> have the tag</h1>
+ <% for (i = 0; i < newTags.length; i++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags,antiTags.concat([newTags[i].tagname]))}) %>" class="anti_padtag" title="<%= newTags[i].antimatches %> matches">!#<%= newTags[i].tagname %></a>
+ <% } %>
+ <% return ejs_data; }); %>
+ </div>
+
+ <dl>
+ <%: template.use('queryResult', function() { var ejs_data=''; %>
+ <% for (i = 0; i < matchingPads.length; i++) { %>
+ <%
+ var matchingPadId = matchingPads[i].ID;
+ var matchingPadUrl = matchingPadId;
+ if (!inArray('writable', matchingPads[i].TAGS)) {
+ matchingPadId = padIdToReadonly(matchingPads[i].ID);
+ matchingPadUrl = 'ep/pad/view/' + matchingPadId + '/latest';
+ }
+ %>
+ <dt><a href="/<%= matchingPadUrl %>"><%= matchingPadId %></a><dt>
+ <dd>
+ <% for (j = 0; j < matchingPads[i].TAGS.length; j++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags.concat([matchingPads[i].TAGS[j]]), antiTags)}) %>" class="padtag" title="<%= matchingPads[i].TAGS[j] %> matches">#<%= matchingPads[i].TAGS[j] %></a>
+ <% } %>
+ </dd>
+ <% } %>
+ <% return ejs_data; }); %>
+ </dl>
+ </div>
+<% return ejs_data; }); %>
diff --git a/etherpad/src/plugins/twitterStyleTags/templates/tagRss.ejs b/etherpad/src/plugins/twitterStyleTags/templates/tagRss.ejs
new file mode 100644
index 0000000..2d06781
--- /dev/null
+++ b/etherpad/src/plugins/twitterStyleTags/templates/tagRss.ejs
@@ -0,0 +1,69 @@
+<?xml version="1.0"?>
+<%
+ function inArray(item, arr) {
+ for (var i = 0; i < arr.length; i++)
+ if (arr[i] == item)
+ return true;
+ return false;
+ }
+%>
+<rss version="2.0">
+ <channel>
+ <title>
+ <% if (tags.length == 0 && antiTags.length == 0) { %>
+ Latest changed pads
+ <% } else { %>
+ <% for (i = 0; i < tags.length; i++) { %>
+ #<%= tags[i] %>
+ <% } %>
+ <% for (i = 0; i < antiTags.length; i++) { %>
+ !#<%= antiTags[i] %>
+ <% } %>
+ <% } %>
+ </title>
+ <link>http://liftoff.msfc.nasa.gov/</link>
+ <description>
+ Etherpad pads / changes for the query:
+ <% if (tags.length == 0 && antiTags.length == 0) { %>
+ Latest changed pads
+ <% } else { %>
+ <% for (i = 0; i < tags.length; i++) { %>
+ #<%= tags[i] %>
+ <% } %>
+ <% for (i = 0; i < antiTags.length; i++) { %>
+ !#<%= antiTags[i] %>
+ <% } %>
+ <% } %>
+ </description>
+ <language>en-us</language>
+ <pubDate>Tue, 10 Jun 2003 04:00:00 GMT</pubDate>
+ <lastBuildDate>Tue, 10 Jun 2003 09:41:01 GMT</lastBuildDate>
+ <docs>http://blogs.law.harvard.edu/tech/rss</docs>
+ <generator>Etherpad</generator>
+ <managingEditor>editor@example.com</managingEditor>
+ <webMaster>webmaster@example.com</webMaster>
+
+ <% for (i = 0; i < matchingPads.length; i++) { %>
+ <%
+ var matchingPadId = matchingPads[i].ID;
+ var matchingPadUrl = matchingPadId;
+ if (!inArray('writable', matchingPads[i].TAGS)) {
+ matchingPadId = padIdToReadonly(matchingPads[i].ID);
+ matchingPadUrl = 'ep/pad/view/' + matchingPadId + '/latest';
+ }
+ %>
+ <item>
+ <title><%= matchingPadId %></title>
+ <link>http://<%= request.host %>/<%= matchingPadUrl %></link>
+ <description>
+ <% for (j = 0; j < matchingPads[i].TAGS.length; j++) { %>
+ #<%= matchingPads[i].TAGS[j] %>
+ <% } %>
+ </description>
+ <pubDate><%= matchingPads[i].lastWriteTime %></pubDate>
+ <guid>http://<%= request.host %>/<%= matchingPadUrl %></guid>
+ </item>
+ <% } %>
+
+ </channel>
+</rss> \ No newline at end of file
diff --git a/etherpad/src/plugins/urlIndexer/controllers/urlBrowser.js b/etherpad/src/plugins/urlIndexer/controllers/urlBrowser.js
new file mode 100644
index 0000000..cdb9602
--- /dev/null
+++ b/etherpad/src/plugins/urlIndexer/controllers/urlBrowser.js
@@ -0,0 +1,132 @@
+/**
+ * Copyright 2009 RedHog, Egil Möller <egil.moller@piratpartiet.se>
+ * Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+ *
+ * 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("plugins.twitterStyleTags.models.tagQuery");
+
+import("faststatic");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+
+import("etherpad.utils.*");
+import("etherpad.collab.server_utils");
+import("etherpad.globals.*");
+import("etherpad.log");
+import("etherpad.pad.padusers");
+import("etherpad.pro.pro_utils");
+import("etherpad.helpers");
+import("etherpad.pro.pro_accounts.getSessionProAccount");
+import("sqlbase.sqlbase");
+import("sqlbase.sqlcommon");
+import("sqlbase.sqlobj");
+import("etherpad.pad.padutils");
+
+function urlSql(querySql, limit, offset) {
+ var sql = '' +
+ 'select ' +
+ ' u.URL, ' +
+ ' m.id as ID, ' +
+ ' DATE_FORMAT(m.lastWriteTime, \'%a, %d %b %Y %H:%i:%s GMT\') as lastWriteTime, ' +
+ ' c.TAGS ' +
+ 'from ' +
+ querySql.sql + ' as q ' +
+ ' join PAD_SQLMETA as m on ' +
+ ' m.id = q.ID ' +
+ ' join PAD_TAG_CACHE as c on ' +
+ ' c.PAD_ID = q.ID ' +
+ ' join PAD_URL as u on ' +
+ ' u.PAD_ID = q.ID ' +
+ 'where ' +
+ ' m.id NOT LIKE \'%$%\'' +
+ 'order by ' +
+ ' u.URL asc ';
+ if (limit != undefined)
+ sql += 'limit ' + limit + " ";
+ if (offset != undefined)
+ sql += 'offset ' + offset + " ";
+ return {
+ sql: sql,
+ params: querySql.params
+ };
+}
+
+function onRequest() {
+ var tags = tagQuery.queryToTags(request.params.query);
+
+ /* Create the pad filter sql */
+ var querySql = tagQuery.getQueryToSql(tags.tags.concat(['public']), tags.antiTags);
+
+ /* Use the pad filter sql to figure out which tags to show in the tag browser this time. */
+ var queryNewTagsSql = tagQuery.newTagsSql(querySql);
+ var newTags = sqlobj.executeRaw(queryNewTagsSql.sql, queryNewTagsSql.params);
+
+ url = urlSql(querySql, 10);
+ var matchingUrls = sqlobj.executeRaw(url.sql, url.params);
+
+ for (i = 0; i < matchingUrls.length; i++) {
+ matchingUrls[i].TAGS = matchingUrls[i].TAGS.split('#');
+ }
+
+ var isPro = pro_utils.isProDomainRequest();
+ var userId = padusers.getUserId();
+
+ helpers.addClientVars({
+ userAgent: request.headers["User-Agent"],
+ debugEnabled: request.params.djs,
+ clientIp: request.clientAddr,
+ colorPalette: COLOR_PALETTE,
+ serverTimestamp: +(new Date),
+ isProPad: isPro,
+ userIsGuest: padusers.isGuest(userId),
+ userId: userId,
+ });
+
+ var isProUser = (isPro && ! padusers.isGuest(userId));
+
+ padutils.setOptsAndCookiePrefs(request);
+ var prefs = helpers.getClientVar('cookiePrefsToSet');
+ var bodyClass = (prefs.isFullWidth ? "fullwidth" : "limwidth")
+
+ var info = {
+ prefs: prefs,
+ config: appjet.config,
+ tagQuery: tagQuery,
+ padIdToReadonly: server_utils.padIdToReadonly,
+ tags: tags.tags,
+ antiTags: tags.antiTags,
+ newTags: newTags,
+ matchingPads: [],
+ matchingUrls: matchingUrls,
+ bodyClass: 'nonpropad',
+ isPro: isPro,
+ isProAccountHolder: isProUser,
+ account: getSessionProAccount(), // may be falsy
+ };
+
+ var format = "html";
+ if (request.params.format != undefined)
+ format = request.params.format;
+
+ if (format == "html")
+ renderHtml("urlBrowser.ejs", info, ['urlIndexer', 'twitterStyleTags']);
+ else if (format == "rss") {
+ response.setContentType("application/xml; charset=utf-8");
+ response.write(renderTemplateAsString("tagRss.ejs", info, 'urlIndexer'));
+ if (request.acceptsGzip) {
+ response.setGzip(true);
+ }
+ }
+ return true;
+}
diff --git a/etherpad/src/plugins/urlIndexer/hooks.js b/etherpad/src/plugins/urlIndexer/hooks.js
new file mode 100644
index 0000000..e0ff050
--- /dev/null
+++ b/etherpad/src/plugins/urlIndexer/hooks.js
@@ -0,0 +1,49 @@
+import("etherpad.log");
+import("dispatch.{Dispatcher,PrefixMatcher,forward}");
+import("sqlbase.sqlobj");
+import("plugins.urlIndexer.controllers.urlBrowser");
+
+function handlePath() {
+ return [[PrefixMatcher('/ep/url'), forward(urlBrowser)]];
+}
+
+REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
+REGEX_URLCHAR = new RegExp('('+/[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source+'|'+REGEX_WORDCHAR.source+')');
+REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source+REGEX_URLCHAR.source+'*(?![:.,;])'+REGEX_URLCHAR.source, 'g');
+
+function padModelWriteToDB(args) {
+ /* Update tags for the pad */
+
+ var new_urls = args.pad.text().match(REGEX_URL);
+ if (new_urls == null) new_urls = new Array();
+ var new_urls_str = new_urls.join(' ')
+
+ var old_urls_row = sqlobj.selectSingle("PAD_URL_CACHE", { PAD_ID: args.padId });
+ var old_urls_str;
+ if (old_urls_row !== null)
+ old_urls_str = old_urls_row['URLS'];
+ else
+ old_urls_str = '';
+
+ // var old_urls = old_urls_str != '' ? old_urls_str.split(' ') : new Array();
+
+ if (new_urls_str != old_urls_str) {
+ // log.info({message: 'Updating urls', new_urls:new_urls, old_urls:old_urls});
+
+ if (old_urls_row)
+ sqlobj.update("PAD_URL_CACHE", {PAD_ID: args.padId }, {URLS: new_urls.join(' ')});
+ else
+ sqlobj.insert("PAD_URL_CACHE", {PAD_ID: args.padId, URLS: new_urls.join(' ')});
+
+ sqlobj.deleteRows("PAD_URL", {PAD_ID: args.padId});
+
+ for (i = 0; i < new_urls.length; i++) {
+ sqlobj.insert("PAD_URL", {PAD_ID: args.padId, URL: new_urls[i]});
+ }
+ }
+}
+
+function docbarItemsTagBrowser() {
+ return ["<td class='docbarbutton'><a href='/ep/url/'>URLs</a></td>"];
+}
+
diff --git a/etherpad/src/plugins/urlIndexer/main.js b/etherpad/src/plugins/urlIndexer/main.js
new file mode 100644
index 0000000..18bdef1
--- /dev/null
+++ b/etherpad/src/plugins/urlIndexer/main.js
@@ -0,0 +1,34 @@
+import("etherpad.log");
+import("plugins.urlIndexer.hooks");
+import("sqlbase.sqlobj");
+import("sqlbase.sqlcommon");
+
+function init() {
+ this.hooks = ['padModelWriteToDB', 'handlePath', 'docbarItemsTagBrowser'];
+ this.description = 'Indexes URLs linked to in pads so that they can be displayed outside pads, searched for etc.';
+ this.padModelWriteToDB = hooks.padModelWriteToDB;
+ this.handlePath = hooks.handlePath;
+ this.docbarItemsTagBrowser = hooks.docbarItemsTagBrowser;
+
+ this.install = install;
+ this.uninstall = uninstall;
+}
+
+function install() {
+ log.info("Installing urlIndexer");
+
+ sqlobj.createTable('PAD_URL', {
+ PAD_ID: 'varchar(128) character set utf8 collate utf8_bin not null references PAD_META(ID)',
+ URL: 'varchar(1024) character set utf8 collate utf8_bin not null',
+ });
+
+ sqlobj.createTable('PAD_URL_CACHE', {
+ PAD_ID: 'varchar(128) character set utf8 collate utf8_bin unique not null references PAD_META(ID)',
+ URLS: 'text collate utf8_bin not null',
+ });
+}
+
+function uninstall() {
+ log.info("Uninstalling urlIndexer");
+}
+
diff --git a/etherpad/src/plugins/urlIndexer/templates/urlBrowser.ejs b/etherpad/src/plugins/urlIndexer/templates/urlBrowser.ejs
new file mode 100644
index 0000000..1996dc5
--- /dev/null
+++ b/etherpad/src/plugins/urlIndexer/templates/urlBrowser.ejs
@@ -0,0 +1,53 @@
+<% /*
+Copyright 2009 Google Inc.
+Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+
+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. */ %>
+<%
+ template.inherit('tagBrowser.ejs');
+ helpers.setHtmlTitle("EtherPad: Browse URLs by tags");
+ helpers.includeCss("plugins/twitterStyleTags/tagBrowser.css");
+ helpers.includeCss("plugins/twitterStyleTags/pad.css");
+ helpers.addToHead('\n<link rel="alternate" href="' + helpers.updateToUrl({format:'rss'}) + '" type="application/rss+xml" title="Query results as RSS" />\n');
+
+ function inArray(item, arr) {
+ for (var i = 0; i < arr.length; i++)
+ if (arr[i] == item)
+ return true;
+ return false;
+ }
+%>
+
+<% template.define('docBarTitle', function() { var ejs_data=''; %>
+ <td id="docbarpadtitle"><span>Browse URLs by tags</span></td>
+<% return ejs_data; }); %>
+
+<% template.define('queryResult', function() { var ejs_data=''; %>
+ <% for (i = 0; i < matchingUrls.length; i++) { %>
+ <%
+ var matchingPadId = matchingUrls[i].ID;
+ var matchingPadUrl = matchingPadId;
+ if (!inArray('writable', matchingUrls[i].TAGS)) {
+ matchingPadId = padIdToReadonly(matchingUrls[i].ID);
+ matchingPadUrl = 'ep/pad/view/' + matchingPadId + '/latest';
+ }
+ %>
+ <dt><a href="<%= matchingUrls[i].URL %>"><%= matchingUrls[i].URL %></a><dt>
+ <dd>
+ <a href="<%= matchingPadUrl %>"><%= matchingPadId %></a>:
+ <% for (j = 0; j < matchingUrls[i].TAGS.length; j++) { %>
+ <a href="<%= helpers.updateToUrl({query:tagQuery.tagsToQuery(tags.concat([matchingUrls[i].TAGS[j]]), antiTags)}) %>" class="padtag" title="<%= matchingUrls[i].TAGS[j] %> matches">#<%= matchingUrls[i].TAGS[j] %></a>
+ <% } %>
+ </dd>
+ <% } %>
+<% return ejs_data; }); %>
diff --git a/etherpad/src/static/crossdomain.xml b/etherpad/src/static/crossdomain.xml
new file mode 100644
index 0000000..9e76390
--- /dev/null
+++ b/etherpad/src/static/crossdomain.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<cross-domain-policy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://www.adobe.com/xml/schemas/PolicyFile.xsd">
+<site-control permitted-cross-domain-policies="all"/>
+<allow-http-request-headers-from domain="*" headers="*"/>
+<allow-access-from domain="*.pad.spline.de" to-ports="*"/>
+<allow-access-from domain="pad.spline.de" to-ports="*"/>
+<allow-access-from domain="*.pad.spline.inf.fu-berlin.de" to-ports="*"/>
+<allow-access-from domain="pad.spline.inf.fu-berlin.de" to-ports="*"/>
+<allow-access-from domain="*.pad.spline.nomad" to-ports="*"/>
+<allow-access-from domain="pad.spline.nomad" to-ports="*"/>
+</cross-domain-policy>
diff --git a/etherpad/src/static/css/admin/admin-stats.css b/etherpad/src/static/css/admin/admin-stats.css
new file mode 100644
index 0000000..94e0d19
--- /dev/null
+++ b/etherpad/src/static/css/admin/admin-stats.css
@@ -0,0 +1,183 @@
+#backtoadmin {
+ color: #88f;
+ padding: 4px;
+ text-decoration: none;
+}
+
+#topnav {
+ margin-top: .5em;
+ margin-bottom: 1em;
+ font-family: Verdana, sans-serif;
+ font-size: 1.2em;
+}
+
+#topnav ul {
+ padding: 0;
+ margin: 0 0 0 12px;
+}
+
+#topnav ul li {
+ float: left;
+ display: inline;
+}
+#topnav ul li a {
+ display: block;
+ padding: .4em 1em;
+ text-decoration: none;
+ color: blue;
+}
+#topnav ul li.selected a {
+ background: #fff;
+ color: black;
+ border-bottom: 1px solid black;
+}
+
+/* ----- */
+
+/*.statbox {
+ display: box;
+ overflow: hidden;
+ padding-left: 8px;
+}
+*/
+
+.latesttable {
+ border-top: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+}
+
+.latesttable td {
+ border-right: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ padding: 2px 6px;
+}
+
+/*
+.statbox table td span { }
+
+.statbox .stat-title {
+ display: block;
+ font-family: Verdana, sans-serif;
+ font-size: 1.4em;
+ text-decoration: none;
+ border-bottom: 1px solid #bbb;
+ margin-top: 1em;
+}
+
+.statbox .stat-table {
+ float: left;
+ padding: 4px;
+}
+.statbox .stat-graph {
+ float: left;
+}
+*/
+form#statprefs {
+ background: #eee;
+ padding: 1em;
+ border-top: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ margin-top: 1em;
+}
+
+a.viewall {
+ margin-left: 8px;
+}
+
+div.statentry {
+/* width: 800px;*/
+ border: 1px solid #060;
+ background: #afa;
+ margin: 1em;
+}
+
+body {
+ margin: 0;
+ min-width: 800px;
+}
+
+div.warning {
+ background: #ffa;
+ border: 1px solid #630;
+}
+
+div.error {
+ background: #faa;
+ border: 1px solid #600;
+}
+
+.statentry h2 {
+ font-size: 13pt;
+ font-family: sans-serif;
+ background: #0a0;
+ color: white;
+ padding: 5px;
+ margin: 0;
+ cursor: pointer;
+}
+
+.statentry h3 {
+ font-size: 13pt;
+ font-family: sans-serif;
+ font-weight: normal;
+ margin: 3px;
+ padding: 0;
+}
+
+.statentry h4 {
+ font-size: 12pt;
+ font-weight: bold;
+ margin: 3px;
+}
+
+.statentry h2:hover {
+ text-decoration: underline;
+}
+
+.warning h2 {
+ background: #ea0;
+}
+
+.error h2 {
+ background: #a00;
+}
+
+.statentry table {
+ width: 100%;
+}
+
+.statentry .graph {
+ background: white;
+ padding: 2px;
+ width: 600px;
+}
+
+.graph .datalinks {
+ margin-top: 10px;
+ font-size: .8em;
+ text-align: right;
+ color: gray;
+}
+
+.graph .datalinks a {
+ color: gray;
+}
+
+.statentry .latest {
+ background: white;
+ vertical-align: top;
+ font-size:;
+}
+
+.statbody {
+ display: none;
+}
+
+/*div.categorywrapper {
+ -moz-column-width: 500px;
+ -moz-column-gap: 20px;
+ -webkit-column-width: 500px;
+ -webkit-column-gap: 20px;
+ column-width: 500px;
+ column-gap: 20px;
+}*/ \ No newline at end of file
diff --git a/etherpad/src/static/css/admin/pluginmanager.css b/etherpad/src/static/css/admin/pluginmanager.css
new file mode 100644
index 0000000..136a713
--- /dev/null
+++ b/etherpad/src/static/css/admin/pluginmanager.css
@@ -0,0 +1,62 @@
+#editorcontainer {
+ padding: 5pt;
+}
+
+#editorcontainerbox {
+ overflow: auto;
+ height: auto;
+}
+
+#editbarinner {
+ line-height: 36px;
+ font-size: 16px;
+ padding-left: 6pt;
+}
+
+#editbarinner a {
+ font-size: 12px;
+}
+
+#editorcontainerbox table {
+ margin: 10pt;
+ border-collapse: collapse;
+}
+
+#editorcontainerbox table tr th {
+ font-weight: bold;
+ background: #c3c3c3;
+}
+
+#editorcontainerbox table tr td,
+#editorcontainerbox table tr th {
+ border: 1px solid #c3c3c3;
+ padding: 2pt;
+}
+
+#editorcontainerbox table tr:first-child th,
+#editorcontainerbox table tr:first-child td {
+ border-top-color: #e6e6e6;
+}
+
+#editorcontainerbox table tr th:first-child,
+#editorcontainerbox table tr td:first-child {
+ border-left-color: #e6e6e6;
+}
+
+.mousover_parent .mouseover_child {
+ display: none;
+ position: absolute;
+ padding: 4pt;
+
+ border-top-color: #e6e6e6;
+ border-bottom-color: #c3c3c3;
+ border-left-color: #e6e6e6;
+ border-right-color: #c3c3c3;
+ border: 1px solid;
+
+ background: #ffffff;
+}
+
+.mousover_parent:hover .mouseover_child {
+ display: block;
+}
diff --git a/etherpad/src/static/css/broadcast.css b/etherpad/src/static/css/broadcast.css
new file mode 100644
index 0000000..afb65b8
--- /dev/null
+++ b/etherpad/src/static/css/broadcast.css
@@ -0,0 +1,386 @@
+*,html.body { margin: 0; padding: 0; }
+h1, h2, h3, h4, h5, h6 { display: inline; line-height: 2em; }
+
+.clear { clear: both; }
+
+html { font-size: 62.5%; }
+
+body { background: #ebebeb url(/static/img/jun09/pad/backgrad.gif) repeat-x left top; }
+body, textarea { font-family: Arial, sans-serif; }
+
+#topbar { height: 25px; background: #326cbd url(/static/img/jun09/pad/padtopback2.gif) repeat-x left top;
+ position: relative; }
+
+#padpage { margin-left: auto; margin-right: auto; width: 914px; vertical-align: top;}
+
+#topbarleft { float: left; height: 100%; overflow: hidden;
+ background: url(/static/img/jun09/pad/padtop4.png) no-repeat left top; width: 5px; }
+#topbarright { float: right; height: 100%; overflow: hidden;
+ background: url(/static/img/jun09/pad/padtop4.png) no-repeat right top; width: 5px; }
+
+
+.propad #topbar { background: #2c2c2c url(/static/img/jun09/pad/protop.png) repeat-x 0 -25px; }
+.propad #topbarleft { background: url(/static/img/jun09/pad/protop.png) no-repeat left top; }
+.propad #topbarright { background: url(/static/img/jun09/pad/protop.png) no-repeat right top; }
+
+a#backtoprosite, #accountnav {
+ display: block; position: absolute; height: 15px; line-height: 15px;
+ width: auto; top: 5px; font-size: 1.2em;
+}
+a#backtoprosite, #accountnav a { color: #cde7ff; text-decoration: underline; }
+#accountnav { right: 10px; color: #fff; }
+
+
+#topbarcenter { margin-left: 150px; margin-right: 150px; }
+a#topbaretherpad { margin-left: auto; margin-right: auto; display: block; width: 127px;
+ position: relative; top: 0px; height: 0; padding-top: 25px;
+ background: url(/static/img/jun09/pad/padtop4.png) no-repeat -397px 0px; overflow: hidden; }
+
+.propad a#topbaretherpad { background: url(/static/img/jun09/pad/protop.png) no-repeat -397px 0px; }
+
+#padmain {
+ margin: 7px;
+ margin-top: 5px;
+ margin-right: 0px;
+ padding: 19px;
+ padding-top:16px;
+ border: 1px solid rgb(194, 194, 194);
+ background-color: white;
+ min-height: 500px;
+ font-family: Arial, sans-serif;
+ font-size: 1.2em;
+ line-height: 17px;
+ width: 670px;
+ position: absolute;
+ top:27px;
+}
+
+/*
+ * Fancy title bar
+ */
+#padmain h1 {
+ font-family: Verdana, sans-serif;
+ font-size: 1.5em;
+ font-weight: 400;
+ display: inline-block;
+ display: -moz-inline-box;
+ padding-top: 4px;
+ padding-bottom: 10px;
+}
+
+#padcontent {
+ font-size: 0.93em;
+ line-height: 1.5em;
+ font-weight: 25;
+}
+
+#titlebar {
+ margin-bottom: 25px;
+ height: 20px;
+ width: auto;
+}
+
+#titlebar #revision {
+ float: right;
+ width: auto;
+ text-align: right;
+ vertical-align: top;
+}
+
+#revision #revision_label {
+ font-weight: bold;
+ font-size: 1.0em;
+ line-height: 1.4em;
+}
+
+#revision #revision_date {
+ font-weight: light;
+ font-size: 0.8em;
+ color: rgb(184, 184, 184);
+}
+
+#rightbars {
+ margin-left: 730px;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ margin-right: 7px;
+ position: absolute;
+ top:27px;
+}
+
+#rightbar {
+ width: 143px;
+ background-color: white;
+ border: 1px solid rgb(194, 194, 194);
+ padding: 16px;
+ padding-top: 13px;
+ font-size: 1.20em;
+ line-height: 1.8em;
+ vertical-align: top;
+}
+
+#rightbars h2 {
+ font-weight: 700;
+ font-size: 1.2em;
+ padding-top: 20px;
+ padding-bottom: 4px;
+}
+
+#rightbar img {
+ padding-left: 4px;
+ padding-right: 8px;
+ vertical-align: text-bottom;
+}
+#rightbar a {
+ color: rgb(50, 132, 213);
+ text-decoration: none;
+}
+
+#legend {
+ width: 143px;
+ background-color: white;
+ border: 1px solid rgb(194, 194, 194);
+ padding: 16px;
+ padding-top: 0px;
+ font-size: 1.20em;
+ line-height: 1.8em;
+ vertical-align: top;
+ margin-top: 10px;
+}
+#legend h2 {
+ padding-top: 10px;
+}
+
+#authorstable {
+ vertical-align: middle;
+}
+
+#authorstable div.swatch {
+ width:15px;
+ height:15px;
+ margin: 5px;
+ margin-top:3px;
+ margin-right: 14px;
+ border: rgb(149, 149, 149) 1px solid;
+}
+
+#rightbar h2 {
+ font-weight: 700;
+ font-size: 1.2em;
+ padding-top: 20px;
+ padding-bottom: 4px;
+}
+
+#rightbar a {
+ color: rgb(50, 132, 213);
+ text-decoration: none;
+}
+
+#timeslider-wrapper {
+ position: relative;
+ left: 0px;
+ right: 0px;
+ top: 0px;
+}
+
+#timeslider-left {
+ position: absolute;
+ left:-2px;
+ background-image: url(/static/img/pad/timeslider/timeslider_left.png);
+ width: 134px;
+ height: 63px;
+}
+
+#timeslider-right {
+ position: absolute;
+ top:0px;
+ right:-2px;
+ background-image: url(/static/img/pad/timeslider/timeslider_right.png);
+ width: 155px;
+ height: 63px;
+}
+
+
+#timeslider {
+ margin:7px;
+ margin-bottom: 0px;
+ width: 894px;
+ height: 63px;
+ margin-left: 9px;
+ margin-right: 9px;
+ background-image: url(/static/img/pad/timeslider/timeslider_background.png);
+ position: relative;
+}
+
+div#timeslider #timeslider-slider {
+ position: absolute;
+ left: 0px;
+ top: 1px;
+ height: 61px;
+ width: 100%;
+}
+
+div#ui-slider-handle {
+ width: 13px;
+ height: 61px;
+ background-image: url(/static/img/pad/timeslider/crushed_current_location.png);
+ cursor: pointer;
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ user-select: none;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+* html div#ui-slider-handle { /* IE 6/7 */
+ background-image: url(/static/img/pad/timeslider/current_location.gif);
+}
+
+div#ui-slider-bar {
+ position: relative;
+ margin-right: 148px;
+ height: 35px;
+ margin-left: 5px;
+ top: 20px;
+ cursor: pointer;
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ user-select: none;
+
+}
+
+div#timeslider div#playpause_button {
+ background-image: url(/static/img/pad/timeslider/crushed_button_undepressed.png);
+ width: 47px;
+ height: 47px;
+ position: absolute;
+ right: 77px;
+ top: 9px;
+}
+
+div#timeslider div#playpause_button div#playpause_button_icon {
+ background-image: url(/static/img/pad/timeslider/play.png);
+ width: 47px;
+ height: 47px;
+ position: absolute;
+ top :0px;
+ left:0px;
+}
+* html div#timeslider div#playpause_button div#playpause_button_icon {
+ background-image: url(/static/img/pad/timeslider/play.gif); /* IE 6/7 */
+}
+
+div#timeslider div#playpause_button div.pause#playpause_button_icon {
+ background-image: url(/static/img/pad/timeslider/pause.png);
+}
+* html div#timeslider div#playpause_button div.pause#playpause_button_icon {
+ background-image: url(/static/img/pad/timeslider/pause.gif); /* IE 6/7 */
+}
+
+div #timeslider div#steppers div#leftstar {
+ position: absolute;
+ right: 34px;
+ top: 8px;
+ width:30px;
+ height:21px;
+ background: url(/static/img/pad/timeslider/stepper_buttons.png) 0px 44px;
+ overflow:hidden;
+}
+
+div #timeslider div#steppers div#rightstar {
+ position: absolute;
+ right: 5px;
+ top: 8px;
+ width:29px;
+ height:21px;
+ background: url(/static/img/pad/timeslider/stepper_buttons.png) 29px 44px;
+ overflow:hidden;
+}
+
+div #timeslider div#steppers div#leftstep {
+ position: absolute;
+ right: 34px;
+ top: 33px;
+ width:30px;
+ height:21px;
+ background: url(/static/img/pad/timeslider/stepper_buttons.png) 0px 22px;
+ overflow:hidden;
+}
+
+div #timeslider div#steppers div#rightstep {
+ position: absolute;
+ right: 5px;
+ top: 33px;
+ width:29px;
+ height:21px;
+ background: url(/static/img/pad/timeslider/stepper_buttons.png) 29px 22px;
+ overflow:hidden;
+}
+
+#timeslider div.star {
+ position: absolute;
+ top: 40px;
+ background-image: url(/static/img/pad/timeslider/star.png);
+ width: 15px;
+ height: 16px;
+ cursor: pointer;
+}
+* html #timeslider div.star {
+ background-image: url(/static/img/pad/timeslider/star.gif); /* IE 6/7 */
+}
+
+#timeslider div#timer {
+ position: absolute;
+ font-family: Arial, sans-serif;
+ left: 7px;
+ top: 9px;
+ width: 122px;
+ text-align: center;
+ color: white;
+ font-size: 11px;
+}
+
+#padcontent ul, ol, li {
+ padding: 0;
+ margin: 0;
+}
+#padcontent ul { margin-left: 1.5em; }
+#padcontent ul ul { margin-left: 0 !important; }
+#padcontent ul.list-bullet1 { margin-left: 1.5em; }
+#padcontent ul.list-bullet2 { margin-left: 3em; }
+#padcontent ul.list-bullet3 { margin-left: 4.5em; }
+#padcontent ul.list-bullet4 { margin-left: 6em; }
+#padcontent ul.list-bullet5 { margin-left: 7.5em; }
+#padcontent ul.list-bullet6 { margin-left: 9em; }
+#padcontent ul.list-bullet7 { margin-left: 10.5em; }
+#padcontent ul.list-bullet8 { margin-left: 12em; }
+
+#padcontent ul { list-style-type: disc; }
+#padcontent ul.list-bullet1 { list-style-type: disc; }
+#padcontent ul.list-bullet2 { list-style-type: circle; }
+#padcontent ul.list-bullet3 { list-style-type: square; }
+#padcontent ul.list-bullet4 { list-style-type: disc; }
+#padcontent ul.list-bullet5 { list-style-type: circle; }
+#padcontent ul.list-bullet6 { list-style-type: square; }
+#padcontent ul.list-bullet7 { list-style-type: disc; }
+#padcontent ul.list-bullet8 { list-style-type: circle; }
+
+#error {
+ position: absolute;
+ margin-left: 9px;
+ top:4px;
+ left: 0px;
+ right:9px;
+/* width:894px;*/
+ height:34px;
+ background-color: rgb(247, 247, 247);
+ z-index:10;
+ text-align: center;
+ font-family: Verdana;
+ padding-top: 20px;
+ font-size: 16px;
+}
+#error a {
+ color: rgb(50, 132, 213);
+ text-decoration: none;
+}
diff --git a/etherpad/src/static/css/etherpad.css b/etherpad/src/static/css/etherpad.css
new file mode 100644
index 0000000..70bf464
--- /dev/null
+++ b/etherpad/src/static/css/etherpad.css
@@ -0,0 +1,770 @@
+/*-----
+ Reset
+-----*/
+
+ html, body, div, span, applet, object, iframe,
+ h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+ a, abbr, acronym, address, big, cite, code,
+ del, dfn, em, font, img, ins, kbd, q, s, samp,
+ small, strike, strong, sub, sup, tt, var,
+ dl, dt, dd, ol, ul, li,
+ fieldset, form, label, legend,
+ table, caption, tbody, tfoot, thead, tr, th, td {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ font-weight: inherit;
+ font-style: inherit;
+ font-size: 1em;
+ font-family: inherit;
+ vertical-align: baseline;
+ }
+ :focus {
+ outline: 0;
+ }
+ body {
+ line-height: 1;
+ color: #333;
+ background: #f7f7f7;
+ font-size: 75%;
+ }
+ html>body {
+ font-size: 12px;
+ }
+ ol, ul {
+ list-style: none;
+ }
+ table {
+ border-collapse: separate;
+ border-spacing: 0;
+ }
+ caption, th, td {
+ text-align: left;
+ font-weight: normal;
+ }
+ blockquote:before, blockquote:after,
+ q:before, q:after {
+ content: "";
+ }
+ blockquote, q {
+ quotes: "" "";
+ }
+
+/*----------------------------------------------------------------*/
+/* global */
+/*----------------------------------------------------------------*/
+a.obfuscemail {
+ text-decoration: none;
+}
+
+a:link,a:visited {
+ text-decoration: none;
+ color: #004ca8;
+}
+a:hover {
+ text-decoration: underline;
+ color: #005CCB;
+}
+.clear {
+ clear: both;
+}
+em {
+ font-style: italic;
+}
+strong {
+ font-weight: bold;
+}
+
+/*----------------------------------------------------------------*/
+/* newpad button */
+/*----------------------------------------------------------------*/
+
+.fpcontent .newpadbuttonwrap {
+ width: 152px;
+ height: 35px;
+ background: url(/static/img/davy/btn/createpad-small.gif) no-repeat bottom left;
+}
+.fpcontent .newpadbuttonwrap a {
+ width: 152px;
+ position: relative;
+ padding: 35px 0 0 0;
+ overflow: hidden;
+ background: transparent url(/static/img/davy/btn/createpad-small.gif) no-repeat top left;
+ height: 0px;
+ display: block;
+}
+
+/*----------------------------------------------------------------*/
+/* 500 error page */
+/*----------------------------------------------------------------*/
+#errorpage .error500 {
+ font-size: 1em;
+ background: #fcc;
+ border: 1px solid #f00;
+ padding: 1em;
+ margin: 1em 2em;
+ font-weight: bold;
+}
+
+/*----------------------------------------------------------------*/
+/* padviewpage */
+/*----------------------------------------------------------------*/
+body#padviewbody {
+ background-color: #ebebeb;
+}
+#padviewpage a {
+ text-decoration: underline;
+}
+#padviewpage #padviewheader {
+ margin: 8px;
+ padding: 8px;
+ border: 1px solid #ccc;
+ background-color: #e0e0ff;
+ line-height: 160%;
+}
+#padviewpage #padviewheader h1 {
+ font-weight: bold;
+ font-family: "Lucida Grande","Lucida Sans Unicode",sans-serif;
+ font-size: 2em;
+}
+#padviewpage .metadata {
+ color: #333;
+}
+#padviewpage .rlabel {
+ font-weight: bold;
+}
+#padviewpage p {
+ margin-top: 2px;
+}
+#padviewpage #padcontent {
+ border: 1px solid #ccc;
+ margin: 8px;
+ padding: 8px;
+ font-family: sans-serif;
+ font-size: 12px;
+ background-color: #fff;
+ line-height: 130%;
+}
+#padviewpage #padviewfooter {
+ margin: 8px;
+ padding: 8px;
+ font-size: 12px;
+}
+
+
+#padviewpage #export td.exportpic a img {
+ border: 0;
+}
+
+#padviewpage #export a.disabledexport {
+ color: gray;
+ text-decoration: none;
+}
+
+#padviewpage #export {
+ font-size: 1em;
+}
+
+#padviewpage #export .exportlink {
+ margin: 2px 0;
+}
+
+#padviewpage #export td.exportpic {
+ padding-left: 10px;
+}
+
+#padviewpage #export td.labelcell {
+ padding-left: 4px;
+}
+
+#export img {
+ vertical-align: middle;
+ padding: 4px;
+ padding-bottom: 8px;
+ padding-left: 3px;
+}
+
+#export span.titlelabel {
+ vertical-align: top;
+ padding-right: 12px;
+ font-size: 1.3em;
+ color: rgb(0, 0, 0);
+ font-weight: bold;
+ margin-top: 10px;
+}
+
+#export td.labelcell a {
+
+ vertical-align: middle;
+ font-size: 1em;
+ padding-right: 12px;
+ color: rgb(0, 52, 143);
+ font-weight: bold;
+}
+
+
+/*----------------------------------------------------------------*/
+/* feature tour page */
+/*----------------------------------------------------------------*/
+#featuretourpage {}
+#featuretourpage p { margin-top: 1em; }
+#featuretourpage #screencastmsg { margin: 2em 0; }
+#featuretourpage .featurebox {
+ clear: both;
+ padding: 1em 1em;
+ margin-top: 2em;
+ margin-left: auto;
+ margin-right: auto;
+ background: #eee;
+ border: 1px solid #ccc;
+}
+#featuretourpage .featurebox .featureprose {
+}
+#featuretourpage .featurebox .featureprose h2 {
+}
+#featuretourpage .featurebox .featureprose p {
+ padding: 0;
+ margin: 1em 0;
+}
+#featuretourpage .featurebox img {
+ border: 1px solid #aaa;
+ margin: 0 1em 1em 1em;
+}
+#featuretourpage #usersbox div.featureprose { }
+#featuretourpage #usersimg { float: right; }
+#featuretourpage #editsimg { padding: 0; margin: 0; }
+#featuretourpage #neverlosework img { float: left; }
+#featuretourpage #neverlosework div.featureprose { }
+#featuretourpage #lockimg { float: right; border: 0;}
+#featuretourpage #revisionsimg { float: left; margin-left: 0; }
+#featuretourpage #codeimg { float: left; margin-left: 0; }
+#featuretourpage h2 {
+ margin-top: 0;
+ font-size: 1.5em;
+ font-weight: bold;
+}
+#featuretourpage p {
+ font-size: 1.1em;
+}
+
+/*----------------------------------------------------------------*/
+/* product page */
+/*----------------------------------------------------------------*/
+
+#productpage p {
+ font-size: 1.2em;
+ color: #333;
+}
+
+#productpage h1 {
+}
+
+#productpage h2 {
+ font-size: 1.6em;
+ font-weight: bold;
+ font-family: inherit;
+ color: #399;
+ font-style: italic;
+ border-bottom: 1px solid #399;
+ margin-top: 1.5em;
+ margin-bottom: 0.3em;
+}
+
+#productpage #howuse {
+ margin-left: 80px;
+ margin-right: 80px;
+}
+
+#productpage #howuse p {
+ font-size: 1.2em;
+ line-height: 150%;
+}
+
+#productpage .tourbar { width: 100%; }
+#productpage .tourbar td { padding: 3px; }
+#productpage .tourbar .left { text-align: left; font-weight: bold; font-size: 1.6em; }
+#productpage .tourbar .right { text-align: right; }
+
+#productpage .tourbar a {
+ color: #33f;
+ font-size: 1.4em;
+ font-weight: bold;
+}
+
+#productpage #tourtop { border-bottom: 1px solid #999; }
+/* #productpage #tourbot { border-top: 1px solid #999; } */
+
+#productpage #pageshot img {
+ display: block;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 0;
+}
+
+#productpage .javascripton #tourbody {
+ /*height: 650px;*/
+ padding-top: 1px;
+ padding-bottom: 1px;
+}
+#productpage .javascripton .tourprose {
+ display: none;
+}
+#productpage #usecases table {
+ height: 300px;
+ padding: 20px auto;
+ border: 1px solid #aaa;
+ width: 100%;
+}
+#productpage #usecases td {
+ vertical-align: top;
+}
+
+#productpage #usecases p {
+ font-family: Georgia, serif;
+ font-size: 1.3em;
+ line-height: 1.3;
+}
+
+#productpage #usecases h3 {
+ padding: 0; margin: 0;
+ font-weight: bold;
+ color: black;
+ font-family: inherit;
+ font-size: 1.6em;
+ margin-top: 0.5em;
+}
+
+#productpage #usecases strong {
+ font-style: normal;
+ font-weight: normal;
+ background-color: #ffc;
+}
+
+#productpage #usecases p.intro { margin: 0.5em 0;}
+
+#productpage #usecases #prosecell {
+ padding-left: 20px;
+ padding-right: 20px;
+ padding-top: 15px;
+ border-left: 1px solid #ccc;
+ background: #fff url(/static/img/oct/insetrect.gif) no-repeat right top;
+}
+
+#productpage #usecases #prosecell p {
+ padding: 0;
+ margin: 0;
+ margin-bottom: 0.8em;
+}
+
+#productpage .showpageshot #usecases { display: none; }
+#productpage .showusecases #pageshot { display: none; }
+
+#productpage #tourleftnavcell { width: 200px; }
+
+#productpage ul#tourleftnav { margin: 0; padding: 0; list-style: none; }
+#productpage ul#tourleftnav li { margin: 0; padding: 0; background: #fff; }
+#productpage ul#tourleftnav li a { color: #33f; text-decoration: none; }
+#productpage ul#tourleftnav li a:hover { text-decoration: underline; }
+#productpage ul#tourleftnav li a { outline: none; }
+#productpage ul#tourleftnav li { background: url(/static/img/oct/usecasesnavup.gif) repeat-x left center; border-bottom: 1px solid #ccc; }
+/*#productpage ul#tourleftnav li:hover { background: url(/static/img/oct/usecasesnavuph.gif) repeat-x left center; }*/
+#productpage ul#tourleftnav li.selected { background: url(/static/img/oct/usecasesnavdown.gif) repeat-x left center }
+/*#productpage ul#tourleftnav li.selected:hover { background: url(/static/img/oct/usecasesnavdownh.gif) repeat-x left center; }*/
+#productpage ul#tourleftnav li.selected a { color: #000; background: url(/static/img/oct/tinytriangle.gif) no-repeat 95% center; }
+
+#productpage #tourleftnav a {
+ font-size: 1.2em;
+ padding: 0.4em 0.4em;
+ display: block;
+ font-weight: bold;
+ font-family: inherit;
+ font-style: italic;
+ cursor: pointer;
+}
+
+.fpcontent .newpadbuttonwrap {
+ margin: 0 auto;
+}
+
+
+/*----------------------------------------------------------------*/
+/* faq page */
+/*----------------------------------------------------------------*/
+#faqpage hr {
+ width: 100%;
+ margin-top: 2em;
+ margin-bottom: 2em;
+ margin-left: auto;
+ margin-right: auto;
+ color: #ccc;
+}
+div#faqpage h2 {
+ border-bottom: 1px solid #aaa;
+ margin: 0;
+}
+#faqpage div.answer {
+ color: #222;
+ padding: 1em;
+}
+#faqpage ul.qlist { margin: 2em 0 4em 1.4em; }
+#faqpage ul.qlist li { margin: .5em 0; }
+
+/*----------------------------------------------------------------*/
+/* contact page */
+/*----------------------------------------------------------------*/
+#contactpage div.cbox {
+ padding: 1em;
+ margin: 2em 0;
+ width: 330px;
+ height: 200px;
+}
+#contactpage h2 {
+ margin: 0;
+}
+#contactpage #boxleft {
+ float: left;
+ margin-left: 1em;
+}
+#contactpage #boxright {
+ float: right;
+ margin-right: 1em;
+}
+#contactpage #boxright p {
+ font-size: 1.4em;
+ font-family: serif;
+ color: #222;
+ padding-left: 2em;
+}
+/*----------------------------------------------------------------*/
+/* company page */
+/*----------------------------------------------------------------*/
+
+#companypage div#appjetinc { width: 300px; float: left; padding: 2em; }
+#companypage div#appjetinc p { padding-left: 1em; }
+#companypage img#ajlogo { margin-top: 1em; border: 0; }
+#companypage img#pier38 { border: 1px solid #888; float: right; margin-top: 1em; }
+#companypage table img { border: 1px solid #444; margin-top: 4px; }
+#companypage table td { padding: 15px; vertical-align: top; }
+#companypage table td p.intro { margin-top: 0; }
+
+/*----------------------------------------------------------------*/
+/* blog */
+/*----------------------------------------------------------------*/
+
+.blogbody { background: #f8f8f8; }
+
+.blogpage a#subscribelink {
+ font-size: .92em;
+ display: block;
+ background: #fff;
+ border: 1px solid #666;
+ font-weight: bold;
+ margin-bottom: 1em;
+ padding: 4px 8px;
+ color: #049;
+ text-decoration: none;
+}
+
+.blogpage div#subscribewrap a:hover {
+ background: #def;
+}
+.blogpage a#subscribelink img {
+ border: 0;
+ float: left;
+}
+.blogpage div#subscribewrap span.subtext {
+ display: block;
+ float: left;
+ padding-left: 8px;
+ padding-top: 2px;
+}
+
+div#blogcol1 {
+ width: 520px;
+ padding-left: 50px;
+ float: left;
+ font-size: .9em;
+}
+
+div#blogcol2 {
+ float: left;
+ width: 240px;
+ margin-top: 24px;
+ margin-left: 1em;
+}
+
+div#recentpostsbox {
+ border: 1px solid #ccc;
+ background: #fefefe;
+ padding: 12px 6px;
+ font-size: .8em;
+}
+div#recentpostsbox p {
+ margin: 0;
+ font-weight: bold;
+ padding-left: .5em;
+}
+div#recentpostsbox a {
+ color: #049;
+ text-decoration: none;
+}
+div#recentpostsbox a:hover {
+ text-decoration: underline;
+}
+div#recentpostsbox ul li {
+ margin: 0;
+ margin-top: .4em;
+}
+
+.blogpage div.bpheader {
+ border-top: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ background: #eee;
+ margin-top: 24px;
+ padding: 1em;
+}
+
+.blogpage div.blogpost_mainpage_content {
+ background: #fff;
+ padding: 0 1em;
+ padding-top: 0.1em;
+ padding-bottom: 0.5em;
+ border-right: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ font-size: 100%;
+ line-height: 1.4;
+}
+.blogpage div.blogpost_mainpage_content a {
+ text-decoration: underline;
+}
+.blogpage div.blogpost_mainpage_content a:hover {
+ color: red;
+}
+
+.blogpage div.bpheader div.postdate a {
+ text-decoration: none;
+ font-size: 1em;
+ font-weight: bold;
+ font-style: italic;
+ color: #555;
+}
+.blogpage div.bpheader h2 { margin: 0; border: 0; }
+.blogpage div.bpheader h2 a {
+ font-size: 1em;
+ text-decoration: none;
+ color: #049;
+ margin: 0;
+ font-style: normal;
+}
+.blogpage div#disqus_thread {
+ margin-top: 40px;
+}
+.blogpage .commentslink {
+ text-align: right;
+}
+.singleblogpost #blogposttop {
+ margin-left: 50px;
+}
+
+.blogpage h3 {
+ font-weight: bold;
+ font-size: 1em;
+ color: #050;
+}
+
+.blogpost_mainpage_content ol { }
+.blogpost_mainpage_content ol li {
+ margin-top: 1em;
+ margin-left: 2em;
+ list-style: decimal;
+}
+
+.blogpage div.code {
+ font-family: monospace;
+ border: 1px solid yellow;
+ padding: 0.5em;
+ background: #ffd;
+}
+
+.blogpage tt {
+ font-family: monospace;
+}
+
+
+/*----------------------------------------------------------------*/
+/* create pad */
+/*----------------------------------------------------------------*/
+#createpadpage form {
+ width: 80%;
+ margin-left: auto;
+ margin-right: auto;
+ border: 1px solid #ddd;
+ background: #eef;
+ font-size: 1.8em;
+ text-align: center;
+ padding: 2em;
+}
+#createpadpage #padurl {
+ background: #fff;
+ border: 1px solid #ccc;
+ padding: 1em;
+}
+#createpadpage input {
+ font-size: 1.8em;
+}
+/*----------------------------------------------------------------*/
+/* pad full */
+/*----------------------------------------------------------------*/
+#padfullpage #msg {
+ margin: 2em 0;
+ padding: 2em;
+ background: #eee;
+ border: 1px solid #aaa;
+ font-size: 1.3em;
+}
+#padfullpage #padurlwrap {
+ text-align: center;
+ margin-bottom: 3em;
+}
+#padfullpage #padurl {
+ background: #fff;
+ border: 1px solid #ccc;
+ padding: 1em;
+}
+/*----------------------------------------------------------------*/
+/* beta signup */
+/*----------------------------------------------------------------*/
+#betasignuppage img#betasign { float: left; margin-top: 20px; }
+#betasignuppage div#betaformwrap {
+ margin: 15px;
+ padding: 20px;
+ margin-left: 200px;
+ border: 1px solid #ccc;
+ background: #eee;
+}
+#betasignuppage div#betaformwrap p {
+ margin: 0;
+ color: #333;
+ margin-bottom: 1em;
+}
+#betasignuppage div#betaform { padding: 2em 0; }
+#betasignuppage div#betaform input#email { font-size: 1.6em; color: #555; }
+#betasignuppage div#betaform button { font-size: 1.6em; }
+#betasignuppage div#confirm { display: none; }
+#betasignuppage div#error {
+ margin: 1em 3em 2em 2em;
+ color: red;
+ display: none;
+}
+#betasignuppage div#confirm {
+ margin: 2em 2em 2em 0em;
+ color: green;
+ font-weight: bold;
+ display: none;
+}
+#betasignuppage div#subtext { font-size: .9em; color: #666; }
+
+/*----------------------------------------------------------------*/
+/* time slider */
+/*----------------------------------------------------------------*/
+
+body#padsliderbody {
+ font-size: 1.2em;
+ padding: 20px;
+ background-color: #fff;
+}
+
+#padsliderbody #stuff {
+ width: 600px;
+ margin-top: 10px;
+}
+
+#padsliderbody #sliderui {
+ margin: 10px;
+}
+
+#padsliderbody #controls {
+ background: #eee;
+ padding: 5px;
+ border: 1px solid #999;
+}
+
+#padsliderbody #currevdisplay {
+ margin-top: 3px;
+}
+
+#padsliderbody #controls a {
+ color: #00f;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+#padsliderbody #stuff {
+ padding: 5px;
+}
+
+/*----------------------------------------------------------------*/
+/* testimonials */
+/*----------------------------------------------------------------*/
+
+#testimonials {
+ padding: 0 3em;
+ font-family: times serif;
+}
+
+#testimonials .head {
+ font-weight: bold;
+ padding-top: 4px;
+ padding-right: 80px;
+}
+
+#testimonials .quote-open {
+ background: url(/static/img/about/quote-open.png) no-repeat left top;
+ padding-left: 80px;
+ margin-top: 2em;
+}
+
+#testimonials .quote-close {
+ background: url(/static/img/about/quote-close.png) no-repeat right bottom;
+ padding-right: 80px;
+ text-align: justify;
+ color: #222;
+}
+
+#testimonials .attrib {
+ font-style: italic;
+ text-align: right;
+ padding-left: 2em;
+ padding-right: 80px;
+ color: #444;
+}
+
+#testimonials .attrib p {
+ margin: 0;
+}
+
+
+/* pne faq */
+
+div.pne-faq dt {
+ font-size: 1.1em;
+ border-bottom: 1px solid #444;
+ color: #666;
+ margin: 1.6em 0 0.75em 0;
+}
+
+/* support page */
+
+div#support-content {
+ margin: 0 4em;
+}
+
+div#forums-content {
+ margin: 0 4em;
+}
diff --git a/etherpad/src/static/css/framedpage.css b/etherpad/src/static/css/framedpage.css
new file mode 100644
index 0000000..a99554b
--- /dev/null
+++ b/etherpad/src/static/css/framedpage.css
@@ -0,0 +1,175 @@
+/*------
+ Global Container
+------*/
+
+#container {
+ font-family: Arial, Helvetica, Calibri, sans-serif;
+}
+body.home {
+ background: #f7f7f7 url(/static/img/davy/bg/home2.png) repeat-x top;
+}
+.home #container {
+ width: 920px; margin: 0 auto;
+}
+body.nothome {
+ background: #f7f7f7 url(/static/img/davy/bg/product.png) repeat-x top;
+}
+.nothome #container {
+ width: 910px; margin: 0 auto;
+}
+
+/*------
+ Layout
+------*/
+
+#navigation,
+.home #top,
+.home #bottom,
+#footer {
+ width: 888px;
+ margin: 0 auto;
+}
+
+/* framed page general */
+div.fpcontent {
+ width: 848px;
+ margin: 0 auto;
+
+ font-size: 1.3em;
+ padding: 20px;
+
+ background-color: #fff;
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ border-top: 0;
+}
+div.fpcontent h1 {
+ color: #666;
+ border-bottom: 1px solid #666;
+ margin: .8m 0 1em 0;
+ font-size: 1.8em;
+}
+div.fpcontent h2 {
+ color: #666;
+ border-bottom: 1px solid #666;
+ font-size: 1.4em;
+ margin: 1em 0;
+}
+div.fpcontent p {
+ margin: 1em 0;
+ line-height: 150%;
+}
+div.fpcontent ul {
+ list-style: disc;
+ padding-left: 1.5em;
+}
+div.fpcontent ul li {
+ margin: 1em 0;
+ padding-left: .5em;
+}
+
+/*----------
+ Navigation
+----------*/
+
+#topnav_wrap {
+ background: url(/static/img/davy/bg/product.png) repeat-x top;
+}
+#navigation {
+ height: 38px;
+ overflow: hidden;
+ background: url(/static/img/davy/bg/home2.png) repeat-x top;
+}
+#navigation h1 a {
+ display: block;
+ width: 120px;
+ position: relative;
+ padding: 38px 0 0 0;
+ overflow: hidden;
+ background: transparent url(/static/img/davy/gfx/product-logo.gif) no-repeat 0 6px;
+ height: 0px;
+ float: left;
+}
+.home #navigation h1 a {
+ display: none;
+}
+#navigation ul {
+ margin-right: -10px;
+}
+#navigation li {
+ display: inline;
+}
+#navigation li a {
+ font-family: Calibri, "Trebuchet MS", Trebuchet, Arial, sans-serif;
+ font-size: 1.208em;
+ text-transform: uppercase;
+ color: #fff;
+ text-shadow: 0 1px 0 #223f6b;
+ letter-spacing: 1px;
+ display: block;
+ padding: 11px 10px 13px 10px;
+ float: right;
+}
+.home #navigation .topnav_pricing a {
+ color: #ffc261;
+}
+.home #navigation .topnav_pricing a:hover {
+ color: #FEAC59;
+}
+#navigation li.selected a {
+ color: #bddbff;
+}
+.home #navigation li.selected a {
+ background: url(/static/img/davy/bg/home-nav-selected.png) no-repeat center 32px;
+}
+.nothome #navigation li.selected a {
+ background: url(/static/img/davy/bg/product-nav-selected-white.png) no-repeat center 32px;
+}
+#navigation li a:hover {
+ color: #DEEDFF;
+ text-decoration: none;
+}
+
+
+/*------
+ Footer
+------*/
+
+.home #footer {
+ border-top: 1px solid #d9d9d9;
+ margin-top: 24px;
+}
+
+.nothome #footer {
+ margin-top: 0px;
+}
+
+ #footer-inner {
+ border-top: 1px solid #f9f9f9;
+ padding: 12px 0;
+ color: #666;
+ font-size: .917em;
+ }
+
+#footer-left {
+ float: left;
+ width: 700px;
+}
+ #footer ul,
+ #footer li {
+ display: inline;
+ }
+ #footer li {
+ margin-left: 1em;
+ }
+
+ #footer #appjet {
+ float: right;
+ margin-right: -12px;
+ }
+ #footer #appjet a {
+ background: url(/static/img/davy/gfx/plane.gif) no-repeat right center;
+ padding-right: 12px;
+ }
+
diff --git a/etherpad/src/static/css/global-pro-account.css b/etherpad/src/static/css/global-pro-account.css
new file mode 100644
index 0000000..6c34446
--- /dev/null
+++ b/etherpad/src/static/css/global-pro-account.css
@@ -0,0 +1,52 @@
+div.error {
+ border: 1px solid red;
+ background: #fee;
+ padding: 1em;
+ margin: 1em 0;
+ width: 600px;
+ font-weight: bold;
+}
+
+form#global-sign-in {
+ background: #eeeef6;
+ padding: 1em;
+ border: 1px solid #ddd;
+ margin: 1em 0;
+ width: 600px;
+}
+
+form label {
+ color: #444;
+ margin-bottom: .2em;
+}
+
+form input {
+ border: 1px solid #377ec6;
+}
+
+form#global-sign-in label {
+ display: block;
+ margin-top: 1em;
+}
+
+form#global-sign-in button {
+ border: 0;
+ cursor: pointer;
+ color: #fff;
+ font-weight: bold;
+ overflow: visible;
+ padding: 0;
+ background: #70a4ec;
+ border: 1px solid #3773c6;
+ padding: 4px 16px;
+ margin-top: 14px;
+}
+
+.global-pro-account p {
+ font-size: 86%;
+}
+
+div.tip {
+ margin: .5em 0;
+ font-size: 90%;
+}
diff --git a/etherpad/src/static/css/home-opensource.css b/etherpad/src/static/css/home-opensource.css
new file mode 100644
index 0000000..bb0201e
--- /dev/null
+++ b/etherpad/src/static/css/home-opensource.css
@@ -0,0 +1,40 @@
+
+#home {
+ width: 600px;
+ margin: 0 auto;
+ padding: 2em;
+ text-align: center;
+}
+
+#home #title {
+ font-size: 3.6em;
+}
+
+#home a#home-newpad{
+ display: block;
+ padding: 1em;
+ margin: 12px 60px;
+ font-size: 1.6em;
+ border: 1px solid black;
+ background: #049;
+ color: #fff;
+}
+
+#home a#home-newpad:hover{
+ background: #26b;
+}
+
+#home a#home-newteam {
+ display: block;
+ padding: 1em;
+ margin: 12px 60px;
+ font-size: 1.6em;
+ border: 1px solid black;
+ background: #940;
+ color: #fff;
+}
+
+#home a#home-newteam:hover {
+ background: #b26;
+}
+
diff --git a/etherpad/src/static/css/lib/jquery.contextmenu.css b/etherpad/src/static/css/lib/jquery.contextmenu.css
new file mode 100644
index 0000000..15a69aa
--- /dev/null
+++ b/etherpad/src/static/css/lib/jquery.contextmenu.css
@@ -0,0 +1,244 @@
+/* Classic Windows Theme (default) */
+/* =============================== */
+.context-menu-theme-default {
+ border:2px outset white;
+ background-color:#D4D0C8;
+}
+.context-menu-theme-default .context-menu-item {
+ text-align:left;
+ cursor:pointer;
+ padding:4px 28px 4px 16px;
+ color:black;
+ font-family:Tahoma,Arial;
+ font-size:11px;
+}
+.context-menu-theme-default .context-menu-separator {
+ margin:4px 2px;
+ font-size:0px;
+ border-top:1px solid #808080;
+ border-bottom:1px solid white;
+}
+.context-menu-theme-default .context-menu-item-disabled {
+ color:#808080;
+}
+.context-menu-theme-default .context-menu-item .context-menu-item-inner {
+ background:none no-repeat fixed 999px 999px; /* Make sure icons don't appear */
+}
+.context-menu-theme-default .context-menu-item-hover {
+ background-color:#0A246A;
+ color:white;
+}
+.context-menu-theme-default .context-menu-item-disabled-hover {
+ background-color:#0A246A;
+}
+
+/* Windows XP Theme */
+/* ================ */
+.context-menu-theme-xp {
+ border:1px solid #666;
+ padding:1px;
+ background:#F9F8F7 url(/static/img/lib/jquery.contextmenu.images/cmenu-xp-bg.gif) repeat-y top left;
+}
+.context-menu-theme-xp .context-menu-separator {
+ margin:4px 2px;
+ font-size:0px;
+ border-top:1px solid #808080;
+ border-bottom:1px solid white;
+}
+.context-menu-theme-xp .context-menu-item {
+ text-align:left;
+ color:black;
+ font-family:arial;
+ font-size:11px;
+ cursor:pointer;
+}
+.context-menu-theme-xp .context-menu-item .context-menu-item-inner {
+ background:none no-repeat 2px center;
+ padding:4px 10px 4px 30px;
+}
+.context-menu-theme-xp .context-menu-item-hover .context-menu-item-inner {
+ background:#B6BDD2 none no-repeat 2px center;
+ padding:3px 9px 3px 29px;
+ border:1px solid #0A246A;
+}
+
+/* Windows Vista Theme */
+/* =================== */
+.context-menu-theme-vista {
+ background:#FAFAFA url(/static/img/lib/jquery.contextmenu.images/cmenu-vista-bg.gif) repeat-y left top;
+ border:1px solid #868686;
+}
+.context-menu-theme-vista .context-menu-item {
+ text-align:left;
+ cursor:pointer;
+ color:black;
+ font-family:Tahoma,Arial;
+ font-size:11px;
+}
+.context-menu-theme-vista .context-menu-separator {
+ margin:0px 0px 0px 32px;
+ font-size:0px;
+ border-top:1px solid #C5C5C5;
+ border-bottom:1px solid #F5F5F5;
+}
+.context-menu-theme-vista .context-menu-item-hover {
+ background:transparent url(/static/img/lib/jquery.contextmenu.images/cmenu-vista-menu-item-hover.gif) repeat-x left center;
+ border:1px solid #D7D0B3;
+}
+.context-menu-theme-vista .context-menu-item .context-menu-item-inner {
+ padding:4px 16px 4px 35px;
+ margin-left:1px;
+ background-color:none;
+ background-repeat:no-repeat;
+ background-position:3px center;
+ background-image:none;
+}
+.context-menu-theme-vista .context-menu-item-hover .context-menu-item-inner {
+ padding:3px 15px 3px 35px;
+ margin-left:0px;
+}
+.context-menu-theme-vista .context-menu-item-disabled {
+ color:#A7A7A7;
+}
+
+/* OSX Theme */
+/* ========= */
+.context-menu-theme-osx {
+ background-color:white;
+ opacity: .93;
+ filter: alpha(opacity=93);
+ zoom:1.0;
+ border:1px solid #b2b2b2;
+}
+.context-menu-theme-osx .context-menu-item {
+ text-align:left;
+ cursor:pointer;
+ color:black;
+ font-family:Lucida Grande,Arial;
+ font-weight:700;
+ font-size:12px;
+ opacity: 1.0;
+ filter: alpha(opacity=100);
+ z-index:1;
+}
+.context-menu-theme-osx .context-menu-separator {
+ margin:5px 1px 4px 1px;
+ font-size:0px;
+ border-top:1px solid #e4e4e4;
+}
+.context-menu-theme-osx .context-menu-item-hover {
+ background-color:#1C44F2;
+ color:white;
+}
+.context-menu-theme-osx .context-menu-item .context-menu-item-inner {
+ padding:2px 10px 2px 22px;
+ background-color:none;
+ background-repeat:no-repeat;
+ background-position:4px center;
+ background-image:none;
+}
+.context-menu-theme-osx .context-menu-item-disabled {
+ color:#939393;
+}
+
+/* Linux Human Theme */
+/* ================= */
+.context-menu-theme-human {
+ background:#F9F5F2;
+ border:1px solid #963;
+}
+.context-menu-theme-human .context-menu-item {
+ text-align:left;
+ cursor:pointer;
+ color:black;
+ font-family:Helvetica,DejaVu Sans,Arial;
+ font-size:12px;
+ line-height:20px;
+ height:28px;
+ border:1px solid #F9F5F2;
+ border-left:0;
+ border-right:0;
+}
+.context-menu-theme-human .context-menu-separator {
+ margin:0px 0px 0px 32px;
+ font-size:0px;
+ border-top:1px solid #C5C5C5;
+ border-bottom:1px solid #F5F5F5;
+}
+.context-menu-theme-human .context-menu-item-hover {
+ background:transparent url(/static/img/lib/jquery.contextmenu.images/cmenu-human-menu-item-hover.gif) repeat-x left center;
+ border-color:#963;
+}
+.context-menu-theme-human .context-menu-item .context-menu-item-inner {
+ padding:4px 16px 4px 35px;
+ margin-left:0px;
+ background-color:none;
+ background-repeat:no-repeat;
+ background-position:3px center;
+ background-image:none;
+}
+.context-menu-theme-human .context-menu-item-hover .context-menu-item-inner {
+}
+.context-menu-theme-human .context-menu-item-disabled {
+ color:#A7A7A7;
+}
+
+/* Gloss Theme */
+/* =========== */
+.context-menu-theme-gloss {
+ background:#f4f4f4 url(/static/img/lib/jquery.contextmenu.images/cmenu-gloss-bg.gif) repeat-y left center;
+ border:1px solid #f4f4f4;
+ padding:1px;
+ padding-right:0;
+}
+.context-menu-theme-gloss .context-menu-item {
+ text-align:left;
+ cursor:pointer;
+ color:black;
+ font-family:Helvetica,DejaVu Sans,Arial;
+ font-size:12px;
+ line-height:20px;
+ height:27px;
+ /*border:1px solid transparent;*/
+ border:1px solid #f4f4f4; /* IE6 doesn't have "transparent" -- DG */
+}
+.context-menu-theme-gloss .context-menu-separator {
+ margin:0px 0px 0px 32px;
+ font-size:0px;
+ border-top:1px solid #C5C5C5;
+ border-bottom:1px solid #F5F5F5;
+}
+.context-menu-theme-gloss .context-menu-item-hover {
+ background:transparent url(/static/img/lib/jquery.contextmenu.images/cmenu-gloss-menu-item-hover.gif) repeat-x left center;
+ color:#fff;
+ border-color:#000;
+ border-radius: 3px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+}
+.context-menu-theme-gloss .context-menu-item .context-menu-item-inner {
+ padding:4px 16px 4px 35px;
+ margin-left:0px;
+ background-color:none;
+ background-repeat:no-repeat;
+ background-position:3px center;
+ background-image:none;
+}
+.context-menu-theme-gloss .context-menu-item-hover .context-menu-item-inner {
+}
+.context-menu-theme-gloss .context-menu-item-disabled {
+ color:#A7A7A7;
+}
+
+.context-menu-theme-gloss-cyan .context-menu-item-hover {
+ background-image:url(/static/img/lib/jquery.contextmenu.images/cmenu-gloss-cyan-menu-item-hover.gif);
+ border-color:#00c;
+}
+
+.context-menu-theme-gloss-semitransparent .context-menu-item-hover {
+ background-image:url(/static/img/lib/jquery.contextmenu.images/cmenu-item-gloss-semitransparent-menu-item-hover.png);
+ border-color:#00c;
+ background-color:#30f;
+}
+
+
diff --git a/etherpad/src/static/css/pad2_ejs.css b/etherpad/src/static/css/pad2_ejs.css
new file mode 100644
index 0000000..71176ee
--- /dev/null
+++ b/etherpad/src/static/css/pad2_ejs.css
@@ -0,0 +1,910 @@
+
+*,html.body { margin: 0; padding: 0; }
+
+h1, h2, h3, h4, h5, h6 { display: inline; line-height: 2em; }
+
+.clear { clear: both; }
+
+html { font-size: 62.5%; }
+
+body { background: #ebebeb url(/static/img/jun09/pad/backgrad.gif) repeat-x left top; }
+body, textarea { font-family: Arial, sans-serif; }
+
+#padpage { margin-left: auto; margin-right: auto; width: 900px; }
+
+body.fullwidth #padpage { width: auto; margin-left: 6px; margin-right: 6px; min-width: 800px; }
+body.squish1width #padpage { width: 900px; }
+body.squish2width #padpage { width: 800px; }
+
+#topbar { height: 25px; background: #326cbd url(/static/img/jun09/pad/padtopback2.gif) repeat-x left top;
+ position: relative; }
+
+#topbarleft { float: left; height: 100%; overflow: hidden;
+ background: url(/static/img/jun09/pad/padtop5.png) no-repeat left top; width: 5px; }
+#topbarright { float: right; height: 100%; overflow: hidden;
+ background: url(/static/img/jun09/pad/padtop5.png) no-repeat right top; width: 5px; }
+
+.propad #topbar { background: #2c2c2c url(/static/img/jun09/pad/protop.png) repeat-x 0 -25px; }
+.propad #topbarleft { background: url(/static/img/jun09/pad/protop.png) no-repeat left top; }
+.propad #topbarright { background: url(/static/img/jun09/pad/protop.png) no-repeat right top; }
+
+a#backtoprosite, #accountnav {
+ display: block; position: absolute; height: 15px; line-height: 15px;
+ width: auto; top: 5px; font-size: 1.2em;
+}
+a#backtoprosite, #accountnav a { color: #cde7ff; text-decoration: underline; }
+
+a#backtoprosite { padding-left: 20px; left: 6px;
+ background: url(/static/img/jun09/pad/protop.png) no-repeat -5px -6px; }
+#accountnav { right: 10px; color: #fff; }
+
+#topbarcenter { margin-left: 150px; margin-right: 150px; }
+a#topbaretherpad { margin-left: auto; margin-right: auto; display: block; width: 127px;
+ position: relative; top: 0px; height: 0; padding-top: 25px;
+ background: url(/static/img/jun09/pad/padtop5.png) no-repeat -397px 0px; overflow: hidden; }
+
+.propad a#topbaretherpad { background: url(/static/img/jun09/pad/protop.png) no-repeat -397px 0px; }
+
+#specialkeyarea { top: 5px; left: 250px; color: yellow; font-weight: bold;
+ font-size: 1.5em; position: absolute; }
+
+#alertbar { margin-top: 6px;
+opacity: 0; filter: alpha(opacity = 0); /* IE */
+display: none;
+}
+
+#servermsg { position: relative; zoom: 1; border: 1px solid #992;
+ background: #ffc; padding: 0.8em; font-size: 1.2em; }
+#servermsg h3 { font-weight: bold; margin-right: 10px;
+ margin-bottom: 1em; float: left; width: auto; }
+#servermsg #servermsgdate { font-style: italic; font-weight: normal; color: #666; }
+a#hidetopmsg { position: absolute; right: 5px; bottom: 5px; }
+
+#shuttingdown { position: relative; zoom: 1; border: 1px solid #992;
+ background: #ffc; padding: 0.6em; font-size: 1.2em; margin-top: 6px; }
+
+#docbar { margin-top: 6px; height: 25px; position: relative; zoom: 1;
+ background: #fbfbfb url(/static/img/jun09/pad/padtopback2.gif) repeat-x 0 -31px; }
+
+.docbarbutton
+{
+ padding-top: 2px;
+ padding-bottom: 2px;
+ padding-left: 4px;
+ padding-right: 4px;
+ border-left: 1px solid #CCC;
+ white-space: nowrap;
+}
+
+.docbarbutton img
+{
+ border: 0px;
+ width: 13px;
+ margin-right: 2px;
+ vertical-align: middle;
+ margin-top: 3px;
+ margin-bottom: 2px;
+}
+
+.docbarbutton a
+{
+ font-size: 10px;
+ line-height: 18px;
+ text-decoration: none;
+ color: #444;
+ font-weight: bold;
+}
+
+.docbarbutton.highlight
+{
+ background-color: #fef2bd;
+ border: 1px solid #CCC;
+ border-right: 0px;
+}
+
+#docbarleft { position: absolute; left: 0; top: 0; height: 100%;
+ overflow: hidden;
+ background: url(/static/img/jun09/pad/padtop5.gif) no-repeat left -31px; width: 7px; }
+
+<% /* changing the size of the title / rename area means adjusting
+ the #docbarpadtitle.width, #padtitlebuttons.left,
+ and #padtitleedit.width */ %>
+
+#docbarpadtitle { position: absolute; height: auto; left: 9px;
+ width: 280px; font-size: 1.6em; color: #444; font-weight: normal;
+ line-height: 22px; margin-left: 2px; height: 22px; top: 2px;
+ overflow: hidden; text-overflow: ellipsis /*not supported in FF*/;
+ white-space:nowrap; }
+.docbar-public #docbarpadtitle { padding-left: 22px;
+ background: url(/static/img/jun09/pad/public.gif) no-repeat left center; }
+
+#docbarrenamelink { position: absolute; top: 6px;
+ font-size: 1.1em; display: none; }
+#docbarrenamelink a { color: #999; }
+#docbarrenamelink a:hover { color: #48d; }
+#padtitlebuttons { position: absolute; width: 74px; zoom: 1;
+ height: 17px; top: 4px; left: 170px; display: none;
+ background: url(/static/img/jun09/pad/ok_or_cancel.gif) 0px 0px; }
+#padtitlesave { position: absolute; display: block;
+ height: 0; padding-top: 17px; overflow: hidden;
+ width: 23px; left: 0; top: 0; }
+#padtitlecancel { position: absolute; display: block;
+ height: 0; padding-top: 17px; overflow: hidden;
+ width: 35px; right: 0; top: 0; }
+#padtitleedit { position: absolute; top: 2px; left: 5px;
+ height: 15px; padding: 2px; font-size: 1.4em;
+ background: white; border-left: 1px solid #c3c3c3;
+ border-top: 1px solid #c3c3c3;
+ border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6;
+ width: 150px; display: none;
+}
+
+#padmain { margin-top: 6px; position: relative; zoom: 1; }
+
+#padeditor { margin-right: 300px; zoom: 1; }
+.hidesidebar #padeditor { margin-right: 0; }
+
+#editbar { height: 36px;
+ background: #a5bfe2 url(/static/img/jun09/pad/editbar_background.gif) repeat-x; position: relative; }
+
+#editbarleft { float: left; height: 100%; overflow: hidden;
+ background: url(/static/img/jun09/pad/editbar_background_left.gif) no-repeat left top; width: 2px; }
+#editbarright { float: right; height: 100%; overflow: hidden;
+ background: url(/static/img/jun09/pad/editbar_background_right.gif) no-repeat right top; width: 2px; }
+
+#editbartable
+{
+ position:absolute;
+ top: 6px;
+ left: 7px;
+ width: 250px;
+ height: 24px;
+ width:520px;
+}
+
+#editbarsavetable
+{
+ position:absolute;
+ top: 6px;
+ right: 7px;
+ height: 24px;
+ width:23px;
+}
+
+#editbarsavetable td, #editbartable td
+{
+ white-space: nowrap;
+}
+
+.editbarbutton
+{
+ border-top: 1px solid #8b9eba;
+ border-bottom: 1px solid #8b9eba;
+ border-left: 1px solid #758aa9;
+ background-color: white;
+}
+
+.editbarbutton img
+{
+ margin: 0px 2px;
+ border: 0px;
+ width: 16px;
+ height: 16px;
+}
+
+.editbarbutton a:active
+{
+ position: relative;
+ top: 1px;
+ left: 1px;
+}
+
+.editbargroupsfirst
+{
+ border-left-width: 0px !important;
+}
+
+#editbar #syncstatussyncing { position: absolute; height: 26px; width: 26px;
+ background: url(/static/img/jun09/pad/syncing2.gif) no-repeat center center;
+ right: 38px; top: 5px; display: none; }
+#editbar #syncstatusdone { position: absolute; height: 26px; width: 26px;
+ background: url(/static/img/jun09/pad/syncdone.gif) no-repeat center center;
+ right: 38px; top: 5px; display: none; }
+
+#editorcontainerbox {
+ border-left: 1px solid #c4c4c4; border-right: 1px solid #c4c4c4;
+ border-bottom: 1px solid #c4c4c4;
+ background: #fff; overflow: hidden; position: relative;
+ zoom: 1; height: 397px; /*...initially*/ }
+
+#editorcontainer { height: 100%; }
+
+#editorcontainer iframe { width: 100%; padding: 0; margin: 0; }
+
+#editorloadingbox { padding-top: 100px; padding-bottom: 100px; font-size: 2.5em; color: #aaa;
+ text-align: center; position: absolute; width: 100%; height: 30px; z-index: 100; }
+
+#padsidebar { float: right; width: 290px; }
+.hidesidebar #padsidebar { width: 0; overflow: hidden; }
+
+#padusers { border: 1px solid #c4c4c4; background: #fafafa; position: relative; zoom: 1; }
+
+#myuser { background: #d9e7f9; padding: 5px; height: 53px; position: relative; }
+#myswatchbox { position: absolute; left: 5px; top: 5px; width: 22px; height: 22px;
+ /*border-top: 1px solid #c3cfe0; border-left: 1px solid #c3cfe0;
+ border-right: 1px solid #ecf3fc; border-bottom: 1px solid #ecf3fc;*/
+ border: 1px solid #bbb;
+ padding: 1px; background: transparent; cursor: pointer; }
+#myuser .myswatchboxhoverable, #myuser .myswatchboxunhoverable {
+ background: white;
+}
+#myuser .myswatchboxhoverable:hover {
+ background: #bbb;
+}
+#myswatch { width: 100%; height: 100%; background: transparent;/*...initially*/ }
+#mycolorpicker {
+ background: url(/static/img/jun09/pad/colorpicker.gif) no-repeat left top;
+ width: 232px; height: 140px;
+ position: absolute;
+ left: 13px; top: 13px; z-index: 101;
+ display: none;/*...initially*/
+}
+#mycolorpicker .n1 { left: 13px; }
+#mycolorpicker .n2 { left: 40px; }
+#mycolorpicker .n3 { left: 67px; }
+#mycolorpicker .n4 { left: 94px; }
+#mycolorpicker .n5 { left: 121px; }
+#mycolorpicker .n6 { left: 148px; }
+#mycolorpicker .n7 { left: 175px; }
+#mycolorpicker .n8 { left: 202px; }
+
+#mycolorpicker .n9 { left: 13px; top: 34px ! important;}
+#mycolorpicker .n10 { left: 40px; top: 34px ! important;}
+#mycolorpicker .n11 { left: 67px; top: 34px ! important;}
+#mycolorpicker .n12 { left: 94px; top: 34px ! important;}
+#mycolorpicker .n13 { left: 121px; top: 34px ! important;}
+#mycolorpicker .n14 { left: 148px; top: 34px ! important;}
+#mycolorpicker .n15 { left: 175px; top: 34px ! important;}
+#mycolorpicker .n16 { left: 202px; top: 34px ! important;}
+
+#mycolorpicker .n17 { left: 13px; top: 56px ! important;}
+#mycolorpicker .n18 { left: 40px; top: 56px ! important;}
+#mycolorpicker .n19 { left: 67px; top: 56px ! important;}
+#mycolorpicker .n20 { left: 94px; top: 56px ! important;}
+#mycolorpicker .n21 { left: 121px; top: 56px ! important;}
+#mycolorpicker .n22 { left: 148px; top: 56px ! important;}
+#mycolorpicker .n23 { left: 175px; top: 56px ! important;}
+#mycolorpicker .n24 { left: 202px; top: 56px ! important;}
+
+#mycolorpicker .n25 { left: 13px; top: 78px ! important;}
+#mycolorpicker .n26 { left: 40px; top: 78px ! important;}
+#mycolorpicker .n27 { left: 67px; top: 78px ! important;}
+#mycolorpicker .n28 { left: 94px; top: 78px ! important;}
+#mycolorpicker .n29 { left: 121px; top: 78px ! important;}
+#mycolorpicker .n30 { left: 148px; top: 78px ! important;}
+#mycolorpicker .n31 { left: 175px; top: 78px ! important;}
+#mycolorpicker .n32 { left: 202px; top: 78px ! important;}
+
+#mycolorpicker .pickerswatchouter {
+ border: 1px solid white;
+ width: 15px; height: 15px; position: absolute;
+ top: 12px;
+}
+#mycolorpicker .pickerswatch {
+ border: 1px solid #999;
+ width: 13px;
+ height: 13px;
+ position: absolute;
+ left: 0; top: 0;
+}
+#mycolorpicker .picked { border: 1px solid #666 !important; }
+#mycolorpicker .picked .pickerswatch { border: 1px solid #666; }
+#mycolorpickersave { position: absolute; left: 14px; top: 102px;
+ width: 47px; height: 0; padding-top: 20px; overflow: hidden;
+ cursor: pointer; }
+#mycolorpickercancel { position: absolute; left: 87px; top: 102px;
+ width: 44px; height: 0; padding-top: 20px; overflow: hidden;
+ cursor: pointer; }
+#myusernameform { margin-left: 35px; }
+#myusernameedit { font-size: 1.6em; color: #444;
+ padding: 3px; height: 18px; margin: 0; border: 0;
+ width: 197px; background: transparent; }
+#myusernameform input.editable { border: 1px solid #bbb; }
+#myuser .myusernameedithoverable:hover { background: white; }
+#mystatusform { margin-left: 35px; margin-top: 5px; }
+#mystatusedit { font-size: 1.2em; color: #777;
+ font-style: italic; display: none;
+ padding: 2px; height: 14px; margin: 0; border: 1px solid #bbb;
+ width: 199px; background: transparent; }
+#myusernameform .editactive, #myusernameform .editempty {
+ background: white; border-left: 1px solid #c3c3c3;
+ border-top: 1px solid #c3c3c3;
+ border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6;
+}
+#myusernameform .editempty { color: #ef641e; }
+
+#otherusers {
+ height: 100px;/*...initially*/
+ overflow: auto;
+}
+
+table#otheruserstable { display: none; }
+#nootherusers { padding: 10px; font-size: 1.2em; color: #999; font-weight: bold;}
+#nootherusers a { color: #48d; }
+
+#otheruserstable td {
+ border-bottom: 1px solid #e1e1e1;
+ height: 26px;
+ vertical-align: middle;
+ padding: 0 2px;
+}
+
+#otheruserstable .swatch {
+ border: 1px solid #999; width: 13px; height: 13px; overflow: hidden;
+ margin: 0 4px;
+}
+
+.usertdswatch { width: 1%; }
+.usertdname { font-size: 1.3em; color: #444; }
+.usertdstatus { font-size: 1.1em; font-style: italic; color: #999; }
+.usertdactivity { font-size: 1.1em; color: #777; }
+
+.usertdname input { border: 1px solid #bbb; width: 80px; padding: 2px; }
+.usertdname input.editactive, .usertdname input.editempty {
+ background: white; border-left: 1px solid #c3c3c3;
+ border-top: 1px solid #c3c3c3;
+ border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6;
+}
+.usertdname input.editempty { color: #888; font-style: italic;}
+
+#userlistbuttonarea { height: 28px; position: relative;
+ background: url(/static/img/jun09/pad/inviteshare2.gif) repeat-x 0 0; }
+#sharebutton {
+ background: url(/static/img/jun09/pad/inviteshare2.gif) no-repeat 0 -31px;
+ position: absolute; display: block; top: 3px; padding-top: 23px;
+ height: 0; overflow: hidden; width: 96px; left: 96px; }
+
+ /*#guestslabel { font-size: 1.2em; position: absolute; width: auto;
+ height: 22px; line-height: 22px; top: 4px; left: 8px; }
+#guestsmenu { font-size: 1.2em; position: absolute; left: 100px;
+ top: 5px; width: 95px; }
+.guestpolicystuff { display: none; }*/
+
+.guestprompt { border: 1px solid #ccc; font-size: 1.2em;
+ padding: 5px; color: #222; background: #ffc; }
+.guestprompt .choices { float: right; }
+.guestprompt a { margin: 0 0.5em; }
+
+#hdraggie {
+ background: url(/static/img/jun09/pad/hdraggie.gif) repeat-x center top;
+ height: 10px; cursor: S-resize; }
+
+#padchat { border: 1px solid #c4c4c4; }
+
+#chattop { background: #ecf2fa; padding: 5px; font-size: 1.2em; border-bottom: 1px solid #ddd; }
+#chattop a { color: #36b; }
+#chatlines { height: 198px;/*...initially*/ overflow: auto; background: #fafafa; position: relative; }
+#chatlines .chatline { color: #444; padding-left: 5px; padding-top: 2px; padding-bottom: 2px;
+ background: #ddd; overflow: hidden; }
+#chatlines .chatlinetime { display: block; font-size: 1em; color: #666; float: right; width: auto;
+ padding-right: 5px; }
+#chatlines .chatlinename, #chatlines .chatlinetext { font-size: 1.2em; }
+#chatlines h2 { margin: 0; padding-left: 5px; padding-top: 2px; padding-bottom: 2px; color: #999; font-style: italic; font-weight: bold; font-size: 1.2em; }
+#chatbottom { background: #ecf2fa; padding: 4px; }
+#chatprompt { font-size: 1.2em; color: #444; float: left; line-height: 22px; width: 35px; text-align: right; }
+#chatentryform { margin-left: 40px; }
+#chatentrybox { font-size: 1.2em; color: #444;
+ padding: 2px; height: 16px; margin: 0; border-left: 1px solid #c3c3c3;
+ border-top: 1px solid #c3c3c3;
+ border-right: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6;
+ width: 230px; }
+#padchat a#chatloadmore { display: none; font-size: 1.2em; padding: 2px 5px; font-style: italic; }
+#padchat #chatloadingmore { display: none; font-size: 1.2em; padding: 2px 5px; font-style: italic;
+ color: #999; }
+#padchat a#chatloadmore:focus { outline: 0; }
+
+#djs { font-family: monospace; font-size: 10pt;
+ height: 200px; overflow: auto; border: 1px solid #ccc;
+ background: #fee; margin: 0; padding: 6px;
+}
+#djs p { margin: 0; padding: 0; display: block; }
+
+#connectionbox {
+ position: absolute; left: 0; top: 0; width: 100%;
+ height: 191px;/*...initially; #padusers height */
+ z-index: 10; zoom: 1; overflow: hidden;
+}
+#connectionboxinner {
+ position: relative; width: 100%; height: 100%; overflow: hidden;
+}
+.cboxconnecting #connectionboxinner {
+ background: #ffd url(/static/img/jun09/pad/connectingbar.gif) no-repeat center 60px;
+}
+.cboxreconnecting #connectionboxinner {
+ background: #fed url(/static/img/jun09/pad/connectingbar.gif) no-repeat center 60px;
+}
+.cboxdisconnected #connectionboxinner {
+ background: #fdd;
+}
+.cboxdisconnected #connectionboxinner div { display: none; }
+.cboxdisconnected_userdup #connectionboxinner #disconnected_userdup { display: block; }
+.cboxdisconnected_initsocketfail #connectionboxinner #disconnected_initsocketfail { display: block; }
+.cboxdisconnected_looping #connectionboxinner #disconnected_looping { display: block; }
+.cboxdisconnected_slowcommit #connectionboxinner #disconnected_slowcommit { display: block; }
+.cboxdisconnected_unauth #connectionboxinner #disconnected_unauth { display: block; }
+.cboxdisconnected_unknown #connectionboxinner #disconnected_unknown { display: block; }
+.cboxdisconnected_initsocketfail #connectionboxinner #reconnect_advise,
+.cboxdisconnected_looping #connectionboxinner #reconnect_advise,
+.cboxdisconnected_slowcommit #connectionboxinner #reconnect_advise,
+.cboxdisconnected_unknown #connectionboxinner #reconnect_advise { display: block; }
+.cboxdisconnected div#reconnect_form { display: block; }
+.cboxdisconnected .disconnected h2 { display: none; }
+.cboxdisconnected .disconnected .h2_disconnect { display: block; }
+.cboxdisconnected_userdup .disconnected h2.h2_disconnect { display: none; }
+.cboxdisconnected_userdup .disconnected h2.h2_userdup { display: block; }
+.cboxdisconnected_unauth .disconnected h2.h2_disconnect { display: none; }
+.cboxdisconnected_unauth .disconnected h2.h2_unauth { display: block; }
+
+#connectionstatus {
+ position: absolute; width: 37px; height: 32px; overflow: hidden;
+ right: 0;
+ z-index: 11;
+}
+#connectionboxinner .connecting {
+ margin-top: 20px;
+ font-size: 2.0em; color: #555;
+ text-align: center; display: none;
+}
+.cboxconnecting #connectionboxinner .connecting { display: block; }
+
+#connectionboxinner .disconnected h2 {
+ font-size: 1.8em; color: #333;
+ text-align: left;
+ margin-top: 10px; margin-left: 10px; margin-right: 10px;
+ margin-bottom: 10px;
+}
+#connectionboxinner .disconnected p {
+ margin: 10px 10px;
+ font-size: 1.2em;
+ line-height: 1.1;
+ color: #333;
+}
+#connectionboxinner .disconnected { display: none; }
+.cboxdisconnected #connectionboxinner .disconnected { display: block; }
+
+#connectionboxinner .reconnecting {
+ margin-top: 20px;
+ font-size: 1.6em; color: #555;
+ text-align: center; display: none;
+}
+.cboxreconnecting #connectionboxinner .reconnecting { display: block; }
+
+#reconnect_form button {
+ position: relative; width: 268px; height: 28px; left: 10px;
+ font-size: 12pt;
+}
+
+/* We give docbar a higher z-index than its descendant impexp-wrapper in
+ order to allow the Import/Export panel to be on top of stuff lower
+ down on the page in IE. Strange but it works! */
+#docbar { z-index: 52; }
+
+#impexp-wrapper { width: 500px; right: 10px; }
+#impexp-panel { height: 160px; }
+.docbarimpexp-closing #impexp-wrapper { z-index: 50; }
+
+#savedrevs-wrapper { width: 100%; left: 0; }
+#savedrevs-panel { height: 79px; }
+.docbarsavedrevs-closing #savedrevs-wrapper { z-index: 50; }
+#savedrevs-wrapper .dbpanel-rightedge { background-position: 0 -10px; }
+
+#options-wrapper { width: 340px; right: 200px; }
+#options-panel { height: 114px; }
+.docbaroptions-closing #options-wrapper { z-index: 50; }
+
+#security-wrapper { width: 320px; right: 300px; }
+#security-panel { height: 130px; }
+.docbarsecurity-closing #security-wrapper { z-index: 50; }
+
+#revision-notifier { position: absolute; right: 8px; top: 25px;
+ width: auto; height: auto; font-size: 1.2em; background: #ffc;
+ border: 1px solid #aaa; color: #444; padding: 3px 5px;
+ display: none; z-index: 55; }
+#revision-notifier .label { color: #777; font-weight: bold; }
+
+/* We don't ever actually hide the wrapper, even when the panel is
+ cloased, so that its contents can always be manipulated accurately. */
+.dbpanel-wrapper { position: absolute;
+ overflow: hidden; /* animated: */ height: 0; top: 25px; /* /animated */
+ z-index: 51; zoom: 1; }
+.dbpanel-panel { position: absolute; bottom: 0; width: 100%; }
+
+.dbpanel-middle { margin-left: 7px; margin-right: 7px;
+ position: relative; height: 100%; overflow: hidden; zoom: 1; }
+.dbpanel-inner { background: #f7f7f7 /* covered up by images */;
+ width: 100%; height: 100%; position: absolute; overflow: hidden; top: -10px; }
+
+.dbpanel-top { position: absolute; top: 0; width: 100%;
+ height: 400px; background-image: url(/static/img/jun09/pad/docpanelmiddle2.png);
+ background-position: left top; }
+
+.dbpanel-bottom { position: absolute; height: 400px;
+ bottom: -390px; width: 100%;
+ background-image: url(/static/img/jun09/pad/docpanelmiddle2.png);
+ background-position: left top;
+}
+
+* html .dbpanel-top, * html .dbpanel-bottom { /* for IE 6+ */
+ background-color: transparent;
+ background-image: url(/static/img/apr09/blank.gif);
+ /* scale the image instead of repeating, but it amounts to the same */
+ filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src="/static/img/jun09/pad/docpanelmiddle2.png", sizingMethod="scale");
+}
+
+.dbpanel-leftedge, .dbpanel-rightedge, .dbpanel-botleftcorner, .dbpanel-botrightcorner {
+ position: absolute;
+ background-repeat: no-repeat;
+ background-color: transparent;
+ background-image: url(/static/img/jun09/pad/docpaneledge2.png);
+}
+
+.dbpanel-leftedge, .dbpanel-rightedge { height: 100%; width: 7px; bottom: 11px; }
+.dbpanel-botleftcorner, .dbpanel-botrightcorner { height: 11px; width: 7px; bottom: 0; }
+
+.dbpanel-leftedge, .dbpanel-botleftcorner { left: 0; background-position: -7px 0; }
+.dbpanel-rightedge, .dbpanel-botrightcorner { right: 0; background-position: 0 0; }
+
+#importexport { position: absolute; top: 5px; left: 0; font-size: 1.2em; color: #444;
+ height: 100%; width: 100%; }
+
+* html .dbpanel-leftedge, * html .dbpanel-rightedge, * html .dbpanel-botleftcorner, * html .dbpanel-botrightcorner {
+ background-color: transparent;
+ background-image: url(/static/img/apr09/blank.gif);
+ /* crop the image */
+ filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src="/static/img/jun09/pad/docpaneledge2.png", sizingMethod="crop");
+}
+* html .dbpanel-leftedge, * html .dbpanel-botleftcorner { left: -7px; width: 14px; }
+
+#impexp-importlabel { position: absolute; top: 5px; left: 10px; width: 300px; }
+
+#importform { position: absolute; top: 24px; left: 5px; width: 300px; height: 60px; }
+#importformsubmitdiv, #importformfilediv { padding: 5px 5px; }
+#importexport .importformenabled {
+ background: #cfc;
+ border: 1px solid #292;
+}
+#importexport span.nowrap { white-space: nowrap; }
+#importexport #importstatusball { margin-left: 3px; padding-top: 1px; display: none; }
+#importexport #importarrow { margin-left: 5px; padding-top: 1px; display: none; }
+#importexport .importmessage { border: 1px solid #992;
+ background: #ffc; padding: 5px; font-size: 85%; display: none; }
+#importexport #importmessagefail { margin-top: 5px; }
+#importexport #importmessagesuccess { margin: 0 20px; }
+#importexport a.disabledexport {
+ color: #333; text-decoration: none;
+ opacity: 0.5; filter: alpha(opacity = 50) /*IE*/;
+}
+#importexport #importfileinput { padding: 2px 0; }
+#importexport #importsubmitinput { padding: 2px; }
+
+#impexp-divider { position: absolute; left: 320px; top: 5px; height: 135px; width: 2px;
+ background: #ddd; }
+#impexp-close { display: block; position: absolute; right: 2px; bottom: 15px;
+ width: auto; height: auto; font-size: 85%; color: #444;
+ z-index: 61 /* > clickcatcher */}
+#impexp-disabled-clickcatcher {
+ display: none;
+ position: absolute; width: 100%; height: 100%;
+ z-index: 60;
+}
+
+#impexp-exportlabel { position: absolute; top: 5px; left: 350px;
+ width: 300px; }
+#exportlinks .exportlink {
+ display: block; position: absolute; height: 22px; width: auto;
+ background-repeat: no-repeat;
+ background-image: url(/static/img/jun09/pad/fileicons.gif);
+ line-height: 22px; padding-left: 22px; padding-right: 2px;
+}
+#exportlinks .n1 { left: 350px; top: 30px; }
+#exportlinks .n2 { left: 350px; top: 57px; }
+#exportlinks .n3 { left: 350px; top: 84px; }
+#exportlinks .n4 { left: 485px; top: 30px; }
+#exportlinks .n5 { left: 485px; top: 57px; }
+#exportlinks .n6 { left: 485px; top: 84px; }
+#exportlinks .exporthrefdoc { background-position: 2px -1px; }
+#exportlinks .exporthrefhtml { background-position: 2px -25px; }
+#exportlinks .exporthreflink { background-position: 2px -49px; }
+#exportlinks .exporthrefodt { background-position: 2px -73px; }
+#exportlinks .exporthrefpdf { background-position: 2px -97px; }
+#exportlinks .exporthreftxt { background-position: 2px -121px; }
+
+#savedrevisions { position: absolute; top: 0; left: 0; font-size: 1.2em;
+ color: #444; height: 100%; width: 100%; }
+#savedrevs-scrolly { height: 75px; width: auto; margin-right: 136px;
+ overflow: hidden; position: relative; top: 1px;
+}
+#savedrevs-scrollleft { height: 100%; width: 14px; position: absolute;
+ left: 0; top: 0; cursor: pointer;
+ background: url(/static/img/jun09/pad/savedrevarrows.gif) no-repeat right top;
+}
+#savedrevs-scrollright { height: 100%; width: 14px; position: absolute;
+ right: 0; top: 0; cursor: pointer;
+ background: url(/static/img/jun09/pad/savedrevarrows.gif) no-repeat left top;
+}
+#savedrevs-scrolly .disabledscrollleft { background-position: right bottom; }
+#savedrevs-scrolly .disabledscrollright { background-position: left bottom; }
+#savedrevs-scrollouter { margin-left: 14px; margin-right: 14px;
+ width: auto; height: 100%; overflow: hidden; position: relative;
+}
+#savedrevs-scrollinner { position: absolute; width: 1px; height: 100%;
+ overflow: visible; right: 0/*...initially*/; top: 0; }
+#savedrevisions .srouterbox { width: 120px; height: 100%;
+ position: absolute; top: 0;
+}
+#savedrevisions .srinnerbox { position: relative; top: 8px;
+ height: 59px; width: auto; border-left: 1px solid #ddd;
+ padding: 0 8px 0 8px; }
+#savedrevisions a.srname { display: block; white-space: nowrap;
+ text-overflow: ellipsis /*no FF support*/; overflow: hidden;
+ text-decoration: none; color: #444; cursor: text;
+ padding: 1px; height: 14px; position: relative; left: -1px;
+ width: 100px /*specify for proper overflow in IE*/;
+}
+#savedrevisions a.srname:hover { text-decoration: none; color: #444;
+ border: 1px solid #ccc; padding: 0; }
+#savedrevisions .sractions { font-size: 85%; color: #ccc;
+ margin-top: 1px; height: 12px; }
+#savedrevisions .sractions a { text-decoration: none;
+ color: #06c; }
+#savedrevisions .sractions a:hover { text-decoration: underline; }
+#savedrevisions .srtime { color: #666; font-size: 90%;
+ white-space: nowrap; margin-top: 3px; }
+#savedrevisions .srauthor { color: #666; font-size: 90%;
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis /*no FF*/;
+}
+#savedrevisions .srtwirly { position: absolute; display: block;
+ bottom: 0; right: 10px; display: none; }
+#savedrevisions .srnameedit {
+ position: absolute;
+}
+#savedrevs-savenow { display: block; position: absolute;
+ overflow: hidden; height: 0; padding-top: 24px; width: 81px;
+ top: 22px; right: 27px;
+ background: url(/static/img/jun09/pad/savedrevsgfx2.gif) no-repeat 0 0;
+}
+#savedrevs-savenow:active { background-position: 0 -24px; }
+#savedrevs-close { display: block; position: absolute; right: 7px; bottom: 8px;
+ width: auto; height: auto; font-size: 85%; color: #444; }
+form#reconnectform { display: none; }
+
+#padoptions { position: absolute; top: 0; left: 0; font-size: 1.2em;
+ color: #444; height: 100%; width: 100%; line-height: 15px; }
+#options-viewhead { font-weight: bold; position: absolute; top: 10px; left: 15px;
+ width: auto; height: auto; }
+#padoptions label { display: block; }
+#padoptions input { padding: 0; margin: 0; }
+#options-colorscheck { position: absolute; left: 15px; top: 34px; width: 15px; height: 15px; }
+#options-colorslabel { position: absolute; left: 35px; top: 34px; }
+#options-linenoscheck { position: absolute; left: 15px; top: 57px; width: 15px; height: 15px; }
+#options-linenoslabel { position: absolute; left: 35px; top: 57px; }
+#options-fontlabel { position: absolute; left: 15px; top: 82px; }
+#viewfontmenu { position: absolute; top: 80px; left: 90px; width: 110px; }
+#options-viewexplain { position: absolute; left: 215px; top: 15px; width: 100px; height: 70px;
+ padding-left: 10px; padding-top: 10px; border-left: 1px solid #ccc;
+ line-height: 20px; font-weight: bold; color: #999; }
+#options-close { display: block; position: absolute; right: 7px; bottom: 8px;
+ width: auto; height: auto; font-size: 85%; color: #444; }
+
+#padsecurity { position: absolute; top: 0; left: 0; font-size: 1.2em;
+ color: #444; height: 100%; width: 100%; line-height: 15px; }
+#security-close { display: block; position: absolute; right: 7px; bottom: 8px;
+ width: auto; height: auto; font-size: 85%; color: #444; }
+#security-passhead { font-weight: bold; position: absolute; top: 90px; left: 15px;
+ width: auto; height: auto; }
+#security-passbody { position: absolute; left: 75px; top: 90px; }
+#security-passwordedit { height: 15px; border: 1px solid #bbb;
+ position: absolute; top: 0; left: 15px; width: 120px; }
+#security-password a { text-decoration: none; display: block;
+ width: auto; height: auto; }
+#password-savelink, #password-cancellink {position: absolute; top: 0; }
+#security-password a:hover { text-decoration: underline; }
+#password-savelink { left: 144px; color: #06c; }
+#password-cancellink { left: 180px; color: #666; }
+#password-nonedit { left: 15px; position: absolute;
+ width: 220px; top: 0; }
+#password-setlink { color: #06c; }
+#password-clearlink { color: #06c; }
+#password-display { height: 15px; width: auto; }
+#password-inedit { display: none; }
+#password-display, #password-setlink, #password-clearlink {
+ float: left; margin-right: 10px;
+}
+#password-display { font-size: 18px; }
+#security-password .nopassword #password-display { font-size: 100%; }
+#security-password .nopassword #password-clearlink { display: none; }
+#security-password .nopassword #password-setlink { left: 60px; }
+
+#security-access { position: absolute; left: 15px; width: 200px; }
+#security-accesshead { font-weight: bold; position: absolute; top: 10px;
+ left: 0; width: auto; height: auto; }
+#security-access input, #security-access label { position: absolute; }
+#security-access input { left: 10px; }
+#security-access label { left: 30px; width: 250px; }
+#access-private, #access-private-label { top: 35px; }
+#access-public, #access-public-label { top: 60px; }
+#security-access label { color: #999; }
+#security-access label strong { font-weight: normal; padding-right: 10px;
+ color: #444; }
+
+#mainmodals { z-index: 600; /* higher than the modals themselves
+ so that modals are on top in IE */ }
+
+.modalfield { font-size: 1.2em; padding: 1px; border: 1px solid #bbb;
+ position: absolute;}
+#mainmodals .editempty { color: #aaa; }
+
+<% feedbackbox = {width:400, height:270}; %>
+#feedbackbox {
+ position: absolute; display: none;
+ width: <%=feedbackbox.width%>px; height: <%=feedbackbox.height%>px;
+ left: 100px/*set in code*/; bottom: 50px;
+ z-index: 501; zoom: 1;
+}
+#feedbackbox-tl, #feedbackbox-tr, #feedbackbox-bl, #feedbackbox-br,
+#feedbackbox-hide, #feedbackbox-send, #feedbackbox-back {
+ position: absolute; display: block;
+ background-repeat: no-repeat;
+ background-image: url(/static/img/jun09/pad/feedbackbox2.gif);
+}
+#feedbackbox-tl { width: <%=feedbackbox.width-8%>px;
+ height: <%=feedbackbox.height-8%>px; left: 0; top: 0;
+ background-position: left top; }
+#feedbackbox-tr { width: 8px; height: <%=feedbackbox.height-8%>px;
+ right: 0; top: 0; background-position: right top; }
+#feedbackbox-bl { width: <%=feedbackbox.width-8%>px;
+ height: 8px; left: 0; bottom: 0;
+ background-position: left bottom; }
+#feedbackbox-br { width: 8px; height: 8px; bottom: 0; right: 0;
+ background-position: right bottom; }
+#feedbackbox-hide { width: 22px; height: 22px; right: 9px; top: 7px;
+ background-position: -569px -6px;
+}
+#feedbackbox-back { width: <%=feedbackbox.width-16%>px;
+ height: <%=feedbackbox.height-16%>px; left: 8px; top: 8px;
+ background-position: -8px -8px;
+ background-color: white; }
+#feedbackbox-contents { width: <%=feedbackbox.width-16%>px;
+ height: <%=feedbackbox.height-16%>px; left: 8px; top: 8px;
+ position: absolute; font-size: 1.4em; color: #444; }
+#feedbackbox-contentsinner { padding: 10px; }
+#feedbackbox-send { width: 50px; height: 22px; right: 15px; bottom: 15px;
+ background-position: -535px -363px;
+}
+#feedbackbox-email { left: 90px; top: 48px; width: 356px; height: auto; }
+#feedbackbox-message { left: 90px; top: 84px; width: 358px; height: 100px; }
+#feedbackbox-response { position: absolute; bottom: 15px; left: 15px;
+ width: 390px; height: auto; font-size: 1.2em; display: none; }
+#feedbackbox .goodresponse { font-weight: bold; color: green; }
+#feedbackbox .badresponse { font-weight: bold; color: red; }
+#feedbackbox p { margin-bottom: 1em; }
+#feedbackbox ul { margin: 1em 0 1em 2em }
+#feedbackbox li { padding: 0.3em 0; }
+#feedbackbox li a { display: block; font-weight: bold; }
+#feedbackbox li a:hover { background: #ffe; }
+#feedbackbox a, #feedbackbox li a:visited { color: #47b; }
+#feedbackbox tt { font-size: 110%; }
+
+<% var shareboxfull = {width:485, height:326}; %>
+#sharebox {
+ position: absolute;
+ width: 485px;
+ left: 300px/*set in code*/; top: 100px; display: none;
+ z-index: 501; zoom: 1;
+ overflow: hidden;
+ background: white; border: 1px solid #999;
+}
+#sharebox { height: 160px/*set in code*/; }
+.nonprouser #sharebox { height: 110px/*set in code*/; }
+#sharebox-inner { width: 100%; }
+#sharebox-forms { position: absolute; top: 50px; width: 100%; }
+#sharebox-hide, #sharebox-send {
+ position: absolute; background-repeat: no-repeat;
+ background-image: url(/static/img/jun09/pad/sharebox4.gif);
+}
+#sharebox-hide, #sharebox-send { display: block; }
+#sharebox-hide { width: 22px; height: 22px; right: 9px; top: 7px;
+ background-position: <%= -(shareboxfull.width-31) %>px -6px;
+}
+#sharebox-send { width: 87px; height: 22px; right: 15px; top: <%= shareboxfull.height-22-15 %>px;
+ background-position: <%= -(shareboxfull.width-87-15) %>px <%= -(shareboxfull.height-22-15) %>px;
+}
+#sharebox-url { position: absolute; left: 20px; top: 42px; width: 440px; height: 18px;
+ text-align: left; font-size: 1.3em; line-height: 18px; padding: 2px; }
+
+#sharebox-to { left: 90px; top: 117px; height: auto; width: 378px; background: #ffe; }
+#sharebox-subject { left: 90px; top: 150px; height: auto; width: 378px; font-weight: bold; }
+#sharebox-message { left: 90px; top: 182px; width: 380px; height: 90px; }
+#sharebox-response { position: absolute; bottom: 15px; left: 15px;
+ width: 350px; height: auto; font-size: 1.2em; display: none; }
+#sharebox .goodresponse { font-weight: bold; color: green; }
+#sharebox .badresponse { font-weight: bold; color: red; }
+#sharebox-dislink { position: absolute; left: 12px; top: 78px;
+ height: 22px; width: 220px; cursor: pointer;
+ background-image: url(/static/img/jun09/pad/sharedistri.gif);
+ background-repeat: no-repeat;
+ background-position: 0 5px;
+}
+.sharebox-open #sharebox-dislink { background-position: 0 -28px; }
+#sharebox-shownwhenexpanded { display: none; }
+.sharebox-open #sharebox-shownwhenexpanded { display: block; }
+
+#sharebox-pastelink { font-size: 155%; font-weight: bold;
+ top: 13px; left: 17px; position: absolute; color: #444; }
+#sharebox-orsend { font-size: 145%; font-weight: bold;
+ top: 80px; left: 31px; position: absolute; color: #444; }
+#sharebox-fieldname-to, #sharebox-fieldname-subject, #sharebox-fieldname-message {
+ position: absolute; font-weight: bold; font-size: 125%;
+ left: 15px; color: #222;
+}
+#sharebox-fieldname-to { top: 119px; }
+#sharebox-fieldname-subject { top: 152px; }
+#sharebox-fieldname-message { top: 183px; }
+
+#sharebox-stripe { position: absolute; left: 10px;
+ width: 436px; top: 8px; height: 45px; line-height: 1.2; }
+#sharebox-stripe div { padding: 5px; font-size: 130%; }
+#sharebox-stripe strong { font-weight: bold; }
+.sharebox-stripe-public { background: #cfc; }
+.sharebox-stripe-private { background: #fec; }
+.sharebox-stripe-public .private { display: none; }
+.sharebox-stripe-private .public { display: none; }
+#sharebox-stripe a { color: #06c; }
+
+.nonprouser #sharebox-stripe { display: none; }
+.nonprouser #sharebox-forms { top: 0; }
+
+#viewbarcontents { display: none; }
+#viewzoomtitle {
+ position: absolute; left: 10px; top: 4px; height: 20px; line-height: 20px;
+ width: auto;
+}
+#viewzoommenu {
+ position: absolute; top: 3px; left: 50px;
+ width: 65px;
+}
+#bottomarea { height: 28px; overflow: hidden; position: relative;
+ font-size: 1.2em; color: #444; }
+#widthprefcheck { position: absolute;
+ background-image: url(/static/img/jun09/pad/layoutbuttons.gif);
+ background-repeat: no-repeat; cursor: pointer;
+ width: 86px; height: 20px; top: 4px; right: 2px; }
+.widthprefunchecked { background-position: -1px -1px; }
+.widthprefchecked { background-position: -1px -23px; }
+#sidebarcheck { position: absolute;
+ background-image: url(/static/img/jun09/pad/layoutbuttons.gif);
+ background-repeat: no-repeat; cursor: pointer;
+ width: 86px; height: 20px; top: 4px; right: 90px; }
+.sidebarunchecked { background-position: -1px -45px; }
+.sidebarchecked { background-position: -1px -67px; }
+#feedbackbutton { display: block; position: absolute; width: 68px;
+ height: 0; padding-top: 17px; overflow: hidden;
+ background: url(/static/img/jun09/pad/bottomareagfx.gif);
+ top: 5px; right: 220px;
+}
+
+#modaloverlay {
+ z-index: 500; display: none;
+ background-image: url(/static/img/jun09/pad/overlay2.png);
+ background-repeat: repeat-both;
+ width: 100%; position: absolute;
+ height: 400px; left: 0; top: 0;
+}
+
+* html #modaloverlay { /* for IE 6+ */
+ opacity: 1; /* in case this is looked at */
+ background-image: none;
+ background-repeat: no-repeat;
+ /* scale the image */
+ filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src="/static/img/jun09/pad/overlay2.png", sizingMethod="scale");
+}
diff --git a/etherpad/src/static/css/pro-signup.css b/etherpad/src/static/css/pro-signup.css
new file mode 100644
index 0000000..b58d86d
--- /dev/null
+++ b/etherpad/src/static/css/pro-signup.css
@@ -0,0 +1,69 @@
+.pro-signup {
+}
+
+.pro-signup #about {
+ width: 400px;
+ font-size: 86%;
+ color: #333;
+}
+
+.pro-signup h1 {
+ border: 0;
+}
+
+.pro-signup h3 {
+ font-size: 1.2em;
+ font-weight: bold;
+ margin: 0 0 .75em 0;
+ color: #888;
+}
+
+form#pro-act-form {
+}
+
+div.inputdiv {
+ width: 400px;
+ float: left;
+ background: #efe;
+ padding: .75em;
+ border-right: 1px solid #999;
+}
+
+div.inputdiv p {
+ margin: .2em 0 .6em 0;
+}
+
+div.inputhelp {
+ width: 300px;
+ font-size: 86%;
+ color: #555;
+ float: left;
+ padding-left: 1em;
+ padding-top: .5em;
+}
+
+form input {
+ border: 1px solid #377ec6;
+}
+
+form button {
+ border: 0;
+ cursor: pointer;
+ color: #fff;
+ font-weight: bold;
+ overflow: visible;
+ padding: 0;
+ background: #70a4ec;
+ border: 1px solid #3773c6;
+ padding: 4px 6px;
+ margin-top: 4px;
+}
+
+div.err {
+ margin: 1em 0;
+ padding: 1em;
+ font-weight: bold;
+ border: 1px solid #500;
+ background: #fdd;
+}
+
diff --git a/etherpad/src/static/css/pro/account.css b/etherpad/src/static/css/pro/account.css
new file mode 100644
index 0000000..212a847
--- /dev/null
+++ b/etherpad/src/static/css/pro/account.css
@@ -0,0 +1,254 @@
+.account-container {
+ width: 434px;
+ margin: 0 auto;
+}
+
+#account-error {
+ margin: 1em 0;
+ padding: 1em;
+ background: #fee;
+ border: 1px solid #f66;
+ font-weight: bold;
+}
+
+#account-message {
+ margin: 1em 0;
+ padding: 1em;
+ background: #efe;
+ border: 1px solid #ccc;
+ font-weight: bold;
+}
+
+#signin-notice {
+ margin: 1em 0;
+ padding: 1em;
+ background: #fff6cc;
+ border: 1px solid #ccc;
+}
+
+/*---- blue box (general) ----*/
+/* TODO: move to different file, bluebox.css? */
+
+div.bb {
+ background: #f7f7f7;
+}
+
+div.bb div.bb-top {
+ position: relative;
+ width: 100%;
+ height: 30px;
+ background: url(/static/img/pro/box/blue-boxtop.gif) repeat-x 0 -30px;
+}
+
+div.bb div.bb-topleft {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 30px;
+ width: 9px;
+ background: url(/static/img/pro/box/blue-boxtop.gif) no-repeat 0 0;
+}
+
+div.bb div.bb-topright {
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 30px;
+ width: 9px;
+ background: url(/static/img/pro/box/blue-boxtop.gif) no-repeat -9px 0;
+}
+
+div.bb div.bb-title {
+ color: #fff;
+ font-weight: bold;
+ line-height: 30px;
+ padding-left: 10px;
+}
+
+div.bb div.bb-in {
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+}
+
+button.bluebutton {
+ border: 0;
+ cursor: pointer;
+ color: #fff;
+ font-weight: bold;
+ overflow: visible;
+ padding: 0;
+ background: #70a4ec;
+ border: 1px solid #3773c6;
+ padding: 4px 6px;
+}
+
+button.bluebutton120 {
+ background: url(/static/img/pro/buttons/bluebutton120.gif) no-repeat;
+ width: 120px;
+ height: 26px;
+ padding: 0;
+ border: 0;
+}
+
+
+/*---- sign-in box ----*/
+
+div.bb-signin div.bb-in {
+ padding: 10px 12px 20px 12px;
+}
+
+div.bb-signin label#email-label,
+div.bb-signin label#password-label {
+ display: block;
+ float: left;
+ width: 92px;
+ margin-top: 10px;
+ font-size: 1.1em;
+ color: #333;
+ padding-top: 5px;
+}
+
+div.bb-signin input.textin,
+div.bb-signin input.passin {
+ border: 1px solid #c2c2c2;
+ background: #ffffff;
+ margin-top: 10px;
+ width: 300px;
+ font-size: 1.1em;
+ float: right;
+}
+
+div.bb-signin input#rememberMe,
+div.bb-signin label#rememberMe-label {
+ float: left;
+}
+div.bb-signin input#rememberMe {
+ margin-top: 32px;
+}
+div.bb-signin label#rememberMe-label {
+ margin-left: 10px;
+ margin-top: 32px;
+ color: #555;
+ font-size: .9em;
+ display: block;
+}
+
+div.bb-signin button.bluebutton {
+ float: right;
+ margin-top: 24px;
+}
+
+
+div.account-container div#bottom-text {
+ padding-top: 20px;
+ padding-left: 4px;
+ font-size: .9em;
+}
+div.account-container div#bottom-text a {
+ text-decoration: none;
+}
+
+#guest-signin-choice {
+ display: block;
+ border: 1px solid green;
+ background: #efe;
+ padding: 1em;
+ margin: 1em 0;
+}
+
+#account-signin-choice {
+ display: block;
+ border: 1px solid blue;
+ background: #eef;
+ padding: 1em;
+ margin: 1em 0;
+}
+
+div#guest-knock-box {
+ width: 500px;
+ margin: 0 auto;
+ border: 1px solid green;
+ background: #efe;
+ font-weight: bold;
+ padding: 1em;
+ font-size: 1.5em;
+}
+
+div#guest-knock-denied {
+ border: 1px solid red;
+ background: #fee;
+ font-weight: bold;
+ font-size: 1.5em;
+ padding: 1em;
+ margin: 0 auto;
+ width: 500px;
+ display: none;
+}
+
+/*---- recover lost password ----*/
+
+div.bb-forgotpass div.bb-in {
+ padding: 10px 12px 12px 12px;
+}
+
+div.bb-forgotpass div#instructions {
+ font-size: .8em;
+ color: #222;
+}
+
+div.bb-forgotpass label {
+ float: left;
+ width: 92px;
+ margin-top: 10px;
+ font-size: 1.1em;
+ color: #333;
+ padding-top: 5px;
+}
+
+div.bb-forgotpass input.textin {
+ border: 1px solid #c2c2c2;
+ background: #fff;
+ margin-top: 14px;
+ width: 300px;
+ float: right;
+}
+
+div.bb-forgotpass button {
+ float: right;
+ margin-top: 16px;
+}
+
+
+/*---- my account ----*/
+/* TODO: re-style this and move to different file */
+
+div.my-account {
+ width: 600px;
+}
+
+div.my-account h2 {
+ font-size: 1.2em;
+ border-bottom: 1px solid #444;
+ color: #444;
+ margin: 1em 0;
+}
+
+div.my-account table {
+ width: 500px;
+}
+
+div.my-account table .ti input {
+ width: 100%;
+}
+
+div.my-account table th {
+ width: 160px;
+}
+
+div.my-account table th,
+div.my-account table td {
+ padding: 4px 8px;
+}
+
+
diff --git a/etherpad/src/static/css/pro/framedpage-pro.css b/etherpad/src/static/css/pro/framedpage-pro.css
new file mode 100644
index 0000000..cffa58b
--- /dev/null
+++ b/etherpad/src/static/css/pro/framedpage-pro.css
@@ -0,0 +1,125 @@
+/*--- farmed page styles ---*/
+
+/*------
+ Global Container
+------*/
+
+body#framedpagebody {
+ background: #fff;
+}
+
+#container {
+ font-family: Arial, Helvetica, Calibri, sans-serif;
+ width: 920px; margin: 0 auto;
+}
+
+/*------
+ Layout
+------*/
+
+/* framed page general */
+div.fpcontent {
+ width: 888px;
+ margin: 0 auto;
+ font-size: 1.3em;
+ background-color: #fff;
+ padding-top: 1em;
+}
+
+div.fpcontent p {
+ margin: 1em 0;
+ line-height: 150%;
+}
+div.fpcontent ul {
+ list-style: disc;
+ padding-left: 2em;
+}
+div.fpcontent ul li {
+ margin: 1em 0;
+}
+
+/* top header */
+
+body.pro-withtopbar {
+ background: url(/static/img/pro/header/pro-header-plustopnav-back.gif) repeat-x top !important;
+}
+
+#pro-topbar {
+ height: 48px;
+}
+
+#pro-topbar-inner {
+ width: 888px;
+ margin: 0 auto;
+ height: 48px;
+ line-height: 48px;
+ background: url(/static/img/pro/header/pro-header-logo.png) no-repeat top center;
+}
+
+#pro-topbar div#org-name a {
+ font-size: 1.4em;
+ color: #fff;
+ vertical-align: center;
+}
+
+#pro-topbar #accountnav {
+ float: right;
+ vertical-align: center;
+ color: #fff;
+}
+
+#pro-topbar #accountnav a {
+ color: #cde7ff;
+ text-decoration: underline;
+}
+
+
+/* navigation */
+
+#pro-topnav {
+ background: url(/static/img/pro/topnav/pro-topnav-back.gif) repeat-x top;
+ height: 36px;
+}
+
+#pro-topnav-inner {
+ margin: 0 auto;
+ height: 36px;
+ width: 888px;
+}
+
+#pro-topnav ul {
+ float: left;
+}
+#pro-topnav ul li {
+ display: block;
+ height: 36px;
+ float: left;
+}
+#pro-topnav ul li a {
+ display: block;
+ line-height: 36px;
+ margin: 0 20px;
+}
+#pro-topnav ul li.topnav_home a {
+ margin-left: 0;
+}
+#pro-topnav ul li a:hover { }
+#pro-topnav ul li.selected a {
+ color: #000;
+ background: url(/static/img/pro/topnav/pro-topnav-notch.gif) no-repeat center 28px;
+}
+
+#shuttingdown { position: relative; zoom: 1; border: 1px solid #992;
+ background: #ffc; padding: 0.6em; font-size: 1.2em; margin-top: 6px; }
+
+
+/*--- framed page styles ---*/
+
+div.global-pro-notice {
+ margin: .5em 1em;
+ border: 1px solid #f84;
+ background: #ffc;
+ font-weight: bold;
+ padding: 1em;
+}
+
diff --git a/etherpad/src/static/css/pro/padlist.css b/etherpad/src/static/css/pro/padlist.css
new file mode 100644
index 0000000..13d3171
--- /dev/null
+++ b/etherpad/src/static/css/pro/padlist.css
@@ -0,0 +1,115 @@
+
+/*---- nav ----*/
+
+#padlist-nav {
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+}
+
+#padlist-nav ul {
+ margin: 0;
+ padding: 0;
+ float: left;
+}
+
+#padlist-nav form {
+ float: right;
+ padding-top: 2px;
+}
+
+#padlist-nav ul li {
+ list-style: none;
+ float: left;
+ padding: 0;
+ margin: 0;
+}
+
+#padlist-nav ul li a {
+ display: block;
+ padding: 8px 12px;
+ font-size: .8em;
+}
+
+#padlist-nav ul li a.selected {
+ color: black;
+}
+
+#padlist-nav ul li a#nav-all-pads {
+ padding-left: 0;
+}
+
+/*---- showing sentence ----*/
+
+#showing-desc {
+ margin-top: 12px;
+ color: #464;
+ font-size: .8em;
+ font-style: italic;
+}
+
+/*---- table ----*/
+
+#padtable {
+ margin-top: 1em;
+ border-top: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+}
+
+#padtable th {
+ font-weight: bold;
+}
+
+#padtable th,
+#padtable td {
+ border-right: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ padding: 4px 8px;
+}
+
+#padtable td.actions {
+ padding: 0;
+}
+
+#padtable tr:hover {
+ background: #ffffaa;
+}
+#padtable tr.toprow:hover {
+ background: inherit;
+}
+
+#padtable div.gear-drop {
+ width: 36px;
+ height: 20px;
+ background: url(/static/img/pro/padlist/gear-drop.gif) no-repeat center 4px;
+ cursor: pointer;
+ padding: 4px 8px;
+}
+
+#padtable tr.selected {
+/* background: #6670ff; */
+ background: #ffff88;
+}
+#padtable tr.selected td {
+ border-top: 1px solid black;
+ border-bottom: 1px solid black;
+ border-right: 0;
+}
+#padtable tr.selected td.first {
+ border-left: 1px solid black;
+}
+#padtable tr.selected td.last {
+ border-right: 1px solid black;
+}
+#padtable tr.selected td a {
+ color: #000;
+}
+
+div.padlist-notice {
+ border: 1px solid #ccc;
+ font-weight: bold;
+ background: #fff6cc;
+ padding: 1em;
+ margin-bottom: 1em;
+ font-size: 82.5%;
+}
+
diff --git a/etherpad/src/static/css/pro/pro-admin.css b/etherpad/src/static/css/pro/pro-admin.css
new file mode 100644
index 0000000..e7462c9
--- /dev/null
+++ b/etherpad/src/static/css/pro/pro-admin.css
@@ -0,0 +1,343 @@
+/*----------------------------------------------------------------*/
+/* admin leftnav */
+/*----------------------------------------------------------------*/
+
+#admin-layout-table {
+ width: 100%;
+}
+
+#admin-layout-table td {
+}
+
+#admin-leftnav {
+ font-size: .81em;
+ border: 1px solid #ccc;
+ white-space: nowrap;
+ background: #eee;
+ padding: 0;
+}
+
+#admin-leftnav .leftnav-title {
+ padding: .75em .25em .25em .25em;
+ border-bottom: 1px solid #ccc;
+}
+#admin-leftnav ul {
+ padding: 0;
+ list-style: none;
+}
+
+#admin-leftnav ul ul {
+ list-style: disc;
+}
+
+#admin-leftnav li {
+ display: block;
+ width: 100%;
+ margin: 0;
+}
+
+#admin-leftnav li a {
+ display: block;
+ margin: 0;
+ padding: .5em;
+}
+
+#admin-leftnav li a:hover {
+ text-decoration: none;
+ background: #ffc;
+}
+
+#admin-leftnav li.selected a {
+ color: #000;
+ font-weight: bold;
+ background: #fff;
+}
+
+/*----------------------------------------------------------------*/
+/* admin content area */
+/*----------------------------------------------------------------*/
+
+#admin-right {
+ padding-left: 1em;
+}
+
+#admin-right h3 {
+ font-weight: bold;
+ font-size: 1.1em;
+ color: #666;
+ border-bottom: 1px solid #666;
+ margin: 1.25em 0;
+}
+
+#admin-right h3.top {
+ margin-top: 0;
+}
+
+/*----------------------------------------------------------------*/
+/* server dashboard */
+/*----------------------------------------------------------------*/
+
+#responsecodes-table {
+ border 1px solid #ccc;
+}
+#responsecodes-table td,
+#responsecodes-table th {
+ padding: .4em;
+}
+#responsecodes-table th {
+ font-weight: bold;
+ border-bottom: 1px solid #ccc;
+ padding-right: 2em;
+}
+
+/*----------------------------------------------------------------*/
+/* license manager */
+/*----------------------------------------------------------------*/
+
+div.lm-error-msg {
+ border: 1px solid #f99;
+ font-weight: bold;
+ background: #fdd;
+ padding: 0 1em;
+ margin-bottom: 1em;
+}
+
+div.lm-notice-msg {
+ border: 1px solid #ccc;
+ font-weight: bold;
+ background: #fff6cc;
+ padding: 0 1em;
+ margin-bottom: 1em;
+}
+
+#lm-status {
+ border: 1px solid #ccc;
+ padding: 1em;
+ background: #dfd;
+}
+
+#lm-status table td {
+ padding: .5em 1.5em .5em 0;
+ border-bottom: 1px solid #ccc;
+ white-space: nowrap;
+}
+
+#lm-edit-button-wrap { margin: 1em 0; }
+
+#lm-edit {
+ background: #eef;
+ border: 1px solid #ccc;
+ padding: 0 1em 1em 1em;
+}
+#lm-edit p {
+ margin: 1em 0 0 0;
+}
+#lm-edit-submit-wrap { margin: 1em 0; }
+
+#lm h3 {
+/* margin-left: 1em; */
+}
+
+/*----------------------------------------------------------------*/
+/* accountmanager */
+/*----------------------------------------------------------------*/
+
+.manage-accounts {
+ font-size: .76em;
+}
+
+.manage-accounts #message {
+ border: 1px solid #ccc;
+ background: #efe;
+ color: #666;
+ font-weight: bold;
+ padding: 1em;
+}
+
+.manage-accounts #warning {
+ border: 1px solid #ccc;
+ background: #ffd;
+ color: #333;
+ font-weight: bold;
+ padding: 1em;
+ margin-top: 1em;
+}
+
+.manage-accounts form#new-account-button {
+ margin: 1em 0;
+}
+
+table#accountlist {
+ border: 1px solid #ccc;
+ border-bottom: 0;
+}
+
+table#accountlist tr:hover {
+ background: #ffc;
+}
+
+table#accountlist th,
+table#accountlist td {
+ white-space: nowrap;
+ padding: .5em 1em .5em .5em;
+ border-bottom: 1px solid #ccc;
+}
+
+table#accountlist th {
+ font-weight: bold;
+ background-color: #eef;
+}
+
+.manage-accounts p.free-notice {
+ font-style: italic;
+ color: #162;
+}
+
+.manage-accounts p.account-tally {
+ font-style: italic;
+}
+
+/* new account form */
+
+.new-account-form {
+ border: 1px solid #ccc;
+ background: #eef;
+ padding: 0;
+ margin: 0;
+}
+
+.new-account-form .forminner {
+ padding: 1em;
+}
+
+.new-account-form div.formfield {
+ margin-top: .5em;
+ padding: 0 1em;
+}
+
+.new-account-form div.formfield label { display: block; margin-top: 1em; }
+.new-account-form div.formfield input.checkboxinput { float: left; width: 20px; }
+.new-account-form div.formfield input.textinput { display: block; width: 240px; }
+.new-account-form div.formfield input.temppassinput { display: block; width: 240px; }
+.new-account-form div.formfield label.checkboxlabel { float: left; margin-top: .333em; padding-left: .25em; }
+.newaccount .buttons-wrap { margin-left: 2em; }
+
+.newaccount #bottom-note {
+ color: #555;
+ margin-left: 2em;
+ width: 50%;
+}
+
+#error-message {
+ border: 1px solid red;
+ background: #fee;
+ padding: 1em;
+ font-weight: bold;
+ margin-bottom: 1em;
+}
+
+/* manage account page */
+
+table#manage-account {
+ border-left: 1px solid #ccc;
+ border-top: 1px solid #ccc;
+ background: #eef;
+}
+table#manage-account td,
+table#manage-account th {
+ border-right: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ padding: 4px 8px;
+}
+table#manage-account th {
+ text-align: right;
+}
+
+#delete-account-page div.confirm {
+ font-weight: bold;
+}
+
+#delete-account-page div.account-info {
+ border: 1px solid #555;
+ background: #fcc;
+ padding: 1em;
+ margin: 1em 0;
+ font-family: monospace;
+}
+
+#delete-account-page div.note {
+ margin-top: 1em;
+ margin-right: 222px;
+ font-size: .9em;
+ color: #555;
+}
+
+
+/*----------------------------------------------------------------*/
+/* PNE server config */
+/*----------------------------------------------------------------*/
+
+table#pne-config {
+ font-family: monospace;
+ font-size: 12px;
+ border-top: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ white-space: nowrap;
+ background: #fefefe;
+}
+
+table#pne-config th {
+ border-bottom: 2px solid #666;
+ font-weight: bold;
+}
+table#pne-config td {
+ padding: 2px;
+ border-bottom: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+}
+
+table#pne-config td.key {
+ color: #009;
+ padding-right: 4px;
+}
+table#pne-config td.val { color: #420; }
+
+/*----------------------------------------------------------------*/
+/* Pro config */
+/*----------------------------------------------------------------*/
+
+div#pro-config-message {
+ border: 1px solid #ccc;
+ padding: 1em;
+ font-weight: bold;
+ margin: 1em 0;
+ background: #cfc;
+}
+
+table#t-pro-config {
+ display: block;
+ border-left: 1px solid #aaa;
+ border-right: 1px solid #aaa;
+ border-bottom: 1px solid #aaa;
+}
+
+table#t-pro-config th,
+table#t-pro-config td {
+ border-top: 1px solid #aaa;
+ padding: 1em;
+ text-align: top;
+ vertical-align: top;
+}
+
+table#t-pro-config td textarea {
+ width: 100%;
+ height: 260px;
+}
+
+table#t-pro-config th {
+ text-align: right;
+ color: #963;
+ font-weight: bold;
+}
+
+
diff --git a/etherpad/src/static/css/pro/pro-home.css b/etherpad/src/static/css/pro/pro-home.css
new file mode 100644
index 0000000..03f163a
--- /dev/null
+++ b/etherpad/src/static/css/pro/pro-home.css
@@ -0,0 +1,65 @@
+
+#welcome-msg {
+ font-size: 1.2em;
+ color: #333;
+}
+
+#homeright {
+ width: 320px;
+ float: right;
+}
+
+#homeleft {
+ width: 548px;
+ float: left;
+}
+
+#homeleft-title {
+ font-weight: bold;
+ font-size: 1.0em;
+ margin-top: 1em;
+}
+
+.news-time-sep {
+ margin-top: 2em;
+}
+
+.news-time-sep .date {
+ float: left;
+ background: #fff;
+ padding-right: 1em;
+ color: #666;
+ font-size: .9em;
+}
+
+.news-time-sep .line {
+ height: .5em;
+ border-bottom: 1px solid #ccc;
+}
+
+.news-item {
+ padding: 0 2em 0 1em;
+ font-size: .86em;
+}
+
+/*--------------------------------------------------------------------------------
+ * recent pads
+ *--------------------------------------------------------------------------------*/
+
+#recent-pads #viewall {
+ display: block;
+ float: left;
+ margin: 0.8em 0;
+ font-size: 0.8em;
+}
+
+#homeright #padtable {
+ width: 100%;
+}
+
+#homeright h3 {
+ font-size: 1.0em;
+ font-weight: bold;
+ margin-top: 1em;
+}
+
diff --git a/etherpad/src/static/favicon.ico b/etherpad/src/static/favicon.ico
new file mode 100644
index 0000000..a833c3a
--- /dev/null
+++ b/etherpad/src/static/favicon.ico
Binary files differ
diff --git a/etherpad/src/static/img/davy/bg/home-createpad.png b/etherpad/src/static/img/davy/bg/home-createpad.png
new file mode 100644
index 0000000..e34e643
--- /dev/null
+++ b/etherpad/src/static/img/davy/bg/home-createpad.png
Binary files differ
diff --git a/etherpad/src/static/img/davy/bg/product.png b/etherpad/src/static/img/davy/bg/product.png
new file mode 100644
index 0000000..5a6f6f2
--- /dev/null
+++ b/etherpad/src/static/img/davy/bg/product.png
Binary files differ
diff --git a/etherpad/src/static/img/davy/btn/createpad-small.gif b/etherpad/src/static/img/davy/btn/createpad-small.gif
new file mode 100644
index 0000000..5df6502
--- /dev/null
+++ b/etherpad/src/static/img/davy/btn/createpad-small.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/backgrad.gif b/etherpad/src/static/img/jun09/pad/backgrad.gif
new file mode 100644
index 0000000..8fee1a5
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/backgrad.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/colorpicker.gif b/etherpad/src/static/img/jun09/pad/colorpicker.gif
new file mode 100644
index 0000000..effa3cc
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/colorpicker.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/connectingbar.gif b/etherpad/src/static/img/jun09/pad/connectingbar.gif
new file mode 100644
index 0000000..34f54e9
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/connectingbar.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/docpaneledge2.png b/etherpad/src/static/img/jun09/pad/docpaneledge2.png
new file mode 100644
index 0000000..c119c74
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/docpaneledge2.png
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/docpanelmiddle2.png b/etherpad/src/static/img/jun09/pad/docpanelmiddle2.png
new file mode 100644
index 0000000..d251c23
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/docpanelmiddle2.png
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_background.gif b/etherpad/src/static/img/jun09/pad/editbar_background.gif
new file mode 100644
index 0000000..54ef6e4
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_background.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_background_left.gif b/etherpad/src/static/img/jun09/pad/editbar_background_left.gif
new file mode 100644
index 0000000..fe8d06e
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_background_left.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_background_right.gif b/etherpad/src/static/img/jun09/pad/editbar_background_right.gif
new file mode 100644
index 0000000..55ab00a
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_background_right.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_bold.gif b/etherpad/src/static/img/jun09/pad/editbar_bold.gif
new file mode 100644
index 0000000..d22bcaf
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_bold.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_clearauthorship.gif b/etherpad/src/static/img/jun09/pad/editbar_clearauthorship.gif
new file mode 100644
index 0000000..2c6d109
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_clearauthorship.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_groupleft.gif b/etherpad/src/static/img/jun09/pad/editbar_groupleft.gif
new file mode 100644
index 0000000..3e18749
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_groupleft.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_groupright.gif b/etherpad/src/static/img/jun09/pad/editbar_groupright.gif
new file mode 100644
index 0000000..bf8b757
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_groupright.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_indent.gif b/etherpad/src/static/img/jun09/pad/editbar_indent.gif
new file mode 100644
index 0000000..989523a
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_indent.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_insertunorderedlist.gif b/etherpad/src/static/img/jun09/pad/editbar_insertunorderedlist.gif
new file mode 100644
index 0000000..b032d59
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_insertunorderedlist.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_italic.gif b/etherpad/src/static/img/jun09/pad/editbar_italic.gif
new file mode 100644
index 0000000..a017402
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_italic.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_outdent.gif b/etherpad/src/static/img/jun09/pad/editbar_outdent.gif
new file mode 100644
index 0000000..4b9bf38
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_outdent.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_redo.gif b/etherpad/src/static/img/jun09/pad/editbar_redo.gif
new file mode 100644
index 0000000..826a254
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_redo.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_save.gif b/etherpad/src/static/img/jun09/pad/editbar_save.gif
new file mode 100644
index 0000000..2ccced6
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_save.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_strikethrough.gif b/etherpad/src/static/img/jun09/pad/editbar_strikethrough.gif
new file mode 100644
index 0000000..92ffa23
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_strikethrough.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_underline.gif b/etherpad/src/static/img/jun09/pad/editbar_underline.gif
new file mode 100644
index 0000000..ec3cc4e
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_underline.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbar_undo.gif b/etherpad/src/static/img/jun09/pad/editbar_undo.gif
new file mode 100644
index 0000000..78ae0be
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbar_undo.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/editbarback.gif b/etherpad/src/static/img/jun09/pad/editbarback.gif
new file mode 100644
index 0000000..ab51802
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/editbarback.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/feedbackbox2.gif b/etherpad/src/static/img/jun09/pad/feedbackbox2.gif
new file mode 100644
index 0000000..f1b8f5b
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/feedbackbox2.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/fileicons.gif b/etherpad/src/static/img/jun09/pad/fileicons.gif
new file mode 100644
index 0000000..26f6388
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/fileicons.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/hdraggie.gif b/etherpad/src/static/img/jun09/pad/hdraggie.gif
new file mode 100644
index 0000000..0a6fe3e
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/hdraggie.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/icon_import_export.gif b/etherpad/src/static/img/jun09/pad/icon_import_export.gif
new file mode 100644
index 0000000..1b77245
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/icon_import_export.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/icon_pad_options.gif b/etherpad/src/static/img/jun09/pad/icon_pad_options.gif
new file mode 100644
index 0000000..68c79a7
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/icon_pad_options.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/icon_saved_revisions.gif b/etherpad/src/static/img/jun09/pad/icon_saved_revisions.gif
new file mode 100644
index 0000000..8040145
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/icon_saved_revisions.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/icon_security.gif b/etherpad/src/static/img/jun09/pad/icon_security.gif
new file mode 100644
index 0000000..9131fc3
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/icon_security.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/icon_time_slider.gif b/etherpad/src/static/img/jun09/pad/icon_time_slider.gif
new file mode 100644
index 0000000..5e4b4ab
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/icon_time_slider.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/inviteshare.gif b/etherpad/src/static/img/jun09/pad/inviteshare.gif
new file mode 100644
index 0000000..55345e5
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/inviteshare.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/inviteshare2.gif b/etherpad/src/static/img/jun09/pad/inviteshare2.gif
new file mode 100644
index 0000000..98d4c85
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/inviteshare2.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/layoutbuttons.gif b/etherpad/src/static/img/jun09/pad/layoutbuttons.gif
new file mode 100644
index 0000000..ea43432
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/layoutbuttons.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/ok_or_cancel.gif b/etherpad/src/static/img/jun09/pad/ok_or_cancel.gif
new file mode 100644
index 0000000..76ba692
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/ok_or_cancel.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/overlay2.png b/etherpad/src/static/img/jun09/pad/overlay2.png
new file mode 100644
index 0000000..c3d3f1c
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/overlay2.png
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/padtop5.gif b/etherpad/src/static/img/jun09/pad/padtop5.gif
new file mode 100644
index 0000000..e6e071d
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/padtop5.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/padtop5.png b/etherpad/src/static/img/jun09/pad/padtop5.png
new file mode 100644
index 0000000..22a0db7
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/padtop5.png
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/padtop5.xcf b/etherpad/src/static/img/jun09/pad/padtop5.xcf
new file mode 100644
index 0000000..c0bf7e4
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/padtop5.xcf
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/padtopback2.gif b/etherpad/src/static/img/jun09/pad/padtopback2.gif
new file mode 100644
index 0000000..db46567
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/padtopback2.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/public.gif b/etherpad/src/static/img/jun09/pad/public.gif
new file mode 100644
index 0000000..ac3093b
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/public.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/roundcorner_left.gif b/etherpad/src/static/img/jun09/pad/roundcorner_left.gif
new file mode 100644
index 0000000..000de75
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/roundcorner_left.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/roundcorner_right.gif b/etherpad/src/static/img/jun09/pad/roundcorner_right.gif
new file mode 100644
index 0000000..97acfbf
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/roundcorner_right.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/roundcorner_right_orange.gif b/etherpad/src/static/img/jun09/pad/roundcorner_right_orange.gif
new file mode 100644
index 0000000..717e3fc
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/roundcorner_right_orange.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/savedrevarrows.gif b/etherpad/src/static/img/jun09/pad/savedrevarrows.gif
new file mode 100644
index 0000000..2aa2e41
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/savedrevarrows.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/savedrevsgfx2.gif b/etherpad/src/static/img/jun09/pad/savedrevsgfx2.gif
new file mode 100644
index 0000000..45c3459
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/savedrevsgfx2.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/sharebox4.gif b/etherpad/src/static/img/jun09/pad/sharebox4.gif
new file mode 100644
index 0000000..eccaa7e
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/sharebox4.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/sharedistri.gif b/etherpad/src/static/img/jun09/pad/sharedistri.gif
new file mode 100644
index 0000000..8eb5891
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/sharedistri.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/syncdone.gif b/etherpad/src/static/img/jun09/pad/syncdone.gif
new file mode 100644
index 0000000..e4d971b
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/syncdone.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/syncing.gif b/etherpad/src/static/img/jun09/pad/syncing.gif
new file mode 100644
index 0000000..bbc731f
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/syncing.gif
Binary files differ
diff --git a/etherpad/src/static/img/jun09/pad/viewbargfx.gif b/etherpad/src/static/img/jun09/pad/viewbargfx.gif
new file mode 100644
index 0000000..396483a
--- /dev/null
+++ b/etherpad/src/static/img/jun09/pad/viewbargfx.gif
Binary files differ
diff --git a/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-cyan-menu-item-hover.gif b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-cyan-menu-item-hover.gif
new file mode 100644
index 0000000..d0e428e
--- /dev/null
+++ b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-cyan-menu-item-hover.gif
Binary files differ
diff --git a/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-menu-item-hover.gif b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-menu-item-hover.gif
new file mode 100644
index 0000000..8240ba3
--- /dev/null
+++ b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-menu-item-hover.gif
Binary files differ
diff --git a/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-semitransparent-menu-item-hover.png b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-semitransparent-menu-item-hover.png
new file mode 100644
index 0000000..6314d53
--- /dev/null
+++ b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-semitransparent-menu-item-hover.png
Binary files differ
diff --git a/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-human-menu-item-hover.gif b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-human-menu-item-hover.gif
new file mode 100644
index 0000000..7e70aae
--- /dev/null
+++ b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-human-menu-item-hover.gif
Binary files differ
diff --git a/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-osx-menu-item-hover.gif b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-osx-menu-item-hover.gif
new file mode 100644
index 0000000..aa802e0
--- /dev/null
+++ b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-osx-menu-item-hover.gif
Binary files differ
diff --git a/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-bg.gif b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-bg.gif
new file mode 100644
index 0000000..565e771
--- /dev/null
+++ b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-bg.gif
Binary files differ
diff --git a/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-menu-item-hover.gif b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-menu-item-hover.gif
new file mode 100644
index 0000000..2825eb1
--- /dev/null
+++ b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-menu-item-hover.gif
Binary files differ
diff --git a/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-xp-bg.gif b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-xp-bg.gif
new file mode 100644
index 0000000..11b238c
--- /dev/null
+++ b/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-xp-bg.gif
Binary files differ
diff --git a/etherpad/src/static/img/may09/doc.gif b/etherpad/src/static/img/may09/doc.gif
new file mode 100644
index 0000000..2b62080
--- /dev/null
+++ b/etherpad/src/static/img/may09/doc.gif
Binary files differ
diff --git a/etherpad/src/static/img/may09/html.gif b/etherpad/src/static/img/may09/html.gif
new file mode 100644
index 0000000..f7837e5
--- /dev/null
+++ b/etherpad/src/static/img/may09/html.gif
Binary files differ
diff --git a/etherpad/src/static/img/may09/pdf.gif b/etherpad/src/static/img/may09/pdf.gif
new file mode 100644
index 0000000..1614d2c
--- /dev/null
+++ b/etherpad/src/static/img/may09/pdf.gif
Binary files differ
diff --git a/etherpad/src/static/img/may09/txt.gif b/etherpad/src/static/img/may09/txt.gif
new file mode 100644
index 0000000..c3f026e
--- /dev/null
+++ b/etherpad/src/static/img/may09/txt.gif
Binary files differ
diff --git a/etherpad/src/static/img/misc/status-ball.gif b/etherpad/src/static/img/misc/status-ball.gif
new file mode 100644
index 0000000..085ccae
--- /dev/null
+++ b/etherpad/src/static/img/misc/status-ball.gif
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/crushed_button_depressed.png b/etherpad/src/static/img/pad/timeslider/crushed_button_depressed.png
new file mode 100644
index 0000000..d75dcce
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/crushed_button_depressed.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/crushed_button_undepressed.png b/etherpad/src/static/img/pad/timeslider/crushed_button_undepressed.png
new file mode 100644
index 0000000..d86e3f3
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/crushed_button_undepressed.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/crushed_current_location.png b/etherpad/src/static/img/pad/timeslider/crushed_current_location.png
new file mode 100644
index 0000000..76e0835
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/crushed_current_location.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/crushed_timeslider_mockup.png b/etherpad/src/static/img/pad/timeslider/crushed_timeslider_mockup.png
new file mode 100644
index 0000000..f4ccbf1
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/crushed_timeslider_mockup.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/current_location.png b/etherpad/src/static/img/pad/timeslider/current_location.png
new file mode 100644
index 0000000..ab02792
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/current_location.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/pause.png b/etherpad/src/static/img/pad/timeslider/pause.png
new file mode 100644
index 0000000..657782c
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/pause.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/play.png b/etherpad/src/static/img/pad/timeslider/play.png
new file mode 100644
index 0000000..19afe03
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/play.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/play_button.png b/etherpad/src/static/img/pad/timeslider/play_button.png
new file mode 100644
index 0000000..bc1736d
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/play_button.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/star.png b/etherpad/src/static/img/pad/timeslider/star.png
new file mode 100644
index 0000000..e0c7099
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/star.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/star_selected.png b/etherpad/src/static/img/pad/timeslider/star_selected.png
new file mode 100644
index 0000000..c336589
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/star_selected.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/stepper_buttons.png b/etherpad/src/static/img/pad/timeslider/stepper_buttons.png
new file mode 100644
index 0000000..e011a45
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/stepper_buttons.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/timeslider_background.png b/etherpad/src/static/img/pad/timeslider/timeslider_background.png
new file mode 100644
index 0000000..faa45c6
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/timeslider_background.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/timeslider_left.png b/etherpad/src/static/img/pad/timeslider/timeslider_left.png
new file mode 100644
index 0000000..594d86b
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/timeslider_left.png
Binary files differ
diff --git a/etherpad/src/static/img/pad/timeslider/timeslider_right.png b/etherpad/src/static/img/pad/timeslider/timeslider_right.png
new file mode 100644
index 0000000..3bf10a2
--- /dev/null
+++ b/etherpad/src/static/img/pad/timeslider/timeslider_right.png
Binary files differ
diff --git a/etherpad/src/static/img/pro/box/blue-boxtop.gif b/etherpad/src/static/img/pro/box/blue-boxtop.gif
new file mode 100644
index 0000000..38e3538
--- /dev/null
+++ b/etherpad/src/static/img/pro/box/blue-boxtop.gif
Binary files differ
diff --git a/etherpad/src/static/img/pro/buttons/bluebutton120.gif b/etherpad/src/static/img/pro/buttons/bluebutton120.gif
new file mode 100644
index 0000000..2f22003
--- /dev/null
+++ b/etherpad/src/static/img/pro/buttons/bluebutton120.gif
Binary files differ
diff --git a/etherpad/src/static/img/pro/header/pro-header-logo.png b/etherpad/src/static/img/pro/header/pro-header-logo.png
new file mode 100644
index 0000000..b36daa8
--- /dev/null
+++ b/etherpad/src/static/img/pro/header/pro-header-logo.png
Binary files differ
diff --git a/etherpad/src/static/img/pro/header/pro-header-plustopnav-back.gif b/etherpad/src/static/img/pro/header/pro-header-plustopnav-back.gif
new file mode 100644
index 0000000..f7398fe
--- /dev/null
+++ b/etherpad/src/static/img/pro/header/pro-header-plustopnav-back.gif
Binary files differ
diff --git a/etherpad/src/static/img/pro/padlist/gear-drop.gif b/etherpad/src/static/img/pro/padlist/gear-drop.gif
new file mode 100644
index 0000000..ded0f24
--- /dev/null
+++ b/etherpad/src/static/img/pro/padlist/gear-drop.gif
Binary files differ
diff --git a/etherpad/src/static/img/pro/padlist/paper-icon.gif b/etherpad/src/static/img/pro/padlist/paper-icon.gif
new file mode 100644
index 0000000..161b66e
--- /dev/null
+++ b/etherpad/src/static/img/pro/padlist/paper-icon.gif
Binary files differ
diff --git a/etherpad/src/static/img/pro/padlist/trash-icon.gif b/etherpad/src/static/img/pro/padlist/trash-icon.gif
new file mode 100644
index 0000000..74b5ede
--- /dev/null
+++ b/etherpad/src/static/img/pro/padlist/trash-icon.gif
Binary files differ
diff --git a/etherpad/src/static/img/pro/topnav/pro-topnav-back.gif b/etherpad/src/static/img/pro/topnav/pro-topnav-back.gif
new file mode 100644
index 0000000..336fd05
--- /dev/null
+++ b/etherpad/src/static/img/pro/topnav/pro-topnav-back.gif
Binary files differ
diff --git a/etherpad/src/static/img/pro/topnav/pro-topnav-notch.gif b/etherpad/src/static/img/pro/topnav/pro-topnav-notch.gif
new file mode 100644
index 0000000..5dbe57b
--- /dev/null
+++ b/etherpad/src/static/img/pro/topnav/pro-topnav-notch.gif
Binary files differ
diff --git a/etherpad/src/static/js/billing.js b/etherpad/src/static/js/billing.js
new file mode 100644
index 0000000..c9fa30e
--- /dev/null
+++ b/etherpad/src/static/js/billing.js
@@ -0,0 +1,111 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+$(function() {
+ billing.initFieldDisplay();
+ billing.initCcValidation();
+});
+
+billing.initFieldDisplay = function() {
+ var id = $('#billingselect input:checked').attr("value");
+ $('.billingfield').not('.billingfield.'+id+'req').hide();
+ $('.paymentbutton').click(billing.selectPaymentType);
+
+ $('#billingCountry').click(billing.selectCountry);
+ billing.selectCountry();
+}
+
+billing.selectCountry = function() {
+ var countryCode = $('#billingCountry').attr("value");
+ var id = $('#billingselect input:checked').attr("value");
+ if (countryCode != 'US') {
+ $('.billingfield.intonly.'+id+'req').show();
+ $('.billingfield.usonly').hide();
+ } else {
+ $('.billingfield.intonly').hide();
+ $('.billingfield.usonly.'+id+'req').show();
+ }
+}
+
+billing.countryAntiSelector = function() {
+ var countryCode = $('#billingCountry').attr("value");
+ if (countryCode != 'US') {
+ return '.usonly';
+ } else {
+ return '.intonly';
+ }
+}
+
+billing.selectPaymentType = function() {
+ var radio = $(this).children('input');
+ var id = radio.attr("value");
+ radio.attr("checked", "checked");
+
+ var selector = billing.countryAntiSelector();
+ var toShow = $('.billingfield.'+id+'req:hidden').not('.billingfield'+selector);
+ var toHide = $('.billingfield:visible').not('.billingfield.'+id+'req');
+
+ if (toShow.size() > 0 && toHide.size() > 0) {
+ toHide.fadeOut(200);
+ setTimeout(function() {
+ toShow.fadeIn(200);
+ }, 200);
+ } else if (toShow.size() > 0 || toHide.size() > 0){
+ toShow.fadeIn(200);
+ toHide.fadeOut(200);
+ }
+}
+
+billing.extractCcType = function(numsrc) {
+ var number = $(numsrc).val();
+ var newType = billing.getCcType(number);
+ $('.ccimage').removeClass('ccimageselected');
+ if (newType) {
+ $('#img'+newType).addClass('ccimageselected');
+ }
+ if (billing.validateCcNumber(number)) {
+ $('input[name=billingCCNumber]').css('border', '1px solid #0f0');
+ } else if (billing.validateCcLength(number) ||
+ ! (/^\d*$/.test(number))) {
+ $('input[name=billingCCNumber]').css('border', '1px solid #f00');
+ } else {
+ $('input[name=billingCCNumber]').css('border', '1px solid black');
+ }
+}
+
+billing.handleCcFieldChange = function(target, event) {
+ if (event &&
+ ! (event.keyCode == 8 ||
+ (event.keyCode >= 32 && event.keyCode <= 126))) {
+ return;
+ }
+ var ccValue = $(target).val();
+ if (ccValue == billing.lastCcValue) {
+ return;
+ }
+ billing.lastCcValue = ccValue;
+ setTimeout(function() {
+ billing.extractCcType(target);
+ }, 0);
+}
+
+billing.initCcValidation = function() {
+ $('input[name=billingCCNumber]').keydown(
+ function(event) { billing.handleCcFieldChange(this, event); });
+ $('input[name=billingCCNumber]').blur(
+ function() { billing.handleCcFieldChange(this) });
+ billing.lastCcValue = $('input[name=billingCCNumber]').val();
+} \ No newline at end of file
diff --git a/etherpad/src/static/js/billing_shared.js b/etherpad/src/static/js/billing_shared.js
new file mode 100644
index 0000000..dc3a00c
--- /dev/null
+++ b/etherpad/src/static/js/billing_shared.js
@@ -0,0 +1,94 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var billing = {};
+
+billing.CC = function(shortName, prefixes, length) {
+ this.type = shortName;
+ this.prefixes = prefixes;
+ this.length = length;
+ function validateLuhn(number) {
+ var digits = [];
+ var sum = 0;
+ for (var i = 0; i < number.length; ++i) {
+ var c = Number(number.charAt(number.length-1-i));
+ sum += c;
+ if (i % 2 == 1) { // every second digit
+ sum += c;
+ if (2*c >= 10) {
+ sum -= 9;
+ }
+ }
+ }
+ return (sum % 10 == 0);
+ }
+ this.validatePrefix = function(number) {
+ for (var i = 0; i < this.prefixes.length; ++i) {
+ if (number.indexOf(String(this.prefixes[i])) == 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+ this.validateLength = function(number) {
+ return number.length == this.length;
+ }
+
+ this.validateNumber = function(number) {
+ return this.validateLength(number) &&
+ this.validatePrefix(number) &&
+ validateLuhn(number);
+ }
+}
+
+billing.ccTypes = [
+ new billing.CC('amex', [34, 37], 15),
+ new billing.CC('disc', [6011, 644, 645, 646, 647, 648, 649, 65], 16),
+ new billing.CC('mc', [51, 52, 53, 54, 55], 16),
+ new billing.CC('visa', [4], 16)];
+
+billing.validateCcNumber = function(number) {
+ if (! (/^\d+$/.test(number))) {
+ return false;
+ }
+ for (var i = 0; i < billing.ccTypes.length; ++i) {
+ var ccType = billing.ccTypes[i];
+ if (ccType.validatePrefix(number)) {
+ return ccType.validateNumber(number);
+ }
+ }
+ return false;
+}
+
+billing.validateCcLength = function(number) {
+ for (var i = 0; i < billing.ccTypes.length; ++i) {
+ var ccType = billing.ccTypes[i];
+ if (ccType.validatePrefix(number)) {
+ return ccType.validateLength(number);
+ }
+ }
+ return false;
+}
+
+billing.getCcType = function(number) {
+ for (var i = 0; i < billing.ccTypes.length; ++i) {
+ var ccType = billing.ccTypes[i];
+ if (ccType.validatePrefix(number)) {
+ return ccType.type;
+ }
+ }
+ return false;
+}
diff --git a/etherpad/src/static/js/broadcast.js b/etherpad/src/static/js/broadcast.js
new file mode 100644
index 0000000..8ea0a15
--- /dev/null
+++ b/etherpad/src/static/js/broadcast.js
@@ -0,0 +1,610 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// just in case... (todo: this must be somewhere else in the client code.)
+// Below Array#map code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_map.htm
+if (!Array.prototype.map)
+{
+ Array.prototype.map = function(fun /*, thisp*/)
+ {
+ var len = this.length >>> 0;
+ if (typeof fun != "function")
+ throw new TypeError();
+
+ var res = new Array(len);
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++)
+ {
+ if (i in this)
+ res[i] = fun.call(thisp, this[i], i, this);
+ }
+
+ return res;
+ };
+}
+
+// Below Array#forEach code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_foreach.htm
+if (!Array.prototype.forEach)
+{
+ Array.prototype.forEach = function(fun /*, thisp*/)
+ {
+ var len = this.length >>> 0;
+ if (typeof fun != "function")
+ throw new TypeError();
+
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++)
+ {
+ if (i in this)
+ fun.call(thisp, this[i], i, this);
+ }
+ };
+}
+
+// Below Array#indexOf code was direct pasted by AppJet/Etherpad, licence unknown. Possible source: http://www.tutorialspoint.com/javascript/array_indexof.htm
+if (!Array.prototype.indexOf)
+{
+ Array.prototype.indexOf = function(elt /*, from*/)
+ {
+ var len = this.length >>> 0;
+
+ var from = Number(arguments[1]) || 0;
+ from = (from < 0)
+ ? Math.ceil(from)
+ : Math.floor(from);
+ if (from < 0)
+ from += len;
+
+ for (; from < len; from++)
+ {
+ if (from in this &&
+ this[from] === elt)
+ return from;
+ }
+ return -1;
+ };
+}
+
+function debugLog() {
+ try {
+ // console.log.apply(console, arguments);
+ } catch (e) {console.log("error printing: ",e);}
+}
+
+function randomString() {
+ return "_"+Math.floor(Math.random() * 1000000);
+}
+
+// for IE
+if ($.browser.msie) {
+ try {
+ document.execCommand("BackgroundImageCache", false, true);
+ } catch (e) {}
+}
+
+var userId = "hiddenUser" + randomString();
+var socketId;
+var socket;
+
+var channelState = "DISCONNECTED";
+
+var appLevelDisconnectReason = null;
+
+var padContents = {
+ currentRevision: clientVars.revNum,
+ currentTime : clientVars.currentTime,
+ currentLines: Changeset.splitTextLines(clientVars.initialStyledContents.atext.text),
+ currentDivs : null, // to be filled in once the dom loads
+ apool: (new AttribPool()).fromJsonable(clientVars.initialStyledContents.apool),
+ alines: Changeset.splitAttributionLines(
+ clientVars.initialStyledContents.atext.attribs,
+ clientVars.initialStyledContents.atext.text),
+
+ // generates a jquery element containing HTML for a line
+ lineToElement: function(line, aline) {
+ var element = document.createElement("div");
+ var emptyLine = (line == '\n');
+ var domInfo = domline.createDomLine(! emptyLine, true);
+ linestylefilter.populateDomLine(line, aline, this.apool,
+ domInfo);
+ domInfo.prepareForAdd();
+ element.className = domInfo.node.className;
+ element.innerHTML = domInfo.node.innerHTML;
+ element.id = Math.random();
+ return $(element);
+ },
+
+ applySpliceToDivs: function(start, numRemoved, newLines) {
+ // remove spliced-out lines from DOM
+ for(var i=start; i<start+numRemoved && i<this.currentDivs.length; i++) {
+ debugLog("removing", this.currentDivs[i].attr('id'));
+ this.currentDivs[i].remove();
+ }
+
+ // remove spliced-out line divs from currentDivs array
+ this.currentDivs.splice(start, numRemoved);
+
+ var newDivs = [];
+ for(var i=0;i<newLines.length;i++) {
+ newDivs.push(this.lineToElement(newLines[i],
+ this.alines[start+i]));
+ }
+
+ // grab the div just before the first one
+ var startDiv = this.currentDivs[start-1] || null;
+
+ // insert the div elements into the correct place, in the correct order
+ for(var i=0; i<newDivs.length; i++) {
+ if (startDiv) {
+ startDiv.after(newDivs[i]);
+ }
+ else {
+ $("#padcontent").prepend(newDivs[i]);
+ }
+ startDiv = newDivs[i];
+ }
+
+ // insert new divs into currentDivs array
+ newDivs.unshift(0); // remove 0 elements
+ newDivs.unshift(start);
+ this.currentDivs.splice.apply(this.currentDivs, newDivs);
+ return this;
+ },
+
+ // splice the lines
+ splice: function(start, numRemoved, newLinesVA) {
+ var newLines = Array.prototype.slice.call(arguments, 2).map(
+ function(s) { return s; });
+
+ // apply this splice to the divs
+ this.applySpliceToDivs(start, numRemoved, newLines);
+
+ // call currentLines.splice, to keep the currentLines array up to date
+ newLines.unshift(numRemoved);
+ newLines.unshift(start);
+ this.currentLines.splice.apply(this.currentLines, arguments);
+ },
+ // returns the contents of the specified line I
+ get: function(i) {
+ return this.currentLines[i];
+ },
+ // returns the number of lines in the document
+ length: function() {
+ return this.currentLines.length;
+ },
+
+ getActiveAuthors: function() {
+ var self = this;
+ var authors = [];
+ var seenNums = {};
+ var alines = self.alines;
+ for(var i=0;i<alines.length;i++) {
+ Changeset.eachAttribNumber(alines[i], function(n) {
+ if (! seenNums[n]) {
+ seenNums[n] = true;
+ if (self.apool.getAttribKey(n) == 'author') {
+ var a = self.apool.getAttribValue(n);
+ if (a) {
+ authors.push(a);
+ }
+ }
+ }
+ });
+ }
+ authors.sort();
+ return authors;
+ }
+};
+
+function callCatchingErrors(catcher, func) {
+ try {
+ wrapRecordingErrors(catcher, func)();
+ }
+ catch (e) { /*absorb*/ }
+}
+
+function wrapRecordingErrors(catcher, func) {
+ return function() {
+ try {
+ return func.apply(this, Array.prototype.slice.call(arguments));
+ }
+ catch (e) {
+ // caughtErrors.push(e);
+ // caughtErrorCatchers.push(catcher);
+ // caughtErrorTimes.push(+new Date());
+ // console.dir({catcher: catcher, e: e});
+ debugLog(e); // TODO(kroo): added temporary, to catch errors
+ throw e;
+ }
+ };
+}
+
+function loadedNewChangeset(changesetForward, changesetBackward, revision, timeDelta) {
+ var broadcasting = (BroadcastSlider.getSliderPosition() == revisionInfo.latest);
+ debugLog("broadcasting:", broadcasting, BroadcastSlider.getSliderPosition(), revisionInfo.latest, revision);
+ revisionInfo.addChangeset(revision, revision+1, changesetForward, changesetBackward, timeDelta);
+ BroadcastSlider.setSliderLength(revisionInfo.latest);
+ if(broadcasting)
+ applyChangeset(changesetForward, revision+1, false, timeDelta);
+}
+
+/*
+ At this point, we must be certain that the changeset really does map from
+ the current revision to the specified revision. Any mistakes here will
+ cause the whole slider to get out of sync.
+ */
+function applyChangeset(changeset, revision, preventSliderMovement, timeDelta) {
+ // disable the next 'gotorevision' call handled by a timeslider update
+ if(!preventSliderMovement) {
+ goToRevisionIfEnabledCount ++;
+ BroadcastSlider.setSliderPosition(revision);
+ }
+
+ try {
+ // must mutate attribution lines before text lines
+ Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
+ }catch(e) { debugLog(e); }
+
+ Changeset.mutateTextLines(changeset, padContents);
+ padContents.currentRevision = revision;
+ padContents.currentTime += timeDelta * 1000;
+ debugLog('Time Delta: ',timeDelta)
+ updateTimer();
+ BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) {return authorData[name];}));
+}
+
+function updateTimer() {
+ var zpad = function(str, length) {
+ str = str+"";
+ while(str.length < length)
+ str = '0'+str;
+ return str;
+ }
+ var date = new Date(padContents.currentTime);
+ var dateFormat = function() {
+ var month = zpad(date.getMonth()+1,2);
+ var day = zpad(date.getDate(),2);
+ var year = (date.getFullYear());
+ var hours = zpad(date.getHours(),2);
+ var minutes = zpad(date.getMinutes(),2);
+ var seconds = zpad(date.getSeconds(),2);
+ return ([month,'/',day,'/',year,' ',hours,':',minutes,':',seconds].join(""));
+ }
+
+ $('#timer').html(dateFormat());
+
+ var revisionDate = [
+ "Saved",
+ [
+ "Jan", "Feb", "March", "April", "May", "June",
+ "July", "Aug", "Sept", "Oct", "Nov", "Dec"
+ ][date.getMonth()],
+ date.getDate()+",",
+ date.getFullYear()
+ ].join(" ")
+ $('#revision_date').html(revisionDate)
+
+}
+
+function goToRevision(newRevision) {
+ padContents.targetRevision = newRevision;
+ var self = this;
+ var path = revisionInfo.getPath(padContents.currentRevision, newRevision);
+ debugLog('newRev: ', padContents.currentRevision, path);
+ if( path.status == 'complete') {
+ var cs = path.changesets;
+ debugLog("status: complete, changesets: ",cs, "path:", path);
+ var changeset = cs[0];
+ var timeDelta = path.times[0];
+ for(var i=1; i<cs.length; i++) {
+ changeset = Changeset.compose(changeset, cs[i], padContents.apool);
+ timeDelta += path.times[i];
+ }
+ if(changeset)
+ applyChangeset(changeset, path.rev, true, timeDelta);
+ } else if(path.status == "partial") {
+ debugLog('partial');
+ var sliderLocation = padContents.currentRevision;
+ // callback is called after changeset information is pulled from server
+ // this may never get called, if the changeset has already been loaded
+ var update = function(start, end) {
+ // if we've called goToRevision in the time since, don't goToRevision
+ goToRevision(padContents.targetRevision);
+ };
+
+ // do our best with what we have...
+ var cs = path.changesets;
+
+ var changeset = cs[0];
+ var timeDelta = path.times[0];
+ for(var i=1; i<cs.length; i++) {
+ changeset = Changeset.compose(changeset, cs[i], padContents.apool);
+ timeDelta += path.times[i];
+ }
+ if(changeset)
+ applyChangeset(changeset, path.rev, true, timeDelta);
+
+
+ if(BroadcastSlider.getSliderLength() > 10000) {
+ var start = (Math.floor((newRevision) / 10000) * 10000); // revision 0 to 10
+ changesetLoader.queueUp(start, 100);
+ }
+
+ if(BroadcastSlider.getSliderLength() > 1000) {
+ var start = (Math.floor((newRevision) / 1000) * 1000); // (start from -1, go to 19) + 1
+ changesetLoader.queueUp(start, 10);
+ }
+
+ start = (Math.floor((newRevision) / 100) * 100);
+
+ changesetLoader.queueUp(start, 1, update);
+ }
+ BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) {return authorData[name];}));
+}
+
+var changesetLoader = {
+ running: false,
+ resolved: [],
+ requestQueue1: [],
+ requestQueue2: [],
+ requestQueue3: [],
+ queueUp: function(revision, width, callback) {
+ if(revision < 0) revision = 0;
+ // if(changesetLoader.requestQueue.indexOf(revision) != -1)
+ // return; // already in the queue.
+ if(changesetLoader.resolved.indexOf(revision+"_"+width) != -1)
+ return; // already loaded from the server
+ changesetLoader.resolved.push(revision+"_"+width);
+
+ var requestQueue = width == 1 ? changesetLoader.requestQueue3 :
+ width == 10 ? changesetLoader.requestQueue2 :
+ changesetLoader.requestQueue1;
+ requestQueue.push({'rev': revision, 'res': width, 'callback': callback});
+ if(!changesetLoader.running) {
+ changesetLoader.running = true;
+ setTimeout(changesetLoader.loadFromQueue, 10);
+ }
+ },
+ loadFromQueue: function() {
+ var self = changesetLoader;
+ var requestQueue = self.requestQueue1.length > 0 ? self.requestQueue1 :
+ self.requestQueue2.length > 0 ? self.requestQueue2 :
+ self.requestQueue3.length > 0 ? self.requestQueue3 : null;
+
+ if(!requestQueue) {
+ self.running = false;
+ return;
+ }
+
+ var request = requestQueue.pop();
+ var granularity = request.res;
+ var callback = request.callback;
+ var start = request.rev;
+ debugLog("loadinging revision", start, "through ajax");
+ $.getJSON(
+ "/ep/pad/changes/"+clientVars.padIdForUrl+"?s="+start + "&g="+granularity,
+ function(data, textStatus) {
+ if(textStatus !== "success") {
+ console.log(textStatus);
+ BroadcastSlider.showReconnectUI();
+ }
+ self.handleResponse(data, start, granularity, callback);
+
+ setTimeout(self.loadFromQueue, 10); // load the next ajax function
+ }
+ );
+ },
+ handleResponse: function(data, start, granularity, callback) {
+ debugLog("response: ", data);
+ var pool = (new AttribPool()).fromJsonable(data.apool);
+ for(var i=0; i<data.forwardsChangesets.length; i++) {
+ var astart = start + i * granularity - 1; // rev -1 is a blank single line
+ var aend = start + (i+1) * granularity - 1;// totalRevs is the most recent revision
+ if(aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
+ debugLog("adding changeset:", astart, aend);
+ var forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
+ var backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
+ revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
+ }
+ if(callback)callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
+ }
+};
+
+function handleMessageFromServer() {
+ debugLog("handleMessage:", arguments);
+ var obj = arguments[0]['data'];
+ var expectedType = "COLLABROOM";
+
+ obj = JSON.parse(obj);
+ if (obj['type'] == expectedType) {
+ obj = obj['data'];
+
+ if (obj['type'] == "NEW_CHANGES") {
+ debugLog(obj);
+ var changeset = Changeset.moveOpsToNewPool(
+ obj.changeset, (new AttribPool()).fromJsonable(obj.apool),
+ padContents.apool);
+
+ var changesetBack = Changeset.moveOpsToNewPool(
+ obj.changesetBack, (new AttribPool()).fromJsonable(obj.apool),
+ padContents.apool);
+
+ loadedNewChangeset(changeset, changesetBack, obj.newRev-1, obj.timeDelta);
+ }
+ else if (obj['type'] == "NEW_AUTHORDATA") {
+ var authorMap = {};
+ authorMap[obj.author] = obj.data;
+ receiveAuthorData(authorMap);
+ BroadcastSlider.setAuthors(padContents.getActiveAuthors().map(function(name) {return authorData[name];}));
+ } else if (obj['type'] == "NEW_SAVEDREV") {
+ var savedRev = obj.savedRev;
+ BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
+ }
+ } else {
+ debugLog("incorrect message type: " + obj['type'] + ", expected " + expectedType);
+ }
+}
+
+function handleSocketClosed(params) {
+ debugLog("socket closed!", params);
+ socket = null;
+
+ BroadcastSlider.showReconnectUI();
+ // var reason = appLevelDisconnectReason || params.reason;
+ // var shouldReconnect = params.reconnect;
+ // if (shouldReconnect) {
+ // // determine if this is a tight reconnect loop due to weird connectivity problems
+ // // reconnectTimes.push(+new Date());
+ // var TOO_MANY_RECONNECTS = 8;
+ // var TOO_SHORT_A_TIME_MS = 10000;
+ // if (reconnectTimes.length >= TOO_MANY_RECONNECTS &&
+ // ((+new Date()) - reconnectTimes[reconnectTimes.length-TOO_MANY_RECONNECTS]) <
+ // TOO_SHORT_A_TIME_MS) {
+ // setChannelState("DISCONNECTED", "looping");
+ // }
+ // else {
+ // setChannelState("RECONNECTING", reason);
+ // setUpSocket();
+ // }
+ // }
+ // else {
+ // BroadcastSlider.showReconnectUI();
+ // setChannelState("DISCONNECTED", reason);
+ // }
+}
+
+function sendMessage(msg) {
+ socket.postMessage(JSON.stringify({type: "COLLABROOM", data: msg}));
+}
+
+function setUpSocket() {
+ // required for Comet
+ if ((! $.browser.msie) &&
+ (! ($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) {
+ document.domain = document.domain; // for comet
+ }
+
+ var success = false;
+ callCatchingErrors("setUpSocket", function() {
+ appLevelDisconnectReason = null;
+
+ socketId = String(Math.floor(Math.random()*1e12));
+ socket = new WebSocket(socketId);
+ socket.onmessage = wrapRecordingErrors("socket.onmessage", handleMessageFromServer);
+ socket.onclosed = wrapRecordingErrors("socket.onclosed", handleSocketClosed);
+ socket.onopen = wrapRecordingErrors("socket.onopen", function() {
+ setChannelState("CONNECTED");
+ var msg = { type:"CLIENT_READY", roomType:'padview',
+ roomName:'padview/'+clientVars.viewId,
+ data: { lastRev:clientVars.revNum,
+ userInfo:{userId: userId} } };
+ sendMessage(msg);
+ });
+ // socket.onhiccup = wrapRecordingErrors("socket.onhiccup", handleCometHiccup);
+ // socket.onlogmessage = function(x) {debugLog(x); };
+ socket.connect();
+ success = true;
+ });
+ if (success) {
+ //initialStartConnectTime = +new Date();
+ }
+ else {
+ abandonConnection("initsocketfail");
+ }
+}
+
+function setChannelState(newChannelState, moreInfo) {
+ if (newChannelState != channelState) {
+ channelState = newChannelState;
+ // callbacks.onChannelStateChange(channelState, moreInfo);
+ }
+}
+
+function abandonConnection(reason) {
+ if (socket) {
+ socket.onclosed = function() {};
+ socket.onhiccup = function() {};
+ socket.disconnect();
+ }
+ socket = null;
+ setChannelState("DISCONNECTED", reason);
+}
+
+window['onloadFuncts'] = [];
+window.onload = function() {
+ window['isloaded'] = true;
+ window['onloadFuncts'].forEach(function(funct) {
+ funct();
+ });
+};
+
+// to start upon window load, just push a function onto this array
+window['onloadFuncts'].push(setUpSocket);
+window['onloadFuncts'].push(function() {
+ // set up the currentDivs and DOM
+ padContents.currentDivs = [];
+ $("#padcontent").html("");
+ for(var i=0; i<padContents.currentLines.length; i++) {
+ var div = padContents.lineToElement(padContents.currentLines[i],
+ padContents.alines[i]);
+ padContents.currentDivs.push(div);
+ $("#padcontent").append(div);
+ }
+ debugLog(padContents.currentDivs);
+});
+
+// this is necessary to keep infinite loops of events firing,
+// since goToRevision changes the slider position
+var goToRevisionIfEnabledCount = 0;
+var goToRevisionIfEnabled = function() {
+ if(goToRevisionIfEnabledCount > 0) {
+ goToRevisionIfEnabledCount --;
+ } else {
+ goToRevision.apply(goToRevision, arguments);
+ }
+}
+
+BroadcastSlider.onSlider(goToRevisionIfEnabled);
+
+(function() {
+ for(var i=0; i<clientVars.initialChangesets.length; i++) {
+ var csgroup = clientVars.initialChangesets[i];
+ var start = clientVars.initialChangesets[i].start;
+ var granularity = clientVars.initialChangesets[i].granularity;
+ debugLog("loading changest on startup: ", start, granularity, csgroup);
+ changesetLoader.handleResponse(csgroup, start, granularity, null);
+ }
+})();
+
+var dynamicCSS = makeCSSManager('dynamicsyntax');
+var authorData = {};
+
+function receiveAuthorData(newAuthorData) {
+ for(var author in newAuthorData) {
+ var data = newAuthorData[author];
+ if ((typeof data.colorId) == 'number') {
+ var bgcolor = clientVars.colorPalette[data.colorId];
+ if (bgcolor && dynamicCSS) {
+ dynamicCSS.selectorStyle(
+ '.'+linestylefilter.getAuthorClassName(author)).backgroundColor =
+ bgcolor;
+ }
+ }
+ authorData[author] = data;
+ }
+}
+
+receiveAuthorData(clientVars.historicalAuthorData);
diff --git a/etherpad/src/static/js/broadcast_revisions.js b/etherpad/src/static/js/broadcast_revisions.js
new file mode 100644
index 0000000..7e99003
--- /dev/null
+++ b/etherpad/src/static/js/broadcast_revisions.js
@@ -0,0 +1,119 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// revision info is a skip list whos entries represent a particular revision
+// of the document. These revisions are connected together by various
+// changesets, or deltas, between any two revisions.
+function Revision(revNum) {
+ this.rev = revNum;
+ this.changesets = [];
+}
+
+Revision.prototype.addChangeset = function(destIndex, changeset, timeDelta) {
+ var changesetWrapper = {
+ deltaRev: destIndex-this.rev,
+ deltaTime: timeDelta,
+ getValue: function() {
+ return changeset;
+ }
+ };
+ this.changesets.push(changesetWrapper);
+ this.changesets.sort(function(a, b) {
+ return (b.deltaRev - a.deltaRev)
+ });
+}
+
+revisionInfo = {};
+revisionInfo.addChangeset = function(fromIndex, toIndex, changeset, backChangeset, timeDelta) {
+ var startRevision = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex);
+ var endRevision = revisionInfo[toIndex] || revisionInfo.createNew(toIndex);
+ startRevision.addChangeset(toIndex, changeset, timeDelta);
+ endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta);
+}
+
+revisionInfo.latest = clientVars.totalRevs || -1;
+
+revisionInfo.createNew = function(index) {
+ revisionInfo[index] = new Revision(index);
+ if(index > revisionInfo.latest) {
+ revisionInfo.latest = index;
+ }
+
+ return revisionInfo[index];
+}
+
+// assuming that there is a path from fromIndex to toIndex, and that the links
+// are laid out in a skip-list format
+revisionInfo.getPath = function(fromIndex, toIndex) {
+ var changesets = [];
+ var spans = [];
+ var times = [];
+ var elem = revisionInfo[fromIndex] || revisionInfo.createNew(fromIndex);
+ if(elem.changesets.length != 0 && fromIndex != toIndex) {
+ var reverse = !(fromIndex < toIndex)
+ while(((elem.rev < toIndex) && !reverse) ||
+ ((elem.rev > toIndex) && reverse)) {
+ var couldNotContinue = false;
+ var oldRev = elem.rev;
+
+ for(var i = reverse ? elem.changesets.length - 1 : 0;
+ reverse?i>=0:i<elem.changesets.length;
+ i += reverse ? -1 : 1) {
+ if(((elem.changesets[i].deltaRev < 0) && !reverse) ||
+ ((elem.changesets[i].deltaRev > 0) && reverse)) {
+ couldNotContinue = true;
+ break;
+ }
+
+ if(((elem.rev + elem.changesets[i].deltaRev <= toIndex) && !reverse) ||
+ ((elem.rev + elem.changesets[i].deltaRev >= toIndex) && reverse)) {
+ var topush = elem.changesets[i];
+ changesets.push(topush.getValue());
+ spans.push(elem.changesets[i].deltaRev);
+ times.push(topush.deltaTime);
+ elem = revisionInfo[elem.rev + elem.changesets[i].deltaRev];
+ break;
+ }
+ }
+
+ if(couldNotContinue || oldRev == elem.rev) break;
+ }
+ }
+
+ var status = 'partial';
+ if(elem.rev == toIndex)
+ status = 'complete';
+
+ return {
+ 'fromRev':fromIndex,
+ 'rev': elem.rev,
+ 'status': status,
+ 'changesets': changesets,
+ 'spans' : spans,
+ 'times' : times
+ };
+}
+
+// revisionInfo.addChangeset(0, 5, "abcde")
+// revisionInfo.addChangeset(5, 10, "fghij")
+// revisionInfo.addChangeset(10, 11, "k")
+// revisionInfo.addChangeset(11, 12, "l")
+// revisionInfo.addChangeset(12, 13, "m")
+// revisionInfo.addChangeset(13, 14, "n")
+// revisionInfo.addChangeset(14, 15, "o")
+// revisionInfo.addChangeset(15, 20, "pqrst")
+//
+// print (revisionInfo.getPath(15, 0))
diff --git a/etherpad/src/static/js/broadcast_slider.js b/etherpad/src/static/js/broadcast_slider.js
new file mode 100644
index 0000000..371663e
--- /dev/null
+++ b/etherpad/src/static/js/broadcast_slider.js
@@ -0,0 +1,401 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var global = this;
+
+(function() { // wrap this code in its own namespace
+ var sliderLength = 1000;
+ var sliderPos = 0;
+ var sliderActive = false;
+ var slidercallbacks = [];
+ var savedRevisions = [];
+ var sliderPlaying = false;
+
+ function disableSelection(element) {
+ element.onselectstart = function() {
+ return false;
+ };
+ element.unselectable = "on";
+ element.style.MozUserSelect = "none";
+ element.style.cursor = "default";
+ }
+ var _callSliderCallbacks = function(newval) {
+ sliderPos = newval;
+ for(var i=0; i<slidercallbacks.length; i++) {
+ slidercallbacks[i](newval);
+ }
+ }
+
+ var updateSliderElements = function() {
+ for(var i=0; i<savedRevisions.length; i++) {
+ var position = parseInt(savedRevisions[i].attr('pos'));
+ savedRevisions[i].css('left', (position * ($("#ui-slider-bar").width()-2) / (sliderLength * 1.0)) - 1);
+ }
+ $("#ui-slider-handle").css('left', sliderPos * ($("#ui-slider-bar").width()-2) / (sliderLength * 1.0));
+ }
+
+ var addSavedRevision = function(position, info) {
+ var newSavedRevision = $('<div></div>');
+ newSavedRevision.addClass("star");
+
+ newSavedRevision.attr('pos', position);
+ newSavedRevision.css('position', 'absolute');
+ newSavedRevision.css('left', (position * ($("#ui-slider-bar").width()-2) / (sliderLength * 1.0)) - 1);
+ $("#timeslider-slider").append(newSavedRevision);
+ newSavedRevision.mouseup(function(evt) {
+ BroadcastSlider.setSliderPosition(position);
+ });
+ savedRevisions.push(newSavedRevision);
+ };
+
+ var removeSavedRevision = function (position) {
+ var element = $("div.star [pos="+position+"]");
+ savedRevisions.remove(element);
+ element.remove();
+ return element;
+ };
+
+ /* Begin small 'API' */
+ function onSlider(callback) {
+ slidercallbacks.push(callback);
+ }
+
+ function getSliderPosition() {
+ return sliderPos;
+ }
+
+ function setSliderPosition(newpos) {
+ newpos = Number(newpos);
+ if(newpos < 0 || newpos > sliderLength) return;
+ $("#ui-slider-handle").css('left', newpos * ($("#ui-slider-bar").width()-2) / (sliderLength * 1.0));
+ $("a.tlink").map(function() {
+ $(this).attr('href', $(this).attr('thref').replace("%revision%", newpos));
+ });
+ $("#revision_label").html("Version " + newpos);
+
+ if(newpos == 0) {
+ $("#leftstar").css('opacity', .5);
+ $("#leftstep").css('opacity', .5);
+ } else {
+ $("#leftstar").css('opacity', 1);
+ $("#leftstep").css('opacity', 1);
+ }
+
+ if(newpos == sliderLength) {
+ $("#rightstar").css('opacity', .5);
+ $("#rightstep").css('opacity', .5);
+ } else {
+ $("#rightstar").css('opacity', 1);
+ $("#rightstep").css('opacity', 1);
+ }
+
+ sliderPos = newpos;
+ _callSliderCallbacks(newpos);
+ }
+
+ function getSliderLength() {
+ return sliderLength;
+ }
+
+ function setSliderLength(newlength) {
+ sliderLength = newlength;
+ updateSliderElements();
+ }
+
+ // just take over the whole slider screen with a reconnect message
+ function showReconnectUI() {
+ if(!clientVars.sliderEnabled || !clientVars.supportsSlider) {
+ $("#padmain, #rightbars").css('top', "95px");
+ $("#timeslider").show();
+ }
+ $('#error').show();
+ }
+
+ function setAuthors(authors) {
+ $("#authorstable").empty();
+ var numAnonymous = 0;
+ var numNamed = 0;
+ authors.forEach(function(author) {
+ if(author.name) {
+ numNamed ++;
+ var tr = $('<tr></tr>');
+ var swatchtd = $('<td></td>');
+ var swatch = $('<div class="swatch"></div>');
+ swatch.css('background-color', clientVars.colorPalette[author.colorId]);
+ swatchtd.append(swatch);
+ tr.append(swatchtd);
+ var nametd = $('<td></td>');
+ nametd.text(author.name || "unnamed");
+ tr.append(nametd);
+ $("#authorstable").append(tr);
+ } else {
+ numAnonymous ++;
+ }
+ });
+ if(numAnonymous > 0) {
+ var html = "<tr><td colspan=\"2\" style=\"color:#999; padding-left: 10px\">"+(numNamed>0?"...and ":"")+numAnonymous+" unnamed author"+(numAnonymous>1?"s":"")+"</td></tr>";
+ $("#authorstable").append($(html));
+ } if(authors.length == 0) {
+ $("#authorstable").append($("<tr><td colspan=\"2\" style=\"color:#999; padding-left: 10px\">No Authors</td></tr>"))
+ }
+ }
+
+ global.BroadcastSlider = {
+ onSlider: onSlider,
+ getSliderPosition: getSliderPosition,
+ setSliderPosition: setSliderPosition,
+ getSliderLength: getSliderLength,
+ setSliderLength: setSliderLength,
+ isSliderActive: function() {return sliderActive;},
+ playpause: playpause,
+ addSavedRevision: addSavedRevision,
+ showReconnectUI : showReconnectUI,
+ setAuthors: setAuthors
+ }
+
+ function playButtonUpdater() {
+ if(sliderPlaying) {
+ if(getSliderPosition()+1 > sliderLength) {
+ $("#playpause_button_icon").toggleClass('pause');
+ sliderPlaying = false;
+ return;
+ }
+ setSliderPosition(getSliderPosition()+1);
+
+ setTimeout(playButtonUpdater, 100);
+ }
+ }
+
+ function playpause() {
+ $("#playpause_button_icon").toggleClass('pause');
+
+ if(!sliderPlaying) {
+ if(getSliderPosition() == sliderLength)
+ setSliderPosition(0);
+ sliderPlaying = true;
+ playButtonUpdater();
+ } else {
+ sliderPlaying = false;
+ }
+ }
+
+ // assign event handlers to html UI elements after page load
+ $(window).load(function() {
+ disableSelection($("#playpause_button")[0]);
+ disableSelection($("#timeslider")[0]);
+
+ if(clientVars.sliderEnabled && clientVars.supportsSlider) {
+ $(document).keyup(function(e) {
+ var code = -1;
+ if (!e) var e = window.event;
+ if (e.keyCode) code = e.keyCode;
+ else if (e.which) code = e.which;
+
+ if(code == 37) { // left
+ if(!e.shiftKey) {
+ setSliderPosition(getSliderPosition() - 1);
+ } else {
+ var nextStar = 0; // default to first revision in document
+ for(var i=0; i<savedRevisions.length; i++) {
+ var pos = parseInt(savedRevisions[i].attr('pos'));
+ if(pos < getSliderPosition() && nextStar < pos)
+ nextStar = pos;
+ }
+ setSliderPosition(nextStar);
+ }
+ } else if(code == 39) {
+ if(!e.shiftKey) {
+ setSliderPosition(getSliderPosition() + 1);
+ } else {
+ var nextStar = sliderLength; // default to last revision in document
+ for(var i=0; i<savedRevisions.length; i++) {
+ var pos = parseInt(savedRevisions[i].attr('pos'));
+ if(pos > getSliderPosition() && nextStar > pos)
+ nextStar = pos;
+ }
+ setSliderPosition(nextStar);
+ }
+ } else if(code == 32)
+ playpause();
+
+ });
+ }
+
+ $(window).resize(function() {
+ updateSliderElements();
+ });
+
+ $("#ui-slider-bar").mousedown(function(evt) {
+ setSliderPosition(Math.floor((evt.clientX-$("#ui-slider-bar").offset().left) * sliderLength / 742));
+ $("#ui-slider-handle").css('left', (evt.clientX-$("#ui-slider-bar").offset().left));
+ $("#ui-slider-handle").trigger(evt);
+ });
+
+ // Slider dragging
+ $("#ui-slider-handle").mousedown(function(evt) {
+ this.startLoc = evt.clientX;
+ this.currentLoc = parseInt($(this).css('left'));
+ var self = this;
+ sliderActive = true;
+ $(document).mousemove(function(evt2) {
+ $(self).css('pointer', 'move')
+ var newloc = self.currentLoc + (evt2.clientX - self.startLoc);
+ if(newloc < 0) newloc = 0;
+ if(newloc > ($("#ui-slider-bar").width()-2)) newloc = ($("#ui-slider-bar").width()-2);
+ $("#revision_label").html("Version " + Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2)));
+ $(self).css('left', newloc);
+ if(getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2)))
+ _callSliderCallbacks(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2)))
+ });
+ $(document).mouseup(function(evt2) {
+ $(document).unbind('mousemove');
+ $(document).unbind('mouseup');
+ sliderActive = false;
+ var newloc = self.currentLoc + (evt2.clientX - self.startLoc);
+ if(newloc < 0) newloc = 0;
+ if(newloc > ($("#ui-slider-bar").width()-2)) newloc = ($("#ui-slider-bar").width()-2);
+ $(self).css('left', newloc);
+ // if(getSliderPosition() != Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2)))
+ setSliderPosition(Math.floor(newloc * sliderLength / ($("#ui-slider-bar").width()-2)))
+ self.currentLoc = parseInt($(self).css('left'));
+ });
+ })
+
+ // play/pause toggling
+ $("#playpause_button").mousedown(function(evt) {
+ var self = this;
+
+ $(self).css('background-image', 'url(/static/img/pad/timeslider/crushed_button_depressed.png)');
+ $(self).mouseup(function(evt2) {
+ $(self).css('background-image', 'url(/static/img/pad/timeslider/crushed_button_undepressed.png)');
+ $(self).unbind('mouseup');
+ BroadcastSlider.playpause();
+ });
+ $(document).mouseup(function(evt2) {
+ $(self).css('background-image', 'url(/static/img/pad/timeslider/crushed_button_undepressed.png)');
+ $(document).unbind('mouseup');
+ });
+ });
+
+ // next/prev saved revision and changeset
+ $('.stepper').mousedown(function(evt) {
+ var self = this;
+ var origcss = $(self).css('background-position');
+ if (! origcss) {
+ origcss = $(self).css('background-position-x')+" "+$(self).css('background-position-y');
+ }
+ var origpos = parseInt(origcss.split(" ")[1]);
+ var newpos = (origpos - 43);
+ if(newpos < 0) newpos += 87;
+
+ var newcss = (origcss.split(" ")[0] + " " + newpos + "px");
+ if($(self).css('opacity') != 1.0)
+ newcss = origcss;
+
+ $(self).css('background-position', newcss)
+
+ $(self).mouseup(function(evt2) {
+ $(self).css('background-position',origcss);
+ $(self).unbind('mouseup');
+ $(document).unbind('mouseup');
+ if($(self).attr("id") == ("leftstep")) {
+ setSliderPosition(getSliderPosition() - 1);
+ }
+ else if($(self).attr("id") == ("rightstep")) {
+ setSliderPosition(getSliderPosition() + 1);
+ }
+ else if($(self).attr("id") == ("leftstar")) {
+ var nextStar = 0; // default to first revision in document
+ for(var i=0; i<savedRevisions.length; i++) {
+ var pos = parseInt(savedRevisions[i].attr('pos'));
+ if(pos < getSliderPosition() && nextStar < pos)
+ nextStar = pos;
+ }
+ setSliderPosition(nextStar);
+ }
+ else if($(self).attr("id") == ("rightstar")) {
+ var nextStar = sliderLength; // default to last revision in document
+ for(var i=0; i<savedRevisions.length; i++) {
+ var pos = parseInt(savedRevisions[i].attr('pos'));
+ if(pos > getSliderPosition() && nextStar > pos)
+ nextStar = pos;
+ }
+ setSliderPosition(nextStar);
+ }
+ });
+ $(document).mouseup(function(evt2) {
+ $(self).css('background-position',origcss);
+ $(self).unbind('mouseup');
+ $(document).unbind('mouseup');
+ });
+ })
+
+ if(clientVars) {
+ if(clientVars.fullWidth) {
+ $("#padpage").css('width', '100%');
+ $("#revision").css('position', "absolute")
+ $("#revision").css('right', "20px")
+ $("#revision").css('top', "20px")
+ $("#padmain").css('left', '0px');
+ $("#padmain").css('right', '197px');
+ $("#padmain").css('width', 'auto');
+ $("#rightbars").css('right', '7px');
+ $("#rightbars").css('margin-right', '0px');
+ $("#timeslider").css('width', 'auto');
+ }
+
+ if(clientVars.disableRightBar) {
+ $("#rightbars").css('display', 'none');
+ $('#padmain').css('width', 'auto');
+ if(clientVars.fullWidth)
+ $("#padmain").css('right', '7px');
+ else
+ $("#padmain").css('width', '860px');
+ $("#revision").css('position', "absolute");
+ $("#revision").css('right', "20px");
+ $("#revision").css('top', "20px");
+ }
+
+
+ if(clientVars.sliderEnabled) {
+ if(clientVars.supportsSlider) {
+ $("#padmain, #rightbars").css('top', "95px");
+ $("#timeslider").show();
+ setSliderLength(clientVars.totalRevs);
+ setSliderPosition(clientVars.revNum);
+ clientVars.savedRevisions.forEach(function(revision) {
+ addSavedRevision(revision.revNum, revision);
+ })
+ } else {
+ // slider is not supported
+ $("#padmain, #rightbars").css('top', "95px");
+ $("#timeslider").show();
+ $("#error").html("The timeslider feature is not supported on this pad. <a href=\"/ep/about/faq#disabledslider\">Why not?</a>");
+ $("#error").show();
+ }
+ } else {
+ if(clientVars.supportsSlider) {
+ setSliderLength(clientVars.totalRevs);
+ setSliderPosition(clientVars.revNum);
+ }
+ }
+ }
+ });
+})();
+
+BroadcastSlider.onSlider(function(loc) {
+ $("#viewlatest").html(loc==BroadcastSlider.getSliderLength()?"Viewing latest content":"View latest content");
+})
diff --git a/etherpad/src/static/js/collab_client.js b/etherpad/src/static/js/collab_client.js
new file mode 100644
index 0000000..d8834d7
--- /dev/null
+++ b/etherpad/src/static/js/collab_client.js
@@ -0,0 +1,628 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+$(window).bind("load", function() {
+ getCollabClient.windowLoaded = true;
+});
+
+/** Call this when the document is ready, and a new Ace2Editor() has been created and inited.
+ ACE's ready callback does not need to have fired yet.
+ "serverVars" are from calling doc.getCollabClientVars() on the server. */
+function getCollabClient(ace2editor, serverVars, initialUserInfo, options) {
+ var editor = ace2editor;
+
+ var rev = serverVars.rev;
+ var padId = serverVars.padId;
+ var globalPadId = serverVars.globalPadId;
+
+ var state = "IDLE";
+ var stateMessage;
+ var stateMessageSocketId;
+ var channelState = "CONNECTING";
+ var appLevelDisconnectReason = null;
+
+ var lastCommitTime = 0;
+ var initialStartConnectTime = 0;
+
+ var userId = initialUserInfo.userId;
+ var socketId;
+ var socket;
+ var userSet = {}; // userId -> userInfo
+ userSet[userId] = initialUserInfo;
+
+ var reconnectTimes = [];
+ var caughtErrors = [];
+ var caughtErrorCatchers = [];
+ var caughtErrorTimes = [];
+ var debugMessages = [];
+
+ tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData);
+ tellAceActiveAuthorInfo(initialUserInfo);
+
+ var callbacks = {
+ onUserJoin: function() {},
+ onUserLeave: function() {},
+ onUpdateUserInfo: function() {},
+ onChannelStateChange: function() {},
+ onClientMessage: function() {},
+ onInternalAction: function() {},
+ onConnectionTrouble: function() {},
+ onServerMessage: function() {}
+ };
+
+ $(window).bind("unload", function() {
+ if (socket) {
+ socket.onclosed = function() {};
+ socket.onhiccup = function() {};
+ socket.disconnect(true);
+ }
+ });
+ if ($.browser.mozilla) {
+ // Prevent "escape" from taking effect and canceling a comet connection;
+ // doesn't work if focus is on an iframe.
+ $(window).bind("keydown", function(evt) { if (evt.which == 27) { evt.preventDefault() } });
+ }
+
+ editor.setProperty("userAuthor", userId);
+ editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool);
+ editor.setUserChangeNotificationCallback(wrapRecordingErrors("handleUserChanges", handleUserChanges));
+
+ function abandonConnection(reason) {
+ if (socket) {
+ socket.onclosed = function() {};
+ socket.onhiccup = function() {};
+ socket.disconnect();
+ }
+ socket = null;
+ setChannelState("DISCONNECTED", reason);
+ }
+
+ function dmesg(str) {
+ if (typeof window.ajlog == "string") window.ajlog += str+'\n';
+ debugMessages.push(str);
+ }
+
+ function handleUserChanges() {
+ if ((! socket) || channelState == "CONNECTING") {
+ if (channelState == "CONNECTING" && (((+new Date()) - initialStartConnectTime) > 20000)) {
+ abandonConnection("initsocketfail"); // give up
+ }
+ else {
+ // check again in a bit
+ setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges),
+ 1000);
+ }
+ return;
+ }
+
+ var t = (+new Date());
+
+ if (state != "IDLE") {
+ if (state == "COMMITTING" && (t - lastCommitTime) > 20000) {
+ // a commit is taking too long
+ appLevelDisconnectReason = "slowcommit";
+ socket.disconnect();
+ }
+ else if (state == "COMMITTING" && (t - lastCommitTime) > 5000) {
+ callbacks.onConnectionTrouble("SLOW");
+ }
+ else {
+ // run again in a few seconds, to detect a disconnect
+ setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges),
+ 3000);
+ }
+ return;
+ }
+
+ var earliestCommit = lastCommitTime + 500;
+ if (t < earliestCommit) {
+ setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges),
+ earliestCommit - t);
+ return;
+ }
+
+ var sentMessage = false;
+ var userChangesData = editor.prepareUserChangeset();
+ if (userChangesData.changeset) {
+ lastCommitTime = t;
+ state = "COMMITTING";
+ stateMessage = {type:"USER_CHANGES", baseRev:rev,
+ changeset:userChangesData.changeset,
+ apool: userChangesData.apool };
+ stateMessageSocketId = socketId;
+ sendMessage(stateMessage);
+ sentMessage = true;
+ callbacks.onInternalAction("commitPerformed");
+ }
+
+ if (sentMessage) {
+ // run again in a few seconds, to detect a disconnect
+ setTimeout(wrapRecordingErrors("setTimeout(handleUserChanges)", handleUserChanges),
+ 3000);
+ }
+ }
+
+ function getStats() {
+ var stats = {};
+
+ stats.screen = [$(window).width(), $(window).height(),
+ window.screen.availWidth, window.screen.availHeight,
+ window.screen.width, window.screen.height].join(',');
+ stats.ip = serverVars.clientIp;
+ stats.useragent = serverVars.clientAgent;
+
+ return stats;
+ }
+
+ function setUpSocket() {
+ var success = false;
+ callCatchingErrors("setUpSocket", function() {
+ appLevelDisconnectReason = null;
+
+ var oldSocketId = socketId;
+ socketId = String(Math.floor(Math.random()*1e12));
+ socket = new WebSocket(socketId);
+ socket.onmessage = wrapRecordingErrors("socket.onmessage", handleMessageFromServer);
+ socket.onclosed = wrapRecordingErrors("socket.onclosed", handleSocketClosed);
+ socket.onopen = wrapRecordingErrors("socket.onopen", function() {
+ hiccupCount = 0;
+ setChannelState("CONNECTED");
+ var msg = { type:"CLIENT_READY", roomType:'padpage',
+ roomName:'padpage/'+globalPadId,
+ data: {
+ lastRev:rev,
+ userInfo:userSet[userId],
+ stats: getStats() } };
+ if (oldSocketId) {
+ msg.data.isReconnectOf = oldSocketId;
+ msg.data.isCommitPending = (state == "COMMITTING");
+ }
+ sendMessage(msg);
+ doDeferredActions();
+ });
+ socket.onhiccup = wrapRecordingErrors("socket.onhiccup", handleCometHiccup);
+ socket.onlogmessage = dmesg;
+ socket.connect();
+ success = true;
+ });
+ if (success) {
+ initialStartConnectTime = +new Date();
+ }
+ else {
+ abandonConnection("initsocketfail");
+ }
+ }
+ function setUpSocketWhenWindowLoaded() {
+ if (getCollabClient.windowLoaded) {
+ setUpSocket();
+ }
+ else {
+ setTimeout(setUpSocketWhenWindowLoaded, 200);
+ }
+ }
+ setTimeout(setUpSocketWhenWindowLoaded, 0);
+
+ var hiccupCount = 0;
+ function handleCometHiccup(params) {
+ dmesg("HICCUP (connected:"+(!!params.connected)+")");
+ var connectedNow = params.connected;
+ if (! connectedNow) {
+ hiccupCount++;
+ // skip first "cut off from server" notification
+ if (hiccupCount > 1) {
+ setChannelState("RECONNECTING");
+ }
+ }
+ else {
+ hiccupCount = 0;
+ setChannelState("CONNECTED");
+ }
+ }
+
+ function sendMessage(msg) {
+ socket.postMessage(JSON.stringify({type: "COLLABROOM", data: msg}));
+ }
+
+ function wrapRecordingErrors(catcher, func) {
+ return function() {
+ try {
+ return func.apply(this, Array.prototype.slice.call(arguments));
+ }
+ catch (e) {
+ caughtErrors.push(e);
+ caughtErrorCatchers.push(catcher);
+ caughtErrorTimes.push(+new Date());
+ //console.dir({catcher: catcher, e: e});
+ throw e;
+ }
+ };
+ }
+
+ function callCatchingErrors(catcher, func) {
+ try {
+ wrapRecordingErrors(catcher, func)();
+ }
+ catch (e) { /*absorb*/ }
+ }
+
+ function handleMessageFromServer(evt) {
+ if (! socket) return;
+ if (! evt.data) return;
+ var wrapper = JSON.parse(evt.data);
+ if(wrapper.type != "COLLABROOM") return;
+ var msg = wrapper.data;
+ if (msg.type == "NEW_CHANGES") {
+ var newRev = msg.newRev;
+ var changeset = msg.changeset;
+ var author = (msg.author || '');
+ var apool = msg.apool;
+ if (newRev != (rev+1)) {
+ dmesg("bad message revision on NEW_CHANGES: "+newRev+" not "+(rev+1));
+ socket.disconnect();
+ return;
+ }
+ rev = newRev;
+ editor.applyChangesToBase(changeset, author, apool);
+ }
+ else if (msg.type == "ACCEPT_COMMIT") {
+ var newRev = msg.newRev;
+ if (newRev != (rev+1)) {
+ dmesg("bad message revision on ACCEPT_COMMIT: "+newRev+" not "+(rev+1));
+ socket.disconnect();
+ return;
+ }
+ rev = newRev;
+ editor.applyPreparedChangesetToBase();
+ setStateIdle();
+ callCatchingErrors("onInternalAction", function() {
+ callbacks.onInternalAction("commitAcceptedByServer");
+ });
+ callCatchingErrors("onConnectionTrouble", function() {
+ callbacks.onConnectionTrouble("OK");
+ });
+ handleUserChanges();
+ }
+ else if (msg.type == "NO_COMMIT_PENDING") {
+ if (state == "COMMITTING") {
+ // server missed our commit message; abort that commit
+ setStateIdle();
+ handleUserChanges();
+ }
+ }
+ else if (msg.type == "USER_NEWINFO") {
+ var userInfo = msg.userInfo;
+ var id = userInfo.userId;
+ if (userSet[id]) {
+ userSet[id] = userInfo;
+ callbacks.onUpdateUserInfo(userInfo);
+ dmesgUsers();
+ }
+ else {
+ userSet[id] = userInfo;
+ callbacks.onUserJoin(userInfo);
+ dmesgUsers();
+ }
+ tellAceActiveAuthorInfo(userInfo);
+ }
+ else if (msg.type == "USER_LEAVE") {
+ var userInfo = msg.userInfo;
+ var id = userInfo.userId;
+ if (userSet[id]) {
+ delete userSet[userInfo.userId];
+ fadeAceAuthorInfo(userInfo);
+ callbacks.onUserLeave(userInfo);
+ dmesgUsers();
+ }
+ }
+ else if (msg.type == "DISCONNECT_REASON") {
+ appLevelDisconnectReason = msg.reason;
+ }
+ else if (msg.type == "CLIENT_MESSAGE") {
+ callbacks.onClientMessage(msg.payload);
+ }
+ else if (msg.type == "SERVER_MESSAGE") {
+ callbacks.onServerMessage(msg.payload);
+ }
+ }
+ function updateUserInfo(userInfo) {
+ userInfo.userId = userId;
+ userSet[userId] = userInfo;
+ tellAceActiveAuthorInfo(userInfo);
+ if (! socket) return;
+ sendMessage({type: "USERINFO_UPDATE", userInfo:userInfo});
+ }
+
+ function tellAceActiveAuthorInfo(userInfo) {
+ tellAceAuthorInfo(userInfo.userId, userInfo.colorId);
+ }
+ function tellAceAuthorInfo(userId, colorId, inactive) {
+ if (colorId || (typeof colorId) == "number") {
+ colorId = Number(colorId);
+ if (options && options.colorPalette && options.colorPalette[colorId]) {
+ var cssColor = options.colorPalette[colorId];
+ if (inactive) {
+ editor.setAuthorInfo(userId, {bgcolor: cssColor, fade: 0.5});
+ }
+ else {
+ editor.setAuthorInfo(userId, {bgcolor: cssColor});
+ }
+ }
+ }
+ }
+ function fadeAceAuthorInfo(userInfo) {
+ tellAceAuthorInfo(userInfo.userId, userInfo.colorId, true);
+ }
+
+ function getConnectedUsers() {
+ return valuesArray(userSet);
+ }
+
+ function tellAceAboutHistoricalAuthors(hadata) {
+ for(var author in hadata) {
+ var data = hadata[author];
+ if (! userSet[author]) {
+ tellAceAuthorInfo(author, data.colorId, true);
+ }
+ }
+ }
+
+ function dmesgUsers() {
+ //pad.dmesg($.map(getConnectedUsers(), function(u) { return u.userId.slice(-2); }).join(','));
+ }
+
+ function handleSocketClosed(params) {
+ socket = null;
+
+ $.each(keys(userSet), function() {
+ var uid = String(this);
+ if (uid != userId) {
+ var userInfo = userSet[uid];
+ delete userSet[uid];
+ callbacks.onUserLeave(userInfo);
+ dmesgUsers();
+ }
+ });
+
+ var reason = appLevelDisconnectReason || params.reason;
+ var shouldReconnect = params.reconnect;
+ if (shouldReconnect) {
+
+ // determine if this is a tight reconnect loop due to weird connectivity problems
+ reconnectTimes.push(+new Date());
+ var TOO_MANY_RECONNECTS = 8;
+ var TOO_SHORT_A_TIME_MS = 10000;
+ if (reconnectTimes.length >= TOO_MANY_RECONNECTS &&
+ ((+new Date()) - reconnectTimes[reconnectTimes.length-TOO_MANY_RECONNECTS]) <
+ TOO_SHORT_A_TIME_MS) {
+ setChannelState("DISCONNECTED", "looping");
+ }
+ else {
+ setChannelState("RECONNECTING", reason);
+ setUpSocket();
+ }
+
+ }
+ else {
+ setChannelState("DISCONNECTED", reason);
+ }
+ }
+
+ function setChannelState(newChannelState, moreInfo) {
+ if (newChannelState != channelState) {
+ channelState = newChannelState;
+ callbacks.onChannelStateChange(channelState, moreInfo);
+ }
+ }
+
+ function keys(obj) {
+ var array = [];
+ $.each(obj, function (k, v) { array.push(k); });
+ return array;
+ }
+ function valuesArray(obj) {
+ var array = [];
+ $.each(obj, function (k, v) { array.push(v); });
+ return array;
+ }
+
+ // We need to present a working interface even before the socket
+ // is connected for the first time.
+ var deferredActions = [];
+ function defer(func, tag) {
+ return function() {
+ var that = this;
+ var args = arguments;
+ function action() {
+ func.apply(that, args);
+ }
+ action.tag = tag;
+ if (channelState == "CONNECTING") {
+ deferredActions.push(action);
+ }
+ else {
+ action();
+ }
+ }
+ }
+ function doDeferredActions(tag) {
+ var newArray = [];
+ for(var i=0;i<deferredActions.length;i++) {
+ var a = deferredActions[i];
+ if ((!tag) || (tag == a.tag)) {
+ a();
+ }
+ else {
+ newArray.push(a);
+ }
+ }
+ deferredActions = newArray;
+ }
+
+ function sendClientMessage(msg) {
+ sendMessage({ type: "CLIENT_MESSAGE", payload: msg });
+ }
+
+ function getCurrentRevisionNumber() {
+ return rev;
+ }
+
+ function getDiagnosticInfo() {
+ var maxCaughtErrors = 3;
+ var maxAceErrors = 3;
+ var maxDebugMessages = 50;
+ var longStringCutoff = 500;
+
+ function trunc(str) {
+ return String(str).substring(0, longStringCutoff);
+ }
+
+ var info = { errors: {length: 0} };
+ function addError(e, catcher, time) {
+ var error = {catcher:catcher};
+ if (time) error.time = time;
+
+ // a little over-cautious?
+ try { if (e.description) error.description = e.description; } catch (x) {}
+ try { if (e.fileName) error.fileName = e.fileName; } catch (x) {}
+ try { if (e.lineNumber) error.lineNumber = e.lineNumber; } catch (x) {}
+ try { if (e.message) error.message = e.message; } catch (x) {}
+ try { if (e.name) error.name = e.name; } catch (x) {}
+ try { if (e.number) error.number = e.number; } catch (x) {}
+ try { if (e.stack) error.stack = trunc(e.stack); } catch (x) {}
+
+ info.errors[info.errors.length] = error;
+ info.errors.length++;
+ }
+ for(var i=0; ((i<caughtErrors.length) && (i<maxCaughtErrors)); i++) {
+ addError(caughtErrors[i], caughtErrorCatchers[i], caughtErrorTimes[i]);
+ }
+ if (editor) {
+ var aceErrors = editor.getUnhandledErrors();
+ for(var i=0; ((i<aceErrors.length) && (i<maxAceErrors)) ;i++) {
+ var errorRecord = aceErrors[i];
+ addError(errorRecord.error, "ACE", errorRecord.time);
+ }
+ }
+
+ info.time = +new Date();
+ info.collabState = state;
+ info.channelState = channelState;
+ info.lastCommitTime = lastCommitTime;
+ info.numSocketReconnects = reconnectTimes.length;
+ info.userId = userId;
+ info.currentRev = rev;
+ info.participants = (function() {
+ var pp = [];
+ for(var u in userSet) {
+ pp.push(u);
+ }
+ return pp.join(',');
+ })();
+
+ if (debugMessages.length > maxDebugMessages) {
+ debugMessages = debugMessages.slice(debugMessages.length-maxDebugMessages,
+ debugMessages.length);
+ }
+
+ info.debugMessages = {length: 0};
+ for(var i=0;i<debugMessages.length;i++) {
+ info.debugMessages[i] = trunc(debugMessages[i]);
+ info.debugMessages.length++;
+ }
+
+ return info;
+ }
+
+ function getMissedChanges() {
+ var obj = {};
+ obj.userInfo = userSet[userId];
+ obj.baseRev = rev;
+ if (state == "COMMITTING" && stateMessage) {
+ obj.committedChangeset = stateMessage.changeset;
+ obj.committedChangesetAPool = stateMessage.apool;
+ obj.committedChangesetSocketId = stateMessageSocketId;
+ editor.applyPreparedChangesetToBase();
+ }
+ var userChangesData = editor.prepareUserChangeset();
+ if (userChangesData.changeset) {
+ obj.furtherChangeset = userChangesData.changeset;
+ obj.furtherChangesetAPool = userChangesData.apool;
+ }
+ return obj;
+ }
+
+ function setStateIdle() {
+ state = "IDLE";
+ callbacks.onInternalAction("newlyIdle");
+ schedulePerhapsCallIdleFuncs();
+ }
+
+ function callWhenNotCommitting(func) {
+ idleFuncs.push(func);
+ schedulePerhapsCallIdleFuncs();
+ }
+
+ var idleFuncs = [];
+ function schedulePerhapsCallIdleFuncs() {
+ setTimeout(function() {
+ if (state == "IDLE") {
+ while (idleFuncs.length > 0) {
+ var f = idleFuncs.shift();
+ f();
+ }
+ }
+ }, 0);
+ }
+
+ var self;
+ return (self = {
+ setOnUserJoin: function(cb) { callbacks.onUserJoin = cb; },
+ setOnUserLeave: function(cb) { callbacks.onUserLeave = cb; },
+ setOnUpdateUserInfo: function(cb) { callbacks.onUpdateUserInfo = cb; },
+ setOnChannelStateChange: function(cb) { callbacks.onChannelStateChange = cb; },
+ setOnClientMessage: function(cb) { callbacks.onClientMessage = cb; },
+ setOnInternalAction: function(cb) { callbacks.onInternalAction = cb; },
+ setOnConnectionTrouble: function(cb) { callbacks.onConnectionTrouble = cb; },
+ setOnServerMessage: function(cb) { callbacks.onServerMessage = cb; },
+ updateUserInfo: defer(updateUserInfo),
+ getConnectedUsers: getConnectedUsers,
+ sendClientMessage: sendClientMessage,
+ getCurrentRevisionNumber: getCurrentRevisionNumber,
+ getDiagnosticInfo: getDiagnosticInfo,
+ getMissedChanges: getMissedChanges,
+ callWhenNotCommitting: callWhenNotCommitting,
+ addHistoricalAuthors: tellAceAboutHistoricalAuthors
+ });
+}
+
+function selectElementContents(elem) {
+ if ($.browser.msie) {
+ var range = document.body.createTextRange();
+ range.moveToElementText(elem);
+ range.select();
+ }
+ else {
+ if (window.getSelection) {
+ var browserSelection = window.getSelection();
+ if (browserSelection) {
+ var range = document.createRange();
+ range.selectNodeContents(elem);
+ browserSelection.removeAllRanges();
+ browserSelection.addRange(range);
+ }
+ }
+ }
+}
diff --git a/etherpad/src/static/js/confirmation.js b/etherpad/src/static/js/confirmation.js
new file mode 100644
index 0000000..a0f725c
--- /dev/null
+++ b/etherpad/src/static/js/confirmation.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+$(function() {
+ $('#shoppingform').submit(function() {
+ $('#contbutton').attr("disabled", true).attr("value", "Purchasing...");
+ });
+}) \ No newline at end of file
diff --git a/etherpad/src/static/js/connection_diagnostics.js b/etherpad/src/static/js/connection_diagnostics.js
new file mode 100644
index 0000000..cc43d46
--- /dev/null
+++ b/etherpad/src/static/js/connection_diagnostics.js
@@ -0,0 +1,126 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+diagnostics = {};
+
+diagnostics.data = {};
+
+diagnostics.steps = [
+ ['init', "Initializing"],
+ ['examineBrowser', "Examining web browser"],
+ ['testStreaming', "Testing primary transport (streaming)"],
+ ['testPolling', "Testing secondary transport (polling)"],
+ ['testHiccups', "Testing connection hiccups"],
+ ['sendInfo', "Sending information"],
+ ['showResult', ""]
+];
+
+diagnostics.processNext = function(i) {
+ if (i < diagnostics.steps.length) {
+ var msg = "Step "+(i+1)+": "+diagnostics.steps[i][1]+"...";
+ $('#statusmsg').html(msg);
+ diagnostics[diagnostics.steps[i][0]](function() {
+ diagnostics.processNext(i+1);
+ });
+ }
+};
+
+$(document).ready(function() {
+ diagnostics.processNext(0);
+
+ var emailClicked = false;
+ $('#email').click(function() {
+ if (!emailClicked) {
+ $('#email').select();
+ emailClicked = true;
+ }
+ });
+
+ $('#emailsubmit').click(function() {
+ function err(m) {
+ $('#emailerrormsg').hide().html(m).fadeIn('fast');
+ }
+ var email = $('#email').val();
+ if (!etherpad.validEmail(email)) {
+ err("That doesn't look like a valid email address.");
+ return;
+ }
+ $.ajax({
+ type: 'post',
+ url: '/ep/connection-diagnostics/submitemail',
+ data: {email: email, diagnosticStorableId: clientVars.diagnosticStorableId},
+ success: success,
+ error: error
+ });
+ function success(responseText) {
+ if (responseText == "OK") {
+ $('#emailform').html("<p>Thanks! We will look at your case shortly.</p>");
+ } else {
+ err(responseText);
+ }
+ }
+ function error() {
+ err("There was an error processing your request.");
+ }
+ });
+});
+
+diagnostics.init = function(done) {
+ setTimeout(done, 1000);
+};
+
+diagnostics.examineBrowser = function(done) {
+ setTimeout(done, 1000);
+};
+
+diagnostics.testStreaming = function(done) {
+ setTimeout(done, 1000);
+};
+
+diagnostics.testPolling = function(done) {
+ setTimeout(done, 1000);
+};
+
+diagnostics.testHiccups = function(done) {
+ setTimeout(done, 1000);
+};
+
+diagnostics.sendInfo = function(done) {
+
+ // TODO(jd): remove these test data when you submit actual data.
+ diagnostics.data.test1 = "foo";
+ diagnostics.data.test2 = "bar";
+ diagnostics.data.testNested = {a: 1, b: 2, c: 3};
+
+ // send data object back to server.
+ $.ajax({
+ type: 'post',
+ url: '/ep/connection-diagnostics/submitdata',
+ data: {dataJson: JSON.stringify(diagnostics.data),
+ diagnosticStorableId: clientVars.diagnosticStorableId},
+ success: done,
+ error: function() { alert("There was an error submitting the diagnostic information to the server."); done(); }
+ });
+};
+
+diagnostics.showResult = function(done) {
+ $('#linkanimation').hide();
+ $('#statusmsg').html("<br/>Result: your browser and internet"
+ + " connection appear to be incompatibile with EtherPad.");
+ $('#statusmsg').css('color', '#520');
+ $('#emailform').show();
+};
+
diff --git a/etherpad/src/static/js/draggable.js b/etherpad/src/static/js/draggable.js
new file mode 100644
index 0000000..97a1a3d
--- /dev/null
+++ b/etherpad/src/static/js/draggable.js
@@ -0,0 +1,60 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+function makeDraggable(jqueryNodes, eventHandler) {
+ jqueryNodes.each(function() {
+ var node = $(this);
+ var state = {};
+ var inDrag = false;
+ function dragStart(evt) {
+ if (inDrag) {
+ return;
+ }
+ inDrag = true;
+ if (eventHandler('dragstart', evt, state) !== false) {
+ $(document).bind('mousemove', dragUpdate);
+ $(document).bind('mouseup', dragEnd);
+ }
+ evt.preventDefault();
+ return false;
+ }
+ function dragUpdate(evt) {
+ if (! inDrag) {
+ return;
+ }
+ eventHandler('dragupdate', evt, state);
+ evt.preventDefault();
+ return false;
+ }
+ function dragEnd(evt) {
+ if (! inDrag) {
+ return;
+ }
+ inDrag = false;
+ try {
+ eventHandler('dragend', evt, state);
+ }
+ finally {
+ $(document).unbind('mousemove', dragUpdate);
+ $(document).unbind('mouseup', dragEnd);
+ evt.preventDefault();
+ }
+ return false;
+ }
+ node.bind('mousedown', dragStart);
+ });
+} \ No newline at end of file
diff --git a/etherpad/src/static/js/etherpad.js b/etherpad/src/static/js/etherpad.js
new file mode 100644
index 0000000..4e51dbf
--- /dev/null
+++ b/etherpad/src/static/js/etherpad.js
@@ -0,0 +1,217 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+$(document).ready(function() {
+ etherpad.deobfuscateEmails();
+
+ if ($('#betasignuppage').size() > 0) {
+ etherpad.betaSignupPageInit();
+ }
+
+ if ($('#productpage').size() > 0) {
+ etherpad.productPageInit();
+ }
+
+ if ($('.pricingpage').size() > 0) {
+ etherpad.pricingPageInit();
+ }
+});
+
+etherpad = {};
+
+//----------------------------------------------------------------
+// general utils
+//----------------------------------------------------------------
+
+etherpad.validEmail = function(x) {
+ return (x.length > 0 &&
+ x.match(/^[\w\.\_\+\-]+\@[\w\_\-]+\.[\w\_\-\.]+$/));
+};
+
+//----------------------------------------------------------------
+// obfuscating emails
+//----------------------------------------------------------------
+
+etherpad.deobfuscateEmails = function() {
+ $("a.obfuscemail").each(function() {
+ $(this).html($(this).html().replace('p*d.sp***e','pad.spline'));
+ this.href = this.href.replace('p*d.sp***e','pad.spline');
+ });
+};
+
+//----------------------------------------------------------------
+// Signing up for pricing info
+//----------------------------------------------------------------
+
+etherpad.pricingPageInit = function() {
+ $('#submitbutton').click(etherpad.pricingSubmit);
+};
+
+etherpad.pricingSubmit = function(edition) {
+ var allData = {};
+ $('#pricingcontact input.ti').each(function() {
+ allData[$(this).attr('id')] = $(this).val();
+ });
+ allData.industry = $('#industry').val();
+
+ $('form button').hide();
+ $('#spinner').show();
+ $('form input').attr('disabled', true);
+
+ $.ajax({
+ type: 'post',
+ url: $('#pricingcontact').attr('action'),
+ data: allData,
+ success: success,
+ error: error
+ });
+
+ function success(responseText) {
+ $('#spinner').hide();
+ if (responseText == "OK") {
+ $('#errorbox').hide();
+ $('#confirmbox').fadeIn('fast');
+ } else {
+ $('#confirmbox').hide();
+ $('#errorbox').hide().html(responseText).fadeIn('fast');
+ $('form button').show();
+ $('form input').removeAttr('disabled');
+ }
+ }
+ function error() {
+ $('#spinner').hide();
+ $('#errorbox').hide().html("Server error.").fadeIn('fast');
+ $('form button').show();
+ $('form input').removeAttr('disabled');
+ }
+
+ return false;
+}
+
+
+//----------------------------------------------------------------
+// Product page (client-side nagivation with JS)
+//----------------------------------------------------------------
+
+etherpad.productPageInit = function() {
+ $("#productpage #tour").addClass("javascripton");
+ etherpad.productPageNavigateTo(window.location.hash.substring(1));
+
+ $("#productpage a.tournav").click(etherpad.tourNavClick);
+}
+
+etherpad.tourNavClick = function() { // to be called as a click event handler
+ var href = $(this).attr('href');
+ var thorpLoc = href.indexOf('#');
+ if (thorpLoc >= 0) {
+ etherpad.productPageNavigateTo(href.substring(thorpLoc+1), true);
+ }
+}
+
+etherpad.productPageNavigateTo = function(hash, shouldAnimate) {
+ function setNavLink(rightOrLeft, text, linkhash) {
+ var navcells = $('#productpage .tourbar .'+rightOrLeft);
+ if (! text) {
+ navcells.html('&nbsp;');
+ }
+ else {
+ navcells.
+ html('<a class="tournav" href="'+clientVars.pageURL+'#'+(linkhash||'')+'">'+text+'</a>').
+ find('a.tournav').click(etherpad.tourNavClick);
+ }
+ }
+ function switchCardsIfNecessary(fromCard, toCard, andThen/*(didAnimate)*/) {
+ if (! $('#productpage #tour').hasClass("show"+toCard)) {
+ var afterAnimate = function() {
+ $("#productpage #"+fromCard).get(0).style.display = "";
+ $('#productpage #tour').removeClass("show"+fromCard).addClass("show"+toCard);
+ if (andThen) andThen(shouldAnimate);
+ }
+ if (shouldAnimate) {
+ $("#productpage #"+fromCard).fadeOut("fast", afterAnimate);
+ }
+ else {
+ afterAnimate();
+ }
+ }
+ else {
+ andThen(false);
+ }
+ }
+ function switchProseIfNecessary(toNum, useAnimation, andThen) {
+ var visibleProse = $("#productpage .tourprose:visible");
+ var alreadyVisible = ($("#productpage #tour"+toNum+"prose:visible").size() > 0);
+ function assignVisibilities() {
+ $("#productpage .tourprose").each(function() {
+ if (this.id == "tour"+toNum+"prose") {
+ this.style.display = 'block';
+ }
+ else {
+ this.style.display = 'none';
+ }
+ });
+ }
+
+ if ((! useAnimation) || visibleProse.size() == 0 || alreadyVisible) {
+ assignVisibilities();
+ andThen();
+ }
+ else {
+ function afterAnimate() {
+ assignVisibilities();
+ andThen();
+ }
+ if (visibleProse.size() > 0 && visibleProse.get(0).id != "tour"+toNum+"prose") {
+ visibleProse.fadeOut("fast", afterAnimate);
+ }
+ else {
+ afterAnimate();
+ }
+ }
+ }
+ function getProseTitle(n) {
+ if (n == 0) return clientVars.screenshotTitle;
+ var atag = $("#productpage #tourleftnav .tour"+n+" a");
+ if (atag.size() > 0) return atag.text();
+ return '';
+ }
+
+ var regexResult;
+ if ((regexResult = /^uses([1-9][0-9]*)$/.exec(hash))) {
+ var tourNum = +regexResult[1];
+ switchCardsIfNecessary("pageshot", "usecases", function(didAnimate) {
+ switchProseIfNecessary(tourNum, shouldAnimate && !didAnimate, function() {
+ /*var n = tourNum;
+ setNavLink("left", "&laquo; "+getProseTitle(n-1), (n == 1 ? "" : "uses"+(n-1)));
+ var nextTitle = getProseTitle(n+1);
+ if (! nextTitle) setNavLink("right", "");
+ else setNavLink("right", nextTitle+" &raquo;", "uses"+(n+1));*/
+ /*setNavLink("left", "&laquo; "+getProseTitle(0), "");
+ setNavLink("right", "");*/
+ setNavLink("right", "&laquo; "+getProseTitle(0), "");
+ $('#tourtop td.left').html("Use Cases");
+ $("#productpage #tourleftnav li").removeClass("selected");
+ $("#productpage #tourleftnav li.tour"+tourNum).addClass("selected");
+ });
+ });
+ }
+ else {
+ switchCardsIfNecessary("usecases", "pageshot", function() {
+ $('#tourtop td.left').html(getProseTitle(0));
+ setNavLink("right", clientVars.screenshotNextLink, "uses1");
+ });
+ }
+}
diff --git a/etherpad/src/static/js/jquery-1.2.6.js b/etherpad/src/static/js/jquery-1.2.6.js
new file mode 100644
index 0000000..88e661e
--- /dev/null
+++ b/etherpad/src/static/js/jquery-1.2.6.js
@@ -0,0 +1,3549 @@
+(function(){
+/*
+ * jQuery 1.2.6 - New Wave Javascript
+ *
+ * Copyright (c) 2008 John Resig (jquery.com)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2008-05-24 14:22:17 -0400 (Sat, 24 May 2008) $
+ * $Rev: 5685 $
+ */
+
+// Map over jQuery in case of overwrite
+var _jQuery = window.jQuery,
+// Map over the $ in case of overwrite
+ _$ = window.$;
+
+var jQuery = window.jQuery = window.$ = function( selector, context ) {
+ // The jQuery object is actually just the init constructor 'enhanced'
+ return new jQuery.fn.init( selector, context );
+};
+
+// A simple way to check for HTML strings or ID strings
+// (both of which we optimize for)
+var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/,
+
+// Is it a simple selector
+ isSimple = /^.[^:#\[\.]*$/,
+
+// Will speed up references to undefined, and allows munging its name.
+ undefined;
+
+jQuery.fn = jQuery.prototype = {
+ init: function( selector, context ) {
+ // Make sure that a selection was provided
+ selector = selector || document;
+
+ // Handle $(DOMElement)
+ if ( selector.nodeType ) {
+ this[0] = selector;
+ this.length = 1;
+ return this;
+ }
+ // Handle HTML strings
+ if ( typeof selector == "string" ) {
+ // Are we dealing with HTML string or an ID?
+ var match = quickExpr.exec( selector );
+
+ // Verify a match, and that no context was specified for #id
+ if ( match && (match[1] || !context) ) {
+
+ // HANDLE: $(html) -> $(array)
+ if ( match[1] )
+ selector = jQuery.clean( [ match[1] ], context );
+
+ // HANDLE: $("#id")
+ else {
+ var elem = document.getElementById( match[3] );
+
+ // Make sure an element was located
+ if ( elem ){
+ // Handle the case where IE and Opera return items
+ // by name instead of ID
+ if ( elem.id != match[3] )
+ return jQuery().find( selector );
+
+ // Otherwise, we inject the element directly into the jQuery object
+ return jQuery( elem );
+ }
+ selector = [];
+ }
+
+ // HANDLE: $(expr, [context])
+ // (which is just equivalent to: $(content).find(expr)
+ } else
+ return jQuery( context ).find( selector );
+
+ // HANDLE: $(function)
+ // Shortcut for document ready
+ } else if ( jQuery.isFunction( selector ) )
+ return jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector );
+
+ return this.setArray(jQuery.makeArray(selector));
+ },
+
+ // The current version of jQuery being used
+ jquery: "1.2.6",
+
+ // The number of elements contained in the matched element set
+ size: function() {
+ return this.length;
+ },
+
+ // The number of elements contained in the matched element set
+ length: 0,
+
+ // Get the Nth element in the matched element set OR
+ // Get the whole matched element set as a clean array
+ get: function( num ) {
+ return num == undefined ?
+
+ // Return a 'clean' array
+ jQuery.makeArray( this ) :
+
+ // Return just the object
+ this[ num ];
+ },
+
+ // Take an array of elements and push it onto the stack
+ // (returning the new matched element set)
+ pushStack: function( elems ) {
+ // Build a new jQuery matched element set
+ var ret = jQuery( elems );
+
+ // Add the old object onto the stack (as a reference)
+ ret.prevObject = this;
+
+ // Return the newly-formed element set
+ return ret;
+ },
+
+ // Force the current matched set of elements to become
+ // the specified array of elements (destroying the stack in the process)
+ // You should use pushStack() in order to do this, but maintain the stack
+ setArray: function( elems ) {
+ // Resetting the length to 0, then using the native Array push
+ // is a super-fast way to populate an object with array-like properties
+ this.length = 0;
+ Array.prototype.push.apply( this, elems );
+
+ return this;
+ },
+
+ // Execute a callback for every element in the matched set.
+ // (You can seed the arguments with an array of args, but this is
+ // only used internally.)
+ each: function( callback, args ) {
+ return jQuery.each( this, callback, args );
+ },
+
+ // Determine the position of an element within
+ // the matched set of elements
+ index: function( elem ) {
+ var ret = -1;
+
+ // Locate the position of the desired element
+ return jQuery.inArray(
+ // If it receives a jQuery object, the first element is used
+ elem && elem.jquery ? elem[0] : elem
+ , this );
+ },
+
+ attr: function( name, value, type ) {
+ var options = name;
+
+ // Look for the case where we're accessing a style value
+ if ( name.constructor == String )
+ if ( value === undefined )
+ return this[0] && jQuery[ type || "attr" ]( this[0], name );
+
+ else {
+ options = {};
+ options[ name ] = value;
+ }
+
+ // Check to see if we're setting style values
+ return this.each(function(i){
+ // Set all the styles
+ for ( name in options )
+ jQuery.attr(
+ type ?
+ this.style :
+ this,
+ name, jQuery.prop( this, options[ name ], type, i, name )
+ );
+ });
+ },
+
+ css: function( key, value ) {
+ // ignore negative width and height values
+ if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 )
+ value = undefined;
+ return this.attr( key, value, "curCSS" );
+ },
+
+ text: function( text ) {
+ if ( typeof text != "object" && text != null )
+ return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
+
+ var ret = "";
+
+ jQuery.each( text || this, function(){
+ jQuery.each( this.childNodes, function(){
+ if ( this.nodeType != 8 )
+ ret += this.nodeType != 1 ?
+ this.nodeValue :
+ jQuery.fn.text( [ this ] );
+ });
+ });
+
+ return ret;
+ },
+
+ wrapAll: function( html ) {
+ if ( this[0] )
+ // The elements to wrap the target around
+ jQuery( html, this[0].ownerDocument )
+ .clone()
+ .insertBefore( this[0] )
+ .map(function(){
+ var elem = this;
+
+ while ( elem.firstChild )
+ elem = elem.firstChild;
+
+ return elem;
+ })
+ .append(this);
+
+ return this;
+ },
+
+ wrapInner: function( html ) {
+ return this.each(function(){
+ jQuery( this ).contents().wrapAll( html );
+ });
+ },
+
+ wrap: function( html ) {
+ return this.each(function(){
+ jQuery( this ).wrapAll( html );
+ });
+ },
+
+ append: function() {
+ return this.domManip(arguments, true, false, function(elem){
+ if (this.nodeType == 1)
+ this.appendChild( elem );
+ });
+ },
+
+ prepend: function() {
+ return this.domManip(arguments, true, true, function(elem){
+ if (this.nodeType == 1)
+ this.insertBefore( elem, this.firstChild );
+ });
+ },
+
+ before: function() {
+ return this.domManip(arguments, false, false, function(elem){
+ this.parentNode.insertBefore( elem, this );
+ });
+ },
+
+ after: function() {
+ return this.domManip(arguments, false, true, function(elem){
+ this.parentNode.insertBefore( elem, this.nextSibling );
+ });
+ },
+
+ end: function() {
+ return this.prevObject || jQuery( [] );
+ },
+
+ find: function( selector ) {
+ var elems = jQuery.map(this, function(elem){
+ return jQuery.find( selector, elem );
+ });
+
+ return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ?
+ jQuery.unique( elems ) :
+ elems );
+ },
+
+ clone: function( events ) {
+ // Do the clone
+ var ret = this.map(function(){
+ if ( jQuery.browser.msie && !jQuery.isXMLDoc(this) ) {
+ // IE copies events bound via attachEvent when
+ // using cloneNode. Calling detachEvent on the
+ // clone will also remove the events from the orignal
+ // In order to get around this, we use innerHTML.
+ // Unfortunately, this means some modifications to
+ // attributes in IE that are actually only stored
+ // as properties will not be copied (such as the
+ // the name attribute on an input).
+ var clone = this.cloneNode(true),
+ container = document.createElement("div");
+ container.appendChild(clone);
+ return jQuery.clean([container.innerHTML])[0];
+ } else
+ return this.cloneNode(true);
+ });
+
+ // Need to set the expando to null on the cloned set if it exists
+ // removeData doesn't work here, IE removes it from the original as well
+ // this is primarily for IE but the data expando shouldn't be copied over in any browser
+ var clone = ret.find("*").andSelf().each(function(){
+ if ( this[ expando ] != undefined )
+ this[ expando ] = null;
+ });
+
+ // Copy the events from the original to the clone
+ if ( events === true )
+ this.find("*").andSelf().each(function(i){
+ if (this.nodeType == 3)
+ return;
+ var events = jQuery.data( this, "events" );
+
+ for ( var type in events )
+ for ( var handler in events[ type ] )
+ jQuery.event.add( clone[ i ], type, events[ type ][ handler ], events[ type ][ handler ].data );
+ });
+
+ // Return the cloned set
+ return ret;
+ },
+
+ filter: function( selector ) {
+ return this.pushStack(
+ jQuery.isFunction( selector ) &&
+ jQuery.grep(this, function(elem, i){
+ return selector.call( elem, i );
+ }) ||
+
+ jQuery.multiFilter( selector, this ) );
+ },
+
+ not: function( selector ) {
+ if ( selector.constructor == String )
+ // test special case where just one selector is passed in
+ if ( isSimple.test( selector ) )
+ return this.pushStack( jQuery.multiFilter( selector, this, true ) );
+ else
+ selector = jQuery.multiFilter( selector, this );
+
+ var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType;
+ return this.filter(function() {
+ return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector;
+ });
+ },
+
+ add: function( selector ) {
+ return this.pushStack( jQuery.unique( jQuery.merge(
+ this.get(),
+ typeof selector == 'string' ?
+ jQuery( selector ) :
+ jQuery.makeArray( selector )
+ )));
+ },
+
+ is: function( selector ) {
+ return !!selector && jQuery.multiFilter( selector, this ).length > 0;
+ },
+
+ hasClass: function( selector ) {
+ return this.is( "." + selector );
+ },
+
+ val: function( value ) {
+ if ( value == undefined ) {
+
+ if ( this.length ) {
+ var elem = this[0];
+
+ // We need to handle select boxes special
+ if ( jQuery.nodeName( elem, "select" ) ) {
+ var index = elem.selectedIndex,
+ values = [],
+ options = elem.options,
+ one = elem.type == "select-one";
+
+ // Nothing was selected
+ if ( index < 0 )
+ return null;
+
+ // Loop through all the selected options
+ for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
+ var option = options[ i ];
+
+ if ( option.selected ) {
+ // Get the specifc value for the option
+ value = jQuery.browser.msie && !option.attributes.value.specified ? option.text : option.value;
+
+ // We don't need an array for one selects
+ if ( one )
+ return value;
+
+ // Multi-Selects return an array
+ values.push( value );
+ }
+ }
+
+ return values;
+
+ // Everything else, we just grab the value
+ } else
+ return (this[0].value || "").replace(/\r/g, "");
+
+ }
+
+ return undefined;
+ }
+
+ if( value.constructor == Number )
+ value += '';
+
+ return this.each(function(){
+ if ( this.nodeType != 1 )
+ return;
+
+ if ( value.constructor == Array && /radio|checkbox/.test( this.type ) )
+ this.checked = (jQuery.inArray(this.value, value) >= 0 ||
+ jQuery.inArray(this.name, value) >= 0);
+
+ else if ( jQuery.nodeName( this, "select" ) ) {
+ var values = jQuery.makeArray(value);
+
+ jQuery( "option", this ).each(function(){
+ this.selected = (jQuery.inArray( this.value, values ) >= 0 ||
+ jQuery.inArray( this.text, values ) >= 0);
+ });
+
+ if ( !values.length )
+ this.selectedIndex = -1;
+
+ } else
+ this.value = value;
+ });
+ },
+
+ html: function( value ) {
+ return value == undefined ?
+ (this[0] ?
+ this[0].innerHTML :
+ null) :
+ this.empty().append( value );
+ },
+
+ replaceWith: function( value ) {
+ return this.after( value ).remove();
+ },
+
+ eq: function( i ) {
+ return this.slice( i, i + 1 );
+ },
+
+ slice: function() {
+ return this.pushStack( Array.prototype.slice.apply( this, arguments ) );
+ },
+
+ map: function( callback ) {
+ return this.pushStack( jQuery.map(this, function(elem, i){
+ return callback.call( elem, i, elem );
+ }));
+ },
+
+ andSelf: function() {
+ return this.add( this.prevObject );
+ },
+
+ data: function( key, value ){
+ var parts = key.split(".");
+ parts[1] = parts[1] ? "." + parts[1] : "";
+
+ if ( value === undefined ) {
+ var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
+
+ if ( data === undefined && this.length )
+ data = jQuery.data( this[0], key );
+
+ return data === undefined && parts[1] ?
+ this.data( parts[0] ) :
+ data;
+ } else
+ return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){
+ jQuery.data( this, key, value );
+ });
+ },
+
+ removeData: function( key ){
+ return this.each(function(){
+ jQuery.removeData( this, key );
+ });
+ },
+
+ domManip: function( args, table, reverse, callback ) {
+ var clone = this.length > 1, elems;
+
+ return this.each(function(){
+ if ( !elems ) {
+ elems = jQuery.clean( args, this.ownerDocument );
+
+ if ( reverse )
+ elems.reverse();
+ }
+
+ var obj = this;
+
+ if ( table && jQuery.nodeName( this, "table" ) && jQuery.nodeName( elems[0], "tr" ) )
+ obj = this.getElementsByTagName("tbody")[0] || this.appendChild( this.ownerDocument.createElement("tbody") );
+
+ var scripts = jQuery( [] );
+
+ jQuery.each(elems, function(){
+ var elem = clone ?
+ jQuery( this ).clone( true )[0] :
+ this;
+
+ // execute all scripts after the elements have been injected
+ if ( jQuery.nodeName( elem, "script" ) )
+ scripts = scripts.add( elem );
+ else {
+ // Remove any inner scripts for later evaluation
+ if ( elem.nodeType == 1 )
+ scripts = scripts.add( jQuery( "script", elem ).remove() );
+
+ // Inject the elements into the document
+ callback.call( obj, elem );
+ }
+ });
+
+ scripts.each( evalScript );
+ });
+ }
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+function evalScript( i, elem ) {
+ if ( elem.src )
+ jQuery.ajax({
+ url: elem.src,
+ async: false,
+ dataType: "script"
+ });
+
+ else
+ jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
+
+ if ( elem.parentNode )
+ elem.parentNode.removeChild( elem );
+}
+
+function now(){
+ return +new Date;
+}
+
+jQuery.extend = jQuery.fn.extend = function() {
+ // copy reference to target object
+ var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
+
+ // Handle a deep copy situation
+ if ( target.constructor == Boolean ) {
+ deep = target;
+ target = arguments[1] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target != "object" && typeof target != "function" )
+ target = {};
+
+ // extend jQuery itself if only one argument is passed
+ if ( length == i ) {
+ target = this;
+ --i;
+ }
+
+ for ( ; i < length; i++ )
+ // Only deal with non-null/undefined values
+ if ( (options = arguments[ i ]) != null )
+ // Extend the base object
+ for ( var name in options ) {
+ var src = target[ name ], copy = options[ name ];
+
+ // Prevent never-ending loop
+ if ( target === copy )
+ continue;
+
+ // Recurse if we're merging object values
+ if ( deep && copy && typeof copy == "object" && !copy.nodeType )
+ target[ name ] = jQuery.extend( deep,
+ // Never move original objects, clone them
+ src || ( copy.length != null ? [ ] : { } )
+ , copy );
+
+ // Don't bring in undefined values
+ else if ( copy !== undefined )
+ target[ name ] = copy;
+
+ }
+
+ // Return the modified object
+ return target;
+};
+
+var expando = "jQuery" + now(), uuid = 0, windowData = {},
+ // exclude the following css properties to add px
+ exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
+ // cache defaultView
+ defaultView = document.defaultView || {};
+
+jQuery.extend({
+ noConflict: function( deep ) {
+ window.$ = _$;
+
+ if ( deep )
+ window.jQuery = _jQuery;
+
+ return jQuery;
+ },
+
+ // See test/unit/core.js for details concerning this function.
+ isFunction: function( fn ) {
+ return !!fn && typeof fn != "string" && !fn.nodeName &&
+ fn.constructor != Array && /^[\s[]?function/.test( fn + "" );
+ },
+
+ // check if an element is in a (or is an) XML document
+ isXMLDoc: function( elem ) {
+ return elem.documentElement && !elem.body ||
+ elem.tagName && elem.ownerDocument && !elem.ownerDocument.body;
+ },
+
+ // Evalulates a script in a global context
+ globalEval: function( data ) {
+ data = jQuery.trim( data );
+
+ if ( data ) {
+ // Inspired by code by Andrea Giammarchi
+ // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
+ var head = document.getElementsByTagName("head")[0] || document.documentElement,
+ script = document.createElement("script");
+
+ script.type = "text/javascript";
+ if ( jQuery.browser.msie )
+ script.text = data;
+ else
+ script.appendChild( document.createTextNode( data ) );
+
+ // Use insertBefore instead of appendChild to circumvent an IE6 bug.
+ // This arises when a base node is used (#2709).
+ head.insertBefore( script, head.firstChild );
+ head.removeChild( script );
+ }
+ },
+
+ nodeName: function( elem, name ) {
+ return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
+ },
+
+ cache: {},
+
+ data: function( elem, name, data ) {
+ elem = elem == window ?
+ windowData :
+ elem;
+
+ var id = elem[ expando ];
+
+ // Compute a unique ID for the element
+ if ( !id )
+ id = elem[ expando ] = ++uuid;
+
+ // Only generate the data cache if we're
+ // trying to access or manipulate it
+ if ( name && !jQuery.cache[ id ] )
+ jQuery.cache[ id ] = {};
+
+ // Prevent overriding the named cache with undefined values
+ if ( data !== undefined )
+ jQuery.cache[ id ][ name ] = data;
+
+ // Return the named cache data, or the ID for the element
+ return name ?
+ jQuery.cache[ id ][ name ] :
+ id;
+ },
+
+ removeData: function( elem, name ) {
+ elem = elem == window ?
+ windowData :
+ elem;
+
+ var id = elem[ expando ];
+
+ // If we want to remove a specific section of the element's data
+ if ( name ) {
+ if ( jQuery.cache[ id ] ) {
+ // Remove the section of cache data
+ delete jQuery.cache[ id ][ name ];
+
+ // If we've removed all the data, remove the element's cache
+ name = "";
+
+ for ( name in jQuery.cache[ id ] )
+ break;
+
+ if ( !name )
+ jQuery.removeData( elem );
+ }
+
+ // Otherwise, we want to remove all of the element's data
+ } else {
+ // Clean up the element expando
+ try {
+ delete elem[ expando ];
+ } catch(e){
+ // IE has trouble directly removing the expando
+ // but it's ok with using removeAttribute
+ if ( elem.removeAttribute )
+ elem.removeAttribute( expando );
+ }
+
+ // Completely remove the data cache
+ delete jQuery.cache[ id ];
+ }
+ },
+
+ // args is for internal usage only
+ each: function( object, callback, args ) {
+ var name, i = 0, length = object.length;
+
+ if ( args ) {
+ if ( length == undefined ) {
+ for ( name in object )
+ if ( callback.apply( object[ name ], args ) === false )
+ break;
+ } else
+ for ( ; i < length; )
+ if ( callback.apply( object[ i++ ], args ) === false )
+ break;
+
+ // A special, fast, case for the most common use of each
+ } else {
+ if ( length == undefined ) {
+ for ( name in object )
+ if ( callback.call( object[ name ], name, object[ name ] ) === false )
+ break;
+ } else
+ for ( var value = object[0];
+ i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
+ }
+
+ return object;
+ },
+
+ prop: function( elem, value, type, i, name ) {
+ // Handle executable functions
+ if ( jQuery.isFunction( value ) )
+ value = value.call( elem, i );
+
+ // Handle passing in a number to a CSS property
+ return value && value.constructor == Number && type == "curCSS" && !exclude.test( name ) ?
+ value + "px" :
+ value;
+ },
+
+ className: {
+ // internal only, use addClass("class")
+ add: function( elem, classNames ) {
+ jQuery.each((classNames || "").split(/\s+/), function(i, className){
+ if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) )
+ elem.className += (elem.className ? " " : "") + className;
+ });
+ },
+
+ // internal only, use removeClass("class")
+ remove: function( elem, classNames ) {
+ if (elem.nodeType == 1)
+ elem.className = classNames != undefined ?
+ jQuery.grep(elem.className.split(/\s+/), function(className){
+ return !jQuery.className.has( classNames, className );
+ }).join(" ") :
+ "";
+ },
+
+ // internal only, use hasClass("class")
+ has: function( elem, className ) {
+ return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
+ }
+ },
+
+ // A method for quickly swapping in/out CSS properties to get correct calculations
+ swap: function( elem, options, callback ) {
+ var old = {};
+ // Remember the old values, and insert the new ones
+ for ( var name in options ) {
+ old[ name ] = elem.style[ name ];
+ elem.style[ name ] = options[ name ];
+ }
+
+ callback.call( elem );
+
+ // Revert the old values
+ for ( var name in options )
+ elem.style[ name ] = old[ name ];
+ },
+
+ css: function( elem, name, force ) {
+ if ( name == "width" || name == "height" ) {
+ var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];
+
+ function getWH() {
+ val = name == "width" ? elem.offsetWidth : elem.offsetHeight;
+ var padding = 0, border = 0;
+ jQuery.each( which, function() {
+ padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
+ border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
+ });
+ val -= Math.round(padding + border);
+ }
+
+ if ( jQuery(elem).is(":visible") )
+ getWH();
+ else
+ jQuery.swap( elem, props, getWH );
+
+ return Math.max(0, val);
+ }
+
+ return jQuery.curCSS( elem, name, force );
+ },
+
+ curCSS: function( elem, name, force ) {
+ var ret, style = elem.style;
+
+ // A helper method for determining if an element's values are broken
+ function color( elem ) {
+ if ( !jQuery.browser.safari )
+ return false;
+
+ // defaultView is cached
+ var ret = defaultView.getComputedStyle( elem, null );
+ return !ret || ret.getPropertyValue("color") == "";
+ }
+
+ // We need to handle opacity special in IE
+ if ( name == "opacity" && jQuery.browser.msie ) {
+ ret = jQuery.attr( style, "opacity" );
+
+ return ret == "" ?
+ "1" :
+ ret;
+ }
+ // Opera sometimes will give the wrong display answer, this fixes it, see #2037
+ if ( jQuery.browser.opera && name == "display" ) {
+ var save = style.outline;
+ style.outline = "0 solid black";
+ style.outline = save;
+ }
+
+ // Make sure we're using the right name for getting the float value
+ if ( name.match( /float/i ) )
+ name = styleFloat;
+
+ if ( !force && style && style[ name ] )
+ ret = style[ name ];
+
+ else if ( defaultView.getComputedStyle ) {
+
+ // Only "float" is needed here
+ if ( name.match( /float/i ) )
+ name = "float";
+
+ name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();
+
+ var computedStyle = defaultView.getComputedStyle( elem, null );
+
+ if ( computedStyle && !color( elem ) )
+ ret = computedStyle.getPropertyValue( name );
+
+ // If the element isn't reporting its values properly in Safari
+ // then some display: none elements are involved
+ else {
+ var swap = [], stack = [], a = elem, i = 0;
+
+ // Locate all of the parent display: none elements
+ for ( ; a && color(a); a = a.parentNode )
+ stack.unshift(a);
+
+ // Go through and make them visible, but in reverse
+ // (It would be better if we knew the exact display type that they had)
+ for ( ; i < stack.length; i++ )
+ if ( color( stack[ i ] ) ) {
+ swap[ i ] = stack[ i ].style.display;
+ stack[ i ].style.display = "block";
+ }
+
+ // Since we flip the display style, we have to handle that
+ // one special, otherwise get the value
+ ret = name == "display" && swap[ stack.length - 1 ] != null ?
+ "none" :
+ ( computedStyle && computedStyle.getPropertyValue( name ) ) || "";
+
+ // Finally, revert the display styles back
+ for ( i = 0; i < swap.length; i++ )
+ if ( swap[ i ] != null )
+ stack[ i ].style.display = swap[ i ];
+ }
+
+ // We should always get a number back from opacity
+ if ( name == "opacity" && ret == "" )
+ ret = "1";
+
+ } else if ( elem.currentStyle ) {
+ var camelCase = name.replace(/\-(\w)/g, function(all, letter){
+ return letter.toUpperCase();
+ });
+
+ ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
+
+ // From the awesome hack by Dean Edwards
+ // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+ // If we're not dealing with a regular pixel number
+ // but a number that has a weird ending, we need to convert it to pixels
+ if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
+ // Remember the original values
+ var left = style.left, rsLeft = elem.runtimeStyle.left;
+
+ // Put in the new values to get a computed value out
+ elem.runtimeStyle.left = elem.currentStyle.left;
+ style.left = ret || 0;
+ ret = style.pixelLeft + "px";
+
+ // Revert the changed values
+ style.left = left;
+ elem.runtimeStyle.left = rsLeft;
+ }
+ }
+
+ return ret;
+ },
+
+ clean: function( elems, context ) {
+ var ret = [];
+ context = context || document;
+ // !context.createElement fails in IE with an error but returns typeof 'object'
+ if (typeof context.createElement == 'undefined')
+ context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
+
+ jQuery.each(elems, function(i, elem){
+ if ( !elem )
+ return;
+
+ if ( elem.constructor == Number )
+ elem += '';
+
+ // Convert html string into DOM nodes
+ if ( typeof elem == "string" ) {
+ // Fix "XHTML"-style tags in all browsers
+ elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
+ return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
+ all :
+ front + "></" + tag + ">";
+ });
+
+ // Trim whitespace, otherwise indexOf won't work as expected
+ var tags = jQuery.trim( elem ).toLowerCase(), div = context.createElement("div");
+
+ var wrap =
+ // option or optgroup
+ !tags.indexOf("<opt") &&
+ [ 1, "<select multiple='multiple'>", "</select>" ] ||
+
+ !tags.indexOf("<leg") &&
+ [ 1, "<fieldset>", "</fieldset>" ] ||
+
+ tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
+ [ 1, "<table>", "</table>" ] ||
+
+ !tags.indexOf("<tr") &&
+ [ 2, "<table><tbody>", "</tbody></table>" ] ||
+
+ // <thead> matched above
+ (!tags.indexOf("<td") || !tags.indexOf("<th")) &&
+ [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||
+
+ !tags.indexOf("<col") &&
+ [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||
+
+ // IE can't serialize <link> and <script> tags normally
+ jQuery.browser.msie &&
+ [ 1, "div<div>", "</div>" ] ||
+
+ [ 0, "", "" ];
+
+ // Go to html and back, then peel off extra wrappers
+ div.innerHTML = wrap[1] + elem + wrap[2];
+
+ // Move to the right depth
+ while ( wrap[0]-- )
+ div = div.lastChild;
+
+ // Remove IE's autoinserted <tbody> from table fragments
+ if ( jQuery.browser.msie ) {
+
+ // String was a <table>, *may* have spurious <tbody>
+ var tbody = !tags.indexOf("<table") && tags.indexOf("<tbody") < 0 ?
+ div.firstChild && div.firstChild.childNodes :
+
+ // String was a bare <thead> or <tfoot>
+ wrap[1] == "<table>" && tags.indexOf("<tbody") < 0 ?
+ div.childNodes :
+ [];
+
+ for ( var j = tbody.length - 1; j >= 0 ; --j )
+ if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length )
+ tbody[ j ].parentNode.removeChild( tbody[ j ] );
+
+ // IE completely kills leading whitespace when innerHTML is used
+ if ( /^\s/.test( elem ) )
+ div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild );
+
+ }
+
+ elem = jQuery.makeArray( div.childNodes );
+ }
+
+ if ( elem.length === 0 && (!jQuery.nodeName( elem, "form" ) && !jQuery.nodeName( elem, "select" )) )
+ return;
+
+ if ( elem[0] == undefined || jQuery.nodeName( elem, "form" ) || elem.options )
+ ret.push( elem );
+
+ else
+ ret = jQuery.merge( ret, elem );
+
+ });
+
+ return ret;
+ },
+
+ attr: function( elem, name, value ) {
+ // don't set attributes on text and comment nodes
+ if (!elem || elem.nodeType == 3 || elem.nodeType == 8)
+ return undefined;
+
+ var notxml = !jQuery.isXMLDoc( elem ),
+ // Whether we are setting (or getting)
+ set = value !== undefined,
+ msie = jQuery.browser.msie;
+
+ // Try to normalize/fix the name
+ name = notxml && jQuery.props[ name ] || name;
+
+ // Only do all the following if this is a node (faster for style)
+ // IE elem.getAttribute passes even for style
+ if ( elem.tagName ) {
+
+ // These attributes require special treatment
+ var special = /href|src|style/.test( name );
+
+ // Safari mis-reports the default selected property of a hidden option
+ // Accessing the parent's selectedIndex property fixes it
+ if ( name == "selected" && jQuery.browser.safari )
+ elem.parentNode.selectedIndex;
+
+ // If applicable, access the attribute via the DOM 0 way
+ if ( name in elem && notxml && !special ) {
+ if ( set ){
+ // We can't allow the type property to be changed (since it causes problems in IE)
+ if ( name == "type" && jQuery.nodeName( elem, "input" ) && elem.parentNode )
+ throw "type property can't be changed";
+
+ elem[ name ] = value;
+ }
+
+ // browsers index elements by id/name on forms, give priority to attributes.
+ if( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) )
+ return elem.getAttributeNode( name ).nodeValue;
+
+ return elem[ name ];
+ }
+
+ if ( msie && notxml && name == "style" )
+ return jQuery.attr( elem.style, "cssText", value );
+
+ if ( set )
+ // convert the value to a string (all browsers do this but IE) see #1070
+ elem.setAttribute( name, "" + value );
+
+ var attr = msie && notxml && special
+ // Some attributes require a special call on IE
+ ? elem.getAttribute( name, 2 )
+ : elem.getAttribute( name );
+
+ // Non-existent attributes return null, we normalize to undefined
+ return attr === null ? undefined : attr;
+ }
+
+ // elem is actually elem.style ... set the style
+
+ // IE uses filters for opacity
+ if ( msie && name == "opacity" ) {
+ if ( set ) {
+ // IE has trouble with opacity if it does not have layout
+ // Force it by setting the zoom level
+ elem.zoom = 1;
+
+ // Set the alpha filter to set the opacity
+ elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) +
+ (parseInt( value ) + '' == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
+ }
+
+ return elem.filter && elem.filter.indexOf("opacity=") >= 0 ?
+ (parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100) + '':
+ "";
+ }
+
+ name = name.replace(/-([a-z])/ig, function(all, letter){
+ return letter.toUpperCase();
+ });
+
+ if ( set )
+ elem[ name ] = value;
+
+ return elem[ name ];
+ },
+
+ trim: function( text ) {
+ return (text || "").replace( /^\s+|\s+$/g, "" );
+ },
+
+ makeArray: function( array ) {
+ var ret = [];
+
+ if( array != null ){
+ var i = array.length;
+ //the window, strings and functions also have 'length'
+ if( i == null || array.split || array.setInterval || array.call )
+ ret[0] = array;
+ else
+ while( i )
+ ret[--i] = array[i];
+ }
+
+ return ret;
+ },
+
+ inArray: function( elem, array ) {
+ for ( var i = 0, length = array.length; i < length; i++ )
+ // Use === because on IE, window == document
+ if ( array[ i ] === elem )
+ return i;
+
+ return -1;
+ },
+
+ merge: function( first, second ) {
+ // We have to loop this way because IE & Opera overwrite the length
+ // expando of getElementsByTagName
+ var i = 0, elem, pos = first.length;
+ // Also, we need to make sure that the correct elements are being returned
+ // (IE returns comment nodes in a '*' query)
+ if ( jQuery.browser.msie ) {
+ while ( elem = second[ i++ ] )
+ if ( elem.nodeType != 8 )
+ first[ pos++ ] = elem;
+
+ } else
+ while ( elem = second[ i++ ] )
+ first[ pos++ ] = elem;
+
+ return first;
+ },
+
+ unique: function( array ) {
+ var ret = [], done = {};
+
+ try {
+
+ for ( var i = 0, length = array.length; i < length; i++ ) {
+ var id = jQuery.data( array[ i ] );
+
+ if ( !done[ id ] ) {
+ done[ id ] = true;
+ ret.push( array[ i ] );
+ }
+ }
+
+ } catch( e ) {
+ ret = array;
+ }
+
+ return ret;
+ },
+
+ grep: function( elems, callback, inv ) {
+ var ret = [];
+
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( var i = 0, length = elems.length; i < length; i++ )
+ if ( !inv != !callback( elems[ i ], i ) )
+ ret.push( elems[ i ] );
+
+ return ret;
+ },
+
+ map: function( elems, callback ) {
+ var ret = [];
+
+ // Go through the array, translating each of the items to their
+ // new value (or values).
+ for ( var i = 0, length = elems.length; i < length; i++ ) {
+ var value = callback( elems[ i ], i );
+
+ if ( value != null )
+ ret[ ret.length ] = value;
+ }
+
+ return ret.concat.apply( [], ret );
+ }
+});
+
+var userAgent = navigator.userAgent.toLowerCase();
+
+// Figure out what browser is being used
+jQuery.browser = {
+ version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [])[1],
+ safari: /webkit/.test( userAgent ),
+ opera: /opera/.test( userAgent ),
+ msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
+ mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
+};
+
+var styleFloat = jQuery.browser.msie ?
+ "styleFloat" :
+ "cssFloat";
+
+jQuery.extend({
+ // Check to see if the W3C box model is being used
+ boxModel: !jQuery.browser.msie || document.compatMode == "CSS1Compat",
+
+ props: {
+ "for": "htmlFor",
+ "class": "className",
+ "float": styleFloat,
+ cssFloat: styleFloat,
+ styleFloat: styleFloat,
+ readonly: "readOnly",
+ maxlength: "maxLength",
+ cellspacing: "cellSpacing"
+ }
+});
+
+jQuery.each({
+ parent: function(elem){return elem.parentNode;},
+ parents: function(elem){return jQuery.dir(elem,"parentNode");},
+ next: function(elem){return jQuery.nth(elem,2,"nextSibling");},
+ prev: function(elem){return jQuery.nth(elem,2,"previousSibling");},
+ nextAll: function(elem){return jQuery.dir(elem,"nextSibling");},
+ prevAll: function(elem){return jQuery.dir(elem,"previousSibling");},
+ siblings: function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);},
+ children: function(elem){return jQuery.sibling(elem.firstChild);},
+ contents: function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);}
+}, function(name, fn){
+ jQuery.fn[ name ] = function( selector ) {
+ var ret = jQuery.map( this, fn );
+
+ if ( selector && typeof selector == "string" )
+ ret = jQuery.multiFilter( selector, ret );
+
+ return this.pushStack( jQuery.unique( ret ) );
+ };
+});
+
+jQuery.each({
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after",
+ replaceAll: "replaceWith"
+}, function(name, original){
+ jQuery.fn[ name ] = function() {
+ var args = arguments;
+
+ return this.each(function(){
+ for ( var i = 0, length = args.length; i < length; i++ )
+ jQuery( args[ i ] )[ original ]( this );
+ });
+ };
+});
+
+jQuery.each({
+ removeAttr: function( name ) {
+ jQuery.attr( this, name, "" );
+ if (this.nodeType == 1)
+ this.removeAttribute( name );
+ },
+
+ addClass: function( classNames ) {
+ jQuery.className.add( this, classNames );
+ },
+
+ removeClass: function( classNames ) {
+ jQuery.className.remove( this, classNames );
+ },
+
+ toggleClass: function( classNames ) {
+ jQuery.className[ jQuery.className.has( this, classNames ) ? "remove" : "add" ]( this, classNames );
+ },
+
+ remove: function( selector ) {
+ if ( !selector || jQuery.filter( selector, [ this ] ).r.length ) {
+ // Prevent memory leaks
+ jQuery( "*", this ).add(this).each(function(){
+ jQuery.event.remove(this);
+ jQuery.removeData(this);
+ });
+ if (this.parentNode)
+ this.parentNode.removeChild( this );
+ }
+ },
+
+ empty: function() {
+ // Remove element nodes and prevent memory leaks
+ jQuery( ">*", this ).remove();
+
+ // Remove any remaining nodes
+ while ( this.firstChild )
+ this.removeChild( this.firstChild );
+ }
+}, function(name, fn){
+ jQuery.fn[ name ] = function(){
+ return this.each( fn, arguments );
+ };
+});
+
+jQuery.each([ "Height", "Width" ], function(i, name){
+ var type = name.toLowerCase();
+
+ jQuery.fn[ type ] = function( size ) {
+ // Get window width or height
+ return this[0] == window ?
+ // Opera reports document.body.client[Width/Height] properly in both quirks and standards
+ jQuery.browser.opera && document.body[ "client" + name ] ||
+
+ // Safari reports inner[Width/Height] just fine (Mozilla and Opera include scroll bar widths)
+ jQuery.browser.safari && window[ "inner" + name ] ||
+
+ // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
+ document.compatMode == "CSS1Compat" && document.documentElement[ "client" + name ] || document.body[ "client" + name ] :
+
+ // Get document width or height
+ this[0] == document ?
+ // Either scroll[Width/Height] or offset[Width/Height], whichever is greater
+ Math.max(
+ Math.max(document.body["scroll" + name], document.documentElement["scroll" + name]),
+ Math.max(document.body["offset" + name], document.documentElement["offset" + name])
+ ) :
+
+ // Get or set width or height on the element
+ size == undefined ?
+ // Get width or height on the element
+ (this.length ? jQuery.css( this[0], type ) : null) :
+
+ // Set the width or height on the element (default to pixels if value is unitless)
+ this.css( type, size.constructor == String ? size : size + "px" );
+ };
+});
+
+// Helper function used by the dimensions and offset modules
+function num(elem, prop) {
+ return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0;
+}var chars = jQuery.browser.safari && parseInt(jQuery.browser.version) < 417 ?
+ "(?:[\\w*_-]|\\\\.)" :
+ "(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",
+ quickChild = new RegExp("^>\\s*(" + chars + "+)"),
+ quickID = new RegExp("^(" + chars + "+)(#)(" + chars + "+)"),
+ quickClass = new RegExp("^([#.]?)(" + chars + "*)");
+
+jQuery.extend({
+ expr: {
+ "": function(a,i,m){return m[2]=="*"||jQuery.nodeName(a,m[2]);},
+ "#": function(a,i,m){return a.getAttribute("id")==m[2];},
+ ":": {
+ // Position Checks
+ lt: function(a,i,m){return i<m[3]-0;},
+ gt: function(a,i,m){return i>m[3]-0;},
+ nth: function(a,i,m){return m[3]-0==i;},
+ eq: function(a,i,m){return m[3]-0==i;},
+ first: function(a,i){return i==0;},
+ last: function(a,i,m,r){return i==r.length-1;},
+ even: function(a,i){return i%2==0;},
+ odd: function(a,i){return i%2;},
+
+ // Child Checks
+ "first-child": function(a){return a.parentNode.getElementsByTagName("*")[0]==a;},
+ "last-child": function(a){return jQuery.nth(a.parentNode.lastChild,1,"previousSibling")==a;},
+ "only-child": function(a){return !jQuery.nth(a.parentNode.lastChild,2,"previousSibling");},
+
+ // Parent Checks
+ parent: function(a){return a.firstChild;},
+ empty: function(a){return !a.firstChild;},
+
+ // Text Check
+ contains: function(a,i,m){return (a.textContent||a.innerText||jQuery(a).text()||"").indexOf(m[3])>=0;},
+
+ // Visibility
+ visible: function(a){return "hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},
+ hidden: function(a){return "hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},
+
+ // Form attributes
+ enabled: function(a){return !a.disabled;},
+ disabled: function(a){return a.disabled;},
+ checked: function(a){return a.checked;},
+ selected: function(a){return a.selected||jQuery.attr(a,"selected");},
+
+ // Form elements
+ text: function(a){return "text"==a.type;},
+ radio: function(a){return "radio"==a.type;},
+ checkbox: function(a){return "checkbox"==a.type;},
+ file: function(a){return "file"==a.type;},
+ password: function(a){return "password"==a.type;},
+ submit: function(a){return "submit"==a.type;},
+ image: function(a){return "image"==a.type;},
+ reset: function(a){return "reset"==a.type;},
+ button: function(a){return "button"==a.type||jQuery.nodeName(a,"button");},
+ input: function(a){return /input|select|textarea|button/i.test(a.nodeName);},
+
+ // :has()
+ has: function(a,i,m){return jQuery.find(m[3],a).length;},
+
+ // :header
+ header: function(a){return /h\d/i.test(a.nodeName);},
+
+ // :animated
+ animated: function(a){return jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length;}
+ }
+ },
+
+ // The regular expressions that power the parsing engine
+ parse: [
+ // Match: [@value='test'], [@foo]
+ /^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,
+
+ // Match: :contains('foo')
+ /^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,
+
+ // Match: :even, :last-child, #id, .class
+ new RegExp("^([:.#]*)(" + chars + "+)")
+ ],
+
+ multiFilter: function( expr, elems, not ) {
+ var old, cur = [];
+
+ while ( expr && expr != old ) {
+ old = expr;
+ var f = jQuery.filter( expr, elems, not );
+ expr = f.t.replace(/^\s*,\s*/, "" );
+ cur = not ? elems = f.r : jQuery.merge( cur, f.r );
+ }
+
+ return cur;
+ },
+
+ find: function( t, context ) {
+ // Quickly handle non-string expressions
+ if ( typeof t != "string" )
+ return [ t ];
+
+ // check to make sure context is a DOM element or a document
+ if ( context && context.nodeType != 1 && context.nodeType != 9)
+ return [ ];
+
+ // Set the correct context (if none is provided)
+ context = context || document;
+
+ // Initialize the search
+ var ret = [context], done = [], last, nodeName;
+
+ // Continue while a selector expression exists, and while
+ // we're no longer looping upon ourselves
+ while ( t && last != t ) {
+ var r = [];
+ last = t;
+
+ t = jQuery.trim(t);
+
+ var foundToken = false,
+
+ // An attempt at speeding up child selectors that
+ // point to a specific element tag
+ re = quickChild,
+
+ m = re.exec(t);
+
+ if ( m ) {
+ nodeName = m[1].toUpperCase();
+
+ // Perform our own iteration and filter
+ for ( var i = 0; ret[i]; i++ )
+ for ( var c = ret[i].firstChild; c; c = c.nextSibling )
+ if ( c.nodeType == 1 && (nodeName == "*" || c.nodeName.toUpperCase() == nodeName) )
+ r.push( c );
+
+ ret = r;
+ t = t.replace( re, "" );
+ if ( t.indexOf(" ") == 0 ) continue;
+ foundToken = true;
+ } else {
+ re = /^([>+~])\s*(\w*)/i;
+
+ if ( (m = re.exec(t)) != null ) {
+ r = [];
+
+ var merge = {};
+ nodeName = m[2].toUpperCase();
+ m = m[1];
+
+ for ( var j = 0, rl = ret.length; j < rl; j++ ) {
+ var n = m == "~" || m == "+" ? ret[j].nextSibling : ret[j].firstChild;
+ for ( ; n; n = n.nextSibling )
+ if ( n.nodeType == 1 ) {
+ var id = jQuery.data(n);
+
+ if ( m == "~" && merge[id] ) break;
+
+ if (!nodeName || n.nodeName.toUpperCase() == nodeName ) {
+ if ( m == "~" ) merge[id] = true;
+ r.push( n );
+ }
+
+ if ( m == "+" ) break;
+ }
+ }
+
+ ret = r;
+
+ // And remove the token
+ t = jQuery.trim( t.replace( re, "" ) );
+ foundToken = true;
+ }
+ }
+
+ // See if there's still an expression, and that we haven't already
+ // matched a token
+ if ( t && !foundToken ) {
+ // Handle multiple expressions
+ if ( !t.indexOf(",") ) {
+ // Clean the result set
+ if ( context == ret[0] ) ret.shift();
+
+ // Merge the result sets
+ done = jQuery.merge( done, ret );
+
+ // Reset the context
+ r = ret = [context];
+
+ // Touch up the selector string
+ t = " " + t.substr(1,t.length);
+
+ } else {
+ // Optimize for the case nodeName#idName
+ var re2 = quickID;
+ var m = re2.exec(t);
+
+ // Re-organize the results, so that they're consistent
+ if ( m ) {
+ m = [ 0, m[2], m[3], m[1] ];
+
+ } else {
+ // Otherwise, do a traditional filter check for
+ // ID, class, and element selectors
+ re2 = quickClass;
+ m = re2.exec(t);
+ }
+
+ m[2] = m[2].replace(/\\/g, "");
+
+ var elem = ret[ret.length-1];
+
+ // Try to do a global search by ID, where we can
+ if ( m[1] == "#" && elem && elem.getElementById && !jQuery.isXMLDoc(elem) ) {
+ // Optimization for HTML document case
+ var oid = elem.getElementById(m[2]);
+
+ // Do a quick check for the existence of the actual ID attribute
+ // to avoid selecting by the name attribute in IE
+ // also check to insure id is a string to avoid selecting an element with the name of 'id' inside a form
+ if ( (jQuery.browser.msie||jQuery.browser.opera) && oid && typeof oid.id == "string" && oid.id != m[2] )
+ oid = jQuery('[@id="'+m[2]+'"]', elem)[0];
+
+ // Do a quick check for node name (where applicable) so
+ // that div#foo searches will be really fast
+ ret = r = oid && (!m[3] || jQuery.nodeName(oid, m[3])) ? [oid] : [];
+ } else {
+ // We need to find all descendant elements
+ for ( var i = 0; ret[i]; i++ ) {
+ // Grab the tag name being searched for
+ var tag = m[1] == "#" && m[3] ? m[3] : m[1] != "" || m[0] == "" ? "*" : m[2];
+
+ // Handle IE7 being really dumb about <object>s
+ if ( tag == "*" && ret[i].nodeName.toLowerCase() == "object" )
+ tag = "param";
+
+ r = jQuery.merge( r, ret[i].getElementsByTagName( tag ));
+ }
+
+ // It's faster to filter by class and be done with it
+ if ( m[1] == "." )
+ r = jQuery.classFilter( r, m[2] );
+
+ // Same with ID filtering
+ if ( m[1] == "#" ) {
+ var tmp = [];
+
+ // Try to find the element with the ID
+ for ( var i = 0; r[i]; i++ )
+ if ( r[i].getAttribute("id") == m[2] ) {
+ tmp = [ r[i] ];
+ break;
+ }
+
+ r = tmp;
+ }
+
+ ret = r;
+ }
+
+ t = t.replace( re2, "" );
+ }
+
+ }
+
+ // If a selector string still exists
+ if ( t ) {
+ // Attempt to filter it
+ var val = jQuery.filter(t,r);
+ ret = r = val.r;
+ t = jQuery.trim(val.t);
+ }
+ }
+
+ // An error occurred with the selector;
+ // just return an empty set instead
+ if ( t )
+ ret = [];
+
+ // Remove the root context
+ if ( ret && context == ret[0] )
+ ret.shift();
+
+ // And combine the results
+ done = jQuery.merge( done, ret );
+
+ return done;
+ },
+
+ classFilter: function(r,m,not){
+ m = " " + m + " ";
+ var tmp = [];
+ for ( var i = 0; r[i]; i++ ) {
+ var pass = (" " + r[i].className + " ").indexOf( m ) >= 0;
+ if ( !not && pass || not && !pass )
+ tmp.push( r[i] );
+ }
+ return tmp;
+ },
+
+ filter: function(t,r,not) {
+ var last;
+
+ // Look for common filter expressions
+ while ( t && t != last ) {
+ last = t;
+
+ var p = jQuery.parse, m;
+
+ for ( var i = 0; p[i]; i++ ) {
+ m = p[i].exec( t );
+
+ if ( m ) {
+ // Remove what we just matched
+ t = t.substring( m[0].length );
+
+ m[2] = m[2].replace(/\\/g, "");
+ break;
+ }
+ }
+
+ if ( !m )
+ break;
+
+ // :not() is a special case that can be optimized by
+ // keeping it out of the expression list
+ if ( m[1] == ":" && m[2] == "not" )
+ // optimize if only one selector found (most common case)
+ r = isSimple.test( m[3] ) ?
+ jQuery.filter(m[3], r, true).r :
+ jQuery( r ).not( m[3] );
+
+ // We can get a big speed boost by filtering by class here
+ else if ( m[1] == "." )
+ r = jQuery.classFilter(r, m[2], not);
+
+ else if ( m[1] == "[" ) {
+ var tmp = [], type = m[3];
+
+ for ( var i = 0, rl = r.length; i < rl; i++ ) {
+ var a = r[i], z = a[ jQuery.props[m[2]] || m[2] ];
+
+ if ( z == null || /href|src|selected/.test(m[2]) )
+ z = jQuery.attr(a,m[2]) || '';
+
+ if ( (type == "" && !!z ||
+ type == "=" && z == m[5] ||
+ type == "!=" && z != m[5] ||
+ type == "^=" && z && !z.indexOf(m[5]) ||
+ type == "$=" && z.substr(z.length - m[5].length) == m[5] ||
+ (type == "*=" || type == "~=") && z.indexOf(m[5]) >= 0) ^ not )
+ tmp.push( a );
+ }
+
+ r = tmp;
+
+ // We can get a speed boost by handling nth-child here
+ } else if ( m[1] == ":" && m[2] == "nth-child" ) {
+ var merge = {}, tmp = [],
+ // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
+ test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
+ m[3] == "even" && "2n" || m[3] == "odd" && "2n+1" ||
+ !/\D/.test(m[3]) && "0n+" + m[3] || m[3]),
+ // calculate the numbers (first)n+(last) including if they are negative
+ first = (test[1] + (test[2] || 1)) - 0, last = test[3] - 0;
+
+ // loop through all the elements left in the jQuery object
+ for ( var i = 0, rl = r.length; i < rl; i++ ) {
+ var node = r[i], parentNode = node.parentNode, id = jQuery.data(parentNode);
+
+ if ( !merge[id] ) {
+ var c = 1;
+
+ for ( var n = parentNode.firstChild; n; n = n.nextSibling )
+ if ( n.nodeType == 1 )
+ n.nodeIndex = c++;
+
+ merge[id] = true;
+ }
+
+ var add = false;
+
+ if ( first == 0 ) {
+ if ( node.nodeIndex == last )
+ add = true;
+ } else if ( (node.nodeIndex - last) % first == 0 && (node.nodeIndex - last) / first >= 0 )
+ add = true;
+
+ if ( add ^ not )
+ tmp.push( node );
+ }
+
+ r = tmp;
+
+ // Otherwise, find the expression to execute
+ } else {
+ var fn = jQuery.expr[ m[1] ];
+ if ( typeof fn == "object" )
+ fn = fn[ m[2] ];
+
+ if ( typeof fn == "string" )
+ fn = eval("false||function(a,i){return " + fn + ";}");
+
+ // Execute it against the current filter
+ r = jQuery.grep( r, function(elem, i){
+ return fn(elem, i, m, r);
+ }, not );
+ }
+ }
+
+ // Return an array of filtered elements (r)
+ // and the modified expression string (t)
+ return { r: r, t: t };
+ },
+
+ dir: function( elem, dir ){
+ var matched = [],
+ cur = elem[dir];
+ while ( cur && cur != document ) {
+ if ( cur.nodeType == 1 )
+ matched.push( cur );
+ cur = cur[dir];
+ }
+ return matched;
+ },
+
+ nth: function(cur,result,dir,elem){
+ result = result || 1;
+ var num = 0;
+
+ for ( ; cur; cur = cur[dir] )
+ if ( cur.nodeType == 1 && ++num == result )
+ break;
+
+ return cur;
+ },
+
+ sibling: function( n, elem ) {
+ var r = [];
+
+ for ( ; n; n = n.nextSibling ) {
+ if ( n.nodeType == 1 && n != elem )
+ r.push( n );
+ }
+
+ return r;
+ }
+});
+/*
+ * A number of helper functions used for managing events.
+ * Many of the ideas behind this code orignated from
+ * Dean Edwards' addEvent library.
+ */
+jQuery.event = {
+
+ // Bind an event to an element
+ // Original by Dean Edwards
+ add: function(elem, types, handler, data) {
+ if ( elem.nodeType == 3 || elem.nodeType == 8 )
+ return;
+
+ // For whatever reason, IE has trouble passing the window object
+ // around, causing it to be cloned in the process
+ if ( jQuery.browser.msie && elem.setInterval )
+ elem = window;
+
+ // Make sure that the function being executed has a unique ID
+ if ( !handler.guid )
+ handler.guid = this.guid++;
+
+ // if data is passed, bind to handler
+ if( data != undefined ) {
+ // Create temporary function pointer to original handler
+ var fn = handler;
+
+ // Create unique handler function, wrapped around original handler
+ handler = this.proxy( fn, function() {
+ // Pass arguments and context to original handler
+ return fn.apply(this, arguments);
+ });
+
+ // Store data in unique handler
+ handler.data = data;
+ }
+
+ // Init the element's event structure
+ var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
+ handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
+ // Handle the second event of a trigger and when
+ // an event is called after a page has unloaded
+ if ( typeof jQuery != "undefined" && !jQuery.event.triggered )
+ return jQuery.event.handle.apply(arguments.callee.elem, arguments);
+ });
+ // Add elem as a property of the handle function
+ // This is to prevent a memory leak with non-native
+ // event in IE.
+ handle.elem = elem;
+
+ // Handle multiple events separated by a space
+ // jQuery(...).bind("mouseover mouseout", fn);
+ jQuery.each(types.split(/\s+/), function(index, type) {
+ // Namespaced event handlers
+ var parts = type.split(".");
+ type = parts[0];
+ handler.type = parts[1];
+
+ // Get the current list of functions bound to this event
+ var handlers = events[type];
+
+ // Init the event handler queue
+ if (!handlers) {
+ handlers = events[type] = {};
+
+ // Check for a special event handler
+ // Only use addEventListener/attachEvent if the special
+ // events handler returns false
+ if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem) === false ) {
+ // Bind the global event handler to the element
+ if (elem.addEventListener)
+ elem.addEventListener(type, handle, false);
+ else if (elem.attachEvent)
+ elem.attachEvent("on" + type, handle);
+ }
+ }
+
+ // Add the function to the element's handler list
+ handlers[handler.guid] = handler;
+
+ // Keep track of which events have been used, for global triggering
+ jQuery.event.global[type] = true;
+ });
+
+ // Nullify elem to prevent memory leaks in IE
+ elem = null;
+ },
+
+ guid: 1,
+ global: {},
+
+ // Detach an event or set of events from an element
+ remove: function(elem, types, handler) {
+ // don't do events on text and comment nodes
+ if ( elem.nodeType == 3 || elem.nodeType == 8 )
+ return;
+
+ var events = jQuery.data(elem, "events"), ret, index;
+
+ if ( events ) {
+ // Unbind all events for the element
+ if ( types == undefined || (typeof types == "string" && types.charAt(0) == ".") )
+ for ( var type in events )
+ this.remove( elem, type + (types || "") );
+ else {
+ // types is actually an event object here
+ if ( types.type ) {
+ handler = types.handler;
+ types = types.type;
+ }
+
+ // Handle multiple events seperated by a space
+ // jQuery(...).unbind("mouseover mouseout", fn);
+ jQuery.each(types.split(/\s+/), function(index, type){
+ // Namespaced event handlers
+ var parts = type.split(".");
+ type = parts[0];
+
+ if ( events[type] ) {
+ // remove the given handler for the given type
+ if ( handler )
+ delete events[type][handler.guid];
+
+ // remove all handlers for the given type
+ else
+ for ( handler in events[type] )
+ // Handle the removal of namespaced events
+ if ( !parts[1] || events[type][handler].type == parts[1] )
+ delete events[type][handler];
+
+ // remove generic event handler if no more handlers exist
+ for ( ret in events[type] ) break;
+ if ( !ret ) {
+ if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem) === false ) {
+ if (elem.removeEventListener)
+ elem.removeEventListener(type, jQuery.data(elem, "handle"), false);
+ else if (elem.detachEvent)
+ elem.detachEvent("on" + type, jQuery.data(elem, "handle"));
+ }
+ ret = null;
+ delete events[type];
+ }
+ }
+ });
+ }
+
+ // Remove the expando if it's no longer used
+ for ( ret in events ) break;
+ if ( !ret ) {
+ var handle = jQuery.data( elem, "handle" );
+ if ( handle ) handle.elem = null;
+ jQuery.removeData( elem, "events" );
+ jQuery.removeData( elem, "handle" );
+ }
+ }
+ },
+
+ trigger: function(type, data, elem, donative, extra) {
+ // Clone the incoming data, if any
+ data = jQuery.makeArray(data);
+
+ if ( type.indexOf("!") >= 0 ) {
+ type = type.slice(0, -1);
+ var exclusive = true;
+ }
+
+ // Handle a global trigger
+ if ( !elem ) {
+ // Only trigger if we've ever bound an event for it
+ if ( this.global[type] )
+ jQuery("*").add([window, document]).trigger(type, data);
+
+ // Handle triggering a single element
+ } else {
+ // don't do events on text and comment nodes
+ if ( elem.nodeType == 3 || elem.nodeType == 8 )
+ return undefined;
+
+ var val, ret, fn = jQuery.isFunction( elem[ type ] || null ),
+ // Check to see if we need to provide a fake event, or not
+ event = !data[0] || !data[0].preventDefault;
+
+ // Pass along a fake event
+ if ( event ) {
+ data.unshift({
+ type: type,
+ target: elem,
+ preventDefault: function(){},
+ stopPropagation: function(){},
+ timeStamp: now()
+ });
+ data[0][expando] = true; // no need to fix fake event
+ }
+
+ // Enforce the right trigger type
+ data[0].type = type;
+ if ( exclusive )
+ data[0].exclusive = true;
+
+ // Trigger the event, it is assumed that "handle" is a function
+ var handle = jQuery.data(elem, "handle");
+ if ( handle )
+ val = handle.apply( elem, data );
+
+ // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
+ if ( (!fn || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )
+ val = false;
+
+ // Extra functions don't get the custom event object
+ if ( event )
+ data.shift();
+
+ // Handle triggering of extra function
+ if ( extra && jQuery.isFunction( extra ) ) {
+ // call the extra function and tack the current return value on the end for possible inspection
+ ret = extra.apply( elem, val == null ? data : data.concat( val ) );
+ // if anything is returned, give it precedence and have it overwrite the previous value
+ if (ret !== undefined)
+ val = ret;
+ }
+
+ // Trigger the native events (except for clicks on links)
+ if ( fn && donative !== false && val !== false && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
+ this.triggered = true;
+ try {
+ elem[ type ]();
+ // prevent IE from throwing an error for some hidden elements
+ } catch (e) {}
+ }
+
+ this.triggered = false;
+ }
+
+ return val;
+ },
+
+ handle: function(event) {
+ // returned undefined or false
+ var val, ret, namespace, all, handlers;
+
+ event = arguments[0] = jQuery.event.fix( event || window.event );
+
+ // Namespaced event handlers
+ namespace = event.type.split(".");
+ event.type = namespace[0];
+ namespace = namespace[1];
+ // Cache this now, all = true means, any handler
+ all = !namespace && !event.exclusive;
+
+ handlers = ( jQuery.data(this, "events") || {} )[event.type];
+
+ for ( var j in handlers ) {
+ var handler = handlers[j];
+
+ // Filter the functions by class
+ if ( all || handler.type == namespace ) {
+ // Pass in a reference to the handler function itself
+ // So that we can later remove it
+ event.handler = handler;
+ event.data = handler.data;
+
+ ret = handler.apply( this, arguments );
+
+ if ( val !== false )
+ val = ret;
+
+ if ( ret === false ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+ }
+
+ return val;
+ },
+
+ fix: function(event) {
+ if ( event[expando] == true )
+ return event;
+
+ // store a copy of the original event object
+ // and "clone" to set read-only properties
+ var originalEvent = event;
+ event = { originalEvent: originalEvent };
+ var props = "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target timeStamp toElement type view wheelDelta which".split(" ");
+ for ( var i=props.length; i; i-- )
+ event[ props[i] ] = originalEvent[ props[i] ];
+
+ // Mark it as fixed
+ event[expando] = true;
+
+ // add preventDefault and stopPropagation since
+ // they will not work on the clone
+ event.preventDefault = function() {
+ // if preventDefault exists run it on the original event
+ if (originalEvent.preventDefault)
+ originalEvent.preventDefault();
+ // otherwise set the returnValue property of the original event to false (IE)
+ originalEvent.returnValue = false;
+ };
+ event.stopPropagation = function() {
+ // if stopPropagation exists run it on the original event
+ if (originalEvent.stopPropagation)
+ originalEvent.stopPropagation();
+ // otherwise set the cancelBubble property of the original event to true (IE)
+ originalEvent.cancelBubble = true;
+ };
+
+ // Fix timeStamp
+ event.timeStamp = event.timeStamp || now();
+
+ // Fix target property, if necessary
+ if ( !event.target )
+ event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
+
+ // check if target is a textnode (safari)
+ if ( event.target.nodeType == 3 )
+ event.target = event.target.parentNode;
+
+ // Add relatedTarget, if necessary
+ if ( !event.relatedTarget && event.fromElement )
+ event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
+
+ // Calculate pageX/Y if missing and clientX/Y available
+ if ( event.pageX == null && event.clientX != null ) {
+ var doc = document.documentElement, body = document.body;
+ event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
+ event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
+ }
+
+ // Add which for key events
+ if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
+ event.which = event.charCode || event.keyCode;
+
+ // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
+ if ( !event.metaKey && event.ctrlKey )
+ event.metaKey = event.ctrlKey;
+
+ // Add which for click: 1 == left; 2 == middle; 3 == right
+ // Note: button is not normalized, so don't use it
+ if ( !event.which && event.button )
+ event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
+
+ return event;
+ },
+
+ proxy: function( fn, proxy ){
+ // Set the guid of unique handler to the same of original handler, so it can be removed
+ proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
+ // So proxy can be declared as an argument
+ return proxy;
+ },
+
+ special: {
+ ready: {
+ setup: function() {
+ // Make sure the ready event is setup
+ bindReady();
+ return;
+ },
+
+ teardown: function() { return; }
+ },
+
+ mouseenter: {
+ setup: function() {
+ if ( jQuery.browser.msie ) return false;
+ jQuery(this).bind("mouseover", jQuery.event.special.mouseenter.handler);
+ return true;
+ },
+
+ teardown: function() {
+ if ( jQuery.browser.msie ) return false;
+ jQuery(this).unbind("mouseover", jQuery.event.special.mouseenter.handler);
+ return true;
+ },
+
+ handler: function(event) {
+ // If we actually just moused on to a sub-element, ignore it
+ if ( withinElement(event, this) ) return true;
+ // Execute the right handlers by setting the event type to mouseenter
+ event.type = "mouseenter";
+ return jQuery.event.handle.apply(this, arguments);
+ }
+ },
+
+ mouseleave: {
+ setup: function() {
+ if ( jQuery.browser.msie ) return false;
+ jQuery(this).bind("mouseout", jQuery.event.special.mouseleave.handler);
+ return true;
+ },
+
+ teardown: function() {
+ if ( jQuery.browser.msie ) return false;
+ jQuery(this).unbind("mouseout", jQuery.event.special.mouseleave.handler);
+ return true;
+ },
+
+ handler: function(event) {
+ // If we actually just moused on to a sub-element, ignore it
+ if ( withinElement(event, this) ) return true;
+ // Execute the right handlers by setting the event type to mouseleave
+ event.type = "mouseleave";
+ return jQuery.event.handle.apply(this, arguments);
+ }
+ }
+ }
+};
+
+jQuery.fn.extend({
+ bind: function( type, data, fn ) {
+ return type == "unload" ? this.one(type, data, fn) : this.each(function(){
+ jQuery.event.add( this, type, fn || data, fn && data );
+ });
+ },
+
+ one: function( type, data, fn ) {
+ var one = jQuery.event.proxy( fn || data, function(event) {
+ jQuery(this).unbind(event, one);
+ return (fn || data).apply( this, arguments );
+ });
+ return this.each(function(){
+ jQuery.event.add( this, type, one, fn && data);
+ });
+ },
+
+ unbind: function( type, fn ) {
+ return this.each(function(){
+ jQuery.event.remove( this, type, fn );
+ });
+ },
+
+ trigger: function( type, data, fn ) {
+ return this.each(function(){
+ jQuery.event.trigger( type, data, this, true, fn );
+ });
+ },
+
+ triggerHandler: function( type, data, fn ) {
+ return this[0] && jQuery.event.trigger( type, data, this[0], false, fn );
+ },
+
+ toggle: function( fn ) {
+ // Save reference to arguments for access in closure
+ var args = arguments, i = 1;
+
+ // link all the functions, so any of them can unbind this click handler
+ while( i < args.length )
+ jQuery.event.proxy( fn, args[i++] );
+
+ return this.click( jQuery.event.proxy( fn, function(event) {
+ // Figure out which function to execute
+ this.lastToggle = ( this.lastToggle || 0 ) % i;
+
+ // Make sure that clicks stop
+ event.preventDefault();
+
+ // and execute the function
+ return args[ this.lastToggle++ ].apply( this, arguments ) || false;
+ }));
+ },
+
+ hover: function(fnOver, fnOut) {
+ return this.bind('mouseenter', fnOver).bind('mouseleave', fnOut);
+ },
+
+ ready: function(fn) {
+ // Attach the listeners
+ bindReady();
+
+ // If the DOM is already ready
+ if ( jQuery.isReady )
+ // Execute the function immediately
+ fn.call( document, jQuery );
+
+ // Otherwise, remember the function for later
+ else
+ // Add the function to the wait list
+ jQuery.readyList.push( function() { return fn.call(this, jQuery); } );
+
+ return this;
+ }
+});
+
+jQuery.extend({
+ isReady: false,
+ readyList: [],
+ // Handle when the DOM is ready
+ ready: function() {
+ // Make sure that the DOM is not already loaded
+ if ( !jQuery.isReady ) {
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If there are functions bound, to execute
+ if ( jQuery.readyList ) {
+ // Execute all of them
+ jQuery.each( jQuery.readyList, function(){
+ this.call( document );
+ });
+
+ // Reset the list of functions
+ jQuery.readyList = null;
+ }
+
+ // Trigger any bound ready events
+ jQuery(document).triggerHandler("ready");
+ }
+ }
+});
+
+var readyBound = false;
+
+function bindReady(){
+ if ( readyBound ) return;
+ readyBound = true;
+
+ // Mozilla, Opera (see further below for it) and webkit nightlies currently support this event
+ if ( document.addEventListener && !jQuery.browser.opera)
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", jQuery.ready, false );
+
+ // If IE is used and is not in a frame
+ // Continually check to see if the document is ready
+ if ( jQuery.browser.msie && window == top ) (function(){
+ if (jQuery.isReady) return;
+ try {
+ // If IE is used, use the trick by Diego Perini
+ // http://javascript.nwbox.com/IEContentLoaded/
+ document.documentElement.doScroll("left");
+ } catch( error ) {
+ setTimeout( arguments.callee, 0 );
+ return;
+ }
+ // and execute any waiting functions
+ jQuery.ready();
+ })();
+
+ if ( jQuery.browser.opera )
+ document.addEventListener( "DOMContentLoaded", function () {
+ if (jQuery.isReady) return;
+ for (var i = 0; i < document.styleSheets.length; i++)
+ if (document.styleSheets[i].disabled) {
+ setTimeout( arguments.callee, 0 );
+ return;
+ }
+ // and execute any waiting functions
+ jQuery.ready();
+ }, false);
+
+ if ( jQuery.browser.safari ) {
+ var numStyles;
+ (function(){
+ if (jQuery.isReady) return;
+ if ( document.readyState != "loaded" && document.readyState != "complete" ) {
+ setTimeout( arguments.callee, 0 );
+ return;
+ }
+ if ( numStyles === undefined )
+ numStyles = jQuery("style, link[rel=stylesheet]").length;
+ if ( document.styleSheets.length != numStyles ) {
+ setTimeout( arguments.callee, 0 );
+ return;
+ }
+ // and execute any waiting functions
+ jQuery.ready();
+ })();
+ }
+
+ // A fallback to window.onload, that will always work
+ jQuery.event.add( window, "load", jQuery.ready );
+}
+
+jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
+ "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," +
+ "submit,keydown,keypress,keyup,error").split(","), function(i, name){
+
+ // Handle event binding
+ jQuery.fn[name] = function(fn){
+ return fn ? this.bind(name, fn) : this.trigger(name);
+ };
+});
+
+// Checks if an event happened on an element within another element
+// Used in jQuery.event.special.mouseenter and mouseleave handlers
+var withinElement = function(event, elem) {
+ // Check if mouse(over|out) are still within the same parent element
+ var parent = event.relatedTarget;
+ // Traverse up the tree
+ while ( parent && parent != elem ) try { parent = parent.parentNode; } catch(error) { parent = elem; }
+ // Return true if we actually just moused on to a sub-element
+ return parent == elem;
+};
+
+// Prevent memory leaks in IE
+// And prevent errors on refresh with events like mouseover in other browsers
+// Window isn't included so as not to unbind existing unload events
+jQuery(window).bind("unload", function() {
+ jQuery("*").add(document).unbind();
+});
+jQuery.fn.extend({
+ // Keep a copy of the old load
+ _load: jQuery.fn.load,
+
+ load: function( url, params, callback ) {
+ if ( typeof url != 'string' )
+ return this._load( url );
+
+ var off = url.indexOf(" ");
+ if ( off >= 0 ) {
+ var selector = url.slice(off, url.length);
+ url = url.slice(0, off);
+ }
+
+ callback = callback || function(){};
+
+ // Default to a GET request
+ var type = "GET";
+
+ // If the second parameter was provided
+ if ( params )
+ // If it's a function
+ if ( jQuery.isFunction( params ) ) {
+ // We assume that it's the callback
+ callback = params;
+ params = null;
+
+ // Otherwise, build a param string
+ } else {
+ params = jQuery.param( params );
+ type = "POST";
+ }
+
+ var self = this;
+
+ // Request the remote document
+ jQuery.ajax({
+ url: url,
+ type: type,
+ dataType: "html",
+ data: params,
+ complete: function(res, status){
+ // If successful, inject the HTML into all the matched elements
+ if ( status == "success" || status == "notmodified" )
+ // See if a selector was specified
+ self.html( selector ?
+ // Create a dummy div to hold the results
+ jQuery("<div/>")
+ // inject the contents of the document in, removing the scripts
+ // to avoid any 'Permission Denied' errors in IE
+ .append(res.responseText.replace(/<script(.|\s)*?\/script>/g, ""))
+
+ // Locate the specified elements
+ .find(selector) :
+
+ // If not, just inject the full result
+ res.responseText );
+
+ self.each( callback, [res.responseText, status, res] );
+ }
+ });
+ return this;
+ },
+
+ serialize: function() {
+ return jQuery.param(this.serializeArray());
+ },
+ serializeArray: function() {
+ return this.map(function(){
+ return jQuery.nodeName(this, "form") ?
+ jQuery.makeArray(this.elements) : this;
+ })
+ .filter(function(){
+ return this.name && !this.disabled &&
+ (this.checked || /select|textarea/i.test(this.nodeName) ||
+ /text|hidden|password/i.test(this.type));
+ })
+ .map(function(i, elem){
+ var val = jQuery(this).val();
+ return val == null ? null :
+ val.constructor == Array ?
+ jQuery.map( val, function(val, i){
+ return {name: elem.name, value: val};
+ }) :
+ {name: elem.name, value: val};
+ }).get();
+ }
+});
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){
+ jQuery.fn[o] = function(f){
+ return this.bind(o, f);
+ };
+});
+
+var jsc = now();
+
+jQuery.extend({
+ get: function( url, data, callback, type ) {
+ // shift arguments if data argument was ommited
+ if ( jQuery.isFunction( data ) ) {
+ callback = data;
+ data = null;
+ }
+
+ return jQuery.ajax({
+ type: "GET",
+ url: url,
+ data: data,
+ success: callback,
+ dataType: type
+ });
+ },
+
+ getScript: function( url, callback ) {
+ return jQuery.get(url, null, callback, "script");
+ },
+
+ getJSON: function( url, data, callback ) {
+ return jQuery.get(url, data, callback, "json");
+ },
+
+ post: function( url, data, callback, type ) {
+ if ( jQuery.isFunction( data ) ) {
+ callback = data;
+ data = {};
+ }
+
+ return jQuery.ajax({
+ type: "POST",
+ url: url,
+ data: data,
+ success: callback,
+ dataType: type
+ });
+ },
+
+ ajaxSetup: function( settings ) {
+ jQuery.extend( jQuery.ajaxSettings, settings );
+ },
+
+ ajaxSettings: {
+ url: location.href,
+ global: true,
+ type: "GET",
+ timeout: 0,
+ contentType: "application/x-www-form-urlencoded",
+ processData: true,
+ async: true,
+ data: null,
+ username: null,
+ password: null,
+ accepts: {
+ xml: "application/xml, text/xml",
+ html: "text/html",
+ script: "text/javascript, application/javascript",
+ json: "application/json, text/javascript",
+ text: "text/plain",
+ _default: "*/*"
+ }
+ },
+
+ // Last-Modified header cache for next request
+ lastModified: {},
+
+ ajax: function( s ) {
+ // Extend the settings, but re-extend 's' so that it can be
+ // checked again later (in the test suite, specifically)
+ s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));
+
+ var jsonp, jsre = /=\?(&|$)/g, status, data,
+ type = s.type.toUpperCase();
+
+ // convert data if not already a string
+ if ( s.data && s.processData && typeof s.data != "string" )
+ s.data = jQuery.param(s.data);
+
+ // Handle JSONP Parameter Callbacks
+ if ( s.dataType == "jsonp" ) {
+ if ( type == "GET" ) {
+ if ( !s.url.match(jsre) )
+ s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?";
+ } else if ( !s.data || !s.data.match(jsre) )
+ s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
+ s.dataType = "json";
+ }
+
+ // Build temporary JSONP function
+ if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
+ jsonp = "jsonp" + jsc++;
+
+ // Replace the =? sequence both in the query string and the data
+ if ( s.data )
+ s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
+ s.url = s.url.replace(jsre, "=" + jsonp + "$1");
+
+ // We need to make sure
+ // that a JSONP style response is executed properly
+ s.dataType = "script";
+
+ // Handle JSONP-style loading
+ window[ jsonp ] = function(tmp){
+ data = tmp;
+ success();
+ complete();
+ // Garbage collect
+ window[ jsonp ] = undefined;
+ try{ delete window[ jsonp ]; } catch(e){}
+ if ( head )
+ head.removeChild( script );
+ };
+ }
+
+ if ( s.dataType == "script" && s.cache == null )
+ s.cache = false;
+
+ if ( s.cache === false && type == "GET" ) {
+ var ts = now();
+ // try replacing _= if it is there
+ var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2");
+ // if nothing was replaced, add timestamp to the end
+ s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : "");
+ }
+
+ // If data is available, append data to url for get requests
+ if ( s.data && type == "GET" ) {
+ s.url += (s.url.match(/\?/) ? "&" : "?") + s.data;
+
+ // IE likes to send both get and post data, prevent this
+ s.data = null;
+ }
+
+ // Watch for a new set of requests
+ if ( s.global && ! jQuery.active++ )
+ jQuery.event.trigger( "ajaxStart" );
+
+ // Matches an absolute URL, and saves the domain
+ var remote = /^(?:\w+:)?\/\/([^\/?#]+)/;
+
+ // If we're requesting a remote document
+ // and trying to load JSON or Script with a GET
+ if ( s.dataType == "script" && type == "GET"
+ && remote.test(s.url) && remote.exec(s.url)[1] != location.host ){
+ var head = document.getElementsByTagName("head")[0];
+ var script = document.createElement("script");
+ script.src = s.url;
+ if (s.scriptCharset)
+ script.charset = s.scriptCharset;
+
+ // Handle Script loading
+ if ( !jsonp ) {
+ var done = false;
+
+ // Attach handlers for all browsers
+ script.onload = script.onreadystatechange = function(){
+ if ( !done && (!this.readyState ||
+ this.readyState == "loaded" || this.readyState == "complete") ) {
+ done = true;
+ success();
+ complete();
+ head.removeChild( script );
+ }
+ };
+ }
+
+ head.appendChild(script);
+
+ // We handle everything using the script element injection
+ return undefined;
+ }
+
+ var requestDone = false;
+
+ // Create the request object; Microsoft failed to properly
+ // implement the XMLHttpRequest in IE7, so we use the ActiveXObject when it is available
+ var xhr = window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
+
+ // Open the socket
+ // Passing null username, generates a login popup on Opera (#2865)
+ if( s.username )
+ xhr.open(type, s.url, s.async, s.username, s.password);
+ else
+ xhr.open(type, s.url, s.async);
+
+ // Need an extra try/catch for cross domain requests in Firefox 3
+ try {
+ // Set the correct header, if data is being sent
+ if ( s.data )
+ xhr.setRequestHeader("Content-Type", s.contentType);
+
+ // Set the If-Modified-Since header, if ifModified mode.
+ if ( s.ifModified )
+ xhr.setRequestHeader("If-Modified-Since",
+ jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
+
+ // Set header so the called script knows that it's an XMLHttpRequest
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+
+ // Set the Accepts header for the server, depending on the dataType
+ xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
+ s.accepts[ s.dataType ] + ", */*" :
+ s.accepts._default );
+ } catch(e){}
+
+ // Allow custom headers/mimetypes
+ if ( s.beforeSend && s.beforeSend(xhr, s) === false ) {
+ // cleanup active request counter
+ s.global && jQuery.active--;
+ // close opended socket
+ xhr.abort();
+ return false;
+ }
+
+ if ( s.global )
+ jQuery.event.trigger("ajaxSend", [xhr, s]);
+
+ // Wait for a response to come back
+ var onreadystatechange = function(isTimeout){
+ // The transfer is complete and the data is available, or the request timed out
+ if ( !requestDone && xhr && (xhr.readyState == 4 || isTimeout == "timeout") ) {
+ requestDone = true;
+
+ // clear poll interval
+ if (ival) {
+ clearInterval(ival);
+ ival = null;
+ }
+
+ status = isTimeout == "timeout" && "timeout" ||
+ !jQuery.httpSuccess( xhr ) && "error" ||
+ s.ifModified && jQuery.httpNotModified( xhr, s.url ) && "notmodified" ||
+ "success";
+
+ if ( status == "success" ) {
+ // Watch for, and catch, XML document parse errors
+ try {
+ // process the data (runs the xml through httpData regardless of callback)
+ data = jQuery.httpData( xhr, s.dataType, s.dataFilter );
+ } catch(e) {
+ status = "parsererror";
+ }
+ }
+
+ // Make sure that the request was successful or notmodified
+ if ( status == "success" ) {
+ // Cache Last-Modified header, if ifModified mode.
+ var modRes;
+ try {
+ modRes = xhr.getResponseHeader("Last-Modified");
+ } catch(e) {} // swallow exception thrown by FF if header is not available
+
+ if ( s.ifModified && modRes )
+ jQuery.lastModified[s.url] = modRes;
+
+ // JSONP handles its own success callback
+ if ( !jsonp )
+ success();
+ } else
+ jQuery.handleError(s, xhr, status);
+
+ // Fire the complete handlers
+ complete();
+
+ // Stop memory leaks
+ if ( s.async )
+ xhr = null;
+ }
+ };
+
+ if ( s.async ) {
+ // don't attach the handler to the request, just poll it instead
+ var ival = setInterval(onreadystatechange, 13);
+
+ // Timeout checker
+ if ( s.timeout > 0 )
+ setTimeout(function(){
+ // Check to see if the request is still happening
+ if ( xhr ) {
+ // Cancel the request
+ xhr.abort();
+
+ if( !requestDone )
+ onreadystatechange( "timeout" );
+ }
+ }, s.timeout);
+ }
+
+ // Send the data
+ try {
+ xhr.send(s.data);
+ } catch(e) {
+ jQuery.handleError(s, xhr, null, e);
+ }
+
+ // firefox 1.5 doesn't fire statechange for sync requests
+ if ( !s.async )
+ onreadystatechange();
+
+ function success(){
+ // If a local callback was specified, fire it and pass it the data
+ if ( s.success )
+ s.success( data, status );
+
+ // Fire the global callback
+ if ( s.global )
+ jQuery.event.trigger( "ajaxSuccess", [xhr, s] );
+ }
+
+ function complete(){
+ // Process result
+ if ( s.complete )
+ s.complete(xhr, status);
+
+ // The request was completed
+ if ( s.global )
+ jQuery.event.trigger( "ajaxComplete", [xhr, s] );
+
+ // Handle the global AJAX counter
+ if ( s.global && ! --jQuery.active )
+ jQuery.event.trigger( "ajaxStop" );
+ }
+
+ // return XMLHttpRequest to allow aborting the request etc.
+ return xhr;
+ },
+
+ handleError: function( s, xhr, status, e ) {
+ // If a local callback was specified, fire it
+ if ( s.error ) s.error( xhr, status, e );
+
+ // Fire the global callback
+ if ( s.global )
+ jQuery.event.trigger( "ajaxError", [xhr, s, e] );
+ },
+
+ // Counter for holding the number of active queries
+ active: 0,
+
+ // Determines if an XMLHttpRequest was successful or not
+ httpSuccess: function( xhr ) {
+ try {
+ // IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450
+ return !xhr.status && location.protocol == "file:" ||
+ ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status == 304 || xhr.status == 1223 ||
+ jQuery.browser.safari && xhr.status == undefined;
+ } catch(e){}
+ return false;
+ },
+
+ // Determines if an XMLHttpRequest returns NotModified
+ httpNotModified: function( xhr, url ) {
+ try {
+ var xhrRes = xhr.getResponseHeader("Last-Modified");
+
+ // Firefox always returns 200. check Last-Modified date
+ return xhr.status == 304 || xhrRes == jQuery.lastModified[url] ||
+ jQuery.browser.safari && xhr.status == undefined;
+ } catch(e){}
+ return false;
+ },
+
+ httpData: function( xhr, type, filter ) {
+ var ct = xhr.getResponseHeader("content-type"),
+ xml = type == "xml" || !type && ct && ct.indexOf("xml") >= 0,
+ data = xml ? xhr.responseXML : xhr.responseText;
+
+ if ( xml && data.documentElement.tagName == "parsererror" )
+ throw "parsererror";
+
+ // Allow a pre-filtering function to sanitize the response
+ if( filter )
+ data = filter( data, type );
+
+ // If the type is "script", eval it in global context
+ if ( type == "script" )
+ jQuery.globalEval( data );
+
+ // Get the JavaScript object, if JSON is used.
+ if ( type == "json" )
+ data = eval("(" + data + ")");
+
+ return data;
+ },
+
+ // Serialize an array of form elements or a set of
+ // key/values into a query string
+ param: function( a ) {
+ var s = [];
+
+ // If an array was passed in, assume that it is an array
+ // of form elements
+ if ( a.constructor == Array || a.jquery )
+ // Serialize the form elements
+ jQuery.each( a, function(){
+ s.push( encodeURIComponent(this.name) + "=" + encodeURIComponent( this.value ) );
+ });
+
+ // Otherwise, assume that it's an object of key/value pairs
+ else
+ // Serialize the key/values
+ for ( var j in a )
+ // If the value is an array then the key names need to be repeated
+ if ( a[j] && a[j].constructor == Array )
+ jQuery.each( a[j], function(){
+ s.push( encodeURIComponent(j) + "=" + encodeURIComponent( this ) );
+ });
+ else
+ s.push( encodeURIComponent(j) + "=" + encodeURIComponent( jQuery.isFunction(a[j]) ? a[j]() : a[j] ) );
+
+ // Return the resulting serialization
+ return s.join("&").replace(/%20/g, "+");
+ }
+
+});
+jQuery.fn.extend({
+ show: function(speed,callback){
+ return speed ?
+ this.animate({
+ height: "show", width: "show", opacity: "show"
+ }, speed, callback) :
+
+ this.filter(":hidden").each(function(){
+ this.style.display = this.oldblock || "";
+ if ( jQuery.css(this,"display") == "none" ) {
+ var elem = jQuery("<" + this.tagName + " />").appendTo("body");
+ this.style.display = elem.css("display");
+ // handle an edge condition where css is - div { display:none; } or similar
+ if (this.style.display == "none")
+ this.style.display = "block";
+ elem.remove();
+ }
+ }).end();
+ },
+
+ hide: function(speed,callback){
+ return speed ?
+ this.animate({
+ height: "hide", width: "hide", opacity: "hide"
+ }, speed, callback) :
+
+ this.filter(":visible").each(function(){
+ this.oldblock = this.oldblock || jQuery.css(this,"display");
+ this.style.display = "none";
+ }).end();
+ },
+
+ // Save the old toggle function
+ _toggle: jQuery.fn.toggle,
+
+ toggle: function( fn, fn2 ){
+ return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ?
+ this._toggle.apply( this, arguments ) :
+ fn ?
+ this.animate({
+ height: "toggle", width: "toggle", opacity: "toggle"
+ }, fn, fn2) :
+ this.each(function(){
+ jQuery(this)[ jQuery(this).is(":hidden") ? "show" : "hide" ]();
+ });
+ },
+
+ slideDown: function(speed,callback){
+ return this.animate({height: "show"}, speed, callback);
+ },
+
+ slideUp: function(speed,callback){
+ return this.animate({height: "hide"}, speed, callback);
+ },
+
+ slideToggle: function(speed, callback){
+ return this.animate({height: "toggle"}, speed, callback);
+ },
+
+ fadeIn: function(speed, callback){
+ return this.animate({opacity: "show"}, speed, callback);
+ },
+
+ fadeOut: function(speed, callback){
+ return this.animate({opacity: "hide"}, speed, callback);
+ },
+
+ fadeTo: function(speed,to,callback){
+ return this.animate({opacity: to}, speed, callback);
+ },
+
+ animate: function( prop, speed, easing, callback ) {
+ var optall = jQuery.speed(speed, easing, callback);
+
+ return this[ optall.queue === false ? "each" : "queue" ](function(){
+ if ( this.nodeType != 1)
+ return false;
+
+ var opt = jQuery.extend({}, optall), p,
+ hidden = jQuery(this).is(":hidden"), self = this;
+
+ for ( p in prop ) {
+ if ( prop[p] == "hide" && hidden || prop[p] == "show" && !hidden )
+ return opt.complete.call(this);
+
+ if ( p == "height" || p == "width" ) {
+ // Store display property
+ opt.display = jQuery.css(this, "display");
+
+ // Make sure that nothing sneaks out
+ opt.overflow = this.style.overflow;
+ }
+ }
+
+ if ( opt.overflow != null )
+ this.style.overflow = "hidden";
+
+ opt.curAnim = jQuery.extend({}, prop);
+
+ jQuery.each( prop, function(name, val){
+ var e = new jQuery.fx( self, opt, name );
+
+ if ( /toggle|show|hide/.test(val) )
+ e[ val == "toggle" ? hidden ? "show" : "hide" : val ]( prop );
+ else {
+ var parts = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),
+ start = e.cur(true) || 0;
+
+ if ( parts ) {
+ var end = parseFloat(parts[2]),
+ unit = parts[3] || "px";
+
+ // We need to compute starting value
+ if ( unit != "px" ) {
+ self.style[ name ] = (end || 1) + unit;
+ start = ((end || 1) / e.cur(true)) * start;
+ self.style[ name ] = start + unit;
+ }
+
+ // If a +=/-= token was provided, we're doing a relative animation
+ if ( parts[1] )
+ end = ((parts[1] == "-=" ? -1 : 1) * end) + start;
+
+ e.custom( start, end, unit );
+ } else
+ e.custom( start, val, "" );
+ }
+ });
+
+ // For JS strict compliance
+ return true;
+ });
+ },
+
+ queue: function(type, fn){
+ if ( jQuery.isFunction(type) || ( type && type.constructor == Array )) {
+ fn = type;
+ type = "fx";
+ }
+
+ if ( !type || (typeof type == "string" && !fn) )
+ return queue( this[0], type );
+
+ return this.each(function(){
+ if ( fn.constructor == Array )
+ queue(this, type, fn);
+ else {
+ queue(this, type).push( fn );
+
+ if ( queue(this, type).length == 1 )
+ fn.call(this);
+ }
+ });
+ },
+
+ stop: function(clearQueue, gotoEnd){
+ var timers = jQuery.timers;
+
+ if (clearQueue)
+ this.queue([]);
+
+ this.each(function(){
+ // go in reverse order so anything added to the queue during the loop is ignored
+ for ( var i = timers.length - 1; i >= 0; i-- )
+ if ( timers[i].elem == this ) {
+ if (gotoEnd)
+ // force the next step to be the last
+ timers[i](true);
+ timers.splice(i, 1);
+ }
+ });
+
+ // start the next in the queue if the last step wasn't forced
+ if (!gotoEnd)
+ this.dequeue();
+
+ return this;
+ }
+
+});
+
+var queue = function( elem, type, array ) {
+ if ( elem ){
+
+ type = type || "fx";
+
+ var q = jQuery.data( elem, type + "queue" );
+
+ if ( !q || array )
+ q = jQuery.data( elem, type + "queue", jQuery.makeArray(array) );
+
+ }
+ return q;
+};
+
+jQuery.fn.dequeue = function(type){
+ type = type || "fx";
+
+ return this.each(function(){
+ var q = queue(this, type);
+
+ q.shift();
+
+ if ( q.length )
+ q[0].call( this );
+ });
+};
+
+jQuery.extend({
+
+ speed: function(speed, easing, fn) {
+ var opt = speed && speed.constructor == Object ? speed : {
+ complete: fn || !fn && easing ||
+ jQuery.isFunction( speed ) && speed,
+ duration: speed,
+ easing: fn && easing || easing && easing.constructor != Function && easing
+ };
+
+ opt.duration = (opt.duration && opt.duration.constructor == Number ?
+ opt.duration :
+ jQuery.fx.speeds[opt.duration]) || jQuery.fx.speeds.def;
+
+ // Queueing
+ opt.old = opt.complete;
+ opt.complete = function(){
+ if ( opt.queue !== false )
+ jQuery(this).dequeue();
+ if ( jQuery.isFunction( opt.old ) )
+ opt.old.call( this );
+ };
+
+ return opt;
+ },
+
+ easing: {
+ linear: function( p, n, firstNum, diff ) {
+ return firstNum + diff * p;
+ },
+ swing: function( p, n, firstNum, diff ) {
+ return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum;
+ }
+ },
+
+ timers: [],
+ timerId: null,
+
+ fx: function( elem, options, prop ){
+ this.options = options;
+ this.elem = elem;
+ this.prop = prop;
+
+ if ( !options.orig )
+ options.orig = {};
+ }
+
+});
+
+jQuery.fx.prototype = {
+
+ // Simple function for setting a style value
+ update: function(){
+ if ( this.options.step )
+ this.options.step.call( this.elem, this.now, this );
+
+ (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
+
+ // Set display property to block for height/width animations
+ if ( this.prop == "height" || this.prop == "width" )
+ this.elem.style.display = "block";
+ },
+
+ // Get the current size
+ cur: function(force){
+ if ( this.elem[this.prop] != null && this.elem.style[this.prop] == null )
+ return this.elem[ this.prop ];
+
+ var r = parseFloat(jQuery.css(this.elem, this.prop, force));
+ return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, this.prop)) || 0;
+ },
+
+ // Start an animation from one number to another
+ custom: function(from, to, unit){
+ this.startTime = now();
+ this.start = from;
+ this.end = to;
+ this.unit = unit || this.unit || "px";
+ this.now = this.start;
+ this.pos = this.state = 0;
+ this.update();
+
+ var self = this;
+ function t(gotoEnd){
+ return self.step(gotoEnd);
+ }
+
+ t.elem = this.elem;
+
+ jQuery.timers.push(t);
+
+ if ( jQuery.timerId == null ) {
+ jQuery.timerId = setInterval(function(){
+ var timers = jQuery.timers;
+
+ for ( var i = 0; i < timers.length; i++ )
+ if ( !timers[i]() )
+ timers.splice(i--, 1);
+
+ if ( !timers.length ) {
+ clearInterval( jQuery.timerId );
+ jQuery.timerId = null;
+ }
+ }, 13);
+ }
+ },
+
+ // Simple 'show' function
+ show: function(){
+ // Remember where we started, so that we can go back to it later
+ this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
+ this.options.show = true;
+
+ // Begin the animation
+ this.custom(0, this.cur());
+
+ // Make sure that we start at a small width/height to avoid any
+ // flash of content
+ if ( this.prop == "width" || this.prop == "height" )
+ this.elem.style[this.prop] = "1px";
+
+ // Start by showing the element
+ jQuery(this.elem).show();
+ },
+
+ // Simple 'hide' function
+ hide: function(){
+ // Remember where we started, so that we can go back to it later
+ this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
+ this.options.hide = true;
+
+ // Begin the animation
+ this.custom(this.cur(), 0);
+ },
+
+ // Each step of an animation
+ step: function(gotoEnd){
+ var t = now();
+
+ if ( gotoEnd || t > this.options.duration + this.startTime ) {
+ this.now = this.end;
+ this.pos = this.state = 1;
+ this.update();
+
+ this.options.curAnim[ this.prop ] = true;
+
+ var done = true;
+ for ( var i in this.options.curAnim )
+ if ( this.options.curAnim[i] !== true )
+ done = false;
+
+ if ( done ) {
+ if ( this.options.display != null ) {
+ // Reset the overflow
+ this.elem.style.overflow = this.options.overflow;
+
+ // Reset the display
+ this.elem.style.display = this.options.display;
+ if ( jQuery.css(this.elem, "display") == "none" )
+ this.elem.style.display = "block";
+ }
+
+ // Hide the element if the "hide" operation was done
+ if ( this.options.hide )
+ this.elem.style.display = "none";
+
+ // Reset the properties, if the item has been hidden or shown
+ if ( this.options.hide || this.options.show )
+ for ( var p in this.options.curAnim )
+ jQuery.attr(this.elem.style, p, this.options.orig[p]);
+ }
+
+ if ( done )
+ // Execute the complete function
+ this.options.complete.call( this.elem );
+
+ return false;
+ } else {
+ var n = t - this.startTime;
+ this.state = n / this.options.duration;
+
+ // Perform the easing function, defaults to swing
+ this.pos = jQuery.easing[this.options.easing || (jQuery.easing.swing ? "swing" : "linear")](this.state, n, 0, 1, this.options.duration);
+ this.now = this.start + ((this.end - this.start) * this.pos);
+
+ // Perform the next step of the animation
+ this.update();
+ }
+
+ return true;
+ }
+
+};
+
+jQuery.extend( jQuery.fx, {
+ speeds:{
+ slow: 600,
+ fast: 200,
+ // Default speed
+ def: 400
+ },
+ step: {
+ scrollLeft: function(fx){
+ fx.elem.scrollLeft = fx.now;
+ },
+
+ scrollTop: function(fx){
+ fx.elem.scrollTop = fx.now;
+ },
+
+ opacity: function(fx){
+ jQuery.attr(fx.elem.style, "opacity", fx.now);
+ },
+
+ _default: function(fx){
+ fx.elem.style[ fx.prop ] = fx.now + fx.unit;
+ }
+ }
+});
+// The Offset Method
+// Originally By Brandon Aaron, part of the Dimension Plugin
+// http://jquery.com/plugins/project/dimensions
+jQuery.fn.offset = function() {
+ var left = 0, top = 0, elem = this[0], results;
+
+ if ( elem ) with ( jQuery.browser ) {
+ var parent = elem.parentNode,
+ offsetChild = elem,
+ offsetParent = elem.offsetParent,
+ doc = elem.ownerDocument,
+ safari2 = safari && parseInt(version) < 522 && !/adobeair/i.test(userAgent),
+ css = jQuery.curCSS,
+ fixed = css(elem, "position") == "fixed";
+
+ // Use getBoundingClientRect if available
+ if ( elem.getBoundingClientRect ) {
+ var box = elem.getBoundingClientRect();
+
+ // Add the document scroll offsets
+ add(box.left + Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft),
+ box.top + Math.max(doc.documentElement.scrollTop, doc.body.scrollTop));
+
+ // IE adds the HTML element's border, by default it is medium which is 2px
+ // IE 6 and 7 quirks mode the border width is overwritable by the following css html { border: 0; }
+ // IE 7 standards mode, the border is always 2px
+ // This border/offset is typically represented by the clientLeft and clientTop properties
+ // However, in IE6 and 7 quirks mode the clientLeft and clientTop properties are not updated when overwriting it via CSS
+ // Therefore this method will be off by 2px in IE while in quirksmode
+ add( -doc.documentElement.clientLeft, -doc.documentElement.clientTop );
+
+ // Otherwise loop through the offsetParents and parentNodes
+ } else {
+
+ // Initial element offsets
+ add( elem.offsetLeft, elem.offsetTop );
+
+ // Get parent offsets
+ while ( offsetParent ) {
+ // Add offsetParent offsets
+ add( offsetParent.offsetLeft, offsetParent.offsetTop );
+
+ // Mozilla and Safari > 2 does not include the border on offset parents
+ // However Mozilla adds the border for table or table cells
+ if ( mozilla && !/^t(able|d|h)$/i.test(offsetParent.tagName) || safari && !safari2 )
+ border( offsetParent );
+
+ // Add the document scroll offsets if position is fixed on any offsetParent
+ if ( !fixed && css(offsetParent, "position") == "fixed" )
+ fixed = true;
+
+ // Set offsetChild to previous offsetParent unless it is the body element
+ offsetChild = /^body$/i.test(offsetParent.tagName) ? offsetChild : offsetParent;
+ // Get next offsetParent
+ offsetParent = offsetParent.offsetParent;
+ }
+
+ // Get parent scroll offsets
+ while ( parent && parent.tagName && !/^body|html$/i.test(parent.tagName) ) {
+ // Remove parent scroll UNLESS that parent is inline or a table to work around Opera inline/table scrollLeft/Top bug
+ if ( !/^inline|table.*$/i.test(css(parent, "display")) )
+ // Subtract parent scroll offsets
+ add( -parent.scrollLeft, -parent.scrollTop );
+
+ // Mozilla does not add the border for a parent that has overflow != visible
+ if ( mozilla && css(parent, "overflow") != "visible" )
+ border( parent );
+
+ // Get next parent
+ parent = parent.parentNode;
+ }
+
+ // Safari <= 2 doubles body offsets with a fixed position element/offsetParent or absolutely positioned offsetChild
+ // Mozilla doubles body offsets with a non-absolutely positioned offsetChild
+ if ( (safari2 && (fixed || css(offsetChild, "position") == "absolute")) ||
+ (mozilla && css(offsetChild, "position") != "absolute") )
+ add( -doc.body.offsetLeft, -doc.body.offsetTop );
+
+ // Add the document scroll offsets if position is fixed
+ if ( fixed )
+ add(Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft),
+ Math.max(doc.documentElement.scrollTop, doc.body.scrollTop));
+ }
+
+ // Return an object with top and left properties
+ results = { top: top, left: left };
+ }
+
+ function border(elem) {
+ add( jQuery.curCSS(elem, "borderLeftWidth", true), jQuery.curCSS(elem, "borderTopWidth", true) );
+ }
+
+ function add(l, t) {
+ left += parseInt(l, 10) || 0;
+ top += parseInt(t, 10) || 0;
+ }
+
+ return results;
+};
+
+
+jQuery.fn.extend({
+ position: function() {
+ var left = 0, top = 0, results;
+
+ if ( this[0] ) {
+ // Get *real* offsetParent
+ var offsetParent = this.offsetParent(),
+
+ // Get correct offsets
+ offset = this.offset(),
+ parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+ // Subtract element margins
+ // note: when an element has margin: auto the offsetLeft and marginLeft
+ // are the same in Safari causing offset.left to incorrectly be 0
+ offset.top -= num( this, 'marginTop' );
+ offset.left -= num( this, 'marginLeft' );
+
+ // Add offsetParent borders
+ parentOffset.top += num( offsetParent, 'borderTopWidth' );
+ parentOffset.left += num( offsetParent, 'borderLeftWidth' );
+
+ // Subtract the two offsets
+ results = {
+ top: offset.top - parentOffset.top,
+ left: offset.left - parentOffset.left
+ };
+ }
+
+ return results;
+ },
+
+ offsetParent: function() {
+ var offsetParent = this[0].offsetParent;
+ while ( offsetParent && (!/^body|html$/i.test(offsetParent.tagName) && jQuery.css(offsetParent, 'position') == 'static') )
+ offsetParent = offsetParent.offsetParent;
+ return jQuery(offsetParent);
+ }
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( ['Left', 'Top'], function(i, name) {
+ var method = 'scroll' + name;
+
+ jQuery.fn[ method ] = function(val) {
+ if (!this[0]) return;
+
+ return val != undefined ?
+
+ // Set the scroll offset
+ this.each(function() {
+ this == window || this == document ?
+ window.scrollTo(
+ !i ? val : jQuery(window).scrollLeft(),
+ i ? val : jQuery(window).scrollTop()
+ ) :
+ this[ method ] = val;
+ }) :
+
+ // Return the scroll offset
+ this[0] == window || this[0] == document ?
+ self[ i ? 'pageYOffset' : 'pageXOffset' ] ||
+ jQuery.boxModel && document.documentElement[ method ] ||
+ document.body[ method ] :
+ this[0][ method ];
+ };
+});
+// Create innerHeight, innerWidth, outerHeight and outerWidth methods
+jQuery.each([ "Height", "Width" ], function(i, name){
+
+ var tl = i ? "Left" : "Top", // top or left
+ br = i ? "Right" : "Bottom"; // bottom or right
+
+ // innerHeight and innerWidth
+ jQuery.fn["inner" + name] = function(){
+ return this[ name.toLowerCase() ]() +
+ num(this, "padding" + tl) +
+ num(this, "padding" + br);
+ };
+
+ // outerHeight and outerWidth
+ jQuery.fn["outer" + name] = function(margin) {
+ return this["inner" + name]() +
+ num(this, "border" + tl + "Width") +
+ num(this, "border" + br + "Width") +
+ (margin ?
+ num(this, "margin" + tl) + num(this, "margin" + br) : 0);
+ };
+
+});})();
diff --git a/etherpad/src/static/js/jquery-1.3.2.js b/etherpad/src/static/js/jquery-1.3.2.js
new file mode 100644
index 0000000..9263574
--- /dev/null
+++ b/etherpad/src/static/js/jquery-1.3.2.js
@@ -0,0 +1,4376 @@
+/*!
+ * jQuery JavaScript Library v1.3.2
+ * http://jquery.com/
+ *
+ * Copyright (c) 2009 John Resig
+ * Dual licensed under the MIT and GPL licenses.
+ * http://docs.jquery.com/License
+ *
+ * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009)
+ * Revision: 6246
+ */
+(function(){
+
+var
+ // Will speed up references to window, and allows munging its name.
+ window = this,
+ // Will speed up references to undefined, and allows munging its name.
+ undefined,
+ // Map over jQuery in case of overwrite
+ _jQuery = window.jQuery,
+ // Map over the $ in case of overwrite
+ _$ = window.$,
+
+ jQuery = window.jQuery = window.$ = function( selector, context ) {
+ // The jQuery object is actually just the init constructor 'enhanced'
+ return new jQuery.fn.init( selector, context );
+ },
+
+ // A simple way to check for HTML strings or ID strings
+ // (both of which we optimize for)
+ quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,
+ // Is it a simple selector
+ isSimple = /^.[^:#\[\.,]*$/;
+
+jQuery.fn = jQuery.prototype = {
+ init: function( selector, context ) {
+ // Make sure that a selection was provided
+ selector = selector || document;
+
+ // Handle $(DOMElement)
+ if ( selector.nodeType ) {
+ this[0] = selector;
+ this.length = 1;
+ this.context = selector;
+ return this;
+ }
+ // Handle HTML strings
+ if ( typeof selector === "string" ) {
+ // Are we dealing with HTML string or an ID?
+ var match = quickExpr.exec( selector );
+
+ // Verify a match, and that no context was specified for #id
+ if ( match && (match[1] || !context) ) {
+
+ // HANDLE: $(html) -> $(array)
+ if ( match[1] )
+ selector = jQuery.clean( [ match[1] ], context );
+
+ // HANDLE: $("#id")
+ else {
+ var elem = document.getElementById( match[3] );
+
+ // Handle the case where IE and Opera return items
+ // by name instead of ID
+ if ( elem && elem.id != match[3] )
+ return jQuery().find( selector );
+
+ // Otherwise, we inject the element directly into the jQuery object
+ var ret = jQuery( elem || [] );
+ ret.context = document;
+ ret.selector = selector;
+ return ret;
+ }
+
+ // HANDLE: $(expr, [context])
+ // (which is just equivalent to: $(content).find(expr)
+ } else
+ return jQuery( context ).find( selector );
+
+ // HANDLE: $(function)
+ // Shortcut for document ready
+ } else if ( jQuery.isFunction( selector ) )
+ return jQuery( document ).ready( selector );
+
+ // Make sure that old selector state is passed along
+ if ( selector.selector && selector.context ) {
+ this.selector = selector.selector;
+ this.context = selector.context;
+ }
+
+ return this.setArray(jQuery.isArray( selector ) ?
+ selector :
+ jQuery.makeArray(selector));
+ },
+
+ // Start with an empty selector
+ selector: "",
+
+ // The current version of jQuery being used
+ jquery: "1.3.2",
+
+ // The number of elements contained in the matched element set
+ size: function() {
+ return this.length;
+ },
+
+ // Get the Nth element in the matched element set OR
+ // Get the whole matched element set as a clean array
+ get: function( num ) {
+ return num === undefined ?
+
+ // Return a 'clean' array
+ Array.prototype.slice.call( this ) :
+
+ // Return just the object
+ this[ num ];
+ },
+
+ // Take an array of elements and push it onto the stack
+ // (returning the new matched element set)
+ pushStack: function( elems, name, selector ) {
+ // Build a new jQuery matched element set
+ var ret = jQuery( elems );
+
+ // Add the old object onto the stack (as a reference)
+ ret.prevObject = this;
+
+ ret.context = this.context;
+
+ if ( name === "find" )
+ ret.selector = this.selector + (this.selector ? " " : "") + selector;
+ else if ( name )
+ ret.selector = this.selector + "." + name + "(" + selector + ")";
+
+ // Return the newly-formed element set
+ return ret;
+ },
+
+ // Force the current matched set of elements to become
+ // the specified array of elements (destroying the stack in the process)
+ // You should use pushStack() in order to do this, but maintain the stack
+ setArray: function( elems ) {
+ // Resetting the length to 0, then using the native Array push
+ // is a super-fast way to populate an object with array-like properties
+ this.length = 0;
+ Array.prototype.push.apply( this, elems );
+
+ return this;
+ },
+
+ // Execute a callback for every element in the matched set.
+ // (You can seed the arguments with an array of args, but this is
+ // only used internally.)
+ each: function( callback, args ) {
+ return jQuery.each( this, callback, args );
+ },
+
+ // Determine the position of an element within
+ // the matched set of elements
+ index: function( elem ) {
+ // Locate the position of the desired element
+ return jQuery.inArray(
+ // If it receives a jQuery object, the first element is used
+ elem && elem.jquery ? elem[0] : elem
+ , this );
+ },
+
+ attr: function( name, value, type ) {
+ var options = name;
+
+ // Look for the case where we're accessing a style value
+ if ( typeof name === "string" )
+ if ( value === undefined )
+ return this[0] && jQuery[ type || "attr" ]( this[0], name );
+
+ else {
+ options = {};
+ options[ name ] = value;
+ }
+
+ // Check to see if we're setting style values
+ return this.each(function(i){
+ // Set all the styles
+ for ( name in options )
+ jQuery.attr(
+ type ?
+ this.style :
+ this,
+ name, jQuery.prop( this, options[ name ], type, i, name )
+ );
+ });
+ },
+
+ css: function( key, value ) {
+ // ignore negative width and height values
+ if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 )
+ value = undefined;
+ return this.attr( key, value, "curCSS" );
+ },
+
+ text: function( text ) {
+ if ( typeof text !== "object" && text != null )
+ return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
+
+ var ret = "";
+
+ jQuery.each( text || this, function(){
+ jQuery.each( this.childNodes, function(){
+ if ( this.nodeType != 8 )
+ ret += this.nodeType != 1 ?
+ this.nodeValue :
+ jQuery.fn.text( [ this ] );
+ });
+ });
+
+ return ret;
+ },
+
+ wrapAll: function( html ) {
+ if ( this[0] ) {
+ // The elements to wrap the target around
+ var wrap = jQuery( html, this[0].ownerDocument ).clone();
+
+ if ( this[0].parentNode )
+ wrap.insertBefore( this[0] );
+
+ wrap.map(function(){
+ var elem = this;
+
+ while ( elem.firstChild )
+ elem = elem.firstChild;
+
+ return elem;
+ }).append(this);
+ }
+
+ return this;
+ },
+
+ wrapInner: function( html ) {
+ return this.each(function(){
+ jQuery( this ).contents().wrapAll( html );
+ });
+ },
+
+ wrap: function( html ) {
+ return this.each(function(){
+ jQuery( this ).wrapAll( html );
+ });
+ },
+
+ append: function() {
+ return this.domManip(arguments, true, function(elem){
+ if (this.nodeType == 1)
+ this.appendChild( elem );
+ });
+ },
+
+ prepend: function() {
+ return this.domManip(arguments, true, function(elem){
+ if (this.nodeType == 1)
+ this.insertBefore( elem, this.firstChild );
+ });
+ },
+
+ before: function() {
+ return this.domManip(arguments, false, function(elem){
+ this.parentNode.insertBefore( elem, this );
+ });
+ },
+
+ after: function() {
+ return this.domManip(arguments, false, function(elem){
+ this.parentNode.insertBefore( elem, this.nextSibling );
+ });
+ },
+
+ end: function() {
+ return this.prevObject || jQuery( [] );
+ },
+
+ // For internal use only.
+ // Behaves like an Array's method, not like a jQuery method.
+ push: [].push,
+ sort: [].sort,
+ splice: [].splice,
+
+ find: function( selector ) {
+ if ( this.length === 1 ) {
+ var ret = this.pushStack( [], "find", selector );
+ ret.length = 0;
+ jQuery.find( selector, this[0], ret );
+ return ret;
+ } else {
+ return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){
+ return jQuery.find( selector, elem );
+ })), "find", selector );
+ }
+ },
+
+ clone: function( events ) {
+ // Do the clone
+ var ret = this.map(function(){
+ if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) {
+ // IE copies events bound via attachEvent when
+ // using cloneNode. Calling detachEvent on the
+ // clone will also remove the events from the orignal
+ // In order to get around this, we use innerHTML.
+ // Unfortunately, this means some modifications to
+ // attributes in IE that are actually only stored
+ // as properties will not be copied (such as the
+ // the name attribute on an input).
+ var html = this.outerHTML;
+ if ( !html ) {
+ var div = this.ownerDocument.createElement("div");
+ div.appendChild( this.cloneNode(true) );
+ html = div.innerHTML;
+ }
+
+ return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0];
+ } else
+ return this.cloneNode(true);
+ });
+
+ // Copy the events from the original to the clone
+ if ( events === true ) {
+ var orig = this.find("*").andSelf(), i = 0;
+
+ ret.find("*").andSelf().each(function(){
+ if ( this.nodeName !== orig[i].nodeName )
+ return;
+
+ var events = jQuery.data( orig[i], "events" );
+
+ for ( var type in events ) {
+ for ( var handler in events[ type ] ) {
+ jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data );
+ }
+ }
+
+ i++;
+ });
+ }
+
+ // Return the cloned set
+ return ret;
+ },
+
+ filter: function( selector ) {
+ return this.pushStack(
+ jQuery.isFunction( selector ) &&
+ jQuery.grep(this, function(elem, i){
+ return selector.call( elem, i );
+ }) ||
+
+ jQuery.multiFilter( selector, jQuery.grep(this, function(elem){
+ return elem.nodeType === 1;
+ }) ), "filter", selector );
+ },
+
+ closest: function( selector ) {
+ var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null,
+ closer = 0;
+
+ return this.map(function(){
+ var cur = this;
+ while ( cur && cur.ownerDocument ) {
+ if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) {
+ jQuery.data(cur, "closest", closer);
+ return cur;
+ }
+ cur = cur.parentNode;
+ closer++;
+ }
+ });
+ },
+
+ not: function( selector ) {
+ if ( typeof selector === "string" )
+ // test special case where just one selector is passed in
+ if ( isSimple.test( selector ) )
+ return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector );
+ else
+ selector = jQuery.multiFilter( selector, this );
+
+ var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType;
+ return this.filter(function() {
+ return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector;
+ });
+ },
+
+ add: function( selector ) {
+ return this.pushStack( jQuery.unique( jQuery.merge(
+ this.get(),
+ typeof selector === "string" ?
+ jQuery( selector ) :
+ jQuery.makeArray( selector )
+ )));
+ },
+
+ is: function( selector ) {
+ return !!selector && jQuery.multiFilter( selector, this ).length > 0;
+ },
+
+ hasClass: function( selector ) {
+ return !!selector && this.is( "." + selector );
+ },
+
+ val: function( value ) {
+ if ( value === undefined ) {
+ var elem = this[0];
+
+ if ( elem ) {
+ if( jQuery.nodeName( elem, 'option' ) )
+ return (elem.attributes.value || {}).specified ? elem.value : elem.text;
+
+ // We need to handle select boxes special
+ if ( jQuery.nodeName( elem, "select" ) ) {
+ var index = elem.selectedIndex,
+ values = [],
+ options = elem.options,
+ one = elem.type == "select-one";
+
+ // Nothing was selected
+ if ( index < 0 )
+ return null;
+
+ // Loop through all the selected options
+ for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
+ var option = options[ i ];
+
+ if ( option.selected ) {
+ // Get the specifc value for the option
+ value = jQuery(option).val();
+
+ // We don't need an array for one selects
+ if ( one )
+ return value;
+
+ // Multi-Selects return an array
+ values.push( value );
+ }
+ }
+
+ return values;
+ }
+
+ // Everything else, we just grab the value
+ return (elem.value || "").replace(/\r/g, "");
+
+ }
+
+ return undefined;
+ }
+
+ if ( typeof value === "number" )
+ value += '';
+
+ return this.each(function(){
+ if ( this.nodeType != 1 )
+ return;
+
+ if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) )
+ this.checked = (jQuery.inArray(this.value, value) >= 0 ||
+ jQuery.inArray(this.name, value) >= 0);
+
+ else if ( jQuery.nodeName( this, "select" ) ) {
+ var values = jQuery.makeArray(value);
+
+ jQuery( "option", this ).each(function(){
+ this.selected = (jQuery.inArray( this.value, values ) >= 0 ||
+ jQuery.inArray( this.text, values ) >= 0);
+ });
+
+ if ( !values.length )
+ this.selectedIndex = -1;
+
+ } else
+ this.value = value;
+ });
+ },
+
+ html: function( value ) {
+ return value === undefined ?
+ (this[0] ?
+ this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") :
+ null) :
+ this.empty().append( value );
+ },
+
+ replaceWith: function( value ) {
+ return this.after( value ).remove();
+ },
+
+ eq: function( i ) {
+ return this.slice( i, +i + 1 );
+ },
+
+ slice: function() {
+ return this.pushStack( Array.prototype.slice.apply( this, arguments ),
+ "slice", Array.prototype.slice.call(arguments).join(",") );
+ },
+
+ map: function( callback ) {
+ return this.pushStack( jQuery.map(this, function(elem, i){
+ return callback.call( elem, i, elem );
+ }));
+ },
+
+ andSelf: function() {
+ return this.add( this.prevObject );
+ },
+
+ domManip: function( args, table, callback ) {
+ if ( this[0] ) {
+ var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(),
+ scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ),
+ first = fragment.firstChild;
+
+ if ( first )
+ for ( var i = 0, l = this.length; i < l; i++ )
+ callback.call( root(this[i], first), this.length > 1 || i > 0 ?
+ fragment.cloneNode(true) : fragment );
+
+ if ( scripts )
+ jQuery.each( scripts, evalScript );
+ }
+
+ return this;
+
+ function root( elem, cur ) {
+ return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ?
+ (elem.getElementsByTagName("tbody")[0] ||
+ elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
+ elem;
+ }
+ }
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+function evalScript( i, elem ) {
+ if ( elem.src )
+ jQuery.ajax({
+ url: elem.src,
+ async: false,
+ dataType: "script"
+ });
+
+ else
+ jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
+
+ if ( elem.parentNode )
+ elem.parentNode.removeChild( elem );
+}
+
+function now(){
+ return +new Date;
+}
+
+jQuery.extend = jQuery.fn.extend = function() {
+ // copy reference to target object
+ var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+ target = arguments[1] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !jQuery.isFunction(target) )
+ target = {};
+
+ // extend jQuery itself if only one argument is passed
+ if ( length == i ) {
+ target = this;
+ --i;
+ }
+
+ for ( ; i < length; i++ )
+ // Only deal with non-null/undefined values
+ if ( (options = arguments[ i ]) != null )
+ // Extend the base object
+ for ( var name in options ) {
+ var src = target[ name ], copy = options[ name ];
+
+ // Prevent never-ending loop
+ if ( target === copy )
+ continue;
+
+ // Recurse if we're merging object values
+ if ( deep && copy && typeof copy === "object" && !copy.nodeType )
+ target[ name ] = jQuery.extend( deep,
+ // Never move original objects, clone them
+ src || ( copy.length != null ? [ ] : { } )
+ , copy );
+
+ // Don't bring in undefined values
+ else if ( copy !== undefined )
+ target[ name ] = copy;
+
+ }
+
+ // Return the modified object
+ return target;
+};
+
+// exclude the following css properties to add px
+var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
+ // cache defaultView
+ defaultView = document.defaultView || {},
+ toString = Object.prototype.toString;
+
+jQuery.extend({
+ noConflict: function( deep ) {
+ window.$ = _$;
+
+ if ( deep )
+ window.jQuery = _jQuery;
+
+ return jQuery;
+ },
+
+ // See test/unit/core.js for details concerning isFunction.
+ // Since version 1.3, DOM methods and functions like alert
+ // aren't supported. They return false on IE (#2968).
+ isFunction: function( obj ) {
+ return toString.call(obj) === "[object Function]";
+ },
+
+ isArray: function( obj ) {
+ return toString.call(obj) === "[object Array]";
+ },
+
+ // check if an element is in a (or is an) XML document
+ isXMLDoc: function( elem ) {
+ return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
+ !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument );
+ },
+
+ // Evalulates a script in a global context
+ globalEval: function( data ) {
+ if ( data && /\S/.test(data) ) {
+ // Inspired by code by Andrea Giammarchi
+ // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
+ var head = document.getElementsByTagName("head")[0] || document.documentElement,
+ script = document.createElement("script");
+
+ script.type = "text/javascript";
+ if ( jQuery.support.scriptEval )
+ script.appendChild( document.createTextNode( data ) );
+ else
+ script.text = data;
+
+ // Use insertBefore instead of appendChild to circumvent an IE6 bug.
+ // This arises when a base node is used (#2709).
+ head.insertBefore( script, head.firstChild );
+ head.removeChild( script );
+ }
+ },
+
+ nodeName: function( elem, name ) {
+ return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
+ },
+
+ // args is for internal usage only
+ each: function( object, callback, args ) {
+ var name, i = 0, length = object.length;
+
+ if ( args ) {
+ if ( length === undefined ) {
+ for ( name in object )
+ if ( callback.apply( object[ name ], args ) === false )
+ break;
+ } else
+ for ( ; i < length; )
+ if ( callback.apply( object[ i++ ], args ) === false )
+ break;
+
+ // A special, fast, case for the most common use of each
+ } else {
+ if ( length === undefined ) {
+ for ( name in object )
+ if ( callback.call( object[ name ], name, object[ name ] ) === false )
+ break;
+ } else
+ for ( var value = object[0];
+ i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
+ }
+
+ return object;
+ },
+
+ prop: function( elem, value, type, i, name ) {
+ // Handle executable functions
+ if ( jQuery.isFunction( value ) )
+ value = value.call( elem, i );
+
+ // Handle passing in a number to a CSS property
+ return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ?
+ value + "px" :
+ value;
+ },
+
+ className: {
+ // internal only, use addClass("class")
+ add: function( elem, classNames ) {
+ jQuery.each((classNames || "").split(/\s+/), function(i, className){
+ if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) )
+ elem.className += (elem.className ? " " : "") + className;
+ });
+ },
+
+ // internal only, use removeClass("class")
+ remove: function( elem, classNames ) {
+ if (elem.nodeType == 1)
+ elem.className = classNames !== undefined ?
+ jQuery.grep(elem.className.split(/\s+/), function(className){
+ return !jQuery.className.has( classNames, className );
+ }).join(" ") :
+ "";
+ },
+
+ // internal only, use hasClass("class")
+ has: function( elem, className ) {
+ return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
+ }
+ },
+
+ // A method for quickly swapping in/out CSS properties to get correct calculations
+ swap: function( elem, options, callback ) {
+ var old = {};
+ // Remember the old values, and insert the new ones
+ for ( var name in options ) {
+ old[ name ] = elem.style[ name ];
+ elem.style[ name ] = options[ name ];
+ }
+
+ callback.call( elem );
+
+ // Revert the old values
+ for ( var name in options )
+ elem.style[ name ] = old[ name ];
+ },
+
+ css: function( elem, name, force, extra ) {
+ if ( name == "width" || name == "height" ) {
+ var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];
+
+ function getWH() {
+ val = name == "width" ? elem.offsetWidth : elem.offsetHeight;
+
+ if ( extra === "border" )
+ return;
+
+ jQuery.each( which, function() {
+ if ( !extra )
+ val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
+ if ( extra === "margin" )
+ val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0;
+ else
+ val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
+ });
+ }
+
+ if ( elem.offsetWidth !== 0 )
+ getWH();
+ else
+ jQuery.swap( elem, props, getWH );
+
+ return Math.max(0, Math.round(val));
+ }
+
+ return jQuery.curCSS( elem, name, force );
+ },
+
+ curCSS: function( elem, name, force ) {
+ var ret, style = elem.style;
+
+ // We need to handle opacity special in IE
+ if ( name == "opacity" && !jQuery.support.opacity ) {
+ ret = jQuery.attr( style, "opacity" );
+
+ return ret == "" ?
+ "1" :
+ ret;
+ }
+
+ // Make sure we're using the right name for getting the float value
+ if ( name.match( /float/i ) )
+ name = styleFloat;
+
+ if ( !force && style && style[ name ] )
+ ret = style[ name ];
+
+ else if ( defaultView.getComputedStyle ) {
+
+ // Only "float" is needed here
+ if ( name.match( /float/i ) )
+ name = "float";
+
+ name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();
+
+ var computedStyle = defaultView.getComputedStyle( elem, null );
+
+ if ( computedStyle )
+ ret = computedStyle.getPropertyValue( name );
+
+ // We should always get a number back from opacity
+ if ( name == "opacity" && ret == "" )
+ ret = "1";
+
+ } else if ( elem.currentStyle ) {
+ var camelCase = name.replace(/\-(\w)/g, function(all, letter){
+ return letter.toUpperCase();
+ });
+
+ ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
+
+ // From the awesome hack by Dean Edwards
+ // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+ // If we're not dealing with a regular pixel number
+ // but a number that has a weird ending, we need to convert it to pixels
+ if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
+ // Remember the original values
+ var left = style.left, rsLeft = elem.runtimeStyle.left;
+
+ // Put in the new values to get a computed value out
+ elem.runtimeStyle.left = elem.currentStyle.left;
+ style.left = ret || 0;
+ ret = style.pixelLeft + "px";
+
+ // Revert the changed values
+ style.left = left;
+ elem.runtimeStyle.left = rsLeft;
+ }
+ }
+
+ return ret;
+ },
+
+ clean: function( elems, context, fragment ) {
+ context = context || document;
+
+ // !context.createElement fails in IE with an error but returns typeof 'object'
+ if ( typeof context.createElement === "undefined" )
+ context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
+
+ // If a single string is passed in and it's a single tag
+ // just do a createElement and skip the rest
+ if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) {
+ var match = /^<(\w+)\s*\/?>$/.exec(elems[0]);
+ if ( match )
+ return [ context.createElement( match[1] ) ];
+ }
+
+ var ret = [], scripts = [], div = context.createElement("div");
+
+ jQuery.each(elems, function(i, elem){
+ if ( typeof elem === "number" )
+ elem += '';
+
+ if ( !elem )
+ return;
+
+ // Convert html string into DOM nodes
+ if ( typeof elem === "string" ) {
+ // Fix "XHTML"-style tags in all browsers
+ elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
+ return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
+ all :
+ front + "></" + tag + ">";
+ });
+
+ // Trim whitespace, otherwise indexOf won't work as expected
+ var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase();
+
+ var wrap =
+ // option or optgroup
+ !tags.indexOf("<opt") &&
+ [ 1, "<select multiple='multiple'>", "</select>" ] ||
+
+ !tags.indexOf("<leg") &&
+ [ 1, "<fieldset>", "</fieldset>" ] ||
+
+ tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
+ [ 1, "<table>", "</table>" ] ||
+
+ !tags.indexOf("<tr") &&
+ [ 2, "<table><tbody>", "</tbody></table>" ] ||
+
+ // <thead> matched above
+ (!tags.indexOf("<td") || !tags.indexOf("<th")) &&
+ [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||
+
+ !tags.indexOf("<col") &&
+ [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||
+
+ // IE can't serialize <link> and <script> tags normally
+ !jQuery.support.htmlSerialize &&
+ [ 1, "div<div>", "</div>" ] ||
+
+ [ 0, "", "" ];
+
+ // Go to html and back, then peel off extra wrappers
+ div.innerHTML = wrap[1] + elem + wrap[2];
+
+ // Move to the right depth
+ while ( wrap[0]-- )
+ div = div.lastChild;
+
+ // Remove IE's autoinserted <tbody> from table fragments
+ if ( !jQuery.support.tbody ) {
+
+ // String was a <table>, *may* have spurious <tbody>
+ var hasBody = /<tbody/i.test(elem),
+ tbody = !tags.indexOf("<table") && !hasBody ?
+ div.firstChild && div.firstChild.childNodes :
+
+ // String was a bare <thead> or <tfoot>
+ wrap[1] == "<table>" && !hasBody ?
+ div.childNodes :
+ [];
+
+ for ( var j = tbody.length - 1; j >= 0 ; --j )
+ if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length )
+ tbody[ j ].parentNode.removeChild( tbody[ j ] );
+
+ }
+
+ // IE completely kills leading whitespace when innerHTML is used
+ if ( !jQuery.support.leadingWhitespace && /^\s/.test( elem ) )
+ div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild );
+
+ elem = jQuery.makeArray( div.childNodes );
+ }
+
+ if ( elem.nodeType )
+ ret.push( elem );
+ else
+ ret = jQuery.merge( ret, elem );
+
+ });
+
+ if ( fragment ) {
+ for ( var i = 0; ret[i]; i++ ) {
+ if ( jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
+ scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
+ } else {
+ if ( ret[i].nodeType === 1 )
+ ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
+ fragment.appendChild( ret[i] );
+ }
+ }
+
+ return scripts;
+ }
+
+ return ret;
+ },
+
+ attr: function( elem, name, value ) {
+ // don't set attributes on text and comment nodes
+ if (!elem || elem.nodeType == 3 || elem.nodeType == 8)
+ return undefined;
+
+ var notxml = !jQuery.isXMLDoc( elem ),
+ // Whether we are setting (or getting)
+ set = value !== undefined;
+
+ // Try to normalize/fix the name
+ name = notxml && jQuery.props[ name ] || name;
+
+ // Only do all the following if this is a node (faster for style)
+ // IE elem.getAttribute passes even for style
+ if ( elem.tagName ) {
+
+ // These attributes require special treatment
+ var special = /href|src|style/.test( name );
+
+ // Safari mis-reports the default selected property of a hidden option
+ // Accessing the parent's selectedIndex property fixes it
+ if ( name == "selected" && elem.parentNode )
+ elem.parentNode.selectedIndex;
+
+ // If applicable, access the attribute via the DOM 0 way
+ if ( name in elem && notxml && !special ) {
+ if ( set ){
+ // We can't allow the type property to be changed (since it causes problems in IE)
+ if ( name == "type" && jQuery.nodeName( elem, "input" ) && elem.parentNode )
+ throw "type property can't be changed";
+
+ elem[ name ] = value;
+ }
+
+ // browsers index elements by id/name on forms, give priority to attributes.
+ if( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) )
+ return elem.getAttributeNode( name ).nodeValue;
+
+ // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+ // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+ if ( name == "tabIndex" ) {
+ var attributeNode = elem.getAttributeNode( "tabIndex" );
+ return attributeNode && attributeNode.specified
+ ? attributeNode.value
+ : elem.nodeName.match(/(button|input|object|select|textarea)/i)
+ ? 0
+ : elem.nodeName.match(/^(a|area)$/i) && elem.href
+ ? 0
+ : undefined;
+ }
+
+ return elem[ name ];
+ }
+
+ if ( !jQuery.support.style && notxml && name == "style" )
+ return jQuery.attr( elem.style, "cssText", value );
+
+ if ( set )
+ // convert the value to a string (all browsers do this but IE) see #1070
+ elem.setAttribute( name, "" + value );
+
+ var attr = !jQuery.support.hrefNormalized && notxml && special
+ // Some attributes require a special call on IE
+ ? elem.getAttribute( name, 2 )
+ : elem.getAttribute( name );
+
+ // Non-existent attributes return null, we normalize to undefined
+ return attr === null ? undefined : attr;
+ }
+
+ // elem is actually elem.style ... set the style
+
+ // IE uses filters for opacity
+ if ( !jQuery.support.opacity && name == "opacity" ) {
+ if ( set ) {
+ // IE has trouble with opacity if it does not have layout
+ // Force it by setting the zoom level
+ elem.zoom = 1;
+
+ // Set the alpha filter to set the opacity
+ elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) +
+ (parseInt( value ) + '' == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
+ }
+
+ return elem.filter && elem.filter.indexOf("opacity=") >= 0 ?
+ (parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100) + '':
+ "";
+ }
+
+ name = name.replace(/-([a-z])/ig, function(all, letter){
+ return letter.toUpperCase();
+ });
+
+ if ( set )
+ elem[ name ] = value;
+
+ return elem[ name ];
+ },
+
+ trim: function( text ) {
+ return (text || "").replace( /^\s+|\s+$/g, "" );
+ },
+
+ makeArray: function( array ) {
+ var ret = [];
+
+ if( array != null ){
+ var i = array.length;
+ // The window, strings (and functions) also have 'length'
+ if( i == null || typeof array === "string" || jQuery.isFunction(array) || array.setInterval )
+ ret[0] = array;
+ else
+ while( i )
+ ret[--i] = array[i];
+ }
+
+ return ret;
+ },
+
+ inArray: function( elem, array ) {
+ for ( var i = 0, length = array.length; i < length; i++ )
+ // Use === because on IE, window == document
+ if ( array[ i ] === elem )
+ return i;
+
+ return -1;
+ },
+
+ merge: function( first, second ) {
+ // We have to loop this way because IE & Opera overwrite the length
+ // expando of getElementsByTagName
+ var i = 0, elem, pos = first.length;
+ // Also, we need to make sure that the correct elements are being returned
+ // (IE returns comment nodes in a '*' query)
+ if ( !jQuery.support.getAll ) {
+ while ( (elem = second[ i++ ]) != null )
+ if ( elem.nodeType != 8 )
+ first[ pos++ ] = elem;
+
+ } else
+ while ( (elem = second[ i++ ]) != null )
+ first[ pos++ ] = elem;
+
+ return first;
+ },
+
+ unique: function( array ) {
+ var ret = [], done = {};
+
+ try {
+
+ for ( var i = 0, length = array.length; i < length; i++ ) {
+ var id = jQuery.data( array[ i ] );
+
+ if ( !done[ id ] ) {
+ done[ id ] = true;
+ ret.push( array[ i ] );
+ }
+ }
+
+ } catch( e ) {
+ ret = array;
+ }
+
+ return ret;
+ },
+
+ grep: function( elems, callback, inv ) {
+ var ret = [];
+
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( var i = 0, length = elems.length; i < length; i++ )
+ if ( !inv != !callback( elems[ i ], i ) )
+ ret.push( elems[ i ] );
+
+ return ret;
+ },
+
+ map: function( elems, callback ) {
+ var ret = [];
+
+ // Go through the array, translating each of the items to their
+ // new value (or values).
+ for ( var i = 0, length = elems.length; i < length; i++ ) {
+ var value = callback( elems[ i ], i );
+
+ if ( value != null )
+ ret[ ret.length ] = value;
+ }
+
+ return ret.concat.apply( [], ret );
+ }
+});
+
+// Use of jQuery.browser is deprecated.
+// It's included for backwards compatibility and plugins,
+// although they should work to migrate away.
+
+var userAgent = navigator.userAgent.toLowerCase();
+
+// Figure out what browser is being used
+jQuery.browser = {
+ version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1],
+ safari: /webkit/.test( userAgent ),
+ opera: /opera/.test( userAgent ),
+ msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
+ mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
+};
+
+jQuery.each({
+ parent: function(elem){return elem.parentNode;},
+ parents: function(elem){return jQuery.dir(elem,"parentNode");},
+ next: function(elem){return jQuery.nth(elem,2,"nextSibling");},
+ prev: function(elem){return jQuery.nth(elem,2,"previousSibling");},
+ nextAll: function(elem){return jQuery.dir(elem,"nextSibling");},
+ prevAll: function(elem){return jQuery.dir(elem,"previousSibling");},
+ siblings: function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);},
+ children: function(elem){return jQuery.sibling(elem.firstChild);},
+ contents: function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);}
+}, function(name, fn){
+ jQuery.fn[ name ] = function( selector ) {
+ var ret = jQuery.map( this, fn );
+
+ if ( selector && typeof selector == "string" )
+ ret = jQuery.multiFilter( selector, ret );
+
+ return this.pushStack( jQuery.unique( ret ), name, selector );
+ };
+});
+
+jQuery.each({
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after",
+ replaceAll: "replaceWith"
+}, function(name, original){
+ jQuery.fn[ name ] = function( selector ) {
+ var ret = [], insert = jQuery( selector );
+
+ for ( var i = 0, l = insert.length; i < l; i++ ) {
+ var elems = (i > 0 ? this.clone(true) : this).get();
+ jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
+ ret = ret.concat( elems );
+ }
+
+ return this.pushStack( ret, name, selector );
+ };
+});
+
+jQuery.each({
+ removeAttr: function( name ) {
+ jQuery.attr( this, name, "" );
+ if (this.nodeType == 1)
+ this.removeAttribute( name );
+ },
+
+ addClass: function( classNames ) {
+ jQuery.className.add( this, classNames );
+ },
+
+ removeClass: function( classNames ) {
+ jQuery.className.remove( this, classNames );
+ },
+
+ toggleClass: function( classNames, state ) {
+ if( typeof state !== "boolean" )
+ state = !jQuery.className.has( this, classNames );
+ jQuery.className[ state ? "add" : "remove" ]( this, classNames );
+ },
+
+ remove: function( selector ) {
+ if ( !selector || jQuery.filter( selector, [ this ] ).length ) {
+ // Prevent memory leaks
+ jQuery( "*", this ).add([this]).each(function(){
+ jQuery.event.remove(this);
+ jQuery.removeData(this);
+ });
+ if (this.parentNode)
+ this.parentNode.removeChild( this );
+ }
+ },
+
+ empty: function() {
+ // Remove element nodes and prevent memory leaks
+ jQuery(this).children().remove();
+
+ // Remove any remaining nodes
+ while ( this.firstChild )
+ this.removeChild( this.firstChild );
+ }
+}, function(name, fn){
+ jQuery.fn[ name ] = function(){
+ return this.each( fn, arguments );
+ };
+});
+
+// Helper function used by the dimensions and offset modules
+function num(elem, prop) {
+ return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0;
+}
+var expando = "jQuery" + now(), uuid = 0, windowData = {};
+
+jQuery.extend({
+ cache: {},
+
+ data: function( elem, name, data ) {
+ elem = elem == window ?
+ windowData :
+ elem;
+
+ var id = elem[ expando ];
+
+ // Compute a unique ID for the element
+ if ( !id )
+ id = elem[ expando ] = ++uuid;
+
+ // Only generate the data cache if we're
+ // trying to access or manipulate it
+ if ( name && !jQuery.cache[ id ] )
+ jQuery.cache[ id ] = {};
+
+ // Prevent overriding the named cache with undefined values
+ if ( data !== undefined )
+ jQuery.cache[ id ][ name ] = data;
+
+ // Return the named cache data, or the ID for the element
+ return name ?
+ jQuery.cache[ id ][ name ] :
+ id;
+ },
+
+ removeData: function( elem, name ) {
+ elem = elem == window ?
+ windowData :
+ elem;
+
+ var id = elem[ expando ];
+
+ // If we want to remove a specific section of the element's data
+ if ( name ) {
+ if ( jQuery.cache[ id ] ) {
+ // Remove the section of cache data
+ delete jQuery.cache[ id ][ name ];
+
+ // If we've removed all the data, remove the element's cache
+ name = "";
+
+ for ( name in jQuery.cache[ id ] )
+ break;
+
+ if ( !name )
+ jQuery.removeData( elem );
+ }
+
+ // Otherwise, we want to remove all of the element's data
+ } else {
+ // Clean up the element expando
+ try {
+ delete elem[ expando ];
+ } catch(e){
+ // IE has trouble directly removing the expando
+ // but it's ok with using removeAttribute
+ if ( elem.removeAttribute )
+ elem.removeAttribute( expando );
+ }
+
+ // Completely remove the data cache
+ delete jQuery.cache[ id ];
+ }
+ },
+ queue: function( elem, type, data ) {
+ if ( elem ){
+
+ type = (type || "fx") + "queue";
+
+ var q = jQuery.data( elem, type );
+
+ if ( !q || jQuery.isArray(data) )
+ q = jQuery.data( elem, type, jQuery.makeArray(data) );
+ else if( data )
+ q.push( data );
+
+ }
+ return q;
+ },
+
+ dequeue: function( elem, type ){
+ var queue = jQuery.queue( elem, type ),
+ fn = queue.shift();
+
+ if( !type || type === "fx" )
+ fn = queue[0];
+
+ if( fn !== undefined )
+ fn.call(elem);
+ }
+});
+
+jQuery.fn.extend({
+ data: function( key, value ){
+ var parts = key.split(".");
+ parts[1] = parts[1] ? "." + parts[1] : "";
+
+ if ( value === undefined ) {
+ var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
+
+ if ( data === undefined && this.length )
+ data = jQuery.data( this[0], key );
+
+ return data === undefined && parts[1] ?
+ this.data( parts[0] ) :
+ data;
+ } else
+ return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){
+ jQuery.data( this, key, value );
+ });
+ },
+
+ removeData: function( key ){
+ return this.each(function(){
+ jQuery.removeData( this, key );
+ });
+ },
+ queue: function(type, data){
+ if ( typeof type !== "string" ) {
+ data = type;
+ type = "fx";
+ }
+
+ if ( data === undefined )
+ return jQuery.queue( this[0], type );
+
+ return this.each(function(){
+ var queue = jQuery.queue( this, type, data );
+
+ if( type == "fx" && queue.length == 1 )
+ queue[0].call(this);
+ });
+ },
+ dequeue: function(type){
+ return this.each(function(){
+ jQuery.dequeue( this, type );
+ });
+ }
+});/*!
+ * Sizzle CSS Selector Engine - v0.9.3
+ * Copyright 2009, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ * More information: http://sizzlejs.com/
+ */
+(function(){
+
+var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,
+ done = 0,
+ toString = Object.prototype.toString;
+
+var Sizzle = function(selector, context, results, seed) {
+ results = results || [];
+ context = context || document;
+
+ if ( context.nodeType !== 1 && context.nodeType !== 9 )
+ return [];
+
+ if ( !selector || typeof selector !== "string" ) {
+ return results;
+ }
+
+ var parts = [], m, set, checkSet, check, mode, extra, prune = true;
+
+ // Reset the position of the chunker regexp (start from head)
+ chunker.lastIndex = 0;
+
+ while ( (m = chunker.exec(selector)) !== null ) {
+ parts.push( m[1] );
+
+ if ( m[2] ) {
+ extra = RegExp.rightContext;
+ break;
+ }
+ }
+
+ if ( parts.length > 1 && origPOS.exec( selector ) ) {
+ if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
+ set = posProcess( parts[0] + parts[1], context );
+ } else {
+ set = Expr.relative[ parts[0] ] ?
+ [ context ] :
+ Sizzle( parts.shift(), context );
+
+ while ( parts.length ) {
+ selector = parts.shift();
+
+ if ( Expr.relative[ selector ] )
+ selector += parts.shift();
+
+ set = posProcess( selector, set );
+ }
+ }
+ } else {
+ var ret = seed ?
+ { expr: parts.pop(), set: makeArray(seed) } :
+ Sizzle.find( parts.pop(), parts.length === 1 && context.parentNode ? context.parentNode : context, isXML(context) );
+ set = Sizzle.filter( ret.expr, ret.set );
+
+ if ( parts.length > 0 ) {
+ checkSet = makeArray(set);
+ } else {
+ prune = false;
+ }
+
+ while ( parts.length ) {
+ var cur = parts.pop(), pop = cur;
+
+ if ( !Expr.relative[ cur ] ) {
+ cur = "";
+ } else {
+ pop = parts.pop();
+ }
+
+ if ( pop == null ) {
+ pop = context;
+ }
+
+ Expr.relative[ cur ]( checkSet, pop, isXML(context) );
+ }
+ }
+
+ if ( !checkSet ) {
+ checkSet = set;
+ }
+
+ if ( !checkSet ) {
+ throw "Syntax error, unrecognized expression: " + (cur || selector);
+ }
+
+ if ( toString.call(checkSet) === "[object Array]" ) {
+ if ( !prune ) {
+ results.push.apply( results, checkSet );
+ } else if ( context.nodeType === 1 ) {
+ for ( var i = 0; checkSet[i] != null; i++ ) {
+ if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) {
+ results.push( set[i] );
+ }
+ }
+ } else {
+ for ( var i = 0; checkSet[i] != null; i++ ) {
+ if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
+ results.push( set[i] );
+ }
+ }
+ }
+ } else {
+ makeArray( checkSet, results );
+ }
+
+ if ( extra ) {
+ Sizzle( extra, context, results, seed );
+
+ if ( sortOrder ) {
+ hasDuplicate = false;
+ results.sort(sortOrder);
+
+ if ( hasDuplicate ) {
+ for ( var i = 1; i < results.length; i++ ) {
+ if ( results[i] === results[i-1] ) {
+ results.splice(i--, 1);
+ }
+ }
+ }
+ }
+ }
+
+ return results;
+};
+
+Sizzle.matches = function(expr, set){
+ return Sizzle(expr, null, null, set);
+};
+
+Sizzle.find = function(expr, context, isXML){
+ var set, match;
+
+ if ( !expr ) {
+ return [];
+ }
+
+ for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
+ var type = Expr.order[i], match;
+
+ if ( (match = Expr.match[ type ].exec( expr )) ) {
+ var left = RegExp.leftContext;
+
+ if ( left.substr( left.length - 1 ) !== "\\" ) {
+ match[1] = (match[1] || "").replace(/\\/g, "");
+ set = Expr.find[ type ]( match, context, isXML );
+ if ( set != null ) {
+ expr = expr.replace( Expr.match[ type ], "" );
+ break;
+ }
+ }
+ }
+ }
+
+ if ( !set ) {
+ set = context.getElementsByTagName("*");
+ }
+
+ return {set: set, expr: expr};
+};
+
+Sizzle.filter = function(expr, set, inplace, not){
+ var old = expr, result = [], curLoop = set, match, anyFound,
+ isXMLFilter = set && set[0] && isXML(set[0]);
+
+ while ( expr && set.length ) {
+ for ( var type in Expr.filter ) {
+ if ( (match = Expr.match[ type ].exec( expr )) != null ) {
+ var filter = Expr.filter[ type ], found, item;
+ anyFound = false;
+
+ if ( curLoop == result ) {
+ result = [];
+ }
+
+ if ( Expr.preFilter[ type ] ) {
+ match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
+
+ if ( !match ) {
+ anyFound = found = true;
+ } else if ( match === true ) {
+ continue;
+ }
+ }
+
+ if ( match ) {
+ for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
+ if ( item ) {
+ found = filter( item, match, i, curLoop );
+ var pass = not ^ !!found;
+
+ if ( inplace && found != null ) {
+ if ( pass ) {
+ anyFound = true;
+ } else {
+ curLoop[i] = false;
+ }
+ } else if ( pass ) {
+ result.push( item );
+ anyFound = true;
+ }
+ }
+ }
+ }
+
+ if ( found !== undefined ) {
+ if ( !inplace ) {
+ curLoop = result;
+ }
+
+ expr = expr.replace( Expr.match[ type ], "" );
+
+ if ( !anyFound ) {
+ return [];
+ }
+
+ break;
+ }
+ }
+ }
+
+ // Improper expression
+ if ( expr == old ) {
+ if ( anyFound == null ) {
+ throw "Syntax error, unrecognized expression: " + expr;
+ } else {
+ break;
+ }
+ }
+
+ old = expr;
+ }
+
+ return curLoop;
+};
+
+var Expr = Sizzle.selectors = {
+ order: [ "ID", "NAME", "TAG" ],
+ match: {
+ ID: /#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,
+ CLASS: /\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,
+ NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,
+ ATTR: /\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
+ TAG: /^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,
+ CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,
+ POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,
+ PSEUDO: /:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/
+ },
+ attrMap: {
+ "class": "className",
+ "for": "htmlFor"
+ },
+ attrHandle: {
+ href: function(elem){
+ return elem.getAttribute("href");
+ }
+ },
+ relative: {
+ "+": function(checkSet, part, isXML){
+ var isPartStr = typeof part === "string",
+ isTag = isPartStr && !/\W/.test(part),
+ isPartStrNotTag = isPartStr && !isTag;
+
+ if ( isTag && !isXML ) {
+ part = part.toUpperCase();
+ }
+
+ for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
+ if ( (elem = checkSet[i]) ) {
+ while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
+
+ checkSet[i] = isPartStrNotTag || elem && elem.nodeName === part ?
+ elem || false :
+ elem === part;
+ }
+ }
+
+ if ( isPartStrNotTag ) {
+ Sizzle.filter( part, checkSet, true );
+ }
+ },
+ ">": function(checkSet, part, isXML){
+ var isPartStr = typeof part === "string";
+
+ if ( isPartStr && !/\W/.test(part) ) {
+ part = isXML ? part : part.toUpperCase();
+
+ for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+ var elem = checkSet[i];
+ if ( elem ) {
+ var parent = elem.parentNode;
+ checkSet[i] = parent.nodeName === part ? parent : false;
+ }
+ }
+ } else {
+ for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+ var elem = checkSet[i];
+ if ( elem ) {
+ checkSet[i] = isPartStr ?
+ elem.parentNode :
+ elem.parentNode === part;
+ }
+ }
+
+ if ( isPartStr ) {
+ Sizzle.filter( part, checkSet, true );
+ }
+ }
+ },
+ "": function(checkSet, part, isXML){
+ var doneName = done++, checkFn = dirCheck;
+
+ if ( !part.match(/\W/) ) {
+ var nodeCheck = part = isXML ? part : part.toUpperCase();
+ checkFn = dirNodeCheck;
+ }
+
+ checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML);
+ },
+ "~": function(checkSet, part, isXML){
+ var doneName = done++, checkFn = dirCheck;
+
+ if ( typeof part === "string" && !part.match(/\W/) ) {
+ var nodeCheck = part = isXML ? part : part.toUpperCase();
+ checkFn = dirNodeCheck;
+ }
+
+ checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML);
+ }
+ },
+ find: {
+ ID: function(match, context, isXML){
+ if ( typeof context.getElementById !== "undefined" && !isXML ) {
+ var m = context.getElementById(match[1]);
+ return m ? [m] : [];
+ }
+ },
+ NAME: function(match, context, isXML){
+ if ( typeof context.getElementsByName !== "undefined" ) {
+ var ret = [], results = context.getElementsByName(match[1]);
+
+ for ( var i = 0, l = results.length; i < l; i++ ) {
+ if ( results[i].getAttribute("name") === match[1] ) {
+ ret.push( results[i] );
+ }
+ }
+
+ return ret.length === 0 ? null : ret;
+ }
+ },
+ TAG: function(match, context){
+ return context.getElementsByTagName(match[1]);
+ }
+ },
+ preFilter: {
+ CLASS: function(match, curLoop, inplace, result, not, isXML){
+ match = " " + match[1].replace(/\\/g, "") + " ";
+
+ if ( isXML ) {
+ return match;
+ }
+
+ for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
+ if ( elem ) {
+ if ( not ^ (elem.className && (" " + elem.className + " ").indexOf(match) >= 0) ) {
+ if ( !inplace )
+ result.push( elem );
+ } else if ( inplace ) {
+ curLoop[i] = false;
+ }
+ }
+ }
+
+ return false;
+ },
+ ID: function(match){
+ return match[1].replace(/\\/g, "");
+ },
+ TAG: function(match, curLoop){
+ for ( var i = 0; curLoop[i] === false; i++ ){}
+ return curLoop[i] && isXML(curLoop[i]) ? match[1] : match[1].toUpperCase();
+ },
+ CHILD: function(match){
+ if ( match[1] == "nth" ) {
+ // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
+ var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
+ match[2] == "even" && "2n" || match[2] == "odd" && "2n+1" ||
+ !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
+
+ // calculate the numbers (first)n+(last) including if they are negative
+ match[2] = (test[1] + (test[2] || 1)) - 0;
+ match[3] = test[3] - 0;
+ }
+
+ // TODO: Move to normal caching system
+ match[0] = done++;
+
+ return match;
+ },
+ ATTR: function(match, curLoop, inplace, result, not, isXML){
+ var name = match[1].replace(/\\/g, "");
+
+ if ( !isXML && Expr.attrMap[name] ) {
+ match[1] = Expr.attrMap[name];
+ }
+
+ if ( match[2] === "~=" ) {
+ match[4] = " " + match[4] + " ";
+ }
+
+ return match;
+ },
+ PSEUDO: function(match, curLoop, inplace, result, not){
+ if ( match[1] === "not" ) {
+ // If we're dealing with a complex expression, or a simple one
+ if ( match[3].match(chunker).length > 1 || /^\w/.test(match[3]) ) {
+ match[3] = Sizzle(match[3], null, null, curLoop);
+ } else {
+ var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
+ if ( !inplace ) {
+ result.push.apply( result, ret );
+ }
+ return false;
+ }
+ } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
+ return true;
+ }
+
+ return match;
+ },
+ POS: function(match){
+ match.unshift( true );
+ return match;
+ }
+ },
+ filters: {
+ enabled: function(elem){
+ return elem.disabled === false && elem.type !== "hidden";
+ },
+ disabled: function(elem){
+ return elem.disabled === true;
+ },
+ checked: function(elem){
+ return elem.checked === true;
+ },
+ selected: function(elem){
+ // Accessing this property makes selected-by-default
+ // options in Safari work properly
+ elem.parentNode.selectedIndex;
+ return elem.selected === true;
+ },
+ parent: function(elem){
+ return !!elem.firstChild;
+ },
+ empty: function(elem){
+ return !elem.firstChild;
+ },
+ has: function(elem, i, match){
+ return !!Sizzle( match[3], elem ).length;
+ },
+ header: function(elem){
+ return /h\d/i.test( elem.nodeName );
+ },
+ text: function(elem){
+ return "text" === elem.type;
+ },
+ radio: function(elem){
+ return "radio" === elem.type;
+ },
+ checkbox: function(elem){
+ return "checkbox" === elem.type;
+ },
+ file: function(elem){
+ return "file" === elem.type;
+ },
+ password: function(elem){
+ return "password" === elem.type;
+ },
+ submit: function(elem){
+ return "submit" === elem.type;
+ },
+ image: function(elem){
+ return "image" === elem.type;
+ },
+ reset: function(elem){
+ return "reset" === elem.type;
+ },
+ button: function(elem){
+ return "button" === elem.type || elem.nodeName.toUpperCase() === "BUTTON";
+ },
+ input: function(elem){
+ return /input|select|textarea|button/i.test(elem.nodeName);
+ }
+ },
+ setFilters: {
+ first: function(elem, i){
+ return i === 0;
+ },
+ last: function(elem, i, match, array){
+ return i === array.length - 1;
+ },
+ even: function(elem, i){
+ return i % 2 === 0;
+ },
+ odd: function(elem, i){
+ return i % 2 === 1;
+ },
+ lt: function(elem, i, match){
+ return i < match[3] - 0;
+ },
+ gt: function(elem, i, match){
+ return i > match[3] - 0;
+ },
+ nth: function(elem, i, match){
+ return match[3] - 0 == i;
+ },
+ eq: function(elem, i, match){
+ return match[3] - 0 == i;
+ }
+ },
+ filter: {
+ PSEUDO: function(elem, match, i, array){
+ var name = match[1], filter = Expr.filters[ name ];
+
+ if ( filter ) {
+ return filter( elem, i, match, array );
+ } else if ( name === "contains" ) {
+ return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0;
+ } else if ( name === "not" ) {
+ var not = match[3];
+
+ for ( var i = 0, l = not.length; i < l; i++ ) {
+ if ( not[i] === elem ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ },
+ CHILD: function(elem, match){
+ var type = match[1], node = elem;
+ switch (type) {
+ case 'only':
+ case 'first':
+ while (node = node.previousSibling) {
+ if ( node.nodeType === 1 ) return false;
+ }
+ if ( type == 'first') return true;
+ node = elem;
+ case 'last':
+ while (node = node.nextSibling) {
+ if ( node.nodeType === 1 ) return false;
+ }
+ return true;
+ case 'nth':
+ var first = match[2], last = match[3];
+
+ if ( first == 1 && last == 0 ) {
+ return true;
+ }
+
+ var doneName = match[0],
+ parent = elem.parentNode;
+
+ if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) {
+ var count = 0;
+ for ( node = parent.firstChild; node; node = node.nextSibling ) {
+ if ( node.nodeType === 1 ) {
+ node.nodeIndex = ++count;
+ }
+ }
+ parent.sizcache = doneName;
+ }
+
+ var diff = elem.nodeIndex - last;
+ if ( first == 0 ) {
+ return diff == 0;
+ } else {
+ return ( diff % first == 0 && diff / first >= 0 );
+ }
+ }
+ },
+ ID: function(elem, match){
+ return elem.nodeType === 1 && elem.getAttribute("id") === match;
+ },
+ TAG: function(elem, match){
+ return (match === "*" && elem.nodeType === 1) || elem.nodeName === match;
+ },
+ CLASS: function(elem, match){
+ return (" " + (elem.className || elem.getAttribute("class")) + " ")
+ .indexOf( match ) > -1;
+ },
+ ATTR: function(elem, match){
+ var name = match[1],
+ result = Expr.attrHandle[ name ] ?
+ Expr.attrHandle[ name ]( elem ) :
+ elem[ name ] != null ?
+ elem[ name ] :
+ elem.getAttribute( name ),
+ value = result + "",
+ type = match[2],
+ check = match[4];
+
+ return result == null ?
+ type === "!=" :
+ type === "=" ?
+ value === check :
+ type === "*=" ?
+ value.indexOf(check) >= 0 :
+ type === "~=" ?
+ (" " + value + " ").indexOf(check) >= 0 :
+ !check ?
+ value && result !== false :
+ type === "!=" ?
+ value != check :
+ type === "^=" ?
+ value.indexOf(check) === 0 :
+ type === "$=" ?
+ value.substr(value.length - check.length) === check :
+ type === "|=" ?
+ value === check || value.substr(0, check.length + 1) === check + "-" :
+ false;
+ },
+ POS: function(elem, match, i, array){
+ var name = match[2], filter = Expr.setFilters[ name ];
+
+ if ( filter ) {
+ return filter( elem, i, match, array );
+ }
+ }
+ }
+};
+
+var origPOS = Expr.match.POS;
+
+for ( var type in Expr.match ) {
+ Expr.match[ type ] = RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source );
+}
+
+var makeArray = function(array, results) {
+ array = Array.prototype.slice.call( array );
+
+ if ( results ) {
+ results.push.apply( results, array );
+ return results;
+ }
+
+ return array;
+};
+
+// Perform a simple check to determine if the browser is capable of
+// converting a NodeList to an array using builtin methods.
+try {
+ Array.prototype.slice.call( document.documentElement.childNodes );
+
+// Provide a fallback method if it does not work
+} catch(e){
+ makeArray = function(array, results) {
+ var ret = results || [];
+
+ if ( toString.call(array) === "[object Array]" ) {
+ Array.prototype.push.apply( ret, array );
+ } else {
+ if ( typeof array.length === "number" ) {
+ for ( var i = 0, l = array.length; i < l; i++ ) {
+ ret.push( array[i] );
+ }
+ } else {
+ for ( var i = 0; array[i]; i++ ) {
+ ret.push( array[i] );
+ }
+ }
+ }
+
+ return ret;
+ };
+}
+
+var sortOrder;
+
+if ( document.documentElement.compareDocumentPosition ) {
+ sortOrder = function( a, b ) {
+ var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;
+ if ( ret === 0 ) {
+ hasDuplicate = true;
+ }
+ return ret;
+ };
+} else if ( "sourceIndex" in document.documentElement ) {
+ sortOrder = function( a, b ) {
+ var ret = a.sourceIndex - b.sourceIndex;
+ if ( ret === 0 ) {
+ hasDuplicate = true;
+ }
+ return ret;
+ };
+} else if ( document.createRange ) {
+ sortOrder = function( a, b ) {
+ var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();
+ aRange.selectNode(a);
+ aRange.collapse(true);
+ bRange.selectNode(b);
+ bRange.collapse(true);
+ var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
+ if ( ret === 0 ) {
+ hasDuplicate = true;
+ }
+ return ret;
+ };
+}
+
+// Check to see if the browser returns elements by name when
+// querying by getElementById (and provide a workaround)
+(function(){
+ // We're going to inject a fake input element with a specified name
+ var form = document.createElement("form"),
+ id = "script" + (new Date).getTime();
+ form.innerHTML = "<input name='" + id + "'/>";
+
+ // Inject it into the root element, check its status, and remove it quickly
+ var root = document.documentElement;
+ root.insertBefore( form, root.firstChild );
+
+ // The workaround has to do additional checks after a getElementById
+ // Which slows things down for other browsers (hence the branching)
+ if ( !!document.getElementById( id ) ) {
+ Expr.find.ID = function(match, context, isXML){
+ if ( typeof context.getElementById !== "undefined" && !isXML ) {
+ var m = context.getElementById(match[1]);
+ return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : [];
+ }
+ };
+
+ Expr.filter.ID = function(elem, match){
+ var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
+ return elem.nodeType === 1 && node && node.nodeValue === match;
+ };
+ }
+
+ root.removeChild( form );
+})();
+
+(function(){
+ // Check to see if the browser returns only elements
+ // when doing getElementsByTagName("*")
+
+ // Create a fake element
+ var div = document.createElement("div");
+ div.appendChild( document.createComment("") );
+
+ // Make sure no comments are found
+ if ( div.getElementsByTagName("*").length > 0 ) {
+ Expr.find.TAG = function(match, context){
+ var results = context.getElementsByTagName(match[1]);
+
+ // Filter out possible comments
+ if ( match[1] === "*" ) {
+ var tmp = [];
+
+ for ( var i = 0; results[i]; i++ ) {
+ if ( results[i].nodeType === 1 ) {
+ tmp.push( results[i] );
+ }
+ }
+
+ results = tmp;
+ }
+
+ return results;
+ };
+ }
+
+ // Check to see if an attribute returns normalized href attributes
+ div.innerHTML = "<a href='#'></a>";
+ if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
+ div.firstChild.getAttribute("href") !== "#" ) {
+ Expr.attrHandle.href = function(elem){
+ return elem.getAttribute("href", 2);
+ };
+ }
+})();
+
+if ( document.querySelectorAll ) (function(){
+ var oldSizzle = Sizzle, div = document.createElement("div");
+ div.innerHTML = "<p class='TEST'></p>";
+
+ // Safari can't handle uppercase or unicode characters when
+ // in quirks mode.
+ if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
+ return;
+ }
+
+ Sizzle = function(query, context, extra, seed){
+ context = context || document;
+
+ // Only use querySelectorAll on non-XML documents
+ // (ID selectors don't work in non-HTML documents)
+ if ( !seed && context.nodeType === 9 && !isXML(context) ) {
+ try {
+ return makeArray( context.querySelectorAll(query), extra );
+ } catch(e){}
+ }
+
+ return oldSizzle(query, context, extra, seed);
+ };
+
+ Sizzle.find = oldSizzle.find;
+ Sizzle.filter = oldSizzle.filter;
+ Sizzle.selectors = oldSizzle.selectors;
+ Sizzle.matches = oldSizzle.matches;
+})();
+
+if ( document.getElementsByClassName && document.documentElement.getElementsByClassName ) (function(){
+ var div = document.createElement("div");
+ div.innerHTML = "<div class='test e'></div><div class='test'></div>";
+
+ // Opera can't find a second classname (in 9.6)
+ if ( div.getElementsByClassName("e").length === 0 )
+ return;
+
+ // Safari caches class attributes, doesn't catch changes (in 3.2)
+ div.lastChild.className = "e";
+
+ if ( div.getElementsByClassName("e").length === 1 )
+ return;
+
+ Expr.order.splice(1, 0, "CLASS");
+ Expr.find.CLASS = function(match, context, isXML) {
+ if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
+ return context.getElementsByClassName(match[1]);
+ }
+ };
+})();
+
+function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
+ var sibDir = dir == "previousSibling" && !isXML;
+ for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+ var elem = checkSet[i];
+ if ( elem ) {
+ if ( sibDir && elem.nodeType === 1 ){
+ elem.sizcache = doneName;
+ elem.sizset = i;
+ }
+ elem = elem[dir];
+ var match = false;
+
+ while ( elem ) {
+ if ( elem.sizcache === doneName ) {
+ match = checkSet[elem.sizset];
+ break;
+ }
+
+ if ( elem.nodeType === 1 && !isXML ){
+ elem.sizcache = doneName;
+ elem.sizset = i;
+ }
+
+ if ( elem.nodeName === cur ) {
+ match = elem;
+ break;
+ }
+
+ elem = elem[dir];
+ }
+
+ checkSet[i] = match;
+ }
+ }
+}
+
+function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
+ var sibDir = dir == "previousSibling" && !isXML;
+ for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+ var elem = checkSet[i];
+ if ( elem ) {
+ if ( sibDir && elem.nodeType === 1 ) {
+ elem.sizcache = doneName;
+ elem.sizset = i;
+ }
+ elem = elem[dir];
+ var match = false;
+
+ while ( elem ) {
+ if ( elem.sizcache === doneName ) {
+ match = checkSet[elem.sizset];
+ break;
+ }
+
+ if ( elem.nodeType === 1 ) {
+ if ( !isXML ) {
+ elem.sizcache = doneName;
+ elem.sizset = i;
+ }
+ if ( typeof cur !== "string" ) {
+ if ( elem === cur ) {
+ match = true;
+ break;
+ }
+
+ } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
+ match = elem;
+ break;
+ }
+ }
+
+ elem = elem[dir];
+ }
+
+ checkSet[i] = match;
+ }
+ }
+}
+
+var contains = document.compareDocumentPosition ? function(a, b){
+ return a.compareDocumentPosition(b) & 16;
+} : function(a, b){
+ return a !== b && (a.contains ? a.contains(b) : true);
+};
+
+var isXML = function(elem){
+ return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
+ !!elem.ownerDocument && isXML( elem.ownerDocument );
+};
+
+var posProcess = function(selector, context){
+ var tmpSet = [], later = "", match,
+ root = context.nodeType ? [context] : context;
+
+ // Position selectors must be done after the filter
+ // And so must :not(positional) so we move all PSEUDOs to the end
+ while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
+ later += match[0];
+ selector = selector.replace( Expr.match.PSEUDO, "" );
+ }
+
+ selector = Expr.relative[selector] ? selector + "*" : selector;
+
+ for ( var i = 0, l = root.length; i < l; i++ ) {
+ Sizzle( selector, root[i], tmpSet );
+ }
+
+ return Sizzle.filter( later, tmpSet );
+};
+
+// EXPOSE
+jQuery.find = Sizzle;
+jQuery.filter = Sizzle.filter;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[":"] = jQuery.expr.filters;
+
+Sizzle.selectors.filters.hidden = function(elem){
+ return elem.offsetWidth === 0 || elem.offsetHeight === 0;
+};
+
+Sizzle.selectors.filters.visible = function(elem){
+ return elem.offsetWidth > 0 || elem.offsetHeight > 0;
+};
+
+Sizzle.selectors.filters.animated = function(elem){
+ return jQuery.grep(jQuery.timers, function(fn){
+ return elem === fn.elem;
+ }).length;
+};
+
+jQuery.multiFilter = function( expr, elems, not ) {
+ if ( not ) {
+ expr = ":not(" + expr + ")";
+ }
+
+ return Sizzle.matches(expr, elems);
+};
+
+jQuery.dir = function( elem, dir ){
+ var matched = [], cur = elem[dir];
+ while ( cur && cur != document ) {
+ if ( cur.nodeType == 1 )
+ matched.push( cur );
+ cur = cur[dir];
+ }
+ return matched;
+};
+
+jQuery.nth = function(cur, result, dir, elem){
+ result = result || 1;
+ var num = 0;
+
+ for ( ; cur; cur = cur[dir] )
+ if ( cur.nodeType == 1 && ++num == result )
+ break;
+
+ return cur;
+};
+
+jQuery.sibling = function(n, elem){
+ var r = [];
+
+ for ( ; n; n = n.nextSibling ) {
+ if ( n.nodeType == 1 && n != elem )
+ r.push( n );
+ }
+
+ return r;
+};
+
+return;
+
+window.Sizzle = Sizzle;
+
+})();
+/*
+ * A number of helper functions used for managing events.
+ * Many of the ideas behind this code originated from
+ * Dean Edwards' addEvent library.
+ */
+jQuery.event = {
+
+ // Bind an event to an element
+ // Original by Dean Edwards
+ add: function(elem, types, handler, data) {
+ if ( elem.nodeType == 3 || elem.nodeType == 8 )
+ return;
+
+ // For whatever reason, IE has trouble passing the window object
+ // around, causing it to be cloned in the process
+ if ( elem.setInterval && elem != window )
+ elem = window;
+
+ // Make sure that the function being executed has a unique ID
+ if ( !handler.guid )
+ handler.guid = this.guid++;
+
+ // if data is passed, bind to handler
+ if ( data !== undefined ) {
+ // Create temporary function pointer to original handler
+ var fn = handler;
+
+ // Create unique handler function, wrapped around original handler
+ handler = this.proxy( fn );
+
+ // Store data in unique handler
+ handler.data = data;
+ }
+
+ // Init the element's event structure
+ var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
+ handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
+ // Handle the second event of a trigger and when
+ // an event is called after a page has unloaded
+ return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
+ jQuery.event.handle.apply(arguments.callee.elem, arguments) :
+ undefined;
+ });
+ // Add elem as a property of the handle function
+ // This is to prevent a memory leak with non-native
+ // event in IE.
+ handle.elem = elem;
+
+ // Handle multiple events separated by a space
+ // jQuery(...).bind("mouseover mouseout", fn);
+ jQuery.each(types.split(/\s+/), function(index, type) {
+ // Namespaced event handlers
+ var namespaces = type.split(".");
+ type = namespaces.shift();
+ handler.type = namespaces.slice().sort().join(".");
+
+ // Get the current list of functions bound to this event
+ var handlers = events[type];
+
+ if ( jQuery.event.specialAll[type] )
+ jQuery.event.specialAll[type].setup.call(elem, data, namespaces);
+
+ // Init the event handler queue
+ if (!handlers) {
+ handlers = events[type] = {};
+
+ // Check for a special event handler
+ // Only use addEventListener/attachEvent if the special
+ // events handler returns false
+ if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem, data, namespaces) === false ) {
+ // Bind the global event handler to the element
+ if (elem.addEventListener)
+ elem.addEventListener(type, handle, false);
+ else if (elem.attachEvent)
+ elem.attachEvent("on" + type, handle);
+ }
+ }
+
+ // Add the function to the element's handler list
+ handlers[handler.guid] = handler;
+
+ // Keep track of which events have been used, for global triggering
+ jQuery.event.global[type] = true;
+ });
+
+ // Nullify elem to prevent memory leaks in IE
+ elem = null;
+ },
+
+ guid: 1,
+ global: {},
+
+ // Detach an event or set of events from an element
+ remove: function(elem, types, handler) {
+ // don't do events on text and comment nodes
+ if ( elem.nodeType == 3 || elem.nodeType == 8 )
+ return;
+
+ var events = jQuery.data(elem, "events"), ret, index;
+
+ if ( events ) {
+ // Unbind all events for the element
+ if ( types === undefined || (typeof types === "string" && types.charAt(0) == ".") )
+ for ( var type in events )
+ this.remove( elem, type + (types || "") );
+ else {
+ // types is actually an event object here
+ if ( types.type ) {
+ handler = types.handler;
+ types = types.type;
+ }
+
+ // Handle multiple events seperated by a space
+ // jQuery(...).unbind("mouseover mouseout", fn);
+ jQuery.each(types.split(/\s+/), function(index, type){
+ // Namespaced event handlers
+ var namespaces = type.split(".");
+ type = namespaces.shift();
+ var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
+
+ if ( events[type] ) {
+ // remove the given handler for the given type
+ if ( handler )
+ delete events[type][handler.guid];
+
+ // remove all handlers for the given type
+ else
+ for ( var handle in events[type] )
+ // Handle the removal of namespaced events
+ if ( namespace.test(events[type][handle].type) )
+ delete events[type][handle];
+
+ if ( jQuery.event.specialAll[type] )
+ jQuery.event.specialAll[type].teardown.call(elem, namespaces);
+
+ // remove generic event handler if no more handlers exist
+ for ( ret in events[type] ) break;
+ if ( !ret ) {
+ if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem, namespaces) === false ) {
+ if (elem.removeEventListener)
+ elem.removeEventListener(type, jQuery.data(elem, "handle"), false);
+ else if (elem.detachEvent)
+ elem.detachEvent("on" + type, jQuery.data(elem, "handle"));
+ }
+ ret = null;
+ delete events[type];
+ }
+ }
+ });
+ }
+
+ // Remove the expando if it's no longer used
+ for ( ret in events ) break;
+ if ( !ret ) {
+ var handle = jQuery.data( elem, "handle" );
+ if ( handle ) handle.elem = null;
+ jQuery.removeData( elem, "events" );
+ jQuery.removeData( elem, "handle" );
+ }
+ }
+ },
+
+ // bubbling is internal
+ trigger: function( event, data, elem, bubbling ) {
+ // Event object or event type
+ var type = event.type || event;
+
+ if( !bubbling ){
+ event = typeof event === "object" ?
+ // jQuery.Event object
+ event[expando] ? event :
+ // Object literal
+ jQuery.extend( jQuery.Event(type), event ) :
+ // Just the event type (string)
+ jQuery.Event(type);
+
+ if ( type.indexOf("!") >= 0 ) {
+ event.type = type = type.slice(0, -1);
+ event.exclusive = true;
+ }
+
+ // Handle a global trigger
+ if ( !elem ) {
+ // Don't bubble custom events when global (to avoid too much overhead)
+ event.stopPropagation();
+ // Only trigger if we've ever bound an event for it
+ if ( this.global[type] )
+ jQuery.each( jQuery.cache, function(){
+ if ( this.events && this.events[type] )
+ jQuery.event.trigger( event, data, this.handle.elem );
+ });
+ }
+
+ // Handle triggering a single element
+
+ // don't do events on text and comment nodes
+ if ( !elem || elem.nodeType == 3 || elem.nodeType == 8 )
+ return undefined;
+
+ // Clean up in case it is reused
+ event.result = undefined;
+ event.target = elem;
+
+ // Clone the incoming data, if any
+ data = jQuery.makeArray(data);
+ data.unshift( event );
+ }
+
+ event.currentTarget = elem;
+
+ // Trigger the event, it is assumed that "handle" is a function
+ var handle = jQuery.data(elem, "handle");
+ if ( handle )
+ handle.apply( elem, data );
+
+ // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
+ if ( (!elem[type] || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )
+ event.result = false;
+
+ // Trigger the native events (except for clicks on links)
+ if ( !bubbling && elem[type] && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
+ this.triggered = true;
+ try {
+ elem[ type ]();
+ // prevent IE from throwing an error for some hidden elements
+ } catch (e) {}
+ }
+
+ this.triggered = false;
+
+ if ( !event.isPropagationStopped() ) {
+ var parent = elem.parentNode || elem.ownerDocument;
+ if ( parent )
+ jQuery.event.trigger(event, data, parent, true);
+ }
+ },
+
+ handle: function(event) {
+ // returned undefined or false
+ var all, handlers;
+
+ event = arguments[0] = jQuery.event.fix( event || window.event );
+ event.currentTarget = this;
+
+ // Namespaced event handlers
+ var namespaces = event.type.split(".");
+ event.type = namespaces.shift();
+
+ // Cache this now, all = true means, any handler
+ all = !namespaces.length && !event.exclusive;
+
+ var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
+
+ handlers = ( jQuery.data(this, "events") || {} )[event.type];
+
+ for ( var j in handlers ) {
+ var handler = handlers[j];
+
+ // Filter the functions by class
+ if ( all || namespace.test(handler.type) ) {
+ // Pass in a reference to the handler function itself
+ // So that we can later remove it
+ event.handler = handler;
+ event.data = handler.data;
+
+ var ret = handler.apply(this, arguments);
+
+ if( ret !== undefined ){
+ event.result = ret;
+ if ( ret === false ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ if( event.isImmediatePropagationStopped() )
+ break;
+
+ }
+ }
+ },
+
+ props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+
+ fix: function(event) {
+ if ( event[expando] )
+ return event;
+
+ // store a copy of the original event object
+ // and "clone" to set read-only properties
+ var originalEvent = event;
+ event = jQuery.Event( originalEvent );
+
+ for ( var i = this.props.length, prop; i; ){
+ prop = this.props[ --i ];
+ event[ prop ] = originalEvent[ prop ];
+ }
+
+ // Fix target property, if necessary
+ if ( !event.target )
+ event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
+
+ // check if target is a textnode (safari)
+ if ( event.target.nodeType == 3 )
+ event.target = event.target.parentNode;
+
+ // Add relatedTarget, if necessary
+ if ( !event.relatedTarget && event.fromElement )
+ event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
+
+ // Calculate pageX/Y if missing and clientX/Y available
+ if ( event.pageX == null && event.clientX != null ) {
+ var doc = document.documentElement, body = document.body;
+ event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
+ event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
+ }
+
+ // Add which for key events
+ if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
+ event.which = event.charCode || event.keyCode;
+
+ // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
+ if ( !event.metaKey && event.ctrlKey )
+ event.metaKey = event.ctrlKey;
+
+ // Add which for click: 1 == left; 2 == middle; 3 == right
+ // Note: button is not normalized, so don't use it
+ if ( !event.which && event.button )
+ event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
+
+ return event;
+ },
+
+ proxy: function( fn, proxy ){
+ proxy = proxy || function(){ return fn.apply(this, arguments); };
+ // Set the guid of unique handler to the same of original handler, so it can be removed
+ proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
+ // So proxy can be declared as an argument
+ return proxy;
+ },
+
+ special: {
+ ready: {
+ // Make sure the ready event is setup
+ setup: bindReady,
+ teardown: function() {}
+ }
+ },
+
+ specialAll: {
+ live: {
+ setup: function( selector, namespaces ){
+ jQuery.event.add( this, namespaces[0], liveHandler );
+ },
+ teardown: function( namespaces ){
+ if ( namespaces.length ) {
+ var remove = 0, name = RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)");
+
+ jQuery.each( (jQuery.data(this, "events").live || {}), function(){
+ if ( name.test(this.type) )
+ remove++;
+ });
+
+ if ( remove < 1 )
+ jQuery.event.remove( this, namespaces[0], liveHandler );
+ }
+ }
+ }
+ }
+};
+
+jQuery.Event = function( src ){
+ // Allow instantiation without the 'new' keyword
+ if( !this.preventDefault )
+ return new jQuery.Event(src);
+
+ // Event object
+ if( src && src.type ){
+ this.originalEvent = src;
+ this.type = src.type;
+ // Event type
+ }else
+ this.type = src;
+
+ // timeStamp is buggy for some events on Firefox(#3843)
+ // So we won't rely on the native value
+ this.timeStamp = now();
+
+ // Mark it as fixed
+ this[expando] = true;
+};
+
+function returnFalse(){
+ return false;
+}
+function returnTrue(){
+ return true;
+}
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+ preventDefault: function() {
+ this.isDefaultPrevented = returnTrue;
+
+ var e = this.originalEvent;
+ if( !e )
+ return;
+ // if preventDefault exists run it on the original event
+ if (e.preventDefault)
+ e.preventDefault();
+ // otherwise set the returnValue property of the original event to false (IE)
+ e.returnValue = false;
+ },
+ stopPropagation: function() {
+ this.isPropagationStopped = returnTrue;
+
+ var e = this.originalEvent;
+ if( !e )
+ return;
+ // if stopPropagation exists run it on the original event
+ if (e.stopPropagation)
+ e.stopPropagation();
+ // otherwise set the cancelBubble property of the original event to true (IE)
+ e.cancelBubble = true;
+ },
+ stopImmediatePropagation:function(){
+ this.isImmediatePropagationStopped = returnTrue;
+ this.stopPropagation();
+ },
+ isDefaultPrevented: returnFalse,
+ isPropagationStopped: returnFalse,
+ isImmediatePropagationStopped: returnFalse
+};
+// Checks if an event happened on an element within another element
+// Used in jQuery.event.special.mouseenter and mouseleave handlers
+var withinElement = function(event) {
+ // Check if mouse(over|out) are still within the same parent element
+ var parent = event.relatedTarget;
+ // Traverse up the tree
+ while ( parent && parent != this )
+ try { parent = parent.parentNode; }
+ catch(e) { parent = this; }
+
+ if( parent != this ){
+ // set the correct event type
+ event.type = event.data;
+ // handle event if we actually just moused on to a non sub-element
+ jQuery.event.handle.apply( this, arguments );
+ }
+};
+
+jQuery.each({
+ mouseover: 'mouseenter',
+ mouseout: 'mouseleave'
+}, function( orig, fix ){
+ jQuery.event.special[ fix ] = {
+ setup: function(){
+ jQuery.event.add( this, orig, withinElement, fix );
+ },
+ teardown: function(){
+ jQuery.event.remove( this, orig, withinElement );
+ }
+ };
+});
+
+jQuery.fn.extend({
+ bind: function( type, data, fn ) {
+ return type == "unload" ? this.one(type, data, fn) : this.each(function(){
+ jQuery.event.add( this, type, fn || data, fn && data );
+ });
+ },
+
+ one: function( type, data, fn ) {
+ var one = jQuery.event.proxy( fn || data, function(event) {
+ jQuery(this).unbind(event, one);
+ return (fn || data).apply( this, arguments );
+ });
+ return this.each(function(){
+ jQuery.event.add( this, type, one, fn && data);
+ });
+ },
+
+ unbind: function( type, fn ) {
+ return this.each(function(){
+ jQuery.event.remove( this, type, fn );
+ });
+ },
+
+ trigger: function( type, data ) {
+ return this.each(function(){
+ jQuery.event.trigger( type, data, this );
+ });
+ },
+
+ triggerHandler: function( type, data ) {
+ if( this[0] ){
+ var event = jQuery.Event(type);
+ event.preventDefault();
+ event.stopPropagation();
+ jQuery.event.trigger( event, data, this[0] );
+ return event.result;
+ }
+ },
+
+ toggle: function( fn ) {
+ // Save reference to arguments for access in closure
+ var args = arguments, i = 1;
+
+ // link all the functions, so any of them can unbind this click handler
+ while( i < args.length )
+ jQuery.event.proxy( fn, args[i++] );
+
+ return this.click( jQuery.event.proxy( fn, function(event) {
+ // Figure out which function to execute
+ this.lastToggle = ( this.lastToggle || 0 ) % i;
+
+ // Make sure that clicks stop
+ event.preventDefault();
+
+ // and execute the function
+ return args[ this.lastToggle++ ].apply( this, arguments ) || false;
+ }));
+ },
+
+ hover: function(fnOver, fnOut) {
+ return this.mouseenter(fnOver).mouseleave(fnOut);
+ },
+
+ ready: function(fn) {
+ // Attach the listeners
+ bindReady();
+
+ // If the DOM is already ready
+ if ( jQuery.isReady )
+ // Execute the function immediately
+ fn.call( document, jQuery );
+
+ // Otherwise, remember the function for later
+ else
+ // Add the function to the wait list
+ jQuery.readyList.push( fn );
+
+ return this;
+ },
+
+ live: function( type, fn ){
+ var proxy = jQuery.event.proxy( fn );
+ proxy.guid += this.selector + type;
+
+ jQuery(document).bind( liveConvert(type, this.selector), this.selector, proxy );
+
+ return this;
+ },
+
+ die: function( type, fn ){
+ jQuery(document).unbind( liveConvert(type, this.selector), fn ? { guid: fn.guid + this.selector + type } : null );
+ return this;
+ }
+});
+
+function liveHandler( event ){
+ var check = RegExp("(^|\\.)" + event.type + "(\\.|$)"),
+ stop = true,
+ elems = [];
+
+ jQuery.each(jQuery.data(this, "events").live || [], function(i, fn){
+ if ( check.test(fn.type) ) {
+ var elem = jQuery(event.target).closest(fn.data)[0];
+ if ( elem )
+ elems.push({ elem: elem, fn: fn });
+ }
+ });
+
+ elems.sort(function(a,b) {
+ return jQuery.data(a.elem, "closest") - jQuery.data(b.elem, "closest");
+ });
+
+ jQuery.each(elems, function(){
+ if ( this.fn.call(this.elem, event, this.fn.data) === false )
+ return (stop = false);
+ });
+
+ return stop;
+}
+
+function liveConvert(type, selector){
+ return ["live", type, selector.replace(/\./g, "`").replace(/ /g, "|")].join(".");
+}
+
+jQuery.extend({
+ isReady: false,
+ readyList: [],
+ // Handle when the DOM is ready
+ ready: function() {
+ // Make sure that the DOM is not already loaded
+ if ( !jQuery.isReady ) {
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If there are functions bound, to execute
+ if ( jQuery.readyList ) {
+ // Execute all of them
+ jQuery.each( jQuery.readyList, function(){
+ this.call( document, jQuery );
+ });
+
+ // Reset the list of functions
+ jQuery.readyList = null;
+ }
+
+ // Trigger any bound ready events
+ jQuery(document).triggerHandler("ready");
+ }
+ }
+});
+
+var readyBound = false;
+
+function bindReady(){
+ if ( readyBound ) return;
+ readyBound = true;
+
+ // Mozilla, Opera and webkit nightlies currently support this event
+ if ( document.addEventListener ) {
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", function(){
+ document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
+ jQuery.ready();
+ }, false );
+
+ // If IE event model is used
+ } else if ( document.attachEvent ) {
+ // ensure firing before onload,
+ // maybe late but safe also for iframes
+ document.attachEvent("onreadystatechange", function(){
+ if ( document.readyState === "complete" ) {
+ document.detachEvent( "onreadystatechange", arguments.callee );
+ jQuery.ready();
+ }
+ });
+
+ // If IE and not an iframe
+ // continually check to see if the document is ready
+ if ( document.documentElement.doScroll && window == window.top ) (function(){
+ if ( jQuery.isReady ) return;
+
+ try {
+ // If IE is used, use the trick by Diego Perini
+ // http://javascript.nwbox.com/IEContentLoaded/
+ document.documentElement.doScroll("left");
+ } catch( error ) {
+ setTimeout( arguments.callee, 0 );
+ return;
+ }
+
+ // and execute any waiting functions
+ jQuery.ready();
+ })();
+ }
+
+ // A fallback to window.onload, that will always work
+ jQuery.event.add( window, "load", jQuery.ready );
+}
+
+jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
+ "mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave," +
+ "change,select,submit,keydown,keypress,keyup,error").split(","), function(i, name){
+
+ // Handle event binding
+ jQuery.fn[name] = function(fn){
+ return fn ? this.bind(name, fn) : this.trigger(name);
+ };
+});
+
+// Prevent memory leaks in IE
+// And prevent errors on refresh with events like mouseover in other browsers
+// Window isn't included so as not to unbind existing unload events
+jQuery( window ).bind( 'unload', function(){
+ for ( var id in jQuery.cache )
+ // Skip the window
+ if ( id != 1 && jQuery.cache[ id ].handle )
+ jQuery.event.remove( jQuery.cache[ id ].handle.elem );
+});
+(function(){
+
+ jQuery.support = {};
+
+ var root = document.documentElement,
+ script = document.createElement("script"),
+ div = document.createElement("div"),
+ id = "script" + (new Date).getTime();
+
+ div.style.display = "none";
+ div.innerHTML = ' <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';
+
+ var all = div.getElementsByTagName("*"),
+ a = div.getElementsByTagName("a")[0];
+
+ // Can't get basic test support
+ if ( !all || !all.length || !a ) {
+ return;
+ }
+
+ jQuery.support = {
+ // IE strips leading whitespace when .innerHTML is used
+ leadingWhitespace: div.firstChild.nodeType == 3,
+
+ // Make sure that tbody elements aren't automatically inserted
+ // IE will insert them into empty tables
+ tbody: !div.getElementsByTagName("tbody").length,
+
+ // Make sure that you can get all elements in an <object> element
+ // IE 7 always returns no results
+ objectAll: !!div.getElementsByTagName("object")[0]
+ .getElementsByTagName("*").length,
+
+ // Make sure that link elements get serialized correctly by innerHTML
+ // This requires a wrapper element in IE
+ htmlSerialize: !!div.getElementsByTagName("link").length,
+
+ // Get the style information from getAttribute
+ // (IE uses .cssText insted)
+ style: /red/.test( a.getAttribute("style") ),
+
+ // Make sure that URLs aren't manipulated
+ // (IE normalizes it by default)
+ hrefNormalized: a.getAttribute("href") === "/a",
+
+ // Make sure that element opacity exists
+ // (IE uses filter instead)
+ opacity: a.style.opacity === "0.5",
+
+ // Verify style float existence
+ // (IE uses styleFloat instead of cssFloat)
+ cssFloat: !!a.style.cssFloat,
+
+ // Will be defined later
+ scriptEval: false,
+ noCloneEvent: true,
+ boxModel: null
+ };
+
+ script.type = "text/javascript";
+ try {
+ script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
+ } catch(e){}
+
+ root.insertBefore( script, root.firstChild );
+
+ // Make sure that the execution of code works by injecting a script
+ // tag with appendChild/createTextNode
+ // (IE doesn't support this, fails, and uses .text instead)
+ if ( window[ id ] ) {
+ jQuery.support.scriptEval = true;
+ delete window[ id ];
+ }
+
+ root.removeChild( script );
+
+ if ( div.attachEvent && div.fireEvent ) {
+ div.attachEvent("onclick", function(){
+ // Cloning a node shouldn't copy over any
+ // bound event handlers (IE does this)
+ jQuery.support.noCloneEvent = false;
+ div.detachEvent("onclick", arguments.callee);
+ });
+ div.cloneNode(true).fireEvent("onclick");
+ }
+
+ // Figure out if the W3C box model works as expected
+ // document.body must exist before we can do this
+ jQuery(function(){
+ var div = document.createElement("div");
+ div.style.width = div.style.paddingLeft = "1px";
+
+ document.body.appendChild( div );
+ jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
+ document.body.removeChild( div ).style.display = 'none';
+ });
+})();
+
+var styleFloat = jQuery.support.cssFloat ? "cssFloat" : "styleFloat";
+
+jQuery.props = {
+ "for": "htmlFor",
+ "class": "className",
+ "float": styleFloat,
+ cssFloat: styleFloat,
+ styleFloat: styleFloat,
+ readonly: "readOnly",
+ maxlength: "maxLength",
+ cellspacing: "cellSpacing",
+ rowspan: "rowSpan",
+ tabindex: "tabIndex"
+};
+jQuery.fn.extend({
+ // Keep a copy of the old load
+ _load: jQuery.fn.load,
+
+ load: function( url, params, callback ) {
+ if ( typeof url !== "string" )
+ return this._load( url );
+
+ var off = url.indexOf(" ");
+ if ( off >= 0 ) {
+ var selector = url.slice(off, url.length);
+ url = url.slice(0, off);
+ }
+
+ // Default to a GET request
+ var type = "GET";
+
+ // If the second parameter was provided
+ if ( params )
+ // If it's a function
+ if ( jQuery.isFunction( params ) ) {
+ // We assume that it's the callback
+ callback = params;
+ params = null;
+
+ // Otherwise, build a param string
+ } else if( typeof params === "object" ) {
+ params = jQuery.param( params );
+ type = "POST";
+ }
+
+ var self = this;
+
+ // Request the remote document
+ jQuery.ajax({
+ url: url,
+ type: type,
+ dataType: "html",
+ data: params,
+ complete: function(res, status){
+ // If successful, inject the HTML into all the matched elements
+ if ( status == "success" || status == "notmodified" )
+ // See if a selector was specified
+ self.html( selector ?
+ // Create a dummy div to hold the results
+ jQuery("<div/>")
+ // inject the contents of the document in, removing the scripts
+ // to avoid any 'Permission Denied' errors in IE
+ .append(res.responseText.replace(/<script(.|\s)*?\/script>/g, ""))
+
+ // Locate the specified elements
+ .find(selector) :
+
+ // If not, just inject the full result
+ res.responseText );
+
+ if( callback )
+ self.each( callback, [res.responseText, status, res] );
+ }
+ });
+ return this;
+ },
+
+ serialize: function() {
+ return jQuery.param(this.serializeArray());
+ },
+ serializeArray: function() {
+ return this.map(function(){
+ return this.elements ? jQuery.makeArray(this.elements) : this;
+ })
+ .filter(function(){
+ return this.name && !this.disabled &&
+ (this.checked || /select|textarea/i.test(this.nodeName) ||
+ /text|hidden|password|search/i.test(this.type));
+ })
+ .map(function(i, elem){
+ var val = jQuery(this).val();
+ return val == null ? null :
+ jQuery.isArray(val) ?
+ jQuery.map( val, function(val, i){
+ return {name: elem.name, value: val};
+ }) :
+ {name: elem.name, value: val};
+ }).get();
+ }
+});
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){
+ jQuery.fn[o] = function(f){
+ return this.bind(o, f);
+ };
+});
+
+var jsc = now();
+
+jQuery.extend({
+
+ get: function( url, data, callback, type ) {
+ // shift arguments if data argument was ommited
+ if ( jQuery.isFunction( data ) ) {
+ callback = data;
+ data = null;
+ }
+
+ return jQuery.ajax({
+ type: "GET",
+ url: url,
+ data: data,
+ success: callback,
+ dataType: type
+ });
+ },
+
+ getScript: function( url, callback ) {
+ return jQuery.get(url, null, callback, "script");
+ },
+
+ getJSON: function( url, data, callback ) {
+ return jQuery.get(url, data, callback, "json");
+ },
+
+ post: function( url, data, callback, type ) {
+ if ( jQuery.isFunction( data ) ) {
+ callback = data;
+ data = {};
+ }
+
+ return jQuery.ajax({
+ type: "POST",
+ url: url,
+ data: data,
+ success: callback,
+ dataType: type
+ });
+ },
+
+ ajaxSetup: function( settings ) {
+ jQuery.extend( jQuery.ajaxSettings, settings );
+ },
+
+ ajaxSettings: {
+ url: location.href,
+ global: true,
+ type: "GET",
+ contentType: "application/x-www-form-urlencoded",
+ processData: true,
+ async: true,
+ /*
+ timeout: 0,
+ data: null,
+ username: null,
+ password: null,
+ */
+ // Create the request object; Microsoft failed to properly
+ // implement the XMLHttpRequest in IE7, so we use the ActiveXObject when it is available
+ // This function can be overriden by calling jQuery.ajaxSetup
+ xhr:function(){
+ return window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
+ },
+ accepts: {
+ xml: "application/xml, text/xml",
+ html: "text/html",
+ script: "text/javascript, application/javascript",
+ json: "application/json, text/javascript",
+ text: "text/plain",
+ _default: "*/*"
+ }
+ },
+
+ // Last-Modified header cache for next request
+ lastModified: {},
+
+ ajax: function( s ) {
+ // Extend the settings, but re-extend 's' so that it can be
+ // checked again later (in the test suite, specifically)
+ s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));
+
+ var jsonp, jsre = /=\?(&|$)/g, status, data,
+ type = s.type.toUpperCase();
+
+ // convert data if not already a string
+ if ( s.data && s.processData && typeof s.data !== "string" )
+ s.data = jQuery.param(s.data);
+
+ // Handle JSONP Parameter Callbacks
+ if ( s.dataType == "jsonp" ) {
+ if ( type == "GET" ) {
+ if ( !s.url.match(jsre) )
+ s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?";
+ } else if ( !s.data || !s.data.match(jsre) )
+ s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
+ s.dataType = "json";
+ }
+
+ // Build temporary JSONP function
+ if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
+ jsonp = "jsonp" + jsc++;
+
+ // Replace the =? sequence both in the query string and the data
+ if ( s.data )
+ s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
+ s.url = s.url.replace(jsre, "=" + jsonp + "$1");
+
+ // We need to make sure
+ // that a JSONP style response is executed properly
+ s.dataType = "script";
+
+ // Handle JSONP-style loading
+ window[ jsonp ] = function(tmp){
+ data = tmp;
+ success();
+ complete();
+ // Garbage collect
+ window[ jsonp ] = undefined;
+ try{ delete window[ jsonp ]; } catch(e){}
+ if ( head )
+ head.removeChild( script );
+ };
+ }
+
+ if ( s.dataType == "script" && s.cache == null )
+ s.cache = false;
+
+ if ( s.cache === false && type == "GET" ) {
+ var ts = now();
+ // try replacing _= if it is there
+ var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2");
+ // if nothing was replaced, add timestamp to the end
+ s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : "");
+ }
+
+ // If data is available, append data to url for get requests
+ if ( s.data && type == "GET" ) {
+ s.url += (s.url.match(/\?/) ? "&" : "?") + s.data;
+
+ // IE likes to send both get and post data, prevent this
+ s.data = null;
+ }
+
+ // Watch for a new set of requests
+ if ( s.global && ! jQuery.active++ )
+ jQuery.event.trigger( "ajaxStart" );
+
+ // Matches an absolute URL, and saves the domain
+ var parts = /^(\w+:)?\/\/([^\/?#]+)/.exec( s.url );
+
+ // If we're requesting a remote document
+ // and trying to load JSON or Script with a GET
+ if ( s.dataType == "script" && type == "GET" && parts
+ && ( parts[1] && parts[1] != location.protocol || parts[2] != location.host )){
+
+ var head = document.getElementsByTagName("head")[0];
+ var script = document.createElement("script");
+ script.src = s.url;
+ if (s.scriptCharset)
+ script.charset = s.scriptCharset;
+
+ // Handle Script loading
+ if ( !jsonp ) {
+ var done = false;
+
+ // Attach handlers for all browsers
+ script.onload = script.onreadystatechange = function(){
+ if ( !done && (!this.readyState ||
+ this.readyState == "loaded" || this.readyState == "complete") ) {
+ done = true;
+ success();
+ complete();
+
+ // Handle memory leak in IE
+ script.onload = script.onreadystatechange = null;
+ head.removeChild( script );
+ }
+ };
+ }
+
+ head.appendChild(script);
+
+ // We handle everything using the script element injection
+ return undefined;
+ }
+
+ var requestDone = false;
+
+ // Create the request object
+ var xhr = s.xhr();
+
+ // Open the socket
+ // Passing null username, generates a login popup on Opera (#2865)
+ if( s.username )
+ xhr.open(type, s.url, s.async, s.username, s.password);
+ else
+ xhr.open(type, s.url, s.async);
+
+ // Need an extra try/catch for cross domain requests in Firefox 3
+ try {
+ // Set the correct header, if data is being sent
+ if ( s.data )
+ xhr.setRequestHeader("Content-Type", s.contentType);
+
+ // Set the If-Modified-Since header, if ifModified mode.
+ if ( s.ifModified )
+ xhr.setRequestHeader("If-Modified-Since",
+ jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
+
+ // Set header so the called script knows that it's an XMLHttpRequest
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+
+ // Set the Accepts header for the server, depending on the dataType
+ xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
+ s.accepts[ s.dataType ] + ", */*" :
+ s.accepts._default );
+ } catch(e){}
+
+ // Allow custom headers/mimetypes and early abort
+ if ( s.beforeSend && s.beforeSend(xhr, s) === false ) {
+ // Handle the global AJAX counter
+ if ( s.global && ! --jQuery.active )
+ jQuery.event.trigger( "ajaxStop" );
+ // close opended socket
+ xhr.abort();
+ return false;
+ }
+
+ if ( s.global )
+ jQuery.event.trigger("ajaxSend", [xhr, s]);
+
+ // Wait for a response to come back
+ var onreadystatechange = function(isTimeout){
+ // The request was aborted, clear the interval and decrement jQuery.active
+ if (xhr.readyState == 0) {
+ if (ival) {
+ // clear poll interval
+ clearInterval(ival);
+ ival = null;
+ // Handle the global AJAX counter
+ if ( s.global && ! --jQuery.active )
+ jQuery.event.trigger( "ajaxStop" );
+ }
+ // The transfer is complete and the data is available, or the request timed out
+ } else if ( !requestDone && xhr && (xhr.readyState == 4 || isTimeout == "timeout") ) {
+ requestDone = true;
+
+ // clear poll interval
+ if (ival) {
+ clearInterval(ival);
+ ival = null;
+ }
+
+ status = isTimeout == "timeout" ? "timeout" :
+ !jQuery.httpSuccess( xhr ) ? "error" :
+ s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? "notmodified" :
+ "success";
+
+ if ( status == "success" ) {
+ // Watch for, and catch, XML document parse errors
+ try {
+ // process the data (runs the xml through httpData regardless of callback)
+ data = jQuery.httpData( xhr, s.dataType, s );
+ } catch(e) {
+ status = "parsererror";
+ }
+ }
+
+ // Make sure that the request was successful or notmodified
+ if ( status == "success" ) {
+ // Cache Last-Modified header, if ifModified mode.
+ var modRes;
+ try {
+ modRes = xhr.getResponseHeader("Last-Modified");
+ } catch(e) {} // swallow exception thrown by FF if header is not available
+
+ if ( s.ifModified && modRes )
+ jQuery.lastModified[s.url] = modRes;
+
+ // JSONP handles its own success callback
+ if ( !jsonp )
+ success();
+ } else
+ jQuery.handleError(s, xhr, status);
+
+ // Fire the complete handlers
+ complete();
+
+ if ( isTimeout )
+ xhr.abort();
+
+ // Stop memory leaks
+ if ( s.async )
+ xhr = null;
+ }
+ };
+
+ if ( s.async ) {
+ // don't attach the handler to the request, just poll it instead
+ var ival = setInterval(onreadystatechange, 13);
+
+ // Timeout checker
+ if ( s.timeout > 0 )
+ setTimeout(function(){
+ // Check to see if the request is still happening
+ if ( xhr && !requestDone )
+ onreadystatechange( "timeout" );
+ }, s.timeout);
+ }
+
+ // Send the data
+ try {
+ xhr.send(s.data);
+ } catch(e) {
+ jQuery.handleError(s, xhr, null, e);
+ }
+
+ // firefox 1.5 doesn't fire statechange for sync requests
+ if ( !s.async )
+ onreadystatechange();
+
+ function success(){
+ // If a local callback was specified, fire it and pass it the data
+ if ( s.success )
+ s.success( data, status );
+
+ // Fire the global callback
+ if ( s.global )
+ jQuery.event.trigger( "ajaxSuccess", [xhr, s] );
+ }
+
+ function complete(){
+ // Process result
+ if ( s.complete )
+ s.complete(xhr, status);
+
+ // The request was completed
+ if ( s.global )
+ jQuery.event.trigger( "ajaxComplete", [xhr, s] );
+
+ // Handle the global AJAX counter
+ if ( s.global && ! --jQuery.active )
+ jQuery.event.trigger( "ajaxStop" );
+ }
+
+ // return XMLHttpRequest to allow aborting the request etc.
+ return xhr;
+ },
+
+ handleError: function( s, xhr, status, e ) {
+ // If a local callback was specified, fire it
+ if ( s.error ) s.error( xhr, status, e );
+
+ // Fire the global callback
+ if ( s.global )
+ jQuery.event.trigger( "ajaxError", [xhr, s, e] );
+ },
+
+ // Counter for holding the number of active queries
+ active: 0,
+
+ // Determines if an XMLHttpRequest was successful or not
+ httpSuccess: function( xhr ) {
+ try {
+ // IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450
+ return !xhr.status && location.protocol == "file:" ||
+ ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status == 304 || xhr.status == 1223;
+ } catch(e){}
+ return false;
+ },
+
+ // Determines if an XMLHttpRequest returns NotModified
+ httpNotModified: function( xhr, url ) {
+ try {
+ var xhrRes = xhr.getResponseHeader("Last-Modified");
+
+ // Firefox always returns 200. check Last-Modified date
+ return xhr.status == 304 || xhrRes == jQuery.lastModified[url];
+ } catch(e){}
+ return false;
+ },
+
+ httpData: function( xhr, type, s ) {
+ var ct = xhr.getResponseHeader("content-type"),
+ xml = type == "xml" || !type && ct && ct.indexOf("xml") >= 0,
+ data = xml ? xhr.responseXML : xhr.responseText;
+
+ if ( xml && data.documentElement.tagName == "parsererror" )
+ throw "parsererror";
+
+ // Allow a pre-filtering function to sanitize the response
+ // s != null is checked to keep backwards compatibility
+ if( s && s.dataFilter )
+ data = s.dataFilter( data, type );
+
+ // The filter can actually parse the response
+ if( typeof data === "string" ){
+
+ // If the type is "script", eval it in global context
+ if ( type == "script" )
+ jQuery.globalEval( data );
+
+ // Get the JavaScript object, if JSON is used.
+ if ( type == "json" )
+ data = window["eval"]("(" + data + ")");
+ }
+
+ return data;
+ },
+
+ // Serialize an array of form elements or a set of
+ // key/values into a query string
+ param: function( a ) {
+ var s = [ ];
+
+ function add( key, value ){
+ s[ s.length ] = encodeURIComponent(key) + '=' + encodeURIComponent(value);
+ };
+
+ // If an array was passed in, assume that it is an array
+ // of form elements
+ if ( jQuery.isArray(a) || a.jquery )
+ // Serialize the form elements
+ jQuery.each( a, function(){
+ add( this.name, this.value );
+ });
+
+ // Otherwise, assume that it's an object of key/value pairs
+ else
+ // Serialize the key/values
+ for ( var j in a )
+ // If the value is an array then the key names need to be repeated
+ if ( jQuery.isArray(a[j]) )
+ jQuery.each( a[j], function(){
+ add( j, this );
+ });
+ else
+ add( j, jQuery.isFunction(a[j]) ? a[j]() : a[j] );
+
+ // Return the resulting serialization
+ return s.join("&").replace(/%20/g, "+");
+ }
+
+});
+var elemdisplay = {},
+ timerId,
+ fxAttrs = [
+ // height animations
+ [ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ],
+ // width animations
+ [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ],
+ // opacity animations
+ [ "opacity" ]
+ ];
+
+function genFx( type, num ){
+ var obj = {};
+ jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function(){
+ obj[ this ] = type;
+ });
+ return obj;
+}
+
+jQuery.fn.extend({
+ show: function(speed,callback){
+ if ( speed ) {
+ return this.animate( genFx("show", 3), speed, callback);
+ } else {
+ for ( var i = 0, l = this.length; i < l; i++ ){
+ var old = jQuery.data(this[i], "olddisplay");
+
+ this[i].style.display = old || "";
+
+ if ( jQuery.css(this[i], "display") === "none" ) {
+ var tagName = this[i].tagName, display;
+
+ if ( elemdisplay[ tagName ] ) {
+ display = elemdisplay[ tagName ];
+ } else {
+ var elem = jQuery("<" + tagName + " />").appendTo("body");
+
+ display = elem.css("display");
+ if ( display === "none" )
+ display = "block";
+
+ elem.remove();
+
+ elemdisplay[ tagName ] = display;
+ }
+
+ jQuery.data(this[i], "olddisplay", display);
+ }
+ }
+
+ // Set the display of the elements in a second loop
+ // to avoid the constant reflow
+ for ( var i = 0, l = this.length; i < l; i++ ){
+ this[i].style.display = jQuery.data(this[i], "olddisplay") || "";
+ }
+
+ return this;
+ }
+ },
+
+ hide: function(speed,callback){
+ if ( speed ) {
+ return this.animate( genFx("hide", 3), speed, callback);
+ } else {
+ for ( var i = 0, l = this.length; i < l; i++ ){
+ var old = jQuery.data(this[i], "olddisplay");
+ if ( !old && old !== "none" )
+ jQuery.data(this[i], "olddisplay", jQuery.css(this[i], "display"));
+ }
+
+ // Set the display of the elements in a second loop
+ // to avoid the constant reflow
+ for ( var i = 0, l = this.length; i < l; i++ ){
+ this[i].style.display = "none";
+ }
+
+ return this;
+ }
+ },
+
+ // Save the old toggle function
+ _toggle: jQuery.fn.toggle,
+
+ toggle: function( fn, fn2 ){
+ var bool = typeof fn === "boolean";
+
+ return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ?
+ this._toggle.apply( this, arguments ) :
+ fn == null || bool ?
+ this.each(function(){
+ var state = bool ? fn : jQuery(this).is(":hidden");
+ jQuery(this)[ state ? "show" : "hide" ]();
+ }) :
+ this.animate(genFx("toggle", 3), fn, fn2);
+ },
+
+ fadeTo: function(speed,to,callback){
+ return this.animate({opacity: to}, speed, callback);
+ },
+
+ animate: function( prop, speed, easing, callback ) {
+ var optall = jQuery.speed(speed, easing, callback);
+
+ return this[ optall.queue === false ? "each" : "queue" ](function(){
+
+ var opt = jQuery.extend({}, optall), p,
+ hidden = this.nodeType == 1 && jQuery(this).is(":hidden"),
+ self = this;
+
+ for ( p in prop ) {
+ if ( prop[p] == "hide" && hidden || prop[p] == "show" && !hidden )
+ return opt.complete.call(this);
+
+ if ( ( p == "height" || p == "width" ) && this.style ) {
+ // Store display property
+ opt.display = jQuery.css(this, "display");
+
+ // Make sure that nothing sneaks out
+ opt.overflow = this.style.overflow;
+ }
+ }
+
+ if ( opt.overflow != null )
+ this.style.overflow = "hidden";
+
+ opt.curAnim = jQuery.extend({}, prop);
+
+ jQuery.each( prop, function(name, val){
+ var e = new jQuery.fx( self, opt, name );
+
+ if ( /toggle|show|hide/.test(val) )
+ e[ val == "toggle" ? hidden ? "show" : "hide" : val ]( prop );
+ else {
+ var parts = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),
+ start = e.cur(true) || 0;
+
+ if ( parts ) {
+ var end = parseFloat(parts[2]),
+ unit = parts[3] || "px";
+
+ // We need to compute starting value
+ if ( unit != "px" ) {
+ self.style[ name ] = (end || 1) + unit;
+ start = ((end || 1) / e.cur(true)) * start;
+ self.style[ name ] = start + unit;
+ }
+
+ // If a +=/-= token was provided, we're doing a relative animation
+ if ( parts[1] )
+ end = ((parts[1] == "-=" ? -1 : 1) * end) + start;
+
+ e.custom( start, end, unit );
+ } else
+ e.custom( start, val, "" );
+ }
+ });
+
+ // For JS strict compliance
+ return true;
+ });
+ },
+
+ stop: function(clearQueue, gotoEnd){
+ var timers = jQuery.timers;
+
+ if (clearQueue)
+ this.queue([]);
+
+ this.each(function(){
+ // go in reverse order so anything added to the queue during the loop is ignored
+ for ( var i = timers.length - 1; i >= 0; i-- )
+ if ( timers[i].elem == this ) {
+ if (gotoEnd)
+ // force the next step to be the last
+ timers[i](true);
+ timers.splice(i, 1);
+ }
+ });
+
+ // start the next in the queue if the last step wasn't forced
+ if (!gotoEnd)
+ this.dequeue();
+
+ return this;
+ }
+
+});
+
+// Generate shortcuts for custom animations
+jQuery.each({
+ slideDown: genFx("show", 1),
+ slideUp: genFx("hide", 1),
+ slideToggle: genFx("toggle", 1),
+ fadeIn: { opacity: "show" },
+ fadeOut: { opacity: "hide" }
+}, function( name, props ){
+ jQuery.fn[ name ] = function( speed, callback ){
+ return this.animate( props, speed, callback );
+ };
+});
+
+jQuery.extend({
+
+ speed: function(speed, easing, fn) {
+ var opt = typeof speed === "object" ? speed : {
+ complete: fn || !fn && easing ||
+ jQuery.isFunction( speed ) && speed,
+ duration: speed,
+ easing: fn && easing || easing && !jQuery.isFunction(easing) && easing
+ };
+
+ opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+ jQuery.fx.speeds[opt.duration] || jQuery.fx.speeds._default;
+
+ // Queueing
+ opt.old = opt.complete;
+ opt.complete = function(){
+ if ( opt.queue !== false )
+ jQuery(this).dequeue();
+ if ( jQuery.isFunction( opt.old ) )
+ opt.old.call( this );
+ };
+
+ return opt;
+ },
+
+ easing: {
+ linear: function( p, n, firstNum, diff ) {
+ return firstNum + diff * p;
+ },
+ swing: function( p, n, firstNum, diff ) {
+ return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum;
+ }
+ },
+
+ timers: [],
+
+ fx: function( elem, options, prop ){
+ this.options = options;
+ this.elem = elem;
+ this.prop = prop;
+
+ if ( !options.orig )
+ options.orig = {};
+ }
+
+});
+
+jQuery.fx.prototype = {
+
+ // Simple function for setting a style value
+ update: function(){
+ if ( this.options.step )
+ this.options.step.call( this.elem, this.now, this );
+
+ (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
+
+ // Set display property to block for height/width animations
+ if ( ( this.prop == "height" || this.prop == "width" ) && this.elem.style )
+ this.elem.style.display = "block";
+ },
+
+ // Get the current size
+ cur: function(force){
+ if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) )
+ return this.elem[ this.prop ];
+
+ var r = parseFloat(jQuery.css(this.elem, this.prop, force));
+ return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, this.prop)) || 0;
+ },
+
+ // Start an animation from one number to another
+ custom: function(from, to, unit){
+ this.startTime = now();
+ this.start = from;
+ this.end = to;
+ this.unit = unit || this.unit || "px";
+ this.now = this.start;
+ this.pos = this.state = 0;
+
+ var self = this;
+ function t(gotoEnd){
+ return self.step(gotoEnd);
+ }
+
+ t.elem = this.elem;
+
+ if ( t() && jQuery.timers.push(t) && !timerId ) {
+ timerId = setInterval(function(){
+ var timers = jQuery.timers;
+
+ for ( var i = 0; i < timers.length; i++ )
+ if ( !timers[i]() )
+ timers.splice(i--, 1);
+
+ if ( !timers.length ) {
+ clearInterval( timerId );
+ timerId = undefined;
+ }
+ }, 13);
+ }
+ },
+
+ // Simple 'show' function
+ show: function(){
+ // Remember where we started, so that we can go back to it later
+ this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
+ this.options.show = true;
+
+ // Begin the animation
+ // Make sure that we start at a small width/height to avoid any
+ // flash of content
+ this.custom(this.prop == "width" || this.prop == "height" ? 1 : 0, this.cur());
+
+ // Start by showing the element
+ jQuery(this.elem).show();
+ },
+
+ // Simple 'hide' function
+ hide: function(){
+ // Remember where we started, so that we can go back to it later
+ this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
+ this.options.hide = true;
+
+ // Begin the animation
+ this.custom(this.cur(), 0);
+ },
+
+ // Each step of an animation
+ step: function(gotoEnd){
+ var t = now();
+
+ if ( gotoEnd || t >= this.options.duration + this.startTime ) {
+ this.now = this.end;
+ this.pos = this.state = 1;
+ this.update();
+
+ this.options.curAnim[ this.prop ] = true;
+
+ var done = true;
+ for ( var i in this.options.curAnim )
+ if ( this.options.curAnim[i] !== true )
+ done = false;
+
+ if ( done ) {
+ if ( this.options.display != null ) {
+ // Reset the overflow
+ this.elem.style.overflow = this.options.overflow;
+
+ // Reset the display
+ this.elem.style.display = this.options.display;
+ if ( jQuery.css(this.elem, "display") == "none" )
+ this.elem.style.display = "block";
+ }
+
+ // Hide the element if the "hide" operation was done
+ if ( this.options.hide )
+ jQuery(this.elem).hide();
+
+ // Reset the properties, if the item has been hidden or shown
+ if ( this.options.hide || this.options.show )
+ for ( var p in this.options.curAnim )
+ jQuery.attr(this.elem.style, p, this.options.orig[p]);
+
+ // Execute the complete function
+ this.options.complete.call( this.elem );
+ }
+
+ return false;
+ } else {
+ var n = t - this.startTime;
+ this.state = n / this.options.duration;
+
+ // Perform the easing function, defaults to swing
+ this.pos = jQuery.easing[this.options.easing || (jQuery.easing.swing ? "swing" : "linear")](this.state, n, 0, 1, this.options.duration);
+ this.now = this.start + ((this.end - this.start) * this.pos);
+
+ // Perform the next step of the animation
+ this.update();
+ }
+
+ return true;
+ }
+
+};
+
+jQuery.extend( jQuery.fx, {
+ speeds:{
+ slow: 600,
+ fast: 200,
+ // Default speed
+ _default: 400
+ },
+ step: {
+
+ opacity: function(fx){
+ jQuery.attr(fx.elem.style, "opacity", fx.now);
+ },
+
+ _default: function(fx){
+ if ( fx.elem.style && fx.elem.style[ fx.prop ] != null )
+ fx.elem.style[ fx.prop ] = fx.now + fx.unit;
+ else
+ fx.elem[ fx.prop ] = fx.now;
+ }
+ }
+});
+if ( document.documentElement["getBoundingClientRect"] )
+ jQuery.fn.offset = function() {
+ if ( !this[0] ) return { top: 0, left: 0 };
+ if ( this[0] === this[0].ownerDocument.body ) return jQuery.offset.bodyOffset( this[0] );
+ var box = this[0].getBoundingClientRect(), doc = this[0].ownerDocument, body = doc.body, docElem = doc.documentElement,
+ clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
+ top = box.top + (self.pageYOffset || jQuery.boxModel && docElem.scrollTop || body.scrollTop ) - clientTop,
+ left = box.left + (self.pageXOffset || jQuery.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft;
+ return { top: top, left: left };
+ };
+else
+ jQuery.fn.offset = function() {
+ if ( !this[0] ) return { top: 0, left: 0 };
+ if ( this[0] === this[0].ownerDocument.body ) return jQuery.offset.bodyOffset( this[0] );
+ jQuery.offset.initialized || jQuery.offset.initialize();
+
+ var elem = this[0], offsetParent = elem.offsetParent, prevOffsetParent = elem,
+ doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement,
+ body = doc.body, defaultView = doc.defaultView,
+ prevComputedStyle = defaultView.getComputedStyle(elem, null),
+ top = elem.offsetTop, left = elem.offsetLeft;
+
+ while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {
+ computedStyle = defaultView.getComputedStyle(elem, null);
+ top -= elem.scrollTop, left -= elem.scrollLeft;
+ if ( elem === offsetParent ) {
+ top += elem.offsetTop, left += elem.offsetLeft;
+ if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.tagName)) )
+ top += parseInt( computedStyle.borderTopWidth, 10) || 0,
+ left += parseInt( computedStyle.borderLeftWidth, 10) || 0;
+ prevOffsetParent = offsetParent, offsetParent = elem.offsetParent;
+ }
+ if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" )
+ top += parseInt( computedStyle.borderTopWidth, 10) || 0,
+ left += parseInt( computedStyle.borderLeftWidth, 10) || 0;
+ prevComputedStyle = computedStyle;
+ }
+
+ if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" )
+ top += body.offsetTop,
+ left += body.offsetLeft;
+
+ if ( prevComputedStyle.position === "fixed" )
+ top += Math.max(docElem.scrollTop, body.scrollTop),
+ left += Math.max(docElem.scrollLeft, body.scrollLeft);
+
+ return { top: top, left: left };
+ };
+
+jQuery.offset = {
+ initialize: function() {
+ if ( this.initialized ) return;
+ var body = document.body, container = document.createElement('div'), innerDiv, checkDiv, table, td, rules, prop, bodyMarginTop = body.style.marginTop,
+ html = '<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';
+
+ rules = { position: 'absolute', top: 0, left: 0, margin: 0, border: 0, width: '1px', height: '1px', visibility: 'hidden' };
+ for ( prop in rules ) container.style[prop] = rules[prop];
+
+ container.innerHTML = html;
+ body.insertBefore(container, body.firstChild);
+ innerDiv = container.firstChild, checkDiv = innerDiv.firstChild, td = innerDiv.nextSibling.firstChild.firstChild;
+
+ this.doesNotAddBorder = (checkDiv.offsetTop !== 5);
+ this.doesAddBorderForTableAndCells = (td.offsetTop === 5);
+
+ innerDiv.style.overflow = 'hidden', innerDiv.style.position = 'relative';
+ this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);
+
+ body.style.marginTop = '1px';
+ this.doesNotIncludeMarginInBodyOffset = (body.offsetTop === 0);
+ body.style.marginTop = bodyMarginTop;
+
+ body.removeChild(container);
+ this.initialized = true;
+ },
+
+ bodyOffset: function(body) {
+ jQuery.offset.initialized || jQuery.offset.initialize();
+ var top = body.offsetTop, left = body.offsetLeft;
+ if ( jQuery.offset.doesNotIncludeMarginInBodyOffset )
+ top += parseInt( jQuery.curCSS(body, 'marginTop', true), 10 ) || 0,
+ left += parseInt( jQuery.curCSS(body, 'marginLeft', true), 10 ) || 0;
+ return { top: top, left: left };
+ }
+};
+
+
+jQuery.fn.extend({
+ position: function() {
+ var left = 0, top = 0, results;
+
+ if ( this[0] ) {
+ // Get *real* offsetParent
+ var offsetParent = this.offsetParent(),
+
+ // Get correct offsets
+ offset = this.offset(),
+ parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+ // Subtract element margins
+ // note: when an element has margin: auto the offsetLeft and marginLeft
+ // are the same in Safari causing offset.left to incorrectly be 0
+ offset.top -= num( this, 'marginTop' );
+ offset.left -= num( this, 'marginLeft' );
+
+ // Add offsetParent borders
+ parentOffset.top += num( offsetParent, 'borderTopWidth' );
+ parentOffset.left += num( offsetParent, 'borderLeftWidth' );
+
+ // Subtract the two offsets
+ results = {
+ top: offset.top - parentOffset.top,
+ left: offset.left - parentOffset.left
+ };
+ }
+
+ return results;
+ },
+
+ offsetParent: function() {
+ var offsetParent = this[0].offsetParent || document.body;
+ while ( offsetParent && (!/^body|html$/i.test(offsetParent.tagName) && jQuery.css(offsetParent, 'position') == 'static') )
+ offsetParent = offsetParent.offsetParent;
+ return jQuery(offsetParent);
+ }
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( ['Left', 'Top'], function(i, name) {
+ var method = 'scroll' + name;
+
+ jQuery.fn[ method ] = function(val) {
+ if (!this[0]) return null;
+
+ return val !== undefined ?
+
+ // Set the scroll offset
+ this.each(function() {
+ this == window || this == document ?
+ window.scrollTo(
+ !i ? val : jQuery(window).scrollLeft(),
+ i ? val : jQuery(window).scrollTop()
+ ) :
+ this[ method ] = val;
+ }) :
+
+ // Return the scroll offset
+ this[0] == window || this[0] == document ?
+ self[ i ? 'pageYOffset' : 'pageXOffset' ] ||
+ jQuery.boxModel && document.documentElement[ method ] ||
+ document.body[ method ] :
+ this[0][ method ];
+ };
+});
+// Create innerHeight, innerWidth, outerHeight and outerWidth methods
+jQuery.each([ "Height", "Width" ], function(i, name){
+
+ var tl = i ? "Left" : "Top", // top or left
+ br = i ? "Right" : "Bottom", // bottom or right
+ lower = name.toLowerCase();
+
+ // innerHeight and innerWidth
+ jQuery.fn["inner" + name] = function(){
+ return this[0] ?
+ jQuery.css( this[0], lower, false, "padding" ) :
+ null;
+ };
+
+ // outerHeight and outerWidth
+ jQuery.fn["outer" + name] = function(margin) {
+ return this[0] ?
+ jQuery.css( this[0], lower, false, margin ? "margin" : "border" ) :
+ null;
+ };
+
+ var type = name.toLowerCase();
+
+ jQuery.fn[ type ] = function( size ) {
+ // Get window width or height
+ return this[0] == window ?
+ // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
+ document.compatMode == "CSS1Compat" && document.documentElement[ "client" + name ] ||
+ document.body[ "client" + name ] :
+
+ // Get document width or height
+ this[0] == document ?
+ // Either scroll[Width/Height] or offset[Width/Height], whichever is greater
+ Math.max(
+ document.documentElement["client" + name],
+ document.body["scroll" + name], document.documentElement["scroll" + name],
+ document.body["offset" + name], document.documentElement["offset" + name]
+ ) :
+
+ // Get or set width or height on the element
+ size === undefined ?
+ // Get width or height on the element
+ (this.length ? jQuery.css( this[0], type ) : null) :
+
+ // Set the width or height on the element (default to pixels if value is unitless)
+ this.css( type, typeof size === "string" ? size : size + "px" );
+ };
+
+});
+})();
diff --git a/etherpad/src/static/js/json2.js b/etherpad/src/static/js/json2.js
new file mode 100644
index 0000000..32988c2
--- /dev/null
+++ b/etherpad/src/static/js/json2.js
@@ -0,0 +1,498 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ http://www.JSON.org/json2.js
+ 2008-09-01
+
+ Public Domain.
+
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+ See http://www.JSON.org/js.html
+
+ This file creates a global JSON object containing two methods: stringify
+ and parse.
+
+ JSON.stringify(value, replacer, space)
+ value any JavaScript value, usually an object or array.
+
+ replacer an optional parameter that determines how object
+ values are stringified for objects. It can be a
+ function or an array.
+
+ space an optional parameter that specifies the indentation
+ of nested structures. If it is omitted, the text will
+ be packed without extra whitespace. If it is a number,
+ it will specify the number of spaces to indent at each
+ level. If it is a string (such as '\t' or '&nbsp;'),
+ it contains the characters used to indent at each level.
+
+ This method produces a JSON text from a JavaScript value.
+
+ When an object value is found, if the object contains a toJSON
+ method, its toJSON method will be called and the result will be
+ stringified. A toJSON method does not serialize: it returns the
+ value represented by the name/value pair that should be serialized,
+ or undefined if nothing should be serialized. The toJSON method
+ will be passed the key associated with the value, and this will be
+ bound to the object holding the key.
+
+ For example, this would serialize Dates as ISO strings.
+
+ Date.prototype.toJSON = function (key) {
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+ You can provide an optional replacer method. It will be passed the
+ key and value of each member, with this bound to the containing
+ object. The value that is returned from your method will be
+ serialized. If your method returns undefined, then the member will
+ be excluded from the serialization.
+
+ If the replacer parameter is an array, then it will be used to
+ select the members to be serialized. It filters the results such
+ that only members with keys listed in the replacer array are
+ stringified.
+
+ Values that do not have JSON representations, such as undefined or
+ functions, will not be serialized. Such values in objects will be
+ dropped; in arrays they will be replaced with null. You can use
+ a replacer function to replace those with JSON values.
+ JSON.stringify(undefined) returns undefined.
+
+ The optional space parameter produces a stringification of the
+ value that is filled with line breaks and indentation to make it
+ easier to read.
+
+ If the space parameter is a non-empty string, then that string will
+ be used for indentation. If the space parameter is a number, then
+ the indentation will be that many spaces.
+
+ Example:
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}]);
+ // text is '["e",{"pluribus":"unum"}]'
+
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+ text = JSON.stringify([new Date()], function (key, value) {
+ return this[key] instanceof Date ?
+ 'Date(' + this[key] + ')' : value;
+ });
+ // text is '["Date(---current time---)"]'
+
+
+ JSON.parse(text, reviver)
+ This method parses a JSON text to produce an object or array.
+ It can throw a SyntaxError exception.
+
+ The optional reviver parameter is a function that can filter and
+ transform the results. It receives each of the keys and values,
+ and its return value is used instead of the original value.
+ If it returns what it received, then the structure is not modified.
+ If it returns undefined then the member is deleted.
+
+ Example:
+
+ // Parse the text. Values that look like ISO date strings will
+ // be converted to Date objects.
+
+ myData = JSON.parse(text, function (key, value) {
+ var a;
+ if (typeof value === 'string') {
+ a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+ if (a) {
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+ +a[5], +a[6]));
+ }
+ }
+ return value;
+ });
+
+ myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+ var d;
+ if (typeof value === 'string' &&
+ value.slice(0, 5) === 'Date(' &&
+ value.slice(-1) === ')') {
+ d = new Date(value.slice(5, -1));
+ if (d) {
+ return d;
+ }
+ }
+ return value;
+ });
+
+
+ This is a reference implementation. You are free to copy, modify, or
+ redistribute.
+
+ This code should be minified before deployment.
+ See http://javascript.crockford.com/jsmin.html
+
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+ NOT CONTROL.
+*/
+
+/*jslint evil: true */
+
+/*global JSON */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", call,
+ charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes,
+ getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length,
+ parse, propertyIsEnumerable, prototype, push, replace, slice, stringify,
+ test, toJSON, toString, valueOf
+*/
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+if (!this.JSON) {
+ JSON = {};
+}
+(function () {
+
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ if (typeof Date.prototype.toJSON !== 'function') {
+
+ Date.prototype.toJSON = function (key) {
+
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+ String.prototype.toJSON =
+ Number.prototype.toJSON =
+ Boolean.prototype.toJSON = function (key) {
+ return this.valueOf();
+ };
+ }
+
+ // APPJET: escape all characters except non-control 7-bit ASCII (changed cx and escapeable)
+ var cx = /[\u0000-\u001f\u007f-\uffff]/g,
+ escapeable = /[\\\"\u0000-\u001f\u007f-\uffff]/g,
+ gap,
+ indent,
+ meta = { // table of character substitutions
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ },
+ rep;
+
+
+ function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+ escapeable.lastIndex = 0;
+ return escapeable.test(string) ?
+ '"' + string.replace(escapeable, function (a) {
+ var c = meta[a];
+ if (typeof c === 'string') {
+ return c;
+ }
+ return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ }) + '"' :
+ '"' + string + '"';
+ }
+
+
+ function str(key, holder) {
+
+// Produce a string from holder[key].
+
+ var i, // The loop counter.
+ k, // The member key.
+ v, // The member value.
+ length,
+ mind = gap,
+ partial,
+ value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+ if (value && typeof value === 'object' &&
+ typeof value.toJSON === 'function') {
+ value = value.toJSON(key);
+ }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+ if (typeof rep === 'function') {
+ value = rep.call(holder, key, value);
+ }
+
+// What happens next depends on the value's type.
+
+ switch (typeof value) {
+ case 'string':
+ return quote(value);
+
+ case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+ return isFinite(value) ? String(value) : 'null';
+
+ case 'boolean':
+ case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+ return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+ case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+ if (!value) {
+ return 'null';
+ }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+ gap += indent;
+ partial = [];
+
+// If the object has a dontEnum length property, we'll treat it as an array.
+
+ if (typeof value.length === 'number' &&
+ !value.propertyIsEnumerable('length')) {
+
+// The object is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+ length = value.length;
+ for (i = 0; i < length; i += 1) {
+ partial[i] = str(i, value) || 'null';
+ }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+ v = partial.length === 0 ? '[]' :
+ gap ? '[\n' + gap +
+ partial.join(',\n' + gap) + '\n' +
+ mind + ']' :
+ '[' + partial.join(',') + ']';
+ gap = mind;
+ return v;
+ }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+ if (rep && typeof rep === 'object') {
+ length = rep.length;
+ for (i = 0; i < length; i += 1) {
+ k = rep[i];
+ if (typeof k === 'string') {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+ for (k in value) {
+ if (Object.hasOwnProperty.call(value, k)) {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+ v = partial.length === 0 ? '{}' :
+ gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
+ mind + '}' : '{' + partial.join(',') + '}';
+ gap = mind;
+ return v;
+ }
+ }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+ if (typeof JSON.stringify !== 'function') {
+ JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+ var i;
+ gap = '';
+ indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+ if (typeof space === 'number') {
+ for (i = 0; i < space; i += 1) {
+ indent += ' ';
+ }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+ } else if (typeof space === 'string') {
+ indent = space;
+ }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+ rep = replacer;
+ if (replacer && typeof replacer !== 'function' &&
+ (typeof replacer !== 'object' ||
+ typeof replacer.length !== 'number')) {
+ throw new Error('JSON.stringify');
+ }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+ return str('', {'': value});
+ };
+ }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+ if (typeof JSON.parse !== 'function') {
+ JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+ var j;
+
+ function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+ var k, v, value = holder[key];
+ if (value && typeof value === 'object') {
+ for (k in value) {
+ if (Object.hasOwnProperty.call(value, k)) {
+ v = walk(value, k);
+ if (v !== undefined) {
+ value[k] = v;
+ } else {
+ delete value[k];
+ }
+ }
+ }
+ }
+ return reviver.call(holder, key, value);
+ }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+ cx.lastIndex = 0;
+ if (cx.test(text)) {
+ text = text.replace(cx, function (a) {
+ return '\\u' +
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ });
+ }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+ if (/^[\],:{}\s]*$/.
+test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
+replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
+replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+ j = window['ev'+'al']('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+ return typeof reviver === 'function' ?
+ walk({'': j}, '') : j;
+ }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+ throw new SyntaxError('JSON.parse');
+ };
+ }
+})();
diff --git a/etherpad/src/static/js/lib/jquery.contextmenu.js b/etherpad/src/static/js/lib/jquery.contextmenu.js
new file mode 100644
index 0000000..42cc17e
--- /dev/null
+++ b/etherpad/src/static/js/lib/jquery.contextmenu.js
@@ -0,0 +1,284 @@
+/**
+ * Copyright (c)2005-2009 Matt Kruse (javascripttoolbox.com)
+ *
+ * Dual licensed under the MIT and GPL licenses.
+ * This basically means you can use this code however you want for
+ * free, but don't claim to have written it yourself!
+ * Donations always accepted: http://www.JavascriptToolbox.com/donate/
+ *
+ * Please do not link to the .js files on javascripttoolbox.com from
+ * your site. Copy the files locally to your server instead.
+ *
+ */
+/**
+ * jquery.contextmenu.js
+ * jQuery Plugin for Context Menus
+ * http://www.JavascriptToolbox.com/lib/contextmenu/
+ *
+ * Copyright (c) 2008 Matt Kruse (javascripttoolbox.com)
+ * Dual licensed under the MIT and GPL licenses.
+ *
+ * @version 1.0
+ * @history 1.0 2008-10-20 Initial Release
+ * @todo slideUp doesn't work in IE - because of iframe?
+ * @todo Hide all other menus when contextmenu is shown?
+ * @todo More themes
+ * @todo Nested context menus
+ */
+;(function($){
+ $.contextMenu = {
+ shadow:true,
+ shadowOffset:0,
+ shadowOffsetX:5,
+ shadowOffsetY:5,
+ shadowWidthAdjust:-3,
+ shadowHeightAdjust:-3,
+ shadowOpacity:.2,
+ shadowClass:'context-menu-shadow',
+ shadowColor:'black',
+
+ offsetX:0,
+ offsetY:0,
+ appendTo:'body',
+ direction:'down',
+ constrainToScreen:true,
+
+ showTransition:'show',
+ hideTransition:'hide',
+ showSpeed:'',
+ hideSpeed:'',
+ showCallback:null,
+ hideCallback:null,
+
+ className:'context-menu',
+ itemClassName:'context-menu-item',
+ itemHoverClassName:'context-menu-item-hover',
+ disabledItemClassName:'context-menu-item-disabled',
+ disabledItemHoverClassName:'context-menu-item-disabled-hover',
+ separatorClassName:'context-menu-separator',
+ innerDivClassName:'context-menu-item-inner',
+ themePrefix:'context-menu-theme-',
+ theme:'default',
+
+ separator:'context-menu-separator', // A specific key to identify a separator
+ target:null, // The target of the context click, to be populated when triggered
+ menu:null, // The jQuery object containing the HTML object that is the menu itself
+ shadowObj:null, // Shadow object
+ bgiframe:null, // The iframe object for IE6
+ shown:false, // Currently being shown?
+ useIframe:/*@cc_on @*//*@if (@_win32) true, @else @*/false,/*@end @*/ // This is a better check than looking at userAgent!
+
+ bindTarget: 'contextmenu',
+
+ // Create the menu instance
+ create: function(menu,opts) {
+ var cmenu = $.extend({},this,opts); // Clone all default properties to created object
+
+ // If a selector has been passed in, then use that as the menu
+ if (typeof menu=="string") {
+ cmenu.menu = $(menu);
+ }
+ // If a function has been passed in, call it each time the menu is shown to create the menu
+ else if (typeof menu=="function") {
+ cmenu.menuFunction = menu;
+ }
+ // Otherwise parse the Array passed in
+ else {
+ cmenu.menu = cmenu.createMenu(menu,cmenu);
+ }
+ if (cmenu.menu) {
+ cmenu.menu.css({display:'none'});
+ $(cmenu.appendTo).append(cmenu.menu);
+ }
+
+ // Create the shadow object if shadow is enabled
+ if (cmenu.shadow) {
+ cmenu.createShadow(cmenu); // Extracted to method for extensibility
+ if (cmenu.shadowOffset) { cmenu.shadowOffsetX = cmenu.shadowOffsetY = cmenu.shadowOffset; }
+ }
+
+ // when to hide the menu:
+ // click anywhere in the body:
+ $('body').bind('click',function(){cmenu.hide();});
+ // right-click anywhere in the body:
+ $('body').bind('contextmenu',function(){cmenu.hide();});
+ // escape key:
+ $(document).keyup(function(e) { if (e.keyCode == 27) { cmenu.hide(); } });
+
+ return cmenu;
+ },
+
+ // Create an iframe object to go behind the menu
+ createIframe: function() {
+ return $('<iframe frameborder="0" tabindex="-1" src="javascript:false" style="display:block;position:absolute;z-index:-1;filter:Alpha(Opacity=0);"/>');
+ },
+
+ // Accept an Array representing a menu structure and turn it into HTML
+ createMenu: function(menu,cmenu) {
+ var className = cmenu.className;
+ $.each(cmenu.theme.split(","),function(i,n){className+=' '+cmenu.themePrefix+n});
+ var $t = $('<table cellspacing=0 cellpadding=0></table>').click(function(){cmenu.hide(); return false;}); // We wrap a table around it so width can be flexible
+ var $tr = $('<tr></tr>');
+ var $td = $('<td></td>');
+ var $div = $('<div class="'+className+'"></div>');
+
+ // Each menu item is specified as either:
+ // title:function
+ // or title: { property:value ... }
+ for (var i=0; i<menu.length; i++) {
+ var m = menu[i];
+ if (m==$.contextMenu.separator) {
+ $div.append(cmenu.createSeparator());
+ }
+ else {
+ for (var opt in menu[i]) {
+ $div.append(cmenu.createMenuItem(opt,menu[i][opt])); // Extracted to method for extensibility
+ }
+ }
+ }
+ if ( cmenu.useIframe ) {
+ $td.append(cmenu.createIframe());
+ }
+ $t.append($tr.append($td.append($div)))
+ return $t;
+ },
+
+ // Create an individual menu item
+ createMenuItem: function(label,obj) {
+ var cmenu = this;
+ if (typeof obj=="function") { obj={onclick:obj}; } // If passed a simple function, turn it into a property of an object
+ // Default properties, extended in case properties are passed
+ var o = $.extend({
+ onclick:function() { },
+ className:'',
+ hoverClassName:cmenu.itemHoverClassName,
+ icon:'',
+ disabled:false,
+ title:'',
+ hoverItem:cmenu.hoverItem,
+ hoverItemOut:cmenu.hoverItemOut
+ },obj);
+ // If an icon is specified, hard-code the background-image style. Themes that don't show images should take this into account in their CSS
+ var iconStyle = (o.icon)?'background-image:url('+o.icon+');':'';
+ var $div = $('<div class="'+cmenu.itemClassName+' '+o.className+((o.disabled)?' '+cmenu.disabledItemClassName:'')+'" title="'+o.title+'"></div>')
+ // If the item is disabled, don't do anything when it is clicked
+ .click(function(e){if(cmenu.isItemDisabled(this)){return false;}else{return o.onclick.call(cmenu.target,this,cmenu,e)}})
+ // Change the class of the item when hovered over
+ .hover( function(){ o.hoverItem.call(this,(cmenu.isItemDisabled(this))?cmenu.disabledItemHoverClassName:o.hoverClassName); }
+ ,function(){ o.hoverItemOut.call(this,(cmenu.isItemDisabled(this))?cmenu.disabledItemHoverClassName:o.hoverClassName); }
+ );
+ var $idiv = $('<div class="'+cmenu.innerDivClassName+'" style="'+iconStyle+'">'+label+'</div>');
+ $div.append($idiv);
+ return $div;
+ },
+
+ // Create a separator row
+ createSeparator: function() {
+ return $('<div class="'+this.separatorClassName+'"></div>');
+ },
+
+ // Determine if an individual item is currently disabled. This is called each time the item is hovered or clicked because the disabled status may change at any time
+ isItemDisabled: function(item) { return $(item).is('.'+this.disabledItemClassName); },
+
+ // Functions to fire on hover. Extracted to methods for extensibility
+ hoverItem: function(c) { $(this).addClass(c); },
+ hoverItemOut: function(c) { $(this).removeClass(c); },
+
+ // Create the shadow object
+ createShadow: function(cmenu) {
+ cmenu.shadowObj = $('<div class="'+cmenu.shadowClass+'"></div>').css( {display:'none',position:"absolute", zIndex:9998, opacity:cmenu.shadowOpacity, backgroundColor:cmenu.shadowColor } );
+ $(cmenu.appendTo).append(cmenu.shadowObj);
+ },
+
+ // Display the shadow object, given the position of the menu itself
+ showShadow: function(x,y,e) {
+ var cmenu = this;
+ if (cmenu.shadow) {
+ cmenu.shadowObj.css( {
+ width:(cmenu.menu.width()+cmenu.shadowWidthAdjust)+"px",
+ height:(cmenu.menu.height()+cmenu.shadowHeightAdjust)+"px",
+ top:(y+cmenu.shadowOffsetY)+"px",
+ left:(x+cmenu.shadowOffsetX)+"px"
+ }).addClass(cmenu.shadowClass)[cmenu.showTransition](cmenu.showSpeed);
+ }
+ },
+
+ // A hook to call before the menu is shown, in case special processing needs to be done.
+ // Return false to cancel the default show operation
+ beforeShow: function() { return true; },
+
+ // Show the context menu
+ show: function(t,e) {
+ var cmenu=this, x=e.pageX, y=e.pageY;
+ cmenu.target = t; // Preserve the object that triggered this context menu so menu item click methods can see it
+ if (cmenu.beforeShow()!==false) {
+ // If the menu content is a function, call it to populate the menu each time it is displayed
+ if (cmenu.menuFunction) {
+ if (cmenu.menu) { $(cmenu.menu).remove(); }
+ cmenu.menu = cmenu.createMenu(cmenu.menuFunction(cmenu,t),cmenu);
+ cmenu.menu.css({display:'none'});
+ $(cmenu.appendTo).append(cmenu.menu);
+ }
+ var $c = cmenu.menu;
+ x+=cmenu.offsetX; y+=cmenu.offsetY;
+ var pos = cmenu.getPosition(x,y,cmenu,e); // Extracted to method for extensibility
+ cmenu.showShadow(pos.x,pos.y,e);
+ // Resize the iframe if needed
+ if (cmenu.useIframe) {
+ $c.find('iframe').css({width:$c.width()+cmenu.shadowOffsetX+cmenu.shadowWidthAdjust,height:$c.height()+cmenu.shadowOffsetY+cmenu.shadowHeightAdjust});
+ }
+ $c.css( {top:pos.y+"px", left:pos.x+"px", position:"absolute",zIndex:9999} )[cmenu.showTransition](cmenu.showSpeed,((cmenu.showCallback)?function(){cmenu.showCallback.call(cmenu);}:null));
+ cmenu.shown=true;
+ $(document).one('click',null,function(){cmenu.hide()}); // Handle a single click to the document to hide the menu
+ }
+ },
+
+ // Find the position where the menu should appear, given an x,y of the click event
+ getPosition: function(clickX,clickY,cmenu,e) {
+ var x = clickX+cmenu.offsetX;
+ var y = clickY+cmenu.offsetY
+ var h = $(cmenu.menu).height();
+ var w = $(cmenu.menu).width();
+ var dir = cmenu.direction;
+ if (cmenu.constrainToScreen) {
+ var $w = $(window);
+ var wh = $w.height();
+ var ww = $w.width();
+ if (dir=="down" && (y+h-$w.scrollTop() > wh)) { dir = "up"; }
+ var maxRight = x+w-$w.scrollLeft();
+ if (maxRight > ww) { x -= (maxRight-ww); }
+ }
+ if (dir=="up") { y -= h; }
+ return {'x':x,'y':y};
+ },
+
+ // Hide the menu, of course
+ hide: function() {
+ var cmenu=this;
+ if (cmenu.shown) {
+ if (cmenu.iframe) {
+ $(cmenu.iframe).hide();
+ }
+ if (cmenu.menu) {
+ cmenu.menu[cmenu.hideTransition](cmenu.hideSpeed,null);
+ }
+ if (cmenu.shadow) {
+ cmenu.shadowObj[cmenu.hideTransition](cmenu.hideSpeed);
+ }
+ if (cmenu.hideCallback) {
+ cmenu.hideCallback.call(cmenu);
+ }
+ }
+ cmenu.shown = false;
+ }
+ };
+
+ // This actually adds the .contextMenu() function to the jQuery namespace
+ $.fn.contextMenu = function(menu,options) {
+ var cmenu = $.contextMenu.create(menu,options);
+ return this.each(function(){
+ $(this).bind(cmenu.bindTarget,function(e){cmenu.show(this,e);return false;});
+ });
+ };
+})(jQuery);
+
diff --git a/etherpad/src/static/js/pad2.js b/etherpad/src/static/js/pad2.js
new file mode 100644
index 0000000..14ac762
--- /dev/null
+++ b/etherpad/src/static/js/pad2.js
@@ -0,0 +1,591 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* global $, window */
+
+$(document).ready(function() {
+ pad.init();
+});
+
+$(window).unload(function() {
+ pad.dispose();
+});
+
+var pad = {
+ // don't access these directly from outside this file, except
+ // for debugging
+ collabClient: null,
+ myUserInfo: null,
+ diagnosticInfo: {},
+ initTime: 0,
+ clientTimeOffset: (+new Date()) - clientVars.serverTimestamp,
+ preloadedImages: false,
+ padOptions: {},
+ resizeInited: false,
+
+ // these don't require init; clientVars should all go through here
+ getPadId: function() { return clientVars.padId; },
+ getClientIp: function() { return clientVars.clientIp; },
+ getIsProPad: function() { return clientVars.isProPad; },
+ getColorPalette: function() { return clientVars.colorPalette; },
+ getDisplayUserAgent: function() {
+ return padutils.uaDisplay(clientVars.userAgent);
+ },
+ getIsDebugEnabled: function() { return clientVars.debugEnabled; },
+ getPrivilege: function(name) { return clientVars.accountPrivs[name]; },
+ getUserIsGuest: function() { return clientVars.userIsGuest; },
+ //
+
+ getUserId: function() { return pad.myUserInfo.userId; },
+ getUserName: function() { return pad.myUserInfo.name; },
+ sendClientMessage: function(msg) {
+ pad.collabClient.sendClientMessage(msg);
+ },
+
+ initResize: function() {
+ $(window).bind("resize", pad.resizePage);
+ pad.resizeInited = true;
+ pad.resizePage();
+ // just in case, periodically check size:
+ setInterval(function() { pad.resizePage(); }, 2000);
+ },
+ init: function() {
+ pad.diagnosticInfo.uniqueId = padutils.uniqueId();
+ pad.initTime = +(new Date());
+ pad.padOptions = clientVars.initialOptions;
+
+ if ((! $.browser.msie) &&
+ (! ($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0))) {
+ document.domain = document.domain; // for comet
+ }
+
+ // for IE
+ if ($.browser.msie) {
+ try {
+ doc.execCommand("BackgroundImageCache", false, true);
+ } catch (e) {}
+ }
+
+ // order of inits is important here:
+
+ padcookie.init(clientVars.cookiePrefsToSet);
+
+ $("#widthprefcheck").click(pad.toggleWidthPref);
+ $("#sidebarcheck").click(pad.toggleSidebar);
+
+ pad.myUserInfo = {
+ userId: clientVars.userId,
+ name: clientVars.userName,
+ ip: pad.getClientIp(),
+ colorId: clientVars.userColor,
+ userAgent: pad.getDisplayUserAgent()
+ };
+ if (clientVars.specialKey) {
+ pad.myUserInfo.specialKey = clientVars.specialKey;
+ if (clientVars.specialKeyTranslation) {
+ $("#specialkeyarea").html("mode: "+
+ String(clientVars.specialKeyTranslation).toUpperCase());
+ }
+ }
+ paddocbar.init({isTitleEditable: pad.getIsProPad(),
+ initialTitle:clientVars.initialTitle,
+ initialPassword:clientVars.initialPassword,
+ guestPolicy: pad.padOptions.guestPolicy
+ });
+ padimpexp.init();
+ padsavedrevs.init(clientVars.initialRevisionList);
+
+ padeditor.init(postAceInit, pad.padOptions.view || {});
+ sidebarSplit.init();
+ pad.initResize();
+
+ paduserlist.init(pad.myUserInfo);
+ padchat.init(clientVars.chatHistory, pad.myUserInfo);
+ padconnectionstatus.init();
+ padmodals.init();
+
+ pad.collabClient =
+ getCollabClient(padeditor.ace,
+ clientVars.collab_client_vars,
+ pad.myUserInfo,
+ { colorPalette: pad.getColorPalette() });
+ pad.collabClient.setOnUserJoin(pad.handleUserJoin);
+ pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
+ pad.collabClient.setOnUserLeave(pad.handleUserLeave);
+ pad.collabClient.setOnClientMessage(pad.handleClientMessage);
+ pad.collabClient.setOnServerMessage(pad.handleServerMessage);
+ pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
+ pad.collabClient.setOnInternalAction(pad.handleCollabAction);
+
+ function postAceInit() {
+ padeditbar.init();
+ setTimeout(function() { padeditor.ace.focus(); }, 0);
+ }
+
+ pad.resizePage();
+ },
+ dispose: function() {
+ padeditor.dispose();
+ },
+ resizePage: function() {
+ if (! pad.resizeInited) {
+ return;
+ }
+ // requires padeditor and sidebarSplit
+ var pageHeight = $(window).height();
+ if ($("#djs").length > 0) {
+ pageHeight -= $("#djs").outerHeight();
+ }
+ var MIN_PAGE_HEIGHT = 400;
+ if (pageHeight < MIN_PAGE_HEIGHT) {
+ pageHeight = MIN_PAGE_HEIGHT;
+ }
+ var bottomAreaHeight = 28;
+ padeditor.setBottom(pageHeight - bottomAreaHeight);
+ sidebarSplit.setBottom(pageHeight - bottomAreaHeight);
+ paddocbar.handleResizePage();
+ padmodals.relayoutWithBottom(pageHeight);
+ pad.handleWidthChange();
+ },
+ notifyChangeName: function(newName) {
+ pad.myUserInfo.name = newName;
+ pad.collabClient.updateUserInfo(pad.myUserInfo);
+ padchat.handleUserJoinOrUpdate(pad.myUserInfo);
+ },
+ notifyChangeColor: function(newColorId) {
+ pad.myUserInfo.colorId = newColorId;
+ pad.collabClient.updateUserInfo(pad.myUserInfo);
+ padchat.handleUserJoinOrUpdate(pad.myUserInfo);
+ },
+ notifyChangeTitle: function(newTitle) {
+ pad.collabClient.sendClientMessage({
+ type: 'padtitle',
+ title: newTitle,
+ changedBy: pad.myUserInfo.name || "unnamed"
+ });
+ },
+ notifyChangePassword: function(newPass) {
+ pad.collabClient.sendClientMessage({
+ type: 'padpassword',
+ password: newPass,
+ changedBy: pad.myUserInfo.name || "unnamed"
+ });
+ },
+ changePadOption: function(key, value) {
+ var options = {};
+ options[key] = value;
+ pad.handleOptionsChange(options);
+ pad.collabClient.sendClientMessage({
+ type: 'padoptions',
+ options: options,
+ changedBy: pad.myUserInfo.name || "unnamed"
+ });
+ },
+ changeViewOption: function(key, value) {
+ var options = {view: {}};
+ options.view[key] = value;
+ pad.handleOptionsChange(options);
+ pad.collabClient.sendClientMessage({
+ type: 'padoptions',
+ options: options,
+ changedBy: pad.myUserInfo.name || "unnamed"
+ });
+ },
+ handleOptionsChange: function(opts) {
+ // opts object is a full set of options or just
+ // some options to change
+ if (opts.view) {
+ if (! pad.padOptions.view) {
+ pad.padOptions.view = {};
+ }
+ for(var k in opts.view) {
+ pad.padOptions.view[k] = opts.view[k];
+ }
+ padeditor.setViewOptions(pad.padOptions.view);
+ }
+ if (opts.guestPolicy) {
+ // order important here
+ pad.padOptions.guestPolicy = opts.guestPolicy;
+ paddocbar.setGuestPolicy(opts.guestPolicy);
+ }
+ },
+ getPadOptions: function() {
+ // caller shouldn't mutate the object
+ return pad.padOptions;
+ },
+ isPadPublic: function() {
+ return (! pad.getIsProPad()) || (pad.getPadOptions().guestPolicy == 'allow');
+ },
+ suggestUserName: function(userId, name) {
+ pad.collabClient.sendClientMessage({
+ type: 'suggestUserName',
+ unnamedId: userId,
+ newName: name
+ });
+ },
+ handleUserJoin: function(userInfo) {
+ paduserlist.userJoinOrUpdate(userInfo);
+ padchat.handleUserJoinOrUpdate(userInfo);
+ },
+ handleUserUpdate: function(userInfo) {
+ paduserlist.userJoinOrUpdate(userInfo);
+ padchat.handleUserJoinOrUpdate(userInfo);
+ },
+ handleUserLeave: function(userInfo) {
+ paduserlist.userLeave(userInfo);
+ padchat.handleUserLeave(userInfo);
+ },
+ handleClientMessage: function(msg) {
+ if (msg.type == 'suggestUserName') {
+ if (msg.unnamedId == pad.myUserInfo.userId && msg.newName &&
+ ! pad.myUserInfo.name) {
+ pad.notifyChangeName(msg.newName);
+ paduserlist.setMyUserInfo(pad.myUserInfo);
+ }
+ }
+ else if (msg.type == 'chat') {
+ padchat.receiveChat(msg);
+ }
+ else if (msg.type == 'padtitle') {
+ paddocbar.changeTitle(msg.title);
+ }
+ else if (msg.type == 'padpassword') {
+ paddocbar.changePassword(msg.password);
+ }
+ else if (msg.type == 'newRevisionList') {
+ padsavedrevs.newRevisionList(msg.revisionList);
+ }
+ else if (msg.type == 'revisionLabel') {
+ padsavedrevs.newRevisionList(msg.revisionList);
+ }
+ else if (msg.type == 'padoptions') {
+ var opts = msg.options;
+ pad.handleOptionsChange(opts);
+ }
+ else if (msg.type == 'guestanswer') {
+ // someone answered a prompt, remove it
+ paduserlist.removeGuestPrompt(msg.guestId);
+ }
+ },
+ editbarClick: function(cmd) {
+ if (padeditbar) {
+ padeditbar.toolbarClick(cmd);
+ }
+ },
+ dmesg: function(m) {
+ if (pad.getIsDebugEnabled()) {
+ var djs = $('#djs').get(0);
+ var wasAtBottom = (djs.scrollTop - (djs.scrollHeight - $(djs).height())
+ >= -20);
+ $('#djs').append('<p>'+m+'</p>');
+ if (wasAtBottom) {
+ djs.scrollTop = djs.scrollHeight;
+ }
+ }
+ },
+ handleServerMessage: function(m) {
+ if (m.type == 'NOTICE') {
+ if (m.text) {
+ alertBar.displayMessage(function (abar) {
+ abar.find("#servermsgdate").html(" ("+padutils.simpleDateTime(new Date)+")");
+ abar.find("#servermsgtext").html(m.text);
+ });
+ }
+ if (m.js) {
+ window['ev'+'al'](m.js);
+ }
+ }
+ else if (m.type == 'GUEST_PROMPT') {
+ paduserlist.showGuestPrompt(m.userId, m.displayName);
+ }
+ },
+ handleChannelStateChange: function(newState, message) {
+ var oldFullyConnected = !! padconnectionstatus.isFullyConnected();
+ var wasConnecting = (padconnectionstatus.getStatus().what == 'connecting');
+ if (newState == "CONNECTED") {
+ padconnectionstatus.connected();
+ }
+ else if (newState == "RECONNECTING") {
+ padconnectionstatus.reconnecting();
+ }
+ else if (newState == "DISCONNECTED") {
+ pad.diagnosticInfo.disconnectedMessage = message;
+ pad.diagnosticInfo.padInitTime = pad.initTime;
+ pad.asyncSendDiagnosticInfo();
+ if (typeof window.ajlog == "string") { window.ajlog += ("Disconnected: "+message+'\n'); }
+ padeditor.disable();
+ padeditbar.disable();
+ paddocbar.disable();
+ padimpexp.disable();
+
+ padconnectionstatus.disconnected(message);
+ }
+ var newFullyConnected = !! padconnectionstatus.isFullyConnected();
+ if (newFullyConnected != oldFullyConnected) {
+ pad.handleIsFullyConnected(newFullyConnected, wasConnecting);
+ }
+ },
+ handleIsFullyConnected: function(isConnected, isInitialConnect) {
+ // load all images referenced from CSS, one at a time,
+ // starting one second after connection is first established.
+ if (isConnected && ! pad.preloadedImages) {
+ window.setTimeout(function() {
+ if (! pad.preloadedImages) {
+ pad.preloadImages();
+ pad.preloadedImages = true;
+ }
+ }, 1000);
+ }
+
+ padsavedrevs.handleIsFullyConnected(isConnected);
+
+ pad.determineSidebarVisibility(isConnected && ! isInitialConnect);
+ },
+ determineSidebarVisibility: function(asNowConnectedFeedback) {
+ if (pad.isFullyConnected()) {
+ var setSidebarVisibility =
+ padutils.getCancellableAction(
+ "set-sidebar-visibility",
+ function() {
+ $("body").toggleClass('hidesidebar',
+ !! padcookie.getPref('hideSidebar'));
+ });
+ window.setTimeout(setSidebarVisibility,
+ asNowConnectedFeedback ? 3000 : 0);
+ }
+ else {
+ padutils.cancelActions("set-sidebar-visibility");
+ $("body").removeClass('hidesidebar');
+ }
+ pad.resizePage();
+ },
+ handleCollabAction: function(action) {
+ if (action == "commitPerformed") {
+ padeditbar.setSyncStatus("syncing");
+ }
+ else if (action == "newlyIdle") {
+ padeditbar.setSyncStatus("done");
+ }
+ },
+ hideServerMessage: function() {
+ alertBar.hideMessage();
+ },
+ asyncSendDiagnosticInfo: function() {
+ pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo();
+ window.setTimeout(function() {
+ $.ajax({
+ type: 'post',
+ url: '/ep/pad/connection-diagnostic-info',
+ data: {padId: pad.getPadId(), diagnosticInfo: JSON.stringify(pad.diagnosticInfo)},
+ success: function() {},
+ error: function() {}
+ });
+ }, 0);
+ },
+ forceReconnect: function() {
+ $('form#reconnectform input.padId').val(pad.getPadId());
+ pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo();
+ $('form#reconnectform input.diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo));
+ $('form#reconnectform input.missedChanges').val(JSON.stringify(pad.collabClient.getMissedChanges()));
+ $('form#reconnectform').submit();
+ },
+ toggleWidthPref: function() {
+ var newValue = ! padcookie.getPref('fullWidth');
+ padcookie.setPref('fullWidth', newValue);
+ $("#widthprefcheck").toggleClass('widthprefchecked', !!newValue).toggleClass(
+ 'widthprefunchecked', !newValue);
+ pad.handleWidthChange();
+ },
+ toggleSidebar: function() {
+ var newValue = ! padcookie.getPref('hideSidebar');
+ padcookie.setPref('hideSidebar', newValue);
+ $("#sidebarcheck").toggleClass('sidebarchecked', !newValue).toggleClass(
+ 'sidebarunchecked', !!newValue);
+ pad.determineSidebarVisibility();
+ },
+ handleWidthChange: function() {
+ var isFullWidth = padcookie.getPref('fullWidth');
+ if (isFullWidth) {
+ $("body").addClass('fullwidth').removeClass('limwidth').removeClass(
+ 'squish1width').removeClass('squish2width');
+ }
+ else {
+ $("body").addClass('limwidth').removeClass('fullwidth');
+
+ var pageWidth = $(window).width();
+ $("body").toggleClass('squish1width', (pageWidth < 912 && pageWidth > 812)).toggleClass(
+ 'squish2width', (pageWidth <= 812));
+ }
+ },
+ // this is called from code put into a frame from the server:
+ handleImportExportFrameCall: function(callName, varargs) {
+ padimpexp.handleFrameCall.call(padimpexp, callName,
+ Array.prototype.slice.call(arguments, 1));
+ },
+ callWhenNotCommitting: function(f) {
+ pad.collabClient.callWhenNotCommitting(f);
+ },
+ getCollabRevisionNumber: function() {
+ return pad.collabClient.getCurrentRevisionNumber();
+ },
+ isFullyConnected: function() {
+ return padconnectionstatus.isFullyConnected();
+ },
+ addHistoricalAuthors: function(data) {
+ if (! pad.collabClient) {
+ window.setTimeout(function() { pad.addHistoricalAuthors(data); },
+ 1000);
+ }
+ else {
+ pad.collabClient.addHistoricalAuthors(data);
+ }
+ },
+ preloadImages: function() {
+ var images = [
+ '/static/img/jun09/pad/feedbackbox2.gif',
+ '/static/img/jun09/pad/sharebox4.gif',
+ '/static/img/jun09/pad/sharedistri.gif',
+ '/static/img/jun09/pad/colorpicker.gif',
+ '/static/img/jun09/pad/docbarstates.png',
+ '/static/img/jun09/pad/overlay.png'
+ ];
+ function loadNextImage() {
+ if (images.length == 0) {
+ return;
+ }
+ var img = new Image();
+ img.src = images.shift();
+ if (img.complete) {
+ scheduleLoadNextImage();
+ }
+ else {
+ $(img).bind('error load onreadystatechange', scheduleLoadNextImage);
+ }
+ }
+ function scheduleLoadNextImage() {
+ window.setTimeout(loadNextImage, 0);
+ }
+ scheduleLoadNextImage();
+ }
+};
+
+var sidebarSplit = (function(){
+ var MIN_SIZED_BOX_HEIGHT = 75;
+
+ function relayout(heightDelta) {
+ heightDelta = (heightDelta || 0);
+
+ var sizedBox1 = $("#otherusers");
+ var sizedBox2 = $("#chatlines");
+ var height1 = sizedBox1.height();
+ var height2 = sizedBox2.height();
+ var newTotalHeight = height1 + height2 + heightDelta;
+ var newHeight1 = height1;
+ var newHeight2 = height2;
+
+ if (newTotalHeight >= MIN_SIZED_BOX_HEIGHT*2) {
+ // room for both panes to be at least min height
+ if (newTotalHeight >= self.desiredUsersBoxHeight + MIN_SIZED_BOX_HEIGHT) {
+ // room for users pane to be desiredUsersBoxHeight
+ newHeight1 = self.desiredUsersBoxHeight;
+ newHeight2 = newTotalHeight - newHeight1;
+ }
+ else {
+ newHeight2 = MIN_SIZED_BOX_HEIGHT;
+ newHeight1 = newTotalHeight - newHeight2;
+ }
+ }
+ else {
+ newHeight1 = Math.round(newTotalHeight/2);
+ newHeight2 = newTotalHeight - newHeight1;
+ }
+
+ sizedBox1.height(newHeight1);
+ sizedBox2.height(newHeight2);
+
+ $("#connectionbox").height(
+ $("#myuser").outerHeight() + $("#userlistbuttonarea").outerHeight() +
+ height1
+ );
+ }
+
+ var self = {
+ desiredUsersBoxHeight: MIN_SIZED_BOX_HEIGHT,
+ init: function() {
+ self.desiredUsersBoxHeight = Math.max(
+ $("#otherusers").height(), MIN_SIZED_BOX_HEIGHT);
+ makeDraggable($("#hdraggie"), function(eType, evt, state) {
+ if (eType == 'dragstart') {
+ state.startY = evt.pageY;
+ state.startHeight = $("#otherusers").height();
+ }
+ else if (eType == 'dragupdate') {
+ var newHeight = state.startHeight + (evt.pageY - state.startY);
+ if (newHeight < MIN_SIZED_BOX_HEIGHT) {
+ newHeight = MIN_SIZED_BOX_HEIGHT;
+ }
+ self.desiredUsersBoxHeight = newHeight;
+ relayout();
+ }
+ });
+ },
+ setBottom: function(bottomPx) {
+ var curBottom = $("#padsidebar").offset().top + $("#padsidebar").height();
+ var deltaBottom = bottomPx - curBottom;
+
+ if (deltaBottom != 0) {
+ relayout(deltaBottom);
+ }
+ }
+ };
+ return self;
+}());
+
+var alertBar = (function() {
+
+ var animator = padutils.makeShowHideAnimator(arriveAtAnimationState, false, 25, 400);
+
+ function arriveAtAnimationState(state) {
+ if (state == -1) {
+ $("#alertbar").css('opacity', 0).css('display', 'block');
+ pad.resizePage();
+ }
+ else if (state == 0) {
+ $("#alertbar").css('opacity', 1);
+ }
+ else if (state == 1) {
+ $("#alertbar").css('opacity', 0).css('display', 'none');
+ pad.resizePage();
+ }
+ else if (state < 0) {
+ $("#alertbar").css('opacity', state+1);
+ }
+ else if (state > 0) {
+ $("#alertbar").css('opacity', 1 - state);
+ }
+ }
+
+ var self = {
+ displayMessage: function(setupFunc) {
+ animator.show();
+ setupFunc($("#alertbar"));
+ },
+ hideMessage: function() {
+ animator.hide();
+ }
+ };
+ return self;
+}());
diff --git a/etherpad/src/static/js/pad_chat.js b/etherpad/src/static/js/pad_chat.js
new file mode 100644
index 0000000..35903c2
--- /dev/null
+++ b/etherpad/src/static/js/pad_chat.js
@@ -0,0 +1,295 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+var padchat = (function(){
+
+ var numToAuthorMap = [''];
+ var authorColorArray = [null];
+ var authorToNumMap = {};
+ var chatLinesByDay = []; // {day:'2009-06-17', lines: [...]}
+ var oldestHistoricalLine = 0;
+
+ var loadingMoreHistory = false;
+ var HISTORY_LINES_TO_LOAD_AT_A_TIME = 50;
+
+ function authorToNum(author, dontAddIfAbsent) {
+ if ((typeof authorToNumMap[author]) == "number") {
+ return authorToNumMap[author];
+ }
+ else if (dontAddIfAbsent) {
+ return -1;
+ }
+ else {
+ var n = numToAuthorMap.length;
+ numToAuthorMap.push(author);
+ authorToNumMap[author] = n;
+ return n;
+ }
+ }
+ function getDateNumCSSDayString(dateNum) {
+ var d = new Date(+dateNum);
+ var year = String(d.getFullYear());
+ var month = ("0"+String(d.getMonth()+1)).slice(-2);
+ var day = ("0"+String(d.getDate())).slice(-2);
+ return year+"-"+month+"-"+day;
+ }
+ function getDateNumHumanDayString(dateNum) {
+ var d = new Date(+dateNum);
+ var monthName = (["January", "February", "March",
+ "April", "May", "June", "July", "August", "September",
+ "October", "November", "December"])[d.getMonth()];
+ var dayOfMonth = d.getDate();
+ var year = d.getFullYear();
+ return monthName+" "+dayOfMonth+", "+year;
+ }
+ function ensureChatDay(time) {
+ var day = getDateNumCSSDayString(time);
+ var dayIndex = padutils.binarySearch(chatLinesByDay.length, function(n) {
+ return chatLinesByDay[n].day >= day;
+ });
+ if (dayIndex >= chatLinesByDay.length ||
+ chatLinesByDay[dayIndex].day != day) {
+ // add new day to chat display!
+
+ chatLinesByDay.splice(dayIndex, 0, {day: day, lines: []});
+ var dayHtml = '<div class="chatday" id="chatday'+day+'">'+
+ '<h2 class="dayheader">'+getDateNumHumanDayString(time)+
+ '</h2></div>';
+ var dayDivs = $("#chatlines .chatday");
+ if (dayIndex == dayDivs.length) {
+ $("#chatlines").append(dayHtml);
+ }
+ else {
+ dayDivs.eq(dayIndex).before(dayHtml);
+ }
+ }
+
+ return dayIndex;
+ }
+ function addChatLine(userId, time, name, lineText, addBefore) {
+ var dayIndex = ensureChatDay(time);
+ var dayDiv = $("#chatday"+getDateNumCSSDayString(time));
+ var d = new Date(+time);
+ var hourmin = d.getHours()+":"+("0"+d.getMinutes()).slice(-2);
+ var nameHtml;
+ if (name) {
+ nameHtml = padutils.escapeHtml(name);
+ }
+ else {
+ nameHtml = "<i>unnamed</i>";
+ }
+ var chatlineClass = "chatline";
+ if (userId) {
+ var authorNum = authorToNum(userId);
+ chatlineClass += " chatauthor"+authorNum;
+ }
+ var textHtml = padutils.escapeHtmlWithClickableLinks(lineText, '_blank');
+ var lineNode = $('<div class="'+chatlineClass+'">'+
+ '<span class="chatlinetime">'+hourmin+' </span>'+
+ '<span class="chatlinename">'+nameHtml+': </span>'+
+ '<span class="chatlinetext">'+textHtml+'</span></div>');
+ var linesArray = chatLinesByDay[dayIndex].lines;
+ var lineObj = {userId:userId, time:time, name:name, lineText:lineText};
+ if (addBefore) {
+ dayDiv.find("h2").after(lineNode);
+ linesArray.splice(0, 0, lineObj);
+ }
+ else {
+ dayDiv.append(lineNode);
+ linesArray.push(lineObj);
+ }
+ if (userId) {
+ var color = getAuthorCSSColor(userId);
+ if (color) {
+ lineNode.css('background', color);
+ }
+ }
+
+ return {lineNode:lineNode};
+ }
+ function receiveChatHistoryBlock(block) {
+ for(var a in block.historicalAuthorData) {
+ var data = block.historicalAuthorData[a];
+ var n = authorToNum(a);
+ if (! authorColorArray[n]) {
+ // no data about this author, use historical info
+ authorColorArray[n] = { colorId: data.colorId, faded: true };
+ }
+ }
+
+ oldestHistoricalLine = block.start;
+
+ var lines = block.lines;
+ for(var i=lines.length-1; i>=0; i--) {
+ var line = lines[i];
+ addChatLine(line.userId, line.time, line.name, line.lineText, true);
+ }
+
+ if (oldestHistoricalLine > 0) {
+ $("a#chatloadmore").css('display', 'block');
+ }
+ else {
+ $("a#chatloadmore").css('display', 'none');
+ }
+ }
+ function fadeColor(colorCSS) {
+ var color = colorutils.css2triple(colorCSS);
+ color = colorutils.blend(color, [1,1,1], 0.5);
+ return colorutils.triple2css(color);
+ }
+ function getAuthorCSSColor(author) {
+ var n = authorToNum(author, true);
+ if (n < 0) {
+ return '';
+ }
+ else {
+ var cdata = authorColorArray[n];
+ if (! cdata) {
+ return '';
+ }
+ else {
+ var c = pad.getColorPalette()[cdata.colorId];
+ if (cdata.faded) {
+ c = fadeColor(c);
+ }
+ return c;
+ }
+ }
+ }
+ function changeAuthorColorData(author, cdata) {
+ var n = authorToNum(author);
+ authorColorArray[n] = cdata;
+ var cssColor = getAuthorCSSColor(author);
+ if (cssColor) {
+ $("#chatlines .chatauthor"+n).css('background',cssColor);
+ }
+ }
+
+ function sendChat() {
+ var lineText = $("#chatentrybox").val();
+ if (lineText) {
+ $("#chatentrybox").val('').focus();
+ var msg = {
+ type: 'chat',
+ userId: pad.getUserId(),
+ lineText: lineText,
+ senderName: pad.getUserName(),
+ authId: pad.getUserId()
+ };
+ pad.sendClientMessage(msg);
+ self.receiveChat(msg);
+ self.scrollToBottom();
+ }
+ }
+
+ var self = {
+ init: function(chatHistoryBlock, initialUserInfo) {
+ ensureChatDay(+new Date); // so that current date shows up right away
+
+ $("a#chatloadmore").click(self.loadMoreHistory);
+
+ self.handleUserJoinOrUpdate(initialUserInfo);
+ receiveChatHistoryBlock(chatHistoryBlock);
+
+ padutils.bindEnterAndEscape($("#chatentrybox"), function(evt) {
+ // return/enter
+ sendChat();
+ }, null);
+
+ self.scrollToBottom();
+ },
+ receiveChat: function(msg) {
+ var box = $("#chatlines").get(0);
+ var wasAtBottom = (box.scrollTop -
+ (box.scrollHeight - $(box).height()) >= -5);
+ addChatLine(msg.userId, +new Date, msg.senderName, msg.lineText, false);
+ if (wasAtBottom) {
+ window.setTimeout(function() {
+ self.scrollToBottom();
+ }, 0);
+ }
+ },
+ handleUserJoinOrUpdate: function(userInfo) {
+ changeAuthorColorData(userInfo.userId,
+ { colorId: userInfo.colorId, faded: false });
+ },
+ handleUserLeave: function(userInfo) {
+ changeAuthorColorData(userInfo.userId,
+ { colorId: userInfo.colorId, faded: true });
+ },
+ scrollToBottom: function() {
+ var box = $("#chatlines").get(0);
+ box.scrollTop = box.scrollHeight;
+ },
+ scrollToTop: function() {
+ var box = $("#chatlines").get(0);
+ box.scrollTop = 0;
+ },
+ loadMoreHistory: function() {
+ if (loadingMoreHistory) {
+ return;
+ }
+
+ var end = oldestHistoricalLine;
+ var start = Math.max(0, end - HISTORY_LINES_TO_LOAD_AT_A_TIME);
+ var padId = pad.getPadId();
+
+ loadingMoreHistory = true;
+ $("#padchat #chatloadmore").css('display', 'none');
+ $("#padchat #chatloadingmore").css('display', 'block');
+
+ $.ajax({
+ type: 'get',
+ url: '/ep/pad/chathistory',
+ data: { padId: padId, start: start, end: end },
+ success: success,
+ error: error
+ });
+
+ function success(text) {
+ notLoading();
+
+ var result = JSON.parse(text);
+
+ // try to keep scrolled to the same place...
+ var scrollBox = $("#chatlines").get(0);
+ var scrollDeterminer = function() { return 0; };
+ var topLine = $("#chatlines .chatday:first .chatline:first").children().eq(0);
+ if (topLine.length > 0) {
+ var posTop = topLine.position().top;
+ var scrollTop = scrollBox.scrollTop;
+ scrollDeterminer = function() {
+ var newPosTop = topLine.position().top;
+ return newPosTop + (scrollTop - posTop);
+ };
+ }
+ receiveChatHistoryBlock(result);
+
+ scrollBox.scrollTop = Math.max(0, Math.min(scrollBox.scrollHeight, scrollDeterminer()));
+ }
+ function error() {
+ notLoading();
+ }
+ function notLoading() {
+ loadingMoreHistory = false;
+ $("#padchat #chatloadmore").css('display', 'block');
+ $("#padchat #chatloadingmore").css('display', 'none');
+ }
+ }
+ };
+ return self;
+}()); \ No newline at end of file
diff --git a/etherpad/src/static/js/pad_connectionstatus.js b/etherpad/src/static/js/pad_connectionstatus.js
new file mode 100644
index 0000000..cc06728
--- /dev/null
+++ b/etherpad/src/static/js/pad_connectionstatus.js
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var padconnectionstatus = (function() {
+
+ var status = {what: 'connecting'};
+
+ var showHideAnimator = padutils.makeShowHideAnimator(function(state) {
+ $("#connectionbox").css('opacity', 1 - Math.abs(state));
+ if (state == -1) {
+ $("#connectionbox").css('display', 'block');
+ }
+ else if (state == 1) {
+ $("#connectionbox").css('display', 'none');
+ }
+ }, true, 25, 200);
+
+ var self = {
+ init: function() {
+ $('button#forcereconnect').click(function() {
+ pad.forceReconnect();
+ });
+ },
+ connected: function() {
+ status = {what: 'connected'};
+ showHideAnimator.hide();
+ },
+ reconnecting: function() {
+ status = {what: 'reconnecting'};
+ $("#connectionbox").get(0).className = 'cboxreconnecting';
+ showHideAnimator.show();
+ },
+ disconnected: function(msg) {
+ status = {what: 'disconnected', why: msg};
+ var k = String(msg).toLowerCase(); // known reason why
+ if (!(k == 'userdup' || k == 'looping' || k == 'slowcommit' ||
+ k == 'initsocketfail' || k == 'unauth')) {
+ k = 'unknown';
+ }
+ var cls = 'cboxdisconnected cboxdisconnected_'+k;
+ $("#connectionbox").get(0).className = cls;
+ showHideAnimator.show();
+ },
+ isFullyConnected: function() {
+ return status.what == 'connected';
+ },
+ getStatus: function() { return status; }
+ };
+ return self;
+}()); \ No newline at end of file
diff --git a/etherpad/src/static/js/pad_cookie.js b/etherpad/src/static/js/pad_cookie.js
new file mode 100644
index 0000000..3cc31ed
--- /dev/null
+++ b/etherpad/src/static/js/pad_cookie.js
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+var padcookie = (function(){
+ function getRawCookie() {
+ // returns null if can't get cookie text
+ if (! document.cookie) {
+ return null;
+ }
+ // look for (start of string OR semicolon) followed by whitespace followed by prefs=(something);
+ var regexResult = document.cookie.match(/(?:^|;)\s*prefs=([^;]*)(?:;|$)/);
+ if ((! regexResult) || (! regexResult[1])) {
+ return null;
+ }
+ return regexResult[1];
+ }
+ function setRawCookie(safeText) {
+ var expiresDate = new Date();
+ expiresDate.setFullYear(3000);
+ document.cookie = ('prefs='+safeText+';expires='+expiresDate.toGMTString());
+ }
+ function parseCookie(text) {
+ // returns null if can't parse cookie.
+
+ try {
+ var cookieData = JSON.parse(unescape(text));
+ return cookieData;
+ }
+ catch (e) {
+ return null;
+ }
+ }
+ function stringifyCookie(data) {
+ return escape(JSON.stringify(data));
+ }
+ function saveCookie() {
+ if (! inited) {
+ return;
+ }
+ setRawCookie(stringifyCookie(cookieData));
+
+ if (pad.getIsProPad() && (! getRawCookie()) && (! alreadyWarnedAboutNoCookies)) {
+ alert("Warning: it appears that your browser does not have cookies enabled."+
+ " EtherPad uses cookies to keep track of unique users for the purpose"+
+ " of putting a quota on the number of active users. Using EtherPad without "+
+ " cookies may fill up your server's user quota faster than expected.");
+ alreadyWarnedAboutNoCookies = true;
+ }
+ }
+
+ var wasNoCookie = true;
+ var cookieData = {};
+ var alreadyWarnedAboutNoCookies = false;
+ var inited = false;
+
+ var self = {
+ init: function(prefsToSet) {
+ var rawCookie = getRawCookie();
+ if (rawCookie) {
+ var cookieObj = parseCookie(rawCookie);
+ if (cookieObj) {
+ wasNoCookie = false; // there was a cookie
+ delete cookieObj.userId;
+ delete cookieObj.name;
+ delete cookieObj.colorId;
+ cookieData = cookieObj;
+ }
+ }
+
+ for(var k in prefsToSet) {
+ cookieData[k] = prefsToSet[k];
+ }
+
+ inited = true;
+ saveCookie();
+ },
+ wasNoCookie: function() { return wasNoCookie; },
+ getPref: function(prefName) {
+ return cookieData[prefName];
+ },
+ setPref: function(prefName, value) {
+ cookieData[prefName] = value;
+ saveCookie();
+ }
+ };
+ return self;
+}()); \ No newline at end of file
diff --git a/etherpad/src/static/js/pad_docbar.js b/etherpad/src/static/js/pad_docbar.js
new file mode 100644
index 0000000..586b20f
--- /dev/null
+++ b/etherpad/src/static/js/pad_docbar.js
@@ -0,0 +1,347 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+var paddocbar = (function() {
+ var isTitleEditable = false;
+ var isEditingTitle = false;
+ var isEditingPassword = false;
+ var enabled = false;
+
+ function getPanelOpenCloseAnimator(panelName, panelHeight) {
+ var wrapper = $("#"+panelName+"-wrapper");
+ var openingClass = "docbar"+panelName+"-opening";
+ var openClass = "docbar"+panelName+"-open";
+ var closingClass = "docbar"+panelName+"-closing";
+ function setPanelState(action) {
+ $("#docbar").removeClass(openingClass).removeClass(openClass).
+ removeClass(closingClass);
+ if (action != "closed") {
+ $("#docbar").addClass("docbar"+panelName+"-"+action);
+ }
+ }
+
+ function openCloseAnimate(state) {
+ function pow(x) { x = 1-x; x *= x*x; return 1-x; }
+
+ if (state == -1) {
+ // startng to open
+ setPanelState("opening");
+ wrapper.css('height', '0');
+ }
+ else if (state < 0) {
+ // opening
+ var height = Math.round(pow(state+1)*(panelHeight-1))+'px';
+ wrapper.css('height', height);
+ }
+ else if (state == 0) {
+ // open
+ setPanelState("open");
+ wrapper.css('height', panelHeight-1);
+ }
+ else if (state < 1) {
+ // closing
+ setPanelState("closing");
+ var height = Math.round((1-pow(state))*(panelHeight-1))+'px';
+ wrapper.css('height', height);
+ }
+ else if (state == 1) {
+ // closed
+ setPanelState("closed");
+ wrapper.css('height', '0');
+ }
+ }
+
+ return padutils.makeShowHideAnimator(openCloseAnimate, false, 25, 500);
+ }
+
+
+ var currentPanel = null;
+ function setCurrentPanel(newCurrentPanel) {
+ if (currentPanel != newCurrentPanel) {
+ currentPanel = newCurrentPanel;
+ padutils.cancelActions("hide-docbar-panel");
+ }
+ }
+ var panels;
+
+ function changePassword(newPass) {
+ if ((newPass || null) != (self.password || null)) {
+ self.password = (newPass || null);
+ pad.notifyChangePassword(newPass);
+ }
+ self.renderPassword();
+ }
+
+ var self = {
+ title: null,
+ password: null,
+ init: function(opts) {
+ panels = {
+ impexp: { animator: getPanelOpenCloseAnimator("impexp", 160) },
+ savedrevs: { animator: getPanelOpenCloseAnimator("savedrevs", 79) },
+ options: { animator: getPanelOpenCloseAnimator(
+ "options", 114) },
+ security: { animator: getPanelOpenCloseAnimator("security", 130) }
+ };
+
+ isTitleEditable = opts.isTitleEditable;
+ self.title = opts.initialTitle;
+ self.password = opts.initialPassword;
+
+ $("#docbarimpexp").click(function() {self.togglePanel("impexp");});
+ $("#docbarsavedrevs").click(function() {self.togglePanel("savedrevs");});
+ $("#docbaroptions").click(function() {self.togglePanel("options");});
+ $("#docbarsecurity").click(function() {self.togglePanel("security");});
+
+ $("#docbarrenamelink").click(self.editTitle);
+ $("#padtitlesave").click(function() { self.closeTitleEdit(true); });
+ $("#padtitlecancel").click(function() { self.closeTitleEdit(false); });
+ padutils.bindEnterAndEscape($("#padtitleedit"),
+ function() {
+ $("#padtitlesave").trigger('click'); },
+ function() {
+ $("#padtitlecancel").trigger('click'); });
+
+ $("#options-close").click(function() {self.setShownPanel(null);});
+ $("#security-close").click(function() {self.setShownPanel(null);});
+
+ if (pad.getIsProPad()) {
+ self.initPassword();
+ }
+
+ enabled = true;
+ self.render();
+
+ // public/private
+ $("#security-access input").bind("change click", function(evt) {
+ pad.changePadOption('guestPolicy',
+ $("#security-access input[name='padaccess']:checked").val());
+ });
+ self.setGuestPolicy(opts.guestPolicy);
+ },
+ setGuestPolicy: function(newPolicy) {
+ $("#security-access input[value='"+newPolicy+"']").attr("checked",
+ "checked");
+ self.render();
+ },
+ initPassword: function() {
+ self.renderPassword();
+ $("#password-clearlink").click(function() {
+ changePassword(null);
+ });
+ $("#password-setlink, #password-display").click(function() {
+ self.enterPassword();
+ });
+ $("#password-cancellink").click(function() {
+ self.exitPassword(false);
+ });
+ $("#password-savelink").click(function() {
+ self.exitPassword(true);
+ });
+ padutils.bindEnterAndEscape($("#security-passwordedit"),
+ function() {
+ self.exitPassword(true);
+ },
+ function() {
+ self.exitPassword(false);
+ });
+ },
+ enterPassword: function() {
+ isEditingPassword = true;
+ $("#security-passwordedit").val(self.password || '');
+ self.renderPassword();
+ $("#security-passwordedit").focus().select();
+ },
+ exitPassword: function(accept) {
+ isEditingPassword = false;
+ if (accept) {
+ changePassword($("#security-passwordedit").val());
+ }
+ else {
+ self.renderPassword();
+ }
+ },
+ renderPassword: function() {
+ if (isEditingPassword) {
+ $("#password-nonedit").hide();
+ $("#password-inedit").show();
+ }
+ else {
+ $("#password-nonedit").toggleClass('nopassword', ! self.password);
+ $("#password-setlink").html(self.password ? "Change..." : "Set...");
+ if (self.password) {
+ $("#password-display").html(self.password.replace(/./g, '&#8226;'));
+ }
+ else {
+ $("#password-display").html("None");
+ }
+ $("#password-inedit").hide();
+ $("#password-nonedit").show();
+ }
+ },
+ togglePanel: function(panelName) {
+ if (panelName in panels) {
+ if (currentPanel == panelName) {
+ self.setShownPanel(null);
+ }
+ else {
+ self.setShownPanel(panelName);
+ }
+ }
+ },
+ setShownPanel: function(panelName) {
+ function animateHidePanel(panelName, next) {
+ var delay = 0;
+ if (panelName == 'options' && isEditingPassword) {
+ // give user feedback that the password they've
+ // typed in won't actually take effect
+ self.exitPassword(false);
+ delay = 500;
+ }
+
+ window.setTimeout(function() {
+ panels[panelName].animator.hide();
+ if (next) {
+ next();
+ }
+ }, delay);
+ }
+
+ if (! panelName) {
+ if (currentPanel) {
+ animateHidePanel(currentPanel);
+ setCurrentPanel(null);
+ }
+ }
+ else if (panelName in panels) {
+ if (currentPanel != panelName) {
+ if (currentPanel) {
+ animateHidePanel(currentPanel, function() {
+ panels[panelName].animator.show();
+ setCurrentPanel(panelName);
+ });
+ }
+ else {
+ panels[panelName].animator.show();
+ setCurrentPanel(panelName);
+ }
+ }
+ }
+ },
+ isPanelShown: function(panelName) {
+ if (! panelName) {
+ return ! currentPanel;
+ }
+ else {
+ return (panelName == currentPanel);
+ }
+ },
+ changeTitle: function(newTitle) {
+ self.title = newTitle;
+ self.render();
+ },
+ editTitle: function() {
+ if (! enabled) {
+ return;
+ }
+ $("#padtitleedit").val(self.title);
+ isEditingTitle = true;
+ self.render();
+ $("#padtitleedit").focus().select();
+ },
+ closeTitleEdit: function(accept) {
+ if (! enabled) {
+ return;
+ }
+ if (accept) {
+ var newTitle = $("#padtitleedit").val();
+ if (newTitle) {
+ newTitle = newTitle.substring(0, 80);
+ self.title = newTitle;
+
+ pad.notifyChangeTitle(newTitle);
+ }
+ }
+
+ isEditingTitle = false;
+ self.render();
+ },
+ changePassword: function(newPass) {
+ if (newPass) {
+ self.password = newPass;
+ }
+ else {
+ self.password = null;
+ }
+ self.renderPassword();
+ },
+ render: function() {
+ if (isEditingTitle) {
+ $("#docbarpadtitle").hide();
+ $("#docbarrenamelink").hide();
+ $("#padtitleedit").show();
+ $("#padtitlebuttons").show();
+ if (! enabled) {
+ $("#padtitleedit").attr('disabled', 'disabled');
+ }
+ else {
+ $("#padtitleedit").removeAttr('disabled');
+ }
+ }
+ else {
+ $("#padtitleedit").hide();
+ $("#padtitlebuttons").hide();
+
+ var titleSpan = $("#docbarpadtitle span");
+ titleSpan.html(padutils.escapeHtml(self.title));
+ $("#docbarpadtitle").attr('title',
+ (pad.isPadPublic() ? "Public Pad: " : "")+
+ self.title);
+ $("#docbarpadtitle").show();
+
+ if (isTitleEditable) {
+ var titleRight = $("#docbarpadtitle").position().left +
+ $("#docbarpadtitle span").position().left +
+ Math.min($("#docbarpadtitle").width(),
+ $("#docbarpadtitle span").width());
+ $("#docbarrenamelink").css('left', titleRight + 10).show();
+ }
+
+ if (pad.isPadPublic()) {
+ $("#docbar").addClass("docbar-public");
+ }
+ else {
+ $("#docbar").removeClass("docbar-public");
+ }
+ }
+ },
+ disable: function() {
+ enabled = false;
+ self.render();
+ },
+ handleResizePage: function() {
+ padsavedrevs.handleResizePage();
+ },
+ hideLaterIfNoOtherInteraction: function() {
+ return padutils.getCancellableAction('hide-docbar-panel',
+ function() {
+ self.setShownPanel(null);
+ });
+ }
+ };
+ return self;
+}());
diff --git a/etherpad/src/static/js/pad_editbar.js b/etherpad/src/static/js/pad_editbar.js
new file mode 100644
index 0000000..34b774a
--- /dev/null
+++ b/etherpad/src/static/js/pad_editbar.js
@@ -0,0 +1,107 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+var padeditbar = (function(){
+
+ var syncAnimation = (function() {
+ var SYNCING = -100;
+ var DONE = 100;
+ var state = DONE;
+ var fps = 25;
+ var step = 1/fps;
+ var T_START = -0.5;
+ var T_FADE = 1.0;
+ var T_GONE = 1.5;
+ var animator = padutils.makeAnimationScheduler(function() {
+ if (state == SYNCING || state == DONE) {
+ return false;
+ }
+ else if (state >= T_GONE) {
+ state = DONE;
+ $("#syncstatussyncing").css('display', 'none');
+ $("#syncstatusdone").css('display', 'none');
+ return false;
+ }
+ else if (state < 0) {
+ state += step;
+ if (state >= 0) {
+ $("#syncstatussyncing").css('display', 'none');
+ $("#syncstatusdone").css('display', 'block').css('opacity', 1);
+ }
+ return true;
+ }
+ else {
+ state += step;
+ if (state >= T_FADE) {
+ $("#syncstatusdone").css('opacity', (T_GONE - state) / (T_GONE - T_FADE));
+ }
+ return true;
+ }
+ }, step*1000);
+ return {
+ syncing: function() {
+ state = SYNCING;
+ $("#syncstatussyncing").css('display', 'block');
+ $("#syncstatusdone").css('display', 'none');
+ },
+ done: function() {
+ state = T_START;
+ animator.scheduleAnimation();
+ }
+ };
+ }());
+
+ var self = {
+ init: function() {
+ $("#editbar .editbarbutton").attr("unselectable", "on"); // for IE
+ $("#editbar").removeClass("disabledtoolbar").addClass("enabledtoolbar");
+ },
+ isEnabled: function() {
+ return ! $("#editbar").hasClass('disabledtoolbar');
+ },
+ disable: function() {
+ $("#editbar").addClass('disabledtoolbar').removeClass("enabledtoolbar");
+ },
+ toolbarClick: function(cmd) {
+ if (self.isEnabled()) {
+ if (cmd == 'save') {
+ padsavedrevs.saveNow();
+ }
+ else if (cmd == 'clearauthorship') {
+ padeditor.ace.execCommand('clearauthorship', function() {
+ if (window.confirm("Clear authorship colors on entire document?")) {
+ padeditor.ace.execCommand('clearauthorship');
+ }
+ });
+ }
+ else {
+ padeditor.ace.execCommand(cmd);
+ }
+ }
+ padeditor.ace.focus();
+ },
+ setSyncStatus: function(status) {
+ if (status == "syncing") {
+ syncAnimation.syncing();
+ }
+ else if (status == "done") {
+ syncAnimation.done();
+ }
+ }
+ };
+ return self;
+}()); \ No newline at end of file
diff --git a/etherpad/src/static/js/pad_editor.js b/etherpad/src/static/js/pad_editor.js
new file mode 100644
index 0000000..f2fab26
--- /dev/null
+++ b/etherpad/src/static/js/pad_editor.js
@@ -0,0 +1,136 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+var padeditor = (function(){
+ var self = {
+ ace: null, // this is accessed directly from other files
+ viewZoom: 100,
+ init: function(readyFunc, initialViewOptions) {
+
+ function aceReady() {
+ $("#editorloadingbox").hide();
+ if (readyFunc) {
+ readyFunc();
+ }
+ }
+
+ self.ace = new Ace2Editor();
+ self.ace.init("editorcontainer", "", aceReady);
+ self.ace.setProperty("wraps", true);
+ if (pad.getIsDebugEnabled()) {
+ self.ace.setProperty("dmesg", pad.dmesg);
+ }
+ self.initViewOptions();
+ self.setViewOptions(initialViewOptions);
+
+ // view bar
+ self.initViewZoom();
+ $("#viewbarcontents").show();
+ },
+ initViewOptions: function() {
+ padutils.bindCheckboxChange($("#options-linenoscheck"), function() {
+ pad.changeViewOption('showLineNumbers',
+ padutils.getCheckbox($("#options-linenoscheck")));
+ });
+ padutils.bindCheckboxChange($("#options-colorscheck"), function() {
+ pad.changeViewOption('showAuthorColors',
+ padutils.getCheckbox("#options-colorscheck"));
+ });
+ $("#viewfontmenu").change(function() {
+ pad.changeViewOption('useMonospaceFont',
+ $("#viewfontmenu").val() == 'monospace');
+ });
+ },
+ setViewOptions: function(newOptions) {
+ function getOption(key, defaultValue) {
+ var value = String(newOptions[key]);
+ if (value == "true") return true;
+ if (value == "false") return false;
+ return defaultValue;
+ }
+ var v;
+
+ v = getOption('showLineNumbers', true);
+ self.ace.setProperty("showslinenumbers", v);
+ padutils.setCheckbox($("#options-linenoscheck"), v);
+
+ v = getOption('showAuthorColors', true);
+ self.ace.setProperty("showsauthorcolors", v);
+ padutils.setCheckbox($("#options-colorscheck"), v);
+
+ v = getOption('useMonospaceFont', false);
+ self.ace.setProperty("textface",
+ (v ? "monospace" : "Arial, sans-serif"));
+ $("#viewfontmenu").val(v ? "monospace" : "normal");
+ },
+ initViewZoom: function() {
+ var viewZoom = Number(padcookie.getPref('viewZoom'));
+ if ((! viewZoom) || isNaN(viewZoom)) {
+ viewZoom = 100;
+ }
+ self.setViewZoom(viewZoom);
+ $("#viewzoommenu").change(function(evt) {
+ // strip initial 'z' from val
+ self.setViewZoom(Number($("#viewzoommenu").val().substring(1)));
+ });
+ },
+ setViewZoom: function(percent) {
+ if (! (percent >= 50 && percent <= 1000)) {
+ // percent is out of sane range or NaN (which fails comparisons)
+ return;
+ }
+
+ self.viewZoom = percent;
+ $("#viewzoommenu").val('z'+percent);
+
+ var baseSize = 13;
+ self.ace.setProperty('textsize',
+ Math.round(baseSize * self.viewZoom / 100));
+
+ padcookie.setPref('viewZoom', percent);
+ },
+ dispose: function() {
+ if (self.ace) {
+ self.ace.destroy();
+ }
+ },
+ setBottom: function(bottomPx) {
+ var myTop = $("#padeditor").offset().top;
+ var myHeight = $("#padeditor").height();
+ var myBottom = myTop + myHeight;
+ var sizedBoxHeight = $("#editorcontainerbox").height();
+
+ var deltaBottom = bottomPx - myBottom;
+ if (deltaBottom != 0) {
+ $("#editorcontainerbox").height(sizedBoxHeight + deltaBottom);
+ }
+ self.ace.adjustSize();
+ },
+ disable: function() {
+ if (self.ace) {
+ self.ace.setProperty("grayedOut", true);
+ self.ace.setEditable(false);
+ }
+ },
+ restoreRevisionText: function(dataFromServer) {
+ pad.addHistoricalAuthors(dataFromServer.historicalAuthorData);
+ self.ace.importAText(dataFromServer.atext, dataFromServer.apool, true);
+ }
+ };
+ return self;
+}());
+
diff --git a/etherpad/src/static/js/pad_impexp.js b/etherpad/src/static/js/pad_impexp.js
new file mode 100644
index 0000000..e928332
--- /dev/null
+++ b/etherpad/src/static/js/pad_impexp.js
@@ -0,0 +1,187 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+var padimpexp = (function() {
+
+ ///// import
+
+ var currentImportTimer = null;
+ var hidePanelCall = null;
+
+ function addImportFrames() {
+ $("#impexp-import .importframe").remove();
+ $('#impexp-import').append(
+ $('<iframe style="display: none;" name="importiframe" class="importframe"></iframe>'));
+ }
+ function fileInputUpdated() {
+ $('#importformfilediv').addClass('importformenabled');
+ $('#importsubmitinput').removeAttr('disabled');
+ $('#importmessagefail').fadeOut("fast");
+ $('#importarrow').show();
+ $('#importarrow').animate({paddingLeft:"0px"}, 500)
+ .animate({paddingLeft:"10px"}, 150, 'swing')
+ .animate({paddingLeft:"0px"}, 150, 'swing')
+ .animate({paddingLeft:"10px"}, 150, 'swing')
+ .animate({paddingLeft:"0px"}, 150, 'swing')
+ .animate({paddingLeft:"10px"}, 150, 'swing')
+ .animate({paddingLeft:"0px"}, 150, 'swing');
+ }
+ function fileInputSubmit() {
+ $('#importmessagefail').fadeOut("fast");
+ var ret = window.confirm(
+ "Importing a file will overwrite the current text of the pad."+
+ " Are you sure you want to proceed?");
+ if (ret) {
+ hidePanelCall = paddocbar.hideLaterIfNoOtherInteraction();
+ currentImportTimer = window.setTimeout(function() {
+ if (! currentImportTimer) {
+ return;
+ }
+ currentImportTimer = null;
+ importFailed("Request timed out.");
+ }, 25000); // time out after some number of seconds
+ $('#importsubmitinput').attr({disabled: true}).val("Importing...");
+ window.setTimeout(function() {
+ $('#importfileinput').attr({disabled: true}); }, 0);
+ $('#importarrow').stop(true, true).hide();
+ $('#importstatusball').show();
+ }
+ return ret;
+ }
+ function importFailed(msg) {
+ importErrorMessage(msg);
+ importDone();
+ addImportFrames();
+ }
+ function importDone() {
+ $('#importsubmitinput').removeAttr('disabled').val("Import Now");
+ window.setTimeout(function() {
+ $('#importfileinput').removeAttr('disabled'); }, 0);
+ $('#importstatusball').hide();
+ importClearTimeout();
+ }
+ function importClearTimeout() {
+ if (currentImportTimer) {
+ window.clearTimeout(currentImportTimer);
+ currentImportTimer = null;
+ }
+ }
+ function importErrorMessage(msg) {
+ function showError(fade) {
+ $('#importmessagefail').html(
+ '<strong style="color: red">Import failed:</strong> '+
+ (msg || 'Please try a different file.'))[(fade?"fadeIn":"show")]();
+ }
+
+ if ($('#importexport .importmessage').is(':visible')) {
+ $('#importmessagesuccess').fadeOut("fast");
+ $('#importmessagefail').fadeOut("fast", function() {
+ showError(true); });
+ } else {
+ showError();
+ }
+ }
+ function importSuccessful(token) {
+ $.ajax({
+ type: 'post',
+ url: '/ep/pad/impexp/import2',
+ data: {token: token, padId: pad.getPadId()},
+ success: importApplicationSuccessful,
+ error: importApplicationFailed,
+ timeout: 25000
+ });
+ addImportFrames();
+ }
+ function importApplicationFailed(xhr, textStatus, errorThrown) {
+ importErrorMessage("Error during conversion.");
+ importDone();
+ }
+ function importApplicationSuccessful(data, textStatus) {
+ if (data.substr(0, 2) == "ok") {
+ if ($('#importexport .importmessage').is(':visible')) {
+ $('#importexport .importmessage').hide();
+ }
+ $('#importmessagesuccess').html(
+ '<strong style="color: green">Import successful!</strong>').show();
+ $('#importformfilediv').hide();
+ window.setTimeout(function() {
+ $('#importmessagesuccess').fadeOut("slow", function() {
+ $('#importformfilediv').show();
+ });
+ if (hidePanelCall) {
+ hidePanelCall();
+ }
+ }, 3000);
+ } else if (data.substr(0, 4) == "fail") {
+ importErrorMessage(
+ "Couldn't update pad contents. This can happen if your web browser has \"cookies\" disabled.");
+ } else if (data.substr(0, 4) == "msg:") {
+ importErrorMessage(data.substr(4));
+ }
+ importDone();
+ }
+
+ ///// export
+
+ function cantExport() {
+ var type = $(this);
+ if (type.hasClass("exporthrefpdf")) {
+ type = "PDF";
+ } else if (type.hasClass("exporthrefdoc")) {
+ type = "Microsoft Word";
+ } else if (type.hasClass("exporthrefodt")) {
+ type = "OpenDocument";
+ } else {
+ type = "this file";
+ }
+ alert("Exporting as "+type+" format is disabled. Please contact your"+
+ " system administrator for details.");
+ return false;
+ }
+
+ /////
+
+ var self = {
+ init: function() {
+ $("#impexp-close").click(function() {paddocbar.setShownPanel(null);});
+
+ addImportFrames();
+ $("#importfileinput").change(fileInputUpdated);
+ $('#importform').submit(fileInputSubmit);
+ $('.disabledexport').click(cantExport);
+ },
+ handleFrameCall: function(callName, argsArray) {
+ if (callName == 'importFailed') {
+ importFailed(argsArray[0]);
+ }
+ else if (callName == 'importSuccessful') {
+ importSuccessful(argsArray[0]);
+ }
+ },
+ disable: function() {
+ $("#impexp-disabled-clickcatcher").show();
+ $("#impexp-import").css('opacity', 0.5);
+ $("#impexp-export").css('opacity', 0.5);
+ },
+ enable: function() {
+ $("#impexp-disabled-clickcatcher").hide();
+ $("#impexp-import").css('opacity', 1);
+ $("#impexp-export").css('opacity', 1);
+ }
+ };
+ return self;
+}());
diff --git a/etherpad/src/static/js/pad_modals.js b/etherpad/src/static/js/pad_modals.js
new file mode 100644
index 0000000..c9f48b5
--- /dev/null
+++ b/etherpad/src/static/js/pad_modals.js
@@ -0,0 +1,364 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var padmodals = (function() {
+
+ /*var clearFeedbackEmail = function() {};
+ function clearFeedback() {
+ clearFeedbackEmail();
+ $("#feedbackbox-message").val('');
+ }
+
+ var sendingFeedback = false;
+ function setSendingFeedback(v) {
+ v = !! v;
+ if (sendingFeedback != v) {
+ sendingFeedback = v;
+ if (v) {
+ $("#feedbackbox-send").css('opacity', 0.75);
+ }
+ else {
+ $("#feedbackbox-send").css('opacity', 1);
+ }
+ }
+ }*/
+
+ var sendingInvite = false;
+ function setSendingInvite(v) {
+ v = !! v;
+ if (sendingInvite != v) {
+ sendingInvite = v;
+ if (v) {
+ $("#sharebox-send").css('opacity', 0.75);
+ }
+ else {
+ $("#sharebox-send").css('opacity', 1);
+ }
+ }
+ }
+
+ var clearShareBoxTo = function() {};
+ function clearShareBox() {
+ clearShareBoxTo();
+ }
+
+ var allModals = $("#feedbackbox, #sharebox");
+
+ var shareboxExpanded = false;
+ var shareExpander = padutils.makeShowHideAnimator(function(state) {
+ function pow(x) { x = 1-x; x *= x*x; return 1-x; }
+ function ease(x) { return (x < 0) ? pow(x+1) : 1-pow(x); }
+ var smallHeight = 110 + (pad.getUserIsGuest() ? 0 : 50);
+ var bigHeight = 326 + (pad.getUserIsGuest() ? 0 : 50);
+ var boxHeight = (Math.abs(ease(state)))*(bigHeight-smallHeight) + smallHeight;
+ $("#sharebox").height(boxHeight);
+ // sharebox-open controls the disclosure triangle
+ // and also the visibility of any response (error/success)
+ // messages, which must be hidden during animation as well
+ // as when closed.
+ if (state == 0) {
+ $("#sharebox").addClass('sharebox-open');
+ }
+ else if (state > 0) {
+ $("#sharebox").removeClass('sharebox-open');
+ $("#sharebox-response").hide();
+ }
+ }, false, 20, 500);
+
+ //var overlayNode = null;
+
+ /*function adjustOverlay() {
+ if (overlayNode) {
+ var nodeWidth = overlayNode.width();
+ var nodeHeight = overlayNode.height();
+ var nodePos = overlayNode.position();
+ var outset = 10;
+ $("#modaloverlay").css({width: nodeWidth+outset*2,
+ height: nodeHeight+outset*2,
+ left: nodePos.left-outset,
+ top: nodePos.top-outset});
+ }
+ }*/
+
+ function showOverlay(forNode) {
+ //overlayNode = forNode;
+ //adjustOverlay();
+ $("#modaloverlay").show();
+ }
+ function hideOverlay() {
+ $("#modaloverlay").hide();
+ }
+
+ var self = {
+ shareExpander: shareExpander,
+ init: function() {
+ self.initFeedback();
+ self.initShareBox();
+ },
+ initFeedback: function() {
+ /*var emailField = $("#feedbackbox-email");
+ clearFeedbackEmail =
+ padutils.makeFieldLabeledWhenEmpty(emailField, '(your email address)').clear;
+ clearFeedback();*/
+
+ $("#feedbackbox-hide").click(function() {
+ self.hideModal();
+ });
+ /*$("#feedbackbox-send").click(function() {
+ self.sendFeedbackEmail();
+ });*/
+
+ $("#feedbackbutton").click(function() {
+ self.showFeedback();
+ });
+
+ $("#uservoicelinks a").click(function() {
+ self.hideModal();
+ return true;
+ });
+ $("#feedbackemails a").each(function() {
+ var node = $(this);
+ node.attr('href', "mailto:"+node.attr('href')+"@pad.spline.inf.fu-berlin.de");
+ });
+ },
+ initShareBox: function() {
+ $("#sharebutton, #nootherusers a").click(function() {
+ self.showShareBox();
+ });
+ $("#sharebox-hide").click(function() {
+ self.hideModal();
+ });
+ $("#sharebox-send").click(function() {
+ self.sendInvite();
+ });
+
+ $("#sharebox-url").click(function() {
+ $("#sharebox-url").focus().select();
+ });
+
+ clearShareBoxTo =
+ padutils.makeFieldLabeledWhenEmpty($("#sharebox-to"),
+ "(email addresses)").clear;
+ clearShareBox();
+
+ $("#sharebox-subject").val(self.getDefaultShareBoxSubjectForName(pad.getUserName()));
+ $("#sharebox-message").val(self.getDefaultShareBoxMessageForName(pad.getUserName()));
+
+ $("#sharebox-dislink").click(function() {
+ if (shareboxExpanded) {
+ shareboxExpanded = false;
+ shareExpander.hide();
+ }
+ else {
+ shareboxExpanded = true;
+ shareExpander.show();
+ }
+ });
+
+ $("#sharebox-stripe .setsecurity").click(function() {
+ self.hideModal();
+ paddocbar.setShownPanel('security');
+ });
+ },
+ getDefaultShareBoxMessageForName: function(name) {
+ return (name || "Somebody")+" has shared an EtherPad document with you."+
+ "\n\n"+"View it here:\n\n"+
+ padutils.escapeHtml($("#sharebox-url").val()+"\n");
+ },
+ getDefaultShareBoxSubjectForName: function(name) {
+ return (name || "Somebody")+" invited you to an EtherPad document";
+ },
+ relayoutWithBottom: function(px) {
+ $("#modaloverlay").height(px);
+ $("#sharebox").css('left',
+ Math.floor(($(window).width() -
+ $("#sharebox").outerWidth())/2));
+ $("#feedbackbox").css('left',
+ Math.floor(($(window).width() -
+ $("#feedbackbox").outerWidth())/2));
+ },
+ showFeedback: function() {
+ allModals.hide();
+ $("#feedbackbox").show();
+ showOverlay($("#feedbackbox"));
+ },
+ showShareBox: function() {
+ // when showing the dialog, if it still says "Somebody" invited you
+ // then we fill in the updated username if there is one;
+ // otherwise, we don't touch it, perhaps the user is happy with it
+ var msgbox = $("#sharebox-message");
+ if (msgbox.val() == self.getDefaultShareBoxMessageForName(null)) {
+ msgbox.val(self.getDefaultShareBoxMessageForName(pad.getUserName()));
+ }
+ var subjBox = $("#sharebox-subject");
+ if (subjBox.val() == self.getDefaultShareBoxSubjectForName(null)) {
+ subjBox.val(self.getDefaultShareBoxSubjectForName(pad.getUserName()));
+ }
+
+ if (pad.isPadPublic()) {
+ $("#sharebox-stripe").get(0).className = 'sharebox-stripe-public';
+ }
+ else {
+ $("#sharebox-stripe").get(0).className = 'sharebox-stripe-private';
+ }
+
+ allModals.hide();
+ $("#sharebox").show();
+ showOverlay($("#sharebox"));
+ $("#sharebox-url").focus().select();
+ },
+ hideModal: function() {
+ padutils.cancelActions('hide-feedbackbox');
+ padutils.cancelActions('hide-sharebox');
+ $("#sharebox-response").hide();
+ allModals.hide();
+ hideOverlay();
+ },
+ hideFeedbackLaterIfNoOtherInteraction: function() {
+ return padutils.getCancellableAction('hide-feedbackbox',
+ function() {
+ self.hideModal();
+ });
+ },
+ hideShareboxLaterIfNoOtherInteraction: function() {
+ return padutils.getCancellableAction('hide-sharebox',
+ function() {
+ self.hideModal();
+ });
+ },
+/* sendFeedbackEmail: function() {
+ if (sendingFeedback) {
+ return;
+ }
+ var message = $("#feedbackbox-message").val();
+ if (! message) {
+ return;
+ }
+ var email = ($("#feedbackbox-email").hasClass('editempty') ? '' :
+ $("#feedbackbox-email").val());
+ var padId = pad.getPadId();
+ var username = pad.getUserName();
+ setSendingFeedback(true);
+ $("#feedbackbox-response").html("Sending...").get(0).className = '';
+ $("#feedbackbox-response").show();
+ $.ajax({
+ type: 'post',
+ url: '/ep/pad/feedback',
+ data: {
+ feedback: message,
+ padId: padId,
+ username: username,
+ email: email
+ },
+ success: success,
+ error: error
+ });
+ var hideCall = self.hideFeedbackLaterIfNoOtherInteraction();
+ function success(msg) {
+ setSendingFeedback(false);
+ clearFeedback();
+ $("#feedbackbox-response").html("Thanks for your feedback").get(0).className = 'goodresponse';
+ $("#feedbackbox-response").show();
+ window.setTimeout(function() {
+ $("#feedbackbox-response").fadeOut('slow', function() {
+ hideCall();
+ });
+ }, 1500);
+ }
+ function error(e) {
+ setSendingFeedback(false);
+ $("#feedbackbox-response").html("Could not send feedback. Please email us at feedback"+"@"+"pad.spline.inf.fu-berlin.de instead.").get(0).className = 'badresponse';
+ $("#feedbackbox-response").show();
+ }
+ },*/
+ sendInvite: function() {
+ if (sendingInvite) {
+ return;
+ }
+ if (! pad.isFullyConnected()) {
+ displayErrorMessage("Error: Connection to the server is down or flaky.");
+ return;
+ }
+ var message = $("#sharebox-message").val();
+ if (! message) {
+ displayErrorMessage("Please enter a message body before sending.");
+ return;
+ }
+ var emails = ($("#sharebox-to").hasClass('editempty') ? '' :
+ $("#sharebox-to").val()) || '';
+ // find runs of characters that aren't obviously non-email punctuation
+ var emailArray = emails.match(/[^\s,:;<>\"\'\/\(\)\[\]{}]+/g) || [];
+ if (emailArray.length == 0) {
+ displayErrorMessage('Please enter at least one "To:" address.');
+ $("#sharebox-to").focus().select();
+ return;
+ }
+ for(var i=0;i<emailArray.length;i++) {
+ var addr = emailArray[i];
+ if (! addr.match(/^[\w\.\_\+\-]+\@[\w\_\-]+\.[\w\_\-\.]+$/)) {
+ displayErrorMessage('"'+padutils.escapeHtml(addr) +
+ '" does not appear to be a valid email address.');
+ return;
+ }
+ }
+ var subject = $("#sharebox-subject").val();
+ if (! subject) {
+ subject = self.getDefaultShareBoxSubjectForName(pad.getUserName());
+ $("#sharebox-subject").val(subject); // force the default subject
+ }
+
+ var padId = pad.getPadId();
+ var username = pad.getUserName();
+ setSendingInvite(true);
+ $("#sharebox-response").html("Sending...").get(0).className = '';
+ $("#sharebox-response").show();
+ $.ajax({
+ type: 'post',
+ url: '/ep/pad/emailinvite',
+ data: {
+ message: message,
+ toEmails: emailArray.join(','),
+ subject: subject,
+ username: username,
+ padId: padId
+ },
+ success: success,
+ error: error
+ });
+ var hideCall = self.hideShareboxLaterIfNoOtherInteraction();
+ function success(msg) {
+ setSendingInvite(false);
+ $("#sharebox-response").html("Email invitation sent!").get(0).className = 'goodresponse';
+ $("#sharebox-response").show();
+ window.setTimeout(function() {
+ $("#sharebox-response").fadeOut('slow', function() {
+ hideCall();
+ });
+ }, 1500);
+ }
+ function error(e) {
+ setSendingFeedback(false);
+ $("#sharebox-response").html("An error occurred; no email was sent.").get(0).className = 'badresponse';
+ $("#sharebox-response").show();
+ }
+ function displayErrorMessage(msgHtml) {
+ $("#sharebox-response").html(msgHtml).get(0).className = 'badresponse';
+ $("#sharebox-response").show();
+ }
+ }
+ };
+ return self;
+}()); \ No newline at end of file
diff --git a/etherpad/src/static/js/pad_savedrevs.js b/etherpad/src/static/js/pad_savedrevs.js
new file mode 100644
index 0000000..ec06a1f
--- /dev/null
+++ b/etherpad/src/static/js/pad_savedrevs.js
@@ -0,0 +1,408 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+var padsavedrevs = (function() {
+
+ function reversedCopy(L) {
+ var L2 = L.slice();
+ L2.reverse();
+ return L2;
+ }
+
+ function makeRevisionBox(revisionInfo, rnum) {
+ var box = $('<div class="srouterbox">'+
+ '<div class="srinnerbox">'+
+ '<a href="javascript:void(0)" class="srname"><!-- --></a>'+
+ '<div class="sractions"><a class="srview" href="javascript:void(0)" target="_blank">view</a> | <a class="srrestore" href="javascript:void(0)">restore</a></div>'+
+ '<div class="srtime"><!-- --></div>'+
+ '<div class="srauthor"><!-- --></div>'+
+ '<img class="srtwirly" src="/static/img/misc/status-ball.gif">'+
+ '</div></div>');
+ setBoxLabel(box, revisionInfo.label);
+ setBoxTimestamp(box, revisionInfo.timestamp);
+ box.find(".srauthor").html("by "+padutils.escapeHtml(revisionInfo.savedBy));
+ var viewLink = '/ep/pad/view/'+pad.getPadId()+'/'+revisionInfo.id;
+ box.find(".srview").attr('href', viewLink);
+ var restoreLink = 'javascript:void padsavedrevs.restoreRevision('+rnum+');';
+ box.find(".srrestore").attr('href', restoreLink);
+ box.find(".srname").click(function(evt) {
+ editRevisionLabel(rnum, box);
+ });
+ return box;
+ }
+ function setBoxLabel(box, label) {
+ box.find(".srname").html(padutils.escapeHtml(label)).attr('title', label);
+ }
+ function setBoxTimestamp(box, timestamp) {
+ box.find(".srtime").html(padutils.escapeHtml(
+ padutils.timediff(new Date(timestamp))));
+ }
+ function getNthBox(n) {
+ return $("#savedrevisions .srouterbox").eq(n);
+ }
+ function editRevisionLabel(rnum, box) {
+ var input = $('<input type="text" class="srnameedit"/>');
+ box.find(".srnameedit").remove(); // just in case
+ var label = box.find(".srname");
+ input.width(label.width());
+ input.height(label.height());
+ input.css('top', label.position().top);
+ input.css('left', label.position().left);
+ label.after(input);
+ label.css('opacity', 0);
+ function endEdit() {
+ input.remove();
+ label.css('opacity', 1);
+ }
+ var rev = currentRevisionList[rnum];
+ var oldLabel = rev.label;
+ input.blur(function() {
+ var newLabel = input.val();
+ if (newLabel && newLabel != oldLabel) {
+ relabelRevision(rnum, newLabel);
+ }
+ endEdit();
+ });
+ input.val(rev.label).focus().select();
+ padutils.bindEnterAndEscape(input, function onEnter() {
+ input.blur();
+ }, function onEscape() {
+ input.val('').blur();
+ });
+ }
+ function relabelRevision(rnum, newLabel) {
+ var rev = currentRevisionList[rnum];
+ $.ajax({
+ type: 'post',
+ url: '/ep/pad/saverevisionlabel',
+ data: {userId: pad.getUserId(),
+ padId: pad.getPadId(),
+ revId: rev.id,
+ newLabel: newLabel},
+ success: success,
+ error: error
+ });
+ function success(text) {
+ var newRevisionList = JSON.parse(text);
+ self.newRevisionList(newRevisionList);
+ pad.sendClientMessage({
+ type: 'revisionLabel',
+ revisionList: reversedCopy(currentRevisionList),
+ savedBy: pad.getUserName(),
+ newLabel: newLabel
+ });
+ }
+ function error(e) {
+ alert("Oops! There was an error saving that revision label. Please try again later.");
+ }
+ }
+
+ var currentRevisionList = [];
+ function setRevisionList(newRevisionList, noAnimation) {
+ // deals with changed labels and new added revisions
+ for(var i=0; i<currentRevisionList.length; i++) {
+ var a = currentRevisionList[i];
+ var b = newRevisionList[i];
+ if (b.label != a.label) {
+ setBoxLabel(getNthBox(i), b.label);
+ }
+ }
+ for(var j=currentRevisionList.length; j<newRevisionList.length; j++) {
+ var newBox = makeRevisionBox(newRevisionList[j], j);
+ $("#savedrevs-scrollinner").append(newBox);
+ newBox.css('left', j * REVISION_BOX_WIDTH);
+ }
+ var newOnes = (newRevisionList.length > currentRevisionList.length);
+ currentRevisionList = newRevisionList;
+ if (newOnes) {
+ setDesiredScroll(getMaxScroll());
+ if (noAnimation) {
+ setScroll(desiredScroll);
+ }
+
+ if (! noAnimation) {
+ var nameOfLast = currentRevisionList[currentRevisionList.length-1].label;
+ displaySavedTip(nameOfLast);
+ }
+ }
+ }
+ function refreshRevisionList() {
+ for(var i=0;i<currentRevisionList.length; i++) {
+ var r = currentRevisionList[i];
+ var box = getNthBox(i);
+ setBoxTimestamp(box, r.timestamp);
+ }
+ }
+
+ var savedTipAnimator = padutils.makeShowHideAnimator(function(state) {
+ if (state == -1) {
+ $("#revision-notifier").css('opacity', 0).css('display', 'block');
+ }
+ else if (state == 0) {
+ $("#revision-notifier").css('opacity', 1);
+ }
+ else if (state == 1) {
+ $("#revision-notifier").css('opacity', 0).css('display', 'none');
+ }
+ else if (state < 0) {
+ $("#revision-notifier").css('opacity', 1);
+ }
+ else if (state > 0) {
+ $("#revision-notifier").css('opacity', 1 - state);
+ }
+ }, false, 25, 300);
+
+ function displaySavedTip(text) {
+ $("#revision-notifier .name").html(padutils.escapeHtml(text));
+ savedTipAnimator.show();
+ padutils.cancelActions("hide-revision-notifier");
+ var hideLater = padutils.getCancellableAction("hide-revision-notifier",
+ function() {
+ savedTipAnimator.hide();
+ });
+ window.setTimeout(hideLater, 3000);
+ }
+
+ var REVISION_BOX_WIDTH = 120;
+ var curScroll = 0; // distance between left of revisions and right of view
+ var desiredScroll = 0;
+ function getScrollWidth() {
+ return REVISION_BOX_WIDTH * currentRevisionList.length;
+ }
+ function getViewportWidth() {
+ return $("#savedrevs-scrollouter").width();
+ }
+ function getMinScroll() {
+ return Math.min(getViewportWidth(), getScrollWidth());
+ }
+ function getMaxScroll() {
+ return getScrollWidth();
+ }
+ function setScroll(newScroll) {
+ curScroll = newScroll;
+ $("#savedrevs-scrollinner").css('right', newScroll);
+ updateScrollArrows();
+ }
+ function setDesiredScroll(newDesiredScroll, dontUpdate) {
+ desiredScroll = Math.min(getMaxScroll(), Math.max(getMinScroll(),
+ newDesiredScroll));
+ if (! dontUpdate) {
+ updateScroll();
+ }
+ }
+ function updateScroll() {
+ updateScrollArrows();
+ scrollAnimator.scheduleAnimation();
+ }
+ function updateScrollArrows() {
+ $("#savedrevs-scrollleft").toggleClass("disabledscrollleft",
+ desiredScroll <= getMinScroll());
+ $("#savedrevs-scrollright").toggleClass("disabledscrollright",
+ desiredScroll >= getMaxScroll());
+ }
+ var scrollAnimator = padutils.makeAnimationScheduler(function() {
+ setDesiredScroll(desiredScroll, true); // re-clamp
+ if (Math.abs(desiredScroll - curScroll) < 1) {
+ setScroll(desiredScroll);
+ return false;
+ }
+ else {
+ setScroll(curScroll + (desiredScroll - curScroll)*0.5);
+ return true;
+ }
+ }, 50, 2);
+
+ var isSaving = false;
+ function setIsSaving(v) {
+ isSaving = v;
+ rerenderButton();
+ }
+
+ function haveReachedRevLimit() {
+ var mv = pad.getPrivilege('maxRevisions');
+ return (!(mv < 0 || mv > currentRevisionList.length));
+ }
+ function rerenderButton() {
+ if (isSaving || (! pad.isFullyConnected()) ||
+ haveReachedRevLimit()) {
+ $("#savedrevs-savenow").css('opacity', 0.75);
+ }
+ else {
+ $("#savedrevs-savenow").css('opacity', 1);
+ }
+ }
+
+ var scrollRepeatTimer = null;
+ var scrollStartTime = 0;
+ function setScrollRepeatTimer(dir) {
+ clearScrollRepeatTimer();
+ scrollStartTime = +new Date;
+ scrollRepeatTimer = window.setTimeout(function f() {
+ if (! scrollRepeatTimer) {
+ return;
+ }
+ self.scroll(dir);
+ var scrollTime = (+new Date) - scrollStartTime;
+ var delay = (scrollTime > 2000 ? 50 : 300);
+ scrollRepeatTimer = window.setTimeout(f, delay);
+ }, 300);
+ $(document).bind('mouseup', clearScrollRepeatTimer);
+ }
+ function clearScrollRepeatTimer() {
+ if (scrollRepeatTimer) {
+ window.clearTimeout(scrollRepeatTimer);
+ scrollRepeatTimer = null;
+ }
+ $(document).unbind('mouseup', clearScrollRepeatTimer);
+ }
+
+ var self = {
+ init: function(initialRevisions) {
+ self.newRevisionList(initialRevisions, true);
+
+ $("#savedrevs-savenow").click(function() { self.saveNow(); });
+ $("#savedrevs-scrollleft").mousedown(function() {
+ self.scroll('left');
+ setScrollRepeatTimer('left');
+ });
+ $("#savedrevs-scrollright").mousedown(function() {
+ self.scroll('right');
+ setScrollRepeatTimer('right');
+ });
+ $("#savedrevs-close").click(function() {paddocbar.setShownPanel(null);});
+
+ // update "saved n minutes ago" times
+ window.setInterval(function() {
+ refreshRevisionList();
+ }, 60*1000);
+ },
+ restoreRevision: function(rnum) {
+ var rev = currentRevisionList[rnum];
+ var warning = ("Restoring this revision will overwrite the current"
+ + " text of the pad. "+
+ "Are you sure you want to continue?");
+ var hidePanel = paddocbar.hideLaterIfNoOtherInteraction();
+ var box = getNthBox(rnum);
+ if (confirm(warning)) {
+ box.find(".srtwirly").show();
+ $.ajax({
+ type: 'get',
+ url: '/ep/pad/getrevisionatext',
+ data: {padId: pad.getPadId(), revId: rev.id},
+ success: success,
+ error: error
+ });
+ }
+ function success(resultJson) {
+ untwirl();
+ var result = JSON.parse(resultJson);
+ padeditor.restoreRevisionText(result);
+ window.setTimeout(function() {
+ hidePanel();
+ }, 0);
+ }
+ function error(e) {
+ untwirl();
+ alert("Oops! There was an error retreiving the text (revNum= "+
+ rev.revNum+"; padId="+pad.getPadId());
+ }
+ function untwirl() {
+ box.find(".srtwirly").hide();
+ }
+ },
+ showReachedLimit: function() {
+ alert("Sorry, you do not have privileges to save more than "+
+ pad.getPrivilege('maxRevisions')+" revisions.");
+ },
+ newRevisionList: function(lst, noAnimation) {
+ // server gives us list with newest first;
+ // we want chronological order
+ var L = reversedCopy(lst);
+ setRevisionList(L, noAnimation);
+ rerenderButton();
+ },
+ saveNow: function() {
+ if (isSaving) {
+ return;
+ }
+ if (! pad.isFullyConnected()) {
+ return;
+ }
+ if (haveReachedRevLimit()) {
+ self.showReachedLimit();
+ return;
+ }
+ setIsSaving(true);
+ var savedBy = pad.getUserName() || "unnamed";
+ pad.callWhenNotCommitting(submitSave);
+
+ function submitSave() {
+ $.ajax({
+ type: 'post',
+ url: '/ep/pad/saverevision',
+ data: {
+ padId: pad.getPadId(),
+ savedBy: savedBy,
+ savedById: pad.getUserId(),
+ revNum: pad.getCollabRevisionNumber()
+ },
+ success: success,
+ error: error
+ });
+ }
+ function success(text) {
+ setIsSaving(false);
+ var newRevisionList = JSON.parse(text);
+ self.newRevisionList(newRevisionList);
+ pad.sendClientMessage({
+ type: 'newRevisionList',
+ revisionList: newRevisionList,
+ savedBy: savedBy
+ });
+ }
+ function error(e) {
+ setIsSaving(false);
+ alert("Oops! The server failed to save the revision. Please try again later.");
+ }
+ },
+ handleResizePage: function() {
+ updateScrollArrows();
+ },
+ handleIsFullyConnected: function(isConnected) {
+ rerenderButton();
+ },
+ scroll: function(dir) {
+ var minScroll = getMinScroll();
+ var maxScroll = getMaxScroll();
+ if (dir == 'left') {
+ if (desiredScroll > minScroll) {
+ var n = Math.floor((desiredScroll - 1 - minScroll) /
+ REVISION_BOX_WIDTH);
+ setDesiredScroll(Math.max(0, n)*REVISION_BOX_WIDTH + minScroll);
+ }
+ }
+ else if (dir == 'right') {
+ if (desiredScroll < maxScroll) {
+ var n = Math.floor((maxScroll - desiredScroll - 1) /
+ REVISION_BOX_WIDTH);
+ setDesiredScroll(maxScroll - Math.max(0, n)*REVISION_BOX_WIDTH);
+ }
+ }
+ }
+ };
+ return self;
+}()); \ No newline at end of file
diff --git a/etherpad/src/static/js/pad_userlist.js b/etherpad/src/static/js/pad_userlist.js
new file mode 100644
index 0000000..13bfd6a
--- /dev/null
+++ b/etherpad/src/static/js/pad_userlist.js
@@ -0,0 +1,604 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+var paduserlist = (function() {
+
+ var rowManager = (function() {
+ // The row manager handles rendering rows of the user list and animating
+ // their insertion, removal, and reordering. It manipulates TD height
+ // and TD opacity.
+
+ function nextRowId() {
+ return "usertr"+(nextRowId.counter++);
+ }
+ nextRowId.counter = 1;
+ // objects are shared; fields are "domId","data","animationStep"
+ var rowsFadingOut = []; // unordered set
+ var rowsFadingIn = []; // unordered set
+ var rowsPresent = []; // in order
+
+ var ANIMATION_START = -12; // just starting to fade in
+ var ANIMATION_END = 12; // just finishing fading out
+ function getAnimationHeight(step, power) {
+ var a = Math.abs(step/12);
+ if (power == 2) a = a*a;
+ else if (power == 3) a = a*a*a;
+ else if (power == 4) a = a*a*a*a;
+ else if (power >= 5) a = a*a*a*a*a;
+ return Math.round(26*(1-a));
+ }
+ var OPACITY_STEPS = 6;
+
+ var ANIMATION_STEP_TIME = 20;
+ var LOWER_FRAMERATE_FACTOR = 2;
+ var scheduleAnimation = padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME,
+ LOWER_FRAMERATE_FACTOR).scheduleAnimation;
+
+ var NUMCOLS = 4;
+
+ // we do lots of manipulation of table rows and stuff that JQuery makes ok, despite
+ // IE's poor handling when manipulating the DOM directly.
+
+ function getEmptyRowHtml(height) {
+ return '<td colspan="'+NUMCOLS+'" style="border:0;height:'+height+'px"><!-- --></td>';
+ }
+ function isNameEditable(data) {
+ return (! data.name) && (data.status != 'Disconnected');
+ }
+ function replaceUserRowContents(tr, height, data) {
+ var tds = getUserRowHtml(height, data).match(/<td.*?<\/td>/gi);
+ if (isNameEditable(data) && tr.find("td.usertdname input:enabled").length > 0) {
+ // preserve input field node
+ for(var i=0; i<tds.length; i++) {
+ var oldTd = $(tr.find("td").get(i));
+ if (! oldTd.hasClass('usertdname')) {
+ oldTd.replaceWith(tds[i]);
+ }
+ }
+ }
+ else {
+ tr.html(tds.join(''));
+ }
+ return tr;
+ }
+ function getUserRowHtml(height, data) {
+ var nameHtml;
+ var isGuest = (data.id.charAt(0) != 'p');
+ if (data.name) {
+ nameHtml = padutils.escapeHtml(data.name);
+ if (isGuest && pad.getIsProPad()) {
+ nameHtml += ' (Guest)';
+ }
+ }
+ else {
+ nameHtml = '<input type="text" class="editempty newinput" value="unnamed" '+
+ (isNameEditable(data) ? '' : 'disabled="disabled" ')+
+ '/>';
+ }
+
+ return ['<td style="height:',height,'px" class="usertdswatch"><div class="swatch" style="background:'+data.color+'">&nbsp;</div></td>',
+ '<td style="height:',height,'px" class="usertdname">',nameHtml,'</td>',
+ '<td style="height:',height,'px" class="usertdstatus">',padutils.escapeHtml(data.status),'</td>',
+ '<td style="height:',height,'px" class="activity">',padutils.escapeHtml(data.activity),'</td>'].join('');
+ }
+ function getRowHtml(id, innerHtml) {
+ return '<tr id="'+id+'">'+innerHtml+'</tr>';
+ }
+ function rowNode(row) {
+ return $("#"+row.domId);
+ }
+ function handleRowData(row) {
+ if (row.data && row.data.status == 'Disconnected') {
+ row.opacity = 0.5;
+ }
+ else {
+ delete row.opacity;
+ }
+ }
+ function handleRowNode(tr, data) {
+ if (data.titleText) {
+ tr.attr('title', data.titleText);
+ }
+ else {
+ tr.removeAttr('title');
+ }
+ }
+ function handleOtherUserInputs() {
+ // handle 'INPUT' elements for naming other unnamed users
+ $("#otheruserstable input.newinput").each(function() {
+ var input = $(this);
+ var tr = input.closest("tr");
+ if (tr.length > 0) {
+ var index = tr.parent().children().index(tr);
+ if (index >= 0) {
+ var userId = rowsPresent[index].data.id;
+ rowManagerMakeNameEditor($(this), userId);
+ }
+ }
+ }).removeClass('newinput');
+ }
+
+ // animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc.
+ function insertRow(position, data, animationPower) {
+ position = Math.max(0, Math.min(rowsPresent.length, position));
+ animationPower = (animationPower === undefined ? 4 : animationPower);
+
+ var domId = nextRowId();
+ var row = {data: data, animationStep: ANIMATION_START, domId: domId,
+ animationPower: animationPower};
+ handleRowData(row);
+ rowsPresent.splice(position, 0, row);
+ var tr;
+ if (animationPower == 0) {
+ tr = $(getRowHtml(domId, getUserRowHtml(getAnimationHeight(0), data)));
+ row.animationStep = 0;
+ }
+ else {
+ rowsFadingIn.push(row);
+ tr = $(getRowHtml(domId, getEmptyRowHtml(getAnimationHeight(ANIMATION_START))));
+ }
+ handleRowNode(tr, data);
+ if (position == 0) {
+ $("table#otheruserstable").prepend(tr);
+ }
+ else {
+ rowNode(rowsPresent[position-1]).after(tr);
+ }
+
+ if (animationPower != 0) {
+ scheduleAnimation();
+ }
+
+ handleOtherUserInputs();
+
+ return row;
+ }
+
+ function updateRow(position, data) {
+ var row = rowsPresent[position];
+ if (row) {
+ row.data = data;
+ handleRowData(row);
+ if (row.animationStep == 0) {
+ // not currently animating
+ var tr = rowNode(row);
+ replaceUserRowContents(tr, getAnimationHeight(0), row.data).find(
+ "td").css('opacity', (row.opacity === undefined ? 1 : row.opacity));
+ handleRowNode(tr, data);
+ handleOtherUserInputs();
+ }
+ }
+ }
+
+ function removeRow(position, animationPower) {
+ animationPower = (animationPower === undefined ? 4 : animationPower);
+ var row = rowsPresent[position];
+ if (row) {
+ rowsPresent.splice(position, 1); // remove
+ if (animationPower == 0) {
+ rowNode(row).remove();
+ }
+ else {
+ row.animationStep = - row.animationStep; // use symmetry
+ row.animationPower = animationPower;
+ rowsFadingOut.push(row);
+ scheduleAnimation();
+ }
+ }
+ }
+
+ // newPosition is position after the row has been removed
+ function moveRow(oldPosition, newPosition, animationPower) {
+ animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best
+ var row = rowsPresent[oldPosition];
+ if (row && oldPosition != newPosition) {
+ var rowData = row.data;
+ removeRow(oldPosition, animationPower);
+ insertRow(newPosition, rowData, animationPower);
+ }
+ }
+
+ function animateStep() {
+ // animation must be symmetrical
+ for(var i=rowsFadingIn.length-1;i>=0;i--) { // backwards to allow removal
+ var row = rowsFadingIn[i];
+ var step = ++row.animationStep;
+ var animHeight = getAnimationHeight(step, row.animationPower);
+ var node = rowNode(row);
+ var baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
+ if (step <= -OPACITY_STEPS) {
+ node.find("td").height(animHeight);
+ }
+ else if (step == -OPACITY_STEPS+1) {
+ node.html(getUserRowHtml(animHeight, row.data)).find("td").css(
+ 'opacity', baseOpacity*1/OPACITY_STEPS);
+ handleRowNode(node, row.data);
+ }
+ else if (step < 0) {
+ node.find("td").css('opacity', baseOpacity*(OPACITY_STEPS-(-step))/OPACITY_STEPS).height(animHeight);
+ }
+ else if (step == 0) {
+ // set HTML in case modified during animation
+ node.html(getUserRowHtml(animHeight, row.data)).find("td").css(
+ 'opacity', baseOpacity*1).height(animHeight);
+ handleRowNode(node, row.data);
+ rowsFadingIn.splice(i, 1); // remove from set
+ }
+ }
+ for(var i=rowsFadingOut.length-1;i>=0;i--) { // backwards to allow removal
+ var row = rowsFadingOut[i];
+ var step = ++row.animationStep;
+ var node = rowNode(row);
+ var animHeight = getAnimationHeight(step, row.animationPower);
+ var baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
+ if (step < OPACITY_STEPS) {
+ node.find("td").css('opacity', baseOpacity*(OPACITY_STEPS - step)/OPACITY_STEPS).height(animHeight);
+ }
+ else if (step == OPACITY_STEPS) {
+ node.html(getEmptyRowHtml(animHeight));
+ }
+ else if (step <= ANIMATION_END) {
+ node.find("td").height(animHeight);
+ }
+ else {
+ rowsFadingOut.splice(i, 1); // remove from set
+ node.remove();
+ }
+ }
+
+ handleOtherUserInputs();
+
+ return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do
+ }
+
+ var self = {
+ insertRow: insertRow,
+ removeRow: removeRow,
+ moveRow: moveRow,
+ updateRow: updateRow
+ };
+ return self;
+ }()); ////////// rowManager
+
+ var myUserInfo = {};
+ var otherUsersInfo = [];
+ var otherUsersData = [];
+ var colorPickerOpen = false;
+
+ function rowManagerMakeNameEditor(jnode, userId) {
+ setUpEditable(jnode, function() {
+ var existingIndex = findExistingIndex(userId);
+ if (existingIndex >= 0) {
+ return otherUsersInfo[existingIndex].name || '';
+ }
+ else {
+ return '';
+ }
+ }, function(newName) {
+ if (! newName) {
+ jnode.addClass("editempty");
+ jnode.val("unnamed");
+ }
+ else {
+ jnode.attr('disabled', 'disabled');
+ pad.suggestUserName(userId, newName);
+ }
+ });
+ }
+
+ function renderMyUserInfo() {
+ if (myUserInfo.name) {
+ $("#myusernameedit").removeClass("editempty").val(
+ myUserInfo.name);
+ }
+ else {
+ $("#myusernameedit").addClass("editempty").val(
+ "< enter your name >");
+ }
+ if (colorPickerOpen) {
+ $("#myswatchbox").addClass('myswatchboxunhoverable').removeClass(
+ 'myswatchboxhoverable');
+ }
+ else {
+ $("#myswatchbox").addClass('myswatchboxhoverable').removeClass(
+ 'myswatchboxunhoverable');
+ }
+ $("#myswatch").css('background', pad.getColorPalette()[myUserInfo.colorId]);
+ }
+
+ function findExistingIndex(userId) {
+ var existingIndex = -1;
+ for(var i=0;i<otherUsersInfo.length;i++) {
+ if (otherUsersInfo[i].userId == userId) {
+ existingIndex = i;
+ break;
+ }
+ }
+ return existingIndex;
+ }
+
+ function setUpEditable(jqueryNode, valueGetter, valueSetter) {
+ jqueryNode.bind('focus', function(evt) {
+ var oldValue = valueGetter();
+ if (jqueryNode.val() !== oldValue) {
+ jqueryNode.val(oldValue);
+ }
+ jqueryNode.addClass("editactive").removeClass("editempty");
+ });
+ jqueryNode.bind('blur', function(evt) {
+ var newValue = jqueryNode.removeClass("editactive").val();
+ valueSetter(newValue);
+ });
+ padutils.bindEnterAndEscape(jqueryNode, function onEnter() {
+ jqueryNode.blur();
+ }, function onEscape() {
+ jqueryNode.val(valueGetter()).blur();
+ });
+ jqueryNode.removeAttr('disabled').addClass('editable');
+ }
+
+ function showColorPicker() {
+ if (! colorPickerOpen) {
+ var palette = pad.getColorPalette();
+ for(var i=0;i<palette.length;i++) {
+ $("#mycolorpicker .n"+(i+1)+" .pickerswatch").css(
+ 'background', palette[i]);
+ }
+ $("#mycolorpicker").css('display', 'block');
+ colorPickerOpen = true;
+ renderMyUserInfo();
+ }
+ // this part happens even if color picker is already open
+ $("#mycolorpicker .pickerswatchouter").removeClass('picked');
+ $("#mycolorpicker .pickerswatchouter:eq("+(myUserInfo.colorId||0)+")").
+ addClass('picked');
+ }
+ function getColorPickerSwatchIndex(jnode) {
+ return Number(jnode.get(0).className.match(/\bn([0-9]+)\b/)[1])-1;
+ }
+ function closeColorPicker(accept) {
+ if (accept) {
+ var newColorId = getColorPickerSwatchIndex($("#mycolorpicker .picked"));
+ if (newColorId >= 0) { // fails on NaN
+ myUserInfo.colorId = newColorId;
+ pad.notifyChangeColor(newColorId);
+ }
+ }
+ colorPickerOpen = false;
+ $("#mycolorpicker").css('display', 'none');
+ renderMyUserInfo();
+ }
+
+ function updateInviteNotice() {
+ if (otherUsersInfo.length == 0) {
+ $("#otheruserstable").hide();
+ $("#nootherusers").show();
+ }
+ else {
+ $("#nootherusers").hide();
+ $("#otheruserstable").show();
+ }
+ }
+
+ var knocksToIgnore = {};
+ var guestPromptFlashState = 0;
+ var guestPromptFlash = padutils.makeAnimationScheduler(
+ function () {
+ var prompts = $("#guestprompts .guestprompt");
+ if (prompts.length == 0) {
+ return false; // no more to do
+ }
+
+ guestPromptFlashState = 1 - guestPromptFlashState;
+ if (guestPromptFlashState) {
+ prompts.css('background', '#ffa');
+ }
+ else {
+ prompts.css('background', '#ffe');
+ }
+
+ return true;
+ }, 1000);
+
+ var self = {
+ init: function(myInitialUserInfo) {
+ self.setMyUserInfo(myInitialUserInfo);
+
+ $("#otheruserstable tr").remove();
+
+ if (pad.getUserIsGuest()) {
+ $("#myusernameedit").addClass('myusernameedithoverable');
+ setUpEditable($("#myusernameedit"),
+ function() {
+ return myUserInfo.name || '';
+ },
+ function(newValue) {
+ myUserInfo.name = newValue;
+ pad.notifyChangeName(newValue);
+ // wrap with setTimeout to do later because we get
+ // a double "blur" fire in IE...
+ window.setTimeout(function() {
+ renderMyUserInfo();
+ }, 0);
+ });
+ }
+
+ // color picker
+ $("#myswatchbox").click(showColorPicker);
+ $("#mycolorpicker .pickerswatchouter").click(function() {
+ $("#mycolorpicker .pickerswatchouter").removeClass('picked');
+ $(this).addClass('picked');
+ });
+ $("#mycolorpickersave").click(function() {
+ closeColorPicker(true);
+ });
+ $("#mycolorpickercancel").click(function() {
+ closeColorPicker(false);
+ });
+ //
+
+ },
+ setMyUserInfo: function(info) {
+ myUserInfo = $.extend({}, info);
+
+ renderMyUserInfo();
+ },
+ userJoinOrUpdate: function(info) {
+ if ((! info.userId) || (info.userId == myUserInfo.userId)) {
+ // not sure how this would happen
+ return;
+ }
+
+ var userData = {};
+ userData.color = pad.getColorPalette()[info.colorId];
+ userData.name = info.name;
+ userData.status = '';
+ userData.activity = '';
+ userData.id = info.userId;
+ // Firefox ignores \n in title text; Safari does a linebreak
+ userData.titleText = [info.userAgent||'', info.ip||''].join(' \n');
+
+ var existingIndex = findExistingIndex(info.userId);
+
+ var numUsersBesides = otherUsersInfo.length;
+ if (existingIndex >= 0) {
+ numUsersBesides--;
+ }
+ var newIndex = padutils.binarySearch(numUsersBesides, function(n) {
+ if (existingIndex >= 0 && n >= existingIndex) {
+ // pretend existingIndex isn't there
+ n++;
+ }
+ var infoN = otherUsersInfo[n];
+ var nameN = (infoN.name||'').toLowerCase();
+ var nameThis = (info.name||'').toLowerCase();
+ var idN = infoN.userId;
+ var idThis = info.userId;
+ return (nameN > nameThis) || (nameN == nameThis &&
+ idN > idThis);
+ });
+
+ if (existingIndex >= 0) {
+ // update
+ if (existingIndex == newIndex) {
+ otherUsersInfo[existingIndex] = info;
+ otherUsersData[existingIndex] = userData;
+ rowManager.updateRow(existingIndex, userData);
+ }
+ else {
+ otherUsersInfo.splice(existingIndex, 1);
+ otherUsersData.splice(existingIndex, 1);
+ otherUsersInfo.splice(newIndex, 0, info);
+ otherUsersData.splice(newIndex, 0, userData);
+ rowManager.updateRow(existingIndex, userData);
+ rowManager.moveRow(existingIndex, newIndex);
+ }
+ }
+ else {
+ otherUsersInfo.splice(newIndex, 0, info);
+ otherUsersData.splice(newIndex, 0, userData);
+ rowManager.insertRow(newIndex, userData);
+ }
+
+ updateInviteNotice();
+ },
+ userLeave: function(info) {
+ var existingIndex = findExistingIndex(info.userId);
+ if (existingIndex >= 0) {
+ var userData = otherUsersData[existingIndex];
+ userData.status = 'Disconnected';
+ rowManager.updateRow(existingIndex, userData);
+ if (userData.leaveTimer) {
+ window.clearTimeout(userData.leaveTimer);
+ }
+ // set up a timer that will only fire if no leaves,
+ // joins, or updates happen for this user in the
+ // next N seconds, to remove the user from the list.
+ var thisUserId = info.userId;
+ var thisLeaveTimer = window.setTimeout(function() {
+ var newExistingIndex = findExistingIndex(thisUserId);
+ if (newExistingIndex >= 0) {
+ var newUserData = otherUsersData[newExistingIndex];
+ if (newUserData.status == 'Disconnected' &&
+ newUserData.leaveTimer == thisLeaveTimer) {
+ otherUsersInfo.splice(newExistingIndex, 1);
+ otherUsersData.splice(newExistingIndex, 1);
+ rowManager.removeRow(newExistingIndex);
+ updateInviteNotice();
+ }
+ }
+ }, 8000); // how long to wait
+ userData.leaveTimer = thisLeaveTimer;
+ }
+ updateInviteNotice();
+ },
+ showGuestPrompt: function(userId, displayName) {
+ if (knocksToIgnore[userId]) {
+ return;
+ }
+
+ var encodedUserId = padutils.encodeUserId(userId);
+
+ var actionName = 'hide-guest-prompt-'+encodedUserId;
+ padutils.cancelActions(actionName);
+
+ var box = $("#guestprompt-"+encodedUserId);
+ if (box.length == 0) {
+ // make guest prompt box
+ box = $('<div id="guestprompt-'+encodedUserId+'" class="guestprompt"><div class="choices"><a href="javascript:void(paduserlist.answerGuestPrompt(\''+encodedUserId+'\',false))">Deny</a> <a href="javascript:void(paduserlist.answerGuestPrompt(\''+encodedUserId+'\',true))">Approve</a></div><div class="guestname"><strong>Guest:</strong> '+padutils.escapeHtml(displayName)+'</div></div>');
+ $("#guestprompts").append(box);
+ }
+ else {
+ // update display name
+ box.find(".guestname").html('<strong>Guest:</strong> '+padutils.escapeHtml(displayName));
+ }
+ var hideLater = padutils.getCancellableAction(actionName, function() {
+ self.removeGuestPrompt(userId);
+ });
+ window.setTimeout(hideLater, 15000); // time-out with no knock
+
+ guestPromptFlash.scheduleAnimation();
+ },
+ removeGuestPrompt: function(userId) {
+ var box = $("#guestprompt-"+padutils.encodeUserId(userId));
+ // remove ID now so a new knock by same user gets new, unfaded box
+ box.removeAttr('id').fadeOut("fast", function() {
+ box.remove();
+ });
+
+ knocksToIgnore[userId] = true;
+ window.setTimeout(function() {
+ delete knocksToIgnore[userId];
+ }, 5000);
+ },
+ answerGuestPrompt: function(encodedUserId, approve) {
+ var guestId = padutils.decodeUserId(encodedUserId);
+
+ var msg = {
+ type: 'guestanswer',
+ authId: pad.getUserId(),
+ guestId: guestId,
+ answer: (approve ? "approved" : "denied")
+ };
+ pad.sendClientMessage(msg);
+
+ self.removeGuestPrompt(guestId);
+ }
+ };
+ return self;
+}());
+
diff --git a/etherpad/src/static/js/pad_utils.js b/etherpad/src/static/js/pad_utils.js
new file mode 100644
index 0000000..de606ad
--- /dev/null
+++ b/etherpad/src/static/js/pad_utils.js
@@ -0,0 +1,359 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var padutils = {
+ escapeHtml: function(x) {
+ return String(x).replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
+ },
+ uniqueId: function() {
+ function encodeNum(n, width) {
+ // returns string that is exactly 'width' chars, padding with zeros
+ // and taking rightmost digits
+ return (Array(width+1).join('0') + Number(n).toString(35)).slice(-width);
+ }
+ return [pad.getClientIp(),
+ encodeNum(+new Date, 7),
+ encodeNum(Math.floor(Math.random()*1e9), 4)].join('.');
+ },
+ uaDisplay: function(ua) {
+ var m;
+
+ function clean(a) {
+ var maxlen = 16;
+ a = a.replace(/[^a-zA-Z0-9\.]/g, '');
+ if (a.length > maxlen) {
+ a = a.substr(0,maxlen);
+ }
+ return a;
+ }
+
+ function checkver(name) {
+ var m = ua.match(RegExp(name + '\\/([\\d\\.]+)'));
+ if (m && m.length > 1) {
+ return clean(name+m[1]);
+ }
+ return null;
+ }
+
+ // firefox
+ if (checkver('Firefox')) { return checkver('Firefox'); }
+
+ // misc browsers, including IE
+ m = ua.match(/compatible; ([^;]+);/);
+ if (m && m.length > 1) {
+ return clean(m[1]);
+ }
+
+ // iphone
+ if (ua.match(/\(iPhone;/)) {
+ return 'iPhone';
+ }
+
+ // chrome
+ if (checkver('Chrome')) { return checkver('Chrome'); }
+
+ // safari
+ m = ua.match(/Safari\/[\d\.]+/);
+ if (m) {
+ var v = '?';
+ m = ua.match(/Version\/([\d\.]+)/);
+ if (m && m.length > 1) {
+ v = m[1];
+ }
+ return clean('Safari'+v);
+ }
+
+ // everything else
+ var x = ua.split(' ')[0];
+ return clean(x);
+ },
+ // "func" is a function over 0..(numItems-1) that is monotonically
+ // "increasing" with index (false, then true). Finds the boundary
+ // between false and true, a number between 0 and numItems inclusive.
+ binarySearch: function (numItems, func) {
+ if (numItems < 1) return 0;
+ if (func(0)) return 0;
+ if (! func(numItems-1)) return numItems;
+ var low = 0; // func(low) is always false
+ var high = numItems-1; // func(high) is always true
+ while ((high - low) > 1) {
+ var x = Math.floor((low+high)/2); // x != low, x != high
+ if (func(x)) high = x;
+ else low = x;
+ }
+ return high;
+ },
+ // e.g. "Thu Jun 18 2009 13:09"
+ simpleDateTime: function(date) {
+ var d = new Date(+date); // accept either number or date
+ var dayOfWeek = (['Sun','Mon','Tue','Wed','Thu','Fri','Sat'])[d.getDay()];
+ var month = (['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'])[d.getMonth()];
+ var dayOfMonth = d.getDate();
+ var year = d.getFullYear();
+ var hourmin = d.getHours()+":"+("0"+d.getMinutes()).slice(-2);
+ return dayOfWeek+' '+month+' '+dayOfMonth+' '+year+' '+hourmin;
+ },
+ findURLs: function(text) {
+ // copied from ACE
+ var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/;
+ var _REGEX_URLCHAR = new RegExp('('+/[-:@a-zA-Z0-9_.,~%+\/?=&#;()$]/.source+'|'+_REGEX_WORDCHAR.source+')');
+ var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source+_REGEX_URLCHAR.source+'*(?![:.,;])'+_REGEX_URLCHAR.source, 'g');
+
+ // returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
+ function _findURLs(text) {
+ _REGEX_URL.lastIndex = 0;
+ var urls = null;
+ var execResult;
+ while ((execResult = _REGEX_URL.exec(text))) {
+ urls = (urls || []);
+ var startIndex = execResult.index;
+ var url = execResult[0];
+ urls.push([startIndex, url]);
+ }
+
+ return urls;
+ }
+
+ return _findURLs(text);
+ },
+ escapeHtmlWithClickableLinks: function(text, target) {
+ var idx = 0;
+ var pieces = [];
+ var urls = padutils.findURLs(text);
+ function advanceTo(i) {
+ if (i > idx) {
+ pieces.push(padutils.escapeHtml(text.substring(idx, i)));
+ idx = i;
+ }
+ }
+ if (urls) {
+ for(var j=0;j<urls.length;j++) {
+ var startIndex = urls[j][0];
+ var href = urls[j][1];
+ advanceTo(startIndex);
+ pieces.push('<a ', (target?'target="'+target+'" ':''),
+ 'href="', href.replace(/\"/g, '&quot;'), '">');
+ advanceTo(startIndex + href.length);
+ pieces.push('</a>');
+ }
+ }
+ advanceTo(text.length);
+ return pieces.join('');
+ },
+ bindEnterAndEscape: function(node, onEnter, onEscape) {
+ function handleKey(evt) {
+ if (evt.which == 27 && onEscape) {
+ // "escape" key
+ if (evt.type == 'keydown') {
+ onEscape(evt);
+ }
+ evt.preventDefault();
+ }
+ else if (evt.which == 13 && onEnter) {
+ // return/enter
+ if (evt.type == 'keyup') {
+ onEnter(evt);
+ }
+ evt.preventDefault();
+ }
+ }
+ $(node).bind('keyup keypress keydown', handleKey);
+ },
+ timediff: function(d) {
+ function format(n, word) {
+ n = Math.round(n);
+ return ('' + n + ' ' + word + (n != 1 ? 's' : '') + ' ago');
+ }
+ d = Math.max(0, (+(new Date) - (+d) - pad.clientTimeOffset) / 1000);
+ if (d < 60) { return format(d, 'second'); }
+ d /= 60;
+ if (d < 60) { return format(d, 'minute'); }
+ d /= 60;
+ if (d < 24) { return format(d, 'hour'); }
+ d /= 24;
+ return format(d, 'day');
+ },
+ makeAnimationScheduler: function(funcToAnimateOneStep, stepTime, stepsAtOnce) {
+ if (stepsAtOnce === undefined) {
+ stepsAtOnce = 1;
+ }
+
+ var animationTimer = null;
+
+ function scheduleAnimation() {
+ if (! animationTimer) {
+ animationTimer = window.setTimeout(function() {
+ animationTimer = null;
+ var n = stepsAtOnce;
+ var moreToDo = true;
+ while (moreToDo && n > 0) {
+ moreToDo = funcToAnimateOneStep();
+ n--;
+ }
+ if (moreToDo) {
+ // more to do
+ scheduleAnimation();
+ }
+ }, stepTime*stepsAtOnce);
+ }
+ }
+ return { scheduleAnimation: scheduleAnimation };
+ },
+ makeShowHideAnimator: function(funcToArriveAtState, initiallyShown, fps, totalMs) {
+ var animationState = (initiallyShown ? 0 : -2); // -2 hidden, -1 to 0 fade in, 0 to 1 fade out
+ var animationFrameDelay = 1000 / fps;
+ var animationStep = animationFrameDelay / totalMs;
+
+ var scheduleAnimation =
+ padutils.makeAnimationScheduler(animateOneStep, animationFrameDelay).scheduleAnimation;
+
+ function doShow() {
+ animationState = -1;
+ funcToArriveAtState(animationState);
+ scheduleAnimation();
+ }
+
+ function doQuickShow() { // start showing without losing any fade-in progress
+ if (animationState < -1) {
+ animationState = -1;
+ }
+ else if (animationState <= 0) {
+ animationState = animationState;
+ }
+ else {
+ animationState = Math.max(-1, Math.min(0, - animationState));
+ }
+ funcToArriveAtState(animationState);
+ scheduleAnimation();
+ }
+
+ function doHide() {
+ if (animationState >= -1 && animationState <= 0) {
+ animationState = 1e-6;
+ scheduleAnimation();
+ }
+ }
+
+ function animateOneStep() {
+ if (animationState < -1 || animationState == 0) {
+ return false;
+ }
+ else if (animationState < 0) {
+ // animate show
+ animationState += animationStep;
+ if (animationState >= 0) {
+ animationState = 0;
+ funcToArriveAtState(animationState);
+ return false;
+ }
+ else {
+ funcToArriveAtState(animationState);
+ return true;
+ }
+ }
+ else if (animationState > 0) {
+ // animate hide
+ animationState += animationStep;
+ if (animationState >= 1) {
+ animationState = 1;
+ funcToArriveAtState(animationState);
+ animationState = -2;
+ return false;
+ }
+ else {
+ funcToArriveAtState(animationState);
+ return true;
+ }
+ }
+ }
+
+ return {show: doShow, hide: doHide, quickShow: doQuickShow};
+ },
+ _nextActionId: 1,
+ uncanceledActions: {},
+ getCancellableAction: function(actionType, actionFunc) {
+ var o = padutils.uncanceledActions[actionType];
+ if (! o) {
+ o = {};
+ padutils.uncanceledActions[actionType] = o;
+ }
+ var actionId = (padutils._nextActionId++);
+ o[actionId] = true;
+ return function() {
+ var p = padutils.uncanceledActions[actionType];
+ if (p && p[actionId]) {
+ actionFunc();
+ }
+ };
+ },
+ cancelActions: function(actionType) {
+ var o = padutils.uncanceledActions[actionType];
+ if (o) {
+ // clear it
+ delete padutils.uncanceledActions[actionType];
+ }
+ },
+ makeFieldLabeledWhenEmpty: function(field, labelText) {
+ field = $(field);
+ function clear() {
+ field.addClass('editempty');
+ field.val(labelText);
+ }
+ field.focus(function() {
+ if (field.hasClass('editempty')) {
+ field.val('');
+ }
+ field.removeClass('editempty');
+ });
+ field.blur(function() {
+ if (! field.val()) {
+ clear();
+ }
+ });
+ return {clear:clear};
+ },
+ getCheckbox: function(node) {
+ return $(node).is(':checked');
+ },
+ setCheckbox: function(node, value) {
+ if (value) {
+ $(node).attr('checked', 'checked');
+ }
+ else {
+ $(node).removeAttr('checked');
+ }
+ },
+ bindCheckboxChange: function(node, func) {
+ $(node).bind("click change", func);
+ },
+ encodeUserId: function(userId) {
+ return userId.replace(/[^a-y0-9]/g, function(c) {
+ if (c == ".") return "-";
+ return 'z'+c.charCodeAt(0)+'z';
+ });
+ },
+ decodeUserId: function(encodedUserId) {
+ return encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, function(cc) {
+ if (cc == '-') return '.';
+ else if (cc.charAt(0) == 'z') {
+ return String.fromCharCode(Number(cc.slice(1,-1)));
+ }
+ else {
+ return cc;
+ }
+ });
+ }
+};
diff --git a/etherpad/src/static/js/plugins.js b/etherpad/src/static/js/plugins.js
new file mode 100644
index 0000000..f7a5990
--- /dev/null
+++ b/etherpad/src/static/js/plugins.js
@@ -0,0 +1,22 @@
+plugins = {
+ callHook: function (hookName, args) {
+ var hook = clientVars.hooks[hookName];
+ if (hook === undefined)
+ return [];
+ var res = [];
+ for (var i = 0, N=hook.length; i < N; i++) {
+ var plugin = hook[i];
+ var pluginRes = eval(plugin.plugin)[plugin.original || hookName](args);
+ if (pluginRes != undefined && pluginRes != null)
+ res = res.concat(pluginRes);
+ }
+ return res;
+ },
+
+ callHookStr: function (hookName, args, sep, pre, post) {
+ if (sep == undefined) sep = '';
+ if (pre == undefined) pre = '';
+ if (post == undefined) post = '';
+ return callHook(hookName, args).map(function (x) { return pre + x + post}).join(sep || "");
+ }
+};
diff --git a/etherpad/src/static/js/pricing.js b/etherpad/src/static/js/pricing.js
new file mode 100644
index 0000000..913d5ce
--- /dev/null
+++ b/etherpad/src/static/js/pricing.js
@@ -0,0 +1,19 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+$(function() {
+ $('#buylink').click(function() { window.location = "/ep/store/eepnet-checkout"; return false; });
+}); \ No newline at end of file
diff --git a/etherpad/src/static/js/pro/guest-knock-client.js b/etherpad/src/static/js/pro/guest-knock-client.js
new file mode 100644
index 0000000..bace225
--- /dev/null
+++ b/etherpad/src/static/js/pro/guest-knock-client.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+function knock() {
+ $.ajax({
+ type: "POST",
+ url: "/ep/account/guest-knock",
+ cache: false,
+ data: {
+ padId: clientVars.localPadId,
+ guestDisplayName: clientVars.guestDisplayName
+ },
+ success: knockReply,
+ error: knockError
+ });
+}
+
+function knockReply(responseText) {
+ //console.log("knockReply: "+responseText);
+ if (responseText == "approved") {
+ window.location.href = clientVars.padUrl;
+ }
+ if (responseText == "denied") {
+ $("#guest-knock-box").hide();
+ $("#guest-knock-denied").show();
+ }
+ if (responseText == "wait") {
+ setTimeout(knock, 1000);
+ }
+}
+
+function knockError() {
+ alert("There was an error requesting access to the pad. Kindly report this by sending email to bugs@pad.spline.inf.fu-berlin.de.");
+}
+
+$(document).ready(function() {
+ knock();
+});
+
diff --git a/etherpad/src/static/js/pro/pro-padlist-client.js b/etherpad/src/static/js/pro/pro-padlist-client.js
new file mode 100644
index 0000000..ba50d95
--- /dev/null
+++ b/etherpad/src/static/js/pro/pro-padlist-client.js
@@ -0,0 +1,104 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+if (!window.etherpad) {
+ etherpad = {};
+}
+if (!window.etherpad.pro) {
+ etherpad.pro = {};
+}
+
+etherpad.pro.padlist = {};
+
+$(document).ready(function() {
+
+ function getTargetPadId(target) {
+ var padmetaId = $(target).attr('id').split('-')[2];
+ //console.log("padmetaId = "+padmetaId);
+ return clientVars.localPadIds[padmetaId];
+ }
+
+ var padActionsMenu = [
+ {"View Read-Only": {
+ onclick: function(menuItem, menu) {
+ var localPadId = getTargetPadId(menu.target);
+ window.location.href = ("/ep/pad/view/"+localPadId+"/latest");
+ },
+ icon: '/static/img/pro/padlist/paper-icon.gif'
+ }
+ },
+ $.contextMenu.separator,
+ {"Archive": {
+ onclick: function(menuItem, menu) {
+ var localPadId = getTargetPadId(menu.target);
+ etherpad.pro.padlist.toggleArchivePad(localPadId);
+ }
+ }
+ },
+ {"Delete": {
+ onclick: function(menuItem, menu) {
+ var localPadId = getTargetPadId(menu.target);
+ etherpad.pro.padlist.deletePad(localPadId);
+ },
+ icon: '/static/img/pro/padlist/trash-icon.gif'
+ }
+ }
+ ];
+
+ if (clientVars.showingArchivedPads) {
+ padActionsMenu[2]["Un-archive"] = padActionsMenu[2]["Archive"];
+ delete padActionsMenu[2]["Archive"];
+ }
+
+ $('.gear-drop').contextMenu(padActionsMenu, {
+ theme: 'gloss,gloss-cyan',
+ bindTarget: 'click',
+ beforeShow: function() {
+ var localPadId = getTargetPadId(this.target);
+ $('tr.selected').removeClass('selected');
+ $('tr#pad-row-'+localPadId).addClass('selected');
+ return true;
+ },
+ hideCallback: function() {
+ var localPadId = getTargetPadId(this.target);
+ $('tr#pad-row-'+localPadId).removeClass('selected');
+ }
+ });
+});
+
+etherpad.pro.padlist.deletePad = function(localPadId) {
+ if (!confirm("Are you sure you want to delete the pad \""+clientVars.padTitles[localPadId]+"\"?")) {
+ return;
+ }
+
+ var inp = $("#padIdToDelete");
+ inp.val(localPadId);
+
+ // sanity check
+ if (! (inp.val() == localPadId)) {
+ alert("Error: "+inp.val());
+ return;
+ }
+
+ $("#delete-pad").submit();
+};
+
+etherpad.pro.padlist.toggleArchivePad = function(localPadId) {
+ var inp = $("#padIdToToggleArchive");
+ inp.val(localPadId);
+ $("#toggle-archive-pad").submit();
+};
+
diff --git a/etherpad/src/static/js/pro/signin-client.js b/etherpad/src/static/js/pro/signin-client.js
new file mode 100644
index 0000000..62847e5
--- /dev/null
+++ b/etherpad/src/static/js/pro/signin-client.js
@@ -0,0 +1,27 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+$(document).ready(function() {
+ if ($("#signin-form").length > 0) {
+ $("#email").focus();
+ }
+ if ($("#guest-signin-form").length > 0) {
+ $("#guestDisplayName").focus();
+ }
+});
+
+
diff --git a/etherpad/src/static/js/pulse.jquery.js b/etherpad/src/static/js/pulse.jquery.js
new file mode 100644
index 0000000..b23aede
--- /dev/null
+++ b/etherpad/src/static/js/pulse.jquery.js
@@ -0,0 +1,105 @@
+/**
+ * jQuery.pulse
+ * Copyright (c) 2008 James Padolsey - jp(at)qd9(dot)co.uk | http://james.padolsey.com / http://enhance.qd-creative.co.uk
+ * Dual licensed under MIT and GPL.
+ * Date: 05/11/08
+ *
+ * @projectDescription Applies a continual pulse to any element specified
+ * http://enhance.qd-creative.co.uk/demos/pulse/
+ * Tested successfully with jQuery 1.2.6. On FF 2/3, IE 6/7, Opera 9.5 and Safari 3. on Windows XP.
+ *
+ * @author James Padolsey
+ * @version 1.11
+ *
+ * @id jQuery.pulse
+ * @id jQuery.recover
+ * @id jQuery.fn.pulse
+ * @id jQuery.fn.recover
+ */
+(function($){
+ $.fn.recover = function() {
+ /* Empty inline styles - i.e. set element back to previous state */
+ /* Note, the recovery might not work properly if you had inline styles set before pulse initiation */
+ return this.each(function(){$(this).stop().css({backgroundColor:'',color:'',borderLeftColor:'',borderRightColor:'',borderTopColor:'',borderBottomColor:'',opacity:1});});
+ }
+ $.fn.pulse = function(options){
+ var defaultOptions = {
+ textColors: [],
+ backgroundColors: [],
+ borderColors: [],
+ opacityPulse: true,
+ opacityRange: [],
+ speed: 1000,
+ duration: false,
+ runLength: false
+ }, o = $.extend(defaultOptions,options);
+ /* Validate custom options */
+ if(o.textColors.length===1||o.backgroundColors.length===1||o.borderColors.length===1) {return false;}
+ /* Begin: */
+ return this.each(function(){
+ var $t = $(this), pulseCount=1, pulseLimit = (o.runLength&&o.runLength>0) ? o.runLength*largestArrayLength([o.textColors.length,o.backgroundColors.length,o.borderColors.length,o.opacityRange.length]) : false;
+ clearTimeout(recover);
+ if(o.duration) {
+ setTimeout(recover,o.duration);
+ }
+ function nudgePulse(textColorIndex,bgColorIndex,borderColorIndex,opacityIndex) {
+ if(pulseLimit&&pulseCount===pulseLimit) {
+ return $t.recover();
+ }
+ pulseCount++;
+ /* Initiate color change - on callback continue */
+ return $t.animate(getColorsAtIndex(textColorIndex,bgColorIndex,borderColorIndex,opacityIndex),o.speed,function(){
+ /* Callback of each step */
+ nudgePulse(
+ getNextIndex(o.textColors,textColorIndex),
+ getNextIndex(o.backgroundColors,bgColorIndex),
+ getNextIndex(o.borderColors,borderColorIndex),
+ getNextIndex(o.opacityRange,opacityIndex)
+ );
+ });
+ }
+ /* Set CSS to first step (no animation) */
+ $t.css(getColorsAtIndex(0,0,0,0));
+ /* Then animate to second step */
+ nudgePulse(1,1,1,1);
+ function getColorsAtIndex(textColorIndex,bgColorIndex,borderColorIndex,opacityIndex) {
+ /* Prepare animation object - get's all property names/values from passed indexes */
+ var params = {};
+ if(o.backgroundColors.length) {
+ params['backgroundColor'] = o.backgroundColors[bgColorIndex];
+ }
+ if(o.textColors.length) {
+ params['color'] = o.textColors[textColorIndex];
+ }
+ if(o.borderColors.length) {
+ params['borderLeftColor'] = o.borderColors[borderColorIndex];
+ params['borderRightColor'] = o.borderColors[borderColorIndex];
+ params['borderTopColor'] = o.borderColors[borderColorIndex];
+ params['borderBottomColor'] = o.borderColors[borderColorIndex];
+ }
+ if(o.opacityPulse&&o.opacityRange.length) {
+ params['opacity'] = o.opacityRange[opacityIndex];
+ }
+ return params;
+ }
+ function getNextIndex(property,currentIndex) {
+ if (property.length>currentIndex+1) {return currentIndex+1;}
+ else {return 0;}
+ }
+ function largestArrayLength(arrayOfArrays) {
+ return Math.max.apply( Math, arrayOfArrays );
+ }
+ function recover() {
+ $t.recover();
+ }
+ });
+ }
+})(jQuery);
+/* The below code extends the animate function so that it works with color animations */
+/* By John Resig */
+(function(jQuery){
+jQuery.each(['backgroundColor','borderBottomColor','borderLeftColor','borderRightColor','borderTopColor','color','outlineColor'],function(i,attr){jQuery.fx.step[attr]=function(fx){if(fx.state==0){fx.start=getColor(fx.elem,attr);fx.end=getRGB(fx.end)}fx.elem.style[attr]="rgb("+[Math.max(Math.min(parseInt((fx.pos*(fx.end[0]-fx.start[0]))+fx.start[0]),255),0),Math.max(Math.min(parseInt((fx.pos*(fx.end[1]-fx.start[1]))+fx.start[1]),255),0),Math.max(Math.min(parseInt((fx.pos*(fx.end[2]-fx.start[2]))+fx.start[2]),255),0)].join(",")+")"}});
+function getRGB(color){var result;if(color&&color.constructor==Array&&color.length==3)return color;if(result=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)){return[parseInt(result[1]),parseInt(result[2]),parseInt(result[3])]}if(result=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)){return[parseFloat(result[1])*2.55,parseFloat(result[2])*2.55,parseFloat(result[3])*2.55]}if(result=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)){return[parseInt(result[1],16),parseInt(result[2],16),parseInt(result[3],16)]}if(result=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)){return[parseInt(result[1]+result[1],16),parseInt(result[2]+result[2],16),parseInt(result[3]+result[3],16)]}return colors[jQuery.trim(color).toLowerCase()]}
+function getColor(elem,attr){var color;do{color=jQuery.curCSS(elem,attr);if(color!=''&&color!='transparent'||jQuery.nodeName(elem,"body")){break}attr="backgroundColor"}while(elem=elem.parentNode);return getRGB(color)};
+var colors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]};
+})(jQuery); \ No newline at end of file
diff --git a/etherpad/src/static/js/statpage.js b/etherpad/src/static/js/statpage.js
new file mode 100644
index 0000000..be2948c
--- /dev/null
+++ b/etherpad/src/static/js/statpage.js
@@ -0,0 +1,143 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+$(function() {
+ $('.title').click(function() {
+ var id = $(this).parent().attr("id")
+ toggleId(id);
+ // $(this).parent().children('.statbody').toggle();
+ });
+ $('.statbody').each(function() {
+ if (isVisible($(this).parent().attr("id"))) {
+ $(this).show();
+ }
+ });
+ var cat = window.location.hash.slice(1);
+ if (! cat) {
+ cat = "health";
+ }
+ $('.navlink').click(function() {
+ var cat = $(this).attr("id").slice(4);
+ showCategory(cat);
+ });
+ showCategory(cat);
+});
+
+function showCategory(cat) {
+ $('#fragment').val(cat);
+ $('.categorywrapper').each(function() {
+ var localCat = $(this).attr("id").slice(3);
+ if (localCat == cat) {
+ $('#link'+localCat).parent().addClass("selected");
+ $(this).show();
+ } else {
+ $('#link'+localCat).parent().removeClass("selected");
+ $(this).hide();
+ }
+ })
+}
+
+function formChanged() {
+ document.forms[0].submit();
+}
+
+if (! String.prototype.startsWith) {
+ String.prototype.startsWith = function(s) {
+ if (this.length < s.length) { return false; }
+ return this.substr(0, s.length) == s;
+ }
+}
+
+if (! String.prototype.trim) {
+ String.prototype.trim = function() {
+ var firstNonSpace;
+ for (var i = 0; i < this.length; ++i) {
+ if (this[i] != ' ') {
+ firstNonSpace = i;
+ break;
+ }
+ }
+ var s = this;
+ if (firstNonSpace) {
+ s = this.substr(firstNonSpace);
+ }
+ var lastNonSpace;
+ for (var i = this.length-1; i >= 0; --i) {
+ if (this[i] != ' ') {
+ lastNonSpace = i;
+ break;
+ }
+ }
+ if (lastNonSpace !== undefined) {
+ s = s.substr(0, lastNonSpace+1);
+ }
+ return s;
+ }
+}
+
+if (! Array.prototype.contains) {
+ Array.prototype.contains = function(obj) {
+ for (var i = 0; i < this.length; ++i) {
+ if (this[i] == obj) return true;
+ }
+ return false;
+ }
+}
+
+if (! Array.prototype.first) {
+ Array.prototype.first = function(f) {
+ for (var i = 0; i < this.length; ++i) {
+ if (f(this[i])) {
+ return this[i];
+ }
+ }
+ }
+}
+
+var cookieprefix = "visiblestats="
+
+function statsCookieValue() {
+ return (document.cookie.split(";").map(function(s) { return s.trim() }).first(function(str) {
+ return str.startsWith(cookieprefix);
+ }) || cookieprefix).split("=")[1];
+}
+
+function isVisible(id) {
+ var cookieValue = statsCookieValue();
+ return ! (cookieValue.split("-").contains(id));
+}
+
+function rememberHidden(id) {
+ if (! isVisible(id)) { return; }
+ document.cookie = cookieprefix+
+ statsCookieValue().split("-").concat([id]).join("-");
+}
+
+function rememberVisible(id) {
+ if (isVisible(id)) { return; }
+ document.cookie = cookieprefix+
+ statsCookieValue().split("-").filter(function(obj) { return obj != id }).join("-");
+}
+
+function toggleId(id) {
+ var body = $('#'+id).children('.statbody');
+ body.toggle();
+ if (body.is(":visible")) {
+ rememberVisible(id);
+ } else {
+ rememberHidden(id);
+ }
+} \ No newline at end of file
diff --git a/etherpad/src/static/js/store.js b/etherpad/src/static/js/store.js
new file mode 100644
index 0000000..5750f42
--- /dev/null
+++ b/etherpad/src/static/js/store.js
@@ -0,0 +1,116 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+store = {};
+
+$(document).ready(function() {
+ if ($('#downloadpage').size() > 0) {
+ $("#license_agree, #license_agree_label").click(function() {
+ if ($("#license_agree").attr("checked")) {
+ $("a.downloadbutton_disabled").removeClass("downloadbutton_disabled")
+ .addClass("downloadbutton")
+ .attr('href', '/ep/store/eepnet-download-nextsteps');
+ } else {
+ $("a.downloadbutton").removeClass("downloadbutton")
+ .addClass("downloadbutton_disabled")
+ .attr('href', 'javascript:void store.mustAgree()');
+ }
+ });
+ }
+
+ if ($('#eepnet_trial_signup_page').size() > 0) {
+ store.eepnetTrial.init();
+ }
+
+});
+
+store.mustAgree = function() {
+ alert("You must first click 'Accept License' before downloading this software.");
+};
+
+//----------------------------------------------------------------
+// trial download page
+//----------------------------------------------------------------
+
+store.eepnetTrial = {};
+
+store.eepnetTrial.init = function() {
+ $("#submit").attr("disabled", false);
+ $("input.signupData").keydown(function() {
+ $("#submit").attr("disabled", false);
+ });
+ $("input.signupData").change(function() {
+ $("#submit").attr("disabled", false);
+ });
+};
+
+store.eepnetTrial.handleError = function(msg) {
+ $('#processingmsg').hide();
+ $('#dlsignup').show();
+ $("#errormsg").hide().html(msg).fadeIn("fast");
+ var href = window.location.href;
+ href = href.split("#")[0];
+ window.location.href = (href + "#toph2");
+ $('#submit').attr('disabled', false);
+};
+
+store.eepnetTrial.submit = function() {
+
+ $("#errormsg").hide();
+ $('#dlsignup').hide();
+ $('#processingmsg').fadeIn('fast');
+
+ // first submit...
+ var data = {};
+ $(".signupData").each(function() {
+ data[$(this).attr("id")] = $(this).val();
+ });
+ data.industry = $('#industry').val();
+
+ $('#submit').attr('disabled', true);
+
+ $.ajax({
+ type: 'post',
+ url: '/ep/store/eepnet-eval-signup',
+ data: data,
+ success: success,
+ error: error
+ });
+
+ function success(text) {
+ var responseData = eval("("+text+")");
+ if (responseData.error) {
+ store.eepnetTrial.handleError(responseData.error);
+ return;
+ }
+
+ store.eepnetTrial.submitWebToLead(responseData);
+ }
+
+ function error(e) {
+ store.eepnetTrial.handleError("Oops! There was an error processing your request.");
+ }
+};
+
+store.eepnetTrial.submitWebToLead = function(data) {
+ for (k in data) {
+ $('#wl_'+k).val(data[k]);
+ }
+ setTimeout(function() { $('#wlform').submit(); }, 50);
+};
+
+
diff --git a/etherpad/src/static/js/swfobject.js b/etherpad/src/static/js/swfobject.js
new file mode 100644
index 0000000..b741304
--- /dev/null
+++ b/etherpad/src/static/js/swfobject.js
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * SWFObject v1.5: Flash Player detection and embed - http://blog.deconcept.com/swfobject/
+ *
+ * SWFObject is (c) 2007 Geoff Stearns and is released under the MIT License:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ */
+if(typeof deconcept=="undefined"){var deconcept=new Object();}if(typeof deconcept.util=="undefined"){deconcept.util=new Object();}if(typeof deconcept.SWFObjectUtil=="undefined"){deconcept.SWFObjectUtil=new Object();}deconcept.SWFObject=function(_1,id,w,h,_5,c,_7,_8,_9,_a){if(!document.getElementById){return;}this.DETECT_KEY=_a?_a:"detectflash";this.skipDetect=deconcept.util.getRequestParameter(this.DETECT_KEY);this.params=new Object();this.variables=new Object();this.attributes=new Array();if(_1){this.setAttribute("swf",_1);}if(id){this.setAttribute("id",id);}if(w){this.setAttribute("width",w);}if(h){this.setAttribute("height",h);}if(_5){this.setAttribute("version",new deconcept.PlayerVersion(_5.toString().split(".")));}this.installedVer=deconcept.SWFObjectUtil.getPlayerVersion();if(!window.opera&&document.all&&this.installedVer.major>7){deconcept.SWFObject.doPrepUnload=true;}if(c){this.addParam("bgcolor",c);}var q=_7?_7:"high";this.addParam("quality",q);this.setAttribute("useExpressInstall",false);this.setAttribute("doExpressInstall",false);var _c=(_8)?_8:window.location;this.setAttribute("xiRedirectUrl",_c);this.setAttribute("redirectUrl","");if(_9){this.setAttribute("redirectUrl",_9);}};deconcept.SWFObject.prototype={useExpressInstall:function(_d){this.xiSWFPath=!_d?"expressinstall.swf":_d;this.setAttribute("useExpressInstall",true);},setAttribute:function(_e,_f){this.attributes[_e]=_f;},getAttribute:function(_10){return this.attributes[_10];},addParam:function(_11,_12){this.params[_11]=_12;},getParams:function(){return this.params;},addVariable:function(_13,_14){this.variables[_13]=_14;},getVariable:function(_15){return this.variables[_15];},getVariables:function(){return this.variables;},getVariablePairs:function(){var _16=new Array();var key;var _18=this.getVariables();for(key in _18){_16[_16.length]=key+"="+_18[key];}return _16;},getSWFHTML:function(){var _19="";if(navigator.plugins&&navigator.mimeTypes&&navigator.mimeTypes.length){if(this.getAttribute("doExpressInstall")){this.addVariable("MMplayerType","PlugIn");this.setAttribute("swf",this.xiSWFPath);}_19="<embed type=\"application/x-shockwave-flash\" src=\""+this.getAttribute("swf")+"\" width=\""+this.getAttribute("width")+"\" height=\""+this.getAttribute("height")+"\" style=\""+this.getAttribute("style")+"\"";_19+=" id=\""+this.getAttribute("id")+"\" name=\""+this.getAttribute("id")+"\" ";var _1a=this.getParams();for(var key in _1a){_19+=[key]+"=\""+_1a[key]+"\" ";}var _1c=this.getVariablePairs().join("&");if(_1c.length>0){_19+="flashvars=\""+_1c+"\"";}_19+="/>";}else{if(this.getAttribute("doExpressInstall")){this.addVariable("MMplayerType","ActiveX");this.setAttribute("swf",this.xiSWFPath);}_19="<object id=\""+this.getAttribute("id")+"\" classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" width=\""+this.getAttribute("width")+"\" height=\""+this.getAttribute("height")+"\" style=\""+this.getAttribute("style")+"\">";_19+="<param name=\"movie\" value=\""+this.getAttribute("swf")+"\" />";var _1d=this.getParams();for(var key in _1d){_19+="<param name=\""+key+"\" value=\""+_1d[key]+"\" />";}var _1f=this.getVariablePairs().join("&");if(_1f.length>0){_19+="<param name=\"flashvars\" value=\""+_1f+"\" />";}_19+="</object>";}return _19;},write:function(_20){if(this.getAttribute("useExpressInstall")){var _21=new deconcept.PlayerVersion([6,0,65]);if(this.installedVer.versionIsValid(_21)&&!this.installedVer.versionIsValid(this.getAttribute("version"))){this.setAttribute("doExpressInstall",true);this.addVariable("MMredirectURL",escape(this.getAttribute("xiRedirectUrl")));document.title=document.title.slice(0,47)+" - Flash Player Installation";this.addVariable("MMdoctitle",document.title);}}if(this.skipDetect||this.getAttribute("doExpressInstall")||this.installedVer.versionIsValid(this.getAttribute("version"))){var n=(typeof _20=="string")?document.getElementById(_20):_20;n.innerHTML=this.getSWFHTML();return true;}else{if(this.getAttribute("redirectUrl")!=""){document.location.replace(this.getAttribute("redirectUrl"));}}return false;}};deconcept.SWFObjectUtil.getPlayerVersion=function(){var _23=new deconcept.PlayerVersion([0,0,0]);if(navigator.plugins&&navigator.mimeTypes.length){var x=navigator.plugins["Shockwave Flash"];if(x&&x.description){_23=new deconcept.PlayerVersion(x.description.replace(/([a-zA-Z]|\s)+/,"").replace(/(\s+r|\s+b[0-9]+)/,".").split("."));}}else{if(navigator.userAgent&&navigator.userAgent.indexOf("Windows CE")>=0){var axo=1;var _26=3;while(axo){try{_26++;axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash."+_26);_23=new deconcept.PlayerVersion([_26,0,0]);}catch(e){axo=null;}}}else{try{var axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");}catch(e){try{var axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");_23=new deconcept.PlayerVersion([6,0,21]);axo.AllowScriptAccess="always";}catch(e){if(_23.major==6){return _23;}}try{axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");}catch(e){}}if(axo!=null){_23=new deconcept.PlayerVersion(axo.GetVariable("$version").split(" ")[1].split(","));}}}return _23;};deconcept.PlayerVersion=function(_29){this.major=_29[0]!=null?parseInt(_29[0]):0;this.minor=_29[1]!=null?parseInt(_29[1]):0;this.rev=_29[2]!=null?parseInt(_29[2]):0;};deconcept.PlayerVersion.prototype.versionIsValid=function(fv){if(this.major<fv.major){return false;}if(this.major>fv.major){return true;}if(this.minor<fv.minor){return false;}if(this.minor>fv.minor){return true;}if(this.rev<fv.rev){return false;}return true;};deconcept.util={getRequestParameter:function(_2b){var q=document.location.search||document.location.hash;if(_2b==null){return q;}if(q){var _2d=q.substring(1).split("&");for(var i=0;i<_2d.length;i++){if(_2d[i].substring(0,_2d[i].indexOf("="))==_2b){return _2d[i].substring((_2d[i].indexOf("=")+1));}}}return "";}};deconcept.SWFObjectUtil.cleanupSWFs=function(){var _2f=document.getElementsByTagName("OBJECT");for(var i=_2f.length-1;i>=0;i--){_2f[i].style.display="none";for(var x in _2f[i]){if(typeof _2f[i][x]=="function"){_2f[i][x]=function(){};}}}};if(deconcept.SWFObject.doPrepUnload){if(!deconcept.unloadSet){deconcept.SWFObjectUtil.prepUnload=function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){};window.attachEvent("onunload",deconcept.SWFObjectUtil.cleanupSWFs);};window.attachEvent("onbeforeunload",deconcept.SWFObjectUtil.prepUnload);deconcept.unloadSet=true;}}if(!document.getElementById&&document.all){document.getElementById=function(id){return document.all[id];};}var getQueryParamValue=deconcept.util.getRequestParameter;var FlashObject=deconcept.SWFObject;var SWFObject=deconcept.SWFObject; \ No newline at end of file
diff --git a/etherpad/src/static/js/timeslider.js b/etherpad/src/static/js/timeslider.js
new file mode 100644
index 0000000..552a971
--- /dev/null
+++ b/etherpad/src/static/js/timeslider.js
@@ -0,0 +1,663 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+
+
+function repeatString(str, times) {
+ if (times <= 0) return "";
+ var s = repeatString(str, times >> 1);
+ s += s;
+ if (times & 1) s += str;
+ return s;
+}
+function chr(n) { return String.fromCharCode(n+48); }
+function ord(c) { return c.charCodeAt(0)-48; }
+
+function map(array, func) {
+ var result = [];
+ // must remain compatible with "arguments" pseudo-array
+ for(var i=0;i<array.length;i++) {
+ if (func) result.push(func(array[i], i));
+ else result.push(array[i]);
+ }
+ return result;
+}
+
+function forEach(array, func) {
+ for(var i=0;i<array.length;i++) {
+ var result = func(array[i], i);
+ if (result) break;
+ }
+}
+
+function getText(padOpaqueRef, r, func/*(text, optErrorData)*/) {
+ doAjaxGet('/ep/pad/history/'+padOpaqueRef+'/text/'+Number(r),
+ function(data, optErrorData) {
+ if (optErrorData) {
+ func(null, optErrorData);
+ }
+ else {
+ var text = data.text;
+ func({text: text});
+ }
+ });
+}
+
+function getChanges(padOpaqueRef, first, last, func/*(data, optErrorData)*/) {
+ doAjaxGet('/ep/pad/history/'+padOpaqueRef+'/changes/'+Number(first)+'-'+Number(last),
+ function(data, optErrorData) {
+ if (optErrorData) {
+ func(null, optErrorData);
+ }
+ else {
+ func(uncompressChangesBlock({charPool: data.charPool,
+ changes: data.changes,
+ firstRev: first}));
+ }
+ });
+}
+
+function statPad(padOpaqueRef, func/*(atext, optErrorData)*/) {
+ doAjaxGet('/ep/pad/history/'+padOpaqueRef+'/stat',
+ function(data, optErrorData) {
+ if (optErrorData) {
+ func(null, optErrorData);
+ }
+ else {
+ var obj = {exists: data.exists};
+ if (obj.exists) {
+ obj.latestRev = data.latestRev;
+ }
+
+ func(obj);
+ }
+ });
+}
+
+function doAjaxGet(url, func/*(data, optErrorData)*/) {
+ $.ajax({
+ type: 'get',
+ dataType: 'json',
+ url: url,
+ success: function(data) {
+ if (data.error) {
+ func(null, {serverError: data});
+ }
+ else {
+ func(data);
+ }
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ func(null, {clientError: { textStatus:textStatus, errorThrown: errorThrown }});
+ }
+ });
+}
+
+function uncompressChangesBlock(data) {
+ var charPool = data.charPool;
+ var changesArray = data.changes.split(',');
+ var firstRev = data.firstRev;
+
+ var changesBlock = {};
+ var changeStructs = [];
+ var charPoolIndex = 0;
+ var lastTimestamp = 0;
+ for(var i=0;i<changesArray.length;i++) {
+ var receiver = [null, 0];
+ var curString = changesArray[i];
+ function nextChar() {
+ return curString.charAt(receiver[1]);
+ }
+ function readChar() {
+ var c = nextChar();
+ receiver[1]++;
+ return c;
+ }
+ function readNum() {
+ return decodeVarInt(curString, receiver[1], receiver);
+ }
+ function readString() {
+ var len = readNum();
+ var str = charPool.substr(charPoolIndex, len);
+ charPoolIndex += len;
+ return str;
+ }
+ function readTimestamp() {
+ var absolute = false;
+ if (nextChar() == "+") {
+ readChar();
+ absolute = true;
+ }
+ var t = readNum()*1000;
+ if (! absolute) {
+ t += lastTimestamp;
+ }
+ lastTimestamp = t;
+ return t;
+ }
+ function atEnd() {
+ return receiver[1] >= curString.length;
+ }
+ var timestamp = readTimestamp();
+ var authorNum = readNum();
+ var splices = [];
+ while (! atEnd()) {
+ var spliceType = readChar();
+ var startChar = readNum();
+ var oldText = "";
+ var newText = "";
+ if (spliceType != '+') {
+ oldText = readString();
+ }
+ if (spliceType != '-') {
+ newText = readString();
+ }
+ splices.push([startChar,oldText,newText]);
+ }
+ changeStructs.push({t:timestamp, a:authorNum, splices:splices});
+ }
+
+ changesBlock.firstRev = firstRev;
+ changesBlock.changeStructs = changeStructs;
+
+ return changesBlock;
+}
+
+var BASE64_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._";
+var BASE64_DIGIT_TO_NUM = (function() {
+ var map = {};
+ for(var i=0;i<BASE64_DIGITS.length;i++) {
+ map[BASE64_DIGITS.charAt(i)] = i;
+ }
+ return map;
+})();
+
+function decodeVarInt(stringIn, indexIn, numAndIndexOut) {
+ var n = 0;
+ var done = false;
+ var i = indexIn;
+ while (! done) {
+ var d = + BASE64_DIGIT_TO_NUM[stringIn.charAt(i++)];
+ if (isNaN(d)) return -1;
+ if ((d & 32) == 0) {
+ done = true;
+ }
+ n = n*32 + (d & 31);
+ }
+ if (numAndIndexOut) {
+ numAndIndexOut[0] = n;
+ numAndIndexOut[1] = i;
+ }
+ return n;
+}
+
+function escapeHTML(s) {
+ var re = /[&<>\n]/g;
+ if (! re.MAP) {
+ // persisted across function calls!
+ re.MAP = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '\n': '<br/>'
+ };
+ }
+ return s.replace(re, function(c) { return re.MAP[c]; });
+}
+
+var padOpaqueRef = clientVars.padOpaqueRef;
+var keyframes = []; // [rev, atext] pairs
+var changesBlocks = []; // [first, last, changesBlock]
+var lastRev;
+var lastRevLoaded = -1;
+var problemData = null;
+var curRev = -1;
+var curText = { lines: [/*string, length+1*/] };
+
+function setLastRevLoaded(r) {
+ lastRevLoaded = r;
+ //$("#sliderui").slider('option', 'max', lastRevLoaded);
+ $("#currevdisplay .max").html(String(lastRevLoaded));
+}
+
+function initialStat(continuation) {
+ statPad(padOpaqueRef, function(data, errorData) {
+ if (errorData) {
+ reportProblem(errorData);
+ continuation(false);
+ }
+ else {
+ if (! data.exists) {
+ reportProblem({msg: "Pad not found."});
+ continuation(false);
+ }
+ else {
+ lastRev = data.latestRev;
+ continuation(true);
+ return;
+ }
+ }
+ });
+}
+
+function loadKeyframe(r, continuation) {
+ getText(padOpaqueRef, r, function(data, errorData) {
+ if (errorData) {
+ reportProblem(errorData);
+ continuation(false);
+ }
+ else {
+ keyframes.push([r, data]);
+ keyframes.sort(function(a, b) {
+ return a[0] - b[0];
+ });
+ continuation(true);
+ }
+ });
+}
+
+function loadChangesBlock(first, last, continuation) {
+ getChanges(padOpaqueRef, first, last, function(data, errorData) {
+ if (errorData) {
+ reportProblem(errorData);
+ continuation(false);
+ }
+ else {
+ changesBlocks.push([first, last, data]);
+ continuation(true);
+ }
+ });
+}
+
+function loadThroughZero(continuation) {
+ initialStat(function(success) {
+ if (success) {
+ loadKeyframe(0, function(success) {
+ if (success) {
+ setLastRevLoaded(0);
+ continuation(true);
+ }
+ else continuation(false);
+ });
+ }
+ else continuation(false);
+ });
+}
+
+function loadMoreRevs(continuation) {
+ if (lastRevLoaded >= lastRev) {
+ continuation(true);
+ }
+ else {
+ var first = lastRevLoaded+1;
+ var last = first + 499;
+ if (last > lastRev) {
+ last = lastRev;
+ }
+ loadChangesBlock(first, last, function(success) {
+ if (success) {
+ loadKeyframe(last, function(success) {
+ if (success) {
+ setLastRevLoaded(last);
+ continuation(true);
+ }
+ else continuation(false);
+ });
+ }
+ else continuation(false);
+ });
+ }
+}
+
+function getDocTextForText(text) {
+ var lines = map(text.split('\n').slice(0, -1), function(s) {
+ return [s, s.length+1];
+ });
+ return { lines: lines };
+}
+
+function getLineAndChar(docText, charIndex) {
+ // returns [lineIndex, charIndexIntoLine];
+ // if the charIndex is after the final newline of the document,
+ // lineIndex may be == docText.lines.length.
+ // Otherwise, lneIndex is an actual line and charIndex
+ // is between 0 and the line's length inclusive.
+ var startLine = 0;
+ var startLineStartChar = 0;
+ var lines = docText.lines;
+ var done = false;
+ while (!done) {
+ if (startLine >= lines.length) {
+ done = true;
+ }
+ else {
+ var lineLength = lines[startLine][1];
+ var nextLineStart = startLineStartChar + lineLength;
+ if (nextLineStart <= charIndex) {
+ startLine++;
+ startLineStartChar = nextLineStart;
+ }
+ else {
+ done = true;
+ }
+ }
+ }
+ return [startLine, charIndex - startLineStartChar];
+}
+
+function applySplice(docText, splice, forward) {
+ var startChar = splice[0];
+ var oldText = splice[1];
+ var newText = splice[2];
+ if (! forward) {
+ var tmp = oldText;
+ oldText = newText;
+ newText = tmp;
+ }
+
+ //var OLD_FULL_TEXT = map(docText.lines, function(L) { return L[0]; }).join('\n')+'\n';
+ //var OLD_NUM_LINES = docText.lines.length;
+
+ var lines = docText.lines;
+ var startLineAndChar = getLineAndChar(docText, startChar);
+ var endLineAndChar = getLineAndChar(docText, startChar+oldText.length);
+
+ var lineSpliceStart = startLineAndChar[0];
+ var lineSpliceEnd = endLineAndChar[0];
+ var newLines = newText.split('\n');
+ // we want to splice in entire lines, so adjust start to include beginning of line
+ // we're starting to insert into
+ if (startLineAndChar[1] > 0) {
+ newLines[0] = lines[startLineAndChar[0]][0].substring(0, startLineAndChar[1]) + newLines[0];
+ }
+ // adjust end to include entire last line that will be changed
+ if (endLineAndChar[1] > 0 || newLines[newLines.length-1].length > 0) {
+ newLines[newLines.length-1] += lines[endLineAndChar[0]][0].substring(endLineAndChar[1]);
+ lineSpliceEnd += 1;
+ }
+ else {
+ // the splice is ok as is, except for an extra newline
+ newLines.pop();
+ }
+
+ var newLineEntries = map(newLines, function(s) {
+ return [s, s.length+1];
+ });
+
+ Array.prototype.splice.apply(lines,
+ [lineSpliceStart, lineSpliceEnd-lineSpliceStart].concat(newLineEntries));
+
+ // check it
+ //var EXPECTED_FULL_TEXT = OLD_FULL_TEXT.substring(0, startChar) + newText +
+ //OLD_FULL_TEXT.substring(startChar + oldText.length, OLD_FULL_TEXT.length);
+ //var ACTUAL_FULL_TEXT = map(docText.lines, function(L) { return L[0]; }).join('\n')+'\n';
+
+ //console.log("%o %o %o %d %d %d %d %d",
+ //docText.lines, startLineAndChar, endLineAndChar, OLD_NUM_LINES,
+ //lines.length, lineSpliceStart, lineSpliceEnd-lineSpliceStart, newLineEntries.length);
+
+ //if (EXPECTED_FULL_TEXT != ACTUAL_FULL_TEXT) {
+ //console.log(escapeHTML("mismatch: "+EXPECTED_FULL_TEXT+" / "+ACTUAL_FULL_TEXT));
+ //}
+
+ return [lineSpliceStart, lineSpliceEnd-lineSpliceStart, newLines];
+}
+
+function lineHTML(line) {
+ return (escapeHTML(line) || '&nbsp;');
+}
+
+function setCurText(docText, dontSetDom) {
+ curText = docText;
+ if (! dontSetDom) {
+ var docNode = $("#stuff");
+ var html = map(docText.lines, function(line) {
+ return '<div>'+lineHTML(line[0])+'</div>';
+ });
+ docNode.html(html.join(''));
+ }
+}
+
+function spliceDom(splice) {
+ var index = splice[0];
+ var numRemoved = splice[1];
+ var newLines = splice[2];
+
+ var overlap = Math.min(numRemoved, newLines.length);
+ var container = $("#stuff").get(0);
+ var oldNumNodes = container.childNodes.length;
+ var i = 0;
+ for(;i<overlap;i++) {
+ var n = container.childNodes.item(index+i);
+ $(n).html(lineHTML(newLines[i]));
+ }
+ for(;i<newLines.length;i++) {
+ var insertIndex = index+i;
+ var content = '<div>'+lineHTML(newLines[i])+'</div>';
+ if (insertIndex >= container.childNodes.length) {
+ $(container).append(content);
+ }
+ else {
+ $(container.childNodes.item(insertIndex)).before(content);
+ }
+ }
+ for(;i<numRemoved;i++) {
+ var deleteIndex = index+overlap;
+ $(container.childNodes.item(deleteIndex)).remove();
+ }
+
+ //console.log("%d %d %d %d %d", splice[0], splice[1], splice[2].length,
+ //oldNumNodes + newLines.length - numRemoved,
+ //container.childNodes.length);
+}
+
+function seekToRev(r) {
+ // precond: r is reachable
+
+ var isStep = false;
+
+ var bestKeyFrameIndex = -1;
+ var bestKeyFrameDistance = -1;
+ function considerKeyframe(index, kr) {
+ var dist = Math.abs(r - kr);
+ if (bestKeyFrameDistance < 0 || dist < bestKeyFrameDistance) {
+ bestKeyFrameDistance = dist;
+ bestKeyFrameIndex = index;
+ }
+ }
+ for(var i=0;i<keyframes.length;i++) {
+ considerKeyframe(i, keyframes[i][0]);
+ }
+ if (curRev >= 0) {
+ if (Math.abs(r - curRev) == 1) {
+ isStep = true;
+ bestKeyFrameIndex = -2; // -2 to mean "current revision"
+ }
+ else {
+ considerKeyframe(-2, curRev);
+ }
+ }
+
+ var docText = curText;
+ var docRev = curRev;
+ if (bestKeyFrameIndex >= 0) {
+ // some keyframe is better than moving from the current location;
+ // move to that keyframe
+ var keyframe = keyframes[bestKeyFrameIndex];
+ docRev = keyframe[0];
+ docText = getDocTextForText(keyframe[1].text);
+ }
+
+ var startRev = docRev;
+ var destRev = r;
+
+ var curChangesBlock = null;
+ function findChangesBlockFor(n) {
+ function changesBlockWorks(arr) {
+ return n >= arr[0] && n <= arr[1];
+ }
+ if (curChangesBlock == null || ! changesBlockWorks(curChangesBlock)) {
+ curChangesBlock = null;
+ for(var i=0;i<changesBlocks.length;i++) {
+ var cba = changesBlocks[i];
+ if (changesBlockWorks(cba)) {
+ curChangesBlock = cba;
+ break;
+ }
+ }
+ }
+ }
+
+ //var DEBUG_REVS_APPLIED = [];
+
+ function applyRev(n, forward) {
+ findChangesBlockFor(n);
+ var cb = curChangesBlock[2];
+ var idx = n - curChangesBlock[0];
+ var chng = cb.changeStructs[idx];
+
+ var splices = chng.splices;
+ if (forward) {
+ for(var i=0;i<splices.length;i++) {
+ var splice = applySplice(docText, splices[i], true);
+ if (isStep) spliceDom(splice);
+ }
+ }
+ else {
+ for(var i=splices.length-1;i>=0;i--) {
+ var splice = applySplice(docText, splices[i], false);
+ if (isStep) spliceDom(splice);
+ }
+ }
+
+ //DEBUG_REVS_APPLIED.push(n);
+ }
+
+ if (destRev > startRev) {
+ for (var j=startRev+1; j<=destRev; j++) {
+ applyRev(j, true);
+ }
+ }
+ else if (destRev < startRev) {
+ for(var j=startRev; j >= destRev+1; j--) {
+ applyRev(j, false);
+ }
+ }
+
+ docRev = destRev;
+
+ setCurText(docText, isStep);
+ curRev = docRev;
+ $("#currevdisplay .cur").html(String(curRev));
+}
+
+function reportProblem(probData) {
+ problemData = probData;
+ if (probData.msg) {
+ $("#stuff").html(escapeHTML(probData.msg));
+ }
+}
+
+var playTimer = null;
+
+$(function() {
+ /*$("#sliderui").slider({min: 0, max: 0, value: 0, step: 1, change: slidechange});
+ function slidechange(event, ui) {
+ alert("HELLO");
+ var value = ui.value;
+ console.log(value);
+ }*/
+
+ $("#controls .next").click(function() {
+ if (curRev < lastRevLoaded) {
+ seekToRev(curRev+1);
+ }
+ return false;
+ });
+
+ $("#controls .prev").click(function() {
+ if (curRev > 0) {
+ seekToRev(curRev-1);
+ }
+ return false;
+ });
+
+ function stop() {
+ if (playTimer) {
+ clearInterval(playTimer);
+ playTimer = null;
+ }
+ }
+
+ function play() {
+ stop();
+ playTimer = setInterval(function() {
+ if (curRev < lastRevLoaded) {
+ seekToRev(curRev+1);
+ }
+ else {
+ stop();
+ }
+ }, 60);
+ return false;
+ }
+
+ $("#controls .play").click(play);
+
+ $("#controls .stop").click(function() {
+ stop();
+ return false;
+ });
+
+ $("#controls .entry").change(function() {
+ var value = $("#controls .entry").val();
+ value = Number(value || 0);
+ if (isNaN(value)) value = 0;
+ if (value < 0) value = 0;
+ if (value > lastRevLoaded) {
+ value = lastRevLoaded;
+ }
+ $("#controls .entry").val('');
+ seekToRev(value);
+ });
+ $("#controls .entry").val('');
+
+ var useAutoplay = true;
+ var hasAutoplayed = false;
+
+ loadThroughZero(function(success) {
+ if (success) {
+ seekToRev(0);
+
+ function loadMoreRevsIfNecessary(continuation) {
+ if (lastRevLoaded < lastRev) {
+ loadMoreRevs(continuation);
+ }
+ }
+ loadMoreRevsIfNecessary(function cont(success) {
+ if (success) {
+ if (lastRevLoaded > 0 && useAutoplay && ! hasAutoplayed) {
+ hasAutoplayed = true;
+ play();
+ }
+ loadMoreRevsIfNecessary(cont);
+ }
+ });
+ }
+ });
+});
+
diff --git a/etherpad/src/static/js/undo-xpopup.js b/etherpad/src/static/js/undo-xpopup.js
new file mode 100644
index 0000000..89cfb4d
--- /dev/null
+++ b/etherpad/src/static/js/undo-xpopup.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+if (window._orig_windowOpen) {
+ window.open = _orig_windowOpen;
+}
+if (window._orig_windowSetTimeout) {
+ window.setTimeout = _orig_windowSetTimeout;
+}
+if (window._orig_windowSetInterval) {
+ window.setInterval = _orig_windowSetInterval;
+}
diff --git a/etherpad/src/static/robots.txt b/etherpad/src/static/robots.txt
new file mode 100644
index 0000000..4f9540b
--- /dev/null
+++ b/etherpad/src/static/robots.txt
@@ -0,0 +1 @@
+User-agent: * \ No newline at end of file
diff --git a/etherpad/src/templates/pad/exporthtml.ejs b/etherpad/src/templates/pad/exporthtml.ejs
new file mode 100644
index 0000000..288a595
--- /dev/null
+++ b/etherpad/src/templates/pad/exporthtml.ejs
@@ -0,0 +1,28 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML>
+<HEAD>
+ <META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8">
+ <TITLE></TITLE>
+ <STYLE TYPE="text/css">
+ <!--
+ @page { margin: 0.79in }
+ P { margin-bottom: 0.08in }
+ -->
+ </STYLE>
+</HEAD>
+<BODY LANG="en-US" DIR="LTR">
+<%= pre ? '<PRE>' : '' %><%= content %><%= pre ? '</PRE>' : '' %>
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/etherpad/src/templates/pro/admin/pro-config.ejs b/etherpad/src/templates/pro/admin/pro-config.ejs
new file mode 100644
index 0000000..32cb610
--- /dev/null
+++ b/etherpad/src/templates/pro/admin/pro-config.ejs
@@ -0,0 +1,55 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<h3 class="top">Application Configuration</h3>
+
+<%= messageDiv() %>
+
+<form action="<%= request.path %>" method="post">
+
+<table id="t-pro-config">
+ <tr>
+ <th width="50%" valign="top">Site Name (appears in
+ the header of all pages):</th>
+ <td width="50%" valign="top">
+ <input type="text" name="siteName" value="<%=
+ config.siteName %>" id="siteName" />
+ </td>
+ </tr>
+
+ <tr>
+ <th valign="top">Always require all users on this domain to use secure
+ (HTTPS) connections?</th>
+ <td valign="top">
+ <input type="checkbox" id="alwaysHttps" name="alwaysHttps"
+ <%= config.alwaysHttps ? 'checked="on"' : '' %> />
+ </td>
+ </tr>
+
+ <tr>
+ <th valign="top">Default pad text:</th>
+ <td valign="top">
+ <textarea name="defaultPadText" id="defaultPadText"><%=
+ config.defaultPadText %></textarea>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="2" style="text-align: right;">
+ <input type="submit" name="save" value="Apply" />
+ </td>
+ </tr>
+</table>
+
+</form>
+
diff --git a/etherpad/src/themes/default/templates/500_body.ejs b/etherpad/src/themes/default/templates/500_body.ejs
new file mode 100644
index 0000000..34549ed
--- /dev/null
+++ b/etherpad/src/themes/default/templates/500_body.ejs
@@ -0,0 +1,26 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.setHtmlTitle("EtherPad Internal Server Error"); %>
+
+<% if (trace) { %>
+ <pre style="background: #fff; font-family: monospace; padding: 1em; border: 1px solid red;
+ margin: 1em; font-size: 1.25em;"><%= trace %></pre>
+<% } else { %>
+ <div id="errorpage" class="fpcontent">
+ <div class="error500">
+ <p>Oops! A server error occured. It's been logged.</p>
+ <p>Please email &lt;support@pad.spline.inf.fu-berlin.de&gt; if this persists.</p>
+ </div>
+ </div>
+<% } %>
+
diff --git a/etherpad/src/themes/default/templates/admin/pluginmanager.ejs b/etherpad/src/themes/default/templates/admin/pluginmanager.ejs
new file mode 100644
index 0000000..cc47928
--- /dev/null
+++ b/etherpad/src/themes/default/templates/admin/pluginmanager.ejs
@@ -0,0 +1,74 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<%
+ template.inherit('page.ejs');
+ helpers.setHtmlTitle("EtherPad: Manage plugins");
+ helpers.includeCss("admin/pluginmanager.css");
+
+ function inArray(item, arr) {
+ for (var i = 0; i < arr.length; i++)
+ if (arr[i] == item)
+ return true;
+ return false;
+ }
+%>
+
+<% template.define('docBarTitle', function() { var ejs_data=''; %>
+ <td id="docbarpadtitle"><span>Plugin manager</span></td>
+<% return ejs_data; }); %>
+
+
+<% template.define('docBarItems', function() { var ejs_data=''; %>
+ <%: plugins.callHookStr('docbarItemsPluginManager', {}, '', '<td class="docbarbutton">', '</td>'); %>
+<% return ejs_data; }); %>
+
+<% template.define('contentArea', function() { var ejs_data=''; %>
+ <div id="editorcontainer">
+ <table>
+ <tr>
+ <th>Module name</th>
+ <th>Status</th>
+ <th></th>
+ </tr>
+ <% for (var plugin in plugins.pluginModules) { %>
+ <tr>
+ <td class="mousover_parent">
+ <%= plugin %>
+ <div class="mouseover_child">
+ <%= plugins.pluginModules[plugin].description %>
+ </div>
+ </td>
+ <td>
+ <% if (plugins.plugins[plugin] !== undefined) { %>
+ Installed
+ <% } else { %>
+ Not installed
+ <% } %>
+ </td>
+ <td>
+ <% if (plugins.plugins[plugin] !== undefined) { %>
+ <a href="/ep/admin/pluginmanager/?plugin=<%= plugin %>&action=uninstall">Uninstall</a>
+ <a href="/ep/admin/pluginmanager/?plugin=<%= plugin %>&action=reinstall">Reinstall</a>
+ <% if (plugins.plugins[plugin].configLink !== undefined) { %>
+ <a href="<%= plugins.plugins[plugin].configLink %>">Configure</a>
+ <% } %>
+ <% } else { %>
+ <a href="/ep/admin/pluginmanager/?plugin=<%= plugin %>&action=install">Install</a>
+ <% } %>
+ </td>
+ </tr>
+ <% } %>
+ </table>
+ </div>
+<% return ejs_data; }); %>
diff --git a/etherpad/src/themes/default/templates/email/padinvite.ejs b/etherpad/src/themes/default/templates/email/padinvite.ejs
new file mode 100644
index 0000000..0f729e3
--- /dev/null
+++ b/etherpad/src/themes/default/templates/email/padinvite.ejs
@@ -0,0 +1,18 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><%= body %>
+
+--
+Sent by <%= request.host %> at the request of an EtherPad user.
+Do not reply to this email.
+Report abuse to: support@pad.spline.inf.fu-berlin.de
diff --git a/etherpad/src/themes/default/templates/framed/framedfooter.ejs b/etherpad/src/themes/default/templates/framed/framedfooter.ejs
new file mode 100644
index 0000000..7994e38
--- /dev/null
+++ b/etherpad/src/themes/default/templates/framed/framedfooter.ejs
@@ -0,0 +1,13 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
diff --git a/etherpad/src/themes/default/templates/framed/framedheader-pro.ejs b/etherpad/src/themes/default/templates/framed/framedheader-pro.ejs
new file mode 100644
index 0000000..afb8a67
--- /dev/null
+++ b/etherpad/src/themes/default/templates/framed/framedheader-pro.ejs
@@ -0,0 +1,78 @@
+<% /*
+Copyright 2009 Google Inc.
+Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+
+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. */ %><% helpers.addBodyClass("pro-withtopbar"); %>
+
+<div id="pro-topbar">
+
+ <div id="pro-topbar-inner">
+
+ <% if (account) { %>
+ <div id="accountnav">
+ <%= toHTML(account.email) %>
+ <a href="/ep/account/sign-out">(sign out)</a>
+ </div>
+ <% } else { %>
+ <% // TODO: eventually have sign-in link here. %>
+ <% } %>
+
+ <div id="org-name">
+ <a href="/">
+ <%= proDomainOrgName %>
+ </a>
+
+ <% if (isAnEtherpadAdmin) { %>
+ <span style="color: #ff0; padding-left: 2em; font-weight: bold;">INVISIBLE ADMIN MODE</span>
+ <% } %>
+ </div>
+
+ <div style="clear: both;"><!-- --></div>
+
+ </div>
+</div>
+
+<% function renderProTopNav() {
+ var links = [
+ ['/', 'Home', 'home'],
+ ['/ep/padlist/', 'Pads', 'padlist'],
+ ['/ep/account/', 'My Account', 'account'],
+ ];
+ if (account && account.isAdmin) {
+ links.push(['/ep/admin/', 'Admin', 'admin']);
+ }
+ var ul = UL();
+ links.forEach(function(l) {
+ var c = l[2];
+ var selc = (request.path == l[0] || navSelection == c) ? " selected" : "";
+ ul.push(LI({className: 'topnav_'+c+selc},
+ A({href: request.scheme + '://'+request.host+l[0]}, l[1])));
+ });
+ return ul;
+} %>
+
+ <%= pneTrackerHtml %>
+
+<div id="pro-topnav">
+ <div id="pro-topnav-inner">
+ <%= renderProTopNav() %>
+ <%= helpers.clearFloats() %>
+ </div>
+</div>
+
+<!--
+<div id="shuttingdown">
+ <strong style="color:red">Note: EtherPad.com is shutting down March 31, 2010.</strong>
+ <a href="http://<%= fullSuperdomain %>/ep/blog/posts/google-acquires-appjet">(more info)</a>
+</div>
+-->
diff --git a/etherpad/src/themes/default/templates/framed/framedheader.ejs b/etherpad/src/themes/default/templates/framed/framedheader.ejs
new file mode 100644
index 0000000..d6c25cb
--- /dev/null
+++ b/etherpad/src/themes/default/templates/framed/framedheader.ejs
@@ -0,0 +1,13 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %> \ No newline at end of file
diff --git a/etherpad/src/themes/default/templates/framed/framedpage-pro.ejs b/etherpad/src/themes/default/templates/framed/framedpage-pro.ejs
new file mode 100644
index 0000000..b3acb07
--- /dev/null
+++ b/etherpad/src/themes/default/templates/framed/framedpage-pro.ejs
@@ -0,0 +1,31 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><%
+ helpers.setBodyId("framedpagebody");
+ helpers.includeCss("etherpad.css");
+ helpers.includeCss("pro/framedpage-pro.css");
+ helpers.addBodyClass("pro-body");
+%>
+
+<div id="container">
+
+<% if (helpers.isHeaderVisible()) { %>
+ <%= renderHeader() %>
+<% } %>
+
+<%= renderGlobalProNotice() %>
+
+<%= getContentHtml() %>
+
+</div>
+
diff --git a/etherpad/src/themes/default/templates/framed/framedpage.ejs b/etherpad/src/themes/default/templates/framed/framedpage.ejs
new file mode 100644
index 0000000..b1590f8
--- /dev/null
+++ b/etherpad/src/themes/default/templates/framed/framedpage.ejs
@@ -0,0 +1,37 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><%
+ helpers.setBodyId("framedpagebody");
+ helpers.includeCss("etherpad.css");
+ helpers.includeCss("framedpage.css");
+
+ if (isProDomainRequest) {
+ helpers.includeCss("pro/pro-page.css");
+ }
+
+ if (request.path != "/") {
+ helpers.addBodyClass("nothome");
+ }
+%>
+
+<div id="container">
+
+<% if (helpers.isHeaderVisible()) { %>
+ <%= renderHeader() %>
+<% } %>
+
+<%= getContentHtml() %>
+
+<%= renderFooter() %>
+
+</div>
diff --git a/etherpad/src/themes/default/templates/html.ejs b/etherpad/src/themes/default/templates/html.ejs
new file mode 100644
index 0000000..056d7a7
--- /dev/null
+++ b/etherpad/src/themes/default/templates/html.ejs
@@ -0,0 +1,43 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><!DOCTYPE html PUBLIC
+ "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+ <head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <meta http-equiv="Content-Language" content="en-us" />
+ <%= helpers.robotsMeta() %>
+ <title><%= helpers.htmlTitle() %></title>
+ <base href="<%= helpers.baseHref() %>" />
+
+ <!-- CSS -->
+ <%= helpers.cssIncludes() %>
+
+ <%= helpers.headExtra() %>
+
+ </head>
+
+ <body id="<%= helpers.bodyId() %>" class="<%= helpers.bodyClasses() %>">
+
+ <%= bodyHtml %>
+
+<!-- javascript -->
+
+<%= helpers.clientVarsScript() %>
+<%= helpers.jsIncludes() %>
+<%= helpers.googleAnalytics() %>
+
+ </body>
+</html>
diff --git a/etherpad/src/themes/default/templates/main/home.ejs b/etherpad/src/themes/default/templates/main/home.ejs
new file mode 100644
index 0000000..7041bee
--- /dev/null
+++ b/etherpad/src/themes/default/templates/main/home.ejs
@@ -0,0 +1,62 @@
+<% /*
+Copyright 2009 Google Inc.
+Copyright 2010 Pita, Peter Martischka <petermartischka@googlemail.com>
+
+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. */ %><% helpers.setHtmlTitle("SplinePad [beta]: Open-Sourced!"); %>
+
+<% helpers.includeCss("home-opensource.css"); %>
+
+<div id="home">
+ <div id="title">
+ SplinePad
+ </div>
+
+ <div id="buttons">
+ <a id="home-newpad" href="/ep/pad/newpad">
+ Create new pad
+ </a>
+ <% if (isProAccountEnabled()) { %>
+ <a id="home-newteam" href="/ep/pro-signup/">
+ Create team site
+ </a>
+ <% } %>
+ </div>
+
+ <div id="tos">
+ <h1>
+ <b>Terms of service and Privacy notice</b>
+ </h1>
+
+ <p>
+ <b>Privacy:</b> We guarantee that we will not intentionally hand
+ over your data to any third party.
+ </p>
+
+ <p>
+ <b>Terms:</b> By using splinepad, you certify to agree to the
+ following terms of service: We are not responsible, and cannot
+ be held liable for any loss or damages that may be caused by the
+ result of our service.
+ </p>
+
+ <p>
+ <b>In short:</b> We love the "Datenschutzgesetz" and if
+ something is wrong, we didn't do it.
+ </p>
+
+ Have fun with splinepad.
+ </div>
+
+</div>
+
+
diff --git a/etherpad/src/themes/default/templates/main/pro_signup_body.ejs b/etherpad/src/themes/default/templates/main/pro_signup_body.ejs
new file mode 100644
index 0000000..ff35dfc
--- /dev/null
+++ b/etherpad/src/themes/default/templates/main/pro_signup_body.ejs
@@ -0,0 +1,71 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.includeCss('pro-signup.css') %>
+
+<div class="fpcontent">
+ <div class="pro-signup">
+
+ <h1>EtherPad Professional for Teams</h1>
+
+ <div id="about">
+
+ <p>Create a team site in order to keep track of pads and share
+ them privately within your team.</p>
+
+ </div>
+
+ <form id="pro-act-form"
+ method="post" action="<%= request.path %>">
+
+ <%= errorDiv() %>
+
+ <div>
+ <div class="inputdiv">
+ <h3>Your team site will live at:</h3>
+
+ <%= input("subdomain") %>.<%= request.host %>/
+ </div>
+ <div class="inputhelp">
+ This is where you and members of your team will sign
+ in.
+ </div>
+ <%= helpers.clearFloats() %>
+ </div>
+
+ <br/><br/>
+
+ <div>
+ <div class="inputdiv">
+ <h3>Administrator account</h3>
+ <%= inf("fullName", "Full Name") %>
+ <%= inf("email", "Email") %>
+ </div>
+ <div class="inputhelp">
+ <p>Instructions for choosing a password and signing in will
+ be emailed here.</p>
+ <p>Please use your <strong>*.fu-berlin.de</strong> address.</p>
+ </div>
+ <%= helpers.clearFloats() %>
+ </div>
+
+ <br/>
+
+ <p><button type="submit" id="createbutton">Create team site now</button></p>
+
+ </form>
+
+ <p style="font-size: 80%;">Existing users: <a href="/ep/pro-account/sign-in">sign in
+ here</a></p>
+ </div>
+</div>
+
diff --git a/etherpad/src/themes/default/templates/misc/pad_default.ejs b/etherpad/src/themes/default/templates/misc/pad_default.ejs
new file mode 100644
index 0000000..96b7e25
--- /dev/null
+++ b/etherpad/src/themes/default/templates/misc/pad_default.ejs
@@ -0,0 +1,16 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+Welcome to EtherPad!
+
+This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!
diff --git a/etherpad/src/themes/default/templates/notice.ejs b/etherpad/src/themes/default/templates/notice.ejs
new file mode 100644
index 0000000..311694f
--- /dev/null
+++ b/etherpad/src/themes/default/templates/notice.ejs
@@ -0,0 +1,16 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<div id="notice" class="fpcontent">
+ <%= content %>
+</div>
diff --git a/etherpad/src/themes/default/templates/pad/create_body.ejs b/etherpad/src/themes/default/templates/pad/create_body.ejs
new file mode 100644
index 0000000..742821f
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pad/create_body.ejs
@@ -0,0 +1,26 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.setHtmlTitle("SplinePad [beta]: Create a new pad?"); %>
+
+<div id="createpadpage" class="fpcontent">
+ <form action="<%= request.path %>" method="post">
+
+ <p><tt id="padurl">http://<%= request.host %>/<%= toHTML(padId) %></tt></p>
+
+ <br/>
+ <p>There is no SplinePad document here. Would you like to create one?</p>
+
+ <input type="hidden" name="padId" value="<%= toHTML(padId) %>" />
+ <input type="submit" id="createPad" value="Create Pad" />
+ </form>
+</div>
diff --git a/etherpad/src/themes/default/templates/pad/pad_body2.ejs b/etherpad/src/themes/default/templates/pad/pad_body2.ejs
new file mode 100644
index 0000000..5c886fb
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pad/pad_body2.ejs
@@ -0,0 +1,505 @@
+<% /*
+Copyright 2009 Google Inc.
+Copyright 2010 Pita, Peter Martischka
+
+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. */ %>
+
+<%
+ template.inherit('page.ejs');
+
+ helpers.setHtmlTitle("EtherPad: "+pageTitle);
+ helpers.includeJs("ace.js");
+ helpers.includeJs("collab_client.js");
+ helpers.includeJs("pad_userlist.js");
+ helpers.includeJs("pad_chat.js");
+ helpers.includeJs("pad_impexp.js");
+ helpers.includeJs("pad_savedrevs.js");
+ helpers.includeJs("pad_connectionstatus.js");
+
+ var padUrlAttrValue = request.url.split("?", 1)[0].replace(/\"/g, '&quot;');
+
+ function exportLink(type, n, label, requiresOffice, url, title) {
+ url = url || '/ep/pad/export/'+localPadId+'/latest?format='+type;
+ var classes = ["exportlink", "exporthref"+type, "n"+n];
+ if (requiresOffice && !hasOffice) {
+ classes.push("disabledexport");
+ }
+ else {
+ classes.push("requiresoffice");
+ }
+ var pieces = ['<a'];
+ pieces.push(' class="', classes.join(' '), '"');
+ pieces.push(' target="_blank"');
+ pieces.push(' href="', url, '"');
+ if (title) {
+ pieces.push(' title="', title.replace(/\"/g, "&quot;"), '"'); //"
+ }
+ pieces.push('>', label);
+ /* if (title) {
+ pieces.push('<sup>?</sup>');
+ }*/
+ pieces.push('</a>');
+ return pieces.join('');
+ }
+%>
+
+
+<% template.define('docBarTitle', function() { var ejs_data=''; %>
+ <td id="docbarpadtitle"><span><%= initialTitle %></span></td>
+<% return ejs_data; }); %>
+
+
+<% template.define('docBarTitleEditor', function() { var ejs_data=''; %>
+ <% if (isProAccountHolder) { %>
+ <div id="docbarrenamelink">
+ <a href="javascript:void(0)">(rename)</a>
+ </div>
+ <% } /* isProAccountHolder */ %>
+ <input type="text" id="padtitleedit"/>
+ <div id="padtitlebuttons">
+ <a id="padtitlesave" href="javascript:void(0)">Save</a>
+ <a id="padtitlecancel" href="javascript:void(0)">Cancel</a>
+ </div>
+<% return ejs_data; }); %>
+
+
+<% template.define('docBarItems', function() { var ejs_data=''; %>
+ <%: plugins.callHookStr('docbarItemsPad', {}, '', '<td class="docbarbutton">', '</td>'); %>
+ <% if (isProAccountHolder) { %>
+ <td id="docbarsecurity-outer" class="docbarbutton">
+ <a href="javascript:void(0)" id="docbarsecurity">
+ <img src="/static/img/jun09/pad/icon_security.gif">Security
+ </a>
+ </td>
+ <% } /* isProAccountHolder */ %>
+ <td id="docbaroptions-outer" class="docbarbutton">
+ <a href="javascript:void(0)" id="docbaroptions">
+ <img src="/static/img/jun09/pad/icon_pad_options.gif">Pad&nbsp;Options</a>
+ </td>
+ <td id="docbarimpexp-outer" class="docbarbutton">
+ <a href="javascript:void(0)" id="docbarimpexp">
+ <img src="/static/img/jun09/pad/icon_import_export.gif">Import/Export</a>
+ </td>
+ <td id="docbarsavedrevs-outer" class="docbarbutton">
+ <a href="javascript:void(0)" id="docbarsavedrevs">
+ <img src="/static/img/jun09/pad/icon_saved_revisions.gif">Saved&nbsp;revisions</a>
+ </td>
+ <td id="docbarslider-outer" class="docbarbutton highlight">
+ <a target="_blank" href="/ep/pad/view/<%= localPadId %>/latest" id="docbarslider">
+ <img src="/static/img/jun09/pad/icon_time_slider.gif">Time&nbsp;Slider</a>
+ </td>
+<% return ejs_data; }); %>
+
+
+<% template.define('docBarDropdowns', function() { var ejs_data=''; %>
+ <div id="impexp-wrapper" class="dbpanel-wrapper">
+ <div id="impexp-panel" class="dbpanel-panel">
+ <div class="dbpanel-leftedge"><!-- --></div>
+ <div class="dbpanel-rightedge"><!-- --></div>
+ <div class="dbpanel-botleftcorner"><!-- --></div>
+ <div class="dbpanel-botrightcorner"><!-- --></div>
+ <div class="dbpanel-middle">
+ <div class="dbpanel-inner">
+ <div class="dbpanel-top"><!-- --></div>
+ </div>
+ <div class="dbpanel-bottom"><!-- --></div>
+ <div id="importexport">
+ <div id="impexp-import">
+ <div id="impexp-importlabel"><b>Import</b> from text file, HTML, Word, or RTF:</div>
+ <form id="importform" method="post" action="/ep/pad/impexp/import"
+ target="importiframe" enctype="multipart/form-data">
+ <div class="importformdiv" id="importformfilediv">
+ <input type="file" name="file" size="20" id="importfileinput" />
+ <div class="importmessage" id="importmessagefail"></div>
+ </div>
+ <div class="importmessage" id="importmessagesuccess">Successful!</div>
+ <div class="importformdiv" id="importformsubmitdiv">
+ <input type="hidden" name="padId" value="<%= encodeURIComponent(localPadId) %>" />
+ <span class="nowrap">
+ <input type="submit" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput" />
+ <img alt="" id="importstatusball" src="/static/img/misc/status-ball.gif" align="top" />
+ <img alt="" id="importarrow" src="/static/img/may09/leftarrow2.gif" align="top" />
+ </span>
+ </div>
+ </form>
+ </div><!-- /impexp-import -->
+ <div id="impexp-export">
+ <div id="impexp-exportlabel"><b>Export</b> current pad as:</div>
+ <div id="exportlinks">
+ <%= exportLink('html', 1, 'HTML', false) %>
+ <%= exportLink('txt', 2, 'Plain text', false) %>
+ <%= exportLink('link', 3, 'Bookmark file', false, '/ep/pad/linkfile?padId='+localPadId, 'This will save a file that, when opened, takes you to this pad.') %>
+ <%= exportLink('doc', 4, 'Microsoft Word', true) %>
+ <%= exportLink('pdf', 5, 'PDF', true) %>
+ <%= exportLink('odt', 6, 'OpenDocument', true) %>
+ </div>
+ </div><!-- /impexp-export -->
+ <div id="impexp-divider"><!-- --></div>
+ <div id="impexp-disabled-clickcatcher"><!-- --></div>
+ <a id="impexp-close" href="javascript:void(0)">Hide</a>
+ </div><!-- /importexport -->
+ </div>
+ </div>
+ </div>
+ <div id="savedrevs-wrapper" class="dbpanel-wrapper">
+ <div id="savedrevs-panel" class="dbpanel-panel">
+ <div class="dbpanel-leftedge"><!-- --></div>
+ <div class="dbpanel-rightedge"><!-- --></div>
+ <div class="dbpanel-botleftcorner"><!-- --></div>
+ <div class="dbpanel-botrightcorner"><!-- --></div>
+ <div class="dbpanel-middle">
+ <div class="dbpanel-inner">
+ <div class="dbpanel-top"><!-- --></div>
+ </div>
+ <div class="dbpanel-bottom"><!-- --></div>
+ </div>
+ <div id="savedrevisions">
+ <a href="javascript:void(0)" id="savedrevs-savenow">
+ Save Now
+ </a>
+ <div id="savedrevs-scrolly">
+ <div id="savedrevs-scrollleft" class="disabledscrollleft"><!-- --></div>
+ <div id="savedrevs-scrollright" class="disabledscrollright"><!-- --></div>
+ <div id="savedrevs-scrollouter">
+ <div id="savedrevs-scrollinner">
+ <!-- -->
+ </div>
+ </div>
+ </div>
+ <a id="savedrevs-close" href="javascript:void(0)">Hide</a>
+ </div><!-- /savedrevs close -->
+ </div>
+ </div><!-- /savedrevs-wrapper -->
+ <div id="revision-notifier"><span class="label">Saved:</span> <span class="name">Revision 1</span></div>
+ <div id="options-wrapper" class="dbpanel-wrapper">
+ <div id="options-panel" class="dbpanel-panel">
+ <div class="dbpanel-leftedge"><!-- --></div>
+ <div class="dbpanel-rightedge"><!-- --></div>
+ <div class="dbpanel-botleftcorner"><!-- --></div>
+ <div class="dbpanel-botrightcorner"><!-- --></div>
+ <div class="dbpanel-middle">
+ <div class="dbpanel-inner">
+ <div class="dbpanel-top"><!-- --></div>
+ </div>
+ <div class="dbpanel-bottom"><!-- --></div>
+ </div>
+ <div id="padoptions">
+ <div id="options-viewhead">Shared view options:</div>
+ <input type="checkbox" id="options-colorscheck" />
+ <label for="options-colorscheck" id="options-colorslabel">Authorship colors</label>
+ <input type="checkbox" id="options-linenoscheck" />
+ <label for="options-linenoscheck" id="options-linenoslabel">Line numbers</label>
+ <div id="options-fontlabel">Display font:</div>
+ <select id="viewfontmenu"><option value="normal">Normal</option><option value="monospace">Monospaced</option></select>
+ <div id="options-viewexplain">These options affect everyone's view of the pad.</div>
+ <a id="options-close" href="javascript:void(0)">Hide</a>
+ </div>
+ </div>
+ </div><!-- /options-wrapper -->
+ <% if (isProAccountHolder) { %>
+ <div id="security-wrapper" class="dbpanel-wrapper">
+ <div id="security-panel" class="dbpanel-panel">
+ <div class="dbpanel-leftedge"><!-- --></div>
+ <div class="dbpanel-rightedge"><!-- --></div>
+ <div class="dbpanel-botleftcorner"><!-- --></div>
+ <div class="dbpanel-botrightcorner"><!-- --></div>
+ <div class="dbpanel-middle">
+ <div class="dbpanel-inner">
+ <div class="dbpanel-top"><!-- --></div>
+ </div>
+ <div class="dbpanel-bottom"><!-- --></div>
+ </div>
+ <div id="padsecurity">
+ <div id="security-access">
+ <div id="security-accesshead">Pad Access:</div>
+ <input type="radio" name="padaccess" id="access-private" value="deny"/>
+ <label for="access-private" id="access-private-label"><strong>Private</strong> (Team account-holders only)</label>
+ <input type="radio" name="padaccess" id="access-public" value="allow"/>
+ <label for="access-public" id="access-public-label"><strong>Public</strong> (Allow Internet guests)</label>
+ </div>
+ <div id="security-password">
+ <div id="security-passhead">Password:</div>
+ <div id="security-passbody">
+ <div class="nopassword" id="password-nonedit">
+ <div id="password-display">None</div>
+ <a href="javascript:void(0)" id="password-setlink">Set...</a>
+ <a href="javascript:void(0)" id="password-clearlink">Clear</a>
+ </div>
+ <div id="password-inedit">
+ <a href="javascript:void(0)" id="password-savelink">Save</a>
+ <a href="javascript:void(0)" id="password-cancellink">Cancel</a>
+ <input type="text" id="security-passwordedit" maxlength="31" />
+ </div>
+ </div>
+ </div>
+ <a id="security-close" href="javascript:void(0)">Hide</a>
+ </div>
+ </div>
+ </div><!-- /security-wrapper -->
+ <% } /* isProAccountHolder */ %>
+<% return ejs_data; }); %>
+
+
+<% template.define('sideBar', function() { var ejs_data=''; %>
+ <div id="padusers">
+ <div id="connectionbox" class="cboxconnecting">
+ <div id="connectionboxinner">
+ <div class="connecting">
+ Connecting...
+ </div>
+ <div class="reconnecting">
+ Reestablishing connection...
+ </div>
+ <div class="disconnected">
+ <h2 class="h2_disconnect">Disconnected.</h2>
+ <h2 class="h2_userdup">Opened in another window.</h2>
+ <h2 class="h2_unauth">No Authorization.</h2>
+ <div id="disconnected_looping">
+ <p><b>We're having trouble talking to the EtherPad synchronization server.</b>
+ You may be connecting through an incompatible firewall or proxy server.</p>
+ </div>
+ <div id="disconnected_initsocketfail">
+ <p><b>We were unable to connect to the EtherPad synchronization server.</b>
+ This may be due to an incompatibility with your web
+ browser or internet connection.</p>
+ </div>
+ <div id="disconnected_userdup">
+ <p><b>You seem to have opened this pad in another browser window.</b>
+ If you'd like to use this window instead, you can reconnect.</p>
+ </div>
+ <div id="disconnected_unknown">
+ <p><b>Lost connection with the EtherPad synchronization
+ server.</b> This may be due to a loss of network connectivity.</p>
+ </div>
+ <div id="disconnected_slowcommit">
+ <p><b>Server not responding.</b> This may be due to network connectivity issues or high load on the server.</p>
+ </div>
+ <div id="disconnected_unauth">
+ <p>Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.</p>
+ </div>
+ <div id="reconnect_advise">
+ <p>If this continues to happen, please <a target="_blank" href="/ep/support">let us know</a>
+ (opens in new window).</p>
+ </div>
+ <div id="reconnect_form">
+ <button id="forcereconnect">Reconnect Now</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div id="connectionstatus">
+ <!-- -->
+ </div>
+
+ <div id="myuser">
+ <div id="mycolorpicker">
+ <div>
+ <div class="pickerswatchouter n1"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n2"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n3"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n4"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n5"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n6"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n7"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n8"><div class="pickerswatch"><!-- --></div></div>
+ </div><div>
+ <div class="pickerswatchouter n9"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n10"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n11"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n12"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n13"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n14"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n15"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n16"><div class="pickerswatch"><!-- --></div></div>
+ </div><div>
+ <div class="pickerswatchouter n17"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n18"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n19"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n20"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n21"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n22"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n23"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n24"><div class="pickerswatch"><!-- --></div></div>
+ </div><div>
+ <div class="pickerswatchouter n25"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n26"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n27"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n28"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n29"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n30"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n31"><div class="pickerswatch"><!-- --></div></div>
+ <div class="pickerswatchouter n32"><div class="pickerswatch"><!-- --></div></div>
+ </div>
+ <div id="mycolorpickersave">Save</div>
+ <div id="mycolorpickercancel">Cancel</div>
+ </div>
+ <div id="myswatchbox"><div id="myswatch"><!-- --></div></div>
+ <div id="myusernameform"><input type="text" id="myusernameedit" disabled="disabled" /></div>
+ <div id="mystatusform"><input type="text" id="mystatusedit" disabled="disabled" /></div>
+ </div>
+ <div id="otherusers">
+ <div id="guestprompts"><!-- --></div>
+ <table id="otheruserstable" cellspacing="0" cellpadding="0" border="0">
+ <tr><td></td></tr>
+ </table>
+ <div id="nootherusers"><a href="javascript:void(0)">Invite</a> other users and they will show up here.</div>
+ </div>
+ <div id="userlistbuttonarea">
+ <a href="javascript:void(0)" id="sharebutton">Share</a>
+ </div>
+ </div> <!-- /padusers -->
+
+ <div id="hdraggie"><!-- --></div>
+
+ <div id="padchat">
+ <!-- <div id="chattop"><a href="#">View chat logs...</a></div> -->
+ <div id="chatlines">
+ <a href="javascript:void(0)" id="chatloadmore">Load more history...</a>
+ <div id="chatloadingmore">Loading history...</div>
+ </div>
+ <div id="chatbottom">
+ <div id="chatprompt">Chat:</div>
+ <div id="chatentryform"><input type="text" id="chatentrybox"/></div>
+ </div>
+ </div>
+<% return ejs_data; }); %>
+
+
+<% template.define('editBarItemsLeft', function() { var ejs_data=''; %>
+ <td><img src="/static/img/jun09/pad/editbar_groupleft.gif" width="2" height="24"></td>
+ <td class="editbarbutton editbargroupsfirst"><a href="javascript:void (window.pad&&pad.editbarClick('bold'));" title="Bold (ctrl-B)"><img src="/static/img/jun09/pad/editbar_bold.gif"></a></td>
+ <td class="editbarbutton"> <a href="javascript:void (window.pad&&pad.editbarClick('italic'));" title="Italics (ctrl-I)"><img src="/static/img/jun09/pad/editbar_italic.gif"></a></td>
+ <td class="editbarbutton"> <a href="javascript:void (window.pad&&pad.editbarClick('underline'));" title="Underline (ctrl-U)"><img src="/static/img/jun09/pad/editbar_underline.gif"></a></td>
+ <td class="editbarbutton"> <a href="javascript:void (window.pad&&pad.editbarClick('strikethrough'));" title="Strikethrough"><img src="/static/img/jun09/pad/editbar_strikethrough.gif"></a></td>
+ <td><img src="/static/img/jun09/pad/editbar_groupright.gif" width="2" height="24"></td>
+
+ <td>&nbsp;&nbsp;</td>
+
+ <td><img src="/static/img/jun09/pad/editbar_groupleft.gif" width="2" height="24"></td>
+ <td class="editbarbutton editbargroupsfirst"><a href="javascript:void (window.pad&&pad.editbarClick('insertunorderedlist'));" title="Toggle Bullet List"><img src="/static/img/jun09/pad/editbar_insertunorderedlist.gif"></a></td>
+ <td><img src="/static/img/jun09/pad/editbar_groupright.gif" width="2" height="24"></td>
+
+ <td>&nbsp;&nbsp;</td>
+
+ <td><img src="/static/img/jun09/pad/editbar_groupleft.gif" width="2" height="24"></td>
+ <td class="editbarbutton editbargroupsfirst"><a href="javascript:void (window.pad&&pad.editbarClick('indent'));" title="Indent List"><img src="/static/img/jun09/pad/editbar_indent.gif"></a></td>
+ <td class="editbarbutton"><a href="javascript:void (window.pad&&pad.editbarClick('outdent'));" title="Unindent List"><img src="/static/img/jun09/pad/editbar_outdent.gif"></a></td>
+ <td><img src="/static/img/jun09/pad/editbar_groupright.gif" width="2" height="24"></td>
+
+ <td>&nbsp;&nbsp;</td>
+
+ <td><img src="/static/img/jun09/pad/editbar_groupleft.gif" width="2" height="24"></td>
+ <td class="editbarbutton editbargroupsfirst"><a href="javascript:void (window.pad&&pad.editbarClick('clearauthorship'));" title="Clear Authorship Colors"><img src="/static/img/jun09/pad/editbar_clearauthorship.gif"></a></td>
+ <td><img src="/static/img/jun09/pad/editbar_groupright.gif" width="2" height="24"></td>
+
+ <td>&nbsp;&nbsp;</td>
+
+ <td><img src="/static/img/jun09/pad/editbar_groupleft.gif" width="2" height="24"></td>
+ <td class="editbarbutton editbargroupsfirst"><a href="javascript:void (window.pad&&pad.editbarClick('undo'));" title="Undo (ctrl-Z)"><img src="/static/img/jun09/pad/editbar_undo.gif"></a></td>
+ <td class="editbarbutton"><a href="javascript:void (window.pad&&pad.editbarClick('redo'));" title="Redo (ctrl-Y)"><img src="/static/img/jun09/pad/editbar_redo.gif"></a></td>
+ <td><img src="/static/img/jun09/pad/editbar_groupright.gif" width="2" height="24"></td>
+<% return ejs_data; }); %>
+
+
+<% template.define('editBarItemsRight', function() { var ejs_data=''; %>
+ <td><img src="/static/img/jun09/pad/editbar_groupleft.gif" width="2" height="24"></td>
+ <td class="editbarbutton editbargroupsfirst"><a href="javascript:void (window.pad&&pad.editbarClick('save'));" title="Save Revision"><img src="/static/img/jun09/pad/editbar_save.gif"></a></td>
+ <td><img src="/static/img/jun09/pad/editbar_groupright.gif" width="2" height="24"></td>
+<% return ejs_data; }); %>
+
+
+<% template.define('contentArea', function() { var ejs_data=''; %>
+ <div id="editorloadingbox">Loading...</div>
+ <div id="editorcontainer"><!-- --></div>
+<% return ejs_data; }); %>
+
+
+<% template.define('modals', function() { var ejs_data=''; %>
+ <div id="modaloverlay"><div id="modaloverlay-inner"><!-- --></div></div>
+
+ <div id="mainmodals">
+ <div id="feedbackbox">
+ <div id="feedbackbox-tl"><!-- --></div>
+ <div id="feedbackbox-tr"><!-- --></div>
+ <div id="feedbackbox-bl"><!-- --></div>
+ <div id="feedbackbox-br"><!-- --></div>
+ <div id="feedbackbox-back"><!-- --></div>
+ <%/* <a href="javascript:void(0)" id="feedbackbox-send"><!-- --></a>
+ <input type="text" id="feedbackbox-email" class="modalfield" />
+ <textarea id="feedbackbox-message" rows="6" cols="40" class="modalfield"></textarea>
+ <div id="feedbackbox-response"><!-- --></div>*/%>
+ <div id="feedbackbox-contents">
+ <div id="feedbackbox-contentsinner">
+ <p><strong>Great, we love feedback! What kind?</strong></p>
+ <ul id="uservoicelinks">
+ <li><a href="http://uservoice.etherpad.com/pages/17280-feature-requests" target="_blank">Feature Request</a></li>
+ <li><a href="http://uservoice.etherpad.com/pages/17285-bugs-and-problems" target="_blank">Bug Report</a></li>
+ <li><a href="http://uservoice.etherpad.com/pages/22732-how-are-you-using-etherpad-" target="_blank">How I'm Using It</a></li>
+ <li><a href="http://uservoice.etherpad.com/pages/22751-general-questions" target="_blank">Other Question</a></li>
+ <li><a href="http://uservoice.etherpad.com/pages/22733-general-feedback" target="_blank">Other Feedback</a></li>
+ </ul>
+ <p>These links will open UserVoice in a new window.</p>
+ <p id="feedbackemails">You can also send email to <a href="feedback"><tt>feedback</tt></a>, <a href="support"><tt>support</tt></a>, or <a href="bugs"><tt>bugs</tt></a> at <tt>etherpad.com</tt>.</p>
+ </div>
+ </div>
+ <a href="javascript:void(0)" id="feedbackbox-hide"><!-- --></a>
+ </div>
+ <div id="sharebox">
+ <div id="sharebox-inner">
+ <a href="javascript:void(0)" id="sharebox-hide"><!-- --></a>
+ <div id="sharebox-stripe" class="sharebox-stripe-private">
+ <div class="public">
+ <strong>Public Pad:</strong> This pad is accessible to anyone who
+ visits its URL. To make it private, <a href="javascript:void(0)" class="setsecurity">change security settings</a>.
+ </div>
+ <div class="private">
+ <strong>Private Pad:</strong> This pad is only accessible to team account-holders. To allow anyone to access it, <a href="javascript:void(0)" class="setsecurity">change security settings</a>.
+ </div>
+ </div>
+ <div id="sharebox-forms">
+ <div id="sharebox-pastelink">Paste link over email or IM:</div>
+ <div id="sharebox-orsend">or send an email invitation...</div>
+ <a href="javascript:void(0)" id="sharebox-send"><!-- --></a>
+ <input id="sharebox-url" type="text" readonly="readonly" value="<%=padUrlAttrValue%>"/>
+ <input type="text" id="sharebox-to" class="modalfield" />
+ <input type="text" id="sharebox-subject" class="modalfield" />
+ <textarea id="sharebox-message" rows="6" cols="40" class="modalfield"></textarea>
+ <div id="sharebox-fieldname-to">To</div>
+ <div id="sharebox-fieldname-subject">Subject</div>
+ <div id="sharebox-fieldname-message">Message</div>
+ <div id="sharebox-dislink"><!-- --></div>
+ </div>
+ <div id="sharebox-shownwhenexpanded">
+ <div id="sharebox-response"><!-- --></div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <% if (request.params.djs) { %>
+ <div id="djs"><!-- --></div>
+ <% } %>
+
+ <form id="reconnectform"
+ method="post"
+ action="/ep/pad/reconnect"
+ accept-charset="UTF-8"
+ style="display: none;">
+ <input type="hidden" class="padId" name="padId" />
+ <input type="hidden" class="diagnosticInfo"
+ name="diagnosticInfo" />
+ <input type="hidden" class="missedChanges" name="missedChanges" />
+ </form>
+
+<% return ejs_data; }); %>
diff --git a/etherpad/src/themes/default/templates/pad/pad_iphone_body.ejs b/etherpad/src/themes/default/templates/pad/pad_iphone_body.ejs
new file mode 100644
index 0000000..96279ce
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pad/pad_iphone_body.ejs
@@ -0,0 +1,29 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<div style="font-size: 4em; font-family: sans-serif; text-align: center;">
+
+ <p>EtherPad is not yet fully-functional on the iPhone.</p>
+
+ <a style="display: block; padding: 1em; border: 3px solid #333; background: #ddd; margin: 1em; -webkit-border-radius: 1em;"
+ href="/ep/pad/view/<%= padId %>/latest">
+ View Read-Only
+ </a>
+
+ <a style="display: block; padding: 1em; border: 3px solid #333; background: #ddd; margin: 1em; -webkit-border-radius: 1em;"
+ href="/<%= padId %>?skipIphoneCheck=1">
+ Proceed to Editor<br/>
+ <span style="font-size: .6em;">(may not be fully-functional)</span>
+ </a>
+
+</div>
diff --git a/etherpad/src/themes/default/templates/pad/padview_body.ejs b/etherpad/src/themes/default/templates/pad/padview_body.ejs
new file mode 100644
index 0000000..e18ff12
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pad/padview_body.ejs
@@ -0,0 +1,141 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><%
+ helpers.addBodyClass(bodyClass);
+ helpers.setHtmlTitle(toHTML(padId + " / " + rlabel));
+ helpers.setBodyId("padviewbody");
+ helpers.includeCss("broadcast.css");
+ helpers.setRobotsPolicy({index: false, follow: false})
+ helpers.includeJQuery();
+ helpers.includeCometJs();
+ helpers.includeJs("json2.js");
+ helpers.includeJs("pad_utils.js");
+ helpers.includeJs("broadcast_slider.js");
+ helpers.includeJs("broadcast_revisions.js");
+ helpers.includeJs("easysync2_client.js");
+ helpers.includeJs("domline_client.js");
+ helpers.includeJs("linestylefilter_client.js");
+ helpers.includeJs("cssmanager_client.js");
+ helpers.includeJs("broadcast.js");
+ helpers.addToHead('\n<style type="text/css" title="dynamicsyntax"></style>\n');
+
+ function dfmt(t) {
+ var d = new Date(t);
+ return d.toString();
+ }
+
+ function exportOption(type, label, requiresOffice, url, title) {
+ url = url || '/ep/pad/export/'+padId+'/'+revisionId+'?format='+type;
+ var aStart =
+ ['<a',
+ (requiresOffice && ! hasOffice ? ' class="disabledexport"' : ' href="'+url+'"'),
+ '>'].join('');
+ var r = ['<div class="exportlink" id="export', type, '"'];
+ if (title) {
+ r.push(' title="'+title+'"');
+ }
+ r.push('>');
+ r.push('<table cellspacing="0" cellpadding="0" border="0">');
+ r.push('<tr>');
+ r.push('<td class="exportpic" valign="middle">');
+ r.push(aStart, '<img src="/static/img/may09/'+type+'.gif" />', '</a>');
+ r.push('<td class="labelcell" valign="middle">');
+ r.push(aStart, label, '</a>');
+ if (title) {
+ r.push('<sup>?</sup>')
+ }
+ r.push('</td>');
+ r.push('</tr>');
+ r.push('</table>');
+ r.push('</div>');
+ return r.join('');
+ }
+%>
+
+<div id="padpage">
+<div id="topbar" style="margin: 7px; margin-top: 0px;">
+ <div id="topbarleft"><!-- --></div>
+ <div id="topbarright"><!-- --></div>
+ <div id="topbarcenter">
+ <a href="/" id="topbaretherpad">EtherPad</a>
+ </div>
+<% if (isProAccountHolder) { %>
+ <div id="accountnav"><%= toHTML(account.email) %>
+ <a href="/ep/account/sign-out">(sign out)</a>
+ </div>
+<% } else if (isPro) { %>
+ <div id="accountnav">
+ <a href="<%= signinUrl %>">sign in</a>
+ </div>
+<% } %>
+</div>
+
+<div id="timeslider-wrapper">
+<div id="error" style="display: none">It looks like you're having connection troubles. <a href="/ep/pad/view/<%= padId %>/latest">Reconnect now</a>.</div>
+<div id="timeslider" unselectable="on" style="display: none">
+ <div id="timeslider-left"></div>
+ <div id="timeslider-right"></div>
+ <div id="timer"><%= dateFormat %></div>
+ <div id="timeslider-slider">
+ <div id="ui-slider-handle">
+
+ </div>
+ <div id="ui-slider-bar">
+
+ </div>
+ </div>
+ <div id="playpause_button">
+ <div id="playpause_button_icon" class=""></div>
+ </div>
+ <div id="steppers">
+ <div class="stepper" id="leftstar"></div>
+ <div class="stepper" id="rightstar"></div>
+ <div class="stepper" id="leftstep"></div>
+ <div class="stepper" id="rightstep"></div>
+ </div>
+</div>
+</div>
+<div id="rightbars">
+<div id="rightbar"><a id="viewlatest" href="/ep/pad/view/<%= padId %>/latest">
+<% if (revisionId != "latest") { %>View latest content<% } else { %>Viewing latest content<% } %></a><br>
+<a class="tlink" href="/ep/pad/view/<%= padId %>/<%= revisionId %>" thref="/ep/pad/view/<%= padId %>/rev.%revision%">Link to this version</a>
+<% if (readOnly === false) { %><br><a class="tlink" href="/ep/pad/view/<%= roPadId %>/<%= revisionId %>" thref="/ep/pad/view/<%= roPadId %>/rev.%revision%">Link to read-only page</a><br><a href="/<%= padId %>">Edit this pad</a><% } %>
+<h2>Download as</h2>
+<img src="/static/img/may09/html.gif"><a class="tlink" href="/ep/pad/export/<%= padId %>/<%= revisionId %>?format=html" thref="/ep/pad/export/<%= padId %>/rev.%revision%?format=html">HTML</a><br>
+<img src="/static/img/may09/txt.gif" ><a class="tlink" href="/ep/pad/export/<%= padId %>/<%= revisionId %>?format=html" thref="/ep/pad/export/<%= padId %>/rev.%revision%?format=txt" >Plain text</a><br>
+
+
+</div>
+<div id="legend">
+<h2>Authors</h2>
+<table id="authorstable" border="0" cellspacing="0" cellpadding="0">
+
+</table>
+</div>
+
+</div>
+<div id="padmain"
+ <% if (request.userAgent.isIPhone()) { %> style="font-size: 3em;" <% } %>
+>
+<div id="titlebar"><h1><%= padTitle %></h1><div id="revision"><span id="revision_label"><%= rlabel %></span><br><span id="revision_date">
+Saved
+<%= ["Jan", "Feb", "March", "April", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"][new Date(savedWhen).getMonth()] %>
+<%= new Date(savedWhen).getDate() %>,
+<%= new Date(savedWhen).getFullYear() %>
+</span></div></div>
+ <div id="padcontent"
+ <% if (request.userAgent.isIPhone()) { %> style="font-size: 1.3em;" <% } %>
+ >
+<%= padHTML %></div>
+</div>
+</div>
diff --git a/etherpad/src/themes/default/templates/page.ejs b/etherpad/src/themes/default/templates/page.ejs
new file mode 100644
index 0000000..3d1632a
--- /dev/null
+++ b/etherpad/src/themes/default/templates/page.ejs
@@ -0,0 +1,135 @@
+<% /*
+Copyright 2009 Google Inc.
+Copyright 2010 Pita, Peter Martischka
+
+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. */ %>
+<%
+ helpers.setBodyId("padbody");
+ helpers.addBodyClass(bodyClass);
+ helpers.includeCss("pad2_ejs.css");
+ helpers.includeJs("undo-xpopup.js");
+ helpers.includeCometJs();
+ helpers.includeJQuery();;
+ helpers.includeJs("json2.js");
+ helpers.includeJs("colorutils.js");
+ helpers.includeJs("draggable.js");
+ helpers.includeJs("pad_utils.js");
+ helpers.includeJs("pad_cookie.js");
+ helpers.includeJs("pad_editor.js");
+ helpers.includeJs("pad_editbar.js");
+ helpers.includeJs("pad_docbar.js");
+ helpers.includeJs("pad_modals.js");
+ helpers.includeJs("pad2.js");
+ helpers.suppressGA();
+ helpers.setRobotsPolicy({index: false, follow: false});
+
+%>
+
+<% template.define('body', function() { var ejs_data=''; %>
+ <div id="padpage">
+ <div id="padtop">
+ <div id="topbar">
+ <% /* floated left */ %>
+ <div id="topbarleft"><!-- --></div>
+ <% /* <a href="#" id="topbarnewpad">New Pad</a> */ %>
+ <% /* floated right */ %>
+ <div id="topbarright"><!-- --></div>
+ <% /* <a href="#" id="topbarfullwidth">Toggle Width</a> */ %>
+ <% /* non-floated */ %>
+ <div id="topbarcenter">
+ <a href="/" id="topbaretherpad">EtherPad</a>
+ </div>
+ <% if (isProAccountHolder) { %>
+ <a id="backtoprosite" href="/ep/padlist/">Return to pad list</a>
+ <div id="accountnav"><%= toHTML(account.email) %>
+ <a href="/ep/account/sign-out">(sign out)</a>
+ </div>
+ <% } else if (isPro) { %>
+ <div id="accountnav">
+ <a href="<%= signinUrl %>">sign in</a>
+ </div>
+ <% } %>
+ <div id="specialkeyarea"><!-- --></div>
+ </div>
+ <div id="alertbar">
+ <div id="servermsg">
+ <h3>Server Notice<span id="servermsgdate"><!-- --></span>:</h3>
+ <a id="hidetopmsg" href="javascript: void pad.hideServerMessage()">hide</a>
+ <p id="servermsgtext"><!-- --></p>
+ </div>
+ </div>
+
+ <div id="docbar">
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" id="docbartable">
+ <tr>
+ <td><img src="/static/img/jun09/pad/roundcorner_left.gif"></td>
+ <%: template.use('docBarTitle'); %>
+ <td width="100%">&nbsp;</td>
+ <%: template.use('docBarItems'); %>
+ <%: plugins.callHookStr('docbarItemsAll', {}, '', '', ''); %>
+ <td><img src="/static/img/jun09/pad/roundcorner_right_orange.gif"></td>
+ </tr>
+ </table>
+ <%: template.use('docBarTitleEditor'); %>
+ <%: template.use('docBarDropdowns'); %>
+ </div><!-- /docbar -->
+ </div>
+
+ <div id="padmain">
+ <div id="padsidebar"><%: template.use('sideBar'); %></div>
+
+ <div id="padeditor">
+ <div id="editbar" class="disabledtoolbar">
+ <% /* floated left */ %>
+ <div id="editbarleft"><!-- --></div>
+ <% /* floated right */ %>
+ <div id="editbarright"><!-- --></div>
+ <% /* non-floated */ %>
+ <div id="editbarinner">
+ <table cellpadding="0" cellspacing="0" border = "0" id="editbartable">
+ <tr>
+ <%: template.use('editBarItemsLeft'); %>
+ <td width="100%">&nbsp;</td>
+ </tr>
+ </table>
+ <table cellpadding="0" cellspacing="0" border = "0" id="editbarsavetable">
+ <tr>
+ <%: template.use('editBarItemsRight'); %>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <div id="editorcontainerbox"><%: template.use('contentArea'); %></div>
+ </div><!-- /padeditor -->
+
+ <div id="bottomarea">
+ <div id="viewbarcontents">
+ <div id="viewzoomtitle">Zoom:</div>
+ <select id="viewzoommenu"><option value="z85">85%</option><option value="z100">100%</option><option value="z115">115%</option><option value="z150">150%</option><option value="z200">200%</option><option value="z300">300%</option></select>
+ </div>
+
+ <div id="widthprefcheck"
+ class="<%= (prefs.isFullWidth?'widthprefchecked':'widthprefunchecked') %>"
+ ><!-- --></div>
+ <div id="sidebarcheck"
+ class="<%= (prefs.hideSidebar?'sidebarunchecked':'sidebarchecked') %>"
+ ><!-- --></div>
+ </div>
+
+ </div><!-- /padmain -->
+
+ </div><!-- /padpage -->
+
+ <%: template.use('modals'); %>
+
+<% return ejs_data; }); %>
diff --git a/etherpad/src/themes/default/templates/pro-account/recover.ejs b/etherpad/src/themes/default/templates/pro-account/recover.ejs
new file mode 100644
index 0000000..686fe3b
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro-account/recover.ejs
@@ -0,0 +1,48 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.includeCss("global-pro-account.css") %>
+
+<div class="fpcontent">
+ <div class="global-pro-account">
+
+ <h1>Recover Lost Account</h1>
+
+ <%= errorDiv() %>
+
+ <p>Enter your email address to recover your
+ account information</p>
+
+ <form id="global-sign-in"
+ action="<%= request.path %>" method="post">
+ <label for="email">Email Address:</label>
+ <input type="text" name="email" id="email" size="30"
+ <% if (oldData.email) { %>value="<%= oldData.email %>"<% } %>
+ />
+
+ <br/>
+ <button type="submit">Send account info</button>
+
+ </form>
+
+ <p><a href="/ep/pro-account/sign-in">&laquo; Back to sign
+ in</a></p>
+
+ <hr>
+
+ <p>New users: <a href="/ep/pro-signup/">create an account
+ instantly</a>.</p>
+
+ </div>
+</div>
+
+
diff --git a/etherpad/src/themes/default/templates/pro-account/sign-in.ejs b/etherpad/src/themes/default/templates/pro-account/sign-in.ejs
new file mode 100644
index 0000000..470bbc4
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro-account/sign-in.ejs
@@ -0,0 +1,57 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.includeCss("global-pro-account.css") %>
+
+<div class="fpcontent">
+ <div class="global-pro-account">
+
+ <h1>EtherPad Professional Sign In</h1>
+
+ <%= errorDiv() %>
+
+ <form id="global-sign-in"
+ action="<%= request.path %>" method="post">
+ <label for="email">Email Address:</label>
+ <input type="text" name="email" id="email" size="30"
+ <% if (oldData.email) { %>value="<%= oldData.email %>"<% } %>
+ />
+
+ <label for="password">Password:</label>
+ <input type="password" name="password" id="password" size="30" />
+
+ <label for="subDomain">Site Address:</label>
+ <input type="text" name="subDomain" id="subDomain" size="30"
+ <% if (oldData.subDomain) { %>value="<%= oldData.subDomain %>"<% } %>
+ />.<%= request.host %>/
+ <br/>
+ <button type="submit">Sign In</button>
+
+ <div class="tip">
+ <b>Tip:</b> you can also sign in by going directly to your site
+ address.
+ </div>
+
+ </form>
+
+ <p><a href="/ep/pro-account/recover">Recover lost password or
+ site address</a></p>
+
+ <hr/>
+
+ <p>New users: <a href="/ep/pro-signup/">create an account
+ instantly</a>.</p>
+
+ </div>
+</div>
+
+
diff --git a/etherpad/src/themes/default/templates/pro/account/account-welcome-email.ejs b/etherpad/src/themes/default/templates/pro/account/account-welcome-email.ejs
new file mode 100644
index 0000000..33e1ac5
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/account/account-welcome-email.ejs
@@ -0,0 +1,32 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+Dear <%= account.fullName %>,
+
+<% if (adminAccount) { %>
+<%= adminAccount.fullName %> has created an EtherPad account for you on <%=
+request.host %> (<%= siteName %>). You can sign in by clicking on the following link:
+<% } else { %>
+Thank you for signing up for EtherPad Professional Edition. You can sign in by clicking on the following link:
+<% } %>
+
+<%= signinLink %>
+
+For help signing in, or general support issues, please email support@pad.spline.inf.fu-berlin.de.
+
+--
+This email was sent to <%= toEmail %> from an EtherPad user.
+If you received it in error, you may safely ignore it.
+<%/* EtherPad's offices are located at Pier 38, The Embarcadero,
+San Francisco, CA 94107 */%>
+
diff --git a/etherpad/src/themes/default/templates/pro/account/forgot-password-email.ejs b/etherpad/src/themes/default/templates/pro/account/forgot-password-email.ejs
new file mode 100644
index 0000000..4595cee
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/account/forgot-password-email.ejs
@@ -0,0 +1,22 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+Dear <%= account.fullName %>,
+
+We received a request to reset your EtherPad password. To proceed, click the following link:
+
+<%= recoverUrl %>
+
+If you did not request a password reset, simply ignore this email.
+
+
diff --git a/etherpad/src/themes/default/templates/pro/account/forgot-password.ejs b/etherpad/src/themes/default/templates/pro/account/forgot-password.ejs
new file mode 100644
index 0000000..bbc78dd
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/account/forgot-password.ejs
@@ -0,0 +1,66 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.includeCss("pro/account.css") %>
+<% helpers.setHtmlTitle("EtherPad: Forgot Password") %>
+
+<div class="fpcontent">
+ <div class="account-container forgotpass-container">
+
+ <% var md = messageDiv(); %>
+ <% if (md) { %>
+ <%= md %>
+ <% } else { %>
+
+ <form action="<%= request.path + '?' + request.query %>"
+ method="post">
+
+ <div class="bb bb-forgotpass">
+ <div class="bb-top">
+ <div class="bb-topleft"><!-- --></div>
+ <div class="bb-topright"><!-- --></div>
+ <div class="bb-title">Recover Lost Password</div>
+ </div>
+ <div class="bb-in">
+
+ <%= errorDiv() %>
+
+ <div id="instructions">
+ Enter your email address and we will send you a link
+ to reset your password.
+ </div>
+
+ <div>
+ <label for="email" id="email-label">Email</label>
+ <input class="textin" type="text" name="email" id="email" value="<%= email
+ %>" />
+ <%= helpers.clearFloats() %>
+ </div>
+
+ <div>
+ <button type="submit" class="bluebutton
+ bluebutton120">
+ Send Email
+ </button>
+ <%= helpers.clearFloats() %>
+ </div>
+
+ </div>
+ </div>
+ </form>
+ <% } %>
+
+ <p><a href="/ep/account/sign-in">&laquo; Back to sign-in</a></p>
+
+ </div>
+</div>
+
diff --git a/etherpad/src/themes/default/templates/pro/account/my-account.ejs b/etherpad/src/themes/default/templates/pro/account/my-account.ejs
new file mode 100644
index 0000000..9634285
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/account/my-account.ejs
@@ -0,0 +1,67 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.includeCss("pro/account.css") %>
+<% helpers.setHtmlTitle("EtherPad: My Account") %>
+
+<div class="fpcontent">
+<div class="my-account">
+ <%= messageDiv() %>
+ <%= errorDiv() %>
+
+<% if (!changePass) { %>
+ <h2>My Info</h2>
+
+ <form method="post" action="/ep/account/update-info">
+ <table>
+ <tr>
+ <th>Full Name:</th>
+ <td class="ti"><%= INPUT({type: 'text', name: 'fullName',
+ value: account.fullName}) %></td>
+ </tr>
+ <tr>
+ <th>Email:</th>
+ <td class="ti"><%= INPUT({type: 'text', name: 'email', value:
+ account.email}) %></td>
+ </tr>
+ <tr>
+ <td colspan="2" style="text-align: right;">
+ <input type="submit" value="Update Info" />
+ </td>
+ </tr>
+ </table>
+ </form>
+<% } %>
+
+<h2>Password</h2>
+
+<form method="post" action="/ep/account/update-password">
+<table>
+ <tr>
+ <th>New Password:</th>
+ <td class="ti"><%= INPUT({type: 'password', name: 'password', value: ''}) %></td>
+ </tr>
+ <tr>
+ <th>Confirm Password:</th>
+ <td class="ti"><%= INPUT({type: 'password', name: 'passwordConfirm', value: ''}) %></td>
+ </tr>
+ <tr>
+ <td colspan="2" style="text-align: right;">
+ <input type="submit" id="passwordSubmit" value="Update Password" />
+ </td>
+ </tr>
+</table>
+</form>
+
+</div>
+</div>
+
diff --git a/etherpad/src/themes/default/templates/pro/account/signin.ejs b/etherpad/src/themes/default/templates/pro/account/signin.ejs
new file mode 100644
index 0000000..c67bea6
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/account/signin.ejs
@@ -0,0 +1,81 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.includeJQuery() %>
+<% helpers.includeJs("pro/signin-client.js") %>
+<% helpers.includeCss("pro/account.css") %>
+<% helpers.setHtmlTitle("EtherPad: Sign In") %>
+
+<div class="fpcontent">
+ <div class="account-container">
+
+ <%= signinNotice() %>
+
+ <form id="signin-form" action="<%= request.path + '?' + request.query %>" method="post">
+ <div class="bb bb-signin">
+ <div class="bb-top">
+ <div class="bb-topleft"><!-- --></div>
+ <div class="bb-topright"><!-- --></div>
+ <div class="bb-title">Sign In to <%= siteName %> EtherPad</div>
+ </div>
+ <div class="bb-in">
+ <%= errorDiv() %>
+ <div>
+ <label for="email" id="email-label">Email</label>
+ <input class="textin" type="text" name="email" id="email" value="<%= email
+ %>" />
+ <%= helpers.clearFloats() %>
+ </div>
+
+ <div>
+ <label for="password" id="password-label">Password</label>
+ <input class="passin" type="password" name="password"
+ id="password"
+ value="<%= password
+ %>" />
+ <%= helpers.clearFloats() %>
+ </div>
+
+ <div>
+ <input type="checkbox" id="rememberMe" name="rememberMe"
+ <%= (rememberMe ? 'checked="on"' : '') %> />
+ <label for="rememberMe" id="rememberMe-label">Remember me on this
+ computer</label>
+
+ <button type="submit" class="bluebutton bluebutton120" id="signInButton">
+ Sign In
+ </button>
+ <%= helpers.clearFloats() %>
+ </div>
+
+ </div>
+ </div>
+ </form>
+
+ <% if (showGuestBox) { %>
+ <form action="/ep/account/guest-sign-in"
+ id="guest-signin-choice"
+ method="get">
+ <input type="hidden" name="padId" value="<%= toHTML(localPadId) %>" />
+ Guests: <button type="submit">Request Guest Access</button>
+ </form>
+ <% } %>
+
+ <div id="bottom-text">
+ <a
+ href="/ep/account/forgot-password">Recover lost
+ password</a>
+ </div>
+
+ </div>
+</div>
+
diff --git a/etherpad/src/themes/default/templates/pro/admin/account-manager.ejs b/etherpad/src/themes/default/templates/pro/admin/account-manager.ejs
new file mode 100644
index 0000000..f1b443f
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/admin/account-manager.ejs
@@ -0,0 +1,59 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<% function fmtdate(d) {
+ if (!d) {
+ return "Never";
+ } else {
+ return d.toString().split(' ').slice(0,5).join(' ');
+ }
+} %>
+
+<h3 class="top">Accounts</h3>
+
+<div class="manage-accounts">
+
+ <%= messageDiv() %>
+ <%= warningDiv() %>
+
+ <p><a href="<%= request.path %>new">Create new account</a></p>
+
+ <% function renderAccountRow(u) {
+ var name = u.fullName;
+ return TR(TD(name),
+ TD(u.email),
+ TD(u.isAdmin ? 'Admin' : ''),
+ TD(fmtdate(u.lastLoginDate)),
+ TD(A({href: request.path + "account/"+u.id}, "Manage")))
+ }
+ %>
+
+ <table id="accountlist">
+ <tr>
+ <th width="99%">Name</th>
+ <th>Email</th>
+ <th>Role</th>
+ <th>Last Signed In</th>
+ <th>&nbsp;</th>
+ </tr>
+
+ <% accountList.forEach(function(u) { %>
+ <%= renderAccountRow(u) %>
+ <% }); %>
+
+ </table>
+
+ <p class="account-tally"><%= accountList.length %> account<%= accountList.length == 1 ? "" : "s" %>.</p>
+
+</div>
+
diff --git a/etherpad/src/themes/default/templates/pro/admin/admin-template.ejs b/etherpad/src/themes/default/templates/pro/admin/admin-template.ejs
new file mode 100644
index 0000000..a54964f
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/admin/admin-template.ejs
@@ -0,0 +1,31 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.setHtmlTitle("Etherpad Administration") %>
+<% helpers.includeCss("pro/pro-admin.css") %>
+
+<div class="fpcontent">
+ <table id="admin-layout-table">
+ <tr>
+ <td width="1%" id="admin-leftnav">
+ <%= renderAdminLeftNav() %>
+ </td>
+ <td width="99%" id="admin-right">
+ <%= getAdminContent() %>
+ </td>
+ </tr>
+ </table>
+
+</div>
+
+
+
diff --git a/etherpad/src/themes/default/templates/pro/admin/delete-account.ejs b/etherpad/src/themes/default/templates/pro/admin/delete-account.ejs
new file mode 100644
index 0000000..3de2122
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/admin/delete-account.ejs
@@ -0,0 +1,35 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><div id="delete-account-page">
+
+ <h3 class="top">Delete Account</h3>
+
+ <%= errorDiv() %>
+
+ <div class="confirm">Do you really want to delete this account?</div>
+
+ <div class="account-info"><%= account.fullName %> (<%= account.email %>)</div>
+
+ <form method="post" action="<%= request.path %>">
+ <input type="submit" name="delete" value="Delete" />
+ &nbsp;&nbsp;&nbsp;&nbsp;
+ <input type="submit" name="cancel" value="Cancel" />
+ </form>
+
+ <div class="note">When an account is deleted, some references to it may remain on the
+ site. For example, edits to pads by the deleted account will remain in the
+ pad's history. However, the deleted account will no longer be able to
+ sign in, and will not be counted toward your monthly quota.</div>
+
+</div>
+
diff --git a/etherpad/src/themes/default/templates/pro/admin/manage-account.ejs b/etherpad/src/themes/default/templates/pro/admin/manage-account.ejs
new file mode 100644
index 0000000..72529b4
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/admin/manage-account.ejs
@@ -0,0 +1,64 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %>
+<h3 class="top">Manage Account</h3>
+
+<div class="manage-accounts">
+
+ <%= errorDiv() %>
+
+ <form method="post" action="<%= request.path %>">
+
+
+ <table id="manage-account">
+ <tr>
+ <th>Email:</th>
+ <td><input type="text" name="newEmail" id="newEmail" value="<%=
+ account.email %>" /></td>
+ </tr>
+
+ <tr>
+ <th>Full Name:</th>
+ <td><input type="text" name="newFullName" id="newFullName" value="<%=
+ account.fullName %>" /></td>
+ </tr>
+
+ <tr>
+ <th><label for="newIsAdmin">Administrator?</label></th>
+ <td>
+ <input type="checkbox" name="newIsAdmin" id="newIsAdmin"
+ <%= (account.isAdmin ? "checked='true'" : '') %>
+ />
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="2" style="text-align: right;">
+ <a href="/ep/admin/account-manager/delete-account/<%=
+ account.id %>">Delete Account</a>
+ </td>
+ </tr>
+
+ </table>
+
+ <div style="padding: 1em;">
+ <input class="submit" type="submit" name="btn" value="Save" />
+ <input class="submit" type="submit" name="cancel" value="Cancel" />
+ </div>
+
+ </form>
+
+</div>
+
+
+
diff --git a/etherpad/src/themes/default/templates/pro/admin/new-account.ejs b/etherpad/src/themes/default/templates/pro/admin/new-account.ejs
new file mode 100644
index 0000000..2f2cccf
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/admin/new-account.ejs
@@ -0,0 +1,86 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% function formField(id, label, type) {
+ if (!type) { type = "text"; }
+ var val = (oldData[id] || "");
+
+ var d = DIV({className: "formfield"});
+
+ if (type == "checkbox") {
+ d.push(INPUT({type: type,
+ id: id,
+ name: id,
+ className: type+"input",
+ checked: (oldData[id] ? true : undefined)}),
+ LABEL({htmlFor: id, className: type+"label"}, label));
+ d.push(html('<div style="clear: both;"><!-- --></div>'));
+ } else if (type == "text") {
+ d.push(LABEL({className: type+"label", htmlFor: id}, label),
+ INPUT({className: type+"input",
+ type: type,
+ id: id,
+ name: id,
+ maxlength: 80,
+ value: val}));
+ } else if (type == "temppass") {
+ if (!val) {
+ val = stringutils.randomString(6).toUpperCase();
+ }
+ d.push(LABEL({className: type+"label", htmlFor: id}, label),
+ INPUT({className: type+"input",
+ type: "text",
+ id: id,
+ name: id,
+ maxlength: 80,
+ value: val
+ }));
+ }
+
+ return d;
+} %>
+
+<h3 class="top">Add new account</h3>
+
+<div class="manage-accounts newaccount">
+
+ <%= errorDiv() %>
+
+ <form method="post" action="<%= request.path %>">
+
+ <div class="new-account-form">
+
+ <div class="forminner">
+ <%= formField('email', 'Email:', 'text') %>
+ <%= formField('fullName', 'Full Name:', 'text') %>
+ <%= formField('tempPass', 'Temporary Password:', 'temppass') %>
+ <%= formField('makeAdmin', 'Make this account an administrator?', 'checkbox') %>
+ </div>
+
+ </div>
+ <br/><br/>
+ <div class="buttons-wrap">
+ <input class="submit" type="submit" name="btn" value="Create Account" />
+ <input class="submit" type="submit" name="cancel" value="Cancel" />
+ </div>
+
+ </form>
+
+ <p id="bottom-note">An email will be sent to this account with a link to sign in.
+ They will be prompted to change their password the first time they sign in.</p>
+ </p>
+
+</div>
+
+<script>document.getElementById('email').focus()</script>
+
+
diff --git a/etherpad/src/themes/default/templates/pro/padlist/pro-padlist.ejs b/etherpad/src/themes/default/templates/pro/padlist/pro-padlist.ejs
new file mode 100644
index 0000000..b762679
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/padlist/pro-padlist.ejs
@@ -0,0 +1,49 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.includeCss("lib/jquery.contextmenu.css") %>
+<% helpers.includeCss("pro/padlist.css") %>
+
+<% helpers.includeJQuery() %>
+<% helpers.includeJs("lib/jquery.contextmenu.js") %>
+<% helpers.includeJs("pro/pro-padlist-client.js") %>
+
+<% helpers.setHtmlTitle("Pad List - " + orgName + " - EtherPad") %>
+
+<div class="fpcontent">
+
+ <%= renderPadNav() %>
+ <%= renderNotice() %>
+ <%= renderShowingDesc(padList.length) %>
+
+ <% if (padList.length > 0) { %>
+ <%= renderPadList() %>
+ <p style="font-size: .8em;"><i><%= padList.length %> pad<% if (padList.length > 1) { %>s<% } %></i> <% if (isAdmin) { %>(<a href="/ep/padlist/all-pads.zip">Download all pads as a ZIP archive</a>.) <% } %>
+</p>
+
+ <% } else { %>
+ <p>No pads in this list.</p>
+ <% } %>
+
+</div>
+
+<form action="/ep/padlist/delete" method="post" id="delete-pad" style="display: none;">
+ <input type="hidden" name="returnPath" value="<%= request.url %>" />
+ <input id="padIdToDelete" name="padIdToDelete" type="hidden" value="-" />
+</form>
+
+<form action="/ep/padlist/toggle-archive" method="post" id="toggle-archive-pad" style="display: none;">
+ <input type="hidden" name="returnPath" value="<%= request.url %>" />
+ <input id="padIdToToggleArchive" name="padIdToToggleArchive" type="hidden" value="-" />
+</form>
+
+
diff --git a/etherpad/src/themes/default/templates/pro/pro_home.ejs b/etherpad/src/themes/default/templates/pro/pro_home.ejs
new file mode 100644
index 0000000..bcf7443
--- /dev/null
+++ b/etherpad/src/themes/default/templates/pro/pro_home.ejs
@@ -0,0 +1,103 @@
+<% /* Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. */ %><% helpers.setHtmlTitle(orgName + " - EtherPad"); %>
+<% helpers.includeJQuery() %>
+<% helpers.includeJs("etherpad.js") %>
+<% helpers.includeCss("pro/pro-home.css"); %>
+<% helpers.includeCss("pro/padlist.css"); %>
+
+<div class="fpcontent">
+
+ <div id="welcome-msg">
+ Welcome <%= account.fullName %>
+ <% if (account.isAdmin) { %>(Administrator)<% } %>
+ <br/>
+ <br/>
+ </div>
+
+
+ <div id="homeright">
+ <a href="/ep/pad/newpad">
+ <img src="/static/img/davy/btn/createpad-small.gif" alt="Create new pad" />
+ </a>
+
+ <% if (livePads.length > 0) { %>
+ <div id="live-pads">
+ <h3>Live Pads (currently being edited)</h3>
+ <div id="listwrap">
+ <%= renderLivePads() %>
+ </div>
+ </div>
+ <% } %>
+
+ <% if (recentPads.length > 0) { %>
+ <div id="recent-pads">
+ <h3>Your Recent Pads:</h3>
+ <div id="listwrap">
+ <%= renderRecentPads() %>
+ </div>
+ <a id="viewall" href="/ep/padlist/">View all pads...</a>
+ <div style="clear:both"><!-- --></div>
+ </div>
+ <% } %>
+
+ </div>
+
+ <div id="homeleft">
+ <div id="homeleft-title">
+ Latest News
+ </div>
+
+ <div class="news-time-sep">
+ <div class="date">
+ June 17th, 2009
+ </div>
+ <div class="line"><!-- --></div>
+ </div>
+
+ <div class="news-item">
+ <p>Welcome to your EtherPad Beta Account! Please report bugs by
+ sending email to <%= helpers.oemail("bugs") %>.
+
+ <p>We hope you enjoy EtherPad!</p>
+
+ <p>Sincerely,</p>
+
+ <p>Spline</p>
+ </p>
+ </div>
+
+ </div>
+
+
+ </div><!-- /homeleft -->
+
+ <%= helpers.clearFloats() %>
+
+ <% if (isPNE) { %>
+ <div id="version-info"
+ style="margin-top: 2em; font-size: 76%; color: #444; text-align: right;">
+ <br/>
+ EtherPad Private Network Edition (PNE)
+ Version <%= pneVersion %><br/>
+
+ <% if (isEvaluation && evalExpDate) { %>
+ <br/>
+ <span style="color: #c22;">EVALUATION EDITION: Expires <%= evalExpDate.toString()
+ %>.<br/>
+ <% } %>
+ </div>
+ <% } %>
+
+</div>
+