From d7c5ad7d6263fd1baf9bfdbaa4c50b70ef2fbdb2 Mon Sep 17 00:00:00 2001 From: Alexander Sulfrian Date: Tue, 8 Jun 2010 08:22:05 +0200 Subject: reverted folder structure change for better mergeing with upstream --- trunk/etherpad/src/etherpad/admin/shell.js | 127 + trunk/etherpad/src/etherpad/billing/billing.js | 800 ++++ trunk/etherpad/src/etherpad/billing/fields.js | 219 + .../etherpad/src/etherpad/billing/team_billing.js | 422 ++ .../src/etherpad/collab/ace/contentcollector.js | 527 +++ trunk/etherpad/src/etherpad/collab/ace/domline.js | 210 + .../etherpad/src/etherpad/collab/ace/easysync1.js | 923 +++++ .../etherpad/src/etherpad/collab/ace/easysync2.js | 1968 +++++++++ .../src/etherpad/collab/ace/easysync2_tests.js | 877 ++++ .../src/etherpad/collab/ace/linestylefilter.js | 253 ++ .../etherpad/src/etherpad/collab/collab_server.js | 778 ++++ .../src/etherpad/collab/collabroom_server.js | 359 ++ trunk/etherpad/src/etherpad/collab/genimg.js | 55 + .../etherpad/src/etherpad/collab/json_sans_eval.js | 178 + .../src/etherpad/collab/readonly_server.js | 174 + trunk/etherpad/src/etherpad/collab/server_utils.js | 204 + .../etherpad/src/etherpad/control/aboutcontrol.js | 263 ++ .../etherpad/src/etherpad/control/admincontrol.js | 1471 +++++++ trunk/etherpad/src/etherpad/control/blogcontrol.js | 199 + .../control/connection_diagnostics_control.js | 87 + .../etherpad/control/global_pro_account_control.js | 143 + .../src/etherpad/control/historycontrol.js | 226 + .../src/etherpad/control/loadtestcontrol.js | 93 + trunk/etherpad/src/etherpad/control/maincontrol.js | 54 + .../etherpad/control/pad/pad_changeset_control.js | 280 ++ .../src/etherpad/control/pad/pad_control.js | 780 ++++ .../control/pad/pad_importexport_control.js | 319 ++ .../src/etherpad/control/pad/pad_view_control.js | 287 ++ .../src/etherpad/control/pne_manual_control.js | 75 + .../src/etherpad/control/pne_tracker_control.js | 48 + .../src/etherpad/control/pro/account_control.js | 369 ++ .../control/pro/admin/account_manager_control.js | 260 ++ .../control/pro/admin/license_manager_control.js | 128 + .../control/pro/admin/pro_admin_control.js | 283 ++ .../control/pro/admin/pro_config_control.js | 54 + .../control/pro/admin/team_billing_control.js | 447 ++ .../src/etherpad/control/pro/pro_main_control.js | 150 + .../etherpad/control/pro/pro_padlist_control.js | 200 + .../src/etherpad/control/pro_beta_control.js | 136 + .../src/etherpad/control/pro_signup_control.js | 173 + .../etherpad/src/etherpad/control/scriptcontrol.js | 75 + .../src/etherpad/control/static_control.js | 65 + .../etherpad/src/etherpad/control/statscontrol.js | 1214 ++++++ .../control/store/eepnet_checkout_control.js | 757 ++++ .../src/etherpad/control/store/storecontrol.js | 201 + trunk/etherpad/src/etherpad/control/testcontrol.js | 74 + .../src/etherpad/db_migrations/m0000_test.js | 23 + .../db_migrations/m0001_eepnet_signups_init.js | 38 + .../db_migrations/m0002_eepnet_signups_2.js | 47 + .../db_migrations/m0003_create_tests_table_v2.js | 29 + .../m0004_convert_all_tables_to_innodb.js | 38 + .../db_migrations/m0005_create_billing_tables.js | 73 + .../db_migrations/m0006_eepnet_signups_3.js | 29 + .../db_migrations/m0007_create_pro_tables_v4.js | 67 + .../db_migrations/m0008_persistent_vars.js | 31 + .../src/etherpad/db_migrations/m0009_pad_tables.js | 31 + .../etherpad/db_migrations/m0010_pad_sqlmeta.js | 71 + .../db_migrations/m0011_pro_users_temppass.js | 33 + .../db_migrations/m0012_pro_users_auto_signin.js | 30 + .../db_migrations/m0013_pne_padv2_upgrade.js | 54 + .../db_migrations/m0014_pne_globalpadids.js | 102 + .../db_migrations/m0015_padmeta_passwords.js | 25 + .../db_migrations/m0016_pne_tracking_data.js | 35 + .../db_migrations/m0017_pne_tracking_data_v2.js | 30 + .../db_migrations/m0018_eepnet_checkout_tables.js | 82 + .../db_migrations/m0019_padmeta_deleted.js | 24 + .../db_migrations/m0020_padmeta_archived.js | 25 + .../db_migrations/m0021_pro_padmeta_json.js | 57 + .../db_migrations/m0022_create_userids_table.js | 30 + .../db_migrations/m0023_create_usagestats_table.js | 32 + .../db_migrations/m0024_statistics_table.js | 42 + .../db_migrations/m0025_rename_pro_users_table.js | 26 + .../db_migrations/m0026_create_guests_table.js | 37 + .../src/etherpad/db_migrations/m0027_pro_config.js | 27 + .../db_migrations/m0028_ondemand_beta_emails.js | 29 + .../db_migrations/m0029_lowercase_subdomains.js | 31 + .../db_migrations/m0030_fix_statistics_values.js | 26 + .../db_migrations/m0031_deleted_pro_users.js | 24 + .../db_migrations/m0032_reduce_topvalues_counts.js | 39 + .../db_migrations/m0033_pro_account_usage.js | 30 + .../m0034_create_recurring_billing_table.js | 42 + .../m0035_add_email_to_paymentinfo.js | 28 + .../m0036_create_missing_subscription_records.js | 45 + .../m0037_create_pro_referral_table.js | 32 + .../db_migrations/m0038_pad_coarse_revs.js | 26 + .../src/etherpad/db_migrations/migration_runner.js | 147 + trunk/etherpad/src/etherpad/debug.js | 26 + trunk/etherpad/src/etherpad/globals.js | 41 + trunk/etherpad/src/etherpad/helpers.js | 276 ++ .../src/etherpad/importexport/importexport.js | 241 ++ trunk/etherpad/src/etherpad/legacy_urls.js | 37 + trunk/etherpad/src/etherpad/licensing.js | 163 + trunk/etherpad/src/etherpad/log.js | 255 ++ trunk/etherpad/src/etherpad/metrics/metrics.js | 438 ++ trunk/etherpad/src/etherpad/pad/activepads.js | 52 + trunk/etherpad/src/etherpad/pad/chatarchive.js | 67 + trunk/etherpad/src/etherpad/pad/dbwriter.js | 338 ++ .../src/etherpad/pad/easysync2migration.js | 675 +++ trunk/etherpad/src/etherpad/pad/exporthtml.js | 383 ++ trunk/etherpad/src/etherpad/pad/importhtml.js | 230 + trunk/etherpad/src/etherpad/pad/model.js | 651 +++ trunk/etherpad/src/etherpad/pad/noprowatcher.js | 110 + trunk/etherpad/src/etherpad/pad/pad_migrations.js | 206 + trunk/etherpad/src/etherpad/pad/pad_security.js | 237 ++ trunk/etherpad/src/etherpad/pad/padevents.js | 170 + trunk/etherpad/src/etherpad/pad/padusers.js | 397 ++ trunk/etherpad/src/etherpad/pad/padutils.js | 154 + trunk/etherpad/src/etherpad/pad/revisions.js | 103 + trunk/etherpad/src/etherpad/pne/pne_utils.js | 187 + trunk/etherpad/src/etherpad/pro/domains.js | 141 + .../src/etherpad/pro/pro_account_auto_signin.js | 101 + trunk/etherpad/src/etherpad/pro/pro_accounts.js | 496 +++ trunk/etherpad/src/etherpad/pro/pro_config.js | 92 + trunk/etherpad/src/etherpad/pro/pro_pad_db.js | 232 ++ trunk/etherpad/src/etherpad/pro/pro_pad_editors.js | 104 + trunk/etherpad/src/etherpad/pro/pro_padlist.js | 289 ++ trunk/etherpad/src/etherpad/pro/pro_padmeta.js | 111 + trunk/etherpad/src/etherpad/pro/pro_quotas.js | 141 + trunk/etherpad/src/etherpad/pro/pro_utils.js | 165 + trunk/etherpad/src/etherpad/quotas.js | 50 + trunk/etherpad/src/etherpad/sessions.js | 203 + .../etherpad/src/etherpad/statistics/exceptions.js | 231 ++ .../etherpad/src/etherpad/statistics/statistics.js | 1248 ++++++ trunk/etherpad/src/etherpad/store/checkout.js | 300 ++ .../etherpad/src/etherpad/store/eepnet_checkout.js | 101 + trunk/etherpad/src/etherpad/store/eepnet_trial.js | 241 ++ trunk/etherpad/src/etherpad/testing/testutils.js | 23 + .../src/etherpad/testing/unit_tests/t0000_test.js | 22 + .../t0001_sqlbase_transaction_rollback.js | 48 + .../testing/unit_tests/t0002_license_generation.js | 89 + .../testing/unit_tests/t0003_persistent_vars.js | 42 + .../etherpad/testing/unit_tests/t0004_sqlobj.js | 214 + .../etherpad/testing/unit_tests/t0005_easysync.js | 22 + .../src/etherpad/usage_stats/usage_stats.js | 162 + trunk/etherpad/src/etherpad/utils.js | 396 ++ trunk/etherpad/src/main.js | 418 ++ trunk/etherpad/src/static/crossdomain.xml | 12 + .../etherpad/src/static/css/admin/admin-stats.css | 183 + trunk/etherpad/src/static/css/beta.css | 49 + trunk/etherpad/src/static/css/broadcast.css | 386 ++ .../src/static/css/connection_diagnostics.css | 13 + trunk/etherpad/src/static/css/etherpad.css | 770 ++++ trunk/etherpad/src/static/css/fluxbb.css | 55 + trunk/etherpad/src/static/css/framedpage.css | 175 + .../etherpad/src/static/css/global-pro-account.css | 52 + trunk/etherpad/src/static/css/home-opensource.css | 44 + trunk/etherpad/src/static/css/home.css | 264 ++ .../src/static/css/lib/jquery.contextmenu.css | 244 ++ trunk/etherpad/src/static/css/pad.css | 1000 +++++ trunk/etherpad/src/static/css/pad2_ejs.css | 889 ++++ trunk/etherpad/src/static/css/pne-manual.css | 143 + trunk/etherpad/src/static/css/pricing.css | 153 + trunk/etherpad/src/static/css/pro-signup.css | 69 + trunk/etherpad/src/static/css/pro/account.css | 254 ++ .../etherpad/src/static/css/pro/framedpage-pro.css | 125 + trunk/etherpad/src/static/css/pro/padlist.css | 115 + .../src/static/css/pro/payment-required.css | 39 + trunk/etherpad/src/static/css/pro/pro-admin.css | 343 ++ trunk/etherpad/src/static/css/pro/pro-home.css | 65 + trunk/etherpad/src/static/css/stats.css | 71 + .../src/static/css/store/eepnet-checkout.css | 284 ++ .../src/static/css/store/ondemand-billing.css | 170 + trunk/etherpad/src/static/css/store/store.css | 90 + trunk/etherpad/src/static/favicon.ico | Bin 0 -> 1354 bytes .../src/static/img/about/appjet-logo-large.gif | Bin 0 -> 11045 bytes .../src/static/img/about/appjet-logo-medium.png | Bin 0 -> 4127 bytes .../src/static/img/about/investors/mitchkapor.jpg | Bin 0 -> 30223 bytes .../etherpad/src/static/img/about/investors/pb.jpg | Bin 0 -> 23929 bytes .../etherpad/src/static/img/about/investors/pg.jpg | Bin 0 -> 28915 bytes .../src/static/img/about/investors/sanjeev.jpg | Bin 0 -> 23342 bytes .../src/static/img/about/investors/seth.jpg | Bin 0 -> 27346 bytes .../img/about/people/aaron-david-iphones-thumb.jpg | Bin 0 -> 145654 bytes .../img/about/people/aaron-david-iphones.jpg | Bin 0 -> 145654 bytes .../static/img/about/people/aaron-google-air.jpg | Bin 0 -> 163592 bytes .../img/about/people/aaron-headshot-thumb.jpg | Bin 0 -> 11310 bytes .../src/static/img/about/people/aaron-headshot.jpg | Bin 0 -> 83517 bytes .../img/about/people/aaron-headshot2-thumb.jpg | Bin 0 -> 29884 bytes .../static/img/about/people/aaron-headshot2.jpg | Bin 0 -> 99738 bytes .../img/about/people/aaron-headshot3-thumb.jpg | Bin 0 -> 27864 bytes .../static/img/about/people/aaron-headshot3.jpg | Bin 0 -> 195114 bytes .../img/about/people/daniel-headshot-thumb.jpg | Bin 0 -> 21991 bytes .../img/about/people/david-headshot-thumb.jpg | Bin 0 -> 18650 bytes .../src/static/img/about/people/david-headshot.jpg | Bin 0 -> 63296 bytes .../src/static/img/about/people/davy-headshot.jpg | Bin 0 -> 20175 bytes .../static/img/about/people/jd-headshot-thumb.jpg | Bin 0 -> 13008 bytes .../src/static/img/about/people/jd-headshot.jpg | Bin 0 -> 37777 bytes .../img/about/people/rhonda-headshot-thumb.jpg | Bin 0 -> 17360 bytes .../static/img/about/people/rhonda-headshot.jpg | Bin 0 -> 133259 bytes trunk/etherpad/src/static/img/about/pier38.png | Bin 0 -> 66877 bytes .../etherpad/src/static/img/about/quote-close.png | Bin 0 -> 1361 bytes trunk/etherpad/src/static/img/about/quote-open.png | Bin 0 -> 1341 bytes .../static/img/about/screencastpreview800x600.jpg | Bin 0 -> 248364 bytes trunk/etherpad/src/static/img/account/betawarn.jpg | Bin 0 -> 13535 bytes trunk/etherpad/src/static/img/acecarets/000000.gif | Bin 0 -> 41 bytes trunk/etherpad/src/static/img/acecarets/666666.gif | Bin 0 -> 41 bytes trunk/etherpad/src/static/img/acecarets/999999.gif | Bin 0 -> 41 bytes .../etherpad/src/static/img/acecarets/default.gif | Bin 0 -> 43 bytes trunk/etherpad/src/static/img/apr09/backgrad.png | Bin 0 -> 2276 bytes trunk/etherpad/src/static/img/apr09/black35.png | Bin 0 -> 221 bytes trunk/etherpad/src/static/img/apr09/blank.gif | Bin 0 -> 129 bytes trunk/etherpad/src/static/img/apr09/modalbar.gif | Bin 0 -> 145 bytes trunk/etherpad/src/static/img/apr09/newpadicon.gif | Bin 0 -> 89 bytes trunk/etherpad/src/static/img/apr09/shadbot.png | Bin 0 -> 149 bytes trunk/etherpad/src/static/img/apr09/shadleft.png | Bin 0 -> 142 bytes .../etherpad/src/static/img/apr09/shadleftbot.png | Bin 0 -> 172 bytes .../etherpad/src/static/img/apr09/shadlefttop.png | Bin 0 -> 929 bytes trunk/etherpad/src/static/img/apr09/shadright.png | Bin 0 -> 136 bytes .../etherpad/src/static/img/apr09/shadrightbot.png | Bin 0 -> 174 bytes .../etherpad/src/static/img/apr09/shadrighttop.png | Bin 0 -> 954 bytes trunk/etherpad/src/static/img/apr09/topbar.gif | Bin 0 -> 180 bytes trunk/etherpad/src/static/img/apr09/topbarlogo.gif | Bin 0 -> 1784 bytes trunk/etherpad/src/static/img/apr09/widthfull.gif | Bin 0 -> 104 bytes .../src/static/img/apr09/widthfullactive.gif | Bin 0 -> 104 bytes trunk/etherpad/src/static/img/apr09/widthlim.gif | Bin 0 -> 102 bytes .../src/static/img/apr09/widthlimactive.gif | Bin 0 -> 102 bytes trunk/etherpad/src/static/img/billing/amex.gif | Bin 0 -> 995 bytes .../etherpad/src/static/img/billing/creditcard.gif | Bin 0 -> 1229 bytes trunk/etherpad/src/static/img/billing/csc-help.gif | Bin 0 -> 9430 bytes trunk/etherpad/src/static/img/billing/disc.gif | Bin 0 -> 370 bytes trunk/etherpad/src/static/img/billing/invoice.gif | Bin 0 -> 424 bytes trunk/etherpad/src/static/img/billing/mc.gif | Bin 0 -> 1370 bytes trunk/etherpad/src/static/img/billing/paypal.gif | Bin 0 -> 812 bytes trunk/etherpad/src/static/img/billing/visa.gif | Bin 0 -> 724 bytes .../img/blog/posts/new-features/fullwidth.gif | Bin 0 -> 7328 bytes .../img/blog/posts/new-features/importexport.gif | Bin 0 -> 9758 bytes .../img/blog/posts/new-features/richtext.gif | Bin 0 -> 2146 bytes .../img/blog/posts/new-features/viewzoom.gif | Bin 0 -> 8257 bytes .../img/blog/posts/pricing-survey-results.png | Bin 0 -> 12994 bytes .../src/static/img/blog/posts/pricing-survey.png | Bin 0 -> 10589 bytes .../img/blog/posts/time-slider-screenshot.gif | Bin 0 -> 6544 bytes .../src/static/img/davy/bg/home-createpad.png | Bin 0 -> 4327 bytes .../static/img/davy/bg/home-features-bottom.gif | Bin 0 -> 308 bytes .../img/davy/bg/home-features-free-bottom.gif | Bin 0 -> 298 bytes .../static/img/davy/bg/home-features-paid-top.gif | Bin 0 -> 459 bytes .../src/static/img/davy/bg/home-features-top.gif | Bin 0 -> 489 bytes .../src/static/img/davy/bg/home-nav-selected.png | Bin 0 -> 195 bytes .../src/static/img/davy/bg/home-screencast.png | Bin 0 -> 5738 bytes trunk/etherpad/src/static/img/davy/bg/home2.png | Bin 0 -> 367 bytes .../img/davy/bg/product-nav-selected-white.png | Bin 0 -> 196 bytes .../static/img/davy/bg/product-nav-selected.png | Bin 0 -> 198 bytes trunk/etherpad/src/static/img/davy/bg/product.png | Bin 0 -> 161 bytes .../src/static/img/davy/btn/createpad-home.gif | Bin 0 -> 5839 bytes .../src/static/img/davy/btn/createpad-large.gif | Bin 0 -> 6614 bytes .../src/static/img/davy/btn/createpad-small.gif | Bin 0 -> 3148 bytes .../src/static/img/davy/btn/intro-screencast.png | Bin 0 -> 815 bytes .../src/static/img/davy/btn/intro-testimonials.png | Bin 0 -> 816 bytes .../etherpad/src/static/img/davy/btn/learnmore.gif | Bin 0 -> 3103 bytes .../src/static/img/davy/btn/signup-home-2.gif | Bin 0 -> 5827 bytes .../src/static/img/davy/btn/signup-home-3.gif | Bin 0 -> 5735 bytes .../src/static/img/davy/btn/signup-home-4.gif | Bin 0 -> 5818 bytes .../src/static/img/davy/btn/signup-home.gif | Bin 0 -> 5815 bytes .../etherpad/src/static/img/davy/btn/uses-more.gif | Bin 0 -> 1485 bytes trunk/etherpad/src/static/img/davy/gfx/32/114.png | Bin 0 -> 2424 bytes trunk/etherpad/src/static/img/davy/gfx/32/15.png | Bin 0 -> 2904 bytes trunk/etherpad/src/static/img/davy/gfx/32/65.png | Bin 0 -> 3028 bytes trunk/etherpad/src/static/img/davy/gfx/32/78.png | Bin 0 -> 2208 bytes trunk/etherpad/src/static/img/davy/gfx/bullet.gif | Bin 0 -> 55 bytes .../src/static/img/davy/gfx/home-logo2.gif | Bin 0 -> 7136 bytes .../src/static/img/davy/gfx/home-screencast.png | Bin 0 -> 65832 bytes trunk/etherpad/src/static/img/davy/gfx/plane.gif | Bin 0 -> 56 bytes .../src/static/img/davy/gfx/product-logo.gif | Bin 0 -> 2222 bytes .../src/static/img/davy/gfx/screenshot.gif | Bin 0 -> 23708 bytes .../src/static/img/davy/gfx/use-meetings.gif | Bin 0 -> 10521 bytes .../src/static/img/davy/gfx/use-meetings.png | Bin 0 -> 25506 bytes .../src/static/img/davy/gfx/use-programming.gif | Bin 0 -> 13655 bytes .../src/static/img/davy/gfx/use-programming.png | Bin 0 -> 56154 bytes .../src/static/img/davy/gfx/use-writing.gif | Bin 0 -> 22275 bytes .../src/static/img/davy/gfx/use-writing.png | Bin 0 -> 38264 bytes .../src/static/img/davy/txt/home-button.gif | Bin 0 -> 4749 bytes trunk/etherpad/src/static/img/featuretour/code.gif | Bin 0 -> 27794 bytes .../etherpad/src/static/img/featuretour/edits.gif | Bin 0 -> 24788 bytes .../src/static/img/featuretour/editsandusers.gif | Bin 0 -> 21815 bytes .../src/static/img/featuretour/padlock.png | Bin 0 -> 13061 bytes .../src/static/img/featuretour/revisions.gif | Bin 0 -> 20820 bytes .../etherpad/src/static/img/featuretour/users.gif | Bin 0 -> 8843 bytes .../src/static/img/feb09/framedheaderback.gif | Bin 0 -> 606 bytes .../src/static/img/feb09/framedheaderlogo.gif | Bin 0 -> 8177 bytes .../etherpad/src/static/img/feb09/home_firstp.gif | Bin 0 -> 7754 bytes .../etherpad/src/static/img/feb09/home_firstp.png | Bin 0 -> 15320 bytes .../etherpad/src/static/img/feb09/home_firstp2.gif | Bin 0 -> 7919 bytes trunk/etherpad/src/static/img/feb09/home_h1.gif | Bin 0 -> 11332 bytes trunk/etherpad/src/static/img/feb09/home_h1.png | Bin 0 -> 28857 bytes .../src/static/img/feb09/home_newpadbutton.gif | Bin 0 -> 6828 bytes .../src/static/img/feb09/home_newpadbutton.png | Bin 0 -> 10554 bytes .../src/static/img/feb09/home_newpadbutton2.gif | Bin 0 -> 7112 bytes .../static/img/feb09/home_newpadbutton_eepnet.gif | Bin 0 -> 5442 bytes .../etherpad/src/static/img/feb09/hometop_back.gif | Bin 0 -> 2743 bytes trunk/etherpad/src/static/img/feb09/nav1.gif | Bin 0 -> 10901 bytes trunk/etherpad/src/static/img/feb09/nav1_back.gif | Bin 0 -> 150 bytes trunk/etherpad/src/static/img/feb09/nav2.gif | Bin 0 -> 17028 bytes trunk/etherpad/src/static/img/feb09/screencast.gif | Bin 0 -> 20091 bytes .../src/static/img/home/etherpad-mainheader1.jpg | Bin 0 -> 48871 bytes .../src/static/img/home/headergradient.gif | Bin 0 -> 246 bytes trunk/etherpad/src/static/img/home/homeheader1.jpg | Bin 0 -> 33227 bytes trunk/etherpad/src/static/img/home/homeheader2.jpg | Bin 0 -> 33259 bytes trunk/etherpad/src/static/img/home/leftgrad.gif | Bin 0 -> 113 bytes .../src/static/img/home/pencilpaperback.png | Bin 0 -> 83487 bytes .../src/static/img/home/screencapture1.gif | Bin 0 -> 106694 bytes .../etherpad/src/static/img/home/underdevicon.gif | Bin 0 -> 98 bytes trunk/etherpad/src/static/img/icon/downarrow.gif | Bin 0 -> 376 bytes trunk/etherpad/src/static/img/icon/feed.gif | Bin 0 -> 1135 bytes .../etherpad/src/static/img/jun09/pad/backgrad.gif | Bin 0 -> 697 bytes .../src/static/img/jun09/pad/bottomareagfx.gif | Bin 0 -> 1045 bytes .../src/static/img/jun09/pad/colorpicker.gif | Bin 0 -> 1806 bytes .../src/static/img/jun09/pad/connectingbar.gif | Bin 0 -> 10819 bytes .../static/img/jun09/pad/connectionindicator.gif | Bin 0 -> 1185 bytes .../src/static/img/jun09/pad/docbarstates.png | Bin 0 -> 3314 bytes .../src/static/img/jun09/pad/docbarstates2.png | Bin 0 -> 4902 bytes .../src/static/img/jun09/pad/docbarstates3.png | Bin 0 -> 4990 bytes .../src/static/img/jun09/pad/docpaneledge.png | Bin 0 -> 589 bytes .../src/static/img/jun09/pad/docpaneledge2.png | Bin 0 -> 635 bytes .../src/static/img/jun09/pad/docpanelmiddle.png | Bin 0 -> 240 bytes .../src/static/img/jun09/pad/docpanelmiddle2.png | Bin 0 -> 295 bytes .../etherpad/src/static/img/jun09/pad/editbar.gif | Bin 0 -> 4667 bytes .../etherpad/src/static/img/jun09/pad/editbar2.gif | Bin 0 -> 9156 bytes .../etherpad/src/static/img/jun09/pad/editbar3.png | Bin 0 -> 16869 bytes .../etherpad/src/static/img/jun09/pad/editbar3.xcf | Bin 0 -> 79212 bytes .../src/static/img/jun09/pad/editbarback.gif | Bin 0 -> 368 bytes .../src/static/img/jun09/pad/feedbackbox2.gif | Bin 0 -> 6262 bytes .../src/static/img/jun09/pad/fileicons.gif | Bin 0 -> 1397 bytes .../etherpad/src/static/img/jun09/pad/hdraggie.gif | Bin 0 -> 453 bytes .../src/static/img/jun09/pad/inviteshare.gif | Bin 0 -> 511 bytes .../src/static/img/jun09/pad/inviteshare2.gif | Bin 0 -> 1836 bytes .../src/static/img/jun09/pad/layoutbuttons.gif | Bin 0 -> 3750 bytes .../etherpad/src/static/img/jun09/pad/overlay.png | Bin 0 -> 141 bytes .../etherpad/src/static/img/jun09/pad/overlay2.png | Bin 0 -> 149 bytes trunk/etherpad/src/static/img/jun09/pad/padtop.gif | Bin 0 -> 8055 bytes .../etherpad/src/static/img/jun09/pad/padtop2.gif | Bin 0 -> 6168 bytes .../etherpad/src/static/img/jun09/pad/padtop3.gif | Bin 0 -> 7511 bytes .../etherpad/src/static/img/jun09/pad/padtop4.gif | Bin 0 -> 8192 bytes .../etherpad/src/static/img/jun09/pad/padtop4.png | Bin 0 -> 17161 bytes .../etherpad/src/static/img/jun09/pad/padtop4.xcf | Bin 0 -> 41184 bytes .../etherpad/src/static/img/jun09/pad/padtop5.png | Bin 0 -> 18850 bytes .../etherpad/src/static/img/jun09/pad/padtop5.xcf | Bin 0 -> 66525 bytes .../src/static/img/jun09/pad/padtopback.gif | Bin 0 -> 553 bytes .../src/static/img/jun09/pad/padtopback2.gif | Bin 0 -> 384 bytes trunk/etherpad/src/static/img/jun09/pad/protop.png | Bin 0 -> 6768 bytes trunk/etherpad/src/static/img/jun09/pad/protop.xcf | Bin 0 -> 16565 bytes trunk/etherpad/src/static/img/jun09/pad/public.gif | Bin 0 -> 1141 bytes .../src/static/img/jun09/pad/savedrevarrows.gif | Bin 0 -> 866 bytes .../src/static/img/jun09/pad/savedrevsgfx2.gif | Bin 0 -> 1904 bytes .../src/static/img/jun09/pad/sharebox2.gif | Bin 0 -> 8836 bytes .../src/static/img/jun09/pad/sharebox3.gif | Bin 0 -> 6056 bytes .../src/static/img/jun09/pad/sharebox4.gif | Bin 0 -> 5788 bytes .../src/static/img/jun09/pad/sharedistri.gif | Bin 0 -> 85 bytes .../etherpad/src/static/img/jun09/pad/syncdone.gif | Bin 0 -> 211 bytes .../etherpad/src/static/img/jun09/pad/syncing.gif | Bin 0 -> 673 bytes .../etherpad/src/static/img/jun09/pad/syncing2.gif | Bin 0 -> 172 bytes .../src/static/img/jun09/pad/viewbargfx.gif | Bin 0 -> 155 bytes .../cmenu-gloss-cyan-menu-item-hover.gif | Bin 0 -> 52 bytes .../cmenu-gloss-menu-item-hover.gif | Bin 0 -> 52 bytes ...cmenu-gloss-semitransparent-menu-item-hover.png | Bin 0 -> 2837 bytes .../cmenu-human-menu-item-hover.gif | Bin 0 -> 195 bytes .../cmenu-osx-menu-item-hover.gif | Bin 0 -> 87 bytes .../jquery.contextmenu.images/cmenu-vista-bg.gif | Bin 0 -> 64 bytes .../cmenu-vista-menu-item-hover.gif | Bin 0 -> 347 bytes .../lib/jquery.contextmenu.images/cmenu-xp-bg.gif | Bin 0 -> 223 bytes trunk/etherpad/src/static/img/may09/bold.gif | Bin 0 -> 70 bytes trunk/etherpad/src/static/img/may09/doc.gif | Bin 0 -> 632 bytes trunk/etherpad/src/static/img/may09/doc.png | Bin 0 -> 3317 bytes trunk/etherpad/src/static/img/may09/html.gif | Bin 0 -> 1040 bytes trunk/etherpad/src/static/img/may09/html.png | Bin 0 -> 3468 bytes trunk/etherpad/src/static/img/may09/italic.gif | Bin 0 -> 73 bytes trunk/etherpad/src/static/img/may09/leftarrow.gif | Bin 0 -> 1016 bytes trunk/etherpad/src/static/img/may09/leftarrow2.gif | Bin 0 -> 950 bytes trunk/etherpad/src/static/img/may09/link.gif | Bin 0 -> 622 bytes trunk/etherpad/src/static/img/may09/link.png | Bin 0 -> 3323 bytes trunk/etherpad/src/static/img/may09/odt.gif | Bin 0 -> 405 bytes trunk/etherpad/src/static/img/may09/odt.png | Bin 0 -> 3341 bytes trunk/etherpad/src/static/img/may09/padlock.gif | Bin 0 -> 1053 bytes .../etherpad/src/static/img/may09/padlockopen.gif | Bin 0 -> 109 bytes .../src/static/img/may09/passwordlocked.gif | Bin 0 -> 1053 bytes .../static/img/may09/passwordlocked_cropped.gif | Bin 0 -> 114 bytes .../etherpad/src/static/img/may09/passwordnone.gif | Bin 0 -> 636 bytes trunk/etherpad/src/static/img/may09/paypal.gif | Bin 0 -> 3794 bytes trunk/etherpad/src/static/img/may09/pdf.gif | Bin 0 -> 398 bytes trunk/etherpad/src/static/img/may09/pdf.png | Bin 0 -> 3320 bytes trunk/etherpad/src/static/img/may09/redo.gif | Bin 0 -> 78 bytes trunk/etherpad/src/static/img/may09/txt.gif | Bin 0 -> 381 bytes trunk/etherpad/src/static/img/may09/txt.png | Bin 0 -> 3139 bytes trunk/etherpad/src/static/img/may09/underline.gif | Bin 0 -> 81 bytes trunk/etherpad/src/static/img/may09/undo.gif | Bin 0 -> 79 bytes trunk/etherpad/src/static/img/miniplane.gif | Bin 0 -> 70 bytes .../src/static/img/misc/diagnostic-links.gif | Bin 0 -> 10132 bytes trunk/etherpad/src/static/img/misc/status-ball.gif | Bin 0 -> 1553 bytes trunk/etherpad/src/static/img/misc/traclogo.gif | Bin 0 -> 5684 bytes trunk/etherpad/src/static/img/oct/atlonglast.gif | Bin 0 -> 4901 bytes trunk/etherpad/src/static/img/oct/banner1.jpg | Bin 0 -> 19897 bytes trunk/etherpad/src/static/img/oct/banner2.jpg | Bin 0 -> 45052 bytes trunk/etherpad/src/static/img/oct/banner3.jpg | Bin 0 -> 38726 bytes trunk/etherpad/src/static/img/oct/banner4.jpg | Bin 0 -> 39563 bytes trunk/etherpad/src/static/img/oct/banner5.gif | Bin 0 -> 24046 bytes trunk/etherpad/src/static/img/oct/banner6.gif | Bin 0 -> 23655 bytes trunk/etherpad/src/static/img/oct/banner7.gif | Bin 0 -> 24352 bytes trunk/etherpad/src/static/img/oct/banner8.gif | Bin 0 -> 24724 bytes trunk/etherpad/src/static/img/oct/banner9.gif | Bin 0 -> 24363 bytes trunk/etherpad/src/static/img/oct/bannerback5.gif | Bin 0 -> 2957 bytes trunk/etherpad/src/static/img/oct/bannerback6.gif | Bin 0 -> 2140 bytes trunk/etherpad/src/static/img/oct/bodyback1.gif | Bin 0 -> 488 bytes trunk/etherpad/src/static/img/oct/bodyback2.gif | Bin 0 -> 560 bytes trunk/etherpad/src/static/img/oct/bodyback3.gif | Bin 0 -> 608 bytes trunk/etherpad/src/static/img/oct/bodyback4.gif | Bin 0 -> 964 bytes trunk/etherpad/src/static/img/oct/bodyback5.gif | Bin 0 -> 579 bytes trunk/etherpad/src/static/img/oct/bodybacktop1.gif | Bin 0 -> 2991 bytes trunk/etherpad/src/static/img/oct/computers.gif | Bin 0 -> 27542 bytes trunk/etherpad/src/static/img/oct/computers2.gif | Bin 0 -> 27434 bytes trunk/etherpad/src/static/img/oct/glossyblue.gif | Bin 0 -> 1521 bytes trunk/etherpad/src/static/img/oct/glossyblue2.gif | Bin 0 -> 994 bytes trunk/etherpad/src/static/img/oct/glossyblueh.gif | Bin 0 -> 920 bytes trunk/etherpad/src/static/img/oct/insetrect.gif | Bin 0 -> 7056 bytes .../etherpad/src/static/img/oct/minilogo1-05e.gif | Bin 0 -> 2201 bytes .../etherpad/src/static/img/oct/minilogo1-07f.gif | Bin 0 -> 2252 bytes trunk/etherpad/src/static/img/oct/minilogo3.jpg | Bin 0 -> 12805 bytes trunk/etherpad/src/static/img/oct/minitopback1.gif | Bin 0 -> 954 bytes trunk/etherpad/src/static/img/oct/minitopback2.gif | Bin 0 -> 1598 bytes .../src/static/img/oct/minitopbar1-05e.gif | Bin 0 -> 284 bytes .../src/static/img/oct/minitopbar2-05e.gif | Bin 0 -> 330 bytes .../src/static/img/oct/minitopbar2-07f.gif | Bin 0 -> 330 bytes trunk/etherpad/src/static/img/oct/minitopbar3.jpg | Bin 0 -> 12805 bytes trunk/etherpad/src/static/img/oct/minitopbar4.gif | Bin 0 -> 2818 bytes trunk/etherpad/src/static/img/oct/minitoplogo1.gif | Bin 0 -> 4184 bytes trunk/etherpad/src/static/img/oct/minitoplogo2.gif | Bin 0 -> 3255 bytes trunk/etherpad/src/static/img/oct/newpadmain.gif | Bin 0 -> 1172 bytes .../etherpad/src/static/img/oct/newpadmainback.gif | Bin 0 -> 801 bytes .../src/static/img/oct/newpadmainbackh.gif | Bin 0 -> 801 bytes trunk/etherpad/src/static/img/oct/pageshot.png | Bin 0 -> 151570 bytes trunk/etherpad/src/static/img/oct/pageshotmini.png | Bin 0 -> 80505 bytes .../src/static/img/oct/sidehead-gradhilite.gif | Bin 0 -> 288 bytes trunk/etherpad/src/static/img/oct/tinytriangle.gif | Bin 0 -> 62 bytes trunk/etherpad/src/static/img/oct/topnav1.gif | Bin 0 -> 12521 bytes trunk/etherpad/src/static/img/oct/topnav2.gif | Bin 0 -> 11286 bytes trunk/etherpad/src/static/img/oct/topnav3.gif | Bin 0 -> 12363 bytes trunk/etherpad/src/static/img/oct/topnav4.gif | Bin 0 -> 11803 bytes trunk/etherpad/src/static/img/oct/topnav5.gif | Bin 0 -> 11650 bytes trunk/etherpad/src/static/img/oct/topnav6.gif | Bin 0 -> 11295 bytes trunk/etherpad/src/static/img/oct/topnavback1.gif | Bin 0 -> 1594 bytes trunk/etherpad/src/static/img/oct/topnavback2.gif | Bin 0 -> 1299 bytes trunk/etherpad/src/static/img/oct/topnavback3.gif | Bin 0 -> 380 bytes .../src/static/img/oct/usecasesnavdown.gif | Bin 0 -> 1388 bytes .../src/static/img/oct/usecasesnavdownh.gif | Bin 0 -> 1337 bytes .../etherpad/src/static/img/oct/usecasesnavup.gif | Bin 0 -> 1119 bytes .../etherpad/src/static/img/oct/usecasesnavuph.gif | Bin 0 -> 720 bytes .../src/static/img/oct/watchscreencast.gif | Bin 0 -> 25840 bytes .../src/static/img/pad/animated-orb-orange-12.gif | Bin 0 -> 2614 bytes trunk/etherpad/src/static/img/pad/backgrad.png | Bin 0 -> 1290 bytes .../pad/backshadow/backshadow-940-20-eee-20.gif | Bin 0 -> 1052 bytes .../pad/backshadow/backshadow-940-20-fff-20.gif | Bin 0 -> 1052 bytes .../pad/backshadow/backshadow-940-20-fff-40.gif | Bin 0 -> 1009 bytes .../pad/backshadow/backshadow-940-20-fff-60.gif | Bin 0 -> 1123 bytes .../img/pad/backshadow/botshadow-940-20-eee-20.gif | Bin 0 -> 1746 bytes .../static/img/pad/etherpad-logo-small-grad.gif | Bin 0 -> 1537 bytes .../src/static/img/pad/etherpad-logo-small.gif | Bin 0 -> 6664 bytes .../src/static/img/pad/etherpad-logo-small2.gif | Bin 0 -> 6646 bytes .../src/static/img/pad/expandy-arrow-down.gif | Bin 0 -> 500 bytes .../src/static/img/pad/expandy-arrow-right.gif | Bin 0 -> 296 bytes .../static/img/pad/expandy-arrow6-down-active.gif | Bin 0 -> 57 bytes .../src/static/img/pad/expandy-arrow6-down.gif | Bin 0 -> 57 bytes .../static/img/pad/expandy-arrow6-right-active.gif | Bin 0 -> 61 bytes .../src/static/img/pad/expandy-arrow6-right.gif | Bin 0 -> 61 bytes .../etherpad/src/static/img/pad/header-revgrad.gif | Bin 0 -> 598 bytes trunk/etherpad/src/static/img/pad/newpad.gif | Bin 0 -> 251 bytes .../src/static/img/pad/orb-greenred-12.gif | Bin 0 -> 1105 bytes trunk/etherpad/src/static/img/pad/padbg1.jpg | Bin 0 -> 120888 bytes trunk/etherpad/src/static/img/pad/padbg2.jpg | Bin 0 -> 44119 bytes trunk/etherpad/src/static/img/pad/padbg3.jpg | Bin 0 -> 12577 bytes trunk/etherpad/src/static/img/pad/padbg4.jpg | Bin 0 -> 12696 bytes trunk/etherpad/src/static/img/pad/padbg5.jpg | Bin 0 -> 8158 bytes trunk/etherpad/src/static/img/pad/padhead1.jpg | Bin 0 -> 13413 bytes trunk/etherpad/src/static/img/pad/padhead2.jpg | Bin 0 -> 14104 bytes trunk/etherpad/src/static/img/pad/padhead3.jpg | Bin 0 -> 6750 bytes .../src/static/img/pad/pencil-icon-small-blue.gif | Bin 0 -> 84 bytes .../etherpad/src/static/img/pad/sidehead-grad.gif | Bin 0 -> 292 bytes .../static/img/pad/timeslider/button_depressed.png | Bin 0 -> 4610 bytes .../img/pad/timeslider/button_undepressed.png | Bin 0 -> 4625 bytes .../pad/timeslider/crushed_button_depressed.png | Bin 0 -> 4134 bytes .../pad/timeslider/crushed_button_undepressed.png | Bin 0 -> 4166 bytes .../pad/timeslider/crushed_current_location.png | Bin 0 -> 1009 bytes .../static/img/pad/timeslider/crushed_pause.png | Bin 0 -> 2876 bytes .../src/static/img/pad/timeslider/crushed_play.png | Bin 0 -> 2946 bytes .../img/pad/timeslider/crushed_play_button.png | Bin 0 -> 4305 bytes .../pad/timeslider/crushed_timeslider_mockup.png | Bin 0 -> 8164 bytes .../static/img/pad/timeslider/current_location.gif | Bin 0 -> 1502 bytes .../static/img/pad/timeslider/current_location.png | Bin 0 -> 1100 bytes .../src/static/img/pad/timeslider/pause.gif | Bin 0 -> 3320 bytes .../src/static/img/pad/timeslider/pause.png | Bin 0 -> 2883 bytes .../src/static/img/pad/timeslider/play.gif | Bin 0 -> 3297 bytes .../src/static/img/pad/timeslider/play.png | Bin 0 -> 3017 bytes .../src/static/img/pad/timeslider/play_button.png | Bin 0 -> 4867 bytes .../src/static/img/pad/timeslider/star.gif | Bin 0 -> 3511 bytes .../src/static/img/pad/timeslider/star.png | Bin 0 -> 3241 bytes .../static/img/pad/timeslider/star_selected.png | Bin 0 -> 3242 bytes .../static/img/pad/timeslider/stepper_buttons.png | Bin 0 -> 4858 bytes .../img/pad/timeslider/timeslider_background.png | Bin 0 -> 915 bytes .../static/img/pad/timeslider/timeslider_left.png | Bin 0 -> 1653 bytes .../img/pad/timeslider/timeslider_mockup.png | Bin 0 -> 4860 bytes .../static/img/pad/timeslider/timeslider_right.png | Bin 0 -> 1581 bytes trunk/etherpad/src/static/img/pricing/free.gif | Bin 0 -> 7419 bytes trunk/etherpad/src/static/img/pricing/group.gif | Bin 0 -> 6783 bytes .../etherpad/src/static/img/pricing/on-demand.gif | Bin 0 -> 5791 bytes .../src/static/img/pricing/private-network.gif | Bin 0 -> 4677 bytes trunk/etherpad/src/static/img/pricing/support.gif | Bin 0 -> 2028 bytes .../src/static/img/pro/billing/cards-button.gif | Bin 0 -> 9524 bytes .../src/static/img/pro/box/blue-boxtop.gif | Bin 0 -> 523 bytes .../src/static/img/pro/buttons/bluebutton120.gif | Bin 0 -> 951 bytes .../src/static/img/pro/header/pro-header-back.gif | Bin 0 -> 213 bytes .../src/static/img/pro/header/pro-header-logo.png | Bin 0 -> 5527 bytes .../src/static/img/pro/header/pro-header-logo.xcf | Bin 0 -> 13274 bytes .../img/pro/header/pro-header-plustopnav-back.gif | Bin 0 -> 474 bytes .../src/static/img/pro/padlist/gear-drop.gif | Bin 0 -> 300 bytes .../src/static/img/pro/padlist/paper-icon.gif | Bin 0 -> 619 bytes .../src/static/img/pro/padlist/trash-icon.gif | Bin 0 -> 1080 bytes .../src/static/img/pro/topnav/pro-topnav-back.gif | Bin 0 -> 137 bytes .../src/static/img/pro/topnav/pro-topnav-notch.gif | Bin 0 -> 92 bytes trunk/etherpad/src/static/img/tinyplane.gif | Bin 0 -> 59 bytes trunk/etherpad/src/static/img/wavejet.jpg | Bin 0 -> 55379 bytes trunk/etherpad/src/static/js/ace.js | 29 + trunk/etherpad/src/static/js/billing.js | 111 + trunk/etherpad/src/static/js/billing_shared.js | 94 + trunk/etherpad/src/static/js/broadcast.js | 607 +++ .../etherpad/src/static/js/broadcast_revisions.js | 119 + trunk/etherpad/src/static/js/broadcast_slider.js | 401 ++ trunk/etherpad/src/static/js/collab_client.js | 628 +++ trunk/etherpad/src/static/js/colorutils.js | 91 + trunk/etherpad/src/static/js/confirmation.js | 21 + .../src/static/js/connection_diagnostics.js | 126 + trunk/etherpad/src/static/js/cssmanager_client.js | 88 + trunk/etherpad/src/static/js/domline_client.js | 210 + trunk/etherpad/src/static/js/draggable.js | 60 + trunk/etherpad/src/static/js/easysync2_client.js | 1777 ++++++++ trunk/etherpad/src/static/js/etherpad.js | 217 + trunk/etherpad/src/static/js/jquery-1.2.6.js | 3549 ++++++++++++++++ trunk/etherpad/src/static/js/jquery-1.3.2.js | 4376 ++++++++++++++++++++ trunk/etherpad/src/static/js/json2.js | 498 +++ .../src/static/js/lib/jquery.contextmenu.js | 284 ++ .../src/static/js/linestylefilter_client.js | 252 ++ trunk/etherpad/src/static/js/pad.js.old | 1984 +++++++++ trunk/etherpad/src/static/js/pad2.js | 591 +++ trunk/etherpad/src/static/js/pad_chat.js | 295 ++ .../etherpad/src/static/js/pad_connectionstatus.js | 63 + trunk/etherpad/src/static/js/pad_cookie.js | 101 + trunk/etherpad/src/static/js/pad_docbar.js | 347 ++ trunk/etherpad/src/static/js/pad_editbar.js | 107 + trunk/etherpad/src/static/js/pad_editor.js | 136 + trunk/etherpad/src/static/js/pad_impexp.js | 187 + trunk/etherpad/src/static/js/pad_modals.js | 364 ++ trunk/etherpad/src/static/js/pad_savedrevs.js | 408 ++ trunk/etherpad/src/static/js/pad_userlist.js | 604 +++ trunk/etherpad/src/static/js/pad_utils.js | 359 ++ trunk/etherpad/src/static/js/pricing.js | 19 + .../src/static/js/pro/guest-knock-client.js | 53 + .../src/static/js/pro/pro-padlist-client.js | 104 + trunk/etherpad/src/static/js/pro/signin-client.js | 27 + trunk/etherpad/src/static/js/pulse.jquery.js | 105 + trunk/etherpad/src/static/js/statpage.js | 143 + trunk/etherpad/src/static/js/store.js | 116 + trunk/etherpad/src/static/js/swfobject.js | 24 + trunk/etherpad/src/static/js/timeslider.js | 663 +++ trunk/etherpad/src/static/js/undo-xpopup.js | 25 + trunk/etherpad/src/static/swf/vidplayer.swf | Bin 0 -> 41390 bytes trunk/etherpad/src/templates/500_body.ejs | 26 + trunk/etherpad/src/templates/beta/signup.ejs | 63 + .../src/templates/email/eepnet_license_info.ejs | 72 + .../templates/email/eepnet_purchase_receipt.ejs | 93 + trunk/etherpad/src/templates/email/padinvite.ejs | 18 + .../src/templates/email/pro_beta_invite.ejs | 23 + .../src/templates/email/pro_payment_failure.ejs | 26 + .../src/templates/email/pro_payment_receipt.ejs | 55 + .../etherpad/src/templates/framed/framedfooter.ejs | 13 + .../src/templates/framed/framedheader-pro.ejs | 76 + .../etherpad/src/templates/framed/framedheader.ejs | 13 + .../src/templates/framed/framedpage-pro.ejs | 31 + trunk/etherpad/src/templates/framed/framedpage.ejs | 37 + trunk/etherpad/src/templates/html.ejs | 43 + trunk/etherpad/src/templates/main/home.ejs | 58 + .../src/templates/main/pro_signup_body.ejs | 71 + trunk/etherpad/src/templates/misc/pad_default.ejs | 16 + trunk/etherpad/src/templates/notice.ejs | 16 + trunk/etherpad/src/templates/pad/create_body.ejs | 26 + .../src/templates/pad/create_body_rafter.ejs | 23 + trunk/etherpad/src/templates/pad/exporthtml.ejs | 28 + trunk/etherpad/src/templates/pad/pad_body.ejs | 69 + trunk/etherpad/src/templates/pad/pad_body2.ejs | 472 +++ trunk/etherpad/src/templates/pad/pad_content.ejs | 297 ++ .../src/templates/pad/pad_download_link.ejs | 27 + .../etherpad/src/templates/pad/pad_iphone_body.ejs | 29 + trunk/etherpad/src/templates/pad/padfull_body.ejs | 32 + .../etherpad/src/templates/pad/padslider_body.ejs | 41 + trunk/etherpad/src/templates/pad/padview_body.ejs | 141 + .../src/templates/pad/total_users_exceeded.ejs | 29 + .../etherpad/src/templates/pro-account/recover.ejs | 48 + .../etherpad/src/templates/pro-account/sign-in.ejs | 57 + .../pro/account/account-welcome-email.ejs | 32 + .../templates/pro/account/create-admin-account.ejs | 37 + .../pro/account/forgot-password-email.ejs | 22 + .../src/templates/pro/account/forgot-password.ejs | 66 + .../account/global-multi-domain-recover-email.ejs | 27 + .../src/templates/pro/account/guest-knock.ejs | 27 + .../src/templates/pro/account/my-account.ejs | 67 + .../src/templates/pro/account/signin-guest.ejs | 51 + .../etherpad/src/templates/pro/account/signin.ejs | 81 + .../src/templates/pro/admin/account-manager.ejs | 59 + .../src/templates/pro/admin/admin-template.ejs | 31 + trunk/etherpad/src/templates/pro/admin/admin.ejs | 15 + .../src/templates/pro/admin/billing-invoices.ejs | 45 + .../src/templates/pro/admin/delete-account.ejs | 35 + .../src/templates/pro/admin/manage-account.ejs | 64 + .../src/templates/pro/admin/manage-billing.ejs | 35 + .../src/templates/pro/admin/new-account.ejs | 86 + .../src/templates/pro/admin/pne-config.ejs | 33 + .../src/templates/pro/admin/pne-dashboard.ejs | 40 + .../templates/pro/admin/pne-license-manager.ejs | 132 + .../etherpad/src/templates/pro/admin/pne-shell.ejs | 33 + .../src/templates/pro/admin/pro-config.ejs | 55 + .../src/templates/pro/admin/single-invoice.ejs | 47 + .../src/templates/pro/padlist/pro-padlist.ejs | 49 + .../src/templates/pro/pro-payment-required.ejs | 51 + trunk/etherpad/src/templates/pro/pro_home.ejs | 103 + .../src/templates/statistics/stat_page.ejs | 89 + trunk/etherpad/src/templates/store/csc-help.ejs | 23 + .../store/eepnet-checkout/billing-info.ejs | 183 + .../src/templates/store/eepnet-checkout/cart.ejs | 119 + .../store/eepnet-checkout/checkout-template.ejs | 38 + .../store/eepnet-checkout/confirmation.ejs | 33 + .../store/eepnet-checkout/license-info.ejs | 40 + .../templates/store/eepnet-checkout/purchase.ejs | 33 + .../templates/store/eepnet-checkout/receipt.ejs | 43 + .../templates/store/eepnet-checkout/summary.ejs | 91 + .../store/eepnet-checkout/support-contract.ejs | 41 + .../src/templates/store/eepnet_download.ejs | 43 + .../src/templates/store/eepnet_eval_nextsteps.ejs | 40 + .../src/templates/store/eepnet_eval_signup.ejs | 125 + 631 files changed, 60524 insertions(+) create mode 100644 trunk/etherpad/src/etherpad/admin/shell.js create mode 100644 trunk/etherpad/src/etherpad/billing/billing.js create mode 100644 trunk/etherpad/src/etherpad/billing/fields.js create mode 100644 trunk/etherpad/src/etherpad/billing/team_billing.js create mode 100644 trunk/etherpad/src/etherpad/collab/ace/contentcollector.js create mode 100644 trunk/etherpad/src/etherpad/collab/ace/domline.js create mode 100644 trunk/etherpad/src/etherpad/collab/ace/easysync1.js create mode 100644 trunk/etherpad/src/etherpad/collab/ace/easysync2.js create mode 100644 trunk/etherpad/src/etherpad/collab/ace/easysync2_tests.js create mode 100644 trunk/etherpad/src/etherpad/collab/ace/linestylefilter.js create mode 100644 trunk/etherpad/src/etherpad/collab/collab_server.js create mode 100644 trunk/etherpad/src/etherpad/collab/collabroom_server.js create mode 100644 trunk/etherpad/src/etherpad/collab/genimg.js create mode 100644 trunk/etherpad/src/etherpad/collab/json_sans_eval.js create mode 100644 trunk/etherpad/src/etherpad/collab/readonly_server.js create mode 100644 trunk/etherpad/src/etherpad/collab/server_utils.js create mode 100644 trunk/etherpad/src/etherpad/control/aboutcontrol.js create mode 100644 trunk/etherpad/src/etherpad/control/admincontrol.js create mode 100644 trunk/etherpad/src/etherpad/control/blogcontrol.js create mode 100644 trunk/etherpad/src/etherpad/control/connection_diagnostics_control.js create mode 100644 trunk/etherpad/src/etherpad/control/global_pro_account_control.js create mode 100644 trunk/etherpad/src/etherpad/control/historycontrol.js create mode 100644 trunk/etherpad/src/etherpad/control/loadtestcontrol.js create mode 100644 trunk/etherpad/src/etherpad/control/maincontrol.js create mode 100644 trunk/etherpad/src/etherpad/control/pad/pad_changeset_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pad/pad_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pad/pad_importexport_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pad/pad_view_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pne_manual_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pne_tracker_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pro/account_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pro/admin/account_manager_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pro/admin/license_manager_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pro/admin/pro_config_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pro/admin/team_billing_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pro/pro_main_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pro/pro_padlist_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pro_beta_control.js create mode 100644 trunk/etherpad/src/etherpad/control/pro_signup_control.js create mode 100644 trunk/etherpad/src/etherpad/control/scriptcontrol.js create mode 100644 trunk/etherpad/src/etherpad/control/static_control.js create mode 100644 trunk/etherpad/src/etherpad/control/statscontrol.js create mode 100644 trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js create mode 100644 trunk/etherpad/src/etherpad/control/store/storecontrol.js create mode 100644 trunk/etherpad/src/etherpad/control/testcontrol.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0000_test.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0009_pad_tables.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0024_statistics_table.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0027_pro_config.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js create mode 100644 trunk/etherpad/src/etherpad/db_migrations/migration_runner.js create mode 100644 trunk/etherpad/src/etherpad/debug.js create mode 100644 trunk/etherpad/src/etherpad/globals.js create mode 100644 trunk/etherpad/src/etherpad/helpers.js create mode 100644 trunk/etherpad/src/etherpad/importexport/importexport.js create mode 100644 trunk/etherpad/src/etherpad/legacy_urls.js create mode 100644 trunk/etherpad/src/etherpad/licensing.js create mode 100644 trunk/etherpad/src/etherpad/log.js create mode 100644 trunk/etherpad/src/etherpad/metrics/metrics.js create mode 100644 trunk/etherpad/src/etherpad/pad/activepads.js create mode 100644 trunk/etherpad/src/etherpad/pad/chatarchive.js create mode 100644 trunk/etherpad/src/etherpad/pad/dbwriter.js create mode 100644 trunk/etherpad/src/etherpad/pad/easysync2migration.js create mode 100644 trunk/etherpad/src/etherpad/pad/exporthtml.js create mode 100644 trunk/etherpad/src/etherpad/pad/importhtml.js create mode 100644 trunk/etherpad/src/etherpad/pad/model.js create mode 100644 trunk/etherpad/src/etherpad/pad/noprowatcher.js create mode 100644 trunk/etherpad/src/etherpad/pad/pad_migrations.js create mode 100644 trunk/etherpad/src/etherpad/pad/pad_security.js create mode 100644 trunk/etherpad/src/etherpad/pad/padevents.js create mode 100644 trunk/etherpad/src/etherpad/pad/padusers.js create mode 100644 trunk/etherpad/src/etherpad/pad/padutils.js create mode 100644 trunk/etherpad/src/etherpad/pad/revisions.js create mode 100644 trunk/etherpad/src/etherpad/pne/pne_utils.js create mode 100644 trunk/etherpad/src/etherpad/pro/domains.js create mode 100644 trunk/etherpad/src/etherpad/pro/pro_account_auto_signin.js create mode 100644 trunk/etherpad/src/etherpad/pro/pro_accounts.js create mode 100644 trunk/etherpad/src/etherpad/pro/pro_config.js create mode 100644 trunk/etherpad/src/etherpad/pro/pro_pad_db.js create mode 100644 trunk/etherpad/src/etherpad/pro/pro_pad_editors.js create mode 100644 trunk/etherpad/src/etherpad/pro/pro_padlist.js create mode 100644 trunk/etherpad/src/etherpad/pro/pro_padmeta.js create mode 100644 trunk/etherpad/src/etherpad/pro/pro_quotas.js create mode 100644 trunk/etherpad/src/etherpad/pro/pro_utils.js create mode 100644 trunk/etherpad/src/etherpad/quotas.js create mode 100644 trunk/etherpad/src/etherpad/sessions.js create mode 100644 trunk/etherpad/src/etherpad/statistics/exceptions.js create mode 100644 trunk/etherpad/src/etherpad/statistics/statistics.js create mode 100644 trunk/etherpad/src/etherpad/store/checkout.js create mode 100644 trunk/etherpad/src/etherpad/store/eepnet_checkout.js create mode 100644 trunk/etherpad/src/etherpad/store/eepnet_trial.js create mode 100644 trunk/etherpad/src/etherpad/testing/testutils.js create mode 100644 trunk/etherpad/src/etherpad/testing/unit_tests/t0000_test.js create mode 100644 trunk/etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js create mode 100644 trunk/etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js create mode 100644 trunk/etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js create mode 100644 trunk/etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js create mode 100644 trunk/etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js create mode 100644 trunk/etherpad/src/etherpad/usage_stats/usage_stats.js create mode 100644 trunk/etherpad/src/etherpad/utils.js create mode 100644 trunk/etherpad/src/main.js create mode 100644 trunk/etherpad/src/static/crossdomain.xml create mode 100644 trunk/etherpad/src/static/css/admin/admin-stats.css create mode 100644 trunk/etherpad/src/static/css/beta.css create mode 100644 trunk/etherpad/src/static/css/broadcast.css create mode 100644 trunk/etherpad/src/static/css/connection_diagnostics.css create mode 100644 trunk/etherpad/src/static/css/etherpad.css create mode 100644 trunk/etherpad/src/static/css/fluxbb.css create mode 100644 trunk/etherpad/src/static/css/framedpage.css create mode 100644 trunk/etherpad/src/static/css/global-pro-account.css create mode 100644 trunk/etherpad/src/static/css/home-opensource.css create mode 100644 trunk/etherpad/src/static/css/home.css create mode 100644 trunk/etherpad/src/static/css/lib/jquery.contextmenu.css create mode 100644 trunk/etherpad/src/static/css/pad.css create mode 100644 trunk/etherpad/src/static/css/pad2_ejs.css create mode 100644 trunk/etherpad/src/static/css/pne-manual.css create mode 100644 trunk/etherpad/src/static/css/pricing.css create mode 100644 trunk/etherpad/src/static/css/pro-signup.css create mode 100644 trunk/etherpad/src/static/css/pro/account.css create mode 100644 trunk/etherpad/src/static/css/pro/framedpage-pro.css create mode 100644 trunk/etherpad/src/static/css/pro/padlist.css create mode 100644 trunk/etherpad/src/static/css/pro/payment-required.css create mode 100644 trunk/etherpad/src/static/css/pro/pro-admin.css create mode 100644 trunk/etherpad/src/static/css/pro/pro-home.css create mode 100644 trunk/etherpad/src/static/css/stats.css create mode 100644 trunk/etherpad/src/static/css/store/eepnet-checkout.css create mode 100644 trunk/etherpad/src/static/css/store/ondemand-billing.css create mode 100644 trunk/etherpad/src/static/css/store/store.css create mode 100644 trunk/etherpad/src/static/favicon.ico create mode 100644 trunk/etherpad/src/static/img/about/appjet-logo-large.gif create mode 100644 trunk/etherpad/src/static/img/about/appjet-logo-medium.png create mode 100644 trunk/etherpad/src/static/img/about/investors/mitchkapor.jpg create mode 100644 trunk/etherpad/src/static/img/about/investors/pb.jpg create mode 100644 trunk/etherpad/src/static/img/about/investors/pg.jpg create mode 100644 trunk/etherpad/src/static/img/about/investors/sanjeev.jpg create mode 100644 trunk/etherpad/src/static/img/about/investors/seth.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/aaron-david-iphones-thumb.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/aaron-david-iphones.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/aaron-google-air.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/aaron-headshot-thumb.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/aaron-headshot.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/aaron-headshot2-thumb.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/aaron-headshot2.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/aaron-headshot3-thumb.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/aaron-headshot3.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/daniel-headshot-thumb.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/david-headshot-thumb.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/david-headshot.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/davy-headshot.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/jd-headshot-thumb.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/jd-headshot.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/rhonda-headshot-thumb.jpg create mode 100644 trunk/etherpad/src/static/img/about/people/rhonda-headshot.jpg create mode 100644 trunk/etherpad/src/static/img/about/pier38.png create mode 100644 trunk/etherpad/src/static/img/about/quote-close.png create mode 100644 trunk/etherpad/src/static/img/about/quote-open.png create mode 100644 trunk/etherpad/src/static/img/about/screencastpreview800x600.jpg create mode 100644 trunk/etherpad/src/static/img/account/betawarn.jpg create mode 100644 trunk/etherpad/src/static/img/acecarets/000000.gif create mode 100644 trunk/etherpad/src/static/img/acecarets/666666.gif create mode 100644 trunk/etherpad/src/static/img/acecarets/999999.gif create mode 100644 trunk/etherpad/src/static/img/acecarets/default.gif create mode 100644 trunk/etherpad/src/static/img/apr09/backgrad.png create mode 100644 trunk/etherpad/src/static/img/apr09/black35.png create mode 100644 trunk/etherpad/src/static/img/apr09/blank.gif create mode 100644 trunk/etherpad/src/static/img/apr09/modalbar.gif create mode 100644 trunk/etherpad/src/static/img/apr09/newpadicon.gif create mode 100644 trunk/etherpad/src/static/img/apr09/shadbot.png create mode 100644 trunk/etherpad/src/static/img/apr09/shadleft.png create mode 100644 trunk/etherpad/src/static/img/apr09/shadleftbot.png create mode 100644 trunk/etherpad/src/static/img/apr09/shadlefttop.png create mode 100644 trunk/etherpad/src/static/img/apr09/shadright.png create mode 100644 trunk/etherpad/src/static/img/apr09/shadrightbot.png create mode 100644 trunk/etherpad/src/static/img/apr09/shadrighttop.png create mode 100644 trunk/etherpad/src/static/img/apr09/topbar.gif create mode 100644 trunk/etherpad/src/static/img/apr09/topbarlogo.gif create mode 100644 trunk/etherpad/src/static/img/apr09/widthfull.gif create mode 100644 trunk/etherpad/src/static/img/apr09/widthfullactive.gif create mode 100644 trunk/etherpad/src/static/img/apr09/widthlim.gif create mode 100644 trunk/etherpad/src/static/img/apr09/widthlimactive.gif create mode 100644 trunk/etherpad/src/static/img/billing/amex.gif create mode 100644 trunk/etherpad/src/static/img/billing/creditcard.gif create mode 100644 trunk/etherpad/src/static/img/billing/csc-help.gif create mode 100644 trunk/etherpad/src/static/img/billing/disc.gif create mode 100644 trunk/etherpad/src/static/img/billing/invoice.gif create mode 100644 trunk/etherpad/src/static/img/billing/mc.gif create mode 100644 trunk/etherpad/src/static/img/billing/paypal.gif create mode 100644 trunk/etherpad/src/static/img/billing/visa.gif create mode 100644 trunk/etherpad/src/static/img/blog/posts/new-features/fullwidth.gif create mode 100644 trunk/etherpad/src/static/img/blog/posts/new-features/importexport.gif create mode 100644 trunk/etherpad/src/static/img/blog/posts/new-features/richtext.gif create mode 100644 trunk/etherpad/src/static/img/blog/posts/new-features/viewzoom.gif create mode 100644 trunk/etherpad/src/static/img/blog/posts/pricing-survey-results.png create mode 100644 trunk/etherpad/src/static/img/blog/posts/pricing-survey.png create mode 100644 trunk/etherpad/src/static/img/blog/posts/time-slider-screenshot.gif create mode 100644 trunk/etherpad/src/static/img/davy/bg/home-createpad.png create mode 100644 trunk/etherpad/src/static/img/davy/bg/home-features-bottom.gif create mode 100644 trunk/etherpad/src/static/img/davy/bg/home-features-free-bottom.gif create mode 100644 trunk/etherpad/src/static/img/davy/bg/home-features-paid-top.gif create mode 100644 trunk/etherpad/src/static/img/davy/bg/home-features-top.gif create mode 100644 trunk/etherpad/src/static/img/davy/bg/home-nav-selected.png create mode 100644 trunk/etherpad/src/static/img/davy/bg/home-screencast.png create mode 100644 trunk/etherpad/src/static/img/davy/bg/home2.png create mode 100644 trunk/etherpad/src/static/img/davy/bg/product-nav-selected-white.png create mode 100644 trunk/etherpad/src/static/img/davy/bg/product-nav-selected.png create mode 100644 trunk/etherpad/src/static/img/davy/bg/product.png create mode 100644 trunk/etherpad/src/static/img/davy/btn/createpad-home.gif create mode 100644 trunk/etherpad/src/static/img/davy/btn/createpad-large.gif create mode 100644 trunk/etherpad/src/static/img/davy/btn/createpad-small.gif create mode 100644 trunk/etherpad/src/static/img/davy/btn/intro-screencast.png create mode 100644 trunk/etherpad/src/static/img/davy/btn/intro-testimonials.png create mode 100644 trunk/etherpad/src/static/img/davy/btn/learnmore.gif create mode 100644 trunk/etherpad/src/static/img/davy/btn/signup-home-2.gif create mode 100644 trunk/etherpad/src/static/img/davy/btn/signup-home-3.gif create mode 100644 trunk/etherpad/src/static/img/davy/btn/signup-home-4.gif create mode 100644 trunk/etherpad/src/static/img/davy/btn/signup-home.gif create mode 100644 trunk/etherpad/src/static/img/davy/btn/uses-more.gif create mode 100644 trunk/etherpad/src/static/img/davy/gfx/32/114.png create mode 100644 trunk/etherpad/src/static/img/davy/gfx/32/15.png create mode 100644 trunk/etherpad/src/static/img/davy/gfx/32/65.png create mode 100644 trunk/etherpad/src/static/img/davy/gfx/32/78.png create mode 100644 trunk/etherpad/src/static/img/davy/gfx/bullet.gif create mode 100644 trunk/etherpad/src/static/img/davy/gfx/home-logo2.gif create mode 100644 trunk/etherpad/src/static/img/davy/gfx/home-screencast.png create mode 100644 trunk/etherpad/src/static/img/davy/gfx/plane.gif create mode 100644 trunk/etherpad/src/static/img/davy/gfx/product-logo.gif create mode 100644 trunk/etherpad/src/static/img/davy/gfx/screenshot.gif create mode 100644 trunk/etherpad/src/static/img/davy/gfx/use-meetings.gif create mode 100644 trunk/etherpad/src/static/img/davy/gfx/use-meetings.png create mode 100644 trunk/etherpad/src/static/img/davy/gfx/use-programming.gif create mode 100644 trunk/etherpad/src/static/img/davy/gfx/use-programming.png create mode 100644 trunk/etherpad/src/static/img/davy/gfx/use-writing.gif create mode 100644 trunk/etherpad/src/static/img/davy/gfx/use-writing.png create mode 100644 trunk/etherpad/src/static/img/davy/txt/home-button.gif create mode 100644 trunk/etherpad/src/static/img/featuretour/code.gif create mode 100644 trunk/etherpad/src/static/img/featuretour/edits.gif create mode 100644 trunk/etherpad/src/static/img/featuretour/editsandusers.gif create mode 100644 trunk/etherpad/src/static/img/featuretour/padlock.png create mode 100644 trunk/etherpad/src/static/img/featuretour/revisions.gif create mode 100644 trunk/etherpad/src/static/img/featuretour/users.gif create mode 100644 trunk/etherpad/src/static/img/feb09/framedheaderback.gif create mode 100644 trunk/etherpad/src/static/img/feb09/framedheaderlogo.gif create mode 100644 trunk/etherpad/src/static/img/feb09/home_firstp.gif create mode 100644 trunk/etherpad/src/static/img/feb09/home_firstp.png create mode 100644 trunk/etherpad/src/static/img/feb09/home_firstp2.gif create mode 100644 trunk/etherpad/src/static/img/feb09/home_h1.gif create mode 100644 trunk/etherpad/src/static/img/feb09/home_h1.png create mode 100644 trunk/etherpad/src/static/img/feb09/home_newpadbutton.gif create mode 100644 trunk/etherpad/src/static/img/feb09/home_newpadbutton.png create mode 100644 trunk/etherpad/src/static/img/feb09/home_newpadbutton2.gif create mode 100644 trunk/etherpad/src/static/img/feb09/home_newpadbutton_eepnet.gif create mode 100644 trunk/etherpad/src/static/img/feb09/hometop_back.gif create mode 100644 trunk/etherpad/src/static/img/feb09/nav1.gif create mode 100644 trunk/etherpad/src/static/img/feb09/nav1_back.gif create mode 100644 trunk/etherpad/src/static/img/feb09/nav2.gif create mode 100644 trunk/etherpad/src/static/img/feb09/screencast.gif create mode 100644 trunk/etherpad/src/static/img/home/etherpad-mainheader1.jpg create mode 100644 trunk/etherpad/src/static/img/home/headergradient.gif create mode 100644 trunk/etherpad/src/static/img/home/homeheader1.jpg create mode 100644 trunk/etherpad/src/static/img/home/homeheader2.jpg create mode 100644 trunk/etherpad/src/static/img/home/leftgrad.gif create mode 100644 trunk/etherpad/src/static/img/home/pencilpaperback.png create mode 100644 trunk/etherpad/src/static/img/home/screencapture1.gif create mode 100644 trunk/etherpad/src/static/img/home/underdevicon.gif create mode 100644 trunk/etherpad/src/static/img/icon/downarrow.gif create mode 100644 trunk/etherpad/src/static/img/icon/feed.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/backgrad.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/bottomareagfx.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/colorpicker.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/connectingbar.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/connectionindicator.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/docbarstates.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/docbarstates2.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/docbarstates3.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/docpaneledge.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/docpaneledge2.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/docpanelmiddle.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/docpanelmiddle2.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/editbar.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/editbar2.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/editbar3.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/editbar3.xcf create mode 100644 trunk/etherpad/src/static/img/jun09/pad/editbarback.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/feedbackbox2.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/fileicons.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/hdraggie.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/inviteshare.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/inviteshare2.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/layoutbuttons.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/overlay.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/overlay2.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/padtop.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/padtop2.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/padtop3.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/padtop4.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/padtop4.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/padtop4.xcf create mode 100644 trunk/etherpad/src/static/img/jun09/pad/padtop5.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/padtop5.xcf create mode 100644 trunk/etherpad/src/static/img/jun09/pad/padtopback.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/padtopback2.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/protop.png create mode 100644 trunk/etherpad/src/static/img/jun09/pad/protop.xcf create mode 100644 trunk/etherpad/src/static/img/jun09/pad/public.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/savedrevarrows.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/savedrevsgfx2.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/sharebox2.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/sharebox3.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/sharebox4.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/sharedistri.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/syncdone.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/syncing.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/syncing2.gif create mode 100644 trunk/etherpad/src/static/img/jun09/pad/viewbargfx.gif create mode 100644 trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-cyan-menu-item-hover.gif create mode 100644 trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-menu-item-hover.gif create mode 100644 trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-semitransparent-menu-item-hover.png create mode 100644 trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-human-menu-item-hover.gif create mode 100644 trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-osx-menu-item-hover.gif create mode 100644 trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-bg.gif create mode 100644 trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-menu-item-hover.gif create mode 100644 trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-xp-bg.gif create mode 100644 trunk/etherpad/src/static/img/may09/bold.gif create mode 100644 trunk/etherpad/src/static/img/may09/doc.gif create mode 100644 trunk/etherpad/src/static/img/may09/doc.png create mode 100644 trunk/etherpad/src/static/img/may09/html.gif create mode 100644 trunk/etherpad/src/static/img/may09/html.png create mode 100644 trunk/etherpad/src/static/img/may09/italic.gif create mode 100644 trunk/etherpad/src/static/img/may09/leftarrow.gif create mode 100644 trunk/etherpad/src/static/img/may09/leftarrow2.gif create mode 100644 trunk/etherpad/src/static/img/may09/link.gif create mode 100644 trunk/etherpad/src/static/img/may09/link.png create mode 100644 trunk/etherpad/src/static/img/may09/odt.gif create mode 100644 trunk/etherpad/src/static/img/may09/odt.png create mode 100644 trunk/etherpad/src/static/img/may09/padlock.gif create mode 100644 trunk/etherpad/src/static/img/may09/padlockopen.gif create mode 100644 trunk/etherpad/src/static/img/may09/passwordlocked.gif create mode 100644 trunk/etherpad/src/static/img/may09/passwordlocked_cropped.gif create mode 100644 trunk/etherpad/src/static/img/may09/passwordnone.gif create mode 100644 trunk/etherpad/src/static/img/may09/paypal.gif create mode 100644 trunk/etherpad/src/static/img/may09/pdf.gif create mode 100644 trunk/etherpad/src/static/img/may09/pdf.png create mode 100644 trunk/etherpad/src/static/img/may09/redo.gif create mode 100644 trunk/etherpad/src/static/img/may09/txt.gif create mode 100644 trunk/etherpad/src/static/img/may09/txt.png create mode 100644 trunk/etherpad/src/static/img/may09/underline.gif create mode 100644 trunk/etherpad/src/static/img/may09/undo.gif create mode 100644 trunk/etherpad/src/static/img/miniplane.gif create mode 100644 trunk/etherpad/src/static/img/misc/diagnostic-links.gif create mode 100644 trunk/etherpad/src/static/img/misc/status-ball.gif create mode 100644 trunk/etherpad/src/static/img/misc/traclogo.gif create mode 100644 trunk/etherpad/src/static/img/oct/atlonglast.gif create mode 100644 trunk/etherpad/src/static/img/oct/banner1.jpg create mode 100644 trunk/etherpad/src/static/img/oct/banner2.jpg create mode 100644 trunk/etherpad/src/static/img/oct/banner3.jpg create mode 100644 trunk/etherpad/src/static/img/oct/banner4.jpg create mode 100644 trunk/etherpad/src/static/img/oct/banner5.gif create mode 100644 trunk/etherpad/src/static/img/oct/banner6.gif create mode 100644 trunk/etherpad/src/static/img/oct/banner7.gif create mode 100644 trunk/etherpad/src/static/img/oct/banner8.gif create mode 100644 trunk/etherpad/src/static/img/oct/banner9.gif create mode 100644 trunk/etherpad/src/static/img/oct/bannerback5.gif create mode 100644 trunk/etherpad/src/static/img/oct/bannerback6.gif create mode 100644 trunk/etherpad/src/static/img/oct/bodyback1.gif create mode 100644 trunk/etherpad/src/static/img/oct/bodyback2.gif create mode 100644 trunk/etherpad/src/static/img/oct/bodyback3.gif create mode 100644 trunk/etherpad/src/static/img/oct/bodyback4.gif create mode 100644 trunk/etherpad/src/static/img/oct/bodyback5.gif create mode 100644 trunk/etherpad/src/static/img/oct/bodybacktop1.gif create mode 100644 trunk/etherpad/src/static/img/oct/computers.gif create mode 100644 trunk/etherpad/src/static/img/oct/computers2.gif create mode 100644 trunk/etherpad/src/static/img/oct/glossyblue.gif create mode 100644 trunk/etherpad/src/static/img/oct/glossyblue2.gif create mode 100644 trunk/etherpad/src/static/img/oct/glossyblueh.gif create mode 100644 trunk/etherpad/src/static/img/oct/insetrect.gif create mode 100644 trunk/etherpad/src/static/img/oct/minilogo1-05e.gif create mode 100644 trunk/etherpad/src/static/img/oct/minilogo1-07f.gif create mode 100644 trunk/etherpad/src/static/img/oct/minilogo3.jpg create mode 100644 trunk/etherpad/src/static/img/oct/minitopback1.gif create mode 100644 trunk/etherpad/src/static/img/oct/minitopback2.gif create mode 100644 trunk/etherpad/src/static/img/oct/minitopbar1-05e.gif create mode 100644 trunk/etherpad/src/static/img/oct/minitopbar2-05e.gif create mode 100644 trunk/etherpad/src/static/img/oct/minitopbar2-07f.gif create mode 100644 trunk/etherpad/src/static/img/oct/minitopbar3.jpg create mode 100644 trunk/etherpad/src/static/img/oct/minitopbar4.gif create mode 100644 trunk/etherpad/src/static/img/oct/minitoplogo1.gif create mode 100644 trunk/etherpad/src/static/img/oct/minitoplogo2.gif create mode 100644 trunk/etherpad/src/static/img/oct/newpadmain.gif create mode 100644 trunk/etherpad/src/static/img/oct/newpadmainback.gif create mode 100644 trunk/etherpad/src/static/img/oct/newpadmainbackh.gif create mode 100644 trunk/etherpad/src/static/img/oct/pageshot.png create mode 100644 trunk/etherpad/src/static/img/oct/pageshotmini.png create mode 100644 trunk/etherpad/src/static/img/oct/sidehead-gradhilite.gif create mode 100644 trunk/etherpad/src/static/img/oct/tinytriangle.gif create mode 100644 trunk/etherpad/src/static/img/oct/topnav1.gif create mode 100644 trunk/etherpad/src/static/img/oct/topnav2.gif create mode 100644 trunk/etherpad/src/static/img/oct/topnav3.gif create mode 100644 trunk/etherpad/src/static/img/oct/topnav4.gif create mode 100644 trunk/etherpad/src/static/img/oct/topnav5.gif create mode 100644 trunk/etherpad/src/static/img/oct/topnav6.gif create mode 100644 trunk/etherpad/src/static/img/oct/topnavback1.gif create mode 100644 trunk/etherpad/src/static/img/oct/topnavback2.gif create mode 100644 trunk/etherpad/src/static/img/oct/topnavback3.gif create mode 100644 trunk/etherpad/src/static/img/oct/usecasesnavdown.gif create mode 100644 trunk/etherpad/src/static/img/oct/usecasesnavdownh.gif create mode 100644 trunk/etherpad/src/static/img/oct/usecasesnavup.gif create mode 100644 trunk/etherpad/src/static/img/oct/usecasesnavuph.gif create mode 100644 trunk/etherpad/src/static/img/oct/watchscreencast.gif create mode 100644 trunk/etherpad/src/static/img/pad/animated-orb-orange-12.gif create mode 100644 trunk/etherpad/src/static/img/pad/backgrad.png create mode 100644 trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-eee-20.gif create mode 100644 trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-20.gif create mode 100644 trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-40.gif create mode 100644 trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-60.gif create mode 100644 trunk/etherpad/src/static/img/pad/backshadow/botshadow-940-20-eee-20.gif create mode 100644 trunk/etherpad/src/static/img/pad/etherpad-logo-small-grad.gif create mode 100644 trunk/etherpad/src/static/img/pad/etherpad-logo-small.gif create mode 100644 trunk/etherpad/src/static/img/pad/etherpad-logo-small2.gif create mode 100644 trunk/etherpad/src/static/img/pad/expandy-arrow-down.gif create mode 100644 trunk/etherpad/src/static/img/pad/expandy-arrow-right.gif create mode 100644 trunk/etherpad/src/static/img/pad/expandy-arrow6-down-active.gif create mode 100644 trunk/etherpad/src/static/img/pad/expandy-arrow6-down.gif create mode 100644 trunk/etherpad/src/static/img/pad/expandy-arrow6-right-active.gif create mode 100644 trunk/etherpad/src/static/img/pad/expandy-arrow6-right.gif create mode 100644 trunk/etherpad/src/static/img/pad/header-revgrad.gif create mode 100644 trunk/etherpad/src/static/img/pad/newpad.gif create mode 100644 trunk/etherpad/src/static/img/pad/orb-greenred-12.gif create mode 100644 trunk/etherpad/src/static/img/pad/padbg1.jpg create mode 100644 trunk/etherpad/src/static/img/pad/padbg2.jpg create mode 100644 trunk/etherpad/src/static/img/pad/padbg3.jpg create mode 100644 trunk/etherpad/src/static/img/pad/padbg4.jpg create mode 100644 trunk/etherpad/src/static/img/pad/padbg5.jpg create mode 100644 trunk/etherpad/src/static/img/pad/padhead1.jpg create mode 100644 trunk/etherpad/src/static/img/pad/padhead2.jpg create mode 100644 trunk/etherpad/src/static/img/pad/padhead3.jpg create mode 100644 trunk/etherpad/src/static/img/pad/pencil-icon-small-blue.gif create mode 100644 trunk/etherpad/src/static/img/pad/sidehead-grad.gif create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/button_depressed.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/button_undepressed.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/crushed_button_depressed.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/crushed_button_undepressed.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/crushed_current_location.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/crushed_pause.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/crushed_play.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/crushed_play_button.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/crushed_timeslider_mockup.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/current_location.gif create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/current_location.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/pause.gif create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/pause.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/play.gif create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/play.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/play_button.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/star.gif create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/star.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/star_selected.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/stepper_buttons.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/timeslider_background.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/timeslider_left.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/timeslider_mockup.png create mode 100644 trunk/etherpad/src/static/img/pad/timeslider/timeslider_right.png create mode 100644 trunk/etherpad/src/static/img/pricing/free.gif create mode 100644 trunk/etherpad/src/static/img/pricing/group.gif create mode 100644 trunk/etherpad/src/static/img/pricing/on-demand.gif create mode 100644 trunk/etherpad/src/static/img/pricing/private-network.gif create mode 100644 trunk/etherpad/src/static/img/pricing/support.gif create mode 100644 trunk/etherpad/src/static/img/pro/billing/cards-button.gif create mode 100644 trunk/etherpad/src/static/img/pro/box/blue-boxtop.gif create mode 100644 trunk/etherpad/src/static/img/pro/buttons/bluebutton120.gif create mode 100644 trunk/etherpad/src/static/img/pro/header/pro-header-back.gif create mode 100644 trunk/etherpad/src/static/img/pro/header/pro-header-logo.png create mode 100644 trunk/etherpad/src/static/img/pro/header/pro-header-logo.xcf create mode 100644 trunk/etherpad/src/static/img/pro/header/pro-header-plustopnav-back.gif create mode 100644 trunk/etherpad/src/static/img/pro/padlist/gear-drop.gif create mode 100644 trunk/etherpad/src/static/img/pro/padlist/paper-icon.gif create mode 100644 trunk/etherpad/src/static/img/pro/padlist/trash-icon.gif create mode 100644 trunk/etherpad/src/static/img/pro/topnav/pro-topnav-back.gif create mode 100644 trunk/etherpad/src/static/img/pro/topnav/pro-topnav-notch.gif create mode 100644 trunk/etherpad/src/static/img/tinyplane.gif create mode 100644 trunk/etherpad/src/static/img/wavejet.jpg create mode 100644 trunk/etherpad/src/static/js/ace.js create mode 100644 trunk/etherpad/src/static/js/billing.js create mode 100644 trunk/etherpad/src/static/js/billing_shared.js create mode 100644 trunk/etherpad/src/static/js/broadcast.js create mode 100644 trunk/etherpad/src/static/js/broadcast_revisions.js create mode 100644 trunk/etherpad/src/static/js/broadcast_slider.js create mode 100644 trunk/etherpad/src/static/js/collab_client.js create mode 100644 trunk/etherpad/src/static/js/colorutils.js create mode 100644 trunk/etherpad/src/static/js/confirmation.js create mode 100644 trunk/etherpad/src/static/js/connection_diagnostics.js create mode 100644 trunk/etherpad/src/static/js/cssmanager_client.js create mode 100644 trunk/etherpad/src/static/js/domline_client.js create mode 100644 trunk/etherpad/src/static/js/draggable.js create mode 100644 trunk/etherpad/src/static/js/easysync2_client.js create mode 100644 trunk/etherpad/src/static/js/etherpad.js create mode 100755 trunk/etherpad/src/static/js/jquery-1.2.6.js create mode 100644 trunk/etherpad/src/static/js/jquery-1.3.2.js create mode 100644 trunk/etherpad/src/static/js/json2.js create mode 100644 trunk/etherpad/src/static/js/lib/jquery.contextmenu.js create mode 100644 trunk/etherpad/src/static/js/linestylefilter_client.js create mode 100644 trunk/etherpad/src/static/js/pad.js.old create mode 100644 trunk/etherpad/src/static/js/pad2.js create mode 100644 trunk/etherpad/src/static/js/pad_chat.js create mode 100644 trunk/etherpad/src/static/js/pad_connectionstatus.js create mode 100644 trunk/etherpad/src/static/js/pad_cookie.js create mode 100644 trunk/etherpad/src/static/js/pad_docbar.js create mode 100644 trunk/etherpad/src/static/js/pad_editbar.js create mode 100644 trunk/etherpad/src/static/js/pad_editor.js create mode 100644 trunk/etherpad/src/static/js/pad_impexp.js create mode 100644 trunk/etherpad/src/static/js/pad_modals.js create mode 100644 trunk/etherpad/src/static/js/pad_savedrevs.js create mode 100644 trunk/etherpad/src/static/js/pad_userlist.js create mode 100644 trunk/etherpad/src/static/js/pad_utils.js create mode 100644 trunk/etherpad/src/static/js/pricing.js create mode 100644 trunk/etherpad/src/static/js/pro/guest-knock-client.js create mode 100644 trunk/etherpad/src/static/js/pro/pro-padlist-client.js create mode 100644 trunk/etherpad/src/static/js/pro/signin-client.js create mode 100644 trunk/etherpad/src/static/js/pulse.jquery.js create mode 100644 trunk/etherpad/src/static/js/statpage.js create mode 100644 trunk/etherpad/src/static/js/store.js create mode 100644 trunk/etherpad/src/static/js/swfobject.js create mode 100644 trunk/etherpad/src/static/js/timeslider.js create mode 100644 trunk/etherpad/src/static/js/undo-xpopup.js create mode 100755 trunk/etherpad/src/static/swf/vidplayer.swf create mode 100644 trunk/etherpad/src/templates/500_body.ejs create mode 100644 trunk/etherpad/src/templates/beta/signup.ejs create mode 100644 trunk/etherpad/src/templates/email/eepnet_license_info.ejs create mode 100644 trunk/etherpad/src/templates/email/eepnet_purchase_receipt.ejs create mode 100644 trunk/etherpad/src/templates/email/padinvite.ejs create mode 100644 trunk/etherpad/src/templates/email/pro_beta_invite.ejs create mode 100644 trunk/etherpad/src/templates/email/pro_payment_failure.ejs create mode 100644 trunk/etherpad/src/templates/email/pro_payment_receipt.ejs create mode 100644 trunk/etherpad/src/templates/framed/framedfooter.ejs create mode 100644 trunk/etherpad/src/templates/framed/framedheader-pro.ejs create mode 100644 trunk/etherpad/src/templates/framed/framedheader.ejs create mode 100644 trunk/etherpad/src/templates/framed/framedpage-pro.ejs create mode 100644 trunk/etherpad/src/templates/framed/framedpage.ejs create mode 100644 trunk/etherpad/src/templates/html.ejs create mode 100644 trunk/etherpad/src/templates/main/home.ejs create mode 100644 trunk/etherpad/src/templates/main/pro_signup_body.ejs create mode 100644 trunk/etherpad/src/templates/misc/pad_default.ejs create mode 100644 trunk/etherpad/src/templates/notice.ejs create mode 100644 trunk/etherpad/src/templates/pad/create_body.ejs create mode 100644 trunk/etherpad/src/templates/pad/create_body_rafter.ejs create mode 100644 trunk/etherpad/src/templates/pad/exporthtml.ejs create mode 100644 trunk/etherpad/src/templates/pad/pad_body.ejs create mode 100644 trunk/etherpad/src/templates/pad/pad_body2.ejs create mode 100644 trunk/etherpad/src/templates/pad/pad_content.ejs create mode 100644 trunk/etherpad/src/templates/pad/pad_download_link.ejs create mode 100644 trunk/etherpad/src/templates/pad/pad_iphone_body.ejs create mode 100644 trunk/etherpad/src/templates/pad/padfull_body.ejs create mode 100644 trunk/etherpad/src/templates/pad/padslider_body.ejs create mode 100644 trunk/etherpad/src/templates/pad/padview_body.ejs create mode 100644 trunk/etherpad/src/templates/pad/total_users_exceeded.ejs create mode 100644 trunk/etherpad/src/templates/pro-account/recover.ejs create mode 100644 trunk/etherpad/src/templates/pro-account/sign-in.ejs create mode 100644 trunk/etherpad/src/templates/pro/account/account-welcome-email.ejs create mode 100644 trunk/etherpad/src/templates/pro/account/create-admin-account.ejs create mode 100644 trunk/etherpad/src/templates/pro/account/forgot-password-email.ejs create mode 100644 trunk/etherpad/src/templates/pro/account/forgot-password.ejs create mode 100644 trunk/etherpad/src/templates/pro/account/global-multi-domain-recover-email.ejs create mode 100644 trunk/etherpad/src/templates/pro/account/guest-knock.ejs create mode 100644 trunk/etherpad/src/templates/pro/account/my-account.ejs create mode 100644 trunk/etherpad/src/templates/pro/account/signin-guest.ejs create mode 100644 trunk/etherpad/src/templates/pro/account/signin.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/account-manager.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/admin-template.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/admin.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/billing-invoices.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/delete-account.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/manage-account.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/manage-billing.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/new-account.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/pne-config.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/pne-dashboard.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/pne-license-manager.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/pne-shell.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/pro-config.ejs create mode 100644 trunk/etherpad/src/templates/pro/admin/single-invoice.ejs create mode 100644 trunk/etherpad/src/templates/pro/padlist/pro-padlist.ejs create mode 100644 trunk/etherpad/src/templates/pro/pro-payment-required.ejs create mode 100644 trunk/etherpad/src/templates/pro/pro_home.ejs create mode 100644 trunk/etherpad/src/templates/statistics/stat_page.ejs create mode 100644 trunk/etherpad/src/templates/store/csc-help.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet-checkout/billing-info.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet-checkout/cart.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet-checkout/checkout-template.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet-checkout/confirmation.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet-checkout/license-info.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet-checkout/purchase.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet-checkout/receipt.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet-checkout/summary.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet-checkout/support-contract.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet_download.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet_eval_nextsteps.ejs create mode 100644 trunk/etherpad/src/templates/store/eepnet_eval_signup.ejs (limited to 'trunk/etherpad/src') diff --git a/trunk/etherpad/src/etherpad/admin/shell.js b/trunk/etherpad/src/etherpad/admin/shell.js new file mode 100644 index 0000000..391d524 --- /dev/null +++ b/trunk/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("« Admin"))); + body.push(BR(), BR()); + body.push(_renderCommandShell()); + response.write(HTML(body)); +} diff --git a/trunk/etherpad/src/etherpad/billing/billing.js b/trunk/etherpad/src/etherpad/billing/billing.js new file mode 100644 index 0000000..444c233 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/billing/fields.js b/trunk/etherpad/src/etherpad/billing/fields.js new file mode 100644 index 0000000..4a307ac --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/billing/team_billing.js b/trunk/etherpad/src/etherpad/billing/team_billing.js new file mode 100644 index 0000000..ae8ae8a --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/collab/ace/contentcollector.js b/trunk/etherpad/src/etherpad/collab/ace/contentcollector.js new file mode 100644 index 0000000..5dd4f9c --- /dev/null +++ b/trunk/etherpad/src/etherpad/collab/ace/contentcollector.js @@ -0,0 +1,527 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/contentcollector.js +import("etherpad.collab.ace.easysync2.Changeset") + +/** + * 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 _MAX_LIST_LEVEL = 8; + +function sanitizeUnicode(s) { + return s.replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?'); +} + +function makeContentCollector(collectStyles, browser, apool, domInterface, + className2Author) { + browser = browser || {}; + + var dom = domInterface || { + isNodeText: function(n) { + return (n.nodeType == 3); + }, + nodeTagName: function(n) { + return n.tagName; + }, + nodeValue: function(n) { + return n.nodeValue; + }, + nodeNumChildren: function(n) { + return n.childNodes.length; + }, + nodeChild: function(n, i) { + return n.childNodes.item(i); + }, + nodeProp: function(n, p) { + return n[p]; + }, + nodeAttr: function(n, a) { + return n.getAttribute(a); + }, + optNodeInnerHTML: function(n) { + return n.innerHTML; + } + }; + + var _blockElems = { "div":1, "p":1, "pre":1, "li":1 }; + function isBlockElement(n) { + return !!_blockElems[(dom.nodeTagName(n) || "").toLowerCase()]; + } + function textify(str) { + return sanitizeUnicode( + str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' ')); + } + function getAssoc(node, name) { + return dom.nodeProp(node, "_magicdom_"+name); + } + + var lines = (function() { + var textArray = []; + var attribsArray = []; + var attribsBuilder = null; + var op = Changeset.newOp('+'); + var self = { + length: function() { return textArray.length; }, + atColumnZero: function() { + return textArray[textArray.length-1] === ""; + }, + startNew: function() { + textArray.push(""); + self.flush(true); + attribsBuilder = Changeset.smartOpAssembler(); + }, + textOfLine: function(i) { return textArray[i]; }, + appendText: function(txt, attrString) { + textArray[textArray.length-1] += txt; + //dmesg(txt+" / "+attrString); + op.attribs = attrString; + op.chars = txt.length; + attribsBuilder.append(op); + }, + textLines: function() { return textArray.slice(); }, + attribLines: function() { return attribsArray; }, + // call flush only when you're done + flush: function(withNewline) { + if (attribsBuilder) { + attribsArray.push(attribsBuilder.toString()); + attribsBuilder = null; + } + } + }; + self.startNew(); + return self; + }()); + var cc = {}; + function _ensureColumnZero(state) { + if (! lines.atColumnZero()) { + _startNewLine(state); + } + } + var selection, startPoint, endPoint; + var selStart = [-1,-1], selEnd = [-1,-1]; + var blockElems = { "div":1, "p":1, "pre":1 }; + function _isEmpty(node, state) { + // consider clean blank lines pasted in IE to be empty + if (dom.nodeNumChildren(node) == 0) return true; + if (dom.nodeNumChildren(node) == 1 && + getAssoc(node, "shouldBeEmpty") && dom.optNodeInnerHTML(node) == " " + && ! getAssoc(node, "unpasted")) { + if (state) { + var child = dom.nodeChild(node, 0); + _reachPoint(child, 0, state); + _reachPoint(child, 1, state); + } + return true; + } + return false; + } + function _pointHere(charsAfter, state) { + var ln = lines.length()-1; + var chr = lines.textOfLine(ln).length; + if (chr == 0 && state.listType && state.listType != 'none') { + chr += 1; // listMarker + } + chr += charsAfter; + return [ln, chr]; + } + function _reachBlockPoint(nd, idx, state) { + if (! dom.isNodeText(nd)) _reachPoint(nd, idx, state); + } + function _reachPoint(nd, idx, state) { + if (startPoint && nd == startPoint.node && startPoint.index == idx) { + selStart = _pointHere(0, state); + } + if (endPoint && nd == endPoint.node && endPoint.index == idx) { + selEnd = _pointHere(0, state); + } + } + function _incrementFlag(state, flagName) { + state.flags[flagName] = (state.flags[flagName] || 0)+1; + } + function _decrementFlag(state, flagName) { + state.flags[flagName]--; + } + function _incrementAttrib(state, attribName) { + if (! state.attribs[attribName]) { + state.attribs[attribName] = 1; + } + else { + state.attribs[attribName]++; + } + _recalcAttribString(state); + } + function _decrementAttrib(state, attribName) { + state.attribs[attribName]--; + _recalcAttribString(state); + } + function _enterList(state, listType) { + var oldListType = state.listType; + state.listLevel = (state.listLevel || 0)+1; + if (listType != 'none') { + state.listNesting = (state.listNesting || 0)+1; + } + state.listType = listType; + _recalcAttribString(state); + return oldListType; + } + function _exitList(state, oldListType) { + state.listLevel--; + if (state.listType != 'none') { + state.listNesting--; + } + state.listType = oldListType; + _recalcAttribString(state); + } + function _enterAuthor(state, author) { + var oldAuthor = state.author; + state.authorLevel = (state.authorLevel || 0)+1; + state.author = author; + _recalcAttribString(state); + return oldAuthor; + } + function _exitAuthor(state, oldAuthor) { + state.authorLevel--; + state.author = oldAuthor; + _recalcAttribString(state); + } + function _recalcAttribString(state) { + var lst = []; + for(var a in state.attribs) { + if (state.attribs[a]) { + lst.push([a,'true']); + } + } + if (state.authorLevel > 0) { + var authorAttrib = ['author', state.author]; + if (apool.putAttrib(authorAttrib, true) >= 0) { + // require that author already be in pool + // (don't add authors from other documents, etc.) + lst.push(authorAttrib); + } + } + state.attribString = Changeset.makeAttribsString('+', lst, apool); + } + function _produceListMarker(state) { + lines.appendText('*', Changeset.makeAttribsString( + '+', [['list', state.listType], + ['insertorder', 'first']], + apool)); + } + function _startNewLine(state) { + if (state) { + var atBeginningOfLine = lines.textOfLine(lines.length()-1).length == 0; + if (atBeginningOfLine && state.listType && state.listType != 'none') { + _produceListMarker(state); + } + } + lines.startNew(); + } + cc.notifySelection = function (sel) { + if (sel) { + selection = sel; + startPoint = selection.startPoint; + endPoint = selection.endPoint; + } + }; + cc.collectContent = function (node, state) { + if (! state) { + state = {flags: {/*name -> nesting counter*/}, + attribs: {/*name -> nesting counter*/}, + attribString: ''}; + } + var isBlock = isBlockElement(node); + var isEmpty = _isEmpty(node, state); + if (isBlock) _ensureColumnZero(state); + var startLine = lines.length()-1; + _reachBlockPoint(node, 0, state); + if (dom.isNodeText(node)) { + var txt = dom.nodeValue(node); + var rest = ''; + var x = 0; // offset into original text + if (txt.length == 0) { + if (startPoint && node == startPoint.node) { + selStart = _pointHere(0, state); + } + if (endPoint && node == endPoint.node) { + selEnd = _pointHere(0, state); + } + } + while (txt.length > 0) { + var consumed = 0; + if (state.flags.preMode) { + var firstLine = txt.split('\n',1)[0]; + consumed = firstLine.length+1; + rest = txt.substring(consumed); + txt = firstLine; + } + else { /* will only run this loop body once */ } + if (startPoint && node == startPoint.node && + startPoint.index-x <= txt.length) { + selStart = _pointHere(startPoint.index-x, state); + } + if (endPoint && node == endPoint.node && + endPoint.index-x <= txt.length) { + selEnd = _pointHere(endPoint.index-x, state); + } + var txt2 = txt; + if ((! state.flags.preMode) && /^[\r\n]*$/.exec(txt)) { + // prevents textnodes containing just "\n" from being significant + // in safari when pasting text, now that we convert them to + // spaces instead of removing them, because in other cases + // removing "\n" from pasted HTML will collapse words together. + txt2 = ""; + } + var atBeginningOfLine = lines.textOfLine(lines.length()-1).length == 0; + if (atBeginningOfLine) { + // newlines in the source mustn't become spaces at beginning of line box + txt2 = txt2.replace(/^\n*/, ''); + } + if (atBeginningOfLine && state.listType && state.listType != 'none') { + _produceListMarker(state); + } + lines.appendText(textify(txt2), state.attribString); + x += consumed; + txt = rest; + if (txt.length > 0) { + _startNewLine(state); + } + } + } + else { + var tname = (dom.nodeTagName(node) || "").toLowerCase(); + if (tname == "br") { + _startNewLine(state); + } + else if (tname == "script" || tname == "style") { + // ignore + } + else if (! isEmpty) { + var styl = dom.nodeAttr(node, "style"); + var cls = dom.nodeProp(node, "className"); + + var isPre = (tname == "pre"); + if ((! isPre) && browser.safari) { + isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl)); + } + if (isPre) _incrementFlag(state, 'preMode'); + var attribs = null; + var oldListTypeOrNull = null; + var oldAuthorOrNull = null; + if (collectStyles) { + function doAttrib(na) { + attribs = (attribs || []); + attribs.push(na); + _incrementAttrib(state, na); + } + if (tname == "b" || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || + tname == "strong") { + doAttrib("bold"); + } + if (tname == "i" || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) || + tname == "em") { + doAttrib("italic"); + } + if (tname == "u" || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || + tname == "ins") { + doAttrib("underline"); + } + if (tname == "s" || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || + tname == "del") { + doAttrib("strikethrough"); + } + if (tname == "h1") { + doAttrib("h1"); + } + if (tname == "h2") { + doAttrib("h2"); + } + if (tname == "h3") { + doAttrib("h3"); + } + if (tname == "h4") { + doAttrib("h4"); + } + if (tname == "h5") { + doAttrib("h5"); + } + if (tname == "h6") { + doAttrib("h6"); + } + if (tname == "ul") { + var type; + var rr = cls && /(?:^| )list-(bullet[12345678])\b/.exec(cls); + type = rr && rr[1] || "bullet"+ + String(Math.min(_MAX_LIST_LEVEL, (state.listNesting||0)+1)); + oldListTypeOrNull = (_enterList(state, type) || 'none'); + } + else if ((tname == "div" || tname == "p") && cls && + cls.match(/(?:^| )ace-line\b/)) { + oldListTypeOrNull = (_enterList(state, type) || 'none'); + } + if (className2Author && cls) { + var classes = cls.match(/\S+/g); + if (classes && classes.length > 0) { + for(var i=0;i=0; i--) { + var oldString = lineStrings[i]; + var oldAttribString = lineAttribs[i]; + if (oldString.length > lineLimit+buffer) { + var newStrings = []; + var newAttribStrings = []; + while (oldString.length > lineLimit) { + //var semiloc = oldString.lastIndexOf(';', lineLimit-1); + //var lengthToTake = (semiloc >= 0 ? (semiloc+1) : lineLimit); + lengthToTake = lineLimit; + newStrings.push(oldString.substring(0, lengthToTake)); + oldString = oldString.substring(lengthToTake); + newAttribStrings.push(Changeset.subattribution(oldAttribString, + 0, lengthToTake)); + oldAttribString = Changeset.subattribution(oldAttribString, + lengthToTake); + } + if (oldString.length > 0) { + newStrings.push(oldString); + newAttribStrings.push(oldAttribString); + } + function fixLineNumber(lineChar) { + if (lineChar[0] < 0) return; + var n = lineChar[0]; + var c = lineChar[1]; + if (n > i) { + n += (newStrings.length-1); + } + else if (n == i) { + var a = 0; + while (c > newStrings[a].length) { + c -= newStrings[a].length; + a++; + } + n += a; + } + lineChar[0] = n; + lineChar[1] = c; + } + fixLineNumber(ss); + fixLineNumber(se); + linesWrapped++; + numLinesAfter += newStrings.length; + + newStrings.unshift(i, 1); + lineStrings.splice.apply(lineStrings, newStrings); + newAttribStrings.unshift(i, 1); + lineAttribs.splice.apply(lineAttribs, newAttribStrings); + } + } + return {linesWrapped:linesWrapped, numLinesAfter:numLinesAfter}; + } + var wrapData = fixLongLines(); + + return { selStart: ss, selEnd: se, linesWrapped: wrapData.linesWrapped, + numLinesAfter: wrapData.numLinesAfter, + lines: lineStrings, lineAttribs: lineAttribs }; + } + + return cc; +} diff --git a/trunk/etherpad/src/etherpad/collab/ace/domline.js b/trunk/etherpad/src/etherpad/collab/ace/domline.js new file mode 100644 index 0000000..de2e7d3 --- /dev/null +++ b/trunk/etherpad/src/etherpad/collab/ace/domline.js @@ -0,0 +1,210 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/domline.js + +/** + * 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 domline = {}; +domline.noop = function() {}; +domline.identity = function(x) { return x; }; + +domline.addToLineClass = function(lineClass, cls) { + // an "empty span" at any point can be used to add classes to + // the line, using line:className. otherwise, we ignore + // the span. + cls.replace(/\S+/g, function (c) { + if (c.indexOf("line:") == 0) { + // add class to line + lineClass = (lineClass ? lineClass+' ' : '')+c.substring(5); + } + }); + return lineClass; +} + +// if "document" is falsy we don't create a DOM node, just +// an object with innerHTML and className +domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) { + var result = { node: null, + appendSpan: domline.noop, + prepareForAdd: domline.noop, + notifyAdded: domline.noop, + clearSpans: domline.noop, + finishUpdate: domline.noop, + lineMarker: 0 }; + + var browser = (optBrowser || {}); + var document = optDocument; + + if (document) { + result.node = document.createElement("div"); + } + else { + result.node = {innerHTML: '', className: ''}; + } + + var html = []; + var preHtml, postHtml; + var curHTML = null; + function processSpaces(s) { + return domline.processSpaces(s, doesWrap); + } + var identity = domline.identity; + var perTextNodeProcess = (doesWrap ? identity : processSpaces); + var perHtmlLineProcess = (doesWrap ? processSpaces : identity); + var lineClass = 'ace-line'; + result.appendSpan = function(txt, cls) { + if (cls.indexOf('list') >= 0) { + var listType = /(?:^| )list:(\S+)/.exec(cls); + if (listType) { + listType = listType[1]; + if (listType) { + preHtml = ''; + } + result.lineMarker += txt.length; + return; // don't append any text + } + } + var href = null; + var simpleTags = null; + if (cls.indexOf('url') >= 0) { + cls = cls.replace(/(^| )url:(\S+)/g, function(x0, space, url) { + href = url; + return space+"url"; + }); + } + if (cls.indexOf('tag') >= 0) { + cls = cls.replace(/(^| )tag:(\S+)/g, function(x0, space, tag) { + if (! simpleTags) simpleTags = []; + simpleTags.push(tag.toLowerCase()); + return space+tag; + }); + } + if ((! txt) && cls) { + lineClass = domline.addToLineClass(lineClass, cls); + } + else if (txt) { + var extraOpenTags = ""; + var extraCloseTags = ""; + if (href) { + extraOpenTags = extraOpenTags+''; + extraCloseTags = ''+extraCloseTags; + } + if (simpleTags) { + simpleTags.sort(); + extraOpenTags = extraOpenTags+'<'+simpleTags.join('><')+'>'; + simpleTags.reverse(); + extraCloseTags = ''+extraCloseTags; + } + html.push('',extraOpenTags, + perTextNodeProcess(domline.escapeHTML(txt)), + extraCloseTags,''); + } + }; + result.clearSpans = function() { + html = []; + lineClass = ''; // non-null to cause update + result.lineMarker = 0; + }; + function writeHTML() { + var newHTML = perHtmlLineProcess(html.join('')); + if (! newHTML) { + if ((! document) || (! optBrowser)) { + newHTML += ' '; + } + else if (! browser.msie) { + newHTML += '
'; + } + } + if (nonEmpty) { + newHTML = (preHtml||'')+newHTML+(postHtml||''); + } + html = preHtml = postHtml = null; // free memory + if (newHTML !== curHTML) { + curHTML = newHTML; + result.node.innerHTML = curHTML; + } + if (lineClass !== null) result.node.className = lineClass; + } + result.prepareForAdd = writeHTML; + result.finishUpdate = writeHTML; + result.getInnerHTML = function() { return curHTML || ''; }; + + return result; +}; + +domline.escapeHTML = function(s) { + var re = /[&<>'"]/g; /']/; // stupid indentation thing + if (! re.MAP) { + // persisted across function calls! + re.MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + } + return s.replace(re, function(c) { return re.MAP[c]; }); +}; + +domline.processSpaces = function(s, doesWrap) { + if (s.indexOf("<") < 0 && ! doesWrap) { + // short-cut + return s.replace(/ /g, ' '); + } + 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] = ' '; + endOfLine = false; + beforeSpace = true; + } + else if (p.charAt(0) != "<") { + endOfLine = false; + beforeSpace = false; + } + } + // beginning of line is nbsp + for(var i=0;i= 0, "bad old text length"); + assert(this[2] >= 0, "bad new text length"); + assert((this.length % 3) == 0, "bad array length"); + assert(this.length >= 6, "must be at least one strip"); + var numStrips = this.numStrips(); + var oldLen = this[1]; + var newLen = this[2]; + // iterate over the "text strips" + var actualNewLen = 0; + this.eachStrip(function(startIndex, numTaken, newText, i) { + var s = startIndex, t = numTaken, n = newText; + var isFirst = (i == 0); + var isLast = (i == numStrips-1); + assert(t >= 0, "can't take negative number of chars"); + assert(isFirst || t > 0, "all strips but first must take"); + assert((t > 0) || (s == 0), "if first strip doesn't take, must have 0 startIndex"); + assert(s >= 0 && s + t <= oldLen, "bad index: "+this.toString()); + assert(t > 0 || n.length > 0 || (isFirst && isLast), "empty strip must be first and only"); + if (! isLast) { + var s2 = this[3 + i*3 + 3]; // startIndex of following strip + var gap = s2 - (s + t); + assert(gap >= 0, "overlapping or out-of-order strips: "+this.toString()); + assert(gap > 0 || n.length > 0, "touching strips with no added text"); + } + actualNewLen += t + n.length; + }); + assert(newLen == actualNewLen, "calculated new text length doesn't match"); + } + + array.applyToText = function(text) { + assert(text.length == this.oldLen(), "mismatched apply: "+text.length+" / "+this.oldLen()); + var buf = []; + this.eachStrip(function (s, t, n) { + buf.push(text.substr(s, t), n); + }); + return buf.join(''); + } + + function _makeBuilder(oldLen, supportAuthors) { + var C = Changeset(oldLen); + if (supportAuthors) { + _ensureAuthors(C); + } + return C.builder(); + } + + function _getNumInserted(C) { + var numChars = 0; + C.eachStrip(function(s,t,n) { + numChars += n.length; + }); + return numChars; + } + + function _ensureAuthors(C) { + if (! C.authors) { + C.setAuthor(); + } + return C; + } + + array.setAuthor = function(author) { + var C = this; + // authors array has even length >= 2; + // alternates [numChars1, author1, numChars2, author2]; + // all numChars > 0 unless there is exactly one, in which + // case it can be == 0. + C.authors = [_getNumInserted(C), author || '']; + return C; + } + + array.builder = function() { + // normal pattern is Changeset(oldLength).builder().appendOldText(...). ... + // builder methods mutate this! + var C = this; + // OOP style: state in environment + var self; + return self = { + appendNewText: function(str, author) { + C[C.length-1] += str; + C[2] += str.length; + + if (C.authors) { + var a = (author || ''); + var lastAuthorPtr = C.authors.length-1; + var lastAuthorLengthPtr = C.authors.length-2; + if ((!a) || a == C.authors[lastAuthorPtr]) { + C.authors[lastAuthorLengthPtr] += str.length; + } + else if (0 == C.authors[lastAuthorLengthPtr]) { + C.authors[lastAuthorLengthPtr] = str.length; + C.authors[lastAuthorPtr] = (a || C.authors[lastAuthorPtr]); + } + else { + C.authors.push(str.length, a); + } + } + + return self; + }, + appendOldText: function(startIndex, numTaken) { + if (numTaken == 0) return self; + // properties of last strip... + var s = C[C.length-3], t = C[C.length-2], n = C[C.length-1]; + if (t == 0 && n == "") { + // must be empty changeset, one strip that doesn't take old chars or add new ones + C[C.length-3] = startIndex; + C[C.length-2] = numTaken; + } + else if (n == "" && (s+t == startIndex)) { + C[C.length-2] += numTaken; // take more + } + else C.push(startIndex, numTaken, ""); // add a strip + C[2] += numTaken; + C.checkRep(); + return self; + }, + toChangeset: function() { return C; } + }; + } + + array.authorSlicer = function(outputBuilder) { + return _makeAuthorSlicer(this, outputBuilder); + } + + function _makeAuthorSlicer(changesetOrAuthorsIn, builderOut) { + // "builderOut" only needs to support appendNewText + var authors; // considered immutable + if (changesetOrAuthorsIn.isChangeset) { + authors = changesetOrAuthorsIn.authors; + } + else { + authors = changesetOrAuthorsIn; + } + + // OOP style: state in environment + var authorPtr = 0; + var charIndex = 0; + var charWithinAuthor = 0; // 0 <= charWithinAuthor <= authors[authorPtr]; max value iff atEnd + var atEnd = false; + function curAuthor() { return authors[authorPtr+1]; } + function curAuthorWidth() { return authors[authorPtr]; } + function assertNotAtEnd() { assert(! atEnd, "_authorSlicer: can't move past end"); } + function forwardInAuthor(numChars) { + charWithinAuthor += numChars; + charIndex += numChars; + } + function nextAuthor() { + assertNotAtEnd(); + assert(charWithinAuthor == curAuthorWidth(), "_authorSlicer: not at author end"); + charWithinAuthor = 0; + authorPtr += 2; + if (authorPtr == authors.length) { + atEnd = true; + } + } + + var self; + return self = { + skipChars: function(n) { + assert(n >= 0, "_authorSlicer: can't skip negative n"); + if (n == 0) return; + assertNotAtEnd(); + + var leftToSkip = n; + while (leftToSkip > 0) { + var leftInAuthor = curAuthorWidth() - charWithinAuthor; + if (leftToSkip >= leftInAuthor) { + forwardInAuthor(leftInAuthor); + leftToSkip -= leftInAuthor; + nextAuthor(); + } + else { + forwardInAuthor(leftToSkip); + leftToSkip = 0; + } + } + }, + takeChars: function(n, text) { + assert(n >= 0, "_authorSlicer: can't take negative n"); + if (n == 0) return; + assertNotAtEnd(); + assert(n == text.length, "_authorSlicer: bad text length"); + + var textLeft = text; + var leftToTake = n; + while (leftToTake > 0) { + if (curAuthorWidth() > 0 && charWithinAuthor < curAuthorWidth()) { + // at least one char to take from current author + var leftInAuthor = (curAuthorWidth() - charWithinAuthor); + assert(leftInAuthor > 0, "_authorSlicer: should have leftInAuthor > 0"); + var toTake = min(leftInAuthor, leftToTake); + assert(toTake > 0, "_authorSlicer: should have toTake > 0"); + builderOut.appendNewText(textLeft.substring(0, toTake), curAuthor()); + forwardInAuthor(toTake); + leftToTake -= toTake; + textLeft = textLeft.substring(toTake); + } + assert(charWithinAuthor <= curAuthorWidth(), "_authorSlicer: past end of author"); + if (charWithinAuthor == curAuthorWidth()) { + nextAuthor(); + } + } + }, + setBuilder: function(builder) { + builderOut = builder; + } + }; + } + + function _makeSlicer(C, output) { + // C: Changeset, output: builder from _makeBuilder + // C is considered immutable, won't change or be changed + + // OOP style: state in environment + var charIndex = 0; // 0 <= charIndex <= C.newLen(); maximum value iff atEnd + var stripIndex = 0; // 0 <= stripIndex <= C.numStrips(); maximum value iff atEnd + var charWithinStrip = 0; // 0 <= charWithinStrip < curStripWidth() + var atEnd = false; + + var authorSlicer; + if (C.authors) { + authorSlicer = _makeAuthorSlicer(C.authors, output); + } + + var ptr = 3; + function curStartIndex() { return C[ptr]; } + function curNumTaken() { return C[ptr+1]; } + function curNewText() { return C[ptr+2]; } + function curStripWidth() { return curNumTaken() + curNewText().length; } + function assertNotAtEnd() { assert(! atEnd, "_slicer: can't move past changeset end"); } + function forwardInStrip(numChars) { + charWithinStrip += numChars; + charIndex += numChars; + } + function nextStrip() { + assertNotAtEnd(); + assert(charWithinStrip == curStripWidth(), "_slicer: not at strip end"); + charWithinStrip = 0; + stripIndex++; + ptr += 3; + if (stripIndex == C.numStrips()) { + atEnd = true; + } + } + function curNumNewCharsInRange(start, end) { + // takes two indices into the current strip's combined "taken" and "new" + // chars, and returns how many "new" chars are included in the range + assert(start <= end, "_slicer: curNumNewCharsInRange given out-of-order indices"); + var nt = curNumTaken(); + var nn = curNewText().length; + var s = nt; + var e = nt+nn; + if (s < start) s = start; + if (e > end) e = end; + if (e < s) return 0; + return e-s; + } + + var self; + return self = { + skipChars: function (n) { + assert(n >= 0, "_slicer: can't skip negative n"); + if (n == 0) return; + assertNotAtEnd(); + + var leftToSkip = n; + while (leftToSkip > 0) { + var leftInStrip = curStripWidth() - charWithinStrip; + if (leftToSkip >= leftInStrip) { + forwardInStrip(leftInStrip); + + if (authorSlicer) + authorSlicer.skipChars(curNumNewCharsInRange(charWithinStrip, + charWithinStrip + leftInStrip)); + + leftToSkip -= leftInStrip; + nextStrip(); + } + else { + if (authorSlicer) + authorSlicer.skipChars(curNumNewCharsInRange(charWithinStrip, + charWithinStrip + leftToSkip)); + + forwardInStrip(leftToSkip); + leftToSkip = 0; + } + } + }, + takeChars: function (n) { + assert(n >= 0, "_slicer: can't take negative n"); + if (n == 0) return; + assertNotAtEnd(); + + var leftToTake = n; + while (leftToTake > 0) { + if (curNumTaken() > 0 && charWithinStrip < curNumTaken()) { + // at least one char to take from current strip's numTaken + var leftInTaken = (curNumTaken() - charWithinStrip); + assert(leftInTaken > 0, "_slicer: should have leftInTaken > 0"); + var toTake = min(leftInTaken, leftToTake); + assert(toTake > 0, "_slicer: should have toTake > 0"); + output.appendOldText(curStartIndex() + charWithinStrip, toTake); + forwardInStrip(toTake); + leftToTake -= toTake; + } + if (leftToTake > 0 && curNewText().length > 0 && charWithinStrip >= curNumTaken() && + charWithinStrip < curStripWidth()) { + // at least one char to take from current strip's newText + var leftInNewText = (curStripWidth() - charWithinStrip); + assert(leftInNewText > 0, "_slicer: should have leftInNewText > 0"); + var toTake = min(leftInNewText, leftToTake); + assert(toTake > 0, "_slicer: should have toTake > 0"); + var newText = curNewText().substr(charWithinStrip - curNumTaken(), toTake); + if (authorSlicer) { + authorSlicer.takeChars(newText.length, newText); + } + else { + output.appendNewText(newText); + } + forwardInStrip(toTake); + leftToTake -= toTake; + } + assert(charWithinStrip <= curStripWidth(), "_slicer: past end of strip"); + if (charWithinStrip == curStripWidth()) { + nextStrip(); + } + } + }, + skipTo: function(n) { + self.skipChars(n - charIndex); + } + }; + } + + array.slicer = function(outputBuilder) { + return _makeSlicer(this, outputBuilder); + } + + array.compose = function(next) { + assert(next.oldLen() == this.newLen(), "mismatched composition"); + + var builder = _makeBuilder(this.oldLen(), !!(this.authors || next.authors)); + var slicer = _makeSlicer(this, builder); + + var authorSlicer; + if (next.authors) { + authorSlicer = _makeAuthorSlicer(next.authors, builder); + } + + next.eachStrip(function(s, t, n) { + slicer.skipTo(s); + slicer.takeChars(t); + if (authorSlicer) { + authorSlicer.takeChars(n.length, n); + } + else { + builder.appendNewText(n); + } + }, this); + + return builder.toChangeset(); + }; + + array.traverser = function() { + return _makeTraverser(this); + } + + function _makeTraverser(C) { + var s = C[3], t = C[4], n = C[5]; + var nextIndex = 6; + var indexIntoNewText = 0; + + var authorSlicer; + if (C.authors) { + authorSlicer = _makeAuthorSlicer(C.authors, null); + } + + function advanceIfPossible() { + if (t == 0 && n == "" && nextIndex < C.length) { + s = C[nextIndex]; + t = C[nextIndex+1]; + n = C[nextIndex+2]; + nextIndex += 3; + } + } + + var self; + return self = { + numTakenChars: function() { + // if starts with taken characters, then how many, else 0 + return (t > 0) ? t : 0; + }, + numNewChars: function() { + // if starts with new characters, then how many, else 0 + return (t == 0 && n.length > 0) ? n.length : 0; + }, + takenCharsStart: function() { + return (self.numTakenChars() > 0) ? s : 0; + }, + hasMore: function() { + return self.numTakenChars() > 0 || self.numNewChars() > 0; + }, + curIndex: function() { + return indexIntoNewText; + }, + consumeTakenChars: function (x) { + assert(self.numTakenChars() > 0, "_traverser: no taken chars"); + assert(x >= 0 && x <= self.numTakenChars(), "_traverser: bad number of taken chars"); + if (x == 0) return; + if (t == x) { s = 0; t = 0; } + else { s += x; t -= x; } + indexIntoNewText += x; + advanceIfPossible(); + }, + consumeNewChars: function(x) { + return self.appendNewChars(x, null); + }, + appendNewChars: function(x, builder) { + assert(self.numNewChars() > 0, "_traverser: no new chars"); + assert(x >= 0 && x <= self.numNewChars(), "_traverser: bad number of new chars"); + if (x == 0) return ""; + var str = n.substring(0, x); + n = n.substring(x); + indexIntoNewText += x; + advanceIfPossible(); + + if (builder) { + if (authorSlicer) { + authorSlicer.setBuilder(builder); + authorSlicer.takeChars(x, str); + } + else { + builder.appendNewText(str); + } + } + else { + if (authorSlicer) authorSlicer.skipChars(x); + return str; + } + }, + consumeAvailableTakenChars: function() { + return self.consumeTakenChars(self.numTakenChars()); + }, + consumeAvailableNewChars: function() { + return self.consumeNewChars(self.numNewChars()); + }, + appendAvailableNewChars: function(builder) { + return self.appendNewChars(self.numNewChars(), builder); + } + }; + } + + array.follow = function(prev, reverseInsertOrder) { + // prev: Changeset, reverseInsertOrder: boolean + + // A.compose(B.follow(A)) is the merging of Changesets A and B, which operate on the same old text. + // It is always the same as B.compose(A.follow(B, true)). + + assert(prev.oldLen() == this.oldLen(), "mismatched follow: "+prev.oldLen()+"/"+this.oldLen()); + var builder = _makeBuilder(prev.newLen(), !! this.authors); + var a = _makeTraverser(prev); + var b = _makeTraverser(this); + while (a.hasMore() || b.hasMore()) { + if (a.numNewChars() > 0 && ! reverseInsertOrder) { + builder.appendOldText(a.curIndex(), a.numNewChars()); + a.consumeAvailableNewChars(); + } + else if (b.numNewChars() > 0) { + b.appendAvailableNewChars(builder); + } + else if (a.numNewChars() > 0 && reverseInsertOrder) { + builder.appendOldText(a.curIndex(), a.numNewChars()); + a.consumeAvailableNewChars(); + } + else if (! b.hasMore()) a.consumeAvailableTakenChars(); + else if (! a.hasMore()) b.consumeAvailableTakenChars(); + else { + var x = a.takenCharsStart(); + var y = b.takenCharsStart(); + if (x < y) a.consumeTakenChars(min(a.numTakenChars(), y-x)); + else if (y < x) b.consumeTakenChars(min(b.numTakenChars(), x-y)); + else { + var takenByBoth = min(a.numTakenChars(), b.numTakenChars()); + builder.appendOldText(a.curIndex(), takenByBoth); + a.consumeTakenChars(takenByBoth); + b.consumeTakenChars(takenByBoth); + } + } + } + return builder.toChangeset(); + } + + array.encodeToString = function(asBinary) { + var stringDataArray = []; + var numsArray = []; + if (! asBinary) numsArray.push(this[0]); + numsArray.push(this[1], this[2]); + this.eachStrip(function(s, t, n) { + numsArray.push(s, t, n.length); + stringDataArray.push(n); + }, this); + if (! asBinary) { + return numsArray.join(',')+'|'+stringDataArray.join(''); + } + else { + return "A" + Changeset.numberArrayToString(numsArray) + +escapeCrazyUnicode(stringDataArray.join('')); + } + } + + function escapeCrazyUnicode(str) { + return str.replace(/\\/g, '\\\\').replace(/[\ud800-\udfff]/g, function (c) { + return "\\u"+("0000"+c.charCodeAt(0).toString(16)).slice(-4); + }); + } + + array.applyToAttributedText = Changeset.applyToAttributedText; + + function splicesFromChanges(c) { + var splices = []; + // get a list of splices, [startChar, endChar, newText] + var traverser = c.traverser(); + var oldTextLength = c.oldLen(); + var indexIntoOldText = 0; + while (traverser.hasMore() || indexIntoOldText < oldTextLength) { + var newText = ""; + var startChar = indexIntoOldText; + var endChar = indexIntoOldText; + if (traverser.numNewChars() > 0) { + newText = traverser.consumeAvailableNewChars(); + } + if (traverser.hasMore()) { + endChar = traverser.takenCharsStart(); + indexIntoOldText = endChar + traverser.numTakenChars(); + traverser.consumeAvailableTakenChars(); + } + else { + endChar = oldTextLength; + indexIntoOldText = endChar; + } + if (endChar != startChar || newText.length > 0) { + splices.push([startChar, endChar, newText]); + } + } + return splices; + } + + array.toSplices = function() { + return splicesFromChanges(this); + } + + array.characterRangeFollowThis = function(selStartChar, selEndChar, insertionsAfter) { + var changeset = this; + // represent the selection as a changeset that replaces the selection with some finite string. + // Because insertions indicate intention, it doesn't matter what this string is, and even + // if the selectionChangeset is made to "follow" other changes it will still be the only + // insertion. + var selectionChangeset = + Changeset(changeset.oldLen()).builder().appendOldText(0, selStartChar).appendNewText( + "X").appendOldText(selEndChar, changeset.oldLen() - selEndChar).toChangeset(); + var newSelectionChangeset = selectionChangeset.follow(changeset, insertionsAfter); + var selectionSplices = newSelectionChangeset.toSplices(); + function includeChar(i) { + if (! includeChar.calledYet) { + selStartChar = i; + selEndChar = i; + includeChar.calledYet = true; + } + else { + if (i < selStartChar) selStartChar = i; + if (i > selEndChar) selEndChar = i; + } + } + for(var i=0; i TRUNC) a.push("..."); + return a.join(' '); + } + function unescapeCrazyUnicode(str) { + return str.replace(/\\(u....|\\)/g, function(seq) { + if (seq == "\\\\") return "\\"; + return String.fromCharCode(Number("0x"+seq.substring(2))); + }); + } + + var numData, stringData; + var binary = false; + var typ = str.charAt(0); + if (typ == "B" || typ == "A") { + var result = Changeset.numberArrayFromString(str, 1); + numData = result[0]; + stringData = result[1]; + if (typ == "A") { + stringData = unescapeCrazyUnicode(stringData); + } + binary = true; + } + else if (typ == "C") { + var barPosition = str.indexOf('|'); + numData = str.substring(0, barPosition).split(','); + stringData = str.substring(barPosition+1); + } + else { + error("Not a changeset: "+toHex(str)); + } + var stringDataOffset = 0; + var array = []; + var ptr; + if (binary) { + array.push("Changeset", numData[0], numData[1]); + var ptr = 2; + } + else { + array.push(numData[0], Number(numData[1]), Number(numData[2])); + var ptr = 3; + } + while (ptr < numData.length) { + array.push(Number(numData[ptr++]), Number(numData[ptr++])); + var newTextLength = Number(numData[ptr++]); + array.push(stringData.substr(stringDataOffset, newTextLength)); + stringDataOffset += newTextLength; + } + if (stringDataOffset != stringData.length) { + error("Extra character data beyond end of encoded string ("+toHex(str)+")"); + } + return Changeset(array); +}; + +Changeset.numberArrayToString = function(nums) { + var array = []; + function writeNum(n) { + // does not support negative numbers + var twentyEightBit = (n & 0xfffffff); + if (twentyEightBit <= 0x7fff) { + array.push(String.fromCharCode(twentyEightBit)); + } + else { + array.push(String.fromCharCode(0xa000 | (twentyEightBit >> 15), + twentyEightBit & 0x7fff)); + } + } + writeNum(nums.length); + var len = nums.length; + for(var i=0;i 0x7fff) { + if (n >= 0xa000) { + n = (((n & 0x1fff) << 15) | str.charCodeAt(strIndex++)); + } + else { + // legacy format + n = (((n & 0x1fff) << 16) | str.charCodeAt(strIndex++)); + } + } + return n; + } + var len = readNum(); + for(var i=0;i> 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 runMatcher(c) { + // Takes "A" and returns /\u0041+/g . + // Avoid creating new objects unnecessarily by caching matchers + // as properties of this function. + var re = runMatcher[c]; + if (re) return re; + re = runMatcher[c] = new RegExp("\\u"+("0000"+c.charCodeAt(0).toString(16)).slice(-4)+"+", 'g'); + return re; + } + function runLength(str, idx, c) { + var re = runMatcher(c); + re.lastIndex = idx; + var result = re.exec(str); + if (result && result[0]) { + return result[0].length; + } + return 0; + } + + // emptyObj may be a StorableObject + Changeset.initAttributedText = function(emptyObj, initialString, initialAuthor) { + var obj = emptyObj; + obj.authorMap = { 1: (initialAuthor || '') }; + obj.text = (initialString || ''); + obj.attribs = repeatString(chr(1), obj.text.length); + return obj; + }; + Changeset.gcAttributedText = function(atObj) { + // "garbage collect" the list of authors + var removedAuthors = []; + for(var a in atObj.authorMap) { + if (atObj.attribs.indexOf(chr(Number(a))) < 0) { + removedAuthors.push(atObj.authorMap[a]); + delete atObj.authorMap[a]; + } + } + return removedAuthors; + }; + Changeset.cloneAttributedText = function(emptyObj, atObj) { + var obj = emptyObj; + obj.text = atObj.text; // string + if (atObj.attribs) obj.attribs = atObj.attribs; // string + if (atObj.attribs_c) obj.attribs_c = atObj.attribs_c; // string + obj.authorMap = {}; + for(var a in atObj.authorMap) { + obj.authorMap[a] = atObj.authorMap[a]; + } + return obj; + }; + Changeset.applyToAttributedText = function(atObj, C) { + C = (C || this); + var oldText = atObj.text; + var oldAttribs = atObj.attribs; + Changeset._assert(C.isChangeset, "applyToAttributedText: 'this' is not a changeset"); + Changeset._assert(oldText.length == C.oldLen(), + "applyToAttributedText: mismatch "+oldText.length+" / "+C.oldLen()); + var textBuf = []; + var attribsBuf = []; + var authorMap = atObj.authorMap; + function authorId(author) { + for(var a in authorMap) { + if (authorMap[Number(a)] === author) { + return Number(a); + } + } + for(var i=1;i<=60000;i++) { + // don't use "in" because it's currently broken on StorableObjects + if (authorMap[i] === undefined) { + authorMap[i] = author; + return i; + } + } + } + var myBuilder = { appendNewText: function(txt, author) { + // object that acts as a "builder" in that it receives requests from + // authorSlicer to append text attributed to different authors + attribsBuf.push(repeatString(chr(authorId(author)), txt.length)); + } }; + var authorSlicer; + if (C.authors) { + authorSlicer = C.authorSlicer(myBuilder); + } + C.eachStrip(function (s, t, n) { + textBuf.push(oldText.substr(s, t), n); + attribsBuf.push(oldAttribs.substr(s, t)); + if (authorSlicer) { + authorSlicer.takeChars(n.length, n); + } + else { + myBuilder.appendNewText(n, ''); + } + }); + atObj.text = textBuf.join(''); + atObj.attribs = attribsBuf.join(''); + return atObj; + }; + Changeset.getAttributedTextCharAuthor = function(atObj, idx) { + return atObj.authorMap[ord(atObj.attribs.charAt(idx))]; + }; + Changeset.getAttributedTextCharRunLength = function(atObj, idx) { + var c = atObj.attribs.charAt(idx); + return runLength(atObj.attribs, idx, c); + }; + Changeset.eachAuthorInAttributedText = function(atObj, func) { + // call func(author, authorNum) + for(var a in atObj.authorMap) { + if (func(atObj.authorMap[a], Number(a))) break; + } + }; + Changeset.getAttributedTextAuthorByNum = function(atObj, n) { + return atObj.authorMap[n]; + }; + // Compressed attributed text can be cloned, but nothing else until uncompressed!! + Changeset.compressAttributedText = function(atObj) { + // idempotent, mutates the object, returns it + if (atObj.attribs) { + atObj.attribs_c = atObj.attribs.replace(/([\s\S])\1{0,63}/g, function(run) { + return run.charAt(0)+chr(run.length);; + }); + delete atObj.attribs; + } + return atObj; + }; + Changeset.decompressAttributedText = function(atObj) { + // idempotent, mutates the object, returns it + if (atObj.attribs_c) { + atObj.attribs = atObj.attribs_c.replace(/[\s\S][\s\S]/g, function(run) { + return repeatString(run.charAt(0), ord(run.charAt(1))); + }); + delete atObj.attribs_c; + } + return atObj; + }; +})(); diff --git a/trunk/etherpad/src/etherpad/collab/ace/easysync2.js b/trunk/etherpad/src/etherpad/collab/ace/easysync2.js new file mode 100644 index 0000000..0fa1ec4 --- /dev/null +++ b/trunk/etherpad/src/etherpad/collab/ace/easysync2.js @@ -0,0 +1,1968 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/easysync2.js +jimport("com.etherpad.Easysync2Support"); + +/** + * 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 _opt = (this.Easysync2Support || null); +var _opt = null; // disable optimization for now + +function AttribPool() { + var p = {}; + p.numToAttrib = {}; // e.g. {0: ['foo','bar']} + p.attribToNum = {}; // e.g. {'foo,bar': 0} + p.nextNum = 0; + + p.putAttrib = function(attrib, dontAddIfAbsent) { + var str = String(attrib); + if (str in p.attribToNum) { + return p.attribToNum[str]; + } + if (dontAddIfAbsent) { + return -1; + } + var num = p.nextNum++; + p.attribToNum[str] = num; + p.numToAttrib[num] = [String(attrib[0]||''), + String(attrib[1]||'')]; + return num; + }; + + p.getAttrib = function(num) { + var pair = p.numToAttrib[num]; + if (! pair) return pair; + return [pair[0], pair[1]]; // return a mutable copy + }; + + p.getAttribKey = function(num) { + var pair = p.numToAttrib[num]; + if (! pair) return ''; + return pair[0]; + }; + + p.getAttribValue = function(num) { + var pair = p.numToAttrib[num]; + if (! pair) return ''; + return pair[1]; + }; + + p.eachAttrib = function(func) { + for(var n in p.numToAttrib) { + var pair = p.numToAttrib[n]; + func(pair[0], pair[1]); + } + }; + + p.toJsonable = function() { + return {numToAttrib: p.numToAttrib, nextNum: p.nextNum}; + }; + + p.fromJsonable = function(obj) { + p.numToAttrib = obj.numToAttrib; + p.nextNum = obj.nextNum; + p.attribToNum = {}; + for(var n in p.numToAttrib) { + p.attribToNum[String(p.numToAttrib[n])] = Number(n); + } + return p; + }; + + return p; +} + +var Changeset = {}; + +Changeset.error = function error(msg) { var e = new Error(msg); e.easysync = true; throw e; }; +Changeset.assert = function assert(b, msgParts) { + if (! b) { + var msg = Array.prototype.slice.call(arguments, 1).join(''); + Changeset.error("Changeset: "+msg); + } +}; + +Changeset.parseNum = function(str) { return parseInt(str, 36); }; +Changeset.numToString = function(num) { return num.toString(36).toLowerCase(); }; +Changeset.toBaseTen = function(cs) { + var dollarIndex = cs.indexOf('$'); + var beforeDollar = cs.substring(0, dollarIndex); + var fromDollar = cs.substring(dollarIndex); + return beforeDollar.replace(/[0-9a-z]+/g, function(s) { + return String(Changeset.parseNum(s)); }) + fromDollar; +}; + +Changeset.oldLen = function(cs) { + return Changeset.unpack(cs).oldLen; +}; +Changeset.newLen = function(cs) { + return Changeset.unpack(cs).newLen; +}; + +Changeset.opIterator = function(opsStr, optStartIndex) { + //print(opsStr); + var regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; + var startIndex = (optStartIndex || 0); + var curIndex = startIndex; + var prevIndex = curIndex; + function nextRegexMatch() { + prevIndex = curIndex; + var result; + if (_opt) { + result = _opt.nextOpInString(opsStr, curIndex); + if (result) { + if (result.opcode() == '?') { + Changeset.error("Hit error opcode in op stream"); + } + curIndex = result.lastIndex(); + } + } + else { + regex.lastIndex = curIndex; + result = regex.exec(opsStr); + curIndex = regex.lastIndex; + if (result[0] == '?') { + Changeset.error("Hit error opcode in op stream"); + } + } + return result; + } + var regexResult = nextRegexMatch(); + var obj = Changeset.newOp(); + function next(optObj) { + var op = (optObj || obj); + if (_opt && regexResult) { + op.attribs = regexResult.attribs(); + op.lines = regexResult.lines(); + op.chars = regexResult.chars(); + op.opcode = regexResult.opcode(); + regexResult = nextRegexMatch(); + } + else if ((! _opt) && regexResult[0]) { + op.attribs = regexResult[1]; + op.lines = Changeset.parseNum(regexResult[2] || 0); + op.opcode = regexResult[3]; + op.chars = Changeset.parseNum(regexResult[4]); + regexResult = nextRegexMatch(); + } + else { + Changeset.clearOp(op); + } + return op; + } + function hasNext() { return !! (_opt ? regexResult : regexResult[0]); } + function lastIndex() { return prevIndex; } + return {next: next, hasNext: hasNext, lastIndex: lastIndex}; +}; + +Changeset.clearOp = function(op) { + op.opcode = ''; + op.chars = 0; + op.lines = 0; + op.attribs = ''; +}; +Changeset.newOp = function(optOpcode) { + return {opcode:(optOpcode || ''), chars:0, lines:0, attribs:''}; +}; +Changeset.cloneOp = function(op) { + return {opcode: op.opcode, chars: op.chars, lines: op.lines, attribs: op.attribs}; +}; +Changeset.copyOp = function(op1, op2) { + op2.opcode = op1.opcode; + op2.chars = op1.chars; + op2.lines = op1.lines; + op2.attribs = op1.attribs; +}; +Changeset.opString = function(op) { + // just for debugging + if (! op.opcode) return 'null'; + var assem = Changeset.opAssembler(); + assem.append(op); + return assem.toString(); +}; +Changeset.stringOp = function(str) { + // just for debugging + return Changeset.opIterator(str).next(); +}; + +Changeset.checkRep = function(cs) { + // doesn't check things that require access to attrib pool (e.g. attribute order) + // or original string (e.g. newline positions) + var unpacked = Changeset.unpack(cs); + var oldLen = unpacked.oldLen; + var newLen = unpacked.newLen; + var ops = unpacked.ops; + var charBank = unpacked.charBank; + + var assem = Changeset.smartOpAssembler(); + var oldPos = 0; + var calcNewLen = 0; + var numInserted = 0; + var iter = Changeset.opIterator(ops); + while (iter.hasNext()) { + var o = iter.next(); + switch (o.opcode) { + case '=': oldPos += o.chars; calcNewLen += o.chars; break; + case '-': oldPos += o.chars; Changeset.assert(oldPos < oldLen, oldPos," >= ",oldLen," in ",cs); break; + case '+': { + calcNewLen += o.chars; numInserted += o.chars; + Changeset.assert(calcNewLen < newLen, calcNewLen," >= ",newLen," in ",cs); + break; + } + } + assem.append(o); + } + + calcNewLen += oldLen - oldPos; + charBank = charBank.substring(0, numInserted); + while (charBank.length < numInserted) { + charBank += "?"; + } + + assem.endDocument(); + var normalized = Changeset.pack(oldLen, calcNewLen, assem.toString(), charBank); + Changeset.assert(normalized == cs, normalized,' != ',cs); + + return cs; +} + +Changeset.smartOpAssembler = function() { + // Like opAssembler but able to produce conforming changesets + // from slightly looser input, at the cost of speed. + // Specifically: + // - merges consecutive operations that can be merged + // - strips final "=" + // - ignores 0-length changes + // - reorders consecutive + and - (which margingOpAssembler doesn't do) + + var minusAssem = Changeset.mergingOpAssembler(); + var plusAssem = Changeset.mergingOpAssembler(); + var keepAssem = Changeset.mergingOpAssembler(); + var assem = Changeset.stringAssembler(); + var lastOpcode = ''; + var lengthChange = 0; + + function flushKeeps() { + assem.append(keepAssem.toString()); + keepAssem.clear(); + } + + function flushPlusMinus() { + assem.append(minusAssem.toString()); + minusAssem.clear(); + assem.append(plusAssem.toString()); + plusAssem.clear(); + } + + function append(op) { + if (! op.opcode) return; + if (! op.chars) return; + + if (op.opcode == '-') { + if (lastOpcode == '=') { + flushKeeps(); + } + minusAssem.append(op); + lengthChange -= op.chars; + } + else if (op.opcode == '+') { + if (lastOpcode == '=') { + flushKeeps(); + } + plusAssem.append(op); + lengthChange += op.chars; + } + else if (op.opcode == '=') { + if (lastOpcode != '=') { + flushPlusMinus(); + } + keepAssem.append(op); + } + lastOpcode = op.opcode; + } + + function appendOpWithText(opcode, text, attribs, pool) { + var op = Changeset.newOp(opcode); + op.attribs = Changeset.makeAttribsString(opcode, attribs, pool); + var lastNewlinePos = text.lastIndexOf('\n'); + if (lastNewlinePos < 0) { + op.chars = text.length; + op.lines = 0; + append(op); + } + else { + op.chars = lastNewlinePos+1; + op.lines = text.match(/\n/g).length; + append(op); + op.chars = text.length - (lastNewlinePos+1); + op.lines = 0; + append(op); + } + } + + function toString() { + flushPlusMinus(); + flushKeeps(); + return assem.toString(); + } + + function clear() { + minusAssem.clear(); + plusAssem.clear(); + keepAssem.clear(); + assem.clear(); + lengthChange = 0; + } + + function endDocument() { + keepAssem.endDocument(); + } + + function getLengthChange() { + return lengthChange; + } + + return {append: append, toString: toString, clear: clear, endDocument: endDocument, + appendOpWithText: appendOpWithText, getLengthChange: getLengthChange }; +}; + +if (_opt) { + Changeset.mergingOpAssembler = function() { + var assem = _opt.mergingOpAssembler(); + + function append(op) { + assem.append(op.opcode, op.chars, op.lines, op.attribs); + } + function toString() { + return assem.toString(); + } + function clear() { + assem.clear(); + } + function endDocument() { + assem.endDocument(); + } + + return {append: append, toString: toString, clear: clear, endDocument: endDocument}; + }; +} +else { + Changeset.mergingOpAssembler = function() { + // This assembler can be used in production; it efficiently + // merges consecutive operations that are mergeable, ignores + // no-ops, and drops final pure "keeps". It does not re-order + // operations. + var assem = Changeset.opAssembler(); + var bufOp = Changeset.newOp(); + + // If we get, for example, insertions [xxx\n,yyy], those don't merge, + // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. + // This variable stores the length of yyy and any other newline-less + // ops immediately after it. + var bufOpAdditionalCharsAfterNewline = 0; + + function flush(isEndDocument) { + if (bufOp.opcode) { + if (isEndDocument && bufOp.opcode == '=' && ! bufOp.attribs) { + // final merged keep, leave it implicit + } + else { + assem.append(bufOp); + if (bufOpAdditionalCharsAfterNewline) { + bufOp.chars = bufOpAdditionalCharsAfterNewline; + bufOp.lines = 0; + assem.append(bufOp); + bufOpAdditionalCharsAfterNewline = 0; + } + } + bufOp.opcode = ''; + } + } + function append(op) { + if (op.chars > 0) { + if (bufOp.opcode == op.opcode && bufOp.attribs == op.attribs) { + if (op.lines > 0) { + // bufOp and additional chars are all mergeable into a multi-line op + bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; + bufOp.lines += op.lines; + bufOpAdditionalCharsAfterNewline = 0; + } + else if (bufOp.lines == 0) { + // both bufOp and op are in-line + bufOp.chars += op.chars; + } + else { + // append in-line text to multi-line bufOp + bufOpAdditionalCharsAfterNewline += op.chars; + } + } + else { + flush(); + Changeset.copyOp(op, bufOp); + } + } + } + function endDocument() { + flush(true); + } + function toString() { + flush(); + return assem.toString(); + } + function clear() { + assem.clear(); + Changeset.clearOp(bufOp); + } + return {append: append, toString: toString, clear: clear, endDocument: endDocument}; + }; +} + +if (_opt) { + Changeset.opAssembler = function() { + var assem = _opt.opAssembler(); + // this function allows op to be mutated later (doesn't keep a ref) + function append(op) { + assem.append(op.opcode, op.chars, op.lines, op.attribs); + } + function toString() { + return assem.toString(); + } + function clear() { + assem.clear(); + } + return {append: append, toString: toString, clear: clear}; + }; +} +else { + Changeset.opAssembler = function() { + var pieces = []; + // this function allows op to be mutated later (doesn't keep a ref) + function append(op) { + pieces.push(op.attribs); + if (op.lines) { + pieces.push('|', Changeset.numToString(op.lines)); + } + pieces.push(op.opcode); + pieces.push(Changeset.numToString(op.chars)); + } + function toString() { + return pieces.join(''); + } + function clear() { + pieces.length = 0; + } + return {append: append, toString: toString, clear: clear}; + }; +} + +Changeset.stringIterator = function(str) { + var curIndex = 0; + function assertRemaining(n) { + Changeset.assert(n <= remaining(), "!(",n," <= ",remaining(),")"); + } + function take(n) { + assertRemaining(n); + var s = str.substr(curIndex, n); + curIndex += n; + return s; + } + function peek(n) { + assertRemaining(n); + var s = str.substr(curIndex, n); + return s; + } + function skip(n) { + assertRemaining(n); + curIndex += n; + } + function remaining() { + return str.length - curIndex; + } + return {take:take, skip:skip, remaining:remaining, peek:peek}; +}; + +Changeset.stringAssembler = function() { + var pieces = []; + function append(x) { + pieces.push(String(x)); + } + function toString() { + return pieces.join(''); + } + return {append: append, toString: toString}; +}; + +// "lines" need not be an array as long as it supports certain calls (lines_foo inside). +Changeset.textLinesMutator = function(lines) { + // Mutates lines, an array of strings, in place. + // Mutation operations have the same constraints as changeset operations + // with respect to newlines, but not the other additional constraints + // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline). + // Can be used to mutate lists of strings where the last char of each string + // is not actually a newline, but for the purposes of N and L values, + // the caller should pretend it is, and for things to work right in that case, the input + // to insert() should be a single line with no newlines. + + var curSplice = [0,0]; + var inSplice = false; + // position in document after curSplice is applied: + var curLine = 0, curCol = 0; + // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && + // curLine >= curSplice[0] + // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then + // curCol == 0 + + function lines_applySplice(s) { + lines.splice.apply(lines, s); + } + function lines_toSource() { + return lines.toSource(); + } + function lines_get(idx) { + if (lines.get) { + return lines.get(idx); + } + else { + return lines[idx]; + } + } + // can be unimplemented if removeLines's return value not needed + function lines_slice(start, end) { + if (lines.slice) { + return lines.slice(start, end); + } + else { + return []; + } + } + function lines_length() { + if ((typeof lines.length) == "number") { + return lines.length; + } + else { + return lines.length(); + } + } + + function enterSplice() { + curSplice[0] = curLine; + curSplice[1] = 0; + if (curCol > 0) { + putCurLineInSplice(); + } + inSplice = true; + } + function leaveSplice() { + lines_applySplice(curSplice); + curSplice.length = 2; + curSplice[0] = curSplice[1] = 0; + inSplice = false; + } + function isCurLineInSplice() { + return (curLine - curSplice[0] < (curSplice.length - 2)); + } + function debugPrint(typ) { + print(typ+": "+curSplice.toSource()+" / "+curLine+","+curCol+" / "+lines_toSource()); + } + function putCurLineInSplice() { + if (! isCurLineInSplice()) { + curSplice.push(lines_get(curSplice[0] + curSplice[1])); + curSplice[1]++; + } + return 2 + curLine - curSplice[0]; + } + + function skipLines(L, includeInSplice) { + if (L) { + if (includeInSplice) { + if (! inSplice) { + enterSplice(); + } + for(var i=0;i 1) { + leaveSplice(); + } + else { + putCurLineInSplice(); + } + } + curLine += L; + curCol = 0; + } + //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); + /*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { + print("BLAH"); + putCurLineInSplice(); + }*/ // tests case foo in remove(), which isn't otherwise covered in current impl + } + //debugPrint("skip"); + } + + function skip(N, L, includeInSplice) { + if (N) { + if (L) { + skipLines(L, includeInSplice); + } + else { + if (includeInSplice && ! inSplice) { + enterSplice(); + } + if (inSplice) { + putCurLineInSplice(); + } + curCol += N; + //debugPrint("skip"); + } + } + } + + function removeLines(L) { + var removed = ''; + if (L) { + if (! inSplice) { + enterSplice(); + } + function nextKLinesText(k) { + var m = curSplice[0] + curSplice[1]; + return lines_slice(m, m+k).join(''); + } + if (isCurLineInSplice()) { + //print(curCol); + if (curCol == 0) { + removed = curSplice[curSplice.length-1]; + // print("FOO"); // case foo + curSplice.length--; + removed += nextKLinesText(L-1); + curSplice[1] += L-1; + } + else { + removed = nextKLinesText(L-1); + curSplice[1] += L-1; + var sline = curSplice.length - 1; + removed = curSplice[sline].substring(curCol) + removed; + curSplice[sline] = curSplice[sline].substring(0, curCol) + + lines_get(curSplice[0] + curSplice[1]); + curSplice[1] += 1; + } + } + else { + removed = nextKLinesText(L); + curSplice[1] += L; + } + //debugPrint("remove"); + } + return removed; + } + + function remove(N, L) { + var removed = ''; + if (N) { + if (L) { + return removeLines(L); + } + else { + if (! inSplice) { + enterSplice(); + } + var sline = putCurLineInSplice(); + removed = curSplice[sline].substring(curCol, curCol+N); + curSplice[sline] = curSplice[sline].substring(0, curCol) + + curSplice[sline].substring(curCol+N); + //debugPrint("remove"); + } + } + return removed; + } + + function insert(text, L) { + if (text) { + if (! inSplice) { + enterSplice(); + } + if (L) { + var newLines = Changeset.splitTextLines(text); + if (isCurLineInSplice()) { + //if (curCol == 0) { + //curSplice.length--; + //curSplice[1]--; + //Array.prototype.push.apply(curSplice, newLines); + //curLine += newLines.length; + //} + //else { + var sline = curSplice.length - 1; + var theLine = curSplice[sline]; + var lineCol = curCol; + curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; + curLine++; + newLines.splice(0, 1); + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + curSplice.push(theLine.substring(lineCol)); + curCol = 0; + //} + } + else { + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + } + } + else { + var sline = putCurLineInSplice(); + curSplice[sline] = curSplice[sline].substring(0, curCol) + + text + curSplice[sline].substring(curCol); + curCol += text.length; + } + //debugPrint("insert"); + } + } + + function hasMore() { + //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); + var docLines = lines_length(); + if (inSplice) { + docLines += curSplice.length - 2 - curSplice[1]; + } + return curLine < docLines; + } + + function close() { + if (inSplice) { + leaveSplice(); + } + //debugPrint("close"); + } + + var self = {skip:skip, remove:remove, insert:insert, close:close, hasMore:hasMore, + removeLines:removeLines, skipLines: skipLines}; + return self; +}; + +Changeset.applyZip = function(in1, idx1, in2, idx2, func) { + var iter1 = Changeset.opIterator(in1, idx1); + var iter2 = Changeset.opIterator(in2, idx2); + var assem = Changeset.smartOpAssembler(); + var op1 = Changeset.newOp(); + var op2 = Changeset.newOp(); + var opOut = Changeset.newOp(); + while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) { + if ((! op1.opcode) && iter1.hasNext()) iter1.next(op1); + if ((! op2.opcode) && iter2.hasNext()) iter2.next(op2); + func(op1, op2, opOut); + if (opOut.opcode) { + //print(opOut.toSource()); + assem.append(opOut); + opOut.opcode = ''; + } + } + assem.endDocument(); + return assem.toString(); +}; + +Changeset.unpack = function(cs) { + var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; + var headerMatch = headerRegex.exec(cs); + if ((! headerMatch) || (! headerMatch[0])) { + Changeset.error("Not a changeset: "+cs); + } + var oldLen = Changeset.parseNum(headerMatch[1]); + var changeSign = (headerMatch[2] == '>') ? 1 : -1; + var changeMag = Changeset.parseNum(headerMatch[3]); + var newLen = oldLen + changeSign*changeMag; + var opsStart = headerMatch[0].length; + var opsEnd = cs.indexOf("$"); + if (opsEnd < 0) opsEnd = cs.length; + return {oldLen: oldLen, newLen: newLen, ops: cs.substring(opsStart, opsEnd), + charBank: cs.substring(opsEnd+1)}; +}; + +Changeset.pack = function(oldLen, newLen, opsStr, bank) { + var lenDiff = newLen - oldLen; + var lenDiffStr = (lenDiff >= 0 ? + '>'+Changeset.numToString(lenDiff) : + '<'+Changeset.numToString(-lenDiff)); + var a = []; + a.push('Z:', Changeset.numToString(oldLen), lenDiffStr, opsStr, '$', bank); + return a.join(''); +}; + +Changeset.applyToText = function(cs, str) { + var unpacked = Changeset.unpack(cs); + Changeset.assert(str.length == unpacked.oldLen, + "mismatched apply: ",str.length," / ",unpacked.oldLen); + var csIter = Changeset.opIterator(unpacked.ops); + var bankIter = Changeset.stringIterator(unpacked.charBank); + var strIter = Changeset.stringIterator(str); + var assem = Changeset.stringAssembler(); + while (csIter.hasNext()) { + var op = csIter.next(); + switch(op.opcode) { + case '+': assem.append(bankIter.take(op.chars)); break; + case '-': strIter.skip(op.chars); break; + case '=': assem.append(strIter.take(op.chars)); break; + } + } + assem.append(strIter.take(strIter.remaining())); + return assem.toString(); +}; + +Changeset.mutateTextLines = function(cs, lines) { + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var bankIter = Changeset.stringIterator(unpacked.charBank); + var mut = Changeset.textLinesMutator(lines); + while (csIter.hasNext()) { + var op = csIter.next(); + switch(op.opcode) { + case '+': mut.insert(bankIter.take(op.chars), op.lines); break; + case '-': mut.remove(op.chars, op.lines); break; + case '=': mut.skip(op.chars, op.lines, (!! op.attribs)); break; + } + } + mut.close(); +}; + +Changeset.composeAttributes = function(att1, att2, resultIsMutation, pool) { + // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. + + // Sometimes attribute (key,value) pairs are treated as attribute presence + // information, while other times they are treated as operations that + // mutate a set of attributes, and this affects whether an empty value + // is a deletion or a change. + // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result + // ([], [(bold, )], true) -> [(bold, )] + // ([], [(bold, )], false) -> [] + // ([], [(bold, true)], true) -> [(bold, true)] + // ([], [(bold, true)], false) -> [(bold, true)] + // ([(bold, true)], [(bold, )], true) -> [(bold, )] + // ([(bold, true)], [(bold, )], false) -> [] + + // pool can be null if att2 has no attributes. + + if ((! att1) && resultIsMutation) { + // In the case of a mutation (i.e. composing two changesets), + // an att2 composed with an empy att1 is just att2. If att1 + // is part of an attribution string, then att2 may remove + // attributes that are already gone, so don't do this optimization. + return att2; + } + if (! att2) return att1; + var atts = []; + att1.replace(/\*([0-9a-z]+)/g, function(_, a) { + atts.push(pool.getAttrib(Changeset.parseNum(a))); + return ''; + }); + att2.replace(/\*([0-9a-z]+)/g, function(_, a) { + var pair = pool.getAttrib(Changeset.parseNum(a)); + var found = false; + for(var i=0;i"); + + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var csBank = unpacked.charBank; + var csBankIndex = 0; + // treat the attribution lines as text lines, mutating a line at a time + var mut = Changeset.textLinesMutator(lines); + + var lineIter = null; + function isNextMutOp() { + return (lineIter && lineIter.hasNext()) || mut.hasMore(); + } + function nextMutOp(destOp) { + if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { + var line = mut.removeLines(1); + lineIter = Changeset.opIterator(line); + } + if (lineIter && lineIter.hasNext()) { + lineIter.next(destOp); + } + else { + destOp.opcode = ''; + } + } + var lineAssem = null; + function outputMutOp(op) { + //print("outputMutOp: "+op.toSource()); + if (! lineAssem) { + lineAssem = Changeset.mergingOpAssembler(); + } + lineAssem.append(op); + if (op.lines > 0) { + Changeset.assert(op.lines == 1, "Can't have op.lines of ",op.lines," in attribution lines"); + // ship it to the mut + mut.insert(lineAssem.toString(), 1); + lineAssem = null; + } + } + + var csOp = Changeset.newOp(); + var attOp = Changeset.newOp(); + var opOut = Changeset.newOp(); + while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { + if ((! csOp.opcode) && csIter.hasNext()) { + csIter.next(csOp); + } + //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); + //print("csOp: "+csOp.toSource()); + if ((! csOp.opcode) && (! attOp.opcode) && + (! lineAssem) && (! (lineIter && lineIter.hasNext()))) { + break; // done + } + else if (csOp.opcode == '=' && csOp.lines > 0 && (! csOp.attribs) && (! attOp.opcode) && + (! lineAssem) && (! (lineIter && lineIter.hasNext()))) { + // skip multiple lines; this is what makes small changes not order of the document size + mut.skipLines(csOp.lines); + //print("skipped: "+csOp.lines); + csOp.opcode = ''; + } + else if (csOp.opcode == '+') { + if (csOp.lines > 1) { + var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; + Changeset.copyOp(csOp, opOut); + csOp.chars -= firstLineLen; + csOp.lines--; + opOut.lines = 1; + opOut.chars = firstLineLen; + } + else { + Changeset.copyOp(csOp, opOut); + csOp.opcode = ''; + } + outputMutOp(opOut); + csBankIndex += opOut.chars; + opOut.opcode = ''; + } + else { + if ((! attOp.opcode) && isNextMutOp()) { + nextMutOp(attOp); + } + //print("attOp: "+attOp.toSource()); + Changeset._slicerZipperFunc(attOp, csOp, opOut, pool); + if (opOut.opcode) { + outputMutOp(opOut); + opOut.opcode = ''; + } + } + } + + Changeset.assert(! lineAssem, "line assembler not finished"); + mut.close(); + + //dmesg("-> "+lines.toSource()); +}; + +Changeset.joinAttributionLines = function(theAlines) { + var assem = Changeset.mergingOpAssembler(); + for(var i=0;i 0) { + lines.push(assem.toString()); + assem.clear(); + } + pos += op.chars; + } + + while (iter.hasNext()) { + var op = iter.next(); + var numChars = op.chars; + var numLines = op.lines; + while (numLines > 1) { + var newlineEnd = text.indexOf('\n', pos)+1; + Changeset.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines"); + op.chars = newlineEnd - pos; + op.lines = 1; + appendOp(op); + numChars -= op.chars; + numLines -= op.lines; + } + if (numLines == 1) { + op.chars = numChars; + op.lines = 1; + } + appendOp(op); + } + + return lines; +}; + +Changeset.splitTextLines = function(text) { + return text.match(/[^\n]*(?:\n|[^\n]$)/g); +}; + +Changeset.compose = function(cs1, cs2, pool) { + var unpacked1 = Changeset.unpack(cs1); + var unpacked2 = Changeset.unpack(cs2); + var len1 = unpacked1.oldLen; + var len2 = unpacked1.newLen; + Changeset.assert(len2 == unpacked2.oldLen, "mismatched composition"); + var len3 = unpacked2.newLen; + var bankIter1 = Changeset.stringIterator(unpacked1.charBank); + var bankIter2 = Changeset.stringIterator(unpacked2.charBank); + var bankAssem = Changeset.stringAssembler(); + + var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function(op1, op2, opOut) { + //var debugBuilder = Changeset.stringAssembler(); + //debugBuilder.append(Changeset.opString(op1)); + //debugBuilder.append(','); + //debugBuilder.append(Changeset.opString(op2)); + //debugBuilder.append(' / '); + + var op1code = op1.opcode; + var op2code = op2.opcode; + if (op1code == '+' && op2code == '-') { + bankIter1.skip(Math.min(op1.chars, op2.chars)); + } + Changeset._slicerZipperFunc(op1, op2, opOut, pool); + if (opOut.opcode == '+') { + if (op2code == '+') { + bankAssem.append(bankIter2.take(opOut.chars)); + } + else { + bankAssem.append(bankIter1.take(opOut.chars)); + } + } + + //debugBuilder.append(Changeset.opString(op1)); + //debugBuilder.append(','); + //debugBuilder.append(Changeset.opString(op2)); + //debugBuilder.append(' -> '); + //debugBuilder.append(Changeset.opString(opOut)); + //print(debugBuilder.toString()); + }); + + return Changeset.pack(len1, len3, newOps, bankAssem.toString()); +}; + +Changeset.attributeTester = function(attribPair, pool) { + // returns a function that tests if a string of attributes + // (e.g. *3*4) contains a given attribute key,value that + // is already present in the pool. + if (! pool) { + return never; + } + var attribNum = pool.putAttrib(attribPair, true); + if (attribNum < 0) { + return never; + } + else { + var re = new RegExp('\\*'+Changeset.numToString(attribNum)+ + '(?!\\w)'); + return function(attribs) { + return re.test(attribs); + }; + } + function never(attribs) { return false; } +}; + +Changeset.identity = function(N) { + return Changeset.pack(N, N, "", ""); +}; + +Changeset.makeSplice = function(oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) { + var oldLen = oldFullText.length; + + if (spliceStart >= oldLen) { + spliceStart = oldLen - 1; + } + if (numRemoved > oldFullText.length - spliceStart - 1) { + numRemoved = oldFullText.length - spliceStart - 1; + } + var oldText = oldFullText.substring(spliceStart, spliceStart+numRemoved); + var newLen = oldLen + newText.length - oldText.length; + + var assem = Changeset.smartOpAssembler(); + assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); + assem.appendOpWithText('-', oldText); + assem.appendOpWithText('+', newText, optNewTextAPairs, pool); + assem.endDocument(); + return Changeset.pack(oldLen, newLen, assem.toString(), newText); +}; + +Changeset.toSplices = function(cs) { + // get a list of splices, [startChar, endChar, newText] + + var unpacked = Changeset.unpack(cs); + var splices = []; + + var oldPos = 0; + var iter = Changeset.opIterator(unpacked.ops); + var charIter = Changeset.stringIterator(unpacked.charBank); + var inSplice = false; + while (iter.hasNext()) { + var op = iter.next(); + if (op.opcode == '=') { + oldPos += op.chars; + inSplice = false; + } + else { + if (! inSplice) { + splices.push([oldPos, oldPos, ""]); + inSplice = true; + } + if (op.opcode == '-') { + oldPos += op.chars; + splices[splices.length-1][1] += op.chars; + } + else if (op.opcode == '+') { + splices[splices.length-1][2] += charIter.take(op.chars); + } + } + } + + return splices; +}; + +Changeset.characterRangeFollow = function(cs, startChar, endChar, insertionsAfter) { + var newStartChar = startChar; + var newEndChar = endChar; + var splices = Changeset.toSplices(cs); + var lengthChangeSoFar = 0; + for(var i=0;i= newEndChar) { + // splice fully replaces/deletes range + // (also case that handles insertion at a collapsed selection) + if (insertionsAfter) { + newStartChar = newEndChar = spliceStart; + } + else { + newStartChar = newEndChar = spliceStart + newTextLength; + } + } + else if (spliceEnd <= newStartChar) { + // splice is before range + newStartChar += thisLengthChange; + newEndChar += thisLengthChange; + } + else if (spliceStart >= newEndChar) { + // splice is after range + } + else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) { + // splice is inside range + newEndChar += thisLengthChange; + } + else if (spliceEnd < newEndChar) { + // splice overlaps beginning of range + newStartChar = spliceStart + newTextLength; + newEndChar += thisLengthChange; + } + else { + // splice overlaps end of range + newEndChar = spliceStart; + } + + lengthChangeSoFar += thisLengthChange; + } + + return [newStartChar, newEndChar]; +}; + +Changeset.moveOpsToNewPool = function(cs, oldPool, newPool) { + // works on changeset or attribution string + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + var fromDollar = cs.substring(dollarPos); + // order of attribs stays the same + return upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) { + var oldNum = Changeset.parseNum(a); + var pair = oldPool.getAttrib(oldNum); + var newNum = newPool.putAttrib(pair); + return '*'+Changeset.numToString(newNum); + }) + fromDollar; +}; + +Changeset.makeAttribution = function(text) { + var assem = Changeset.smartOpAssembler(); + assem.appendOpWithText('+', text); + return assem.toString(); +}; + +// callable on a changeset, attribution string, or attribs property of an op +Changeset.eachAttribNumber = function(cs, func) { + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + + upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) { + func(Changeset.parseNum(a)); + return ''; + }); +}; + +// callable on a changeset, attribution string, or attribs property of an op, +// though it may easily create adjacent ops that can be merged. +Changeset.filterAttribNumbers = function(cs, filter) { + return Changeset.mapAttribNumbers(cs, filter); +}; + +Changeset.mapAttribNumbers = function(cs, func) { + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + + var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function(s, a) { + var n = func(Changeset.parseNum(a)); + if (n === true) { + return s; + } + else if ((typeof n) === "number") { + return '*'+Changeset.numToString(n); + } + else { + return ''; + } + }); + + return newUpToDollar + cs.substring(dollarPos); +}; + +Changeset.makeAText = function(text, attribs) { + return { text: text, attribs: (attribs || Changeset.makeAttribution(text)) }; +}; + +Changeset.applyToAText = function(cs, atext, pool) { + return { text: Changeset.applyToText(cs, atext.text), + attribs: Changeset.applyToAttribution(cs, atext.attribs, pool) }; +}; + +Changeset.cloneAText = function(atext) { + return { text: atext.text, attribs: atext.attribs }; +}; + +Changeset.copyAText = function(atext1, atext2) { + atext2.text = atext1.text; + atext2.attribs = atext1.attribs; +}; + +Changeset.appendATextToAssembler = function(atext, assem) { + // intentionally skips last newline char of atext + var iter = Changeset.opIterator(atext.attribs); + var op = Changeset.newOp(); + while (iter.hasNext()) { + iter.next(op); + if (! iter.hasNext()) { + // last op, exclude final newline + if (op.lines <= 1) { + op.lines = 0; + op.chars--; + if (op.chars) { + assem.append(op); + } + } + else { + var nextToLastNewlineEnd = + atext.text.lastIndexOf('\n', atext.text.length-2) + 1; + var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; + op.lines--; + op.chars -= (lastLineLength + 1); + assem.append(op); + op.lines = 0; + op.chars = lastLineLength; + if (op.chars) { + assem.append(op); + } + } + } + else { + assem.append(op); + } + } +}; + +Changeset.prepareForWire = function(cs, pool) { + var newPool = new AttribPool(); + var newCs = Changeset.moveOpsToNewPool(cs, pool, newPool); + return {translated: newCs, pool: newPool}; +}; + +Changeset.isIdentity = function(cs) { + var unpacked = Changeset.unpack(cs); + return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; +}; + +Changeset.opAttributeValue = function(op, key, pool) { + return Changeset.attribsAttributeValue(op.attribs, key, pool); +}; + +Changeset.attribsAttributeValue = function(attribs, key, pool) { + var value = ''; + if (attribs) { + Changeset.eachAttribNumber(attribs, function(n) { + if (pool.getAttribKey(n) == key) { + value = pool.getAttribValue(n); + } + }); + } + return value; +}; + +Changeset.builder = function(oldLen) { + var assem = Changeset.smartOpAssembler(); + var o = Changeset.newOp(); + var charBank = Changeset.stringAssembler(); + + var self = { + // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) + keep: function(N, L, attribs, pool) { + o.opcode = '='; + o.attribs = (attribs && + Changeset.makeAttribsString('=', attribs, pool)) || ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + keepText: function(text, attribs, pool) { + assem.appendOpWithText('=', text, attribs, pool); + return self; + }, + insert: function(text, attribs, pool) { + assem.appendOpWithText('+', text, attribs, pool); + charBank.append(text); + return self; + }, + remove: function(N, L) { + o.opcode = '-'; + o.attribs = ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + toString: function() { + assem.endDocument(); + var newLen = oldLen + assem.getLengthChange(); + return Changeset.pack(oldLen, newLen, assem.toString(), + charBank.toString()); + } + }; + + return self; +}; + +Changeset.makeAttribsString = function(opcode, attribs, pool) { + // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work + if (! attribs) { + return ''; + } + else if ((typeof attribs) == "string") { + return attribs; + } + else if (pool && attribs && attribs.length) { + if (attribs.length > 1) { + attribs = attribs.slice(); + attribs.sort(); + } + var result = []; + for(var i=0;i= attOp.chars && + attOp.lines > 0 && csOp.lines <= 0) { + csOp.lines++; + } + + Changeset._slicerZipperFunc(attOp, csOp, opOut, null); + if (opOut.opcode) { + assem.append(opOut); + opOut.opcode = ''; + } + } + } + } + + csOp.opcode = '-'; + csOp.chars = start; + + doCsOp(); + + if (optEnd === undefined) { + if (attOp.opcode) { + assem.append(attOp); + } + while (iter.hasNext()) { + iter.next(attOp); + assem.append(attOp); + } + } + else { + csOp.opcode = '='; + csOp.chars = optEnd - start; + doCsOp(); + } + + return assem.toString(); +}; + +Changeset.inverse = function(cs, lines, alines, pool) { + // lines and alines are what the changeset is meant to apply to. + // They may be arrays or objects with .get(i) and .length methods. + // They include final newlines on lines. + function lines_get(idx) { + if (lines.get) { + return lines.get(idx); + } + else { + return lines[idx]; + } + } + function lines_length() { + if ((typeof lines.length) == "number") { + return lines.length; + } + else { + return lines.length(); + } + } + function alines_get(idx) { + if (alines.get) { + return alines.get(idx); + } + else { + return alines[idx]; + } + } + function alines_length() { + if ((typeof alines.length) == "number") { + return alines.length; + } + else { + return alines.length(); + } + } + + var curLine = 0; + var curChar = 0; + var curLineOpIter = null; + var curLineOpIterLine; + var curLineNextOp = Changeset.newOp('+'); + + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var builder = Changeset.builder(unpacked.newLen); + + function consumeAttribRuns(numChars, func/*(len, attribs, endsLine)*/) { + + if ((! curLineOpIter) || (curLineOpIterLine != curLine)) { + // create curLineOpIter and advance it to curChar + curLineOpIter = Changeset.opIterator(alines_get(curLine)); + curLineOpIterLine = curLine; + var indexIntoLine = 0; + var done = false; + while (! done) { + curLineOpIter.next(curLineNextOp); + if (indexIntoLine + curLineNextOp.chars >= curChar) { + curLineNextOp.chars -= (curChar - indexIntoLine); + done = true; + } + else { + indexIntoLine += curLineNextOp.chars; + } + } + } + + while (numChars > 0) { + if ((! curLineNextOp.chars) && (! curLineOpIter.hasNext())) { + curLine++; + curChar = 0; + curLineOpIterLine = curLine; + curLineNextOp.chars = 0; + curLineOpIter = Changeset.opIterator(alines_get(curLine)); + } + if (! curLineNextOp.chars) { + curLineOpIter.next(curLineNextOp); + } + var charsToUse = Math.min(numChars, curLineNextOp.chars); + func(charsToUse, curLineNextOp.attribs, + charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); + numChars -= charsToUse; + curLineNextOp.chars -= charsToUse; + curChar += charsToUse; + } + + if ((! curLineNextOp.chars) && (! curLineOpIter.hasNext())) { + curLine++; + curChar = 0; + } + } + + function skip(N, L) { + if (L) { + curLine += L; + curChar = 0; + } + else { + if (curLineOpIter && curLineOpIterLine == curLine) { + consumeAttribRuns(N, function() {}); + } + else { + curChar += N; + } + } + } + + function nextText(numChars) { + var len = 0; + var assem = Changeset.stringAssembler(); + var firstString = lines_get(curLine).substring(curChar); + len += firstString.length; + assem.append(firstString); + + var lineNum = curLine+1; + while (len < numChars) { + var nextString = lines_get(lineNum); + len += nextString.length; + assem.append(nextString); + lineNum++; + } + + return assem.toString().substring(0, numChars); + } + + function cachedStrFunc(func) { + var cache = {}; + return function(s) { + if (! cache[s]) { + cache[s] = func(s); + } + return cache[s]; + }; + } + + var attribKeys = []; + var attribValues = []; + while (csIter.hasNext()) { + var csOp = csIter.next(); + if (csOp.opcode == '=') { + if (csOp.attribs) { + attribKeys.length = 0; + attribValues.length = 0; + Changeset.eachAttribNumber(csOp.attribs, function(n) { + attribKeys.push(pool.getAttribKey(n)); + attribValues.push(pool.getAttribValue(n)); + }); + var undoBackToAttribs = cachedStrFunc(function(attribs) { + var backAttribs = []; + for(var i=0;i throughIterator"); + var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; + assert("throughIterator("+literal(x)+") == "+literal(x)); + })(); + + (function() { + print("> throughSmartAssembler"); + var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; + assert("throughSmartAssembler("+literal(x)+") == "+literal(x)); + })(); + + function applyMutations(mu, arrayOfArrays) { + arrayOfArrays.forEach(function (a) { + var result = mu[a[0]].apply(mu, a.slice(1)); + if (a[0] == 'remove' && a[3]) { + assertEqualStrings(a[3], result); + } + }); + } + + function mutationsToChangeset(oldLen, arrayOfArrays) { + var assem = Changeset.smartOpAssembler(); + var op = Changeset.newOp(); + var bank = Changeset.stringAssembler(); + var oldPos = 0; + var newLen = 0; + arrayOfArrays.forEach(function (a) { + if (a[0] == 'skip') { + op.opcode = '='; + op.chars = a[1]; + op.lines = (a[2] || 0); + assem.append(op); + oldPos += op.chars; + newLen += op.chars; + } + else if (a[0] == 'remove') { + op.opcode = '-'; + op.chars = a[1]; + op.lines = (a[2] || 0); + assem.append(op); + oldPos += op.chars; + } + else if (a[0] == 'insert') { + op.opcode = '+'; + bank.append(a[1]); + op.chars = a[1].length; + op.lines = (a[2] || 0); + assem.append(op); + newLen += op.chars; + } + }); + newLen += oldLen - oldPos; + assem.endDocument(); + return Changeset.pack(oldLen, newLen, assem.toString(), + bank.toString()); + } + + function runMutationTest(testId, origLines, muts, correct) { + print("> runMutationTest#"+testId); + var lines = origLines.slice(); + var mu = Changeset.textLinesMutator(lines); + applyMutations(mu, muts); + mu.close(); + assertEqualArrays(correct, lines); + + var inText = origLines.join(''); + var cs = mutationsToChangeset(inText.length, muts); + lines = origLines.slice(); + Changeset.mutateTextLines(cs, lines); + assertEqualArrays(correct, lines); + + var correctText = correct.join(''); + //print(literal(cs)); + var outText = Changeset.applyToText(cs, inText); + assertEqualStrings(correctText, outText); + } + + runMutationTest(1, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], + [['remove',1,0,"a"],['insert',"tu"],['remove',1,0,"p"],['skip',4,1],['skip',7,1], + ['insert',"cream\npie\n",2],['skip',2],['insert',"bot"],['insert',"\n",1], + ['insert',"bu"],['skip',3],['remove',3,1,"ge\n"],['remove',6,0,"duffle"]], + ["tuple\n","banana\n","cream\n","pie\n", "cabot\n","bubba\n","eggplant\n"]); + + runMutationTest(2, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], + [['remove',1,0,"a"],['remove',1,0,"p"],['insert',"tu"],['skip',11,2], + ['insert',"cream\npie\n",2],['skip',2],['insert',"bot"],['insert',"\n",1], + ['insert',"bu"],['skip',3],['remove',3,1,"ge\n"],['remove',6,0,"duffle"]], + ["tuple\n","banana\n","cream\n","pie\n", "cabot\n","bubba\n","eggplant\n"]); + + runMutationTest(3, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], + [['remove',6,1,"apple\n"],['skip',15,2],['skip',6],['remove',1,1,"\n"], + ['remove',8,0,"eggplant"],['skip',1,1]], + ["banana\n","cabbage\n","duffle\n"]); + + runMutationTest(4, ["15\n"], + [['skip',1],['insert',"\n2\n3\n4\n",4],['skip',2,1]], + ["1\n","2\n","3\n","4\n","5\n"]); + + runMutationTest(5, ["1\n","2\n","3\n","4\n","5\n"], + [['skip',1],['remove',7,4,"\n2\n3\n4\n"],['skip',2,1]], + ["15\n"]); + + runMutationTest(6, ["123\n","abc\n","def\n","ghi\n","xyz\n"], + [['insert',"0"],['skip',4,1],['skip',4,1],['remove',8,2,"def\nghi\n"],['skip',4,1]], + ["0123\n", "abc\n", "xyz\n"]); + + runMutationTest(7, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], + [['remove',6,1,"apple\n"],['skip',15,2,true],['skip',6,0,true],['remove',1,1,"\n"], + ['remove',8,0,"eggplant"],['skip',1,1,true]], + ["banana\n","cabbage\n","duffle\n"]); + + function poolOrArray(attribs) { + if (attribs.getAttrib) { + return attribs; // it's already an attrib pool + } + else { + // assume it's an array of attrib strings to be split and added + var p = new AttribPool(); + attribs.forEach(function (kv) { p.putAttrib(kv.split(',')); }); + return p; + } + } + + function runApplyToAttributionTest(testId, attribs, cs, inAttr, outCorrect) { + print("> applyToAttribution#"+testId); + var p = poolOrArray(attribs); + var result = Changeset.applyToAttribution( + Changeset.checkRep(cs), inAttr, p); + assertEqualStrings(outCorrect, result); + } + + // turn cactus\n into actusabcd\n + runApplyToAttributionTest(1, ['bold,', 'bold,true'], + "Z:7>3-1*0=1*1=1=3+4$abcd", + "+1*1+1|1+5", "+1*1+1|1+8"); + + // turn "david\ngreenspan\n" into "david\ngreen\n" + runApplyToAttributionTest(2, ['bold,', 'bold,true'], + "Z:g<4*1|1=6*1=5-4$", + "|2+g", "*1|1+6*1+5|1+1"); + + (function() { + print("> mutatorHasMore"); + var lines = ["1\n", "2\n", "3\n", "4\n"]; + var mu; + + mu = Changeset.textLinesMutator(lines); + assert(mu.hasMore()+' == true'); + mu.skip(8,4); + assert(mu.hasMore()+' == false'); + mu.close(); + assert(mu.hasMore()+' == false'); + + // still 1,2,3,4 + mu = Changeset.textLinesMutator(lines); + assert(mu.hasMore()+' == true'); + mu.remove(2,1); + assert(mu.hasMore()+' == true'); + mu.skip(2,1); + assert(mu.hasMore()+' == true'); + mu.skip(2,1); + assert(mu.hasMore()+' == true'); + mu.skip(2,1); + assert(mu.hasMore()+' == false'); + mu.insert("5\n", 1); + assert(mu.hasMore()+' == false'); + mu.close(); + assert(mu.hasMore()+' == false'); + + // 2,3,4,5 now + mu = Changeset.textLinesMutator(lines); + assert(mu.hasMore()+' == true'); + mu.remove(6,3); + assert(mu.hasMore()+' == true'); + mu.remove(2,1); + assert(mu.hasMore()+' == false'); + mu.insert("hello\n", 1); + assert(mu.hasMore()+' == false'); + mu.close(); + assert(mu.hasMore()+' == false'); + + })(); + + function runMutateAttributionTest(testId, attribs, cs, alines, outCorrect) { + print("> runMutateAttributionTest#"+testId); + var p = poolOrArray(attribs); + var alines2 = Array.prototype.slice.call(alines); + var result = Changeset.mutateAttributionLines( + Changeset.checkRep(cs), alines2, p); + assertEqualArrays(outCorrect, alines2); + + print("> runMutateAttributionTest#"+testId+".applyToAttribution"); + function removeQuestionMarks(a) { return a.replace(/\?/g, ''); } + var inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks)); + var correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks)); + var mergedResult = Changeset.applyToAttribution(cs, inMerged, p); + assertEqualStrings(correctMerged, mergedResult); + } + + // turn 123\n 456\n 789\n into 123\n 456\n 789\n + runMutateAttributionTest(1, ["bold,true"], "Z:c>0|1=4=1*0=1$", ["|1+4", "|1+4", "|1+4"], + ["|1+4", "+1*0+1|1+2", "|1+4"]); + + // make a document bold + runMutateAttributionTest(2, ["bold,true"], "Z:c>0*0|3=c$", ["|1+4", "|1+4", "|1+4"], + ["*0|1+4", "*0|1+4", "*0|1+4"]); + + // clear bold on document + runMutateAttributionTest(3, ["bold,","bold,true"], "Z:c>0*0|3=c$", + ["*1+1+1*1+1|1+1", "+1*1+1|1+2", "*1+1+1*1+1|1+1"], + ["|1+4", "|1+4", "|1+4"]); + + // add a character on line 3 of a document with 5 blank lines, and make sure + // the optimization that skips purely-kept lines is working; if any attribution string + // with a '?' is parsed it will cause an error. + runMutateAttributionTest(4, ['foo,bar','line,1','line,2','line,3','line,4','line,5'], + "Z:5>1|2=2+1$x", + ["?*1|1+1", "?*2|1+1", "*3|1+1", "?*4|1+1", "?*5|1+1"], + ["?*1|1+1", "?*2|1+1", "+1*3|1+1", "?*4|1+1", "?*5|1+1"]); + + var testPoolWithChars = (function() { + var p = new AttribPool(); + p.putAttrib(['char','newline']); + for(var i=1;i<36;i++) { + p.putAttrib(['char',Changeset.numToString(i)]); + } + p.putAttrib(['char','']); + return p; + })(); + + // based on runMutationTest#1 + runMutateAttributionTest(5, testPoolWithChars, + "Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$"+ + "tucream\npie\nbot\nbu", + ["*a+1*p+2*l+1*e+1*0|1+1", + "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", + "*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1", + "*d+1*u+1*f+2*l+1*e+1*0|1+1", + "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"], + ["*t+1*u+1*p+1*l+1*e+1*0|1+1", + "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", + "|1+6", + "|1+4", + "*c+1*a+1*b+1*o+1*t+1*0|1+1", + "*b+1*u+1*b+2*a+1*0|1+1", + "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"]); + + // based on runMutationTest#3 + runMutateAttributionTest(6, testPoolWithChars, + "Z:117=1|4+7$\n2\n3\n4\n", + ["*1+1*5|1+2"], + ["*1+1|1+1","|1+2","|1+2","|1+2","*5|1+2"]); + + // based on runMutationTest#5 + runMutateAttributionTest(8, testPoolWithChars, + "Z:a<7=1|4-7$", + ["*1|1+2","*2|1+2","*3|1+2","*4|1+2","*5|1+2"], + ["*1+1*5|1+2"]); + + // based on runMutationTest#6 + runMutateAttributionTest(9, testPoolWithChars, + "Z:k<7*0+1*10|2=8|2-8$0", + ["*1+1*2+1*3+1|1+1","*a+1*b+1*c+1|1+1", + "*d+1*e+1*f+1|1+1","*g+1*h+1*i+1|1+1","?*x+1*y+1*z+1|1+1"], + ["*0+1|1+4", "|1+4", "?*x+1*y+1*z+1|1+1"]); + + runMutateAttributionTest(10, testPoolWithChars, + "Z:6>4=1+1=1+1|1=1+1=1*0+1$abcd", + ["|1+3", "|1+3"], + ["|1+5", "+2*0+1|1+2"]); + + + runMutateAttributionTest(11, testPoolWithChars, + "Z:s>1|1=4=6|1+1$\n", + ["*0|1+4", "*0|1+8", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"], + ["*0|1+4", "*0+6|1+1", "*0|1+2", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"]); + + function randomInlineString(len, rand) { + var assem = Changeset.stringAssembler(); + for(var i=0;i 1) doOp(); + for(var i=0;i<5;i++) doOp(); // do some more (only insertions will happen) + + var outText = outTextAssem.toString()+'\n'; + opAssem.endDocument(); + var cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); + Changeset.checkRep(cs); + return [cs, outText]; + } + + function testCompose(randomSeed) { + var rand = new java.util.Random(randomSeed); + print("> testCompose#"+randomSeed); + + var p = new AttribPool(); + + var startText = randomMultiline(10, 20, rand)+'\n'; + + var x1 = randomTestChangeset(startText, rand); + var change1 = x1[0]; + var text1 = x1[1]; + + var x2 = randomTestChangeset(text1, rand); + var change2 = x2[0]; + var text2 = x2[1]; + + var x3 = randomTestChangeset(text2, rand); + var change3 = x3[0]; + var text3 = x3[1]; + + //print(literal(Changeset.toBaseTen(startText))); + //print(literal(Changeset.toBaseTen(change1))); + //print(literal(Changeset.toBaseTen(change2))); + var change12 = Changeset.checkRep(Changeset.compose(change1, change2, p)); + var change23 = Changeset.checkRep(Changeset.compose(change2, change3, p)); + var change123 = Changeset.checkRep(Changeset.compose(change12, change3, p)); + var change123a = Changeset.checkRep(Changeset.compose(change1, change23, p)); + assertEqualStrings(change123, change123a); + + assertEqualStrings(text2, Changeset.applyToText(change12, startText)); + assertEqualStrings(text3, Changeset.applyToText(change23, text1)); + assertEqualStrings(text3, Changeset.applyToText(change123, startText)); + } + + for(var i=0;i<30;i++) testCompose(i); + + (function simpleComposeAttributesTest() { + print("> simpleComposeAttributesTest"); + var p = new AttribPool(); + p.putAttrib(['bold','']); + p.putAttrib(['bold','true']); + var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x"); + var cs2 = Changeset.checkRep("Z:3>0*0|1=3$"); + var cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p)); + assertEqualStrings("Z:2>1+1*0|1=2$x", cs12); + })(); + + (function followAttributesTest() { + var p = new AttribPool(); + p.putAttrib(['x','']); + p.putAttrib(['x','abc']); + p.putAttrib(['x','def']); + p.putAttrib(['y','']); + p.putAttrib(['y','abc']); + p.putAttrib(['y','def']); + + function testFollow(a, b, afb, bfa, merge) { + assertEqualStrings(afb, Changeset.followAttributes(a, b, p)); + assertEqualStrings(bfa, Changeset.followAttributes(b, a, p)); + assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p)); + assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p)); + } + + testFollow('', '', '', '', ''); + testFollow('*0', '', '', '*0', '*0'); + testFollow('*0', '*0', '', '', '*0'); + testFollow('*0', '*1', '', '*0', '*0'); + testFollow('*1', '*2', '', '*1', '*1'); + testFollow('*0*1', '', '', '*0*1', '*0*1'); + testFollow('*0*4', '*2*3', '*3', '*0', '*0*3'); + testFollow('*0*4', '*2', '', '*0*4', '*0*4'); + })(); + + function testFollow(randomSeed) { + var rand = new java.util.Random(randomSeed + 1000); + print("> testFollow#"+randomSeed); + + var p = new AttribPool(); + + var startText = randomMultiline(10, 20, rand)+'\n'; + + var cs1 = randomTestChangeset(startText, rand)[0]; + var cs2 = randomTestChangeset(startText, rand)[0]; + + var afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p)); + var bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p)); + + var merge1 = Changeset.checkRep(Changeset.compose(cs1, afb)); + var merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); + + assertEqualStrings(merge1, merge2); + } + + for(var i=0;i<30;i++) testFollow(i); + + function testSplitJoinAttributionLines(randomSeed) { + var rand = new java.util.Random(randomSeed + 2000); + print("> testSplitJoinAttributionLines#"+randomSeed); + + var doc = randomMultiline(10, 20, rand)+'\n'; + + function stringToOps(str) { + var assem = Changeset.mergingOpAssembler(); + var o = Changeset.newOp('+'); + o.chars = 1; + for(var i=0;i testMoveOpsToNewPool"); + + var pool1 = new AttribPool(); + var pool2 = new AttribPool(); + + pool1.putAttrib(['baz','qux']); + pool1.putAttrib(['foo','bar']); + + pool2.putAttrib(['foo','bar']); + + assertEqualStrings(Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab'); + assertEqualStrings(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1'); + })(); + + + (function testMakeSplice() { + print("> testMakeSplice"); + + var t = "a\nb\nc\n"; + var t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, "def"), t); + assertEqualStrings("a\nb\ncdef\n", t2); + + })(); + + (function testToSplices() { + print("> testToSplices"); + + var cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk'); + var correctSplices = [[5, 8, "123456789"], [9, 17, "abcdefghijk"]]; + assertEqualArrays(correctSplices, Changeset.toSplices(cs)); + })(); + + function testCharacterRangeFollow(testId, cs, oldRange, insertionsAfter, correctNewRange) { + print("> testCharacterRangeFollow#"+testId); + + var cs = Changeset.checkRep(cs); + assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], + insertionsAfter)); + + } + + testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', + [7, 10], false, [14, 15]); + testCharacterRangeFollow(2, "Z:bc<6|x=b4|2-6$", [400, 407], false, [400, 401]); + testCharacterRangeFollow(3, "Z:4>0-3+3$abc", [0,3], false, [3,3]); + testCharacterRangeFollow(4, "Z:4>0-3+3$abc", [0,3], true, [0,0]); + testCharacterRangeFollow(5, "Z:5>1+1=1-3+3$abcd", [1,4], false, [5,5]); + testCharacterRangeFollow(6, "Z:5>1+1=1-3+3$abcd", [1,4], true, [2,2]); + testCharacterRangeFollow(7, "Z:5>1+1=1-3+3$abcd", [0,6], false, [1,7]); + testCharacterRangeFollow(8, "Z:5>1+1=1-3+3$abcd", [0,3], false, [1,2]); + testCharacterRangeFollow(9, "Z:5>1+1=1-3+3$abcd", [2,5], false, [5,6]); + testCharacterRangeFollow(10, "Z:2>1+1$a", [0,0], false, [1,1]); + testCharacterRangeFollow(11, "Z:2>1+1$a", [0,0], true, [0,0]); + + (function testOpAttributeValue() { + print("> testOpAttributeValue"); + + var p = new AttribPool(); + p.putAttrib(['name','david']); + p.putAttrib(['color','green']); + + assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p)); + assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p)); + assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p)); + assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p)); + assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p)); + assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p)); + assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p)); + assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p)); + })(); + + function testAppendATextToAssembler(testId, atext, correctOps) { + print("> testAppendATextToAssembler#"+testId); + + var assem = Changeset.smartOpAssembler(); + Changeset.appendATextToAssembler(atext, assem); + assertEqualStrings(correctOps, assem.toString()); + } + + testAppendATextToAssembler(1, {text:"\n", attribs:"|1+1"}, ""); + testAppendATextToAssembler(2, {text:"\n\n", attribs:"|2+2"}, "|1+1"); + testAppendATextToAssembler(3, {text:"\n\n", attribs:"*x|2+2"}, "*x|1+1"); + testAppendATextToAssembler(4, {text:"\n\n", attribs:"*x|1+1|1+1"}, "*x|1+1"); + testAppendATextToAssembler(5, {text:"foo\n", attribs:"|1+4"}, "+3"); + testAppendATextToAssembler(6, {text:"\nfoo\n", attribs:"|2+5"}, "|1+1+3"); + testAppendATextToAssembler(7, {text:"\nfoo\n", attribs:"*x|2+5"}, "*x|1+1*x+3"); + testAppendATextToAssembler(8, {text:"\n\n\nfoo\n", attribs:"|2+2*x|2+5"}, "|2+2*x|1+1*x+3"); + + function testMakeAttribsString(testId, pool, opcode, attribs, correctString) { + print("> testMakeAttribsString#"+testId); + + var p = poolOrArray(pool); + var str = Changeset.makeAttribsString(opcode, attribs, p); + assertEqualStrings(correctString, str); + } + + testMakeAttribsString(1, ['bold,'], '+', [['bold','']], ''); + testMakeAttribsString(2, ['abc,def','bold,'], '=', [['bold','']], '*1'); + testMakeAttribsString(3, ['abc,def','bold,true'], '+', [['abc','def'],['bold','true']], '*0*1'); + testMakeAttribsString(4, ['abc,def','bold,true'], '+', [['bold','true'],['abc','def']], '*0*1'); + + function testSubattribution(testId, astr, start, end, correctOutput) { + print("> testSubattribution#"+testId); + + var str = Changeset.subattribution(astr, start, end); + assertEqualStrings(correctOutput, str); + } + + testSubattribution(1, "+1", 0, 0, ""); + testSubattribution(2, "+1", 0, 1, "+1"); + testSubattribution(3, "+1", 0, undefined, "+1"); + testSubattribution(4, "|1+1", 0, 0, ""); + testSubattribution(5, "|1+1", 0, 1, "|1+1"); + testSubattribution(6, "|1+1", 0, undefined, "|1+1"); + testSubattribution(7, "*0+1", 0, 0, ""); + testSubattribution(8, "*0+1", 0, 1, "*0+1"); + testSubattribution(9, "*0+1", 0, undefined, "*0+1"); + testSubattribution(10, "*0|1+1", 0, 0, ""); + testSubattribution(11, "*0|1+1", 0, 1, "*0|1+1"); + testSubattribution(12, "*0|1+1", 0, undefined, "*0|1+1"); + testSubattribution(13, "*0+2+1*1+3", 0, 1, "*0+1"); + testSubattribution(14, "*0+2+1*1+3", 0, 2, "*0+2"); + testSubattribution(15, "*0+2+1*1+3", 0, 3, "*0+2+1"); + testSubattribution(16, "*0+2+1*1+3", 0, 4, "*0+2+1*1+1"); + testSubattribution(17, "*0+2+1*1+3", 0, 5, "*0+2+1*1+2"); + testSubattribution(18, "*0+2+1*1+3", 0, 6, "*0+2+1*1+3"); + testSubattribution(19, "*0+2+1*1+3", 0, 7, "*0+2+1*1+3"); + testSubattribution(20, "*0+2+1*1+3", 0, undefined, "*0+2+1*1+3"); + testSubattribution(21, "*0+2+1*1+3", 1, undefined, "*0+1+1*1+3"); + testSubattribution(22, "*0+2+1*1+3", 2, undefined, "+1*1+3"); + testSubattribution(23, "*0+2+1*1+3", 3, undefined, "*1+3"); + testSubattribution(24, "*0+2+1*1+3", 4, undefined, "*1+2"); + testSubattribution(25, "*0+2+1*1+3", 5, undefined, "*1+1"); + testSubattribution(26, "*0+2+1*1+3", 6, undefined, ""); + testSubattribution(27, "*0+2+1*1|1+3", 0, 1, "*0+1"); + testSubattribution(28, "*0+2+1*1|1+3", 0, 2, "*0+2"); + testSubattribution(29, "*0+2+1*1|1+3", 0, 3, "*0+2+1"); + testSubattribution(30, "*0+2+1*1|1+3", 0, 4, "*0+2+1*1+1"); + testSubattribution(31, "*0+2+1*1|1+3", 0, 5, "*0+2+1*1+2"); + testSubattribution(32, "*0+2+1*1|1+3", 0, 6, "*0+2+1*1|1+3"); + testSubattribution(33, "*0+2+1*1|1+3", 0, 7, "*0+2+1*1|1+3"); + testSubattribution(34, "*0+2+1*1|1+3", 0, undefined, "*0+2+1*1|1+3"); + testSubattribution(35, "*0+2+1*1|1+3", 1, undefined, "*0+1+1*1|1+3"); + testSubattribution(36, "*0+2+1*1|1+3", 2, undefined, "+1*1|1+3"); + testSubattribution(37, "*0+2+1*1|1+3", 3, undefined, "*1|1+3"); + testSubattribution(38, "*0+2+1*1|1+3", 4, undefined, "*1|1+2"); + testSubattribution(39, "*0+2+1*1|1+3", 5, undefined, "*1|1+1"); + testSubattribution(40, "*0+2+1*1|1+3", 1, 5, "*0+1+1*1+2"); + testSubattribution(41, "*0+2+1*1|1+3", 2, 6, "+1*1|1+3"); + testSubattribution(42, "*0+2+1*1+3", 2, 6, "+1*1+3"); + + function testFilterAttribNumbers(testId, cs, filter, correctOutput) { + print("> testFilterAttribNumbers#"+testId); + + var str = Changeset.filterAttribNumbers(cs, filter); + assertEqualStrings(correctOutput, str); + } + + testFilterAttribNumbers(1, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", + function(n) { return (n%2) == 0; }, + "*0+1+2+3+4*2+5*0*2*c+6"); + testFilterAttribNumbers(2, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", + function(n) { return (n%2) == 1; }, + "*1+1+2+3*1+4+5*1*b+6"); + + function testInverse(testId, cs, lines, alines, pool, correctOutput) { + print("> testInverse#"+testId); + + pool = poolOrArray(pool); + var str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool); + assertEqualStrings(correctOutput, str); + } + + // take "FFFFTTTTT" and apply "-FT--FFTT", the inverse of which is "--F--TT--" + testInverse(1, "Z:9>0=1*0=1*1=1=2*0=2*1|1=2$", null, ["+4*1+5"], ['bold,','bold,true'], + "Z:9>0=2*0=1=2*1=2$"); + + function testMutateTextLines(testId, cs, lines, correctLines) { + print("> testMutateTextLines#"+testId); + + var a = lines.slice(); + Changeset.mutateTextLines(cs, a); + assertEqualArrays(correctLines, a); + } + + testMutateTextLines(1, "Z:4<1|1-2-1|1+1+1$\nc", ["a\n", "b\n"], ["\n", "c\n"]); + testMutateTextLines(2, "Z:4>0|1-2-1|2+3$\nc\n", ["a\n", "b\n"], ["\n", "c\n", "\n"]); + + function testInverseRandom(randomSeed) { + var rand = new java.util.Random(randomSeed + 3000); + print("> testInverseRandom#"+randomSeed); + + var p = poolOrArray(['apple,','apple,true','banana,','banana,true']); + + var startText = randomMultiline(10, 20, rand)+'\n'; + var alines = Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText); + var lines = startText.slice(0,-1).split('\n').map(function(s) { return s+'\n'; }); + + var stylifier = randomTestChangeset(startText, rand, true)[0]; + + //print(alines.join('\n')); + Changeset.mutateAttributionLines(stylifier, alines, p); + //print(stylifier); + //print(alines.join('\n')); + Changeset.mutateTextLines(stylifier, lines); + + var changeset = randomTestChangeset(lines.join(''), rand, true)[0]; + var inverseChangeset = Changeset.inverse(changeset, lines, alines, p); + + var origLines = lines.slice(); + var origALines = alines.slice(); + + Changeset.mutateTextLines(changeset, lines); + Changeset.mutateAttributionLines(changeset, alines, p); + //print(origALines.join('\n')); + //print(changeset); + //print(inverseChangeset); + //print(origLines.map(function(s) { return '1: '+s.slice(0,-1); }).join('\n')); + //print(lines.map(function(s) { return '2: '+s.slice(0,-1); }).join('\n')); + //print(alines.join('\n')); + Changeset.mutateTextLines(inverseChangeset, lines); + Changeset.mutateAttributionLines(inverseChangeset, alines, p); + //print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n')); + + assertEqualArrays(origLines, lines); + assertEqualArrays(origALines, alines); + } + + for(var i=0;i<30;i++) testInverseRandom(i); +} \ No newline at end of file diff --git a/trunk/etherpad/src/etherpad/collab/ace/linestylefilter.js b/trunk/etherpad/src/etherpad/collab/ace/linestylefilter.js new file mode 100644 index 0000000..c7f79a5 --- /dev/null +++ b/trunk/etherpad/src/etherpad/collab/ace/linestylefilter.js @@ -0,0 +1,253 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/linestylefilter.js +import("etherpad.collab.ace.easysync2.Changeset"); + +/** + * 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. + */ + +// requires: easysync2.Changeset + +var linestylefilter = {}; + +linestylefilter.ATTRIB_CLASSES = { + 'bold':'tag:b', + 'italic':'tag:i', + 'underline':'tag:u', + 'strikethrough':'tag:s', + 'h1':'tag:h1', + 'h2':'tag:h2', + 'h3':'tag:h3', + 'h4':'tag:h4', + 'h5':'tag:h5', + 'h6':'tag:h6' +}; + +linestylefilter.getAuthorClassName = function(author) { + return "author-"+author.replace(/[^a-y0-9]/g, function(c) { + if (c == ".") return "-"; + return 'z'+c.charCodeAt(0)+'z'; + }); +}; + +// lineLength is without newline; aline includes newline, +// but may be falsy if lineLength == 0 +linestylefilter.getLineStyleFilter = function(lineLength, aline, + textAndClassFunc, apool) { + + if (lineLength == 0) return textAndClassFunc; + + var nextAfterAuthorColors = textAndClassFunc; + + var authorColorFunc = (function() { + var lineEnd = lineLength; + var curIndex = 0; + var extraClasses; + var leftInAuthor; + + function attribsToClasses(attribs) { + var classes = ''; + Changeset.eachAttribNumber(attribs, function(n) { + var key = apool.getAttribKey(n); + if (key) { + var value = apool.getAttribValue(n); + if (value) { + if (key == 'author') { + classes += ' '+linestylefilter.getAuthorClassName(value); + } + else if (key == 'list') { + classes += ' list:'+value; + } + else if (linestylefilter.ATTRIB_CLASSES[key]) { + classes += ' '+linestylefilter.ATTRIB_CLASSES[key]; + } + } + } + }); + return classes.substring(1); + } + + var attributionIter = Changeset.opIterator(aline); + var nextOp, nextOpClasses; + function goNextOp() { + nextOp = attributionIter.next(); + nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); + } + goNextOp(); + function nextClasses() { + if (curIndex < lineEnd) { + extraClasses = nextOpClasses; + leftInAuthor = nextOp.chars; + goNextOp(); + while (nextOp.opcode && nextOpClasses == extraClasses) { + leftInAuthor += nextOp.chars; + goNextOp(); + } + } + } + nextClasses(); + + return function(txt, cls) { + while (txt.length > 0) { + if (leftInAuthor <= 0) { + // prevent infinite loop if something funny's going on + return nextAfterAuthorColors(txt, cls); + } + var spanSize = txt.length; + if (spanSize > leftInAuthor) { + spanSize = leftInAuthor; + } + var curTxt = txt.substring(0, spanSize); + txt = txt.substring(spanSize); + nextAfterAuthorColors(curTxt, (cls&&cls+" ")+extraClasses); + curIndex += spanSize; + leftInAuthor -= spanSize; + if (leftInAuthor == 0) { + nextClasses(); + } + } + }; + })(); + return authorColorFunc; +}; + +linestylefilter.getAtSignSplitterFilter = function(lineText, + textAndClassFunc) { + var at = /@/g; + at.lastIndex = 0; + var splitPoints = null; + var execResult; + while ((execResult = at.exec(lineText))) { + if (! splitPoints) { + splitPoints = []; + } + splitPoints.push(execResult.index); + } + + if (! splitPoints) return textAndClassFunc; + + return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, + splitPoints); +}; + +linestylefilter.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]/; +linestylefilter.REGEX_URLCHAR = new RegExp('('+/[-:@a-zA-Z0-9_.,~%+\/\\?=&#;()$]/.source+'|'+linestylefilter.REGEX_WORDCHAR.source+')'); +linestylefilter.REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source+linestylefilter.REGEX_URLCHAR.source+'*(?![:.,;])'+linestylefilter.REGEX_URLCHAR.source, 'g'); + +linestylefilter.getURLFilter = function(lineText, textAndClassFunc) { + linestylefilter.REGEX_URL.lastIndex = 0; + var urls = null; + var splitPoints = null; + var execResult; + while ((execResult = linestylefilter.REGEX_URL.exec(lineText))) { + if (! urls) { + urls = []; + splitPoints = []; + } + var startIndex = execResult.index; + var url = execResult[0]; + urls.push([startIndex, url]); + splitPoints.push(startIndex, startIndex + url.length); + } + + if (! urls) return textAndClassFunc; + + function urlForIndex(idx) { + for(var k=0; k= u[0] && idx < u[0]+u[1].length) { + return u[1]; + } + } + return false; + } + + var handleUrlsAfterSplit = (function() { + var curIndex = 0; + return function(txt, cls) { + var txtlen = txt.length; + var newCls = cls; + var url = urlForIndex(curIndex); + if (url) { + newCls += " url:"+url; + } + textAndClassFunc(txt, newCls); + curIndex += txtlen; + }; + })(); + + return linestylefilter.textAndClassFuncSplitter(handleUrlsAfterSplit, + splitPoints); +}; + +linestylefilter.textAndClassFuncSplitter = function(func, splitPointsOpt) { + var nextPointIndex = 0; + var idx = 0; + + // don't split at 0 + while (splitPointsOpt && + nextPointIndex < splitPointsOpt.length && + splitPointsOpt[nextPointIndex] == 0) { + nextPointIndex++; + } + + function spanHandler(txt, cls) { + if ((! splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) { + func(txt, cls); + idx += txt.length; + } + else { + var splitPoints = splitPointsOpt; + var pointLocInSpan = splitPoints[nextPointIndex] - idx; + var txtlen = txt.length; + if (pointLocInSpan >= txtlen) { + func(txt, cls); + idx += txt.length; + if (pointLocInSpan == txtlen) { + nextPointIndex++; + } + } + else { + if (pointLocInSpan > 0) { + func(txt.substring(0, pointLocInSpan), cls); + idx += pointLocInSpan; + } + nextPointIndex++; + // recurse + spanHandler(txt.substring(pointLocInSpan), cls); + } + } + } + return spanHandler; +}; + +// domLineObj is like that returned by domline.createDomLine +linestylefilter.populateDomLine = function(textLine, aline, apool, + domLineObj) { + // remove final newline from text if any + var text = textLine; + if (text.slice(-1) == '\n') { + text = text.substring(0, text.length-1); + } + + function textAndClassFunc(tokenText, tokenClass) { + domLineObj.appendSpan(tokenText, tokenClass); + } + + var func = textAndClassFunc; + func = linestylefilter.getURLFilter(text, func); + func = linestylefilter.getLineStyleFilter(text.length, aline, + func, apool); + func(text, ''); +}; diff --git a/trunk/etherpad/src/etherpad/collab/collab_server.js b/trunk/etherpad/src/etherpad/collab/collab_server.js new file mode 100644 index 0000000..78c9921 --- /dev/null +++ b/trunk/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 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/trunk/etherpad/src/etherpad/collab/collabroom_server.js b/trunk/etherpad/src/etherpad/collab/collabroom_server.js new file mode 100644 index 0000000..ab1f844 --- /dev/null +++ b/trunk/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, + // type: } + 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/trunk/etherpad/src/etherpad/collab/genimg.js b/trunk/etherpad/src/etherpad/collab/genimg.js new file mode 100644 index 0000000..04d1b3b --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/collab/json_sans_eval.js b/trunk/etherpad/src/etherpad/collab/json_sans_eval.js new file mode 100644 index 0000000..6cbd497 --- /dev/null +++ b/trunk/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 + */ +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/trunk/etherpad/src/etherpad/collab/readonly_server.js b/trunk/etherpad/src/etherpad/collab/readonly_server.js new file mode 100644 index 0000000..e367f04 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/collab/server_utils.js b/trunk/etherpad/src/etherpad/collab/server_utils.js new file mode 100644 index 0000000..ece3aea --- /dev/null +++ b/trunk/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 1) { + for(var i=0; i= 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/trunk/etherpad/src/etherpad/control/aboutcontrol.js b/trunk/etherpad/src/etherpad/control/aboutcontrol.js new file mode 100644 index 0000000..9d77142 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/admincontrol.js b/trunk/etherpad/src/etherpad/control/admincontrol.js new file mode 100644 index 0000000..02f6428 --- /dev/null +++ b/trunk/etherpad/src/etherpad/control/admincontrol.js @@ -0,0 +1,1471 @@ +/** + * 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"); + +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"] +]; + +function onRequest(name) { + if (name == "auth") { + return; + } + if (!_isAuthorizedAdmin()) { + getSession().cont = request.path; + response.redirect('/ep/admin/auth'); + } + + var disp = new Dispatcher(); + disp.addLocations([ + [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("«"), " home")); + div.push(H1("Admin")); + + _mainLinks.forEach(function(l) { + div.push(DIV(A({href: l[0]}, l[1]))); + }); + 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("« 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("µ"), "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(" "), 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: ''}), + 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"); + + 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(" » "))); + 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(""), + 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("« back"))); + response.write(HTML(body)); +} \ No newline at end of file diff --git a/trunk/etherpad/src/etherpad/control/blogcontrol.js b/trunk/etherpad/src/etherpad/control/blogcontrol.js new file mode 100644 index 0000000..9ec485d --- /dev/null +++ b/trunk/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 [ + '' + ].join('\n'); +}; + +bloghelpers.feedburnerUrl = function() { + var name = isProduction() ? "TheEtherPadBlog" : "TheEtherPadBlogDev"; + return "http://feeds.feedburner.com/"+name; +}; + +bloghelpers.feedLink = function() { + return [ + '' + ].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/trunk/etherpad/src/etherpad/control/connection_diagnostics_control.js b/trunk/etherpad/src/etherpad/control/connection_diagnostics_control.js new file mode 100644 index 0000000..aaa1bb3 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/global_pro_account_control.js b/trunk/etherpad/src/etherpad/control/global_pro_account_control.js new file mode 100644 index 0000000..65d2124 --- /dev/null +++ b/trunk/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" '; + 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/trunk/etherpad/src/etherpad/control/historycontrol.js b/trunk/etherpad/src/etherpad/control/historycontrol.js new file mode 100644 index 0000000..a78cfad --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/loadtestcontrol.js b/trunk/etherpad/src/etherpad/control/loadtestcontrol.js new file mode 100644 index 0000000..2a4e3f7 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/maincontrol.js b/trunk/etherpad/src/etherpad/control/maincontrol.js new file mode 100644 index 0000000..261ddaf --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/pad/pad_changeset_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_changeset_control.js new file mode 100644 index 0000000..5af7ed0 --- /dev/null +++ b/trunk/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= 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/trunk/etherpad/src/etherpad/control/pad/pad_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_control.js new file mode 100644 index 0000000..3c32202 --- /dev/null +++ b/trunk/etherpad/src/etherpad/control/pad/pad_control.js @@ -0,0 +1,780 @@ +/** + * 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//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(); + } + // + if (DISABLE_PAD_CREATION) { + if (! pro_utils.isProDomainRequest()) { + utils.render500(); + return; + } + } + // + + padutils.accessPadLocal(localPadId, function(pad) { + if (!pad.exists()) { + pad.create(getDefaultPadText()); + } + }); + response.setContentType('text/plain; charset=utf-8'); + response.write([ + '', + 'http://'+request.host+'/'+localPadId+'', + '' + ].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); + } + // + if (DISABLE_PAD_CREATION) { + if (! pro_utils.isProDomainRequest()) { + response.redirect("/ep/pad/create?padId="+encodeURIComponent(localPadId)); + return; + } + } + // + // 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); + 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({ + 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), + opts: opts + }); + }); + + var isProUser = (isPro && ! padusers.isGuest(userId)); + + var isFullWidth = false; + var hideSidebar = false; + var cookiePrefs = padutils.getPrefsCookieData(); + if (cookiePrefs) { + isFullWidth = !! cookiePrefs.fullWidth; + hideSidebar = !! cookiePrefs.hideSidebar; + } + if (opts.fullScreen) { + isFullWidth = true; + if (opts.tokbox) { + hideSidebar = true; + } + } + if ('sidebar' in opts) { + hideSidebar = ! opts.sidebar; + } + var bodyClass = (isFullWidth ? "fullwidth" : "limwidth")+ + " "+(isPro ? "propad" : "nonpropad")+" "+ + (isProUser ? "prouser" : "nonprouser"); + + var cookiePrefsToSet = {fullWidth:isFullWidth, hideSidebar:hideSidebar}; + helpers.addClientVars({cookiePrefsToSet: cookiePrefsToSet}); + + 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: {isFullWidth:isFullWidth, hideSidebar:hideSidebar}, + signinUrl: '/ep/account/sign-in?cont='+ + encodeURIComponent(request.url), + fullSuperdomain: pro_utils.getFullSuperdomainHost() + }); + return true; +} + +function render_create_get() { + var padId = request.params.padId; + // + var template = (DISABLE_PAD_CREATION && ! pro_utils.isProDomainRequest()) ? + "pad/create_body_rafter.ejs" : + "pad/create_body.ejs"; + // + 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" '; + // 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/trunk/etherpad/src/etherpad/control/pad/pad_importexport_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_importexport_control.js new file mode 100644 index 0000000..b7e5f4d --- /dev/null +++ b/trunk/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(""); + 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/trunk/etherpad/src/etherpad/control/pad/pad_view_control.js b/trunk/etherpad/src/etherpad/control/pad/pad_view_control.js new file mode 100644 index 0000000..0606d2c --- /dev/null +++ b/trunk/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', + node.innerHTML, '\n'); + } + return pieces.join(''); +} \ No newline at end of file diff --git a/trunk/etherpad/src/etherpad/control/pne_manual_control.js b/trunk/etherpad/src/etherpad/control/pne_manual_control.js new file mode 100644 index 0000000..0dd65f8 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/pne_tracker_control.js b/trunk/etherpad/src/etherpad/control/pne_tracker_control.js new file mode 100644 index 0000000..ee36645 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/account_control.js b/trunk/etherpad/src/etherpad/control/pro/account_control.js new file mode 100644 index 0000000..031dbe6 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/admin/account_manager_control.js b/trunk/etherpad/src/etherpad/control/pro/admin/account_manager_control.js new file mode 100644 index 0000000..8f93b2e --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/admin/license_manager_control.js b/trunk/etherpad/src/etherpad/control/pro/admin/license_manager_control.js new file mode 100644 index 0000000..ca6d6a6 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js b/trunk/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js new file mode 100644 index 0000000..f9ce179 --- /dev/null +++ b/trunk/etherpad/src/etherpad/control/pro/admin/pro_admin_control.js @@ -0,0 +1,283 @@ +/** + * 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"], + [_PNE_ONLY, "pne-license-manager/", "Manage License"], + [_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"]]], + [_PNE_ONLY, null, "Documentation"], + [_PNE_ONLY, "/ep/pne-manual/", "Administrator's Manual"], + ] + ]; + 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/trunk/etherpad/src/etherpad/control/pro/admin/pro_config_control.js b/trunk/etherpad/src/etherpad/control/pro/admin/pro_config_control.js new file mode 100644 index 0000000..b03da45 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/admin/team_billing_control.js b/trunk/etherpad/src/etherpad/control/pro/admin/team_billing_control.js new file mode 100644 index 0000000..5be6a0e --- /dev/null +++ b/trunk/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. \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/trunk/etherpad/src/etherpad/control/pro/pro_main_control.js b/trunk/etherpad/src/etherpad/control/pro/pro_main_control.js new file mode 100644 index 0000000..b4e3bc4 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro/pro_padlist_control.js b/trunk/etherpad/src/etherpad/control/pro/pro_padlist_control.js new file mode 100644 index 0000000..9a90c67 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/pro_beta_control.js b/trunk/etherpad/src/etherpad/control/pro_beta_control.js new file mode 100644 index 0000000..ec99b43 --- /dev/null +++ b/trunk/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 ", + "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/trunk/etherpad/src/etherpad/control/pro_signup_control.js b/trunk/etherpad/src/etherpad/control/pro_signup_control.js new file mode 100644 index 0000000..6bf7cc3 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/scriptcontrol.js b/trunk/etherpad/src/etherpad/control/scriptcontrol.js new file mode 100644 index 0000000..16efc60 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/control/static_control.js b/trunk/etherpad/src/etherpad/control/static_control.js new file mode 100644 index 0000000..5c087b6 --- /dev/null +++ b/trunk/etherpad/src/etherpad/control/static_control.js @@ -0,0 +1,65 @@ +/** + * 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.*"); + +function onRequest() { + var staticBase = '/static'; + + var opts = {cache: isProduction()}; + + 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); + + var disp = new Dispatcher(); + + 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/trunk/etherpad/src/etherpad/control/statscontrol.js b/trunk/etherpad/src/etherpad/control/statscontrol.js new file mode 100644 index 0000000..3659107 --- /dev/null +++ b/trunk/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 "no data"; + } + 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 "no data"; + } + 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 "no data"; + } + 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 "No sparkline handler!"; + } +} + +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 "no data"; + } + + 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 "No latest handler!"; + } +} + +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("«"), " back to admin")); +// body.push(_renderTopnav(cat)); +// +// body.push(form); +// +// if (request.params.stat) { +// body.push(A({className: "viewall", +// href: qpath({stat: null})}, html("«"), " 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/trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js b/trunk/etherpad/src/etherpad/control/store/eepnet_checkout_control.js new file mode 100644 index 0000000..ddd4973 --- /dev/null +++ b/trunk/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: "

You will not be charged until you review"+ + " and confirm your order on the next page.

", + 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 "+ + "sales@pad.spline.inf.fu-berlin.de.)"); + } + 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. \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/trunk/etherpad/src/etherpad/control/store/storecontrol.js b/trunk/etherpad/src/etherpad/control/store/storecontrol.js new file mode 100644 index 0000000..43569e4 --- /dev/null +++ b/trunk/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."+ +// ' Recover a lost license key here.'); +// } +// +// // 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([ +// '' +// ].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+" ("+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/trunk/etherpad/src/etherpad/control/testcontrol.js b/trunk/etherpad/src/etherpad/control/testcontrol.js new file mode 100644 index 0000000..ed13006 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0000_test.js b/trunk/etherpad/src/etherpad/db_migrations/m0000_test.js new file mode 100644 index 0000000..7df9bfd --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js b/trunk/etherpad/src/etherpad/db_migrations/m0001_eepnet_signups_init.js new file mode 100644 index 0000000..0e65779 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js b/trunk/etherpad/src/etherpad/db_migrations/m0002_eepnet_signups_2.js new file mode 100644 index 0000000..786e4e9 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js b/trunk/etherpad/src/etherpad/db_migrations/m0003_create_tests_table_v2.js new file mode 100644 index 0000000..f121145 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js b/trunk/etherpad/src/etherpad/db_migrations/m0004_convert_all_tables_to_innodb.js new file mode 100644 index 0000000..959865d --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js b/trunk/etherpad/src/etherpad/db_migrations/m0005_create_billing_tables.js new file mode 100644 index 0000000..0dfd37e --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js b/trunk/etherpad/src/etherpad/db_migrations/m0006_eepnet_signups_3.js new file mode 100644 index 0000000..349b27a --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js b/trunk/etherpad/src/etherpad/db_migrations/m0007_create_pro_tables_v4.js new file mode 100644 index 0000000..bda5853 --- /dev/null +++ b/trunk/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 = "<>"; + if (!sqlobj.selectSingle('pro_domains', {subDomain: pneDomain})) { + sqlobj.insert('pro_domains', {subDomain: pneDomain}); + } +} + diff --git a/trunk/etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js b/trunk/etherpad/src/etherpad/db_migrations/m0008_persistent_vars.js new file mode 100644 index 0000000..30e379a --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0009_pad_tables.js b/trunk/etherpad/src/etherpad/db_migrations/m0009_pad_tables.js new file mode 100644 index 0000000..93f5a62 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js b/trunk/etherpad/src/etherpad/db_migrations/m0010_pad_sqlmeta.js new file mode 100644 index 0000000..36150b1 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js b/trunk/etherpad/src/etherpad/db_migrations/m0011_pro_users_temppass.js new file mode 100644 index 0000000..5ac8b26 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js b/trunk/etherpad/src/etherpad/db_migrations/m0012_pro_users_auto_signin.js new file mode 100644 index 0000000..ddd4cf6 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js b/trunk/etherpad/src/etherpad/db_migrations/m0013_pne_padv2_upgrade.js new file mode 100644 index 0000000..146923a --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js b/trunk/etherpad/src/etherpad/db_migrations/m0014_pne_globalpadids.js new file mode 100644 index 0000000..445b32d --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js b/trunk/etherpad/src/etherpad/db_migrations/m0015_padmeta_passwords.js new file mode 100644 index 0000000..8fa98bb --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js b/trunk/etherpad/src/etherpad/db_migrations/m0016_pne_tracking_data.js new file mode 100644 index 0000000..abcc93f --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js b/trunk/etherpad/src/etherpad/db_migrations/m0017_pne_tracking_data_v2.js new file mode 100644 index 0000000..1067840 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js b/trunk/etherpad/src/etherpad/db_migrations/m0018_eepnet_checkout_tables.js new file mode 100644 index 0000000..6e10000 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js b/trunk/etherpad/src/etherpad/db_migrations/m0019_padmeta_deleted.js new file mode 100644 index 0000000..1f9ecbb --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js b/trunk/etherpad/src/etherpad/db_migrations/m0020_padmeta_archived.js new file mode 100644 index 0000000..a776622 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js b/trunk/etherpad/src/etherpad/db_migrations/m0021_pro_padmeta_json.js new file mode 100644 index 0000000..9f357b7 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js b/trunk/etherpad/src/etherpad/db_migrations/m0022_create_userids_table.js new file mode 100644 index 0000000..23ca8d3 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js b/trunk/etherpad/src/etherpad/db_migrations/m0023_create_usagestats_table.js new file mode 100644 index 0000000..927cdc9 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0024_statistics_table.js b/trunk/etherpad/src/etherpad/db_migrations/m0024_statistics_table.js new file mode 100644 index 0000000..9d6e58c --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js b/trunk/etherpad/src/etherpad/db_migrations/m0025_rename_pro_users_table.js new file mode 100644 index 0000000..a429f41 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js b/trunk/etherpad/src/etherpad/db_migrations/m0026_create_guests_table.js new file mode 100644 index 0000000..7c41309 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0027_pro_config.js b/trunk/etherpad/src/etherpad/db_migrations/m0027_pro_config.js new file mode 100644 index 0000000..9cbb629 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js b/trunk/etherpad/src/etherpad/db_migrations/m0028_ondemand_beta_emails.js new file mode 100644 index 0000000..f708363 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js b/trunk/etherpad/src/etherpad/db_migrations/m0029_lowercase_subdomains.js new file mode 100644 index 0000000..36b76ab --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js b/trunk/etherpad/src/etherpad/db_migrations/m0030_fix_statistics_values.js new file mode 100644 index 0000000..aeaa40f --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js b/trunk/etherpad/src/etherpad/db_migrations/m0031_deleted_pro_users.js new file mode 100644 index 0000000..b9744a3 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js b/trunk/etherpad/src/etherpad/db_migrations/m0032_reduce_topvalues_counts.js new file mode 100644 index 0000000..5e748f5 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js b/trunk/etherpad/src/etherpad/db_migrations/m0033_pro_account_usage.js new file mode 100644 index 0000000..4b33f52 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js b/trunk/etherpad/src/etherpad/db_migrations/m0034_create_recurring_billing_table.js new file mode 100644 index 0000000..491581b --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js b/trunk/etherpad/src/etherpad/db_migrations/m0035_add_email_to_paymentinfo.js new file mode 100644 index 0000000..a49e9f9 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js b/trunk/etherpad/src/etherpad/db_migrations/m0036_create_missing_subscription_records.js new file mode 100644 index 0000000..ce77734 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js b/trunk/etherpad/src/etherpad/db_migrations/m0037_create_pro_referral_table.js new file mode 100644 index 0000000..7a9982c --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js b/trunk/etherpad/src/etherpad/db_migrations/m0038_pad_coarse_revs.js new file mode 100644 index 0000000..1e9a53c --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/db_migrations/migration_runner.js b/trunk/etherpad/src/etherpad/db_migrations/migration_runner.js new file mode 100644 index 0000000..ddf201d --- /dev/null +++ b/trunk/etherpad/src/etherpad/db_migrations/migration_runner.js @@ -0,0 +1,147 @@ +/** + * 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" +]; + +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/trunk/etherpad/src/etherpad/debug.js b/trunk/etherpad/src/etherpad/debug.js new file mode 100644 index 0000000..069ad14 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/globals.js b/trunk/etherpad/src/etherpad/globals.js new file mode 100644 index 0000000..2bae776 --- /dev/null +++ b/trunk/etherpad/src/etherpad/globals.js @@ -0,0 +1,41 @@ +/** + * 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 variabls +//---------------------------------------------------------------- + +var COMETPATH = "/comet"; + +var COLOR_PALETTE = ['#ffc6c6','#ffe2bf','#fffcbf','#cbffb3','#b3fff1','#c6e7ff','#dcccff','#ffd9fb']; + +function isProduction() { + return (appjet.config['etherpad.isProduction'] == "true"); +} + +var SUPERDOMAINS = { + 'localhost': true, + 'pad.spline.inf.fu-berlin.de': true, + 'pad.spline.de': true, + 'pad.spline.nomad': true +}; + +var PNE_RELEASE_VERSION = "1.1.3"; +var PNE_RELEASE_DATE = "June 15, 2009"; + +var PRO_FREE_ACCOUNTS = 1e9; + + diff --git a/trunk/etherpad/src/etherpad/helpers.js b/trunk/etherpad/src/etherpad/helpers.js new file mode 100644 index 0000000..cafa201 --- /dev/null +++ b/trunk/etherpad/src/etherpad/helpers.js @@ -0,0 +1,276 @@ +/** + * 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 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 ''; + } + x = x.replace(/', + ' // ', + '' + ].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 ''; + } else { + var ts = +(new Date); + var r = []; + if (_hd().includeCometJs) { + r.push(''); + } + _hd().jsIncludes.asArray().forEach(function(relpath) { + r.push(''); + }); + return r.join('\n'); + } +} + +function cssIncludes() { + if (isProduction()) { + var key = faststatic.getCompressedFilesKey('css', '/static/css', _hd().cssIncludes.asArray()); + return ''; + } else { + var ts = +(new Date); + var r = []; + _hd().cssIncludes.asArray().forEach(function(relpath) { + r.push(''); + }); + return r.join('\n'); + } +} + +function oemail(username) { + return '<'+ + username+'@p*d.sp***e.inf.fu-berlin.de>'; +} + +function googleAnalytics() { + // GA disabled always now. + return ''; + + if (!isProduction()) { return ''; } + if (_hd().suppressGA) { return ''; } + return [ + '', + '' + ].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 [ + '
', + '', + '', + '', + '', + '', + '', + '', + '
', + '', + '
', + '', + '', + 'ABOUT SSL CERTIFICATES', + '', + '
', + '
' + ].join('\n'); +} + +function clearFloats() { + return '
'; +} + +function rafterBlogUrl() { + return '/ep/blog/posts/google-acquires-appjet'; +} + +function rafterNote() { + return """
+ Note: We are no longer accepting new accounts. Read more. +
"""; +} + +function rafterTerminationDate() { + return "March 31, 2010"; +} + diff --git a/trunk/etherpad/src/etherpad/importexport/importexport.js b/trunk/etherpad/src/etherpad/importexport/importexport.js new file mode 100644 index 0000000..304a1f4 --- /dev/null +++ b/trunk/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 ? ": "+from+"." : ".")+" Etherpad can only import txt, html, rtf, doc, and docx 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 txt and html. 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, "&").replace(/ 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/trunk/etherpad/src/etherpad/pad/activepads.js b/trunk/etherpad/src/etherpad/pad/activepads.js new file mode 100644 index 0000000..07f5e2e --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/pad/chatarchive.js b/trunk/etherpad/src/etherpad/pad/chatarchive.js new file mode 100644 index 0000000..2f8e33a --- /dev/null +++ b/trunk/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 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/trunk/etherpad/src/etherpad/pad/easysync2migration.js b/trunk/etherpad/src/etherpad/pad/easysync2migration.js new file mode 100644 index 0000000..c2a1523 --- /dev/null +++ b/trunk/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 0) { + //P var diffs = []; + //P for(var i=0;i= (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/trunk/etherpad/src/etherpad/pad/exporthtml.js b/trunk/etherpad/src/etherpad/pad/exporthtml.js new file mode 100644 index 0000000..2512603 --- /dev/null +++ b/trunk/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= 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, + // Just bold Bold and italics Just italics + // becomes + // Just bold Bold and italics Just italics + + 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(''); + } + + 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=0; i--) { + if (propVals[i] === LEAVE) { + emitCloseTag(i); + propVals[i] = false; + } + else if (propVals[i] === STAY) { + emitCloseTag(i); + } + } + for(var i=0; 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(''); + processNextChars(urlLength); + assem.append(''); + }); + } + 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 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('
  • ', lineContent || '
    '); + } + else if (whichList == -1) { + if (line.text) { + // non-blank line, end all lists + pieces.push(new Array(lists.length+1).join('
  • ')); + lists.length = 0; + pieces.push(lineContent, ''); + } + else { + pieces.push('
    '); + } + } + else { + while (whichList < lists.length-1) { + pieces.push(''); + lists.length--; + } + pieces.push('
  • ', lineContent || '
    '); + } + } + else { + pieces.push(lineContent, ''); + } + } + pieces.push(new Array(lists.length+1).join('
  • ')); + + 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?'':'\n')+ + '\n'+ + (noDocType?'': + '\n'+ + '\n'+ + '\n'+ + ''+'/'+pad.getId()+'\n'+ + '\n' + + '\n')+ + ''; + + var foot = '\n\n'; + + return head + getPadHTML(pad, revNum) + foot; +} + +function _escapeHTML(s) { + var re = /[&<>]/g; + if (! re.MAP) { + // persisted across function calls! + re.MAP = { + '&': '&', + '<': '<', + '>': '>', + }; + } + 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, ' '); + } + 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] = ' '; + endOfLine = false; + beforeSpace = true; + } + else if (p.charAt(0) != "<") { + endOfLine = false; + beforeSpace = false; + } + } + // beginning of line is nbsp + for(var i=0;i 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= 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/trunk/etherpad/src/etherpad/pad/noprowatcher.js b/trunk/etherpad/src/etherpad/pad/noprowatcher.js new file mode 100644 index 0000000..8eb2a92 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/pad/pad_migrations.js b/trunk/etherpad/src/etherpad/pad/pad_migrations.js new file mode 100644 index 0000000..e81cf63 --- /dev/null +++ b/trunk/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 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/trunk/etherpad/src/etherpad/pad/padevents.js b/trunk/etherpad/src/etherpad/pad/padevents.js new file mode 100644 index 0000000..52b303c --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/pad/padusers.js b/trunk/etherpad/src/etherpad/pad/padusers.js new file mode 100644 index 0000000..f04f0eb --- /dev/null +++ b/trunk/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 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); + } +} + diff --git a/trunk/etherpad/src/etherpad/pad/revisions.js b/trunk/etherpad/src/etherpad/pad/revisions.js new file mode 100644 index 0000000..c7c84e8 --- /dev/null +++ b/trunk/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 (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/trunk/etherpad/src/etherpad/pne/pne_utils.js b/trunk/etherpad/src/etherpad/pne/pne_utils.js new file mode 100644 index 0000000..74e0598 --- /dev/null +++ b/trunk/etherpad/src/etherpad/pne/pne_utils.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. + */ + +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 force = (appjet.config['etherpad.forceDbUpgrade'] == "true"); + + 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.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() { + var licenseInfo = licensing.getLicense(); + if (!licenseInfo) { + return false; + } + if (licensing.isVersionTooOld()) { + return false; + } + if (licensing.isExpired()) { + return false; + } + return true; +} + +function enableTrackingAgain() { + delete appjet.cache.noMorePneTracking; +} + +function pneTrackerHtml() { + if (!isPNE()) { + return ""; + } + if (appjet.cache.noMorePneTracking) { + return ""; + } + + var div = DIV({style: "height: 1px; width: 1px; overflow: hidden;"}); + + var licenseInfo = licensing.getLicense(); + var key = null; + if (licenseInfo) { + key = md5(licenseInfo.key).substr(0, 16); + } + + function trackData(name, value) { + var imgurl = "http://pad.spline.inf.fu-berlin.de/ep/tpne/t?"; + if (key) { + imgurl += ("k="+key+"&"); + } + imgurl += (encodeURIComponent(name) + "=" + encodeURIComponent(value)); + div.push(IMG({src: imgurl})); + } + + trackData("ping", "1"); + trackData("dbdriver", appjet.config['etherpad.SQL_JDBC_DRIVER']); + trackData("request.url", request.url); + + appjet.cache.noMorePneTracking = true; + return div; +} + + + diff --git a/trunk/etherpad/src/etherpad/pro/domains.js b/trunk/etherpad/src/etherpad/pro/domains.js new file mode 100644 index 0000000..e56a408 --- /dev/null +++ b/trunk/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('<>'); + if (!r) { + throw Error("<> 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/trunk/etherpad/src/etherpad/pro/pro_account_auto_signin.js b/trunk/etherpad/src/etherpad/pro/pro_account_auto_signin.js new file mode 100644 index 0000000..ebcd227 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/pro/pro_accounts.js b/trunk/etherpad/src/etherpad/pro/pro_accounts.js new file mode 100644 index 0000000..2024970 --- /dev/null +++ b/trunk/etherpad/src/etherpad/pro/pro_accounts.js @@ -0,0 +1,496 @@ +/** + * 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_quotas"); +import("etherpad.pad.padusers"); +import("etherpad.log"); +import("etherpad.billing.team_billing"); + +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) { + if (!domainId) { + domainId = domains.getRequestDomainId(); + } + email = trim(email); + isAdmin = !!isAdmin; // convert to bool + + // validation + 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 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 { + 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) { + 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) { + return [ + 'https://', httpsHost(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/trunk/etherpad/src/etherpad/pro/pro_config.js b/trunk/etherpad/src/etherpad/pro/pro_config.js new file mode 100644 index 0000000..d2d119f --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/pro/pro_pad_db.js b/trunk/etherpad/src/etherpad/pro/pro_pad_db.js new file mode 100644 index 0000000..dbb412c --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/pro/pro_pad_editors.js b/trunk/etherpad/src/etherpad/pro/pro_pad_editors.js new file mode 100644 index 0000000..a90f05b --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/pro/pro_padlist.js b/trunk/etherpad/src/etherpad/pro/pro_padlist.js new file mode 100644 index 0000000..73b179c --- /dev/null +++ b/trunk/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("↓"); + } + if (sortBy == ("-"+m.id)) { + arrow = html("↑"); + } + 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/trunk/etherpad/src/etherpad/pro/pro_padmeta.js b/trunk/etherpad/src/etherpad/pro/pro_padmeta.js new file mode 100644 index 0000000..6f911b2 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/pro/pro_quotas.js b/trunk/etherpad/src/etherpad/pro/pro_quotas.js new file mode 100644 index 0000000..ed69e1c --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/pro/pro_utils.js b/trunk/etherpad/src/etherpad/pro/pro_utils.js new file mode 100644 index 0000000..1dc2468 --- /dev/null +++ b/trunk/etherpad/src/etherpad/pro/pro_utils.js @@ -0,0 +1,165 @@ +/** + * 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("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 (SUPERDOMAINS[domain]) { + return domain; + } + parts.shift(); // Remove next level + } +} + +function isProDomainRequest() { + // 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 (SUPERDOMAINS[domain]) { + return false; + } + + var requestSuperdomain = getRequestSuperdomain(); + + if (SUPERDOMAINS[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" '); +} + +function renderGlobalProNotice() { + if (request.cache.globalProNotice) { + return DIV({className: 'global-pro-notice'}, + request.cache.globalProNotice); + } else { + return ""; + } +} + diff --git a/trunk/etherpad/src/etherpad/quotas.js b/trunk/etherpad/src/etherpad/quotas.js new file mode 100644 index 0000000..7e939ec --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/sessions.js b/trunk/etherpad/src/etherpad/sessions.js new file mode 100644 index 0000000..c218da8 --- /dev/null +++ b/trunk/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 = SUPERDOMAINS[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/trunk/etherpad/src/etherpad/statistics/exceptions.js b/trunk/etherpad/src/etherpad/statistics/exceptions.js new file mode 100644 index 0000000..723085d --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/statistics/statistics.js b/trunk/etherpad/src/etherpad/statistics/statistics.js new file mode 100644 index 0000000..8174405 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/store/checkout.js b/trunk/etherpad/src/etherpad/store/checkout.js new file mode 100644 index 0000000..2a4d7e7 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/store/eepnet_checkout.js b/trunk/etherpad/src/etherpad/store/eepnet_checkout.js new file mode 100644 index 0000000..62137d3 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/store/eepnet_trial.js b/trunk/etherpad/src/etherpad/store/eepnet_trial.js new file mode 100644 index 0000000..570d351 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/testing/testutils.js b/trunk/etherpad/src/etherpad/testing/testutils.js new file mode 100644 index 0000000..eac7840 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/testing/unit_tests/t0000_test.js b/trunk/etherpad/src/etherpad/testing/unit_tests/t0000_test.js new file mode 100644 index 0000000..9e0e78b --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js b/trunk/etherpad/src/etherpad/testing/unit_tests/t0001_sqlbase_transaction_rollback.js new file mode 100644 index 0000000..96a74e4 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js b/trunk/etherpad/src/etherpad/testing/unit_tests/t0002_license_generation.js new file mode 100644 index 0000000..67c79d8 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js b/trunk/etherpad/src/etherpad/testing/unit_tests/t0003_persistent_vars.js new file mode 100644 index 0000000..0898fbe --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js b/trunk/etherpad/src/etherpad/testing/unit_tests/t0004_sqlobj.js new file mode 100644 index 0000000..7f8c996 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js b/trunk/etherpad/src/etherpad/testing/unit_tests/t0005_easysync.js new file mode 100644 index 0000000..9cd3f21 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/usage_stats/usage_stats.js b/trunk/etherpad/src/etherpad/usage_stats/usage_stats.js new file mode 100644 index 0000000..59074ed --- /dev/null +++ b/trunk/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/trunk/etherpad/src/etherpad/utils.js b/trunk/etherpad/src/etherpad/utils.js new file mode 100644 index 0000000..da9972f --- /dev/null +++ b/trunk/etherpad/src/etherpad/utils.js @@ -0,0 +1,396 @@ +/** + * 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"); + +jimport("java.lang.System.out.print"); +jimport("java.lang.System.out.println"); + +//---------------------------------------------------------------- +// 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 renderTemplateAsString(filename, data) { + data = data || {}; + data.helpers = helpers; // global helpers + + var f = "/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); + 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) { + response.write(renderTemplateAsString(filename, data)); + if (request.acceptsGzip) { + response.setGzip(true); + } +} + +function renderHtml(bodyFileName, data) { + var bodyHtml = renderTemplateAsString(bodyFileName, data); + response.write(renderTemplateAsString("html.ejs", {bodyHtml: bodyHtml})); + if (request.acceptsGzip) { + response.setGzip(true); + } +} + +function renderFramedHtml(contentHtml) { + 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 + }); +} + +function renderFramed(bodyFileName, data) { + function _getContentHtml() { + return renderTemplateAsString(bodyFileName, data); + } + renderFramedHtml(_getContentHtml); +} + +function renderFramedError(error) { + 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); +} + +function renderNotice(bodyFileName, data) { + renderNoticeString(renderTemplateAsString(bodyFileName, data)); +} + +function renderNoticeString(contentHtml) { + renderFramed("notice.ejs", {content: contentHtml}); +} + +function render404(noStop) { + 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)))); + if (! noStop) { + response.stop(); + } +} + +function render500(ex) { + response.reset(); + response.setStatusCode(500); + var trace = null; + if (ex && (!isProduction())) { + trace = exceptionutils.getStackTracePlain(ex); + } + renderFramed("500_body.ejs", {trace: trace}); +} + +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 '
     
    '; + } + + 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'); + 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") { + h = (h + ":" + appjet.config.listenSecurePort); + } + return h; +} + +function httpHost(h) { + h = h.split(":")[0]; // strip any existing port + if (appjet.config.listenPort != "80") { + 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/trunk/etherpad/src/main.js b/trunk/etherpad/src/main.js new file mode 100644 index 0000000..503bf9d --- /dev/null +++ b/trunk/etherpad/src/main.js @@ -0,0 +1,418 @@ +/** + * 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"); + +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(); +}; + +serverhandlers.resetHandler = function() { + statistics.onReset(); +} + +serverhandlers.shutdownHandler = function() { + 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() { + if (appjet.config['etherpad.skipHostnameCheck'] == "true") { + return; + } + + if (isPrivateNetworkEdition()) { + return; + } + + // we require the domain to either be or a pro domain request. + if (SUPERDOMAINS[request.domain]) { + return; + } + if (pro_utils.isProDomainRequest()) { + return; + } + + // redirect to pad.spline.inf.fu-berlin.de + 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 + }; + var _requiredHttpsPrefixes = [ + '/ep/admin', // pro and etherpad + '/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(); + + // these paths are handled identically on all sites/subdomains. + var commonDispatcher = new Dispatcher(); + 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)], + ]); + + var etherpadDotComDispatcher = new Dispatcher(); + etherpadDotComDispatcher.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)] + ]); + + 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 + // pad.spline.inf.fu-berlin.de 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 (etherpadDotComDispatcher.dispatch()) { + return; + } + } + + if (!isProDomainRequest()) { + legacy_urls.checkPath(); + } + + render404(); +} + diff --git a/trunk/etherpad/src/static/crossdomain.xml b/trunk/etherpad/src/static/crossdomain.xml new file mode 100644 index 0000000..9e76390 --- /dev/null +++ b/trunk/etherpad/src/static/crossdomain.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/trunk/etherpad/src/static/css/admin/admin-stats.css b/trunk/etherpad/src/static/css/admin/admin-stats.css new file mode 100644 index 0000000..94e0d19 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/beta.css b/trunk/etherpad/src/static/css/beta.css new file mode 100644 index 0000000..afba271 --- /dev/null +++ b/trunk/etherpad/src/static/css/beta.css @@ -0,0 +1,49 @@ +div.beta-signup { } + +div.right { + float: right; + width: 500px; +} +div.left { + width: 224x; + float: left; + text-align: center; + padding: 60px 0 0 80px; +} + +form#beta-signup-form { + border: 1px solid #ccc; + margin: 2em 0; + padding: 1em; + background: #eee; +} + +form#beta-signup-form p { margin: 0; } + +form input { + border: 1px solid #3773c6; + font-size: 14pt; +} + +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; +} + +#error-msg { + margin: 0; + padding: .5em; + margin-bottom: .5em; + border: 1px solid red; + background: #fee; + font-weight: bold; +} + diff --git a/trunk/etherpad/src/static/css/broadcast.css b/trunk/etherpad/src/static/css/broadcast.css new file mode 100644 index 0000000..afb65b8 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/connection_diagnostics.css b/trunk/etherpad/src/static/css/connection_diagnostics.css new file mode 100644 index 0000000..fc040d0 --- /dev/null +++ b/trunk/etherpad/src/static/css/connection_diagnostics.css @@ -0,0 +1,13 @@ +#content { + text-align: center; +} + +#statusmsg { + font-size: 200%; + color: #444; +} + +#emailform { margin: 2em; padding: 1em; background: #eee; border: 1px solid #999; } +#emailform p { font-size: 200%; color: #444; } +#emailform input { font-size: 200%; margin-bottom: 1em; } +#emailform #email { color: #555; } diff --git a/trunk/etherpad/src/static/css/etherpad.css b/trunk/etherpad/src/static/css/etherpad.css new file mode 100644 index 0000000..70bf464 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/fluxbb.css b/trunk/etherpad/src/static/css/fluxbb.css new file mode 100644 index 0000000..844ceca --- /dev/null +++ b/trunk/etherpad/src/static/css/fluxbb.css @@ -0,0 +1,55 @@ +/* fluxbb-specific CSS rules go here. */ + +/*----------------------------------------------------------------*/ +/* Etherpad overriding shit */ +/*----------------------------------------------------------------*/ + +div.epforum { + width: 780px; + padding-top: 1em; + margin-left: auto; + margin-right: auto; +} + +div#punwrap div.pun { + font-size: 120%; +} + +div#idx1 h2 { display: none; } + +div#punwrap div.pun h2 { + color: white; + font-style: normal; + font-weight: normal; + font-family: sans-serif; + font-size: 1.2em; +} + +div#punwrap div.pun p { margin: 0; } + +.pun div.box { + border: 1px solid #ccc; +} + +div#brdheader div.box { + background: #fff; + border: 0; + padding: 0; + margin: 0; +} + +div#brdheader div#brdtitle { + padding: 0; +} +div#brdheader h1 { + font-family: "Lucida Grande","Lucida Sans Unicode",sans-serif; + color: #666; + border-bottom: 1px solid #666; + font-size: 1.8em; +} + +div#brdwelcome { + margin-top: 1em; + border: 1px solid #ccc; + background: #f1f1f1; +} diff --git a/trunk/etherpad/src/static/css/framedpage.css b/trunk/etherpad/src/static/css/framedpage.css new file mode 100644 index 0000000..a99554b --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/global-pro-account.css b/trunk/etherpad/src/static/css/global-pro-account.css new file mode 100644 index 0000000..6c34446 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/home-opensource.css b/trunk/etherpad/src/static/css/home-opensource.css new file mode 100644 index 0000000..0d6da6d --- /dev/null +++ b/trunk/etherpad/src/static/css/home-opensource.css @@ -0,0 +1,44 @@ +#home { + width: 600px; + margin: 0 auto; + padding: 4em; + text-align: center; +} + +#home #title { + font-size: 3.6em; +} + +#home #buttons { + padding-top: 5em; +} + +#home a#home-newpad, #home a#home-newsite { + padding: 1em; + margin: 12px 40px; + font-size: 1.6em; + border: 1px solid black; + background: #049; + color: #fff; + width: 30%; +} + +#home a#home-newpad:hover, #home a#home-newsite:hover { + background: #26b; + text-decoration: none; +} + +#tos { + margin-top: 8em; + color: #222; +} + +#tos h1, #tos p { + margin: 1.5em 0; +} + +#tos h1 { + font-weight: bold; + font-size: 1.1em; +} + diff --git a/trunk/etherpad/src/static/css/home.css b/trunk/etherpad/src/static/css/home.css new file mode 100644 index 0000000..797a8a7 --- /dev/null +++ b/trunk/etherpad/src/static/css/home.css @@ -0,0 +1,264 @@ +/*-------- + Homepage + --------*/ + +/* Top */ + +#top { + height: 349px; + position: relative; + background: url(/static/img/davy/gfx/screenshot.gif) no-repeat bottom right; +} + +#homepage-notice { + margin: 10px auto; + width: 888px; + background: #ffc; + color: #000; + border: 2px solid #550; + font-size: 1.4em; + padding: 8px; +} + +#intro-left { + width: 340px; + float: left; + font-family: Arial, Helvetica, sans-serif; + text-shadow: 0 0 1px #18487F; + padding-top: 10px; +} + #intro-left h1 { + width: 210px; + position: relative; + padding: 57px 0 0 0; + overflow: hidden; + background: transparent url(/static/img/davy/gfx/home-logo2.gif) no-repeat top left; + height: 0px; + margin: 0 0 5px -8px; + } + #intro-left h2 { + color: #d1e5ff; + font-size: 1.5em; + font-weight: bold; + line-height: 1.2; + } + #intro-left h2 a { + color: #9ac6ff; + font-style: italic; + border-bottom: 2px solid; + } + #intro-left h2 a:hover { + text-decoration: none; + color: #4A91EE; + } + #intro-left p { + color: #fff; + font-size: 1.167em; + line-height: 1.3; + margin: 10px 0; + } + #intro-links { + position: absolute; + bottom: 17px; + left: -1px; + width: 500px; + } + #intro-links a { + display: block; + float: left; + position: relative; + padding: 64px 0 0 0; + overflow: hidden; + background-repeat: no-repeat; + background-position: top left; + height: 0; + } + #newpadbutton { + width: 212px; + background-image: url(/static/img/davy/btn/createpad-home.gif); + margin-right: 11px; + } + #betabutton { + width: 220px; + background-image: url(/static/img/davy/btn/signup-home-4.gif); + } + +/* Bottom */ + +#bottom { + padding-top: 28px; +} + +#quote { + border-bottom: 1px solid #e0e0e0; + padding-bottom: 24px; + text-shadow: 0 0 1px #F7F7F7; +} + #quote q { + width: 680px; + float: left; + font-size: 1.33em; + height: 1.5em; + } + #quote #quote-right { + width: 200px; + float: right; + text-align: right; + } + #quote #quote-right cite { + display: block; + font-size: 1.33em; + font-style: italic; + margin-bottom: 8px; + } + +#features { + border-top: 1px solid #fff; + padding-top: 22px; + text-shadow: 0 0 1px #F7F7F7; +} + #features li { + width: 213px; + float: left; + margin-left: 12px; + } + #features li.first { + margin-left: 0; + } + #features li img { + float: left; + margin-right: 8px; + } + #features li strong, + #features li span { + display: block; + margin-left: 40px; + } + #features li strong { + font-size: 16px; + font-weight: normal; + margin-bottom: 6px; + } + #features li span { + line-height: 17px; + } + +#uses { +width: 675px; +float: left; + margin-bottom: 28px; + margin-right: -12px; +position: relative; +} +#uses li { +width: 213px; +float: left; + margin-right: 12px; +} +#uses li a { + font-size: 1.5em; + font-family: Calibri, Arial, Helvetica, sans-serif; + text-shadow: 0 1px 0 #fff; +color: #214b7e; +} +#uses li a:hover { + text-decoration: none; +color: #30619c; +} +#uses li .thumb { +display: block; +width: 213px; +position: relative; +padding: 123px 0 0 0; +overflow: hidden; + background-repeat: no-repeat; + background-position: top left; +height: 0px; + margin-top: 10px; +} +#uses #use-meetings .thumb { background-image: url(/static/img/davy/gfx/use-meetings.png); } +#uses #use-programming .thumb { background-image: url(/static/img/davy/gfx/use-programming.png); } +#uses #use-writing .thumb { background-image: url(/static/img/davy/gfx/use-writing.png); } +#uses .more { +display: block; +position: absolute; +padding: 21px 0 0 0; +overflow: hidden; +background: transparent url(/static/img/davy/btn/uses-more.gif) no-repeat top left; +height: 0px; +width: 58px; +top: 0; +right: 12px; + z-index: 10; +} +* html #uses .more { +right: 24px; +} + +#blog { +width: 213px; +float: left; + margin-right: 12px; +clear: left; +} +#blog h3 { + font-family: Calibri, "Trebuchet MS", Trebuchet, Arial, Helvetica, sans-serif; +color: #666; + text-transform: uppercase; + text-shadow: 0 1px 0 #fff; + letter-spacing: 1px; +} +#blog li { +margin: 1.25em 0; +} +#blog li strong a { + font-size: 1.33em; + font-family: Calibri, Arial, Helvetica, sans-serif; + font-weight: bold; +} +#blog li small { +display: block; + font-size: .917em; + font-family: Calibri, Arial, Helvetica, sans-serif; +color: #666; +margin: 2px 0; +} +#blog li small a { +color: #4182c2; +} +#blog li span { + line-height: 1.3; +} + +#quotes { +width: 406px; +float: left; +background: #e0ecf9; +border: 1px solid #8ba9cc; + border-left: 0; + border-right: 0; +color: #334050; + font-family: Calibri, Arial, Helvetica, sans-serif; +padding: 12px 16px 0; + text-shadow: 0 0 1px #e0ecf9; +} +#quotes q { + font-size: 1.5em; + line-height: 1.22; +display: block; + text-indent: -.45em; +} +#quotes cite { + font-size: 1.33em; +display: block; + font-style: italic; + text-align: right; +margin: .5em 0 1em;; +} +#quotes .more { + text-align: right; + margin-bottom: 1em; + position: relative; + right: -.75em; + font-size: 1.167em; +} diff --git a/trunk/etherpad/src/static/css/lib/jquery.contextmenu.css b/trunk/etherpad/src/static/css/lib/jquery.contextmenu.css new file mode 100644 index 0000000..15a69aa --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/pad.css b/trunk/etherpad/src/static/css/pad.css new file mode 100644 index 0000000..02c341f --- /dev/null +++ b/trunk/etherpad/src/static/css/pad.css @@ -0,0 +1,1000 @@ +*,html.body,p { margin: 0; padding: 0; } +html { + font-size: 62.5%; +} +div.hidden { display: none; } + +/*----------------------------------------------------------------*/ +/* pad */ +/*----------------------------------------------------------------*/ +body#padbody { + font-family: verdana, helvetica, sans-serif; + background: white; + color: black; +} + +body#padbody.limwidth { + background: #d2d2d2 url(/static/img/apr09/backgrad.png) repeat-x left top; +} + +body #padoutertable { width: 100%; } +body.limwidth #padoutertable { width: 940px; margin-left: auto; margin-right: auto; } +#padoutertable td#pot_main, #padoutertable td#pot_top, #padoutertable td.potshad { + vertical-align: top; zoom: 1; position: relative; } +#padoutertable #pot_main { background: white; padding-left: 12px; padding-right: 12px; + padding-top: 2px; +} + +body.fullwidth #padoutertable .potshad { display: none; } + +/* Achieve side drop shadows on top of background gradient using two drop shadow + images for each side (one with gradient in background, one to repeat down the + page). Each side drop shadow gets a column of the padoutertable, but split into + two cells, because if a single cell is used on each side with rowspan=2 with a + tall image in it, IE 6 chooses an unsightly initial height for the top bar. +*/ +body.limwidth #padoutertable .potshad { width: 4px; } +body.limwidth #padoutertable .potshad div { height: 200px; } +body.limwidth #padoutertable #pot_shadleft { + background: url(/static/img/apr09/shadleft.png) repeat-y right top; } +body.limwidth #padoutertable #pot_shadleft div, +body.limwidth #padoutertable #pot_shadlefttopseg { + background: url(/static/img/apr09/shadlefttop.png) no-repeat right top; } +body.limwidth #padoutertable #pot_shadright { + background: url(/static/img/apr09/shadright.png) repeat-y left top; } +body.limwidth #padoutertable #pot_shadright div, +body.limwidth #padoutertable #pot_shadrighttopseg { + background: url(/static/img/apr09/shadrighttop.png) no-repeat left top; } +body.limwidth #padoutertable #pot_main, body.limwidth #padoutertable #pot_top { + border-left: 1px solid #333; + border-right: 1px solid #333; +} +body.limwidth #padoutertable #pot_main { border-bottom: 1px solid #333; } + +#padoutertable #pot_top { background: #2e609e url(/static/img/apr09/topbar.gif) repeat-x left top; + height: 28px; vertical-align: middle; + border-bottom: 1px solid #333; + padding-left: 1px; padding-right: 1px; /* a little padding helps "active" rects not extend outside */ +} + +#padpage #pot_top a#headhomelink { + display: block; float: left; + height: 0; width: 88px; + padding-top: 28px; + overflow: hidden; + text-decoration: none; + background: url(/static/img/apr09/topbarlogo.gif) no-repeat left top; +} +#padpage #pot_top a#widthlink { + display: block; float: right; + height: 0; width: 28px; + padding-top: 28px; + overflow: hidden; + text-decoration: none; +} + +#padpage #pot_top, #padpage #pot_top a { color: #cbd7e7; } +#padpage #pot_top a:focus { outline: 0; } /* for firefox */ + +body.limwidth #padpage #pot_top a#widthlink { + background: url(/static/img/apr09/widthfull.gif) no-repeat center 8px; } +body.fullwidth #padpage #pot_top a#widthlink { + background: url(/static/img/apr09/widthlim.gif) no-repeat center 8px; } +body.limwidth #padpage #pot_top a#widthlink:hover { + background: url(/static/img/apr09/widthfullactive.gif) no-repeat center 8px; } +body.fullwidth #padpage #pot_top a#widthlink:hover { + background: url(/static/img/apr09/widthlimactive.gif) no-repeat center 8px; } + +#padpage #pot_top #headurl { + margin-left: 30px; + margin-top: 5px; + float: left; + margin-right: 20px; + padding: 2px; + height: 15px; + line-height: 14px; + font-size: 1.1em; +} + +#padpage #pot_top #shareurl { font-weight: bold; } + +#padpage #pot_top #newpadlink { + display: block; float: right; margin-right: 30px; font-size: 1.0em; + font-weight: bold; text-decoration: none; + height: 0; + padding-top: 8px; + padding-bottom: 20px; + padding-left: 20px; + padding-right: 6px; + overflow: hidden; + background: url(/static/img/apr09/newpadicon.gif) no-repeat 2px 8px; +} +#padpage #pot_top a#newpadlink:hover { + text-decoration: underline; color: white; +} + +/* +body#padbody.fullwidth { + background: #ddd; +} +#padpage { + +} +body.limwidth #padcontent { + width: 940px; + margin-left: auto; + margin-right: auto; +} +#padpage #padhead { + height: 38px; + text-align: center; + margin-bottom: 4px; +} +#padpage #padhead_inner { + width: 938px; + margin-left: auto; + margin-right: auto; + padding: 0 1px; + background: url(/static/img/oct/minitopback2.gif) repeat center top; + border: 1px solid #666; + border-top: 0; +} +#padpage #padhead a#headhomelink { + display: block; + float: left; + height: 0; + width: 154px; + padding-top: 37px; + overflow: hidden; + text-decoration: none; + background: url(/static/img/oct/minitoplogo2.gif) no-repeat left top; +} +#padpage #headnewpad { + float: right; +} +#padpage #headnewpad #newbutton { + margin-top: 5px; + margin-right: 20px; +} +#padpage #headurl { + margin-left: 70px; + margin-top: 5px; + float: left; + font-size: 1.4em; + margin-right: 20px; + padding: 5px 5px 5px 5px; + height: 17px; + line-height: 17px; +} +#padpage #headurl label { + font-weight: bold; + font-family:"Lucida Grande", Tahoma, Arial, Verdana, sans-serif; + font-size:1em; + position: relative; + top: 1px; +} +#padpage #headurl #shareurl { + border: 1px solid #999; + padding: 3px; +} +#padpage #padtablediv { + margin: 0; +} +body.limwidth #padpage #padtablediv { + margin: 0 20px; +} +*/ + +#padpage #padtable { + width: 100%; +} +#padpage #padtable td#topbar { + height: 16px; +} +#padpage #padtable div#topbarmsg { + display: none; + float: left; + color: #642; + font-size: 1.0em; + padding-top: 2px; + padding-left: 4px; + border-left: 1px solid #ccc; +} + +#padpage #topbar #connectionstatus { + float: left; + padding-top: 2px; + padding-right: 4px; + padding-left: 16px; + height: 16px; + font-size: 1.0em; + color: #666; +} +#padpage #topbar .connecting { + background: url(/static/img/pad/animated-orb-orange-12.gif) no-repeat left center; +} +#padpage #topbar .connected { + background: url(/static/img/pad/orb-greenred-12.gif) no-repeat left 3px; +} +#padpage #topbar .disconnected { + background: url(/static/img/pad/orb-greenred-12.gif) no-repeat left -17px; +} +#padpage #padtable a.showhide { + display: block; + margin: 0; + font-size: 1em; + text-decoration: none; +} +#padpage #padtable a.showhide:hover { + text-decoration: underline; +} +#padpage #padtable a.showhide, #padpage #padtable a.showhide:visited { + color: #66f; +} +#padpage #padtable a#showsidebar { + float: right; + display: none; + position: relative; + top: 2px; +} +#padpage #padtable a#hidesidebar { + float: left; + display: none; +} +/*#padpage #editorcontainer { + display: none; +}*/ +/*#padpage #toptoolbar { + display: none; +}*/ +#padpage .editorcell_loaded #editorcontainer { + background: #fff; + overflow: hidden; + display: block; +} +#padpage #toptoolbar { + border-bottom: 1px solid #666; + height: 30px; + background: #eee; + display: block; + position: relative; /* make it an offsetParent for padtitle stuff */ + overflow: hidden; +} +#padpage #bottoolbar { + border-top: 1px solid #666; + height: 30px; + background: #eee; + display: none; /* set in pad.js */ +} +#padpage #bottoolbar #viewzoom { + padding-left: 5px; + padding-top: 5px; + padding-right: 5px; + float: left; +} +#padpage #bottoolbar #viewfont { + padding-left: 5px; + padding-top: 5px; + padding-right: 5px; + float: left; +} +#padpage #editorcell { + border: 1px solid #666; +} +#padpage #toptoolbar a.toptoolbarbutton { + float: left; + height: 20px; + width: 20px; + border: 1px solid #999; + background-color: #eee; + background-repeat: no-repeat; + background-position: center center; + margin-top: 5px; + text-decoration: none; +} +#padpage #toptoolbar.disabledtoolbar a.toptoolbarbutton { + opacity: 0.5; + filter: alpha(opacity = 50); /* IE */ + zoom: 1; + cursor: auto; +} +#padpage #toptoolbar #padtitle { + float: left; + margin-top: 5px; + margin-left: 20px; + line-height: 20px; + width: 400px; + height: 20px; + overflow: hidden; + display: none; +} +#padpage #toptoolbar .padtitlepad { + font-style: italic; + color: #666; + font-size: 1.2em; +} +#padpage #toptoolbar #padtitletitle { + font-weight: bold; + font-size: 1.2em; +} +#padpage #toptoolbar .editlink { + font-size: 1em; + color: #666; +} +#padpage #toptoolbar .oklink { + display: none; + z-index: 2; + position: absolute; + line-height: 20px; + font-size: 1em; +} +#padpage #toptoolbar #padtitleedit { + z-index: 2; + position: absolute; + left: 0; + top: 0; + display: none; +} +#padpage #toptoolbar a:focus { + outline: 0; +} +#padpage #toptoolbar .bold { background-image: url(/static/img/may09/bold.gif); } +#padpage #toptoolbar .italic { background-image: url(/static/img/may09/italic.gif); } +#padpage #toptoolbar .underline { background-image: url(/static/img/may09/underline.gif); } +#padpage #toptoolbar .undo { background-image: url(/static/img/may09/undo.gif); } +#padpage #toptoolbar .redo { background-image: url(/static/img/may09/redo.gif); } +#padpage #toptoolbar .bold, #padpage #toptoolbar .undo { + margin-left: 5px; +} +#padpage #toptoolbar #passwordlock { + float: right; + margin-top: 5px; + margin-right: 5px; + width: 22px; + height: 22px; + text-decoration: none; +} +#padpage #toptoolbar a#passwordlock:hover { + background-color: #ffffee; +} +#padpage #toptoolbar .passwordhidden { display: none; } +#padpage #toptoolbar .passwordlocked { + display: block; + background: url(/static/img/may09/passwordlocked.gif) no-repeat center center; +} +#padpage #toptoolbar .passwordnone { + display: block; + background: url(/static/img/may09/passwordnone.gif) no-repeat center center; +} +#padpage #sidebarcell {} +#padpage #sidebar { + width: 300px; + border-top: 1px solid #666; + border-right: 1px solid #666; + border-bottom: 1px solid #666; + background: #fff; + overflow: auto; +} +#padpage div.sidebar_loading { + border-left: 1px solid #666; +} +#padpage #editorcontainer iframe { + width: 100%; + padding:0; + margin:0; +} +#padpage #appjetfooter { + padding: 3px 3px; + font-family: Verdana, Helvetica, sans-serif; + font-size: 1em; + text-align: right; +} +div#djs { + font-family: monospace; + font-size: 10pt; + height: 300px; + overflow: scroll; + border: 1px solid #ccc; + background: #fee; + margin: 5px 0; + padding: 6px; +} +div#djs p { margin: 0; padding: 0; display: block; } +#padpage a.small_link { + font-style: normal; + color: #66f; + text-decoration: none; + font-size: 1.1em; +} +#padpage a.small_link:hover { text-decoration: underline; } +#padpage .editorcell_loading #editorcellinner { + height: 400px; /* make #sizedcontent stretch the outer table for height calc */ +} +#padpage #editorcellinner { + position: relative; + zoom: 1; +} +#padpage #loadingbox { + padding-top: 100px; + padding-bottom: 100px; + font-size: 2.5em; + color: #aaa; + text-align: center; + position: absolute; + width: 100%; + height: 30px; + z-index: 100; +} +/*----------------------------------------------------------------*/ +/* userlist */ +/*----------------------------------------------------------------*/ +#sidebar div.sideheadwrap { + font-weight: normal; + font-size: 1.2em; + text-align: center; + padding: 3px 6px; + background: #eee url(/static/img/pad/sidehead-grad.gif) repeat-x bottom left; + border-bottom: 1px solid #666; + cursor: pointer; + zoom: 1; +} +#sidebar div.sh_hilited { + background-image: url(/static/img/oct/sidehead-gradhilite.gif); +} +#sidebar div.sideheadwrap p.sidehead { + display: block; + text-align: left; + padding: 0 0 0 18px; + margin: 0; +} +#sidebar div.sh_uncollapsed p.sidehead { + background: url(/static/img/pad/expandy-arrow6-down.gif) no-repeat center left; +} +#sidebar div.sh_collapsed p.sidehead { + background: url(/static/img/pad/expandy-arrow6-right.gif) no-repeat center left; +} +#sidebar div.sideheadwrap:hover { + cursor: pointer; + background: #bbb; +} +#sidebar div.sh_uncollapsed:hover p.sidehead { + background: url(/static/img/pad/expandy-arrow6-down-active.gif) no-repeat center left; +} +#sidebar div.sh_collapsed:hover p.sidehead { + background: url(/static/img/pad/expandy-arrow6-right-active.gif) no-repeat center left; +} +#sidebar div.sidebox { margin-bottom: 10px; } +#sidebar div#chatbox { margin-bottom: 2px; } +#sidebar div.sidebox_last { margin-bottom: 0; } +#sidebar #userlist div.userbox { + border-bottom: 1px solid #ccc; +} +#sidebar #userlist div.lastuser { + border-bottom: 0; +} +#sidebar #userlist div.userbox div.userinfo { + font-style: italic; + margin-top: 3px; +} +#sidebar #userlist div.userbox div.userinfo span.username { + padding-bottom: 4px; + font-size: 1.2em; +} +#sidebar #userlist div.userbox div.userinfo div.ip { + color: #999; + font-size: 1em; + margin-bottom: 3px; +} +#sidebar #userlist div.userbox div.usercolor { + border: 1px solid black; + width: 12px; + height: 12px; + float: left; + margin: 6px; + margin-top: 3px; + margin-left: 0; +} +#sidebar #userlist div.userbox div#rightuserlink { + float: right; + text-align: right; + width: 120px; +} +#sidebar #userlist a#changenamelink { + padding-right: 18px; + background: url(/static/img/pad/pencil-icon-small-blue.gif) no-repeat top right; +} +#sidebar #userlist div.userinfowrap { + padding: 6px 0 6px 6px; +} +#sidebar #userlist div.myuserwrap:hover { + cursor: pointer; + background: #eee; +} +/*----------------------------------------------------------------*/ +/* editing my user info */ +/*----------------------------------------------------------------*/ +#userlist div.edituserinfo { + color: black; + padding: 6px 0 12px 12px; +} +#userlist div.edituserinfo p { + font-size: 1.2em; + margin: 8px 4px; +} +#userlist div.edituserinfo h4 { + margin-top: 1em; + margin-left: 4px; + font-size: 1.3em; + color: black; + font-weight: bold; +} +#userlist div.edituserinfo input { + width: 260px; +} +#userlist div.edituserinfo button { + margin: 8px 4px; +} +#colorpicker a { + border: 3px solid #ccc; + text-decoration: none; + display: block; + width: 12px; + height: 12px; + float: left; + margin: 4px; + cursor: pointer; +} +#colorpicker a.selectedcolor { + border: 3px solid black; +} +#colorpicker a:hover { + border: 3px solid black; +} +/*----------------------------------------------------------------*/ +/* invitemore */ +/*----------------------------------------------------------------*/ +#sidebar #invitemore { + display: none; + text-align: center; + margin-top: 10px; + font-size: 1.1em; +} +#sidebar #invitemore input { + font-size: 1.1em; +} +#sidebar #invitemore a {} +#sidebar #invitemore #inviteinstructions { + background-color: #efe; + border: 1px solid #ccc; +} +#sidebar #invitemore #inviteinstructions p { + text-align: justify; + padding: .4em 1em; +} +#sidebar #invitemore #inviteinstructions p#hideinstructions { + text-align: center; +} +#sidebar #invitemore #inviteinstructions p#emailinviteleadin { + margin-top: .6em; +} +#sidebar #invite_email { width: 160px; } +#sidebar #invitemore #inviteinstructions #invite_email_submit {} +#sidebar #invitemore #invite_email_status { color: #642; } +/*----------------------------------------------------------------*/ +/* prefs */ +/*----------------------------------------------------------------*/ +#sidebar div#headprefs { border-top: 1px solid #666; } +#sidebar div#headfeedback { border-top: 1px solid #666; } +#sidebar div#headrevisions { border-top: 1px solid #666; } +#sidebar div#headchatbox { border-top: 1px solid #666; } +#sidebar div#headimportexport { border-top: 1px solid #666; } +#sidebar #prefs div.prefcheckbox { + margin: 0; + cursor: pointer; + border: 1px solid #fff; + font-size: 1em; +} +#sidebar #prefs div.prefcheckbox td.checkboxcell { + padding: 0 4px; +} +#sidebar #prefs div.prefcheckbox td.labelcell { + padding: 3px 4px; +} +#sidebar #prefs div.prefcheckbox:hover { + cursor: pointer; + background-color: #def; + border: 1px solid #aaa; +} +/*----------------------------------------------------------------*/ +/* revisions */ +/*----------------------------------------------------------------*/ +#sidebar #revisions { + text-align: center; +} +#sidebar #revisionlist {} +#sidebar #revisions input#savenow { + width: 260px; + margin-left: auto; margin-right: auto; + margin-top: 6px; margin-bottom: 6px; +} +#sidebar #revisions .revisioninfo { + text-align: left; + font-size: 1.1em; + border-top: 1px solid #ccc; + padding: 3px 2px 3px 6px; +} +#sidebar #revisions .revisioninfo .ractions { + color: #aaa; + font-size: 1em; +} +#sidebar #revisions .revisioninfo .rleft { + width: 96px; + float: left; +} +#sidebar #revisions .revisioninfo .editrlabel { + padding-left: 16px; + background: url(/static/img/pad/pencil-icon-small-blue.gif) no-repeat center left; +} +#sidebar #revisions .revisioninfo .rright { + margin-left: 18px; + color: #777; + font-style: italic; +} +#sidebar #revisions .revisionbottomlinks { + border-top: 1px solid #eee; + padding-top: 5px; + color: #888; + font-size: 1.1em; +} +#sidebar #revisions #nosaveprivs { + display: none; + color: #282; + font-size: 1.2em; + padding: 1em; +} +#sidebar #revisions p.revlabelprompt { + color: #444; + padding: 2px; +} +#sidebar #revisions input.inputrevlabel { + display: block; + width: 260px; + margin-left: auto; margin-right: auto; + margin-top: 6px; margin-bottom: 6px; + border: 1px solid #ccf; +} + +/*----------------------------------------------------------------*/ +/* feedback */ +/*----------------------------------------------------------------*/ +#sidebar #feedback { + background: #eee; + padding: 1px 8px; /* non-zero padding so that background extends */ + border-bottom: 1px solid #bbb; + text-align: center; + padding-bottom: 12px; +} +#sidebar #feedback p { + font-size: 1.1em; + margin: 10px 0; + color: #333; + text-align: justify; +} +#sidebar #feedback p em { + font-weight: bold; + font-style: italic; +} +#sidebar #feedback #formbox { + width: 260px; + margin-left: auto; + margin-right: auto; + zoom: 1; + positive: relative; +} +#sidebar #feedback textarea { + width: 100%; + margin-left: auto; + margin-right: auto; + height: 100px; +} +#sidebar #feedbacksubmit { + width: 100%; + text-align: center; + margin-left: auto; + margin-right: auto; +} +#sidebar #feedbackresult { + display: none; +} +/*----------------------------------------------------------------*/ +/* other */ +/*----------------------------------------------------------------*/ +a#newbutton { + font-family:"Lucida Grande", Tahoma, Arial, Verdana, sans-serif; + font-size:1.2em; + line-height:1.0; + margin:0 0 0 0; + text-decoration:none; + + background-color:#eee; + border:1px solid #999; + border-top: 1px solid #bbb; + border-bottom: 1px solid #666; + + padding:5px 5px 3px 5px; + + cursor:pointer; + font-weight:bold; + color:#555; + display: block; +} +a#newbutton:hover { + background-color: #ddd; + border: 1px solid #999; + color: #333; +} +a#newbutton img { + padding:0; + padding-bottom: 2px; + border:none; + width: 16px; + height: 16px; + vertical-align: middle; +} + +#framedpage #notice { + padding: 40px 20px; +} +#framedpage #notice p { + margin: 16px 0; +} + +img#plane { border: 0; vertical-align: middle; } + +/*----------------------------------------------------------------*/ +/* top msgs */ +/*----------------------------------------------------------------*/ + +div.topmsg { + zoom: 1; + margin: 5px 0; + position: relative; +} +div.topmsg a#hidetopmsg { + position: absolute; + right: 5px; + bottom: 5px; +} + +div#bigtoperror_wrap { + border: 1px solid #a66; + background: #fdd; + font-size: 1.2em; + padding: 1em; + padding-bottom: .5em; +} +div#bigtoperror_wrap p { + margin-bottom: 6px; + color: #222; +} +div#bigtoperror_wrap p.whynote { + color: #444; +} +div#bigtoperror_wrap p.whynote a { + color: #33f; +} +div#bigtoperror_wrap button.forcereconnect { + margin-top: 6px; +} +div#bigtoperror_wrap a { color: #00a; } +div#bigtoperror_wrap a:visited { color: #00a; } + +div#servermsg { + position: relative; + border: 1px solid #992; + background: #ffc; + padding: 1em; +} + +/*----------------------------------------------------------------*/ +/* chat */ +/*----------------------------------------------------------------*/ +#chatbox {} +#chatbox #chatmessages { + margin: 0; + margin-bottom: 2px; + height: 160px; + border: 1px solid #ccc; + overflow: auto; +} +#chatbox div.chatmessage { padding: 2px 0; } +#chatbox div.chatusermessage0 { background-color: #eee; } +#chatbox span.chatname { font-style: italic; } +#chatbox span.chattime { font-style: italic; color: #444; } +#chatbox span.chatline { color: #222; } +#chatbox input#chatinput { width: 100%; } +#chatbox #chatsaytable { + width: 270px; + margin-left: auto; + margin-right: auto; + padding: 0; + border-spacing: 0; +} +#chatbox #chatsaytable td { padding: 0 2px; } + +/*----------------------------------------------------------------*/ +/* import/export */ +/*----------------------------------------------------------------*/ + +#importexport td.exportpic a img { + border: 0; +} + +#importexport .exportspinner { + display: none; +} + +#importexport .exportspinner img { + margin-left: 7px; +} + +#importexport a.disabledexport { + color: gray; +} + +#importexport { + font-size: 1em; + font-family: verdana, helvetica, sans-serif; +} + +#importexport .exportlink { + margin: 2px 0; +} + +#importexport td.labelcell { + padding-left: 4px; +} + +#importexport td.firsttd { + padding-left: 10px; +} + +#importexport td.secondtd { + padding-left: 50px; +} + +#importexport #headexport { + font-size: 1.1em; + margin: 7px; + margin-top: 10px; +} + +#importexport #importsection { + border-top: 1px solid #ccc; + margin-top: 5px; + padding-top: 3px; +} + +#importexport .importformdiv { + padding: 5px 15px; +} + +#importexport #importformsubmitdiv { + margin-top: 5px; +} + +.importformenabled { + background: #cfc; + border-top: 1px solid #292; + border-bottom: 1px solid #292; +} + +#importexport #headimport { + font-size: 1.1em; + margin: 7px; +} + +#importexport .importmessage { + display: none; + border: 1px solid #992; + background: #ffc; + padding: 5px; +} + +#importexport #exportmessage { + display: none; + border: 1px solid #992; + background: #ffc; + padding: 5px; + margin: 10px 15px; +} + +#importexport #importmessagefail { + margin-top: 10px; + margin-bottom: 5px; +} + +#importexport #importmessagesuccess { + margin: 0 20px; +} + +#importexport #importstatusball { + display: none; + padding-bottom: 3px; +} + +#importexport #importarrow { + display: none; + margin-left: 5px; +} + +span.nowrap { + white-space: nowrap; +} + +#sidebar #prefs div.prefcheckbox { + margin: 0; + cursor: pointer; + border: 1px solid #fff; + font-size: 1em; +} +#sidebar #prefs div.prefcheckbox td.checkboxcell { + padding: 0 4px; +} +#sidebar #prefs div.prefcheckbox td.labelcell { + padding: 3px 4px; +} +#sidebar #prefs div.prefcheckbox:hover { + cursor: pointer; + background-color: #def; + border: 1px solid #aaa; +} + +/*----------------------------------------------------------------*/ +/* modal dialogs */ +/*----------------------------------------------------------------*/ + +#modaloverlay { + position: absolute; + z-index: 100; + background-image: url(/static/img/apr09/black35.png); + zoom: 1; + display: none; + left: 0; top: 0; + width: 100%; +} + +* html #modaloverlay { /* for IE 6+ */ + background-color: transparent; + background-image: url(/static/img/apr09/blank.gif); + filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src="/static/img/apr09/black35.png", sizingMethod="scale"); +} + +#modaldialog { + border: 1px solid #333; + background: #ddd; + width: 400px; + margin-left: auto; + margin-right: auto; + position: relative; +} + +#dialogtopbar { + height: 18px; + border-bottom: 1px solid #333; + background: #2e609e url(/static/img/apr09/modalbar.gif) repeat-x left top; + color: #cbd7e7; + font-size: 1.0em; + font-weight: bold; + line-height: 18px; + padding-left: 10px; + cursor: default; +} + +td#dialogcontent { + padding: 10px; + height: 100px; + vertical-align: top; +} + +table#dialogcontenttable { width: 100%; } diff --git a/trunk/etherpad/src/static/css/pad2_ejs.css b/trunk/etherpad/src/static/css/pad2_ejs.css new file mode 100644 index 0000000..253b8e2 --- /dev/null +++ b/trunk/etherpad/src/static/css/pad2_ejs.css @@ -0,0 +1,889 @@ + +*,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#topbarnewpad { display: block; float: left; position: relative; top: 4px; width: 94px; + height: 0; padding-top: 26px; overflow: hidden; + background: url(/static/img/jun09/pad/padtop5.png) no-repeat -5px -4px; } +a#topbarnewpad:focus { outline: 0; } + +a#topbarfullwidth { display: block; float: right; position: relative; top: 2px; width: 107px; + height: 0; padding-top: 27px; overflow: hidden; + background: url(/static/img/jun09/pad/padtop5.png) no-repeat -788px -2px; } +a#topbarfullwidth:focus { outline: 0; } +*/ + +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: 30px; position: relative; zoom: 1; + background: #fbfbfb url(/static/img/jun09/pad/padtopback2.gif) repeat-x 0 -31px; } + +#docbarleft { position: absolute; left: 0; top: 0; height: 100%; + overflow: hidden; + background: url(/static/img/jun09/pad/padtop5.png) no-repeat left -31px; width: 7px; } + +<% function docbarButton(name, width, imgleft, posright, hoverimgleft, openimgleft) { + return ("#docbar$name$-outer { width: "+width+"px; position: absolute; height: 30px; top: 0; right: "+posright+"px; "+ + "background: url(/static/img/jun09/pad/padtop5.png) no-repeat "+(-imgleft)+"px -31px; "+ + "/* avoid IE flicker using double background */ }"+ + "a#docbar$name$ { display: block; height: 0; padding-top: 30px; position: absolute; width: 100%; "+ + "overflow: hidden; background: url(/static/img/jun09/pad/padtop5.png) no-repeat "+(-imgleft)+"px -31px; "+ + "z-index: 53; /* > .dbpanel-wrapper */} "+ + "a#docbar$name$:focus { outline: 0; } "+ + "a#docbar$name$:hover { background: url(/static/img/jun09/pad/docbarstates3.png) no-repeat "+(-hoverimgleft)+"px 0; } "+ + ".docbar$name$-opening a#docbar$name$, .docbar$name$-opening a#docbar$name$:hover, .docbar$name$-closing a#docbar$name$, .docbar$name$-closing a#docbar$name$:hover, .docbar$name$-open a#docbar$name$, .docbar$name$-open a#docbar$name$:hover { "+ + "background: url(/static/img/jun09/pad/docbarstates3.png) no-repeat "+(-openimgleft)+"px 0; } "+ + "a#docbar$name$:hover, .docbar$name$-closing a#docbar$name$ { padding-top: 29px; } "+ + ".docbar$name$-opening a#docbar$name$, .docbar$name$-opening a#docbar$name$:hover { "+ + "/* opening or closing: link covers gray line below it */ "+ + "padding-top: 30px; }"+ + ".docbar$name$-open a#docbar$name$, .docbar$name$-open a#docbar$name$:hover { "+ + "/* link covers gray line below it, and also top highlight of panel */ "+ + "padding-top: 30px; }").replace(/\$name\$/g, name); +} %> + +<% // include left border, not right %> +<%= docbarButton("savedrevs", 128, 669, 103, 123, 123) %> +<%= docbarButton("impexp", 122, 547, 231, 0, 0) %> +<%= docbarButton("options", 109, 438, 353, 379, 379) %> +<%= docbarButton("security", 85, 353, 462, 489, 489) %> + +#docbarslider-outer { width: 104px; position: absolute; height: 30px; top: 0; right: 0; + background: url(/static/img/jun09/pad/padtop5.png) no-repeat -796px -31px; + /* avoid IE flicker using double background */ } +a#docbarslider { display: block; height: 0; padding-top: 30px; position: absolute; width: 100%; + overflow: hidden; background: url(/static/img/jun09/pad/padtop5.png) no-repeat -796px -31px; z-index: 53; /* > .dbpanel-wrapper */} + +<% /* 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: 4px; + 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: 9px; + font-size: 1.1em; display: none; } +#docbarrenamelink a { color: #999; } +#docbarrenamelink a:hover { color: #48d; } +#padtitlebuttons { position: absolute; width: 120px; zoom: 1; + height: 22px; top: 4px; left: 223px; display: none; + background: url(/static/img/jun09/pad/padtop5.png) -19px -35px; } +#padtitlesave { position: absolute; display: block; + height: 0; padding-top: 22px; overflow: hidden; + width: 49px; left: 0; top: 0; } +#padtitlecancel { position: absolute; display: block; + height: 0; padding-top: 22px; overflow: hidden; + width: 49px; right: 0; top: 0; } +#padtitleedit { position: absolute; top: 4px; left: 5px; + height: 17px; 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: 207px; 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/editbar3.png) repeat-x left -36px; position: relative; } + +#editbarleft { float: left; height: 100%; overflow: hidden; + background: url(/static/img/jun09/pad/editbar3.png) no-repeat left top; width: 3px; } +#editbarright { float: right; height: 100%; overflow: hidden; + background: url(/static/img/jun09/pad/editbar3.png) no-repeat right top; width: 3px; } + +#editbar a.editbarbutton { + display: block; + position: absolute; + height: 26px; + width: 26px; + background-image: url(/static/img/jun09/pad/editbar3.png); + background-color: transparent; + background-repeat: no-repeat; + text-decoration: none; + top: 5px; +} +#editbar.disabledtoolbar a.editbarbutton { + opacity: 0.5; + filter: alpha(opacity = 50); /* IE */ + zoom: 1; + cursor: auto; +} +/*#editbar .divider { position: absolute; width: 4px; height: 15px; + background-image: url(/static/img/jun09/pad/editbar3.png); + background-color: transparent; background-repeat: no-repeat; } +#editbar .divider1 { left: 137px; top: 11px; background-position: -137px -11px; } +#editbar .divider2 { left: 188px; top: 11px; background-position: -188px -11px; }*/ +#editbar a:focus { outline: 0; } + +<% function editbarButton(name, pos, width, fromRight) { + width = width || 26; + var bposX = - (fromRight ? 600-width-pos : pos); + return "div#editbar a."+name+" { "+ + (fromRight?'right':'left')+": "+pos+"px; background-position: "+ + bposX+"px -5px; width: "+width+"px; }\n"+ + "div#padeditor div.enabledtoolbar a."+name+":active { background-position: "+ + bposX+"px -77px; }"; +} %> +<%= editbarButton('bold', 7, 25) %> +<%= editbarButton('italic', 32, 23) %> +<%= editbarButton('underline', 55, 23) %> +<%= editbarButton('strikethrough', 78, 24) %> +<%= editbarButton('h1', 108, 25) %> +<%= editbarButton('h2', 133, 23) %> +<%= editbarButton('h3', 156, 23) %> +<%= editbarButton('h4', 179, 23) %> +<%= editbarButton('h5', 202, 23) %> +<%= editbarButton('h6', 225, 24) %> +<%= editbarButton('clearauthorship', 342) %> +<%= editbarButton('undo', 374, 25) %> +<%= editbarButton('redo', 399, 24) %> +<%= editbarButton('insertunorderedlist', 255) %> +<%= editbarButton('indent', 287, 25) %> +<%= editbarButton('outdent', 312, 24) %> +<%= editbarButton('save', 6, null, true) %> + +#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: 76px; + 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 .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: 42px; + width: 47px; height: 0; padding-top: 20px; overflow: hidden; + cursor: pointer; } +#mycolorpickercancel { position: absolute; left: 87px; top: 42px; + 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: 30px; /* /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/trunk/etherpad/src/static/css/pne-manual.css b/trunk/etherpad/src/static/css/pne-manual.css new file mode 100644 index 0000000..19f1ba0 --- /dev/null +++ b/trunk/etherpad/src/static/css/pne-manual.css @@ -0,0 +1,143 @@ +/* global */ + +div.pne-manpage { + font-size: 1.1em; +} + +.pne-manual-topnav { + border-bottom: 1px solid #ccc; + margin-bottom: 1em; + padding-bottom: 4px; +} + +div.pne-manpage h2 { + color: #111; + border-bottom: 1px solid #111; +} + +div.pne-manpage ul,ol { + padding-left: 2em; +} +div.pne-manpage li { + margin-top: .4em; +} +div.pne-manpage ol li { + list-style: decimal; + margin-top: 1em; +} + + +div.pne-manpage tt { + font-family: monospace; + font-size: 1.1em; + color: #040; + font-style: italic; +} + +div.pne-manpage div.code { + font-family: monospace; + font-size: 1.0em; + padding: 0 1em; + border: 1px solid #ccc; + background: #eee; + margin: 0; +} + +div.pne-manpage div.code span.prompt { + color: #609; +} + +div.pne-manpage div.code tt { + font-style: normal; + font-size: 1.0em; + color: #00f; +} + +div.pne-manpage div.code p { + line-height: 125%; + margin: 1em 0; + padding: 0; +} + +/* main */ + +div#pne-main h2 { + font-size: 1.4em; + color: black; + font-weight: bold; + border: 0; + margin: 0; + padding: 0; +} +div#pne-main h3 { + font-size: 1.1em; + color: #555; + font-style: italic; +} +div#pne-main h4 { + color: #888; + margin-top: 2em; + font-weight: bold; +} +div#pne-main ul { + padding-left: 2em; +} +div#pne-main ul li { + list-style: square; +} +div#pne-main p#version-notice { + font-size: 88%; + color: #333; + margin-top: 2em; + border-top: 1px solid #ccc; +} + +/* configuration-guide */ + +table#opts { + width: 100%; + border-top: 1px solid #ccc; + border-left: 1px solid #ccc; + border-right: 0; + border-bottom: 0; + margin: 2em 0 1em 0; +} +table#opts td, table#opts th { + padding: 4px 0; + border-right: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} +table#opts td { + font-family: monospace; +} +table#opts td.desc { + font-family: Verdana, sans-serif; +} +table#opts th { + text-align: left; + font-weight: bold; +} +table#opts td.rowhead { + padding-top: 1em; + font-style: italic; + color: #222; + border-bottom: 1px solid #222; +} + +/* changelog */ + +div#pne-changelog h2 { + margin-top: 2em; +} + +div#pne-changelog h3 { + font-weight: bold; + padding-left: 1em; + margin: 1em 0; +} + +div#pne-changelog ul { + padding-left: 3em; +} + + diff --git a/trunk/etherpad/src/static/css/pricing.css b/trunk/etherpad/src/static/css/pricing.css new file mode 100644 index 0000000..0b7c9d5 --- /dev/null +++ b/trunk/etherpad/src/static/css/pricing.css @@ -0,0 +1,153 @@ +/*----------------------------------------------------------------*/ +/* pricing */ +/*----------------------------------------------------------------*/ + +div.pricingpage { +} + +.pricingpage form#pricingcontact { + display: block; + margin: 1em 0; + background: #eee; + border: 1px solid #ccc; + padding: 1em; +} +.pricingpage form#pricingcontact p { margin: .75em 0; font-weight: bold; } +.pricingpage form#pricingcontact ul li { list-style: none; margin: 0;} + +.pricingpage .eepnet-inquiry label { + display: block; + float: left; + width: 140px; + text-align: right; + font-weight: normal; + color: #444; +} +.pricingpage .eepnet-inquiry input.ti, +.pricingpage .eepnet-inquiry select { + width: 240px; + margin-left: 12px; +} +.pricingpage .eepnet-inquiry button { + width: 100px; + margin-left: 160px; +} +.pricingpage div.inquiryhead { + font-weight: bold; + color: #000; + margin-bottom: 1em; + border-bottom: 2px solid #aaa; +} + +.pricingpage div#errorbox, .pricingpage div#confirmbox { + color: #222; + font-weight: bold; + padding: 1em .5em; +} +.pricingpage div#errorbox { + background: #fee; + border: 1px solid #f66; +} +.pricingpage div#confirmbox { + background: #efe; + border: 1px solid #ccc; +} + +a.pricingbox { + display: block; + height: 280px; + border: 1px solid #777; + background: #fcfcfc; + cursor: pointer; + text-align: center; + text-decoration: none; +} + +a.pricingbox span { + display: block; +} + +a.pricingbox span#buylink { + display: inline; + color: #004ca8; +} + +a.pricingbox span#buylink:hover { + text-decoration: underline; +} + +a.pricingbox:hover { + background: #e2f2ff; + border: 1px solid #000; + text-decoration: none; +} + +a.pricingbox img { + margin: 10px 0; + border: 0; +} + +a.pricingbox span.pricingtitle { + display: block; + margin-top: 5px; + margin-left: 10px; + font-size: 1.2em; + color: #119; + text-decoration: underline; +} + +a.pricingbox span.pricingdesc { + display: block; + color: #555; + margin: 10px; + height: 4.25em; +} + +a.pricingbox span.pricingcost { + top: 10px; + display: block; + color: #000; + margin: 10px; + font-weight: bold; +} + +a.pricingbox span.pricingcost p { + font-weight: normal; +} + +#freetrialwrap a.freetrialbox { + padding-top: 4px; + display: block; + border: 1px solid #ccc; + background: #eee; + cursor: pointer; + color: #000; + text-decoration: underline; +} +#freetrialwrap a.freetrialbox:hover { + background: #def; +} +#freetrialwrap a.freetrialbox span.freetrialtext { + margin-top: 7px; + float: left; +} +a.freetrialbox img { + border: 0; + float: left; + margin: 5px 10px; +} + +a.pro-signup-button { + display: block + 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; +} + diff --git a/trunk/etherpad/src/static/css/pro-signup.css b/trunk/etherpad/src/static/css/pro-signup.css new file mode 100644 index 0000000..b58d86d --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/pro/account.css b/trunk/etherpad/src/static/css/pro/account.css new file mode 100644 index 0000000..212a847 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/pro/framedpage-pro.css b/trunk/etherpad/src/static/css/pro/framedpage-pro.css new file mode 100644 index 0000000..cffa58b --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/pro/padlist.css b/trunk/etherpad/src/static/css/pro/padlist.css new file mode 100644 index 0000000..13d3171 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/pro/payment-required.css b/trunk/etherpad/src/static/css/pro/payment-required.css new file mode 100644 index 0000000..44d55b2 --- /dev/null +++ b/trunk/etherpad/src/static/css/pro/payment-required.css @@ -0,0 +1,39 @@ + +#outside{ + padding: 0 0 0 266px; + background: url(/static/img/pro/billing/cards-button.gif) 50px 25px no-repeat; +} + +#inside { + margin: 0; + padding: 1em; + border-left: 1px solid #ccc; + background: #fff; +} + +h1 { + font-weight: bold; + font-size: 1.33em; + border-bottom: 1px solid #ccc; +} + +#message { + border: 1px solid #b97; + background: #ffe; + padding: 1em; + margin: 1em 0; +} + +a.manage-billing-button { + display: block + border: 0; + cursor: pointer; + color: #fff; + font-weight: bold; + overflow: visible; + background: #70a4ec; + border: 1px solid #3773c6; + padding: 8px 12px; + margin-top: 4px; +} + diff --git a/trunk/etherpad/src/static/css/pro/pro-admin.css b/trunk/etherpad/src/static/css/pro/pro-admin.css new file mode 100644 index 0000000..e7462c9 --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/pro/pro-home.css b/trunk/etherpad/src/static/css/pro/pro-home.css new file mode 100644 index 0000000..03f163a --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/css/stats.css b/trunk/etherpad/src/static/css/stats.css new file mode 100644 index 0000000..25dd074 --- /dev/null +++ b/trunk/etherpad/src/static/css/stats.css @@ -0,0 +1,71 @@ +div.statentry { + width: 600px; + border: 1px solid #060; + background: #afa; + margin: 1em; +} + +body { + margin: 0; +} + +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 h2:hover { + text-decoration: underline; +} + +.warning h2 { + background: #ea0; +} + +.error h2 { + background: #a00; +} + +.statentry th { + padding: 3px; + font-weight: normal; +} + +.statentry td { + text-align: left; + padding: 3px; + width: 400px; + font-size: 24px; +} + +.statentry table { + width: 100%; +} + +.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/trunk/etherpad/src/static/css/store/eepnet-checkout.css b/trunk/etherpad/src/static/css/store/eepnet-checkout.css new file mode 100644 index 0000000..20254af --- /dev/null +++ b/trunk/etherpad/src/static/css/store/eepnet-checkout.css @@ -0,0 +1,284 @@ +#shoppingmain dt { + margin: 0.5em; + margin-left: 0.5em; + font-weight: bold; + line-height: 120%; +} + +#shoppingmain dd { + margin: 0.5em; + margin-top: 1em; + margin-bottom: 2em; + line-height: 120%; +} + +#shoppingmain dd:first-letter, +#shoppingmain dt:first-letter { + font-weight: bold; +} + +#shoppingmain table { + background: #eef; + width: 100%; +} + +#shoppingmain table tr {} +#shoppingmain > table td, +#shoppingmain > table th { + padding: 6px 8px; +} + +#shoppingmain { + width: 100%; + border: 1px solid #ccc; +} + +#shoppingmain p { + margin: 1em; +} + +#shoppingmain a { + text-decoration: underline; +} + +#shoppingmain ul, +#shoppingmain ol { + margin-right: 1em; +} + +#shoppingmain h3 { + background: #eee; + padding: 0.5em 1em; + border-bottom: 1px solid #ccc; + font-weight: bold; + font-size: 110%; +} + +#shoppingwrapper { + width: 580px; +} + +.shoppingcart table td.pcell, table th.pcell { + text-align: right; + white-space: nowrap; +} + +#shoppingcart { + border: 1px solid #ccc; + background: white; +} + +.shoppingcart table { + width: 100%; +} + +.shoppingcart table th { + font-weight: bold; + background: #eee; + border-bottom: 1px solid #ccc; +} + +.shoppingcart table td { + padding: 0 3px; +} + +.shoppingcart table th { + padding: 5px; +} + +div.checkoutnav { + padding-top: 10px; + clear: both; + display: block; + text-align: center; +} + +.center, +.center td { + text-align: center; +} + +div.errormsg { + border: 1px solid #caa; + padding: 1em; + margin: 1em 0; +} + +div.errormsg, +table tr.error, +p.error, +div.error { + background: #fdd; +} + +div.innererrormsg { + margin: 1em; + border: 1px solid #aca; + padding: 1em; + background: #dfd; +} + +a#cschelp { + font-size: 7pt; + color: blue; + border-bottom: 1px dashed blue; +} + +table tr.total td { + font-weight: bold; +} + +#shoppingcart { + width: 250px; + float: right; + font-size: 85%; +} + +.shoppingcart span.desc { + display: block; + padding-top: 4px; + padding-left: 10px; + font-style: italic; +} + +table tr.withoutsubtotal td, +table tr.subtotal td { + border-top: 1px solid #ccc; +} + +#backbutton { + float: right; + padding: 5px; +} + +#continuebutton { + float: right; + padding: 5px; +} + +.shoppingcart tr.base td { + padding-top: 5px; +} + +.shoppingcart tr.refer td { + color: green; + font-weight: bold; +} + +.shoppingcart tr.support td { + padding-top: 10px; +} + +.shoppingcart tr.referralbonus td { + padding-top: 10px; +} + +.shoppingcart tr.spacer td { + padding-top: 10px; +} + +.shoppingcart tr.subtotal td { + padding-top: 5px; +} + +.shoppingcart tr.referraldiscount td { + padding-top: 5px; +} + +.shoppingcart tr.total td { + padding-top: 5px; + padding-bottom: 5px; +} + +.shoppingcart td.noitems { + text-align: center; + padding-top: 15px; +} + +.paymentbutton label { + cursor: pointer; +} + +.billingtable td.pcell { + width: 300px; +} + +.billingtable tr.intonly { + display: none; +} + +.billingtable #ccimages img.ccimageselected { + border: 3px solid #0f0; + opacity: 1; +} + +.billingtable #ccimages img { + border: 3px solid #eef; + opacity: 0.5; +} + +input[type="text"].greenborder { + border: 1px solid #0f0; +} + +input[type="text"].redborder { + border: 1px solid #f00; +} + +input[type="text"] { + border: 1px solid black; + padding: 2px; +} + +td.tcell { + width: 200px; +} + +#shoppingmain td { + font-weight: bold; +} + +#shoppingmain input, +#shoppingmain p, +#shoppingmain td, +#shoppingmain li { + font-size: 85%; +} + +h4 { + font-weight: bold; + font-size: 100%; + margin: 10px; + margin-top: 20px; +} + +h4 .editlink { + font-size: 75%; +} + +span.item .editlink { + font-size: 75%; +} + +span.desc .editlink { + font-size: 75%; + font-style: normal; +} + +.paymentbutton { + padding-left: 1.5em; +} + +div.position { + font-size: 80%; + color: #999; + padding-bottom: 10px; +} + +div.position .current { + color: #f93; +} + +span.poslabel { + padding: 0 1em; +} diff --git a/trunk/etherpad/src/static/css/store/ondemand-billing.css b/trunk/etherpad/src/static/css/store/ondemand-billing.css new file mode 100644 index 0000000..7c4afe3 --- /dev/null +++ b/trunk/etherpad/src/static/css/store/ondemand-billing.css @@ -0,0 +1,170 @@ +input[type="text"].greenborder { + border: 1px solid #0f0; +} + +input[type="text"].redborder { + border: 1px solid #f00; +} + +input[type="text"] { + border: 1px solid black; + padding: 2px; +} + +h4 { + font-weight: bold; + margin-bottom: 0.75em; + margin-top: 1em; + margin-left: 15px; +} + +div.billinginfo { + margin-left: 65px; + border-left: 1px solid gray; + border-right: 1px solid gray; + border-top: 1px solid gray; + border-bottom: 1px solid gray; + width: 600px; +} + +.billinginfo table.billingtable { + width: 100%; + margin: 0; + background: #eef; +} + +.billinginfo table.billingtable td, +.billinginfo table.billingtable th { + padding: 5px; +} + +.billinginfo table.billingtable td.pcell, +.billinginfo table.billingtable td.firstcell { + padding-left: 35px; +} + +.billinginfo table.billingtable td.pcell { + width: 200px; +} + +.billinginfo div { + padding-left: 35px; +} + +.billinginfo div#billingselect { + padding-left: 0; +} + +.billinginfo div#billingselect p { + padding: 3px; + padding-left: 35px; +} + +.billingtable #ccimages img.ccimageselected { + border: 3px solid #0f0; + opacity: 1; +} + +.billingtable #ccimages img { + border: 3px solid #eef; + opacity: 0.5; +} + +a#cschelp { + font-size: 7pt; + color: blue; + border-bottom: 1px dashed blue; +} + +div#contbutton { + margin-top: 1em; + margin-right: 45px; + float: right; +} + +div#backbutton { + margin-top: 1em; + float: right; +} + +div.errormsg { + border: 1px solid #caa; + padding: 1em; + margin: 1em 0; +} + +div.errormsg, +table tr.error, +p.error, +div.error { + background: #fdd; +} + +table.billingsummary { + width: 100%; + border-top: 1px solid gray; + border-left: 1px solid gray; +} + +table.billingsummary th, +table.billingsummary td { + padding: 5px; + border-bottom: 1px solid gray; + border-right: 1px solid gray; +} + +table.billingsummary th { + font-weight: bold; +} + +span#editpaymentslink { + text-align: right; + font-size: 80%; + margin-left: 0.5em; +} + +#editpaymentslink a { + text-decoration: underline; +} + + +.paymentbutton label { + cursor: pointer; +} +.paymentbutton { + padding-left: 1.5em; +} + +/* invoice list */ + +.informational { + font-style: italic; +} + +table.invoicelist { + border-left: 1px solid #ccc; + border-top: 1px solid #ccc; + border-right: 1px solid #ccc; + width: 100%; + font-size: 10pt; +} + +table.invoicelist tr:hover { + background: #ffc; +} + +table.invoicelist td, +table.invoicelist th { + border-bottom: 1px solid #ccc; + padding: 5px; +} + +table.invoicelist th { + font-weight: bold; + background: #eef; +} + +.returnlink { + font-size: 10pt; + font-style: italic; +} \ No newline at end of file diff --git a/trunk/etherpad/src/static/css/store/store.css b/trunk/etherpad/src/static/css/store/store.css new file mode 100644 index 0000000..f228698 --- /dev/null +++ b/trunk/etherpad/src/static/css/store/store.css @@ -0,0 +1,90 @@ +div.storepage a.downloadbutton_disabled { + text-decoration: none; + display: block; + padding: .4em; + margin: 1em; + font-size: 1.4em; + width: 270px; + margin-left: auto; + margin-right: auto; + border: 2px solid #ccc; + background: #eee; + color: #888; +} + +div.storepage a.downloadbutton { + text-decoration: none; + display: block; + padding: .4em; + margin: 1em; + font-size: 1.4em; + width: 270px; + margin-left: auto; + margin-right: auto; + border: 2px solid #333; + background: #ccc; +} +div.storepage a.downloadbutton:hover { + background: #ddf; + cursor: pointer; +} + +div.storepage label:hover { + cursor: pointer; +} + +div.storepage div#topmsg { + border: 1px solid #999; + background: #dfd; + font-weight: bold; + padding: 1em; +} + +div.storepage div#errormsg { + border: 1px solid #f00; + background: #fee; + font-weight: bold; + padding: 1em 2em; + magin-top: 1em; + margin-bottom: 1em; +} + +#dlsignup { + border: 1px solid #ccc; +/* background: #ffefdf; */ + background: #efe; + padding: 1em 2em; +} + +#dlsignup, div#errormsg { + display: block; +} + +#dlsignup p { + margin: 0; +} + +#dlsignup p label { + display: block; + margin-top: .7em; + color: #444; +} + +#dlsignup input, +#dlsignup select { + width: 400px; +} + +#dlsignup button { + margin-top: 1em; + width: 200px; + margin-left: 100px; +} + +div#processingmsg { + border: 1px solid #ccc; + background: #efe; + padding: 1em 2em; + font-weight: bold; + font-size: 1.4em; +} diff --git a/trunk/etherpad/src/static/favicon.ico b/trunk/etherpad/src/static/favicon.ico new file mode 100644 index 0000000..a833c3a Binary files /dev/null and b/trunk/etherpad/src/static/favicon.ico differ diff --git a/trunk/etherpad/src/static/img/about/appjet-logo-large.gif b/trunk/etherpad/src/static/img/about/appjet-logo-large.gif new file mode 100644 index 0000000..11351b2 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/appjet-logo-large.gif differ diff --git a/trunk/etherpad/src/static/img/about/appjet-logo-medium.png b/trunk/etherpad/src/static/img/about/appjet-logo-medium.png new file mode 100644 index 0000000..f6297e1 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/appjet-logo-medium.png differ diff --git a/trunk/etherpad/src/static/img/about/investors/mitchkapor.jpg b/trunk/etherpad/src/static/img/about/investors/mitchkapor.jpg new file mode 100644 index 0000000..5a65938 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/investors/mitchkapor.jpg differ diff --git a/trunk/etherpad/src/static/img/about/investors/pb.jpg b/trunk/etherpad/src/static/img/about/investors/pb.jpg new file mode 100644 index 0000000..bd59c5c Binary files /dev/null and b/trunk/etherpad/src/static/img/about/investors/pb.jpg differ diff --git a/trunk/etherpad/src/static/img/about/investors/pg.jpg b/trunk/etherpad/src/static/img/about/investors/pg.jpg new file mode 100644 index 0000000..184155d Binary files /dev/null and b/trunk/etherpad/src/static/img/about/investors/pg.jpg differ diff --git a/trunk/etherpad/src/static/img/about/investors/sanjeev.jpg b/trunk/etherpad/src/static/img/about/investors/sanjeev.jpg new file mode 100644 index 0000000..9073b50 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/investors/sanjeev.jpg differ diff --git a/trunk/etherpad/src/static/img/about/investors/seth.jpg b/trunk/etherpad/src/static/img/about/investors/seth.jpg new file mode 100644 index 0000000..00f2aa9 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/investors/seth.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/aaron-david-iphones-thumb.jpg b/trunk/etherpad/src/static/img/about/people/aaron-david-iphones-thumb.jpg new file mode 100644 index 0000000..70c17db Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/aaron-david-iphones-thumb.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/aaron-david-iphones.jpg b/trunk/etherpad/src/static/img/about/people/aaron-david-iphones.jpg new file mode 100644 index 0000000..70c17db Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/aaron-david-iphones.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/aaron-google-air.jpg b/trunk/etherpad/src/static/img/about/people/aaron-google-air.jpg new file mode 100644 index 0000000..5948454 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/aaron-google-air.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/aaron-headshot-thumb.jpg b/trunk/etherpad/src/static/img/about/people/aaron-headshot-thumb.jpg new file mode 100644 index 0000000..b0cffcd Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/aaron-headshot-thumb.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/aaron-headshot.jpg b/trunk/etherpad/src/static/img/about/people/aaron-headshot.jpg new file mode 100644 index 0000000..2b88437 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/aaron-headshot.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/aaron-headshot2-thumb.jpg b/trunk/etherpad/src/static/img/about/people/aaron-headshot2-thumb.jpg new file mode 100644 index 0000000..d6c1a97 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/aaron-headshot2-thumb.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/aaron-headshot2.jpg b/trunk/etherpad/src/static/img/about/people/aaron-headshot2.jpg new file mode 100644 index 0000000..e4b2a77 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/aaron-headshot2.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/aaron-headshot3-thumb.jpg b/trunk/etherpad/src/static/img/about/people/aaron-headshot3-thumb.jpg new file mode 100644 index 0000000..cca1b68 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/aaron-headshot3-thumb.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/aaron-headshot3.jpg b/trunk/etherpad/src/static/img/about/people/aaron-headshot3.jpg new file mode 100644 index 0000000..13c433d Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/aaron-headshot3.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/daniel-headshot-thumb.jpg b/trunk/etherpad/src/static/img/about/people/daniel-headshot-thumb.jpg new file mode 100644 index 0000000..567316c Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/daniel-headshot-thumb.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/david-headshot-thumb.jpg b/trunk/etherpad/src/static/img/about/people/david-headshot-thumb.jpg new file mode 100644 index 0000000..5f9da44 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/david-headshot-thumb.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/david-headshot.jpg b/trunk/etherpad/src/static/img/about/people/david-headshot.jpg new file mode 100644 index 0000000..89ab3ea Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/david-headshot.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/davy-headshot.jpg b/trunk/etherpad/src/static/img/about/people/davy-headshot.jpg new file mode 100644 index 0000000..9430186 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/davy-headshot.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/jd-headshot-thumb.jpg b/trunk/etherpad/src/static/img/about/people/jd-headshot-thumb.jpg new file mode 100644 index 0000000..b399a57 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/jd-headshot-thumb.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/jd-headshot.jpg b/trunk/etherpad/src/static/img/about/people/jd-headshot.jpg new file mode 100644 index 0000000..182d534 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/jd-headshot.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/rhonda-headshot-thumb.jpg b/trunk/etherpad/src/static/img/about/people/rhonda-headshot-thumb.jpg new file mode 100644 index 0000000..8d9358b Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/rhonda-headshot-thumb.jpg differ diff --git a/trunk/etherpad/src/static/img/about/people/rhonda-headshot.jpg b/trunk/etherpad/src/static/img/about/people/rhonda-headshot.jpg new file mode 100644 index 0000000..b4c4ec8 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/people/rhonda-headshot.jpg differ diff --git a/trunk/etherpad/src/static/img/about/pier38.png b/trunk/etherpad/src/static/img/about/pier38.png new file mode 100644 index 0000000..d15b3a8 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/pier38.png differ diff --git a/trunk/etherpad/src/static/img/about/quote-close.png b/trunk/etherpad/src/static/img/about/quote-close.png new file mode 100644 index 0000000..de4b18b Binary files /dev/null and b/trunk/etherpad/src/static/img/about/quote-close.png differ diff --git a/trunk/etherpad/src/static/img/about/quote-open.png b/trunk/etherpad/src/static/img/about/quote-open.png new file mode 100644 index 0000000..e637705 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/quote-open.png differ diff --git a/trunk/etherpad/src/static/img/about/screencastpreview800x600.jpg b/trunk/etherpad/src/static/img/about/screencastpreview800x600.jpg new file mode 100644 index 0000000..2a4ed39 Binary files /dev/null and b/trunk/etherpad/src/static/img/about/screencastpreview800x600.jpg differ diff --git a/trunk/etherpad/src/static/img/account/betawarn.jpg b/trunk/etherpad/src/static/img/account/betawarn.jpg new file mode 100644 index 0000000..c3cec1b Binary files /dev/null and b/trunk/etherpad/src/static/img/account/betawarn.jpg differ diff --git a/trunk/etherpad/src/static/img/acecarets/000000.gif b/trunk/etherpad/src/static/img/acecarets/000000.gif new file mode 100644 index 0000000..f67bd3d Binary files /dev/null and b/trunk/etherpad/src/static/img/acecarets/000000.gif differ diff --git a/trunk/etherpad/src/static/img/acecarets/666666.gif b/trunk/etherpad/src/static/img/acecarets/666666.gif new file mode 100644 index 0000000..cd8e264 Binary files /dev/null and b/trunk/etherpad/src/static/img/acecarets/666666.gif differ diff --git a/trunk/etherpad/src/static/img/acecarets/999999.gif b/trunk/etherpad/src/static/img/acecarets/999999.gif new file mode 100644 index 0000000..fa75d25 Binary files /dev/null and b/trunk/etherpad/src/static/img/acecarets/999999.gif differ diff --git a/trunk/etherpad/src/static/img/acecarets/default.gif b/trunk/etherpad/src/static/img/acecarets/default.gif new file mode 100644 index 0000000..196d9ff Binary files /dev/null and b/trunk/etherpad/src/static/img/acecarets/default.gif differ diff --git a/trunk/etherpad/src/static/img/apr09/backgrad.png b/trunk/etherpad/src/static/img/apr09/backgrad.png new file mode 100644 index 0000000..c61d830 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/backgrad.png differ diff --git a/trunk/etherpad/src/static/img/apr09/black35.png b/trunk/etherpad/src/static/img/apr09/black35.png new file mode 100644 index 0000000..9d82846 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/black35.png differ diff --git a/trunk/etherpad/src/static/img/apr09/blank.gif b/trunk/etherpad/src/static/img/apr09/blank.gif new file mode 100644 index 0000000..8fb6fb0 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/blank.gif differ diff --git a/trunk/etherpad/src/static/img/apr09/modalbar.gif b/trunk/etherpad/src/static/img/apr09/modalbar.gif new file mode 100644 index 0000000..3e86759 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/modalbar.gif differ diff --git a/trunk/etherpad/src/static/img/apr09/newpadicon.gif b/trunk/etherpad/src/static/img/apr09/newpadicon.gif new file mode 100644 index 0000000..a282728 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/newpadicon.gif differ diff --git a/trunk/etherpad/src/static/img/apr09/shadbot.png b/trunk/etherpad/src/static/img/apr09/shadbot.png new file mode 100644 index 0000000..9506058 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/shadbot.png differ diff --git a/trunk/etherpad/src/static/img/apr09/shadleft.png b/trunk/etherpad/src/static/img/apr09/shadleft.png new file mode 100644 index 0000000..72049e0 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/shadleft.png differ diff --git a/trunk/etherpad/src/static/img/apr09/shadleftbot.png b/trunk/etherpad/src/static/img/apr09/shadleftbot.png new file mode 100644 index 0000000..7d3fb5b Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/shadleftbot.png differ diff --git a/trunk/etherpad/src/static/img/apr09/shadlefttop.png b/trunk/etherpad/src/static/img/apr09/shadlefttop.png new file mode 100644 index 0000000..9af4e90 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/shadlefttop.png differ diff --git a/trunk/etherpad/src/static/img/apr09/shadright.png b/trunk/etherpad/src/static/img/apr09/shadright.png new file mode 100644 index 0000000..41d099c Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/shadright.png differ diff --git a/trunk/etherpad/src/static/img/apr09/shadrightbot.png b/trunk/etherpad/src/static/img/apr09/shadrightbot.png new file mode 100644 index 0000000..6770ec5 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/shadrightbot.png differ diff --git a/trunk/etherpad/src/static/img/apr09/shadrighttop.png b/trunk/etherpad/src/static/img/apr09/shadrighttop.png new file mode 100644 index 0000000..0f7a0ba Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/shadrighttop.png differ diff --git a/trunk/etherpad/src/static/img/apr09/topbar.gif b/trunk/etherpad/src/static/img/apr09/topbar.gif new file mode 100644 index 0000000..65ed7c8 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/topbar.gif differ diff --git a/trunk/etherpad/src/static/img/apr09/topbarlogo.gif b/trunk/etherpad/src/static/img/apr09/topbarlogo.gif new file mode 100644 index 0000000..9a3aa77 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/topbarlogo.gif differ diff --git a/trunk/etherpad/src/static/img/apr09/widthfull.gif b/trunk/etherpad/src/static/img/apr09/widthfull.gif new file mode 100644 index 0000000..8850ec2 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/widthfull.gif differ diff --git a/trunk/etherpad/src/static/img/apr09/widthfullactive.gif b/trunk/etherpad/src/static/img/apr09/widthfullactive.gif new file mode 100644 index 0000000..36b566f Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/widthfullactive.gif differ diff --git a/trunk/etherpad/src/static/img/apr09/widthlim.gif b/trunk/etherpad/src/static/img/apr09/widthlim.gif new file mode 100644 index 0000000..551e741 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/widthlim.gif differ diff --git a/trunk/etherpad/src/static/img/apr09/widthlimactive.gif b/trunk/etherpad/src/static/img/apr09/widthlimactive.gif new file mode 100644 index 0000000..8cf0194 Binary files /dev/null and b/trunk/etherpad/src/static/img/apr09/widthlimactive.gif differ diff --git a/trunk/etherpad/src/static/img/billing/amex.gif b/trunk/etherpad/src/static/img/billing/amex.gif new file mode 100644 index 0000000..96e2cbb Binary files /dev/null and b/trunk/etherpad/src/static/img/billing/amex.gif differ diff --git a/trunk/etherpad/src/static/img/billing/creditcard.gif b/trunk/etherpad/src/static/img/billing/creditcard.gif new file mode 100644 index 0000000..ac2353e Binary files /dev/null and b/trunk/etherpad/src/static/img/billing/creditcard.gif differ diff --git a/trunk/etherpad/src/static/img/billing/csc-help.gif b/trunk/etherpad/src/static/img/billing/csc-help.gif new file mode 100644 index 0000000..1afb6b7 Binary files /dev/null and b/trunk/etherpad/src/static/img/billing/csc-help.gif differ diff --git a/trunk/etherpad/src/static/img/billing/disc.gif b/trunk/etherpad/src/static/img/billing/disc.gif new file mode 100644 index 0000000..a5e3a90 Binary files /dev/null and b/trunk/etherpad/src/static/img/billing/disc.gif differ diff --git a/trunk/etherpad/src/static/img/billing/invoice.gif b/trunk/etherpad/src/static/img/billing/invoice.gif new file mode 100644 index 0000000..cee681a Binary files /dev/null and b/trunk/etherpad/src/static/img/billing/invoice.gif differ diff --git a/trunk/etherpad/src/static/img/billing/mc.gif b/trunk/etherpad/src/static/img/billing/mc.gif new file mode 100644 index 0000000..2331849 Binary files /dev/null and b/trunk/etherpad/src/static/img/billing/mc.gif differ diff --git a/trunk/etherpad/src/static/img/billing/paypal.gif b/trunk/etherpad/src/static/img/billing/paypal.gif new file mode 100644 index 0000000..25333b1 Binary files /dev/null and b/trunk/etherpad/src/static/img/billing/paypal.gif differ diff --git a/trunk/etherpad/src/static/img/billing/visa.gif b/trunk/etherpad/src/static/img/billing/visa.gif new file mode 100644 index 0000000..4769f0c Binary files /dev/null and b/trunk/etherpad/src/static/img/billing/visa.gif differ diff --git a/trunk/etherpad/src/static/img/blog/posts/new-features/fullwidth.gif b/trunk/etherpad/src/static/img/blog/posts/new-features/fullwidth.gif new file mode 100644 index 0000000..73f427d Binary files /dev/null and b/trunk/etherpad/src/static/img/blog/posts/new-features/fullwidth.gif differ diff --git a/trunk/etherpad/src/static/img/blog/posts/new-features/importexport.gif b/trunk/etherpad/src/static/img/blog/posts/new-features/importexport.gif new file mode 100644 index 0000000..f0d2bac Binary files /dev/null and b/trunk/etherpad/src/static/img/blog/posts/new-features/importexport.gif differ diff --git a/trunk/etherpad/src/static/img/blog/posts/new-features/richtext.gif b/trunk/etherpad/src/static/img/blog/posts/new-features/richtext.gif new file mode 100644 index 0000000..5d03bdc Binary files /dev/null and b/trunk/etherpad/src/static/img/blog/posts/new-features/richtext.gif differ diff --git a/trunk/etherpad/src/static/img/blog/posts/new-features/viewzoom.gif b/trunk/etherpad/src/static/img/blog/posts/new-features/viewzoom.gif new file mode 100644 index 0000000..47e1caa Binary files /dev/null and b/trunk/etherpad/src/static/img/blog/posts/new-features/viewzoom.gif differ diff --git a/trunk/etherpad/src/static/img/blog/posts/pricing-survey-results.png b/trunk/etherpad/src/static/img/blog/posts/pricing-survey-results.png new file mode 100644 index 0000000..668dd72 Binary files /dev/null and b/trunk/etherpad/src/static/img/blog/posts/pricing-survey-results.png differ diff --git a/trunk/etherpad/src/static/img/blog/posts/pricing-survey.png b/trunk/etherpad/src/static/img/blog/posts/pricing-survey.png new file mode 100644 index 0000000..7289aa8 Binary files /dev/null and b/trunk/etherpad/src/static/img/blog/posts/pricing-survey.png differ diff --git a/trunk/etherpad/src/static/img/blog/posts/time-slider-screenshot.gif b/trunk/etherpad/src/static/img/blog/posts/time-slider-screenshot.gif new file mode 100644 index 0000000..782ac15 Binary files /dev/null and b/trunk/etherpad/src/static/img/blog/posts/time-slider-screenshot.gif differ diff --git a/trunk/etherpad/src/static/img/davy/bg/home-createpad.png b/trunk/etherpad/src/static/img/davy/bg/home-createpad.png new file mode 100644 index 0000000..e34e643 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/home-createpad.png differ diff --git a/trunk/etherpad/src/static/img/davy/bg/home-features-bottom.gif b/trunk/etherpad/src/static/img/davy/bg/home-features-bottom.gif new file mode 100644 index 0000000..7dc7644 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/home-features-bottom.gif differ diff --git a/trunk/etherpad/src/static/img/davy/bg/home-features-free-bottom.gif b/trunk/etherpad/src/static/img/davy/bg/home-features-free-bottom.gif new file mode 100644 index 0000000..3bd7ba8 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/home-features-free-bottom.gif differ diff --git a/trunk/etherpad/src/static/img/davy/bg/home-features-paid-top.gif b/trunk/etherpad/src/static/img/davy/bg/home-features-paid-top.gif new file mode 100644 index 0000000..faed96c Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/home-features-paid-top.gif differ diff --git a/trunk/etherpad/src/static/img/davy/bg/home-features-top.gif b/trunk/etherpad/src/static/img/davy/bg/home-features-top.gif new file mode 100644 index 0000000..2db70a6 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/home-features-top.gif differ diff --git a/trunk/etherpad/src/static/img/davy/bg/home-nav-selected.png b/trunk/etherpad/src/static/img/davy/bg/home-nav-selected.png new file mode 100644 index 0000000..01c0a99 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/home-nav-selected.png differ diff --git a/trunk/etherpad/src/static/img/davy/bg/home-screencast.png b/trunk/etherpad/src/static/img/davy/bg/home-screencast.png new file mode 100644 index 0000000..e03b83d Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/home-screencast.png differ diff --git a/trunk/etherpad/src/static/img/davy/bg/home2.png b/trunk/etherpad/src/static/img/davy/bg/home2.png new file mode 100644 index 0000000..814c339 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/home2.png differ diff --git a/trunk/etherpad/src/static/img/davy/bg/product-nav-selected-white.png b/trunk/etherpad/src/static/img/davy/bg/product-nav-selected-white.png new file mode 100644 index 0000000..4365d4a Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/product-nav-selected-white.png differ diff --git a/trunk/etherpad/src/static/img/davy/bg/product-nav-selected.png b/trunk/etherpad/src/static/img/davy/bg/product-nav-selected.png new file mode 100644 index 0000000..a3094d0 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/product-nav-selected.png differ diff --git a/trunk/etherpad/src/static/img/davy/bg/product.png b/trunk/etherpad/src/static/img/davy/bg/product.png new file mode 100644 index 0000000..5a6f6f2 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/bg/product.png differ diff --git a/trunk/etherpad/src/static/img/davy/btn/createpad-home.gif b/trunk/etherpad/src/static/img/davy/btn/createpad-home.gif new file mode 100644 index 0000000..5a46f02 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/createpad-home.gif differ diff --git a/trunk/etherpad/src/static/img/davy/btn/createpad-large.gif b/trunk/etherpad/src/static/img/davy/btn/createpad-large.gif new file mode 100644 index 0000000..9e37808 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/createpad-large.gif differ diff --git a/trunk/etherpad/src/static/img/davy/btn/createpad-small.gif b/trunk/etherpad/src/static/img/davy/btn/createpad-small.gif new file mode 100644 index 0000000..5df6502 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/createpad-small.gif differ diff --git a/trunk/etherpad/src/static/img/davy/btn/intro-screencast.png b/trunk/etherpad/src/static/img/davy/btn/intro-screencast.png new file mode 100644 index 0000000..8343ddf Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/intro-screencast.png differ diff --git a/trunk/etherpad/src/static/img/davy/btn/intro-testimonials.png b/trunk/etherpad/src/static/img/davy/btn/intro-testimonials.png new file mode 100644 index 0000000..b1df4f2 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/intro-testimonials.png differ diff --git a/trunk/etherpad/src/static/img/davy/btn/learnmore.gif b/trunk/etherpad/src/static/img/davy/btn/learnmore.gif new file mode 100644 index 0000000..9f0f612 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/learnmore.gif differ diff --git a/trunk/etherpad/src/static/img/davy/btn/signup-home-2.gif b/trunk/etherpad/src/static/img/davy/btn/signup-home-2.gif new file mode 100644 index 0000000..1aea6ff Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/signup-home-2.gif differ diff --git a/trunk/etherpad/src/static/img/davy/btn/signup-home-3.gif b/trunk/etherpad/src/static/img/davy/btn/signup-home-3.gif new file mode 100644 index 0000000..a237242 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/signup-home-3.gif differ diff --git a/trunk/etherpad/src/static/img/davy/btn/signup-home-4.gif b/trunk/etherpad/src/static/img/davy/btn/signup-home-4.gif new file mode 100644 index 0000000..966371e Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/signup-home-4.gif differ diff --git a/trunk/etherpad/src/static/img/davy/btn/signup-home.gif b/trunk/etherpad/src/static/img/davy/btn/signup-home.gif new file mode 100644 index 0000000..0a83858 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/signup-home.gif differ diff --git a/trunk/etherpad/src/static/img/davy/btn/uses-more.gif b/trunk/etherpad/src/static/img/davy/btn/uses-more.gif new file mode 100644 index 0000000..6a73bbc Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/btn/uses-more.gif differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/32/114.png b/trunk/etherpad/src/static/img/davy/gfx/32/114.png new file mode 100644 index 0000000..cbab795 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/32/114.png differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/32/15.png b/trunk/etherpad/src/static/img/davy/gfx/32/15.png new file mode 100644 index 0000000..596fd0f Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/32/15.png differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/32/65.png b/trunk/etherpad/src/static/img/davy/gfx/32/65.png new file mode 100644 index 0000000..c331ee1 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/32/65.png differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/32/78.png b/trunk/etherpad/src/static/img/davy/gfx/32/78.png new file mode 100644 index 0000000..fae3f29 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/32/78.png differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/bullet.gif b/trunk/etherpad/src/static/img/davy/gfx/bullet.gif new file mode 100644 index 0000000..cb2e123 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/bullet.gif differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/home-logo2.gif b/trunk/etherpad/src/static/img/davy/gfx/home-logo2.gif new file mode 100644 index 0000000..24b67bd Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/home-logo2.gif differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/home-screencast.png b/trunk/etherpad/src/static/img/davy/gfx/home-screencast.png new file mode 100644 index 0000000..b5516d3 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/home-screencast.png differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/plane.gif b/trunk/etherpad/src/static/img/davy/gfx/plane.gif new file mode 100644 index 0000000..cf3226e Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/plane.gif differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/product-logo.gif b/trunk/etherpad/src/static/img/davy/gfx/product-logo.gif new file mode 100644 index 0000000..72975bb Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/product-logo.gif differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/screenshot.gif b/trunk/etherpad/src/static/img/davy/gfx/screenshot.gif new file mode 100644 index 0000000..ba87f99 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/screenshot.gif differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/use-meetings.gif b/trunk/etherpad/src/static/img/davy/gfx/use-meetings.gif new file mode 100644 index 0000000..3451969 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/use-meetings.gif differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/use-meetings.png b/trunk/etherpad/src/static/img/davy/gfx/use-meetings.png new file mode 100644 index 0000000..4e41581 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/use-meetings.png differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/use-programming.gif b/trunk/etherpad/src/static/img/davy/gfx/use-programming.gif new file mode 100644 index 0000000..795e7fd Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/use-programming.gif differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/use-programming.png b/trunk/etherpad/src/static/img/davy/gfx/use-programming.png new file mode 100644 index 0000000..825095d Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/use-programming.png differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/use-writing.gif b/trunk/etherpad/src/static/img/davy/gfx/use-writing.gif new file mode 100644 index 0000000..0e2d11b Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/use-writing.gif differ diff --git a/trunk/etherpad/src/static/img/davy/gfx/use-writing.png b/trunk/etherpad/src/static/img/davy/gfx/use-writing.png new file mode 100644 index 0000000..c6e267c Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/gfx/use-writing.png differ diff --git a/trunk/etherpad/src/static/img/davy/txt/home-button.gif b/trunk/etherpad/src/static/img/davy/txt/home-button.gif new file mode 100644 index 0000000..bdf4945 Binary files /dev/null and b/trunk/etherpad/src/static/img/davy/txt/home-button.gif differ diff --git a/trunk/etherpad/src/static/img/featuretour/code.gif b/trunk/etherpad/src/static/img/featuretour/code.gif new file mode 100644 index 0000000..abff862 Binary files /dev/null and b/trunk/etherpad/src/static/img/featuretour/code.gif differ diff --git a/trunk/etherpad/src/static/img/featuretour/edits.gif b/trunk/etherpad/src/static/img/featuretour/edits.gif new file mode 100644 index 0000000..d38f83a Binary files /dev/null and b/trunk/etherpad/src/static/img/featuretour/edits.gif differ diff --git a/trunk/etherpad/src/static/img/featuretour/editsandusers.gif b/trunk/etherpad/src/static/img/featuretour/editsandusers.gif new file mode 100644 index 0000000..90c36f8 Binary files /dev/null and b/trunk/etherpad/src/static/img/featuretour/editsandusers.gif differ diff --git a/trunk/etherpad/src/static/img/featuretour/padlock.png b/trunk/etherpad/src/static/img/featuretour/padlock.png new file mode 100644 index 0000000..f6d6c05 Binary files /dev/null and b/trunk/etherpad/src/static/img/featuretour/padlock.png differ diff --git a/trunk/etherpad/src/static/img/featuretour/revisions.gif b/trunk/etherpad/src/static/img/featuretour/revisions.gif new file mode 100644 index 0000000..a0d8220 Binary files /dev/null and b/trunk/etherpad/src/static/img/featuretour/revisions.gif differ diff --git a/trunk/etherpad/src/static/img/featuretour/users.gif b/trunk/etherpad/src/static/img/featuretour/users.gif new file mode 100644 index 0000000..48b4470 Binary files /dev/null and b/trunk/etherpad/src/static/img/featuretour/users.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/framedheaderback.gif b/trunk/etherpad/src/static/img/feb09/framedheaderback.gif new file mode 100644 index 0000000..5b17b9f Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/framedheaderback.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/framedheaderlogo.gif b/trunk/etherpad/src/static/img/feb09/framedheaderlogo.gif new file mode 100644 index 0000000..01de8cd Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/framedheaderlogo.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/home_firstp.gif b/trunk/etherpad/src/static/img/feb09/home_firstp.gif new file mode 100644 index 0000000..af0a2cd Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/home_firstp.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/home_firstp.png b/trunk/etherpad/src/static/img/feb09/home_firstp.png new file mode 100644 index 0000000..cb7fc8c Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/home_firstp.png differ diff --git a/trunk/etherpad/src/static/img/feb09/home_firstp2.gif b/trunk/etherpad/src/static/img/feb09/home_firstp2.gif new file mode 100644 index 0000000..9e0ed1e Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/home_firstp2.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/home_h1.gif b/trunk/etherpad/src/static/img/feb09/home_h1.gif new file mode 100644 index 0000000..1fe08cb Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/home_h1.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/home_h1.png b/trunk/etherpad/src/static/img/feb09/home_h1.png new file mode 100644 index 0000000..1b5784d Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/home_h1.png differ diff --git a/trunk/etherpad/src/static/img/feb09/home_newpadbutton.gif b/trunk/etherpad/src/static/img/feb09/home_newpadbutton.gif new file mode 100644 index 0000000..8706407 Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/home_newpadbutton.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/home_newpadbutton.png b/trunk/etherpad/src/static/img/feb09/home_newpadbutton.png new file mode 100644 index 0000000..ead253c Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/home_newpadbutton.png differ diff --git a/trunk/etherpad/src/static/img/feb09/home_newpadbutton2.gif b/trunk/etherpad/src/static/img/feb09/home_newpadbutton2.gif new file mode 100644 index 0000000..ba5d478 Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/home_newpadbutton2.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/home_newpadbutton_eepnet.gif b/trunk/etherpad/src/static/img/feb09/home_newpadbutton_eepnet.gif new file mode 100644 index 0000000..a365351 Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/home_newpadbutton_eepnet.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/hometop_back.gif b/trunk/etherpad/src/static/img/feb09/hometop_back.gif new file mode 100644 index 0000000..7cdd406 Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/hometop_back.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/nav1.gif b/trunk/etherpad/src/static/img/feb09/nav1.gif new file mode 100644 index 0000000..4af4869 Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/nav1.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/nav1_back.gif b/trunk/etherpad/src/static/img/feb09/nav1_back.gif new file mode 100644 index 0000000..d2db7ee Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/nav1_back.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/nav2.gif b/trunk/etherpad/src/static/img/feb09/nav2.gif new file mode 100644 index 0000000..f43a40e Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/nav2.gif differ diff --git a/trunk/etherpad/src/static/img/feb09/screencast.gif b/trunk/etherpad/src/static/img/feb09/screencast.gif new file mode 100644 index 0000000..3819082 Binary files /dev/null and b/trunk/etherpad/src/static/img/feb09/screencast.gif differ diff --git a/trunk/etherpad/src/static/img/home/etherpad-mainheader1.jpg b/trunk/etherpad/src/static/img/home/etherpad-mainheader1.jpg new file mode 100644 index 0000000..ed7e830 Binary files /dev/null and b/trunk/etherpad/src/static/img/home/etherpad-mainheader1.jpg differ diff --git a/trunk/etherpad/src/static/img/home/headergradient.gif b/trunk/etherpad/src/static/img/home/headergradient.gif new file mode 100644 index 0000000..d6526dc Binary files /dev/null and b/trunk/etherpad/src/static/img/home/headergradient.gif differ diff --git a/trunk/etherpad/src/static/img/home/homeheader1.jpg b/trunk/etherpad/src/static/img/home/homeheader1.jpg new file mode 100644 index 0000000..3436158 Binary files /dev/null and b/trunk/etherpad/src/static/img/home/homeheader1.jpg differ diff --git a/trunk/etherpad/src/static/img/home/homeheader2.jpg b/trunk/etherpad/src/static/img/home/homeheader2.jpg new file mode 100644 index 0000000..e19ba41 Binary files /dev/null and b/trunk/etherpad/src/static/img/home/homeheader2.jpg differ diff --git a/trunk/etherpad/src/static/img/home/leftgrad.gif b/trunk/etherpad/src/static/img/home/leftgrad.gif new file mode 100644 index 0000000..ff3931a Binary files /dev/null and b/trunk/etherpad/src/static/img/home/leftgrad.gif differ diff --git a/trunk/etherpad/src/static/img/home/pencilpaperback.png b/trunk/etherpad/src/static/img/home/pencilpaperback.png new file mode 100644 index 0000000..e0d2f9d Binary files /dev/null and b/trunk/etherpad/src/static/img/home/pencilpaperback.png differ diff --git a/trunk/etherpad/src/static/img/home/screencapture1.gif b/trunk/etherpad/src/static/img/home/screencapture1.gif new file mode 100644 index 0000000..7c3ce5a Binary files /dev/null and b/trunk/etherpad/src/static/img/home/screencapture1.gif differ diff --git a/trunk/etherpad/src/static/img/home/underdevicon.gif b/trunk/etherpad/src/static/img/home/underdevicon.gif new file mode 100644 index 0000000..5b75fd1 Binary files /dev/null and b/trunk/etherpad/src/static/img/home/underdevicon.gif differ diff --git a/trunk/etherpad/src/static/img/icon/downarrow.gif b/trunk/etherpad/src/static/img/icon/downarrow.gif new file mode 100644 index 0000000..6389439 Binary files /dev/null and b/trunk/etherpad/src/static/img/icon/downarrow.gif differ diff --git a/trunk/etherpad/src/static/img/icon/feed.gif b/trunk/etherpad/src/static/img/icon/feed.gif new file mode 100644 index 0000000..fb9b66c Binary files /dev/null and b/trunk/etherpad/src/static/img/icon/feed.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/backgrad.gif b/trunk/etherpad/src/static/img/jun09/pad/backgrad.gif new file mode 100644 index 0000000..8fee1a5 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/backgrad.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/bottomareagfx.gif b/trunk/etherpad/src/static/img/jun09/pad/bottomareagfx.gif new file mode 100644 index 0000000..c499a62 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/bottomareagfx.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/colorpicker.gif b/trunk/etherpad/src/static/img/jun09/pad/colorpicker.gif new file mode 100644 index 0000000..602e7e6 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/colorpicker.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/connectingbar.gif b/trunk/etherpad/src/static/img/jun09/pad/connectingbar.gif new file mode 100644 index 0000000..34f54e9 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/connectingbar.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/connectionindicator.gif b/trunk/etherpad/src/static/img/jun09/pad/connectionindicator.gif new file mode 100644 index 0000000..ecc270d Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/connectionindicator.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/docbarstates.png b/trunk/etherpad/src/static/img/jun09/pad/docbarstates.png new file mode 100644 index 0000000..13a5913 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/docbarstates.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/docbarstates2.png b/trunk/etherpad/src/static/img/jun09/pad/docbarstates2.png new file mode 100644 index 0000000..9e26b1d Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/docbarstates2.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/docbarstates3.png b/trunk/etherpad/src/static/img/jun09/pad/docbarstates3.png new file mode 100644 index 0000000..cdc6a45 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/docbarstates3.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/docpaneledge.png b/trunk/etherpad/src/static/img/jun09/pad/docpaneledge.png new file mode 100644 index 0000000..de22d6a Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/docpaneledge.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/docpaneledge2.png b/trunk/etherpad/src/static/img/jun09/pad/docpaneledge2.png new file mode 100644 index 0000000..c119c74 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/docpaneledge2.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/docpanelmiddle.png b/trunk/etherpad/src/static/img/jun09/pad/docpanelmiddle.png new file mode 100644 index 0000000..9290e86 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/docpanelmiddle.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/docpanelmiddle2.png b/trunk/etherpad/src/static/img/jun09/pad/docpanelmiddle2.png new file mode 100644 index 0000000..d251c23 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/docpanelmiddle2.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/editbar.gif b/trunk/etherpad/src/static/img/jun09/pad/editbar.gif new file mode 100644 index 0000000..eb7100c Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/editbar.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/editbar2.gif b/trunk/etherpad/src/static/img/jun09/pad/editbar2.gif new file mode 100644 index 0000000..d222fb8 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/editbar2.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/editbar3.png b/trunk/etherpad/src/static/img/jun09/pad/editbar3.png new file mode 100644 index 0000000..ff65146 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/editbar3.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/editbar3.xcf b/trunk/etherpad/src/static/img/jun09/pad/editbar3.xcf new file mode 100644 index 0000000..91cae20 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/editbar3.xcf differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/editbarback.gif b/trunk/etherpad/src/static/img/jun09/pad/editbarback.gif new file mode 100644 index 0000000..ab51802 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/editbarback.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/feedbackbox2.gif b/trunk/etherpad/src/static/img/jun09/pad/feedbackbox2.gif new file mode 100644 index 0000000..f1b8f5b Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/feedbackbox2.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/fileicons.gif b/trunk/etherpad/src/static/img/jun09/pad/fileicons.gif new file mode 100644 index 0000000..26f6388 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/fileicons.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/hdraggie.gif b/trunk/etherpad/src/static/img/jun09/pad/hdraggie.gif new file mode 100644 index 0000000..0a6fe3e Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/hdraggie.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/inviteshare.gif b/trunk/etherpad/src/static/img/jun09/pad/inviteshare.gif new file mode 100644 index 0000000..55345e5 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/inviteshare.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/inviteshare2.gif b/trunk/etherpad/src/static/img/jun09/pad/inviteshare2.gif new file mode 100644 index 0000000..98d4c85 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/inviteshare2.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/layoutbuttons.gif b/trunk/etherpad/src/static/img/jun09/pad/layoutbuttons.gif new file mode 100644 index 0000000..ea43432 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/layoutbuttons.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/overlay.png b/trunk/etherpad/src/static/img/jun09/pad/overlay.png new file mode 100644 index 0000000..46abff3 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/overlay.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/overlay2.png b/trunk/etherpad/src/static/img/jun09/pad/overlay2.png new file mode 100644 index 0000000..c3d3f1c Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/overlay2.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/padtop.gif b/trunk/etherpad/src/static/img/jun09/pad/padtop.gif new file mode 100644 index 0000000..9e77b07 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/padtop.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/padtop2.gif b/trunk/etherpad/src/static/img/jun09/pad/padtop2.gif new file mode 100644 index 0000000..1e3d8c2 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/padtop2.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/padtop3.gif b/trunk/etherpad/src/static/img/jun09/pad/padtop3.gif new file mode 100644 index 0000000..b6bc589 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/padtop3.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/padtop4.gif b/trunk/etherpad/src/static/img/jun09/pad/padtop4.gif new file mode 100644 index 0000000..d896250 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/padtop4.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/padtop4.png b/trunk/etherpad/src/static/img/jun09/pad/padtop4.png new file mode 100644 index 0000000..11c4595 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/padtop4.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/padtop4.xcf b/trunk/etherpad/src/static/img/jun09/pad/padtop4.xcf new file mode 100644 index 0000000..1e735aa Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/padtop4.xcf differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/padtop5.png b/trunk/etherpad/src/static/img/jun09/pad/padtop5.png new file mode 100644 index 0000000..6546509 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/padtop5.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/padtop5.xcf b/trunk/etherpad/src/static/img/jun09/pad/padtop5.xcf new file mode 100644 index 0000000..4abbd02 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/padtop5.xcf differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/padtopback.gif b/trunk/etherpad/src/static/img/jun09/pad/padtopback.gif new file mode 100644 index 0000000..335979b Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/padtopback.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/padtopback2.gif b/trunk/etherpad/src/static/img/jun09/pad/padtopback2.gif new file mode 100644 index 0000000..eb92358 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/padtopback2.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/protop.png b/trunk/etherpad/src/static/img/jun09/pad/protop.png new file mode 100644 index 0000000..740d903 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/protop.png differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/protop.xcf b/trunk/etherpad/src/static/img/jun09/pad/protop.xcf new file mode 100644 index 0000000..4f3abfd Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/protop.xcf differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/public.gif b/trunk/etherpad/src/static/img/jun09/pad/public.gif new file mode 100644 index 0000000..e6f09c7 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/public.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/savedrevarrows.gif b/trunk/etherpad/src/static/img/jun09/pad/savedrevarrows.gif new file mode 100644 index 0000000..2aa2e41 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/savedrevarrows.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/savedrevsgfx2.gif b/trunk/etherpad/src/static/img/jun09/pad/savedrevsgfx2.gif new file mode 100644 index 0000000..45c3459 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/savedrevsgfx2.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/sharebox2.gif b/trunk/etherpad/src/static/img/jun09/pad/sharebox2.gif new file mode 100644 index 0000000..8e89925 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/sharebox2.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/sharebox3.gif b/trunk/etherpad/src/static/img/jun09/pad/sharebox3.gif new file mode 100644 index 0000000..6f8f03c Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/sharebox3.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/sharebox4.gif b/trunk/etherpad/src/static/img/jun09/pad/sharebox4.gif new file mode 100644 index 0000000..eccaa7e Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/sharebox4.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/sharedistri.gif b/trunk/etherpad/src/static/img/jun09/pad/sharedistri.gif new file mode 100644 index 0000000..8eb5891 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/sharedistri.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/syncdone.gif b/trunk/etherpad/src/static/img/jun09/pad/syncdone.gif new file mode 100644 index 0000000..e4d971b Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/syncdone.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/syncing.gif b/trunk/etherpad/src/static/img/jun09/pad/syncing.gif new file mode 100644 index 0000000..bbc731f Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/syncing.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/syncing2.gif b/trunk/etherpad/src/static/img/jun09/pad/syncing2.gif new file mode 100644 index 0000000..29470e3 Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/syncing2.gif differ diff --git a/trunk/etherpad/src/static/img/jun09/pad/viewbargfx.gif b/trunk/etherpad/src/static/img/jun09/pad/viewbargfx.gif new file mode 100644 index 0000000..396483a Binary files /dev/null and b/trunk/etherpad/src/static/img/jun09/pad/viewbargfx.gif differ diff --git a/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-cyan-menu-item-hover.gif b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-cyan-menu-item-hover.gif new file mode 100644 index 0000000..d0e428e Binary files /dev/null and b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-cyan-menu-item-hover.gif differ diff --git a/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-menu-item-hover.gif b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-menu-item-hover.gif new file mode 100644 index 0000000..8240ba3 Binary files /dev/null and b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-menu-item-hover.gif differ diff --git a/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-semitransparent-menu-item-hover.png b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-semitransparent-menu-item-hover.png new file mode 100644 index 0000000..6314d53 Binary files /dev/null and b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-gloss-semitransparent-menu-item-hover.png differ diff --git a/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-human-menu-item-hover.gif b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-human-menu-item-hover.gif new file mode 100644 index 0000000..7e70aae Binary files /dev/null and b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-human-menu-item-hover.gif differ diff --git a/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-osx-menu-item-hover.gif b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-osx-menu-item-hover.gif new file mode 100644 index 0000000..aa802e0 Binary files /dev/null and b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-osx-menu-item-hover.gif differ diff --git a/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-bg.gif b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-bg.gif new file mode 100644 index 0000000..565e771 Binary files /dev/null and b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-bg.gif differ diff --git a/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-menu-item-hover.gif b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-menu-item-hover.gif new file mode 100644 index 0000000..2825eb1 Binary files /dev/null and b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-vista-menu-item-hover.gif differ diff --git a/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-xp-bg.gif b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-xp-bg.gif new file mode 100644 index 0000000..11b238c Binary files /dev/null and b/trunk/etherpad/src/static/img/lib/jquery.contextmenu.images/cmenu-xp-bg.gif differ diff --git a/trunk/etherpad/src/static/img/may09/bold.gif b/trunk/etherpad/src/static/img/may09/bold.gif new file mode 100644 index 0000000..49e0d17 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/bold.gif differ diff --git a/trunk/etherpad/src/static/img/may09/doc.gif b/trunk/etherpad/src/static/img/may09/doc.gif new file mode 100644 index 0000000..2b62080 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/doc.gif differ diff --git a/trunk/etherpad/src/static/img/may09/doc.png b/trunk/etherpad/src/static/img/may09/doc.png new file mode 100644 index 0000000..b374d0d Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/doc.png differ diff --git a/trunk/etherpad/src/static/img/may09/html.gif b/trunk/etherpad/src/static/img/may09/html.gif new file mode 100644 index 0000000..f7837e5 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/html.gif differ diff --git a/trunk/etherpad/src/static/img/may09/html.png b/trunk/etherpad/src/static/img/may09/html.png new file mode 100644 index 0000000..917f1a4 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/html.png differ diff --git a/trunk/etherpad/src/static/img/may09/italic.gif b/trunk/etherpad/src/static/img/may09/italic.gif new file mode 100644 index 0000000..2876ed9 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/italic.gif differ diff --git a/trunk/etherpad/src/static/img/may09/leftarrow.gif b/trunk/etherpad/src/static/img/may09/leftarrow.gif new file mode 100644 index 0000000..c26475f Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/leftarrow.gif differ diff --git a/trunk/etherpad/src/static/img/may09/leftarrow2.gif b/trunk/etherpad/src/static/img/may09/leftarrow2.gif new file mode 100644 index 0000000..63bbddc Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/leftarrow2.gif differ diff --git a/trunk/etherpad/src/static/img/may09/link.gif b/trunk/etherpad/src/static/img/may09/link.gif new file mode 100644 index 0000000..44ffdd2 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/link.gif differ diff --git a/trunk/etherpad/src/static/img/may09/link.png b/trunk/etherpad/src/static/img/may09/link.png new file mode 100644 index 0000000..4e46fcb Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/link.png differ diff --git a/trunk/etherpad/src/static/img/may09/odt.gif b/trunk/etherpad/src/static/img/may09/odt.gif new file mode 100644 index 0000000..be42352 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/odt.gif differ diff --git a/trunk/etherpad/src/static/img/may09/odt.png b/trunk/etherpad/src/static/img/may09/odt.png new file mode 100644 index 0000000..5d630db Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/odt.png differ diff --git a/trunk/etherpad/src/static/img/may09/padlock.gif b/trunk/etherpad/src/static/img/may09/padlock.gif new file mode 100644 index 0000000..167bf75 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/padlock.gif differ diff --git a/trunk/etherpad/src/static/img/may09/padlockopen.gif b/trunk/etherpad/src/static/img/may09/padlockopen.gif new file mode 100644 index 0000000..0aaf413 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/padlockopen.gif differ diff --git a/trunk/etherpad/src/static/img/may09/passwordlocked.gif b/trunk/etherpad/src/static/img/may09/passwordlocked.gif new file mode 100644 index 0000000..167bf75 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/passwordlocked.gif differ diff --git a/trunk/etherpad/src/static/img/may09/passwordlocked_cropped.gif b/trunk/etherpad/src/static/img/may09/passwordlocked_cropped.gif new file mode 100644 index 0000000..969a8bd Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/passwordlocked_cropped.gif differ diff --git a/trunk/etherpad/src/static/img/may09/passwordnone.gif b/trunk/etherpad/src/static/img/may09/passwordnone.gif new file mode 100644 index 0000000..61e38d9 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/passwordnone.gif differ diff --git a/trunk/etherpad/src/static/img/may09/paypal.gif b/trunk/etherpad/src/static/img/may09/paypal.gif new file mode 100644 index 0000000..add5454 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/paypal.gif differ diff --git a/trunk/etherpad/src/static/img/may09/pdf.gif b/trunk/etherpad/src/static/img/may09/pdf.gif new file mode 100644 index 0000000..1614d2c Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/pdf.gif differ diff --git a/trunk/etherpad/src/static/img/may09/pdf.png b/trunk/etherpad/src/static/img/may09/pdf.png new file mode 100644 index 0000000..ac4fa75 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/pdf.png differ diff --git a/trunk/etherpad/src/static/img/may09/redo.gif b/trunk/etherpad/src/static/img/may09/redo.gif new file mode 100644 index 0000000..bb07b8e Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/redo.gif differ diff --git a/trunk/etherpad/src/static/img/may09/txt.gif b/trunk/etherpad/src/static/img/may09/txt.gif new file mode 100644 index 0000000..c3f026e Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/txt.gif differ diff --git a/trunk/etherpad/src/static/img/may09/txt.png b/trunk/etherpad/src/static/img/may09/txt.png new file mode 100644 index 0000000..f830cc6 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/txt.png differ diff --git a/trunk/etherpad/src/static/img/may09/underline.gif b/trunk/etherpad/src/static/img/may09/underline.gif new file mode 100644 index 0000000..1d1f573 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/underline.gif differ diff --git a/trunk/etherpad/src/static/img/may09/undo.gif b/trunk/etherpad/src/static/img/may09/undo.gif new file mode 100644 index 0000000..273e9e6 Binary files /dev/null and b/trunk/etherpad/src/static/img/may09/undo.gif differ diff --git a/trunk/etherpad/src/static/img/miniplane.gif b/trunk/etherpad/src/static/img/miniplane.gif new file mode 100644 index 0000000..aeaacb0 Binary files /dev/null and b/trunk/etherpad/src/static/img/miniplane.gif differ diff --git a/trunk/etherpad/src/static/img/misc/diagnostic-links.gif b/trunk/etherpad/src/static/img/misc/diagnostic-links.gif new file mode 100644 index 0000000..f76669a Binary files /dev/null and b/trunk/etherpad/src/static/img/misc/diagnostic-links.gif differ diff --git a/trunk/etherpad/src/static/img/misc/status-ball.gif b/trunk/etherpad/src/static/img/misc/status-ball.gif new file mode 100644 index 0000000..085ccae Binary files /dev/null and b/trunk/etherpad/src/static/img/misc/status-ball.gif differ diff --git a/trunk/etherpad/src/static/img/misc/traclogo.gif b/trunk/etherpad/src/static/img/misc/traclogo.gif new file mode 100644 index 0000000..7ee31d1 Binary files /dev/null and b/trunk/etherpad/src/static/img/misc/traclogo.gif differ diff --git a/trunk/etherpad/src/static/img/oct/atlonglast.gif b/trunk/etherpad/src/static/img/oct/atlonglast.gif new file mode 100644 index 0000000..88e1c98 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/atlonglast.gif differ diff --git a/trunk/etherpad/src/static/img/oct/banner1.jpg b/trunk/etherpad/src/static/img/oct/banner1.jpg new file mode 100644 index 0000000..431d2ba Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/banner1.jpg differ diff --git a/trunk/etherpad/src/static/img/oct/banner2.jpg b/trunk/etherpad/src/static/img/oct/banner2.jpg new file mode 100644 index 0000000..50570a8 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/banner2.jpg differ diff --git a/trunk/etherpad/src/static/img/oct/banner3.jpg b/trunk/etherpad/src/static/img/oct/banner3.jpg new file mode 100644 index 0000000..c0260a3 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/banner3.jpg differ diff --git a/trunk/etherpad/src/static/img/oct/banner4.jpg b/trunk/etherpad/src/static/img/oct/banner4.jpg new file mode 100644 index 0000000..e1593b7 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/banner4.jpg differ diff --git a/trunk/etherpad/src/static/img/oct/banner5.gif b/trunk/etherpad/src/static/img/oct/banner5.gif new file mode 100644 index 0000000..82c8eee Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/banner5.gif differ diff --git a/trunk/etherpad/src/static/img/oct/banner6.gif b/trunk/etherpad/src/static/img/oct/banner6.gif new file mode 100644 index 0000000..9016ed8 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/banner6.gif differ diff --git a/trunk/etherpad/src/static/img/oct/banner7.gif b/trunk/etherpad/src/static/img/oct/banner7.gif new file mode 100644 index 0000000..a999f93 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/banner7.gif differ diff --git a/trunk/etherpad/src/static/img/oct/banner8.gif b/trunk/etherpad/src/static/img/oct/banner8.gif new file mode 100644 index 0000000..cc2c436 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/banner8.gif differ diff --git a/trunk/etherpad/src/static/img/oct/banner9.gif b/trunk/etherpad/src/static/img/oct/banner9.gif new file mode 100644 index 0000000..e7d9043 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/banner9.gif differ diff --git a/trunk/etherpad/src/static/img/oct/bannerback5.gif b/trunk/etherpad/src/static/img/oct/bannerback5.gif new file mode 100644 index 0000000..d748357 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/bannerback5.gif differ diff --git a/trunk/etherpad/src/static/img/oct/bannerback6.gif b/trunk/etherpad/src/static/img/oct/bannerback6.gif new file mode 100644 index 0000000..416314f Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/bannerback6.gif differ diff --git a/trunk/etherpad/src/static/img/oct/bodyback1.gif b/trunk/etherpad/src/static/img/oct/bodyback1.gif new file mode 100644 index 0000000..39d3fd7 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/bodyback1.gif differ diff --git a/trunk/etherpad/src/static/img/oct/bodyback2.gif b/trunk/etherpad/src/static/img/oct/bodyback2.gif new file mode 100644 index 0000000..5bbdad8 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/bodyback2.gif differ diff --git a/trunk/etherpad/src/static/img/oct/bodyback3.gif b/trunk/etherpad/src/static/img/oct/bodyback3.gif new file mode 100644 index 0000000..a04faf4 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/bodyback3.gif differ diff --git a/trunk/etherpad/src/static/img/oct/bodyback4.gif b/trunk/etherpad/src/static/img/oct/bodyback4.gif new file mode 100644 index 0000000..a875cd6 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/bodyback4.gif differ diff --git a/trunk/etherpad/src/static/img/oct/bodyback5.gif b/trunk/etherpad/src/static/img/oct/bodyback5.gif new file mode 100644 index 0000000..66f67ce Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/bodyback5.gif differ diff --git a/trunk/etherpad/src/static/img/oct/bodybacktop1.gif b/trunk/etherpad/src/static/img/oct/bodybacktop1.gif new file mode 100644 index 0000000..20a9261 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/bodybacktop1.gif differ diff --git a/trunk/etherpad/src/static/img/oct/computers.gif b/trunk/etherpad/src/static/img/oct/computers.gif new file mode 100644 index 0000000..c8f2aff Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/computers.gif differ diff --git a/trunk/etherpad/src/static/img/oct/computers2.gif b/trunk/etherpad/src/static/img/oct/computers2.gif new file mode 100644 index 0000000..bd8d133 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/computers2.gif differ diff --git a/trunk/etherpad/src/static/img/oct/glossyblue.gif b/trunk/etherpad/src/static/img/oct/glossyblue.gif new file mode 100644 index 0000000..eed77e7 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/glossyblue.gif differ diff --git a/trunk/etherpad/src/static/img/oct/glossyblue2.gif b/trunk/etherpad/src/static/img/oct/glossyblue2.gif new file mode 100644 index 0000000..c323ebb Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/glossyblue2.gif differ diff --git a/trunk/etherpad/src/static/img/oct/glossyblueh.gif b/trunk/etherpad/src/static/img/oct/glossyblueh.gif new file mode 100644 index 0000000..e56ceeb Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/glossyblueh.gif differ diff --git a/trunk/etherpad/src/static/img/oct/insetrect.gif b/trunk/etherpad/src/static/img/oct/insetrect.gif new file mode 100644 index 0000000..1d8124d Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/insetrect.gif differ diff --git a/trunk/etherpad/src/static/img/oct/minilogo1-05e.gif b/trunk/etherpad/src/static/img/oct/minilogo1-05e.gif new file mode 100644 index 0000000..a09ef16 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minilogo1-05e.gif differ diff --git a/trunk/etherpad/src/static/img/oct/minilogo1-07f.gif b/trunk/etherpad/src/static/img/oct/minilogo1-07f.gif new file mode 100644 index 0000000..8565272 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minilogo1-07f.gif differ diff --git a/trunk/etherpad/src/static/img/oct/minilogo3.jpg b/trunk/etherpad/src/static/img/oct/minilogo3.jpg new file mode 100644 index 0000000..d0cd89b Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minilogo3.jpg differ diff --git a/trunk/etherpad/src/static/img/oct/minitopback1.gif b/trunk/etherpad/src/static/img/oct/minitopback1.gif new file mode 100644 index 0000000..da8f575 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minitopback1.gif differ diff --git a/trunk/etherpad/src/static/img/oct/minitopback2.gif b/trunk/etherpad/src/static/img/oct/minitopback2.gif new file mode 100644 index 0000000..a1f43ab Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minitopback2.gif differ diff --git a/trunk/etherpad/src/static/img/oct/minitopbar1-05e.gif b/trunk/etherpad/src/static/img/oct/minitopbar1-05e.gif new file mode 100644 index 0000000..1115749 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minitopbar1-05e.gif differ diff --git a/trunk/etherpad/src/static/img/oct/minitopbar2-05e.gif b/trunk/etherpad/src/static/img/oct/minitopbar2-05e.gif new file mode 100644 index 0000000..2c5d10f Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minitopbar2-05e.gif differ diff --git a/trunk/etherpad/src/static/img/oct/minitopbar2-07f.gif b/trunk/etherpad/src/static/img/oct/minitopbar2-07f.gif new file mode 100644 index 0000000..5687aed Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minitopbar2-07f.gif differ diff --git a/trunk/etherpad/src/static/img/oct/minitopbar3.jpg b/trunk/etherpad/src/static/img/oct/minitopbar3.jpg new file mode 100644 index 0000000..d0cd89b Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minitopbar3.jpg differ diff --git a/trunk/etherpad/src/static/img/oct/minitopbar4.gif b/trunk/etherpad/src/static/img/oct/minitopbar4.gif new file mode 100644 index 0000000..bf7aec9 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minitopbar4.gif differ diff --git a/trunk/etherpad/src/static/img/oct/minitoplogo1.gif b/trunk/etherpad/src/static/img/oct/minitoplogo1.gif new file mode 100644 index 0000000..6317c0f Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minitoplogo1.gif differ diff --git a/trunk/etherpad/src/static/img/oct/minitoplogo2.gif b/trunk/etherpad/src/static/img/oct/minitoplogo2.gif new file mode 100644 index 0000000..bbb5e21 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/minitoplogo2.gif differ diff --git a/trunk/etherpad/src/static/img/oct/newpadmain.gif b/trunk/etherpad/src/static/img/oct/newpadmain.gif new file mode 100644 index 0000000..6427037 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/newpadmain.gif differ diff --git a/trunk/etherpad/src/static/img/oct/newpadmainback.gif b/trunk/etherpad/src/static/img/oct/newpadmainback.gif new file mode 100644 index 0000000..2016864 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/newpadmainback.gif differ diff --git a/trunk/etherpad/src/static/img/oct/newpadmainbackh.gif b/trunk/etherpad/src/static/img/oct/newpadmainbackh.gif new file mode 100644 index 0000000..3060634 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/newpadmainbackh.gif differ diff --git a/trunk/etherpad/src/static/img/oct/pageshot.png b/trunk/etherpad/src/static/img/oct/pageshot.png new file mode 100644 index 0000000..cb86428 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/pageshot.png differ diff --git a/trunk/etherpad/src/static/img/oct/pageshotmini.png b/trunk/etherpad/src/static/img/oct/pageshotmini.png new file mode 100644 index 0000000..0f8a9d0 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/pageshotmini.png differ diff --git a/trunk/etherpad/src/static/img/oct/sidehead-gradhilite.gif b/trunk/etherpad/src/static/img/oct/sidehead-gradhilite.gif new file mode 100644 index 0000000..5469d87 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/sidehead-gradhilite.gif differ diff --git a/trunk/etherpad/src/static/img/oct/tinytriangle.gif b/trunk/etherpad/src/static/img/oct/tinytriangle.gif new file mode 100644 index 0000000..1821e3b Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/tinytriangle.gif differ diff --git a/trunk/etherpad/src/static/img/oct/topnav1.gif b/trunk/etherpad/src/static/img/oct/topnav1.gif new file mode 100644 index 0000000..d801c59 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/topnav1.gif differ diff --git a/trunk/etherpad/src/static/img/oct/topnav2.gif b/trunk/etherpad/src/static/img/oct/topnav2.gif new file mode 100644 index 0000000..c1ab5c5 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/topnav2.gif differ diff --git a/trunk/etherpad/src/static/img/oct/topnav3.gif b/trunk/etherpad/src/static/img/oct/topnav3.gif new file mode 100644 index 0000000..fa25e75 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/topnav3.gif differ diff --git a/trunk/etherpad/src/static/img/oct/topnav4.gif b/trunk/etherpad/src/static/img/oct/topnav4.gif new file mode 100644 index 0000000..1f4c714 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/topnav4.gif differ diff --git a/trunk/etherpad/src/static/img/oct/topnav5.gif b/trunk/etherpad/src/static/img/oct/topnav5.gif new file mode 100644 index 0000000..fa8b737 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/topnav5.gif differ diff --git a/trunk/etherpad/src/static/img/oct/topnav6.gif b/trunk/etherpad/src/static/img/oct/topnav6.gif new file mode 100644 index 0000000..e0e6815 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/topnav6.gif differ diff --git a/trunk/etherpad/src/static/img/oct/topnavback1.gif b/trunk/etherpad/src/static/img/oct/topnavback1.gif new file mode 100644 index 0000000..55103a3 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/topnavback1.gif differ diff --git a/trunk/etherpad/src/static/img/oct/topnavback2.gif b/trunk/etherpad/src/static/img/oct/topnavback2.gif new file mode 100644 index 0000000..9b4bdca Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/topnavback2.gif differ diff --git a/trunk/etherpad/src/static/img/oct/topnavback3.gif b/trunk/etherpad/src/static/img/oct/topnavback3.gif new file mode 100644 index 0000000..327cba1 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/topnavback3.gif differ diff --git a/trunk/etherpad/src/static/img/oct/usecasesnavdown.gif b/trunk/etherpad/src/static/img/oct/usecasesnavdown.gif new file mode 100644 index 0000000..c8fc7df Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/usecasesnavdown.gif differ diff --git a/trunk/etherpad/src/static/img/oct/usecasesnavdownh.gif b/trunk/etherpad/src/static/img/oct/usecasesnavdownh.gif new file mode 100644 index 0000000..e1fa3a5 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/usecasesnavdownh.gif differ diff --git a/trunk/etherpad/src/static/img/oct/usecasesnavup.gif b/trunk/etherpad/src/static/img/oct/usecasesnavup.gif new file mode 100644 index 0000000..470dcbe Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/usecasesnavup.gif differ diff --git a/trunk/etherpad/src/static/img/oct/usecasesnavuph.gif b/trunk/etherpad/src/static/img/oct/usecasesnavuph.gif new file mode 100644 index 0000000..4207386 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/usecasesnavuph.gif differ diff --git a/trunk/etherpad/src/static/img/oct/watchscreencast.gif b/trunk/etherpad/src/static/img/oct/watchscreencast.gif new file mode 100644 index 0000000..f52ed17 Binary files /dev/null and b/trunk/etherpad/src/static/img/oct/watchscreencast.gif differ diff --git a/trunk/etherpad/src/static/img/pad/animated-orb-orange-12.gif b/trunk/etherpad/src/static/img/pad/animated-orb-orange-12.gif new file mode 100644 index 0000000..9db02c6 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/animated-orb-orange-12.gif differ diff --git a/trunk/etherpad/src/static/img/pad/backgrad.png b/trunk/etherpad/src/static/img/pad/backgrad.png new file mode 100644 index 0000000..d85f73c Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/backgrad.png differ diff --git a/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-eee-20.gif b/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-eee-20.gif new file mode 100644 index 0000000..bc3088b Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-eee-20.gif differ diff --git a/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-20.gif b/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-20.gif new file mode 100644 index 0000000..8a87283 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-20.gif differ diff --git a/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-40.gif b/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-40.gif new file mode 100644 index 0000000..695ecbe Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-40.gif differ diff --git a/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-60.gif b/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-60.gif new file mode 100644 index 0000000..0005405 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/backshadow/backshadow-940-20-fff-60.gif differ diff --git a/trunk/etherpad/src/static/img/pad/backshadow/botshadow-940-20-eee-20.gif b/trunk/etherpad/src/static/img/pad/backshadow/botshadow-940-20-eee-20.gif new file mode 100644 index 0000000..92fb5dc Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/backshadow/botshadow-940-20-eee-20.gif differ diff --git a/trunk/etherpad/src/static/img/pad/etherpad-logo-small-grad.gif b/trunk/etherpad/src/static/img/pad/etherpad-logo-small-grad.gif new file mode 100644 index 0000000..a65aa15 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/etherpad-logo-small-grad.gif differ diff --git a/trunk/etherpad/src/static/img/pad/etherpad-logo-small.gif b/trunk/etherpad/src/static/img/pad/etherpad-logo-small.gif new file mode 100644 index 0000000..6669f39 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/etherpad-logo-small.gif differ diff --git a/trunk/etherpad/src/static/img/pad/etherpad-logo-small2.gif b/trunk/etherpad/src/static/img/pad/etherpad-logo-small2.gif new file mode 100644 index 0000000..6e33392 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/etherpad-logo-small2.gif differ diff --git a/trunk/etherpad/src/static/img/pad/expandy-arrow-down.gif b/trunk/etherpad/src/static/img/pad/expandy-arrow-down.gif new file mode 100644 index 0000000..4b67c17 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/expandy-arrow-down.gif differ diff --git a/trunk/etherpad/src/static/img/pad/expandy-arrow-right.gif b/trunk/etherpad/src/static/img/pad/expandy-arrow-right.gif new file mode 100644 index 0000000..61303e6 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/expandy-arrow-right.gif differ diff --git a/trunk/etherpad/src/static/img/pad/expandy-arrow6-down-active.gif b/trunk/etherpad/src/static/img/pad/expandy-arrow6-down-active.gif new file mode 100644 index 0000000..5f530b7 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/expandy-arrow6-down-active.gif differ diff --git a/trunk/etherpad/src/static/img/pad/expandy-arrow6-down.gif b/trunk/etherpad/src/static/img/pad/expandy-arrow6-down.gif new file mode 100644 index 0000000..42fa9af Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/expandy-arrow6-down.gif differ diff --git a/trunk/etherpad/src/static/img/pad/expandy-arrow6-right-active.gif b/trunk/etherpad/src/static/img/pad/expandy-arrow6-right-active.gif new file mode 100644 index 0000000..4496fad Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/expandy-arrow6-right-active.gif differ diff --git a/trunk/etherpad/src/static/img/pad/expandy-arrow6-right.gif b/trunk/etherpad/src/static/img/pad/expandy-arrow6-right.gif new file mode 100644 index 0000000..9a8274c Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/expandy-arrow6-right.gif differ diff --git a/trunk/etherpad/src/static/img/pad/header-revgrad.gif b/trunk/etherpad/src/static/img/pad/header-revgrad.gif new file mode 100644 index 0000000..e803e2a Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/header-revgrad.gif differ diff --git a/trunk/etherpad/src/static/img/pad/newpad.gif b/trunk/etherpad/src/static/img/pad/newpad.gif new file mode 100644 index 0000000..c631cc4 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/newpad.gif differ diff --git a/trunk/etherpad/src/static/img/pad/orb-greenred-12.gif b/trunk/etherpad/src/static/img/pad/orb-greenred-12.gif new file mode 100644 index 0000000..d722168 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/orb-greenred-12.gif differ diff --git a/trunk/etherpad/src/static/img/pad/padbg1.jpg b/trunk/etherpad/src/static/img/pad/padbg1.jpg new file mode 100644 index 0000000..8e640fd Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/padbg1.jpg differ diff --git a/trunk/etherpad/src/static/img/pad/padbg2.jpg b/trunk/etherpad/src/static/img/pad/padbg2.jpg new file mode 100644 index 0000000..1248bc0 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/padbg2.jpg differ diff --git a/trunk/etherpad/src/static/img/pad/padbg3.jpg b/trunk/etherpad/src/static/img/pad/padbg3.jpg new file mode 100644 index 0000000..99bba32 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/padbg3.jpg differ diff --git a/trunk/etherpad/src/static/img/pad/padbg4.jpg b/trunk/etherpad/src/static/img/pad/padbg4.jpg new file mode 100644 index 0000000..2497360 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/padbg4.jpg differ diff --git a/trunk/etherpad/src/static/img/pad/padbg5.jpg b/trunk/etherpad/src/static/img/pad/padbg5.jpg new file mode 100644 index 0000000..bc3953c Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/padbg5.jpg differ diff --git a/trunk/etherpad/src/static/img/pad/padhead1.jpg b/trunk/etherpad/src/static/img/pad/padhead1.jpg new file mode 100644 index 0000000..e263cf4 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/padhead1.jpg differ diff --git a/trunk/etherpad/src/static/img/pad/padhead2.jpg b/trunk/etherpad/src/static/img/pad/padhead2.jpg new file mode 100644 index 0000000..a2f247d Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/padhead2.jpg differ diff --git a/trunk/etherpad/src/static/img/pad/padhead3.jpg b/trunk/etherpad/src/static/img/pad/padhead3.jpg new file mode 100644 index 0000000..101432f Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/padhead3.jpg differ diff --git a/trunk/etherpad/src/static/img/pad/pencil-icon-small-blue.gif b/trunk/etherpad/src/static/img/pad/pencil-icon-small-blue.gif new file mode 100644 index 0000000..f60b0f2 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/pencil-icon-small-blue.gif differ diff --git a/trunk/etherpad/src/static/img/pad/sidehead-grad.gif b/trunk/etherpad/src/static/img/pad/sidehead-grad.gif new file mode 100644 index 0000000..32bac92 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/sidehead-grad.gif differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/button_depressed.png b/trunk/etherpad/src/static/img/pad/timeslider/button_depressed.png new file mode 100644 index 0000000..1e96692 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/button_depressed.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/button_undepressed.png b/trunk/etherpad/src/static/img/pad/timeslider/button_undepressed.png new file mode 100644 index 0000000..cd2d7a8 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/button_undepressed.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/crushed_button_depressed.png b/trunk/etherpad/src/static/img/pad/timeslider/crushed_button_depressed.png new file mode 100644 index 0000000..d75dcce Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/crushed_button_depressed.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/crushed_button_undepressed.png b/trunk/etherpad/src/static/img/pad/timeslider/crushed_button_undepressed.png new file mode 100644 index 0000000..d86e3f3 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/crushed_button_undepressed.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/crushed_current_location.png b/trunk/etherpad/src/static/img/pad/timeslider/crushed_current_location.png new file mode 100644 index 0000000..76e0835 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/crushed_current_location.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/crushed_pause.png b/trunk/etherpad/src/static/img/pad/timeslider/crushed_pause.png new file mode 100644 index 0000000..437b384 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/crushed_pause.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/crushed_play.png b/trunk/etherpad/src/static/img/pad/timeslider/crushed_play.png new file mode 100644 index 0000000..c5b754b Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/crushed_play.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/crushed_play_button.png b/trunk/etherpad/src/static/img/pad/timeslider/crushed_play_button.png new file mode 100644 index 0000000..3b112f6 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/crushed_play_button.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/crushed_timeslider_mockup.png b/trunk/etherpad/src/static/img/pad/timeslider/crushed_timeslider_mockup.png new file mode 100644 index 0000000..f4ccbf1 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/crushed_timeslider_mockup.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/current_location.gif b/trunk/etherpad/src/static/img/pad/timeslider/current_location.gif new file mode 100644 index 0000000..5d5062f Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/current_location.gif differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/current_location.png b/trunk/etherpad/src/static/img/pad/timeslider/current_location.png new file mode 100644 index 0000000..ab02792 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/current_location.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/pause.gif b/trunk/etherpad/src/static/img/pad/timeslider/pause.gif new file mode 100644 index 0000000..0fa105b Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/pause.gif differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/pause.png b/trunk/etherpad/src/static/img/pad/timeslider/pause.png new file mode 100644 index 0000000..657782c Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/pause.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/play.gif b/trunk/etherpad/src/static/img/pad/timeslider/play.gif new file mode 100644 index 0000000..ae308c2 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/play.gif differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/play.png b/trunk/etherpad/src/static/img/pad/timeslider/play.png new file mode 100644 index 0000000..19afe03 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/play.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/play_button.png b/trunk/etherpad/src/static/img/pad/timeslider/play_button.png new file mode 100644 index 0000000..bc1736d Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/play_button.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/star.gif b/trunk/etherpad/src/static/img/pad/timeslider/star.gif new file mode 100644 index 0000000..f6a2c70 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/star.gif differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/star.png b/trunk/etherpad/src/static/img/pad/timeslider/star.png new file mode 100644 index 0000000..e0c7099 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/star.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/star_selected.png b/trunk/etherpad/src/static/img/pad/timeslider/star_selected.png new file mode 100644 index 0000000..c336589 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/star_selected.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/stepper_buttons.png b/trunk/etherpad/src/static/img/pad/timeslider/stepper_buttons.png new file mode 100644 index 0000000..e011a45 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/stepper_buttons.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/timeslider_background.png b/trunk/etherpad/src/static/img/pad/timeslider/timeslider_background.png new file mode 100644 index 0000000..faa45c6 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/timeslider_background.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/timeslider_left.png b/trunk/etherpad/src/static/img/pad/timeslider/timeslider_left.png new file mode 100644 index 0000000..594d86b Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/timeslider_left.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/timeslider_mockup.png b/trunk/etherpad/src/static/img/pad/timeslider/timeslider_mockup.png new file mode 100644 index 0000000..bc93914 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/timeslider_mockup.png differ diff --git a/trunk/etherpad/src/static/img/pad/timeslider/timeslider_right.png b/trunk/etherpad/src/static/img/pad/timeslider/timeslider_right.png new file mode 100644 index 0000000..3bf10a2 Binary files /dev/null and b/trunk/etherpad/src/static/img/pad/timeslider/timeslider_right.png differ diff --git a/trunk/etherpad/src/static/img/pricing/free.gif b/trunk/etherpad/src/static/img/pricing/free.gif new file mode 100644 index 0000000..bce2a0b Binary files /dev/null and b/trunk/etherpad/src/static/img/pricing/free.gif differ diff --git a/trunk/etherpad/src/static/img/pricing/group.gif b/trunk/etherpad/src/static/img/pricing/group.gif new file mode 100644 index 0000000..b839d3e Binary files /dev/null and b/trunk/etherpad/src/static/img/pricing/group.gif differ diff --git a/trunk/etherpad/src/static/img/pricing/on-demand.gif b/trunk/etherpad/src/static/img/pricing/on-demand.gif new file mode 100644 index 0000000..19d2e57 Binary files /dev/null and b/trunk/etherpad/src/static/img/pricing/on-demand.gif differ diff --git a/trunk/etherpad/src/static/img/pricing/private-network.gif b/trunk/etherpad/src/static/img/pricing/private-network.gif new file mode 100644 index 0000000..70d197f Binary files /dev/null and b/trunk/etherpad/src/static/img/pricing/private-network.gif differ diff --git a/trunk/etherpad/src/static/img/pricing/support.gif b/trunk/etherpad/src/static/img/pricing/support.gif new file mode 100644 index 0000000..7f42f76 Binary files /dev/null and b/trunk/etherpad/src/static/img/pricing/support.gif differ diff --git a/trunk/etherpad/src/static/img/pro/billing/cards-button.gif b/trunk/etherpad/src/static/img/pro/billing/cards-button.gif new file mode 100644 index 0000000..2f9c8cf Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/billing/cards-button.gif differ diff --git a/trunk/etherpad/src/static/img/pro/box/blue-boxtop.gif b/trunk/etherpad/src/static/img/pro/box/blue-boxtop.gif new file mode 100644 index 0000000..38e3538 Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/box/blue-boxtop.gif differ diff --git a/trunk/etherpad/src/static/img/pro/buttons/bluebutton120.gif b/trunk/etherpad/src/static/img/pro/buttons/bluebutton120.gif new file mode 100644 index 0000000..2f22003 Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/buttons/bluebutton120.gif differ diff --git a/trunk/etherpad/src/static/img/pro/header/pro-header-back.gif b/trunk/etherpad/src/static/img/pro/header/pro-header-back.gif new file mode 100644 index 0000000..9514dbf Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/header/pro-header-back.gif differ diff --git a/trunk/etherpad/src/static/img/pro/header/pro-header-logo.png b/trunk/etherpad/src/static/img/pro/header/pro-header-logo.png new file mode 100644 index 0000000..b36daa8 Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/header/pro-header-logo.png differ diff --git a/trunk/etherpad/src/static/img/pro/header/pro-header-logo.xcf b/trunk/etherpad/src/static/img/pro/header/pro-header-logo.xcf new file mode 100644 index 0000000..524c00f Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/header/pro-header-logo.xcf differ diff --git a/trunk/etherpad/src/static/img/pro/header/pro-header-plustopnav-back.gif b/trunk/etherpad/src/static/img/pro/header/pro-header-plustopnav-back.gif new file mode 100644 index 0000000..f7398fe Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/header/pro-header-plustopnav-back.gif differ diff --git a/trunk/etherpad/src/static/img/pro/padlist/gear-drop.gif b/trunk/etherpad/src/static/img/pro/padlist/gear-drop.gif new file mode 100644 index 0000000..ded0f24 Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/padlist/gear-drop.gif differ diff --git a/trunk/etherpad/src/static/img/pro/padlist/paper-icon.gif b/trunk/etherpad/src/static/img/pro/padlist/paper-icon.gif new file mode 100644 index 0000000..161b66e Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/padlist/paper-icon.gif differ diff --git a/trunk/etherpad/src/static/img/pro/padlist/trash-icon.gif b/trunk/etherpad/src/static/img/pro/padlist/trash-icon.gif new file mode 100644 index 0000000..74b5ede Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/padlist/trash-icon.gif differ diff --git a/trunk/etherpad/src/static/img/pro/topnav/pro-topnav-back.gif b/trunk/etherpad/src/static/img/pro/topnav/pro-topnav-back.gif new file mode 100644 index 0000000..336fd05 Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/topnav/pro-topnav-back.gif differ diff --git a/trunk/etherpad/src/static/img/pro/topnav/pro-topnav-notch.gif b/trunk/etherpad/src/static/img/pro/topnav/pro-topnav-notch.gif new file mode 100644 index 0000000..5dbe57b Binary files /dev/null and b/trunk/etherpad/src/static/img/pro/topnav/pro-topnav-notch.gif differ diff --git a/trunk/etherpad/src/static/img/tinyplane.gif b/trunk/etherpad/src/static/img/tinyplane.gif new file mode 100644 index 0000000..5aaa223 Binary files /dev/null and b/trunk/etherpad/src/static/img/tinyplane.gif differ diff --git a/trunk/etherpad/src/static/img/wavejet.jpg b/trunk/etherpad/src/static/img/wavejet.jpg new file mode 100644 index 0000000..8d10fd0 Binary files /dev/null and b/trunk/etherpad/src/static/img/wavejet.jpg differ diff --git a/trunk/etherpad/src/static/js/ace.js b/trunk/etherpad/src/static/js/ace.js new file mode 100644 index 0000000..6766cee --- /dev/null +++ b/trunk/etherpad/src/static/js/ace.js @@ -0,0 +1,29 @@ +Ace2Editor.registry={nextId:1};function Ace2Editor(){var K="Ace2Editor";var F=Ace2Editor;var B={};var A={editor:B,id:(F.registry.nextId++)}; +var D=false;var E=[];function C(R,Q){return function(){var T=this;var S=arguments;function U(){R.apply(T,S); +}if(Q){Q.apply(T,S);}if(D){U();}else{E.push(U);}};}function I(){for(var Q=0;Q'; +};var J=function(Q){return'\x3cscript type="text/javascript" src="'+Q+'">\x3c/script>';};var M=J;var N=H; +var L=function(Q){return'\''";};var G=function(Q){return'\'\\x3cscript type="text/javascript" src="'+Q+"\">\\x3c/script>'"; +};var P=G;var O=L;B.destroy=C(function(){A.ace_dispose();A.frame.parentNode.removeChild(A.frame);delete F.registry[A.id]; +A=null;});B.init=function(Q,S,R){B.importText(S);A.onEditorReady=function(){D=true;I();R();};(function(){var W=''; +var T=["'"+W+"'"];T.push(("('\\n\'');T.push('\' \''); +var X='editorId = "'+A.id+'"; editorInfo = parent.'+K+'.registry[editorId]; window.onload = function() { window.onload = null; setTimeout(function() { var iframe = document.createElement("IFRAME"); iframe.scrolling = "no"; var outerdocbody = document.getElementById("outerdocbody"); iframe.frameBorder = 0; iframe.allowTransparency = true; outerdocbody.insertBefore(iframe, outerdocbody.firstChild); iframe.ace_outerWin = window; readyFunc = function() { editorInfo.onEditorReady(); readyFunc = null; editorInfo = null; }; var doc = iframe.contentWindow.document; doc.open(); doc.write('+T.join("+")+"); doc.close(); }, 0); }"; +var Y=[W,"",(''),'',"\x3cscript>",X,"\x3c/script>",'
    x
    ']; +var U=document.createElement("IFRAME");U.frameBorder=0;A.frame=U;document.getElementById(Q).appendChild(U); +var V=U.contentWindow.document;V.open();V.write(Y.join(""));V.close();B.adjustSize();})();};return B; +} \ No newline at end of file diff --git a/trunk/etherpad/src/static/js/billing.js b/trunk/etherpad/src/static/js/billing.js new file mode 100644 index 0000000..c9fa30e --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/js/billing_shared.js b/trunk/etherpad/src/static/js/billing_shared.js new file mode 100644 index 0000000..dc3a00c --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/js/broadcast.js b/trunk/etherpad/src/static/js/broadcast.js new file mode 100644 index 0000000..9fa8141 --- /dev/null +++ b/trunk/etherpad/src/static/js/broadcast.js @@ -0,0 +1,607 @@ +/** + * 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.) +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; + }; +} + +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); + } + }; +} + +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 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.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 0) { + goToRevisionIfEnabledCount --; + } else { + goToRevision.apply(goToRevision, arguments); + } +} + +BroadcastSlider.onSlider(goToRevisionIfEnabled); + +(function() { + for(var i=0; i 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 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/trunk/etherpad/src/static/js/broadcast_slider.js b/trunk/etherpad/src/static/js/broadcast_slider.js new file mode 100644 index 0000000..371663e --- /dev/null +++ b/trunk/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'); + 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 = $(''); + var swatchtd = $(''); + var swatch = $('
    '); + swatch.css('background-color', clientVars.colorPalette[author.colorId]); + swatchtd.append(swatch); + tr.append(swatchtd); + var nametd = $(''); + nametd.text(author.name || "unnamed"); + tr.append(nametd); + $("#authorstable").append(tr); + } else { + numAnonymous ++; + } + }); + if(numAnonymous > 0) { + var html = ""+(numNamed>0?"...and ":"")+numAnonymous+" unnamed author"+(numAnonymous>1?"s":"")+""; + $("#authorstable").append($(html)); + } if(authors.length == 0) { + $("#authorstable").append($("No Authors")) + } + } + + 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 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 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. Why not?"); + $("#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/trunk/etherpad/src/static/js/collab_client.js b/trunk/etherpad/src/static/js/collab_client.js new file mode 100644 index 0000000..d8834d7 --- /dev/null +++ b/trunk/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 maxDebugMessages) { + debugMessages = debugMessages.slice(debugMessages.length-maxDebugMessages, + debugMessages.length); + } + + info.debugMessages = {length: 0}; + for(var i=0;i 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/trunk/etherpad/src/static/js/colorutils.js b/trunk/etherpad/src/static/js/colorutils.js new file mode 100644 index 0000000..e745f8e --- /dev/null +++ b/trunk/etherpad/src/static/js/colorutils.js @@ -0,0 +1,91 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js + +/** + * 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 colorutils = {}; + +// "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0] +colorutils.css2triple = function(cssColor) { + var sixHex = colorutils.css2sixhex(cssColor); + function hexToFloat(hh) { + return Number("0x"+hh)/255; + } + return [hexToFloat(sixHex.substr(0,2)), + hexToFloat(sixHex.substr(2,2)), + hexToFloat(sixHex.substr(4,2))]; +} + +// "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff" +colorutils.css2sixhex = function(cssColor) { + var h = /[0-9a-fA-F]+/.exec(cssColor)[0]; + if (h.length != 6) { + var a = h.charAt(0); + var b = h.charAt(1); + var c = h.charAt(2); + h = a+a+b+b+c+c; + } + return h; +} + +// [1.0, 1.0, 1.0] -> "#ffffff" +colorutils.triple2css = function(triple) { + function floatToHex(n) { + var n2 = colorutils.clamp(Math.round(n*255), 0, 255); + return ("0"+n2.toString(16)).slice(-2); + } + return "#" + floatToHex(triple[0]) + + floatToHex(triple[1]) + floatToHex(triple[2]); +} + + +colorutils.clamp = function(v,bot,top) { return v < bot ? bot : (v > top ? top : v); }; +colorutils.min3 = function(a,b,c) { return (a < b) ? (a < c ? a : c) : (b < c ? b : c); }; +colorutils.max3 = function(a,b,c) { return (a > b) ? (a > c ? a : c) : (b > c ? b : c); }; +colorutils.colorMin = function(c) { return colorutils.min3(c[0], c[1], c[2]); }; +colorutils.colorMax = function(c) { return colorutils.max3(c[0], c[1], c[2]); }; +colorutils.scale = function(v, bot, top) { return colorutils.clamp(bot + v*(top-bot), 0, 1); }; +colorutils.unscale = function(v, bot, top) { return colorutils.clamp((v-bot)/(top-bot), 0, 1); }; + +colorutils.scaleColor = function(c, bot, top) { + return [colorutils.scale(c[0], bot, top), + colorutils.scale(c[1], bot, top), + colorutils.scale(c[2], bot, top)]; +} + +colorutils.unscaleColor = function(c, bot, top) { + return [colorutils.unscale(c[0], bot, top), + colorutils.unscale(c[1], bot, top), + colorutils.unscale(c[2], bot, top)]; +} + +colorutils.luminosity = function(c) { + // rule of thumb for RGB brightness; 1.0 is white + return c[0]*0.30 + c[1]*0.59 + c[2]*0.11; +} + +colorutils.saturate = function(c) { + var min = colorutils.colorMin(c); + var max = colorutils.colorMax(c); + if (max - min <= 0) return [1.0, 1.0, 1.0]; + return colorutils.unscaleColor(c, min, max); +} + +colorutils.blend = function(c1, c2, t) { + return [colorutils.scale(t, c1[0], c2[0]), + colorutils.scale(t, c1[1], c2[1]), + colorutils.scale(t, c1[2], c2[2])]; +} diff --git a/trunk/etherpad/src/static/js/confirmation.js b/trunk/etherpad/src/static/js/confirmation.js new file mode 100644 index 0000000..a0f725c --- /dev/null +++ b/trunk/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/trunk/etherpad/src/static/js/connection_diagnostics.js b/trunk/etherpad/src/static/js/connection_diagnostics.js new file mode 100644 index 0000000..cc43d46 --- /dev/null +++ b/trunk/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("

    Thanks! We will look at your case shortly.

    "); + } 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("
    Result: your browser and internet" + + " connection appear to be incompatibile with EtherPad."); + $('#statusmsg').css('color', '#520'); + $('#emailform').show(); +}; + diff --git a/trunk/etherpad/src/static/js/cssmanager_client.js b/trunk/etherpad/src/static/js/cssmanager_client.js new file mode 100644 index 0000000..04ed641 --- /dev/null +++ b/trunk/etherpad/src/static/js/cssmanager_client.js @@ -0,0 +1,88 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/cssmanager.js + +/** + * 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 makeCSSManager(emptyStylesheetTitle) { + + function getSheetByTitle(title) { + var allSheets = document.styleSheets; + for(var i=0;i= 0) { + browserDeleteRule(i); + selectorList.splice(i, 1); + } + } + + return {selectorStyle:selectorStyle, removeSelectorStyle:removeSelectorStyle, + info: function() { + return selectorList.length+":"+browserRules().length; + }}; +} diff --git a/trunk/etherpad/src/static/js/domline_client.js b/trunk/etherpad/src/static/js/domline_client.js new file mode 100644 index 0000000..de2e7d3 --- /dev/null +++ b/trunk/etherpad/src/static/js/domline_client.js @@ -0,0 +1,210 @@ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/domline.js + +/** + * 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 domline = {}; +domline.noop = function() {}; +domline.identity = function(x) { return x; }; + +domline.addToLineClass = function(lineClass, cls) { + // an "empty span" at any point can be used to add classes to + // the line, using line:className. otherwise, we ignore + // the span. + cls.replace(/\S+/g, function (c) { + if (c.indexOf("line:") == 0) { + // add class to line + lineClass = (lineClass ? lineClass+' ' : '')+c.substring(5); + } + }); + return lineClass; +} + +// if "document" is falsy we don't create a DOM node, just +// an object with innerHTML and className +domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) { + var result = { node: null, + appendSpan: domline.noop, + prepareForAdd: domline.noop, + notifyAdded: domline.noop, + clearSpans: domline.noop, + finishUpdate: domline.noop, + lineMarker: 0 }; + + var browser = (optBrowser || {}); + var document = optDocument; + + if (document) { + result.node = document.createElement("div"); + } + else { + result.node = {innerHTML: '', className: ''}; + } + + var html = []; + var preHtml, postHtml; + var curHTML = null; + function processSpaces(s) { + return domline.processSpaces(s, doesWrap); + } + var identity = domline.identity; + var perTextNodeProcess = (doesWrap ? identity : processSpaces); + var perHtmlLineProcess = (doesWrap ? processSpaces : identity); + var lineClass = 'ace-line'; + result.appendSpan = function(txt, cls) { + if (cls.indexOf('list') >= 0) { + var listType = /(?:^| )list:(\S+)/.exec(cls); + if (listType) { + listType = listType[1]; + if (listType) { + preHtml = '
    • '; + postHtml = '
    '; + } + result.lineMarker += txt.length; + return; // don't append any text + } + } + var href = null; + var simpleTags = null; + if (cls.indexOf('url') >= 0) { + cls = cls.replace(/(^| )url:(\S+)/g, function(x0, space, url) { + href = url; + return space+"url"; + }); + } + if (cls.indexOf('tag') >= 0) { + cls = cls.replace(/(^| )tag:(\S+)/g, function(x0, space, tag) { + if (! simpleTags) simpleTags = []; + simpleTags.push(tag.toLowerCase()); + return space+tag; + }); + } + if ((! txt) && cls) { + lineClass = domline.addToLineClass(lineClass, cls); + } + else if (txt) { + var extraOpenTags = ""; + var extraCloseTags = ""; + if (href) { + extraOpenTags = extraOpenTags+''; + extraCloseTags = ''+extraCloseTags; + } + if (simpleTags) { + simpleTags.sort(); + extraOpenTags = extraOpenTags+'<'+simpleTags.join('><')+'>'; + simpleTags.reverse(); + extraCloseTags = ''+extraCloseTags; + } + html.push('',extraOpenTags, + perTextNodeProcess(domline.escapeHTML(txt)), + extraCloseTags,''); + } + }; + result.clearSpans = function() { + html = []; + lineClass = ''; // non-null to cause update + result.lineMarker = 0; + }; + function writeHTML() { + var newHTML = perHtmlLineProcess(html.join('')); + if (! newHTML) { + if ((! document) || (! optBrowser)) { + newHTML += ' '; + } + else if (! browser.msie) { + newHTML += '
    '; + } + } + if (nonEmpty) { + newHTML = (preHtml||'')+newHTML+(postHtml||''); + } + html = preHtml = postHtml = null; // free memory + if (newHTML !== curHTML) { + curHTML = newHTML; + result.node.innerHTML = curHTML; + } + if (lineClass !== null) result.node.className = lineClass; + } + result.prepareForAdd = writeHTML; + result.finishUpdate = writeHTML; + result.getInnerHTML = function() { return curHTML || ''; }; + + return result; +}; + +domline.escapeHTML = function(s) { + var re = /[&<>'"]/g; /']/; // stupid indentation thing + if (! re.MAP) { + // persisted across function calls! + re.MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + } + return s.replace(re, function(c) { return re.MAP[c]; }); +}; + +domline.processSpaces = function(s, doesWrap) { + if (s.indexOf("<") < 0 && ! doesWrap) { + // short-cut + return s.replace(/ /g, ' '); + } + 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] = ' '; + endOfLine = false; + beforeSpace = true; + } + else if (p.charAt(0) != "<") { + endOfLine = false; + beforeSpace = false; + } + } + // beginning of line is nbsp + for(var i=0;i= ",oldLen," in ",cs); break; + case '+': { + calcNewLen += o.chars; numInserted += o.chars; + Changeset.assert(calcNewLen < newLen, calcNewLen," >= ",newLen," in ",cs); + break; + } + } + assem.append(o); + } + + calcNewLen += oldLen - oldPos; + charBank = charBank.substring(0, numInserted); + while (charBank.length < numInserted) { + charBank += "?"; + } + + assem.endDocument(); + var normalized = Changeset.pack(oldLen, calcNewLen, assem.toString(), charBank); + Changeset.assert(normalized == cs, normalized,' != ',cs); + + return cs; +} + +Changeset.smartOpAssembler = function() { + // Like opAssembler but able to produce conforming changesets + // from slightly looser input, at the cost of speed. + // Specifically: + // - merges consecutive operations that can be merged + // - strips final "=" + // - ignores 0-length changes + // - reorders consecutive + and - (which margingOpAssembler doesn't do) + + var minusAssem = Changeset.mergingOpAssembler(); + var plusAssem = Changeset.mergingOpAssembler(); + var keepAssem = Changeset.mergingOpAssembler(); + var assem = Changeset.stringAssembler(); + var lastOpcode = ''; + var lengthChange = 0; + + function flushKeeps() { + assem.append(keepAssem.toString()); + keepAssem.clear(); + } + + function flushPlusMinus() { + assem.append(minusAssem.toString()); + minusAssem.clear(); + assem.append(plusAssem.toString()); + plusAssem.clear(); + } + + function append(op) { + if (! op.opcode) return; + if (! op.chars) return; + + if (op.opcode == '-') { + if (lastOpcode == '=') { + flushKeeps(); + } + minusAssem.append(op); + lengthChange -= op.chars; + } + else if (op.opcode == '+') { + if (lastOpcode == '=') { + flushKeeps(); + } + plusAssem.append(op); + lengthChange += op.chars; + } + else if (op.opcode == '=') { + if (lastOpcode != '=') { + flushPlusMinus(); + } + keepAssem.append(op); + } + lastOpcode = op.opcode; + } + + function appendOpWithText(opcode, text, attribs, pool) { + var op = Changeset.newOp(opcode); + op.attribs = Changeset.makeAttribsString(opcode, attribs, pool); + var lastNewlinePos = text.lastIndexOf('\n'); + if (lastNewlinePos < 0) { + op.chars = text.length; + op.lines = 0; + append(op); + } + else { + op.chars = lastNewlinePos+1; + op.lines = text.match(/\n/g).length; + append(op); + op.chars = text.length - (lastNewlinePos+1); + op.lines = 0; + append(op); + } + } + + function toString() { + flushPlusMinus(); + flushKeeps(); + return assem.toString(); + } + + function clear() { + minusAssem.clear(); + plusAssem.clear(); + keepAssem.clear(); + assem.clear(); + lengthChange = 0; + } + + function endDocument() { + keepAssem.endDocument(); + } + + function getLengthChange() { + return lengthChange; + } + + return {append: append, toString: toString, clear: clear, endDocument: endDocument, + appendOpWithText: appendOpWithText, getLengthChange: getLengthChange }; +}; + +if (_opt) { + Changeset.mergingOpAssembler = function() { + var assem = _opt.mergingOpAssembler(); + + function append(op) { + assem.append(op.opcode, op.chars, op.lines, op.attribs); + } + function toString() { + return assem.toString(); + } + function clear() { + assem.clear(); + } + function endDocument() { + assem.endDocument(); + } + + return {append: append, toString: toString, clear: clear, endDocument: endDocument}; + }; +} +else { + Changeset.mergingOpAssembler = function() { + // This assembler can be used in production; it efficiently + // merges consecutive operations that are mergeable, ignores + // no-ops, and drops final pure "keeps". It does not re-order + // operations. + var assem = Changeset.opAssembler(); + var bufOp = Changeset.newOp(); + + // If we get, for example, insertions [xxx\n,yyy], those don't merge, + // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. + // This variable stores the length of yyy and any other newline-less + // ops immediately after it. + var bufOpAdditionalCharsAfterNewline = 0; + + function flush(isEndDocument) { + if (bufOp.opcode) { + if (isEndDocument && bufOp.opcode == '=' && ! bufOp.attribs) { + // final merged keep, leave it implicit + } + else { + assem.append(bufOp); + if (bufOpAdditionalCharsAfterNewline) { + bufOp.chars = bufOpAdditionalCharsAfterNewline; + bufOp.lines = 0; + assem.append(bufOp); + bufOpAdditionalCharsAfterNewline = 0; + } + } + bufOp.opcode = ''; + } + } + function append(op) { + if (op.chars > 0) { + if (bufOp.opcode == op.opcode && bufOp.attribs == op.attribs) { + if (op.lines > 0) { + // bufOp and additional chars are all mergeable into a multi-line op + bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; + bufOp.lines += op.lines; + bufOpAdditionalCharsAfterNewline = 0; + } + else if (bufOp.lines == 0) { + // both bufOp and op are in-line + bufOp.chars += op.chars; + } + else { + // append in-line text to multi-line bufOp + bufOpAdditionalCharsAfterNewline += op.chars; + } + } + else { + flush(); + Changeset.copyOp(op, bufOp); + } + } + } + function endDocument() { + flush(true); + } + function toString() { + flush(); + return assem.toString(); + } + function clear() { + assem.clear(); + Changeset.clearOp(bufOp); + } + return {append: append, toString: toString, clear: clear, endDocument: endDocument}; + }; +} + +if (_opt) { + Changeset.opAssembler = function() { + var assem = _opt.opAssembler(); + // this function allows op to be mutated later (doesn't keep a ref) + function append(op) { + assem.append(op.opcode, op.chars, op.lines, op.attribs); + } + function toString() { + return assem.toString(); + } + function clear() { + assem.clear(); + } + return {append: append, toString: toString, clear: clear}; + }; +} +else { + Changeset.opAssembler = function() { + var pieces = []; + // this function allows op to be mutated later (doesn't keep a ref) + function append(op) { + pieces.push(op.attribs); + if (op.lines) { + pieces.push('|', Changeset.numToString(op.lines)); + } + pieces.push(op.opcode); + pieces.push(Changeset.numToString(op.chars)); + } + function toString() { + return pieces.join(''); + } + function clear() { + pieces.length = 0; + } + return {append: append, toString: toString, clear: clear}; + }; +} + +Changeset.stringIterator = function(str) { + var curIndex = 0; + function assertRemaining(n) { + Changeset.assert(n <= remaining(), "!(",n," <= ",remaining(),")"); + } + function take(n) { + assertRemaining(n); + var s = str.substr(curIndex, n); + curIndex += n; + return s; + } + function peek(n) { + assertRemaining(n); + var s = str.substr(curIndex, n); + return s; + } + function skip(n) { + assertRemaining(n); + curIndex += n; + } + function remaining() { + return str.length - curIndex; + } + return {take:take, skip:skip, remaining:remaining, peek:peek}; +}; + +Changeset.stringAssembler = function() { + var pieces = []; + function append(x) { + pieces.push(String(x)); + } + function toString() { + return pieces.join(''); + } + return {append: append, toString: toString}; +}; + +// "lines" need not be an array as long as it supports certain calls (lines_foo inside). +Changeset.textLinesMutator = function(lines) { + // Mutates lines, an array of strings, in place. + // Mutation operations have the same constraints as changeset operations + // with respect to newlines, but not the other additional constraints + // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline). + // Can be used to mutate lists of strings where the last char of each string + // is not actually a newline, but for the purposes of N and L values, + // the caller should pretend it is, and for things to work right in that case, the input + // to insert() should be a single line with no newlines. + + var curSplice = [0,0]; + var inSplice = false; + // position in document after curSplice is applied: + var curLine = 0, curCol = 0; + // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && + // curLine >= curSplice[0] + // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then + // curCol == 0 + + function lines_applySplice(s) { + lines.splice.apply(lines, s); + } + function lines_toSource() { + return lines.toSource(); + } + function lines_get(idx) { + if (lines.get) { + return lines.get(idx); + } + else { + return lines[idx]; + } + } + // can be unimplemented if removeLines's return value not needed + function lines_slice(start, end) { + if (lines.slice) { + return lines.slice(start, end); + } + else { + return []; + } + } + function lines_length() { + if ((typeof lines.length) == "number") { + return lines.length; + } + else { + return lines.length(); + } + } + + function enterSplice() { + curSplice[0] = curLine; + curSplice[1] = 0; + if (curCol > 0) { + putCurLineInSplice(); + } + inSplice = true; + } + function leaveSplice() { + lines_applySplice(curSplice); + curSplice.length = 2; + curSplice[0] = curSplice[1] = 0; + inSplice = false; + } + function isCurLineInSplice() { + return (curLine - curSplice[0] < (curSplice.length - 2)); + } + function debugPrint(typ) { + print(typ+": "+curSplice.toSource()+" / "+curLine+","+curCol+" / "+lines_toSource()); + } + function putCurLineInSplice() { + if (! isCurLineInSplice()) { + curSplice.push(lines_get(curSplice[0] + curSplice[1])); + curSplice[1]++; + } + return 2 + curLine - curSplice[0]; + } + + function skipLines(L, includeInSplice) { + if (L) { + if (includeInSplice) { + if (! inSplice) { + enterSplice(); + } + for(var i=0;i 1) { + leaveSplice(); + } + else { + putCurLineInSplice(); + } + } + curLine += L; + curCol = 0; + } + //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); + /*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { + print("BLAH"); + putCurLineInSplice(); + }*/ // tests case foo in remove(), which isn't otherwise covered in current impl + } + //debugPrint("skip"); + } + + function skip(N, L, includeInSplice) { + if (N) { + if (L) { + skipLines(L, includeInSplice); + } + else { + if (includeInSplice && ! inSplice) { + enterSplice(); + } + if (inSplice) { + putCurLineInSplice(); + } + curCol += N; + //debugPrint("skip"); + } + } + } + + function removeLines(L) { + var removed = ''; + if (L) { + if (! inSplice) { + enterSplice(); + } + function nextKLinesText(k) { + var m = curSplice[0] + curSplice[1]; + return lines_slice(m, m+k).join(''); + } + if (isCurLineInSplice()) { + //print(curCol); + if (curCol == 0) { + removed = curSplice[curSplice.length-1]; + // print("FOO"); // case foo + curSplice.length--; + removed += nextKLinesText(L-1); + curSplice[1] += L-1; + } + else { + removed = nextKLinesText(L-1); + curSplice[1] += L-1; + var sline = curSplice.length - 1; + removed = curSplice[sline].substring(curCol) + removed; + curSplice[sline] = curSplice[sline].substring(0, curCol) + + lines_get(curSplice[0] + curSplice[1]); + curSplice[1] += 1; + } + } + else { + removed = nextKLinesText(L); + curSplice[1] += L; + } + //debugPrint("remove"); + } + return removed; + } + + function remove(N, L) { + var removed = ''; + if (N) { + if (L) { + return removeLines(L); + } + else { + if (! inSplice) { + enterSplice(); + } + var sline = putCurLineInSplice(); + removed = curSplice[sline].substring(curCol, curCol+N); + curSplice[sline] = curSplice[sline].substring(0, curCol) + + curSplice[sline].substring(curCol+N); + //debugPrint("remove"); + } + } + return removed; + } + + function insert(text, L) { + if (text) { + if (! inSplice) { + enterSplice(); + } + if (L) { + var newLines = Changeset.splitTextLines(text); + if (isCurLineInSplice()) { + //if (curCol == 0) { + //curSplice.length--; + //curSplice[1]--; + //Array.prototype.push.apply(curSplice, newLines); + //curLine += newLines.length; + //} + //else { + var sline = curSplice.length - 1; + var theLine = curSplice[sline]; + var lineCol = curCol; + curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; + curLine++; + newLines.splice(0, 1); + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + curSplice.push(theLine.substring(lineCol)); + curCol = 0; + //} + } + else { + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + } + } + else { + var sline = putCurLineInSplice(); + curSplice[sline] = curSplice[sline].substring(0, curCol) + + text + curSplice[sline].substring(curCol); + curCol += text.length; + } + //debugPrint("insert"); + } + } + + function hasMore() { + //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); + var docLines = lines_length(); + if (inSplice) { + docLines += curSplice.length - 2 - curSplice[1]; + } + return curLine < docLines; + } + + function close() { + if (inSplice) { + leaveSplice(); + } + //debugPrint("close"); + } + + var self = {skip:skip, remove:remove, insert:insert, close:close, hasMore:hasMore, + removeLines:removeLines, skipLines: skipLines}; + return self; +}; + +Changeset.applyZip = function(in1, idx1, in2, idx2, func) { + var iter1 = Changeset.opIterator(in1, idx1); + var iter2 = Changeset.opIterator(in2, idx2); + var assem = Changeset.smartOpAssembler(); + var op1 = Changeset.newOp(); + var op2 = Changeset.newOp(); + var opOut = Changeset.newOp(); + while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) { + if ((! op1.opcode) && iter1.hasNext()) iter1.next(op1); + if ((! op2.opcode) && iter2.hasNext()) iter2.next(op2); + func(op1, op2, opOut); + if (opOut.opcode) { + //print(opOut.toSource()); + assem.append(opOut); + opOut.opcode = ''; + } + } + assem.endDocument(); + return assem.toString(); +}; + +Changeset.unpack = function(cs) { + var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; + var headerMatch = headerRegex.exec(cs); + if ((! headerMatch) || (! headerMatch[0])) { + Changeset.error("Not a changeset: "+cs); + } + var oldLen = Changeset.parseNum(headerMatch[1]); + var changeSign = (headerMatch[2] == '>') ? 1 : -1; + var changeMag = Changeset.parseNum(headerMatch[3]); + var newLen = oldLen + changeSign*changeMag; + var opsStart = headerMatch[0].length; + var opsEnd = cs.indexOf("$"); + if (opsEnd < 0) opsEnd = cs.length; + return {oldLen: oldLen, newLen: newLen, ops: cs.substring(opsStart, opsEnd), + charBank: cs.substring(opsEnd+1)}; +}; + +Changeset.pack = function(oldLen, newLen, opsStr, bank) { + var lenDiff = newLen - oldLen; + var lenDiffStr = (lenDiff >= 0 ? + '>'+Changeset.numToString(lenDiff) : + '<'+Changeset.numToString(-lenDiff)); + var a = []; + a.push('Z:', Changeset.numToString(oldLen), lenDiffStr, opsStr, '$', bank); + return a.join(''); +}; + +Changeset.applyToText = function(cs, str) { + var unpacked = Changeset.unpack(cs); + Changeset.assert(str.length == unpacked.oldLen, + "mismatched apply: ",str.length," / ",unpacked.oldLen); + var csIter = Changeset.opIterator(unpacked.ops); + var bankIter = Changeset.stringIterator(unpacked.charBank); + var strIter = Changeset.stringIterator(str); + var assem = Changeset.stringAssembler(); + while (csIter.hasNext()) { + var op = csIter.next(); + switch(op.opcode) { + case '+': assem.append(bankIter.take(op.chars)); break; + case '-': strIter.skip(op.chars); break; + case '=': assem.append(strIter.take(op.chars)); break; + } + } + assem.append(strIter.take(strIter.remaining())); + return assem.toString(); +}; + +Changeset.mutateTextLines = function(cs, lines) { + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var bankIter = Changeset.stringIterator(unpacked.charBank); + var mut = Changeset.textLinesMutator(lines); + while (csIter.hasNext()) { + var op = csIter.next(); + switch(op.opcode) { + case '+': mut.insert(bankIter.take(op.chars), op.lines); break; + case '-': mut.remove(op.chars, op.lines); break; + case '=': mut.skip(op.chars, op.lines, (!! op.attribs)); break; + } + } + mut.close(); +}; + +Changeset.composeAttributes = function(att1, att2, resultIsMutation, pool) { + // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. + + // Sometimes attribute (key,value) pairs are treated as attribute presence + // information, while other times they are treated as operations that + // mutate a set of attributes, and this affects whether an empty value + // is a deletion or a change. + // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result + // ([], [(bold, )], true) -> [(bold, )] + // ([], [(bold, )], false) -> [] + // ([], [(bold, true)], true) -> [(bold, true)] + // ([], [(bold, true)], false) -> [(bold, true)] + // ([(bold, true)], [(bold, )], true) -> [(bold, )] + // ([(bold, true)], [(bold, )], false) -> [] + + // pool can be null if att2 has no attributes. + + if ((! att1) && resultIsMutation) { + // In the case of a mutation (i.e. composing two changesets), + // an att2 composed with an empy att1 is just att2. If att1 + // is part of an attribution string, then att2 may remove + // attributes that are already gone, so don't do this optimization. + return att2; + } + if (! att2) return att1; + var atts = []; + att1.replace(/\*([0-9a-z]+)/g, function(_, a) { + atts.push(pool.getAttrib(Changeset.parseNum(a))); + return ''; + }); + att2.replace(/\*([0-9a-z]+)/g, function(_, a) { + var pair = pool.getAttrib(Changeset.parseNum(a)); + var found = false; + for(var i=0;i"); + + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var csBank = unpacked.charBank; + var csBankIndex = 0; + // treat the attribution lines as text lines, mutating a line at a time + var mut = Changeset.textLinesMutator(lines); + + var lineIter = null; + function isNextMutOp() { + return (lineIter && lineIter.hasNext()) || mut.hasMore(); + } + function nextMutOp(destOp) { + if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { + var line = mut.removeLines(1); + lineIter = Changeset.opIterator(line); + } + if (lineIter && lineIter.hasNext()) { + lineIter.next(destOp); + } + else { + destOp.opcode = ''; + } + } + var lineAssem = null; + function outputMutOp(op) { + //print("outputMutOp: "+op.toSource()); + if (! lineAssem) { + lineAssem = Changeset.mergingOpAssembler(); + } + lineAssem.append(op); + if (op.lines > 0) { + Changeset.assert(op.lines == 1, "Can't have op.lines of ",op.lines," in attribution lines"); + // ship it to the mut + mut.insert(lineAssem.toString(), 1); + lineAssem = null; + } + } + + var csOp = Changeset.newOp(); + var attOp = Changeset.newOp(); + var opOut = Changeset.newOp(); + while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { + if ((! csOp.opcode) && csIter.hasNext()) { + csIter.next(csOp); + } + //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); + //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); + //print("csOp: "+csOp.toSource()); + if ((! csOp.opcode) && (! attOp.opcode) && + (! lineAssem) && (! (lineIter && lineIter.hasNext()))) { + break; // done + } + else if (csOp.opcode == '=' && csOp.lines > 0 && (! csOp.attribs) && (! attOp.opcode) && + (! lineAssem) && (! (lineIter && lineIter.hasNext()))) { + // skip multiple lines; this is what makes small changes not order of the document size + mut.skipLines(csOp.lines); + //print("skipped: "+csOp.lines); + csOp.opcode = ''; + } + else if (csOp.opcode == '+') { + if (csOp.lines > 1) { + var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; + Changeset.copyOp(csOp, opOut); + csOp.chars -= firstLineLen; + csOp.lines--; + opOut.lines = 1; + opOut.chars = firstLineLen; + } + else { + Changeset.copyOp(csOp, opOut); + csOp.opcode = ''; + } + outputMutOp(opOut); + csBankIndex += opOut.chars; + opOut.opcode = ''; + } + else { + if ((! attOp.opcode) && isNextMutOp()) { + nextMutOp(attOp); + } + //print("attOp: "+attOp.toSource()); + Changeset._slicerZipperFunc(attOp, csOp, opOut, pool); + if (opOut.opcode) { + outputMutOp(opOut); + opOut.opcode = ''; + } + } + } + + Changeset.assert(! lineAssem, "line assembler not finished"); + mut.close(); + + //dmesg("-> "+lines.toSource()); +}; + +Changeset.joinAttributionLines = function(theAlines) { + var assem = Changeset.mergingOpAssembler(); + for(var i=0;i 0) { + lines.push(assem.toString()); + assem.clear(); + } + pos += op.chars; + } + + while (iter.hasNext()) { + var op = iter.next(); + var numChars = op.chars; + var numLines = op.lines; + while (numLines > 1) { + var newlineEnd = text.indexOf('\n', pos)+1; + Changeset.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines"); + op.chars = newlineEnd - pos; + op.lines = 1; + appendOp(op); + numChars -= op.chars; + numLines -= op.lines; + } + if (numLines == 1) { + op.chars = numChars; + op.lines = 1; + } + appendOp(op); + } + + return lines; +}; + +Changeset.splitTextLines = function(text) { + return text.match(/[^\n]*(?:\n|[^\n]$)/g); +}; + +Changeset.compose = function(cs1, cs2, pool) { + var unpacked1 = Changeset.unpack(cs1); + var unpacked2 = Changeset.unpack(cs2); + var len1 = unpacked1.oldLen; + var len2 = unpacked1.newLen; + Changeset.assert(len2 == unpacked2.oldLen, "mismatched composition"); + var len3 = unpacked2.newLen; + var bankIter1 = Changeset.stringIterator(unpacked1.charBank); + var bankIter2 = Changeset.stringIterator(unpacked2.charBank); + var bankAssem = Changeset.stringAssembler(); + + var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function(op1, op2, opOut) { + //var debugBuilder = Changeset.stringAssembler(); + //debugBuilder.append(Changeset.opString(op1)); + //debugBuilder.append(','); + //debugBuilder.append(Changeset.opString(op2)); + //debugBuilder.append(' / '); + + var op1code = op1.opcode; + var op2code = op2.opcode; + if (op1code == '+' && op2code == '-') { + bankIter1.skip(Math.min(op1.chars, op2.chars)); + } + Changeset._slicerZipperFunc(op1, op2, opOut, pool); + if (opOut.opcode == '+') { + if (op2code == '+') { + bankAssem.append(bankIter2.take(opOut.chars)); + } + else { + bankAssem.append(bankIter1.take(opOut.chars)); + } + } + + //debugBuilder.append(Changeset.opString(op1)); + //debugBuilder.append(','); + //debugBuilder.append(Changeset.opString(op2)); + //debugBuilder.append(' -> '); + //debugBuilder.append(Changeset.opString(opOut)); + //print(debugBuilder.toString()); + }); + + return Changeset.pack(len1, len3, newOps, bankAssem.toString()); +}; + +Changeset.attributeTester = function(attribPair, pool) { + // returns a function that tests if a string of attributes + // (e.g. *3*4) contains a given attribute key,value that + // is already present in the pool. + if (! pool) { + return never; + } + var attribNum = pool.putAttrib(attribPair, true); + if (attribNum < 0) { + return never; + } + else { + var re = new RegExp('\\*'+Changeset.numToString(attribNum)+ + '(?!\\w)'); + return function(attribs) { + return re.test(attribs); + }; + } + function never(attribs) { return false; } +}; + +Changeset.identity = function(N) { + return Changeset.pack(N, N, "", ""); +}; + +Changeset.makeSplice = function(oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) { + var oldLen = oldFullText.length; + + if (spliceStart >= oldLen) { + spliceStart = oldLen - 1; + } + if (numRemoved > oldFullText.length - spliceStart - 1) { + numRemoved = oldFullText.length - spliceStart - 1; + } + var oldText = oldFullText.substring(spliceStart, spliceStart+numRemoved); + var newLen = oldLen + newText.length - oldText.length; + + var assem = Changeset.smartOpAssembler(); + assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); + assem.appendOpWithText('-', oldText); + assem.appendOpWithText('+', newText, optNewTextAPairs, pool); + assem.endDocument(); + return Changeset.pack(oldLen, newLen, assem.toString(), newText); +}; + +Changeset.toSplices = function(cs) { + // get a list of splices, [startChar, endChar, newText] + + var unpacked = Changeset.unpack(cs); + var splices = []; + + var oldPos = 0; + var iter = Changeset.opIterator(unpacked.ops); + var charIter = Changeset.stringIterator(unpacked.charBank); + var inSplice = false; + while (iter.hasNext()) { + var op = iter.next(); + if (op.opcode == '=') { + oldPos += op.chars; + inSplice = false; + } + else { + if (! inSplice) { + splices.push([oldPos, oldPos, ""]); + inSplice = true; + } + if (op.opcode == '-') { + oldPos += op.chars; + splices[splices.length-1][1] += op.chars; + } + else if (op.opcode == '+') { + splices[splices.length-1][2] += charIter.take(op.chars); + } + } + } + + return splices; +}; + +Changeset.characterRangeFollow = function(cs, startChar, endChar, insertionsAfter) { + var newStartChar = startChar; + var newEndChar = endChar; + var splices = Changeset.toSplices(cs); + var lengthChangeSoFar = 0; + for(var i=0;i= newEndChar) { + // splice fully replaces/deletes range + // (also case that handles insertion at a collapsed selection) + if (insertionsAfter) { + newStartChar = newEndChar = spliceStart; + } + else { + newStartChar = newEndChar = spliceStart + newTextLength; + } + } + else if (spliceEnd <= newStartChar) { + // splice is before range + newStartChar += thisLengthChange; + newEndChar += thisLengthChange; + } + else if (spliceStart >= newEndChar) { + // splice is after range + } + else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) { + // splice is inside range + newEndChar += thisLengthChange; + } + else if (spliceEnd < newEndChar) { + // splice overlaps beginning of range + newStartChar = spliceStart + newTextLength; + newEndChar += thisLengthChange; + } + else { + // splice overlaps end of range + newEndChar = spliceStart; + } + + lengthChangeSoFar += thisLengthChange; + } + + return [newStartChar, newEndChar]; +}; + +Changeset.moveOpsToNewPool = function(cs, oldPool, newPool) { + // works on changeset or attribution string + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + var fromDollar = cs.substring(dollarPos); + // order of attribs stays the same + return upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) { + var oldNum = Changeset.parseNum(a); + var pair = oldPool.getAttrib(oldNum); + var newNum = newPool.putAttrib(pair); + return '*'+Changeset.numToString(newNum); + }) + fromDollar; +}; + +Changeset.makeAttribution = function(text) { + var assem = Changeset.smartOpAssembler(); + assem.appendOpWithText('+', text); + return assem.toString(); +}; + +// callable on a changeset, attribution string, or attribs property of an op +Changeset.eachAttribNumber = function(cs, func) { + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + + upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) { + func(Changeset.parseNum(a)); + return ''; + }); +}; + +// callable on a changeset, attribution string, or attribs property of an op, +// though it may easily create adjacent ops that can be merged. +Changeset.filterAttribNumbers = function(cs, filter) { + return Changeset.mapAttribNumbers(cs, filter); +}; + +Changeset.mapAttribNumbers = function(cs, func) { + var dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + var upToDollar = cs.substring(0, dollarPos); + + var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function(s, a) { + var n = func(Changeset.parseNum(a)); + if (n === true) { + return s; + } + else if ((typeof n) === "number") { + return '*'+Changeset.numToString(n); + } + else { + return ''; + } + }); + + return newUpToDollar + cs.substring(dollarPos); +}; + +Changeset.makeAText = function(text, attribs) { + return { text: text, attribs: (attribs || Changeset.makeAttribution(text)) }; +}; + +Changeset.applyToAText = function(cs, atext, pool) { + return { text: Changeset.applyToText(cs, atext.text), + attribs: Changeset.applyToAttribution(cs, atext.attribs, pool) }; +}; + +Changeset.cloneAText = function(atext) { + return { text: atext.text, attribs: atext.attribs }; +}; + +Changeset.copyAText = function(atext1, atext2) { + atext2.text = atext1.text; + atext2.attribs = atext1.attribs; +}; + +Changeset.appendATextToAssembler = function(atext, assem) { + // intentionally skips last newline char of atext + var iter = Changeset.opIterator(atext.attribs); + var op = Changeset.newOp(); + while (iter.hasNext()) { + iter.next(op); + if (! iter.hasNext()) { + // last op, exclude final newline + if (op.lines <= 1) { + op.lines = 0; + op.chars--; + if (op.chars) { + assem.append(op); + } + } + else { + var nextToLastNewlineEnd = + atext.text.lastIndexOf('\n', atext.text.length-2) + 1; + var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; + op.lines--; + op.chars -= (lastLineLength + 1); + assem.append(op); + op.lines = 0; + op.chars = lastLineLength; + if (op.chars) { + assem.append(op); + } + } + } + else { + assem.append(op); + } + } +}; + +Changeset.prepareForWire = function(cs, pool) { + var newPool = new AttribPool(); + var newCs = Changeset.moveOpsToNewPool(cs, pool, newPool); + return {translated: newCs, pool: newPool}; +}; + +Changeset.isIdentity = function(cs) { + var unpacked = Changeset.unpack(cs); + return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; +}; + +Changeset.opAttributeValue = function(op, key, pool) { + return Changeset.attribsAttributeValue(op.attribs, key, pool); +}; + +Changeset.attribsAttributeValue = function(attribs, key, pool) { + var value = ''; + if (attribs) { + Changeset.eachAttribNumber(attribs, function(n) { + if (pool.getAttribKey(n) == key) { + value = pool.getAttribValue(n); + } + }); + } + return value; +}; + +Changeset.builder = function(oldLen) { + var assem = Changeset.smartOpAssembler(); + var o = Changeset.newOp(); + var charBank = Changeset.stringAssembler(); + + var self = { + // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) + keep: function(N, L, attribs, pool) { + o.opcode = '='; + o.attribs = (attribs && + Changeset.makeAttribsString('=', attribs, pool)) || ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + keepText: function(text, attribs, pool) { + assem.appendOpWithText('=', text, attribs, pool); + return self; + }, + insert: function(text, attribs, pool) { + assem.appendOpWithText('+', text, attribs, pool); + charBank.append(text); + return self; + }, + remove: function(N, L) { + o.opcode = '-'; + o.attribs = ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + toString: function() { + assem.endDocument(); + var newLen = oldLen + assem.getLengthChange(); + return Changeset.pack(oldLen, newLen, assem.toString(), + charBank.toString()); + } + }; + + return self; +}; + +Changeset.makeAttribsString = function(opcode, attribs, pool) { + // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work + if (! attribs) { + return ''; + } + else if ((typeof attribs) == "string") { + return attribs; + } + else if (pool && attribs && attribs.length) { + if (attribs.length > 1) { + attribs = attribs.slice(); + attribs.sort(); + } + var result = []; + for(var i=0;i= attOp.chars && + attOp.lines > 0 && csOp.lines <= 0) { + csOp.lines++; + } + + Changeset._slicerZipperFunc(attOp, csOp, opOut, null); + if (opOut.opcode) { + assem.append(opOut); + opOut.opcode = ''; + } + } + } + } + + csOp.opcode = '-'; + csOp.chars = start; + + doCsOp(); + + if (optEnd === undefined) { + if (attOp.opcode) { + assem.append(attOp); + } + while (iter.hasNext()) { + iter.next(attOp); + assem.append(attOp); + } + } + else { + csOp.opcode = '='; + csOp.chars = optEnd - start; + doCsOp(); + } + + return assem.toString(); +}; + +Changeset.inverse = function(cs, lines, alines, pool) { + // lines and alines are what the changeset is meant to apply to. + // They may be arrays or objects with .get(i) and .length methods. + // They include final newlines on lines. + function lines_get(idx) { + if (lines.get) { + return lines.get(idx); + } + else { + return lines[idx]; + } + } + function lines_length() { + if ((typeof lines.length) == "number") { + return lines.length; + } + else { + return lines.length(); + } + } + function alines_get(idx) { + if (alines.get) { + return alines.get(idx); + } + else { + return alines[idx]; + } + } + function alines_length() { + if ((typeof alines.length) == "number") { + return alines.length; + } + else { + return alines.length(); + } + } + + var curLine = 0; + var curChar = 0; + var curLineOpIter = null; + var curLineOpIterLine; + var curLineNextOp = Changeset.newOp('+'); + + var unpacked = Changeset.unpack(cs); + var csIter = Changeset.opIterator(unpacked.ops); + var builder = Changeset.builder(unpacked.newLen); + + function consumeAttribRuns(numChars, func/*(len, attribs, endsLine)*/) { + + if ((! curLineOpIter) || (curLineOpIterLine != curLine)) { + // create curLineOpIter and advance it to curChar + curLineOpIter = Changeset.opIterator(alines_get(curLine)); + curLineOpIterLine = curLine; + var indexIntoLine = 0; + var done = false; + while (! done) { + curLineOpIter.next(curLineNextOp); + if (indexIntoLine + curLineNextOp.chars >= curChar) { + curLineNextOp.chars -= (curChar - indexIntoLine); + done = true; + } + else { + indexIntoLine += curLineNextOp.chars; + } + } + } + + while (numChars > 0) { + if ((! curLineNextOp.chars) && (! curLineOpIter.hasNext())) { + curLine++; + curChar = 0; + curLineOpIterLine = curLine; + curLineNextOp.chars = 0; + curLineOpIter = Changeset.opIterator(alines_get(curLine)); + } + if (! curLineNextOp.chars) { + curLineOpIter.next(curLineNextOp); + } + var charsToUse = Math.min(numChars, curLineNextOp.chars); + func(charsToUse, curLineNextOp.attribs, + charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); + numChars -= charsToUse; + curLineNextOp.chars -= charsToUse; + curChar += charsToUse; + } + + if ((! curLineNextOp.chars) && (! curLineOpIter.hasNext())) { + curLine++; + curChar = 0; + } + } + + function skip(N, L) { + if (L) { + curLine += L; + curChar = 0; + } + else { + if (curLineOpIter && curLineOpIterLine == curLine) { + consumeAttribRuns(N, function() {}); + } + else { + curChar += N; + } + } + } + + function nextText(numChars) { + var len = 0; + var assem = Changeset.stringAssembler(); + var firstString = lines_get(curLine).substring(curChar); + len += firstString.length; + assem.append(firstString); + + var lineNum = curLine+1; + while (len < numChars) { + var nextString = lines_get(lineNum); + len += nextString.length; + assem.append(nextString); + lineNum++; + } + + return assem.toString().substring(0, numChars); + } + + function cachedStrFunc(func) { + var cache = {}; + return function(s) { + if (! cache[s]) { + cache[s] = func(s); + } + return cache[s]; + }; + } + + var attribKeys = []; + var attribValues = []; + while (csIter.hasNext()) { + var csOp = csIter.next(); + if (csOp.opcode == '=') { + if (csOp.attribs) { + attribKeys.length = 0; + attribValues.length = 0; + Changeset.eachAttribNumber(csOp.attribs, function(n) { + attribKeys.push(pool.getAttribKey(n)); + attribValues.push(pool.getAttribValue(n)); + }); + var undoBackToAttribs = cachedStrFunc(function(attribs) { + var backAttribs = []; + for(var i=0;i 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(' '); + } + else { + navcells. + html(''+text+''). + 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", "« "+getProseTitle(n-1), (n == 1 ? "" : "uses"+(n-1))); + var nextTitle = getProseTitle(n+1); + if (! nextTitle) setNavLink("right", ""); + else setNavLink("right", nextTitle+" »", "uses"+(n+1));*/ + /*setNavLink("left", "« "+getProseTitle(0), ""); + setNavLink("right", "");*/ + setNavLink("right", "« "+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/trunk/etherpad/src/static/js/jquery-1.2.6.js b/trunk/etherpad/src/static/js/jquery-1.2.6.js new file mode 100755 index 0000000..88e661e --- /dev/null +++ b/trunk/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 + ">"; + }); + + // 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("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
    " ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and + + diff --git a/trunk/etherpad/src/templates/pro/admin/pne-config.ejs b/trunk/etherpad/src/templates/pro/admin/pne-config.ejs new file mode 100644 index 0000000..56fe68d --- /dev/null +++ b/trunk/etherpad/src/templates/pro/admin/pne-config.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. */ %>

    Private Server Configuration

    + +

    Your private EtherPad server can be configured using either command-line arguments (of the +form --argName=value), or by adding the options to the file +data/etherpad.properties.

    + +

    Learn more about server options in the PNE Server Manual.

    + +

    Current Config Values

    + + + +<% propKeys.forEach(function(k) { %> + +<% }) %> +
    Option NameCurrent Value
    <%= k %><%= appjetConfig[k] %>
    + + + + + diff --git a/trunk/etherpad/src/templates/pro/admin/pne-dashboard.ejs b/trunk/etherpad/src/templates/pro/admin/pne-dashboard.ejs new file mode 100644 index 0000000..6b9b456 --- /dev/null +++ b/trunk/etherpad/src/templates/pro/admin/pne-dashboard.ejs @@ -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. */ %><% helpers.setHtmlTitle("EtherPad Private Server Dashboard") %> + +

    User Quota

    + +

    Your maximum daily unique user quota is: <%= userQuota %>

    +

    So far today, there have been <%= todayActiveUsers %> applied against this quota.

    + +

    Uptime

    + +This server has been running for <%= renderUptime() %>. + +

    HTTP Response Codes

    + +<%= renderResponseCodes() %> + +

    Current Realtime Pad Connections

    + +<%= renderPadConnections() %> + +

    Realtime Transport Performance

    + +<%= renderTransportStats() %> + +
    + * +
    + + diff --git a/trunk/etherpad/src/templates/pro/admin/pne-license-manager.ejs b/trunk/etherpad/src/templates/pro/admin/pne-license-manager.ejs new file mode 100644 index 0000000..42594b8 --- /dev/null +++ b/trunk/etherpad/src/templates/pro/admin/pne-license-manager.ejs @@ -0,0 +1,132 @@ +<% /* 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 PNE License Manager"); %> +<% helpers.includeJQuery() %> +<% helpers.includeJs("etherpad.js") %> + +
    + + <% if (isExpired) { %> + +
    +

    Your evaluation license has expired!

    +

    Please contact <%= helpers.oemail("sales") %> or visit the pricing page on pad.spline.inf.fu-berlin.de + to purchase a license key.

    +
    + + <% } %> + + <% if (isTooOld) { %> + +
    +

    The version of EtherPad you are running (<%= runningVersionString %>) is too old. + Please update to version <%= licenseVersionString %> or newer by downloading the latest version on + pad.spline.inf.fu-berlin.de.

    +
    + + <% } %> + + <% if (errorMessage) { %> +
    +

    <%= errorMessage %>

    +
    + <% } %> + + <% if (licenseInfo && !edit) { %> + +

    License Info:

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    Licensed To: <%= licenseInfo.personName %>
    Organization: <%= licenseInfo.organizationName %>
    Software Edition: <%= licenseInfo.editionName %>
    Maximum Users: <%= licenseInfo.userQuota %>
    Expires: <%= licenseInfo.expiresDate ? licenseInfo.expiresDate.toString() : "never" %>
    + +
    + +
    +
    + +
    +
    + + <% } %> + + <% if (isExpired || !licenseInfo || edit) { %> + +

    Enter New License Info:

    + + <% if (isUnlicensed) { %> +

    Before you can use this copy of EtherPad Private Network Edition, you must first + enter a valid license. Free trial licenses can be obtained obtained here. +

    + <% } %> + +
    +
    + +

    Name:

    + " /> + +

    Organization:

    + " /> + +

    License Key:

    + + +
    + +
    + + +
    +
    + + <% } %> + +
    + diff --git a/trunk/etherpad/src/templates/pro/admin/pne-shell.ejs b/trunk/etherpad/src/templates/pro/admin/pne-shell.ejs new file mode 100644 index 0000000..f398b15 --- /dev/null +++ b/trunk/etherpad/src/templates/pro/admin/pne-shell.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("Shell") %> + +

    Warning! Be careful with this page.

    + +

    Shell

    + +

    Enter command:

    + +
    + + +
    + +<% if (result) { %> +

    Result

    +
    + <%= result %> +
    +

    Computed in <%= elapsedMs %>ms.

    +<% } %> + diff --git a/trunk/etherpad/src/templates/pro/admin/pro-config.ejs b/trunk/etherpad/src/templates/pro/admin/pro-config.ejs new file mode 100644 index 0000000..32cb610 --- /dev/null +++ b/trunk/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. */ %> +

    Application Configuration

    + +<%= messageDiv() %> + +
    + + + + + + + + + + + + + + + + + + + + +
    Site Name (appears in + the header of all pages): + +
    Always require all users on this domain to use secure + (HTTPS) connections? + /> +
    Default pad text: + +
    + +
    + +
    + diff --git a/trunk/etherpad/src/templates/pro/admin/single-invoice.ejs b/trunk/etherpad/src/templates/pro/admin/single-invoice.ejs new file mode 100644 index 0000000..aeab184 --- /dev/null +++ b/trunk/etherpad/src/templates/pro/admin/single-invoice.ejs @@ -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. */ %><% + helpers.includeCss('store/ondemand-billing.css'); +%> + +

    Past Invoices

    + +

    Invoice #<%= invoice.id %>, dated <%= formatDate(invoice.time) %>.

    + + + + + + + + + + + + + + + <% if (transaction) { %> + + + + + + + + + <% } %> +
    Invoice status<%= invoice.status == 'paid' ? "Paid" : (invoice.status == 'pending' ? "Pending" : (invoice.status == 'refunded' ? "Refunded" : invoice.status)) %>
    Number of users<%= invoice.users %>
    CostUS $<%= dollars(centsToDollars(invoice.amt)) %>
    Paid on<%= formatDate(transaction.time) %>
    Paid using<%= transaction.payInfo %>
    + + + diff --git a/trunk/etherpad/src/templates/pro/padlist/pro-padlist.ejs b/trunk/etherpad/src/templates/pro/padlist/pro-padlist.ejs new file mode 100644 index 0000000..b762679 --- /dev/null +++ b/trunk/etherpad/src/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") %> + +
    + + <%= renderPadNav() %> + <%= renderNotice() %> + <%= renderShowingDesc(padList.length) %> + + <% if (padList.length > 0) { %> + <%= renderPadList() %> +

    <%= padList.length %> pad<% if (padList.length > 1) { %>s<% } %> <% if (isAdmin) { %>(Download all pads as a ZIP archive.) <% } %> +

    + + <% } else { %> +

    No pads in this list.

    + <% } %> + +
    + + + + + + diff --git a/trunk/etherpad/src/templates/pro/pro-payment-required.ejs b/trunk/etherpad/src/templates/pro/pro-payment-required.ejs new file mode 100644 index 0000000..3649990 --- /dev/null +++ b/trunk/etherpad/src/templates/pro/pro-payment-required.ejs @@ -0,0 +1,51 @@ +<% /* 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("etherpad.js") %> +<% helpers.includeCss("pro/payment-required.css") %> + +
    + + +
    +
    + +

    Payment Required

    + +
    <%= message %>
    +
    + + <% if (isAdmin) { %> + + Manage Billing Info + + <% } else { %> +

    Please contact one of the following site administrator to + set up a billing profile on <%= request.domain %>:

    + +
      + <% adminList.forEach(function(a) { %> +
    • <%= a.fullName %>  <<%= TT(a.email) + %>>
    • + <% }); %> +
    + + <% } %> + +

    +

    Questions? Contact <%= helpers.oemail("support") %>.

    + +
    +
    + + diff --git a/trunk/etherpad/src/templates/pro/pro_home.ejs b/trunk/etherpad/src/templates/pro/pro_home.ejs new file mode 100644 index 0000000..bcf7443 --- /dev/null +++ b/trunk/etherpad/src/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"); %> + +
    + +
    + Welcome <%= account.fullName %> + <% if (account.isAdmin) { %>(Administrator)<% } %> +
    +
    +
    + + +
    + + Create new pad + + + <% if (livePads.length > 0) { %> +
    +

    Live Pads (currently being edited)

    +
    + <%= renderLivePads() %> +
    +
    + <% } %> + + <% if (recentPads.length > 0) { %> +
    +

    Your Recent Pads:

    +
    + <%= renderRecentPads() %> +
    + View all pads... +
    +
    + <% } %> + +
    + +
    +
    + Latest News +
    + +
    +
    + June 17th, 2009 +
    +
    +
    + +
    +

    Welcome to your EtherPad Beta Account! Please report bugs by + sending email to <%= helpers.oemail("bugs") %>. + +

    We hope you enjoy EtherPad!

    + +

    Sincerely,

    + +

    Spline

    +

    +
    + +
    + + +
    + + <%= helpers.clearFloats() %> + + <% if (isPNE) { %> +
    +
    + EtherPad Private Network Edition (PNE) + Version <%= pneVersion %>
    + + <% if (isEvaluation && evalExpDate) { %> +
    + EVALUATION EDITION: Expires <%= evalExpDate.toString() + %>.
    + <% } %> +
    + <% } %> + +
    + diff --git a/trunk/etherpad/src/templates/statistics/stat_page.ejs b/trunk/etherpad/src/templates/statistics/stat_page.ejs new file mode 100644 index 0000000..22277b3 --- /dev/null +++ b/trunk/etherpad/src/templates/statistics/stat_page.ejs @@ -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. */ %><% + +helpers.includeCss('admin/admin-stats.css'); +helpers.includeJQuery(); +helpers.includeJs('statpage.js'); + +%> + +« back to admin + +
    +
      + <% statCategoryNames.forEach(function(catName) { + %>
    • + <%= catName %> +
    • <% + }); %> +
    +
    + +
     
    + +<%= helpers.clearFloats() %> + +<%= optionsForm %> + +<% function formatLatest(latest) { + if (typeof(latest) == 'string') { + return latest; + } else { + return ''+ + latest.map(function(x) { return ""; }).join("\n")+ + "
    "+x.value+""+x.description+"
    "; + } +} +%> + +<% +function displayStat(statObject) { + %> +
    +

    <%= statObject.name %>

    +
    +

    <%= statObject.displayName %>

    + + + + + +
    + <%= statObject.graph %> + <% if (statObject.dataLinks) { %> + + <% } %> + +

    Latest values:

    + <%= formatLatest(statObject.latest) %> +
    +
    +
    + <% +} + +function displayCategory(categoryName) { + %> +
    + <% + categoriesToStats[categoryName].forEach(displayStat); + %> +
    + <% +} + +statCategoryNames.forEach(displayCategory); + +%> + diff --git a/trunk/etherpad/src/templates/store/csc-help.ejs b/trunk/etherpad/src/templates/store/csc-help.ejs new file mode 100644 index 0000000..3623fac --- /dev/null +++ b/trunk/etherpad/src/templates/store/csc-help.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. */ %> + + +

    The CSC (or CVC) is the 3-digit number printed on the back of your card. +For American Express, it's the 4-digit number on the front.

    + +cc back + + + + diff --git a/trunk/etherpad/src/templates/store/eepnet-checkout/billing-info.ejs b/trunk/etherpad/src/templates/store/eepnet-checkout/billing-info.ejs new file mode 100644 index 0000000..69e0ead --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet-checkout/billing-info.ejs @@ -0,0 +1,183 @@ +<% /* 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 (!cart.billingCountry) { + cart.billingCountry = "US"; + } + helpers.includeJQuery(); + helpers.includeJs("billing_shared.js"); + helpers.includeJs("billing.js"); + + function classesPlusError(classes, id) { + return (classes || []).concat(errorIfInvalid(id) || []).join(' '); + } +%> + +<% function textRow(id, label, classes, notBillingField) { + var val = (cart[id] || ""); + var maxlen=60; + var border; + if (id == "billingCCNumber") { + if (billing.validateCcNumber(val)) { + border = "greenborder"; + } else if (billing.validateCcLength(val)) { + border = "redborder"; + } + val = obfuscateCC(val); + maxlen = 16; + } + var classString = classesPlusError((notBillingField?[]:['billingfield']).concat(classes), id); + return TR({className: classString}, + TD({className: 'pcell'}, + LABEL({htmlFor: id}, label+(label.length > 0 ? ":" : ''))), + TD({className: 'tcell'}, + INPUT({type: 'text', name: id, size:35, maxlength:maxlen, + value: val, + className: border}))); + } %> + +

    Your name:

    + + <%= textRow("billingFirstName", "First Name", [], true) %> + <%= textRow("billingLastName", "Last Name", [], true) %> +
    + +

    Payment information:

    + +<% if (request.scheme == 'https') { %> +
    +

    Your payment information will be sent securely.

    +
    +<% } %> + +<% + function purchaseType(id, title) { + var sel; + if (! cart.billingPurchaseType) { + sel = (id == 'creditcard'); + } else { + sel = (cart.billingPurchaseType == id); + } + %> + + style="display: inline-block; vertical-align: middle;"/> + + + <% + } +%> + +
    +

    ">Pay using: +<% purchaseType('creditcard', 'Credit Card'); %> +<% purchaseType('invoice', 'Invoice'); %> +<% purchaseType('paypal', 'PayPal'); %> +

    +
    + + + <%= textRow("billingCCNumber", "Credit Card Number", ['creditcardreq']) %> + +<% function cardInput(cctype) { + var classes = []; + if (cart.billingCCNumber) { + if (cctype == billing.getCcType(cart.billingCCNumber)) { + classes.push("ccimageselected"); + } + } + classes.push("ccimage"); + var img = IMG({ + src: "/static/img/billing/"+cctype+".gif", + alt: cctype, + className: classes.join(" "), + style: "vertical-align: middle", + id: "img"+cctype}); + return img; + } %> + + + + + + + "> + + + + + + + + + + + + + + <%= textRow("billingAddressLine1", "Address", ['creditcardreq', 'invoicereq']) %> + <%= textRow("billingAddressLine2", "", ['creditcardreq', 'invoicereq']) %> + <%= textRow("billingCity", "City", ['creditcardreq', 'invoicereq']) %> + + + + + + <%= textRow("billingProvince", "Province", ['creditcardreq', 'invoicereq', 'intonly'])%> + <%= textRow("billingZipCode", "Zip Code", ['creditcardreq', 'invoicereq', 'usonly']) %> + <%= textRow("billingPostalCode", "Postal Code", ['creditcardreq', 'invoicereq', 'intonly'])%> + +
      +
    + <% ["visa", "mc", "disc", "amex"].forEach(function(t) { %> + <%= cardInput(t) %> + <% }); %> +
    +
    Expiration (MM/YY): + + / + +     CSC/CVC: + + what's this? +
    (Be sure to enter your credit card billing address below.)
    Country: + +
    State: + +
    Click "<%= billingButtonName %>" below to continue with PayPal.
    + +<% if (showCouponCode) { %> +

    Optional information:

    + + <%= textRow("billingReferralCode", "Referral Code", [], true) %> +
    +<% } %> + + +<%= billingFinalPhrase %> \ No newline at end of file diff --git a/trunk/etherpad/src/templates/store/eepnet-checkout/cart.ejs b/trunk/etherpad/src/templates/store/eepnet-checkout/cart.ejs new file mode 100644 index 0000000..147ff1b --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet-checkout/cart.ejs @@ -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. */ %> +
    + + + + + + + <% if (! ('baseCost' in cart) && ! ('supportCost' in cart)) { %> + + + + <% } %> + + <% if (cart.baseCost) { %> + + + + + <% if (cart.couponProductPctDiscount) { %> + + + + + <% } %> + <% } %> + + <% if (cart.supportCost) { %> + + + + + <% if (cart.couponSupportPctDiscount) { %> + + + + + <% } %> + <% } else if (cart.baseCost) { %> + + + + + <% } %> + + <% if (cart.freeUserCount) { %> + + + + + <% } %> + + + + <% + var pctDiscount = cart.couponTotalPctDiscount; + var hasSubtotal = pctDiscount > 0; + %> + <% if (hasSubtotal) { %> + + + + + + + + + <% } %> + + + + +
    ItemCost
    + Nothing selected. +
    + Etherpad Private Network
    + <%= cart.numUsers %> users + <% if (editable) { %> + (">edit) + <% } %> + +
    US$<%= dollars(cart.baseCost) %>
    + Referral - <%= cart.couponProductPctDiscount %>% savings + -US$<%= dollars(cart.productReferralDiscount) %>
    + Support Contract + <% if (editable) { %> + (">edit) + <% } %> + +
    + 1 year +
    US$<%= dollars(cart.supportCost) %>
    + Referral - <%= cart.couponSupportPctDiscount %>% savings + -US$<%= dollars(cart.supportReferralDiscount) %>
    + No Support Contract + <% if (editable) { %> + (">edit) + <% } %> + + US$0.00
    + Bonus Users
    + + Referral - <%= cart.freeUserCount %> free + user<%= (cart.freeUserCount == 1 ? '' : "s") %> + +
    US$0.00
     
    SubtotalUS$<%= dollars(cart.subTotal) %>
    Referral - <%= pctDiscount %>% savings-US$<%= dollars(cart.totalReferralDiscount) %>
    TotalUS$<%= dollars(cart.total) %>
    +
    \ No newline at end of file diff --git a/trunk/etherpad/src/templates/store/eepnet-checkout/checkout-template.ejs b/trunk/etherpad/src/templates/store/eepnet-checkout/checkout-template.ejs new file mode 100644 index 0000000..817f0eb --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet-checkout/checkout-template.ejs @@ -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. */ %><% helpers.includeCss("store/eepnet-checkout.css"); %> +<% helpers.includeJQuery() %> +<% helpers.includeJs("etherpad.js") %> +<% helpers.setHtmlTitle(title); %> + +<% +var selectCount = 0; +function select(id, title) { + var className = 'poslabel'; + if (pageId == id) { + className += ' current'; + } + selectCount++; + return SPAN({className: className}, selectCount+". "+title); +} +%> + +
    +
    + +

    Private Network Edition: Purchase Online

    + + <%= helpers.rafterNote() %> + +
    +
    diff --git a/trunk/etherpad/src/templates/store/eepnet-checkout/confirmation.ejs b/trunk/etherpad/src/templates/store/eepnet-checkout/confirmation.ejs new file mode 100644 index 0000000..3b38775 --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet-checkout/confirmation.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.includeJs('confirmation.js'); +%> + +<% if (request.params.frompaypal) { + handlePayPalRedirect(); +} %> + +<%= displaySummary(true) %> + +<% switch(cart.billingPurchaseType) { + case 'creditcard': case 'paypal': %> +

    If this looks good, click "Purchase" below to complete your purchase.

    + <% break; + case 'invoice': %> +

    If this looks good, print this page and mail it along with a check or other + prearranged payment to:

    AppJet, Inc.
    Pier 38 - Suite 210
    The Embarcadero
    San Francisco, CA 94107

    + <% break; +} + %> + diff --git a/trunk/etherpad/src/templates/store/eepnet-checkout/license-info.ejs b/trunk/etherpad/src/templates/store/eepnet-checkout/license-info.ejs new file mode 100644 index 0000000..4d710f2 --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet-checkout/license-info.ejs @@ -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. */ %>

    Your license key will be issued to a particular individual at your organization, and will be delivered to the email address you specify below.

    + + + + + + + + + + + + + + + + + + + + +
    Email address to receive license key:
    Name of license owner (your name):
    Organization or company name:
    + /> + +
    \ No newline at end of file diff --git a/trunk/etherpad/src/templates/store/eepnet-checkout/purchase.ejs b/trunk/etherpad/src/templates/store/eepnet-checkout/purchase.ejs new file mode 100644 index 0000000..49cb3bb --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet-checkout/purchase.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. */ %>

    Thank you for choosing to purchase Enterprise EtherPad Private Network Edition.

    + +

    A license allows a certain number of concurrent users, at a one-time cost of US $<%= dollars(costPerUser) %> per user with no recurring costs. Learn more about how we count users.

    + +

    How many users should your license support?

    + + + + + + +
    Number of Users at US $<%= dollars(costPerUser) %>/user:
    + + + + + + +
    Referral Code (optional):
    diff --git a/trunk/etherpad/src/templates/store/eepnet-checkout/receipt.ejs b/trunk/etherpad/src/templates/store/eepnet-checkout/receipt.ejs new file mode 100644 index 0000000..8d3a2a5 --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet-checkout/receipt.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. */ %><% if (cart.showStartOverMessage) { %> +
    + Your purchase is complete! To purchase another item, please return to the purchase page. +
    +<% } + +switch(cart.status) { + case 'success': + %>

    Thank you for your purchase! This page serves as your receipt. Please print it for your records. You will receive a copy of this receipt and license key by email shortly.

    <% + break; + case 'pending': + %>

    Your purchase is pending approval by PayPal. Once it clears, + usually in 2-5 business days, you will receive a copy of this receipt and + your license key by email.

    <% + break; +} %> + +<% + var instructions = "/ep/pne-manual"; + var download = "/ep/store/eepnet-download-nextsteps"; +%> + +

    To install EtherPad Private Network Edition:

    + + +

    + +<%= displaySummary() %> diff --git a/trunk/etherpad/src/templates/store/eepnet-checkout/summary.ejs b/trunk/etherpad/src/templates/store/eepnet-checkout/summary.ejs new file mode 100644 index 0000000..753873c --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet-checkout/summary.ejs @@ -0,0 +1,91 @@ +<% /* 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 textRow(tcell, pcell) { + %> + <%= tcell %> + <%= pcell %> + + <% +} +var keyData = { + ownerName: cart.ownerName, + orgName: cart.orgName +} +if (cart.licenseKey) { + var parts = cart.licenseKey.split(":"); + keyData.ownerName = parts[0]; + keyData.orgName = parts[1]; + keyData.key = parts[2]; + + keyData.keyLine1 = keyData.key.substring(0, keyData.key.length/3); + keyData.keyLine2 = keyData.key.substring(keyData.key.length/3, 2*keyData.key.length/3); + keyData.keyLine3 = keyData.key.substring(2*keyData.key.length/3, keyData.key.length); +} + +function makeRows(arr) { + arr.forEach(function(arr) { textRow(arr[0], arr[1]); }); +} +%> + +

    License Information <% if (editable) { %>(">edit)<% } %>

    + + + <% + makeRows([ + [ "Administrator name:", keyData.ownerName ], + [ "Organization/Company:", keyData.orgName ], + [ "Email address for delivery:", cart.email ], + [ "Total users:", cart.userCount ] + ]); + if (keyData.key) { + textRow("License key:", keyData.keyLine1+"
    "+keyData.keyLine2+"
    "+keyData.keyLine3); + %><% + } + %> +
    + +

    Payment Information <% if (editable) { %>(">edit)<% } %>

    + + + <% + var isUs = cart.billingCountry == "US"; + switch(cart.billingPurchaseType) { + case 'creditcard': + makeRows([ + [ "Credit card number:", obfuscateCC(cart.billingCCNumber) ], + [ "Expiration date:", cart.billingExpirationMonth+" / 20"+cart.billingExpirationYear ] + ]); + // falling through intentional. + case 'invoice': + makeRows([ + [ "Purchaser name:", cart.billingFirstName + " " + cart.billingLastName ], + [ "Purchaser address: ", cart.billingAddressLine1 + "
    " + + (cart.billingAddressLine2 ? cart.billingAddressLine2 + "
    " : "") + + cart.billingCity + ", " + + (isUs?cart.billingState:cart.billingProvince) + "
    " + + (isUs?cart.billingZipCode:cart.billingPostalCode) + + (isUs?'':', '+cart.billingCountry) ], + [ "Invoice number: ", cart.invoiceId ] + ]); + break; + case 'paypal': + textRow("Paid using:", "PayPal"); + textRow("InvoiceNumber:", cart.invoiceId); + } + %> +
    + +

    Summary of Charges

    + +<%= displayCart("shoppingconfirmation", editable) %> diff --git a/trunk/etherpad/src/templates/store/eepnet-checkout/support-contract.ejs b/trunk/etherpad/src/templates/store/eepnet-checkout/support-contract.ejs new file mode 100644 index 0000000..ff33fda --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet-checkout/support-contract.ejs @@ -0,0 +1,41 @@ +<% /* 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. */ %>

    A support contract gives you free upgrades and help directly from the engineers who developed EtherPad. Support contracts cost US $<%= dollars(costPerUser * supportCostPct/100) %> per user per year, with a US $<%= dollars(supportMinCost) %> per year minimum. Learn more about support contracts.

    + +

    For the <%= cart.numUsers %>-user license you've selected, a support contract costs US $<%= discountedSupportCost() !== undefined ? dollars(discountedSupportCost()) : dollars(supportCost()) %>. + +

    Do you want a support contract?

    + + + + + + + + + + + +
    + /> + + +
    + /> + + +
    \ No newline at end of file diff --git a/trunk/etherpad/src/templates/store/eepnet_download.ejs b/trunk/etherpad/src/templates/store/eepnet_download.ejs new file mode 100644 index 0000000..42c89ee --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet_download.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. */ %><% helpers.includeCss("store/store.css") %> +<% helpers.includeJQuery() %> +<% helpers.includeJs("store.js") %> + +
    + + <% if (message) { %> +
    + <%= message %> +
    + <% } %> + +

    Download EtherPad Private Network Edition:

    + +

    +
    + + + + + Download Now + +

    Version: <%= versionString %>


    + +
    + + +
    diff --git a/trunk/etherpad/src/templates/store/eepnet_eval_nextsteps.ejs b/trunk/etherpad/src/templates/store/eepnet_eval_nextsteps.ejs new file mode 100644 index 0000000..4c4cec4 --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet_eval_nextsteps.ejs @@ -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. */ %>
    +

    Downloading...

    + +

    Your download should begin automatically. If it does not, you + can click this link:

    + + + +

    Next Steps

    + + + +
    + + + diff --git a/trunk/etherpad/src/templates/store/eepnet_eval_signup.ejs b/trunk/etherpad/src/templates/store/eepnet_eval_signup.ejs new file mode 100644 index 0000000..5a1edf4 --- /dev/null +++ b/trunk/etherpad/src/templates/store/eepnet_eval_signup.ejs @@ -0,0 +1,125 @@ +<% /* 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("Sign up for EtherPad PNE Free Trial"); %> +<% helpers.includeJQuery() %> +<% helpers.includeJs("etherpad.js") %> +<% helpers.includeJs("store.js") %> +<% helpers.includeCss("store/store.css") %> + +<% function renderField(maxlen, id, title) { + var oldValue = (oldData[id] || ""); + return DIV(P(LABEL({htmlFor: id}, title), + INPUT({maxlength: maxlen, + type: "text", + className: "signupData", + name: id, + id: id, + value: oldValue}))); + } + + function renderWebLeadField(name) { + return INPUT({type: 'hidden', name: name, id: "wl_"+name, value: ""}); + } +%> + +
    + +

    Private Network Edition: <%= trialDays %>-Day Free Trial

    + +

    Enter your information here to download a free <%= trialDays + %>-day trial of EtherPad Private Network Edition.

    + + + + + +
    + +
    + + <% /* note: these fields should match exactly the eepnet-pricingcontact + form in pricing_eepnet.ejs */ %> + + <%= renderField(40, "firstName", "First Name:") %> + <%= renderField(80, "lastName", "Last Name:") %> + <%= renderField(80, "email", "Your Email (license key will be sent here):") %> + <%= renderField(40, "orgName", "Company/Organization:") %> + +

    + + +

    + + <%= renderField(40, "jobTitle", "Your Title:") %> + <%= renderField(40, "phone", "Phone Number:") %> + <%= renderField(160, "estUsers", "Estimated number of users:") %> + +
    + +

    + +
    + +

    If you already have a license, you + can skip directly to download.

    + +

    You can also recover a + lost license key.

    + +

    Questions? Email <%= helpers.oemail("sales") %>.

    + +
    + +
    + + + + <% [ + "oid", + "first_name", + "last_name", + "email", + "company", + "title", + "phone", + "00N80000003FYtG", + "00N80000003FYto", + "00N80000003FYuI", + "lead_source", + "industry" + ].forEach(function(f) { %> + + <%= renderWebLeadField(f) %> + + <% }); %> + +
    + + + -- cgit v1.2.3-1-g7c22