From 69b62b6f33bea9243f289d75d9ec8c6016b5d1cb Mon Sep 17 00:00:00 2001 From: Jacques Distler Date: Mon, 22 Jan 2007 07:43:50 -0600 Subject: [PATCH] Checkout of Instiki Trunk 1/21/2007. --- CHANGELOG | 270 ++ README | 113 + app/controllers/admin_controller.rb | 94 + app/controllers/application.rb | 190 ++ app/controllers/cache_sweeping_helper.rb | 23 + app/controllers/file_controller.rb | 100 + app/controllers/revision_sweeper.rb | 29 + app/controllers/web_sweeper.rb | 14 + app/controllers/wiki_controller.rb | 429 +++ app/helpers/application_helper.rb | 93 + app/helpers/wiki_helper.rb | 89 + app/models/author.rb | 18 + app/models/page.rb | 121 + app/models/page_observer.rb | 15 + app/models/page_set.rb | 92 + app/models/revision.rb | 4 + app/models/system.rb | 4 + app/models/web.rb | 147 + app/models/wiki.rb | 92 + app/models/wiki_file.rb | 64 + app/models/wiki_reference.rb | 82 + app/views/admin/create_system.rhtml | 86 + app/views/admin/create_web.rhtml | 72 + app/views/admin/edit_web.rhtml | 136 + app/views/file/file.rhtml | 33 + app/views/file/import.rhtml | 23 + app/views/layouts/default.rhtml | 79 + app/views/markdown_help.rhtml | 12 + app/views/mixed_help.rhtml | 7 + app/views/navigation.rhtml | 28 + app/views/rdoc_help.rhtml | 12 + app/views/textile_help.rhtml | 24 + app/views/wiki/_inbound_links.rhtml | 13 + app/views/wiki/authors.rhtml | 11 + app/views/wiki/edit.rhtml | 40 + app/views/wiki/export.rhtml | 12 + app/views/wiki/feeds.rhtml | 14 + app/views/wiki/list.rhtml | 64 + app/views/wiki/locked.rhtml | 23 + app/views/wiki/login.rhtml | 22 + app/views/wiki/new.rhtml | 33 + app/views/wiki/page.rhtml | 51 + app/views/wiki/print.rhtml | 14 + app/views/wiki/published.rhtml | 9 + app/views/wiki/recently_revised.rhtml | 19 + app/views/wiki/revision.rhtml | 28 + app/views/wiki/rollback.rhtml | 39 + app/views/wiki/rss_feed.rxml | 21 + app/views/wiki/search.rhtml | 38 + app/views/wiki/tex.rhtml | 23 + app/views/wiki/tex_web.rhtml | 35 + app/views/wiki/web_list.rhtml | 25 + app/views/wiki_words_help.rhtml | 9 + config/boot.rb | 17 + config/database.yml | 105 + config/environment.rb | 29 + config/environments/development.rb | 17 + config/environments/production.rb | 17 + config/environments/test.rb | 23 + config/routes.rb | 38 + config/spam_patterns.txt | 97 + db/migrate/001_beta1_schema.rb | 56 + db/migrate/002_beta2_changes_bulk.rb | 36 + instiki | 7 + instiki.cmd | 2 + instiki.rb | 2 + lib/bluecloth_tweaked.rb | 1127 +++++++ lib/chunks/category.rb | 33 + lib/chunks/chunk.rb | 79 + lib/chunks/engines.rb | 62 + lib/chunks/include.rb | 49 + lib/chunks/literal.rb | 31 + lib/chunks/nowiki.rb | 28 + lib/chunks/test.rb | 18 + lib/chunks/uri.rb | 182 ++ lib/chunks/wiki.rb | 143 + lib/diff.rb | 316 ++ lib/instiki_errors.rb | 15 + lib/native/win32/sqlite3.dll | Bin 0 -> 250368 bytes lib/page_renderer.rb | 134 + lib/rdocsupport.rb | 152 + lib/redcloth.rb | 1130 +++++++ lib/redcloth_for_tex.rb | 736 +++++ lib/url_generator.rb | 121 + lib/wiki_content.rb | 202 ++ lib/wiki_words.rb | 23 + natives/osx/desktop_launcher/AppDelegate.h | 18 + natives/osx/desktop_launcher/AppDelegate.mm | 109 + natives/osx/desktop_launcher/Credits.html | 16 + .../English.lproj/InfoPlist.strings | Bin 0 -> 550 bytes .../English.lproj/MainMenu.nib/classes.nib | 13 + .../English.lproj/MainMenu.nib/info.nib | 24 + .../English.lproj/MainMenu.nib/objects.nib | Bin 0 -> 1607 bytes natives/osx/desktop_launcher/Info.plist | 13 + .../Instiki.xcode/project.pbxproj | 592 ++++ .../osx/desktop_launcher/Instiki_Prefix.pch | 7 + natives/osx/desktop_launcher/MakeDMG.sh | 9 + natives/osx/desktop_launcher/main.mm | 14 + natives/osx/desktop_launcher/version.plist | 16 + public/.htaccess | 40 + public/404.html | 8 + public/500.html | 8 + public/dispatch.cgi | 10 + public/dispatch.fcgi | 24 + public/dispatch.rb | 10 + public/favicon.ico | Bin 0 -> 4710 bytes public/images/.images_go_here | 0 public/images/bg_normal.gif | Bin 0 -> 48 bytes public/images/bg_protected.gif | Bin 0 -> 48 bytes public/javascripts/controls.js | 708 +++++ public/javascripts/dragdrop.js | 516 ++++ public/javascripts/edit_web.js | 55 + public/javascripts/effects.js | 1101 +++++++ public/javascripts/prototype.js | 1724 +++++++++++ public/javascripts/scriptaculous.js | 47 + public/javascripts/slider.js | 258 ++ public/robots.txt | 1 + public/stylesheets/instiki.css | 320 ++ rakefile.rb | 10 + script/benchmarker | 19 + script/breakpointer | 4 + script/console | 23 + script/destroy | 7 + script/generate | 7 + script/import_storage | 228 ++ script/profiler | 34 + script/reset_references | 28 + script/runner | 29 + script/server | 49 + test/fixtures/exported_markup.zip | Bin 0 -> 283 bytes test/fixtures/pages.yml | 55 + test/fixtures/rails.gif | Bin 0 -> 8533 bytes test/fixtures/revisions.yml | 83 + test/fixtures/system.yml | 2 + test/fixtures/webs.yml | 15 + test/fixtures/wiki_references.yml | 112 + test/functional/admin_controller_test.rb | 234 ++ test/functional/application_test.rb | 30 + test/functional/file_controller_test.rb | 114 + test/functional/routes_test.rb | 51 + test/functional/wiki_controller_test.rb | 676 +++++ test/test_helper.rb | 168 ++ test/unit/chunks/category_test.rb | 22 + test/unit/chunks/nowiki_test.rb | 15 + test/unit/chunks/wiki_test.rb | 98 + test/unit/diff_test.rb | 110 + test/unit/page_renderer_test.rb | 389 +++ test/unit/page_test.rb | 122 + test/unit/redcloth_for_tex_test.rb | 69 + test/unit/uri_test.rb | 217 ++ test/unit/web_test.rb | 105 + test/unit/wiki_file_test.rb | 84 + test/unit/wiki_words_test.rb | 14 + test/watir/e2e.rb | 370 +++ vendor/plugins/dnsbl_check/README | 35 + vendor/plugins/dnsbl_check/init.rb | 1 + vendor/plugins/dnsbl_check/lib/dnsbl_check.rb | 58 + vendor/plugins/rubyzip-0.9.1/ChangeLog | 1081 +++++++ vendor/plugins/rubyzip-0.9.1/NEWS | 144 + vendor/plugins/rubyzip-0.9.1/README | 72 + vendor/plugins/rubyzip-0.9.1/Rakefile | 110 + vendor/plugins/rubyzip-0.9.1/TODO | 16 + vendor/plugins/rubyzip-0.9.1/install.rb | 22 + .../plugins/rubyzip-0.9.1/lib/zip/ioextras.rb | 155 + .../rubyzip-0.9.1/lib/zip/stdrubyext.rb | 111 + .../lib/zip/tempfile_bugfixed.rb | 195 ++ vendor/plugins/rubyzip-0.9.1/lib/zip/zip.rb | 1847 ++++++++++++ .../rubyzip-0.9.1/lib/zip/zipfilesystem.rb | 609 ++++ .../rubyzip-0.9.1/lib/zip/ziprequire.rb | 90 + .../plugins/rubyzip-0.9.1/samples/example.rb | 69 + .../samples/example_filesystem.rb | 34 + .../rubyzip-0.9.1/samples/gtkRubyzip.rb | 86 + vendor/plugins/rubyzip-0.9.1/samples/qtzip.rb | 101 + .../rubyzip-0.9.1/samples/write_simple.rb | 13 + .../plugins/rubyzip-0.9.1/samples/zipfind.rb | 74 + vendor/plugins/rubyzip-0.9.1/test/alltests.rb | 9 + .../plugins/rubyzip-0.9.1/test/data/file1.txt | 46 + .../test/data/file1.txt.deflatedData | Bin 0 -> 482 bytes .../plugins/rubyzip-0.9.1/test/data/file2.txt | 1504 ++++++++++ .../rubyzip-0.9.1/test/data/notzippedruby.rb | 7 + .../rubyzip-0.9.1/test/data/rubycode.zip | Bin 0 -> 617 bytes .../rubyzip-0.9.1/test/data/rubycode2.zip | Bin 0 -> 261 bytes .../rubyzip-0.9.1/test/data/testDirectory.bin | Bin 0 -> 303 bytes .../rubyzip-0.9.1/test/data/zipWithDirs.zip | Bin 0 -> 1934 bytes .../rubyzip-0.9.1/test/gentestfiles.rb | 157 + .../rubyzip-0.9.1/test/ioextrastest.rb | 208 ++ .../rubyzip-0.9.1/test/stdrubyexttest.rb | 52 + .../rubyzip-0.9.1/test/zipfilesystemtest.rb | 831 ++++++ .../rubyzip-0.9.1/test/ziprequiretest.rb | 43 + vendor/plugins/rubyzip-0.9.1/test/ziptest.rb | 1599 ++++++++++ vendor/plugins/sqlite3-ruby/sqlite3.rb | 33 + .../plugins/sqlite3-ruby/sqlite3/constants.rb | 81 + .../plugins/sqlite3-ruby/sqlite3/database.rb | 745 +++++ .../sqlite3-ruby/sqlite3/driver/dl/api.rb | 184 ++ .../sqlite3-ruby/sqlite3/driver/dl/driver.rb | 338 +++ .../sqlite3/driver/native/driver.rb | 243 ++ vendor/plugins/sqlite3-ruby/sqlite3/errors.rb | 100 + .../plugins/sqlite3-ruby/sqlite3/pragmas.rb | 254 ++ .../plugins/sqlite3-ruby/sqlite3/resultset.rb | 190 ++ .../plugins/sqlite3-ruby/sqlite3/statement.rb | 258 ++ .../sqlite3-ruby/sqlite3/translator.rb | 136 + vendor/plugins/sqlite3-ruby/sqlite3/value.rb | 89 + .../plugins/sqlite3-ruby/sqlite3/version.rb | 45 + vendor/rails/actionmailer/CHANGELOG | 257 ++ vendor/rails/actionmailer/MIT-LICENSE | 21 + vendor/rails/actionmailer/README | 148 + vendor/rails/actionmailer/Rakefile | 95 + vendor/rails/actionmailer/install.rb | 30 + .../rails/actionmailer/lib/action_mailer.rb | 51 + .../lib/action_mailer/adv_attr_accessor.rb | 31 + .../actionmailer/lib/action_mailer/base.rb | 528 ++++ .../actionmailer/lib/action_mailer/helpers.rb | 115 + .../lib/action_mailer/mail_helper.rb | 19 + .../actionmailer/lib/action_mailer/part.rb | 113 + .../lib/action_mailer/part_container.rb | 51 + .../actionmailer/lib/action_mailer/quoting.rb | 59 + .../actionmailer/lib/action_mailer/utils.rb | 8 + .../lib/action_mailer/vendor/text/format.rb | 1466 +++++++++ .../lib/action_mailer/vendor/tmail.rb | 3 + .../lib/action_mailer/vendor/tmail/address.rb | 242 ++ .../action_mailer/vendor/tmail/attachments.rb | 39 + .../lib/action_mailer/vendor/tmail/base64.rb | 71 + .../lib/action_mailer/vendor/tmail/config.rb | 69 + .../lib/action_mailer/vendor/tmail/encode.rb | 467 +++ .../lib/action_mailer/vendor/tmail/facade.rb | 552 ++++ .../lib/action_mailer/vendor/tmail/header.rb | 914 ++++++ .../lib/action_mailer/vendor/tmail/info.rb | 35 + .../lib/action_mailer/vendor/tmail/loader.rb | 1 + .../lib/action_mailer/vendor/tmail/mail.rb | 447 +++ .../lib/action_mailer/vendor/tmail/mailbox.rb | 433 +++ .../lib/action_mailer/vendor/tmail/mbox.rb | 1 + .../lib/action_mailer/vendor/tmail/net.rb | 280 ++ .../action_mailer/vendor/tmail/obsolete.rb | 135 + .../lib/action_mailer/vendor/tmail/parser.rb | 1522 ++++++++++ .../lib/action_mailer/vendor/tmail/port.rb | 377 +++ .../lib/action_mailer/vendor/tmail/quoting.rb | 125 + .../lib/action_mailer/vendor/tmail/scanner.rb | 41 + .../action_mailer/vendor/tmail/scanner_r.rb | 263 ++ .../action_mailer/vendor/tmail/stringio.rb | 277 ++ .../lib/action_mailer/vendor/tmail/tmail.rb | 1 + .../lib/action_mailer/vendor/tmail/utils.rb | 238 ++ .../actionmailer/lib/action_mailer/version.rb | 9 + .../fixtures/helper_mailer/use_helper.rhtml | 1 + .../helper_mailer/use_helper_method.rhtml | 1 + .../helper_mailer/use_mail_helper.rhtml | 5 + .../helper_mailer/use_test_helper.rhtml | 1 + .../test/fixtures/helpers/test_helper.rb | 5 + ...ltipart_with_template_path_with_dots.rhtml | 1 + .../actionmailer/test/fixtures/raw_email | 14 + .../actionmailer/test/fixtures/raw_email10 | 20 + .../actionmailer/test/fixtures/raw_email11 | 34 + .../actionmailer/test/fixtures/raw_email12 | 32 + .../actionmailer/test/fixtures/raw_email13 | 29 + .../actionmailer/test/fixtures/raw_email2 | 114 + .../actionmailer/test/fixtures/raw_email3 | 70 + .../actionmailer/test/fixtures/raw_email4 | 59 + .../actionmailer/test/fixtures/raw_email5 | 19 + .../actionmailer/test/fixtures/raw_email6 | 20 + .../actionmailer/test/fixtures/raw_email7 | 66 + .../actionmailer/test/fixtures/raw_email8 | 47 + .../actionmailer/test/fixtures/raw_email9 | 28 + .../test/fixtures/templates/signed_up.rhtml | 3 + ...implicitly_multipart_example.ignored.rhtml | 1 + ...plicitly_multipart_example.text.html.rhtml | 10 + ...licitly_multipart_example.text.plain.rhtml | 2 + ...plicitly_multipart_example.text.yaml.rhtml | 1 + .../test/fixtures/test_mailer/signed_up.rhtml | 3 + .../actionmailer/test/mail_helper_test.rb | 97 + .../actionmailer/test/mail_render_test.rb | 48 + .../actionmailer/test/mail_service_test.rb | 832 ++++++ .../rails/actionmailer/test/quoting_test.rb | 48 + vendor/rails/actionmailer/test/tmail_test.rb | 17 + vendor/rails/actionpack/CHANGELOG | 2595 ++++++++++++++++ vendor/rails/actionpack/MIT-LICENSE | 21 + vendor/rails/actionpack/README | 471 +++ vendor/rails/actionpack/RUNNING_UNIT_TESTS | 25 + vendor/rails/actionpack/Rakefile | 151 + vendor/rails/actionpack/examples/.htaccess | 24 + .../examples/address_book/index.rhtml | 33 + .../examples/address_book/layout.rhtml | 8 + .../examples/address_book_controller.cgi | 9 + .../examples/address_book_controller.fcgi | 6 + .../examples/address_book_controller.rb | 52 + .../examples/address_book_controller.rbx | 4 + vendor/rails/actionpack/examples/benchmark.rb | 52 + .../examples/benchmark_with_ar.fcgi | 89 + .../actionpack/examples/blog_controller.cgi | 53 + .../actionpack/examples/debate/index.rhtml | 14 + .../examples/debate/new_topic.rhtml | 22 + .../actionpack/examples/debate/topic.rhtml | 32 + .../actionpack/examples/debate_controller.cgi | 57 + vendor/rails/actionpack/filler.txt | 50 + vendor/rails/actionpack/install.rb | 30 + .../rails/actionpack/lib/action_controller.rb | 83 + .../lib/action_controller/assertions.rb | 320 ++ .../actionpack/lib/action_controller/base.rb | 1060 +++++++ .../lib/action_controller/benchmarking.rb | 92 + .../lib/action_controller/caching.rb | 555 ++++ .../lib/action_controller/cgi_ext/cgi_ext.rb | 43 + .../action_controller/cgi_ext/cgi_methods.rb | 217 ++ .../cgi_ext/cookie_performance_fix.rb | 125 + .../cgi_ext/raw_post_data_fix.rb | 74 + .../lib/action_controller/cgi_process.rb | 207 ++ .../lib/action_controller/code_generation.rb | 235 ++ .../lib/action_controller/components.rb | 186 ++ .../lib/action_controller/cookies.rb | 77 + .../lib/action_controller/dependencies.rb | 83 + .../deprecated_assertions.rb | 204 ++ .../action_controller/deprecated_redirects.rb | 17 + .../deprecated_request_methods.rb | 34 + .../lib/action_controller/filters.rb | 444 +++ .../actionpack/lib/action_controller/flash.rb | 178 ++ .../lib/action_controller/helpers.rb | 134 + .../lib/action_controller/integration.rb | 528 ++++ .../lib/action_controller/layout.rb | 310 ++ .../action_controller/macros/auto_complete.rb | 52 + .../macros/in_place_editing.rb | 32 + .../lib/action_controller/mime_responds.rb | 169 ++ .../lib/action_controller/mime_type.rb | 142 + .../lib/action_controller/pagination.rb | 401 +++ .../lib/action_controller/request.rb | 257 ++ .../lib/action_controller/rescue.rb | 139 + .../lib/action_controller/response.rb | 17 + .../lib/action_controller/routing.rb | 716 +++++ .../lib/action_controller/scaffolding.rb | 190 ++ .../session/active_record_store.rb | 333 +++ .../action_controller/session/drb_server.rb | 32 + .../action_controller/session/drb_store.rb | 31 + .../session/mem_cache_store.rb | 101 + .../action_controller/session_management.rb | 145 + .../lib/action_controller/streaming.rb | 147 + .../rescues/_request_and_response.rhtml | 44 + .../templates/rescues/_trace.rhtml | 26 + .../templates/rescues/diagnostics.rhtml | 11 + .../templates/rescues/layout.rhtml | 29 + .../templates/rescues/missing_template.rhtml | 2 + .../templates/rescues/routing_error.rhtml | 10 + .../templates/rescues/template_error.rhtml | 21 + .../templates/rescues/unknown_action.rhtml | 2 + .../templates/scaffolds/edit.rhtml | 7 + .../templates/scaffolds/layout.rhtml | 69 + .../templates/scaffolds/list.rhtml | 27 + .../templates/scaffolds/new.rhtml | 6 + .../templates/scaffolds/show.rhtml | 9 + .../lib/action_controller/test_process.rb | 482 +++ .../lib/action_controller/url_rewriter.rb | 74 + .../vendor/html-scanner/html/document.rb | 64 + .../vendor/html-scanner/html/node.rb | 533 ++++ .../vendor/html-scanner/html/tokenizer.rb | 105 + .../vendor/html-scanner/html/version.rb | 11 + .../lib/action_controller/vendor/xml_node.rb | 97 + .../action_controller/vendor/xml_simple.rb | 1019 +++++++ .../lib/action_controller/verification.rb | 96 + vendor/rails/actionpack/lib/action_pack.rb | 24 + .../actionpack/lib/action_pack/version.rb | 9 + vendor/rails/actionpack/lib/action_view.rb | 32 + .../rails/actionpack/lib/action_view/base.rb | 535 ++++ .../lib/action_view/compiled_templates.rb | 70 + .../helpers/active_record_helper.rb | 200 ++ .../action_view/helpers/asset_tag_helper.rb | 167 ++ .../action_view/helpers/benchmark_helper.rb | 24 + .../lib/action_view/helpers/cache_helper.rb | 10 + .../lib/action_view/helpers/capture_helper.rb | 128 + .../lib/action_view/helpers/date_helper.rb | 307 ++ .../lib/action_view/helpers/debug_helper.rb | 17 + .../lib/action_view/helpers/form_helper.rb | 406 +++ .../helpers/form_options_helper.rb | 361 +++ .../action_view/helpers/form_tag_helper.rb | 138 + .../helpers/java_script_macros_helper.rb | 220 ++ .../action_view/helpers/javascript_helper.rb | 132 + .../helpers/javascripts/controls.js | 815 ++++++ .../helpers/javascripts/dragdrop.js | 913 ++++++ .../helpers/javascripts/effects.js | 958 ++++++ .../helpers/javascripts/prototype.js | 2006 +++++++++++++ .../lib/action_view/helpers/number_helper.rb | 109 + .../action_view/helpers/pagination_helper.rb | 86 + .../action_view/helpers/prototype_helper.rb | 901 ++++++ .../helpers/scriptaculous_helper.rb | 135 + .../lib/action_view/helpers/tag_helper.rb | 50 + .../lib/action_view/helpers/text_helper.rb | 369 +++ .../lib/action_view/helpers/url_helper.rb | 313 ++ .../actionpack/lib/action_view/partials.rb | 128 + .../lib/action_view/template_error.rb | 86 + vendor/rails/actionpack/test/abstract_unit.rb | 14 + .../actionpack/test/active_record_unit.rb | 88 + .../active_record_assertions_test.rb | 84 + .../activerecord/active_record_store_test.rb | 174 ++ .../test/activerecord/pagination_test.rb | 161 + .../controller/action_pack_assertions_test.rb | 493 ++++ .../test/controller/addresses_render_test.rb | 49 + .../actionpack/test/controller/base_test.rb | 66 + .../test/controller/benchmark_test.rb | 33 + .../test/controller/caching_filestore.rb | 74 + .../test/controller/capture_test.rb | 80 + .../actionpack/test/controller/cgi_test.rb | 363 +++ .../test/controller/components_test.rb | 129 + .../actionpack/test/controller/cookie_test.rb | 80 + .../test/controller/custom_handler_test.rb | 41 + .../test/controller/fake_controllers.rb | 17 + .../test/controller/filter_params_test.rb | 42 + .../test/controller/filters_test.rb | 410 +++ .../actionpack/test/controller/flash_test.rb | 85 + .../controller/fragment_store_setting_test.rb | 45 + .../actionpack/test/controller/helper_test.rb | 187 ++ .../actionpack/test/controller/layout_test.rb | 73 + .../test/controller/mime_responds_test.rb | 257 ++ .../test/controller/mime_type_test.rb | 24 + .../test/controller/new_render_test.rb | 600 ++++ .../test/controller/raw_post_test.rb | 31 + .../test/controller/redirect_test.rb | 143 + .../actionpack/test/controller/render_test.rb | 246 ++ .../test/controller/request_test.rb | 266 ++ .../test/controller/routing_test.rb | 1049 +++++++ .../test/controller/send_file_test.rb | 109 + .../controller/session_management_test.rb | 94 + .../actionpack/test/controller/test_test.rb | 411 +++ .../test/controller/url_rewriter_test.rb | 46 + .../test/controller/verification_test.rb | 225 ++ .../test/controller/webservice_test.rb | 255 ++ .../test/fixtures/addresses/list.rhtml | 1 + .../poorly_placed_controller.rb | 7 + .../nested_controller.rb | 3 + .../a_class_that_contains_a_controller.rb | 7 + .../actionpack/test/fixtures/companies.yml | 24 + .../rails/actionpack/test/fixtures/company.rb | 9 + .../test/fixtures/db_definitions/sqlite.sql | 42 + .../actionpack/test/fixtures/developer.rb | 7 + .../actionpack/test/fixtures/developers.yml | 21 + .../test/fixtures/developers_projects.yml | 13 + .../actionpack/test/fixtures/dont_load.rb | 3 + .../test/fixtures/fun/games/hello_world.rhtml | 1 + .../test/fixtures/helpers/abc_helper.rb | 5 + .../test/fixtures/helpers/fun/games_helper.rb | 3 + .../test/fixtures/helpers/fun/pdf_helper.rb | 3 + .../controller_name_space/nested.rhtml | 1 + .../fixtures/layout_tests/layouts/item.rhtml | 1 + .../layout_tests/layouts/layout_test.rhtml | 1 + .../layouts/third_party_template_library.mab | 1 + .../fixtures/layout_tests/views/hello.rhtml | 1 + .../test/fixtures/layouts/builder.rxml | 3 + .../test/fixtures/layouts/standard.rhtml | 1 + .../fixtures/layouts/talk_from_action.rhtml | 2 + .../test/fixtures/layouts/yield.rhtml | 2 + .../test/fixtures/multipart/binary_file | Bin 0 -> 19844 bytes .../test/fixtures/multipart/large_text_file | 10 + .../test/fixtures/multipart/mixed_files | Bin 0 -> 19937 bytes .../test/fixtures/multipart/mona_lisa.jpg | Bin 0 -> 159528 bytes .../test/fixtures/multipart/single_parameter | 5 + .../test/fixtures/multipart/text_file | 10 + .../rails/actionpack/test/fixtures/project.rb | 3 + .../actionpack/test/fixtures/projects.yml | 7 + .../test/fixtures/public/images/rails.png | Bin 0 -> 1787 bytes .../actionpack/test/fixtures/replies.yml | 13 + .../rails/actionpack/test/fixtures/reply.rb | 5 + .../respond_to/all_types_with_layout.rhtml | 1 + .../respond_to/all_types_with_layout.rjs | 1 + .../respond_to/layouts/standard.rhtml | 1 + .../fixtures/respond_to/using_defaults.rhtml | 1 + .../fixtures/respond_to/using_defaults.rjs | 1 + .../fixtures/respond_to/using_defaults.rxml | 1 + .../using_defaults_with_type_list.rhtml | 1 + .../using_defaults_with_type_list.rjs | 1 + .../using_defaults_with_type_list.rxml | 1 + .../test/fixtures/scope/test/modgreet.rhtml | 1 + .../test/fixtures/test/_customer.rhtml | 1 + .../fixtures/test/_customer_greeting.rhtml | 1 + .../test/fixtures/test/_hash_object.rhtml | 1 + .../test/fixtures/test/_partial_only.rhtml | 1 + .../test/fixtures/test/_person.rhtml | 2 + .../fixtures/test/action_talk_to_layout.rhtml | 2 + .../fixtures/test/block_content_for.rhtml | 2 + .../test/fixtures/test/capturing.rhtml | 4 + .../test/fixtures/test/content_for.rhtml | 2 + .../test/fixtures/test/delete_with_js.rjs | 2 + .../dot.directory/render_file_with_ivar.rhtml | 1 + .../test/fixtures/test/enum_rjs_test.rjs | 6 + .../test/fixtures/test/erb_content_for.rhtml | 2 + .../test/fixtures/test/greeting.rhtml | 1 + .../actionpack/test/fixtures/test/hello.rxml | 4 + .../test/fixtures/test/hello_world.rhtml | 1 + .../test/fixtures/test/hello_world.rxml | 3 + .../test/hello_world_with_layout_false.rhtml | 1 + .../test/fixtures/test/hello_xml_world.rxml | 11 + .../actionpack/test/fixtures/test/list.rhtml | 1 + .../test/non_erb_block_content_for.rxml | 4 + .../fixtures/test/potential_conflicts.rhtml | 4 + .../fixtures/test/render_file_with_ivar.rhtml | 1 + .../test/render_file_with_locals.rhtml | 1 + .../fixtures/test/render_to_string_test.rhtml | 1 + .../test/update_element_with_capture.rhtml | 9 + .../rails/actionpack/test/fixtures/topic.rb | 3 + .../rails/actionpack/test/fixtures/topics.yml | 22 + .../template/active_record_helper_test.rb | 133 + .../test/template/asset_tag_helper_test.rb | 252 ++ .../test/template/benchmark_helper_test.rb | 72 + .../test/template/compiled_templates_test.rb | 134 + .../test/template/date_helper_test.rb | 630 ++++ .../test/template/form_helper_test.rb | 423 +++ .../test/template/form_options_helper_test.rb | 466 +++ .../test/template/form_tag_helper_test.rb | 108 + .../java_script_macros_helper_test.rb | 106 + .../test/template/javascript_helper_test.rb | 38 + .../test/template/number_helper_test.rb | 56 + .../test/template/prototype_helper_test.rb | 423 +++ .../template/scriptaculous_helper_test.rb | 90 + .../test/template/tag_helper_test.rb | 41 + .../test/template/text_helper_test.rb | 290 ++ .../test/template/url_helper_test.rb | 214 ++ .../rails/actionpack/test/testing_sandbox.rb | 26 + vendor/rails/actionwebservice/CHANGELOG | 249 ++ vendor/rails/actionwebservice/MIT-LICENSE | 21 + vendor/rails/actionwebservice/README | 364 +++ vendor/rails/actionwebservice/Rakefile | 171 ++ vendor/rails/actionwebservice/TODO | 32 + .../examples/googlesearch/README | 143 + .../autoloading/google_search_api.rb | 50 + .../autoloading/google_search_controller.rb | 57 + .../delegated/google_search_service.rb | 108 + .../delegated/search_controller.rb | 7 + .../googlesearch/direct/google_search_api.rb | 50 + .../googlesearch/direct/search_controller.rb | 58 + .../examples/metaWeblog/README | 17 + .../examples/metaWeblog/apis/blogger_api.rb | 60 + .../metaWeblog/apis/blogger_service.rb | 34 + .../metaWeblog/apis/meta_weblog_api.rb | 67 + .../metaWeblog/apis/meta_weblog_service.rb | 48 + .../controllers/xmlrpc_controller.rb | 16 + vendor/rails/actionwebservice/install.rb | 30 + .../lib/action_web_service.rb | 66 + .../lib/action_web_service/api.rb | 249 ++ .../lib/action_web_service/base.rb | 42 + .../lib/action_web_service/casting.rb | 136 + .../lib/action_web_service/client.rb | 3 + .../lib/action_web_service/client/base.rb | 28 + .../action_web_service/client/soap_client.rb | 111 + .../client/xmlrpc_client.rb | 58 + .../lib/action_web_service/container.rb | 3 + .../container/action_controller_container.rb | 95 + .../container/delegated_container.rb | 87 + .../container/direct_container.rb | 70 + .../lib/action_web_service/dispatcher.rb | 2 + .../action_web_service/dispatcher/abstract.rb | 201 ++ .../action_controller_dispatcher.rb | 380 +++ .../lib/action_web_service/invocation.rb | 205 ++ .../lib/action_web_service/protocol.rb | 4 + .../action_web_service/protocol/abstract.rb | 112 + .../action_web_service/protocol/discovery.rb | 37 + .../protocol/soap_protocol.rb | 176 ++ .../protocol/soap_protocol/marshaler.rb | 241 ++ .../protocol/xmlrpc_protocol.rb | 97 + .../lib/action_web_service/scaffolding.rb | 284 ++ .../lib/action_web_service/struct.rb | 68 + .../support/class_inheritable_options.rb | 26 + .../support/signature_types.rb | 222 ++ .../templates/scaffolds/layout.rhtml | 65 + .../templates/scaffolds/methods.rhtml | 6 + .../templates/scaffolds/parameters.rhtml | 29 + .../templates/scaffolds/result.rhtml | 30 + .../lib/action_web_service/test_invoke.rb | 110 + .../lib/action_web_service/version.rb | 9 + vendor/rails/actionwebservice/setup.rb | 1379 +++++++++ .../actionwebservice/test/abstract_client.rb | 184 ++ .../test/abstract_dispatcher.rb | 500 ++++ .../actionwebservice/test/abstract_unit.rb | 38 + .../rails/actionwebservice/test/api_test.rb | 102 + .../test/apis/auto_load_api.rb | 3 + .../test/apis/broken_auto_load_api.rb | 2 + .../rails/actionwebservice/test/base_test.rb | 42 + .../actionwebservice/test/casting_test.rb | 86 + .../actionwebservice/test/client_soap_test.rb | 152 + .../test/client_xmlrpc_test.rb | 151 + .../actionwebservice/test/container_test.rb | 73 + .../dispatcher_action_controller_soap_test.rb | 139 + ...ispatcher_action_controller_xmlrpc_test.rb | 57 + .../test/fixtures/db_definitions/mysql.sql | 7 + .../actionwebservice/test/fixtures/users.yml | 10 + vendor/rails/actionwebservice/test/gencov | 3 + .../actionwebservice/test/invocation_test.rb | 185 ++ vendor/rails/actionwebservice/test/run | 6 + .../test/scaffolded_controller_test.rb | 145 + .../actionwebservice/test/struct_test.rb | 52 + .../actionwebservice/test/test_invoke_test.rb | 100 + vendor/rails/activerecord/CHANGELOG | 2608 +++++++++++++++++ vendor/rails/activerecord/MIT-LICENSE | 20 + vendor/rails/activerecord/README | 360 +++ vendor/rails/activerecord/RUNNING_UNIT_TESTS | 46 + vendor/rails/activerecord/Rakefile | 181 ++ .../activerecord/benchmarks/benchmark.rb | 26 + .../benchmarks/mysql_benchmark.rb | 19 + .../activerecord/examples/associations.png | Bin 0 -> 40623 bytes .../activerecord/examples/associations.rb | 87 + .../activerecord/examples/shared_setup.rb | 15 + .../rails/activerecord/examples/validation.rb | 85 + vendor/rails/activerecord/install.rb | 30 + .../rails/activerecord/lib/active_record.rb | 79 + .../lib/active_record/acts/list.rb | 233 ++ .../lib/active_record/acts/nested_set.rb | 212 ++ .../lib/active_record/acts/tree.rb | 90 + .../lib/active_record/aggregations.rb | 167 ++ .../lib/active_record/associations.rb | 1559 ++++++++++ .../associations/association_collection.rb | 160 + .../associations/association_proxy.rb | 139 + .../associations/belongs_to_association.rb | 56 + .../belongs_to_polymorphic_association.rb | 50 + .../has_and_belongs_to_many_association.rb | 169 ++ .../associations/has_many_association.rb | 190 ++ .../has_many_through_association.rb | 147 + .../associations/has_one_association.rb | 80 + .../activerecord/lib/active_record/base.rb | 2073 +++++++++++++ .../lib/active_record/calculations.rb | 229 ++ .../lib/active_record/callbacks.rb | 378 +++ .../abstract/connection_specification.rb | 268 ++ .../abstract/database_statements.rb | 104 + .../connection_adapters/abstract/quoting.rb | 51 + .../abstract/schema_definitions.rb | 259 ++ .../abstract/schema_statements.rb | 271 ++ .../connection_adapters/abstract_adapter.rb | 153 + .../connection_adapters/db2_adapter.rb | 238 ++ .../connection_adapters/firebird_adapter.rb | 414 +++ .../connection_adapters/mysql_adapter.rb | 357 +++ .../connection_adapters/openbase_adapter.rb | 349 +++ .../connection_adapters/oracle_adapter.rb | 665 +++++ .../connection_adapters/postgresql_adapter.rb | 507 ++++ .../connection_adapters/sqlite_adapter.rb | 371 +++ .../connection_adapters/sqlserver_adapter.rb | 563 ++++ .../connection_adapters/sybase_adapter.rb | 684 +++++ .../active_record/deprecated_associations.rb | 90 + .../lib/active_record/deprecated_finders.rb | 41 + .../lib/active_record/fixtures.rb | 600 ++++ .../activerecord/lib/active_record/locking.rb | 79 + .../lib/active_record/migration.rb | 391 +++ .../lib/active_record/observer.rb | 139 + .../lib/active_record/query_cache.rb | 64 + .../lib/active_record/reflection.rb | 204 ++ .../activerecord/lib/active_record/schema.rb | 58 + .../lib/active_record/schema_dumper.rb | 121 + .../lib/active_record/timestamp.rb | 62 + .../lib/active_record/transactions.rb | 129 + .../lib/active_record/validations.rb | 827 ++++++ .../lib/active_record/vendor/db2.rb | 362 +++ .../lib/active_record/vendor/mysql.rb | 1195 ++++++++ .../lib/active_record/vendor/simple.rb | 693 +++++ .../activerecord/lib/active_record/version.rb | 9 + .../active_record/wrappers/yaml_wrapper.rb | 15 + .../lib/active_record/wrappings.rb | 59 + .../test/aaa_create_tables_test.rb | 55 + .../rails/activerecord/test/abstract_unit.rb | 67 + .../activerecord/test/active_schema_mysql.rb | 31 + .../rails/activerecord/test/adapter_test.rb | 85 + .../activerecord/test/aggregations_test.rb | 66 + vendor/rails/activerecord/test/all.sh | 8 + .../rails/activerecord/test/ar_schema_test.rb | 33 + .../test/association_callbacks_test.rb | 124 + .../test/association_inheritance_reload.rb | 14 + ...ssociations_cascaded_eager_loading_test.rb | 106 + .../test/associations_extensions_test.rb | 37 + .../test/associations_go_eager_test.rb | 359 +++ .../test/associations_join_model_test.rb | 370 +++ .../activerecord/test/associations_test.rb | 1515 ++++++++++ vendor/rails/activerecord/test/base_test.rb | 1314 +++++++++ vendor/rails/activerecord/test/binary_test.rb | 37 + .../activerecord/test/calculations_test.rb | 181 ++ .../rails/activerecord/test/callbacks_test.rb | 364 +++ .../test/class_inheritable_attributes_test.rb | 32 + .../activerecord/test/column_alias_test.rb | 17 + .../test/connections/native_db2/connection.rb | 24 + .../connections/native_firebird/connection.rb | 24 + .../connections/native_mysql/connection.rb | 21 + .../connections/native_openbase/connection.rb | 22 + .../connections/native_oracle/connection.rb | 23 + .../native_postgresql/connection.rb | 24 + .../connections/native_sqlite/connection.rb | 37 + .../connections/native_sqlite3/connection.rb | 37 + .../native_sqlite3/in_memory_connection.rb | 18 + .../native_sqlserver/connection.rb | 24 + .../native_sqlserver_odbc/connection.rb | 26 + .../connections/native_sybase/connection.rb | 24 + .../activerecord/test/copy_table_sqlite.rb | 64 + .../test/default_test_firebird.rb | 16 + .../rails/activerecord/test/defaults_test.rb | 18 + .../test/deprecated_associations_test.rb | 352 +++ .../test/deprecated_finder_test.rb | 134 + vendor/rails/activerecord/test/finder_test.rb | 381 +++ .../activerecord/test/fixtures/accounts.yml | 23 + .../activerecord/test/fixtures/author.rb | 76 + .../test/fixtures/author_favorites.yml | 4 + .../activerecord/test/fixtures/authors.yml | 7 + .../activerecord/test/fixtures/auto_id.rb | 4 + .../bad_fixtures/attr_with_numeric_first_char | 1 + .../fixtures/bad_fixtures/attr_with_spaces | 1 + .../test/fixtures/bad_fixtures/blank_line | 3 + .../bad_fixtures/duplicate_attributes | 3 + .../test/fixtures/bad_fixtures/missing_value | 1 + .../activerecord/test/fixtures/binary.rb | 2 + .../activerecord/test/fixtures/categories.yml | 14 + .../categories/special_categories.yml | 9 + .../subsubdir/arbitrary_filename.yml | 4 + .../test/fixtures/categories_ordered.yml | 7 + .../test/fixtures/categories_posts.yml | 23 + .../test/fixtures/categorization.rb | 5 + .../test/fixtures/categorizations.yml | 11 + .../activerecord/test/fixtures/category.rb | 21 + .../activerecord/test/fixtures/column_name.rb | 3 + .../activerecord/test/fixtures/comment.rb | 23 + .../activerecord/test/fixtures/comments.yml | 65 + .../activerecord/test/fixtures/companies.yml | 50 + .../activerecord/test/fixtures/company.rb | 85 + .../test/fixtures/company_in_module.rb | 61 + .../activerecord/test/fixtures/computer.rb | 3 + .../activerecord/test/fixtures/computers.yml | 4 + .../activerecord/test/fixtures/course.rb | 3 + .../activerecord/test/fixtures/courses.yml | 7 + .../activerecord/test/fixtures/customer.rb | 55 + .../activerecord/test/fixtures/customers.yml | 8 + .../test/fixtures/db_definitions/db2.drop.sql | 30 + .../test/fixtures/db_definitions/db2.sql | 217 ++ .../fixtures/db_definitions/db22.drop.sql | 2 + .../test/fixtures/db_definitions/db22.sql | 5 + .../fixtures/db_definitions/firebird.drop.sql | 58 + .../test/fixtures/db_definitions/firebird.sql | 285 ++ .../db_definitions/firebird2.drop.sql | 2 + .../fixtures/db_definitions/firebird2.sql | 6 + .../fixtures/db_definitions/mysql.drop.sql | 30 + .../test/fixtures/db_definitions/mysql.sql | 219 ++ .../fixtures/db_definitions/mysql2.drop.sql | 2 + .../test/fixtures/db_definitions/mysql2.sql | 5 + .../fixtures/db_definitions/openbase.drop.sql | 2 + .../test/fixtures/db_definitions/openbase.sql | 282 ++ .../db_definitions/openbase2.drop.sql | 2 + .../fixtures/db_definitions/openbase2.sql | 7 + .../fixtures/db_definitions/oracle.drop.sql | 61 + .../test/fixtures/db_definitions/oracle.sql | 292 ++ .../fixtures/db_definitions/oracle2.drop.sql | 2 + .../test/fixtures/db_definitions/oracle2.sql | 6 + .../db_definitions/postgresql.drop.sql | 34 + .../fixtures/db_definitions/postgresql.sql | 248 ++ .../db_definitions/postgresql2.drop.sql | 2 + .../fixtures/db_definitions/postgresql2.sql | 5 + .../test/fixtures/db_definitions/schema.rb | 32 + .../fixtures/db_definitions/sqlite.drop.sql | 30 + .../test/fixtures/db_definitions/sqlite.sql | 201 ++ .../fixtures/db_definitions/sqlite2.drop.sql | 2 + .../test/fixtures/db_definitions/sqlite2.sql | 5 + .../db_definitions/sqlserver.drop.sql | 30 + .../fixtures/db_definitions/sqlserver.sql | 203 ++ .../db_definitions/sqlserver2.drop.sql | 2 + .../fixtures/db_definitions/sqlserver2.sql | 5 + .../fixtures/db_definitions/sybase.drop.sql | 31 + .../test/fixtures/db_definitions/sybase.sql | 204 ++ .../fixtures/db_definitions/sybase2.drop.sql | 4 + .../test/fixtures/db_definitions/sybase2.sql | 5 + .../activerecord/test/fixtures/default.rb | 2 + .../activerecord/test/fixtures/developer.rb | 40 + .../activerecord/test/fixtures/developers.yml | 21 + .../test/fixtures/developers_projects.yml | 17 + .../david_action_controller | 3 + .../developers_projects/david_active_record | 3 + .../developers_projects/jamis_active_record | 2 + .../activerecord/test/fixtures/entrant.rb | 3 + .../activerecord/test/fixtures/entrants.yml | 14 + .../test/fixtures/fk_test_has_fk.yml | 3 + .../test/fixtures/fk_test_has_pk.yml | 2 + .../activerecord/test/fixtures/flowers.jpg | Bin 0 -> 19512 bytes .../test/fixtures/funny_jokes.yml | 14 + .../rails/activerecord/test/fixtures/joke.rb | 6 + .../activerecord/test/fixtures/keyboard.rb | 3 + .../test/fixtures/legacy_thing.rb | 3 + .../test/fixtures/legacy_things.yml | 3 + .../migrations/1_people_have_last_names.rb | 9 + .../migrations/2_we_need_reminders.rb | 12 + .../migrations/3_innocent_jointable.rb | 12 + .../1_people_have_last_names.rb | 9 + .../2_we_need_reminders.rb | 12 + .../migrations_with_duplicate/3_foo.rb | 7 + .../3_innocent_jointable.rb | 12 + .../rails/activerecord/test/fixtures/mixin.rb | 48 + .../activerecord/test/fixtures/mixins.yml | 89 + .../rails/activerecord/test/fixtures/movie.rb | 5 + .../activerecord/test/fixtures/movies.yml | 7 + .../test/fixtures/naked/csv/accounts.csv | 1 + .../test/fixtures/naked/yml/accounts.yml | 1 + .../test/fixtures/naked/yml/companies.yml | 1 + .../test/fixtures/naked/yml/courses.yml | 1 + .../rails/activerecord/test/fixtures/order.rb | 4 + .../activerecord/test/fixtures/people.yml | 3 + .../activerecord/test/fixtures/person.rb | 4 + .../rails/activerecord/test/fixtures/post.rb | 57 + .../activerecord/test/fixtures/posts.yml | 48 + .../activerecord/test/fixtures/project.rb | 25 + .../activerecord/test/fixtures/projects.yml | 7 + .../activerecord/test/fixtures/reader.rb | 4 + .../activerecord/test/fixtures/readers.yml | 4 + .../rails/activerecord/test/fixtures/reply.rb | 37 + .../activerecord/test/fixtures/subject.rb | 4 + .../activerecord/test/fixtures/subscriber.rb | 6 + .../test/fixtures/subscribers/first | 2 + .../test/fixtures/subscribers/second | 2 + .../rails/activerecord/test/fixtures/tag.rb | 5 + .../activerecord/test/fixtures/tagging.rb | 6 + .../activerecord/test/fixtures/taggings.yml | 18 + .../rails/activerecord/test/fixtures/tags.yml | 7 + .../rails/activerecord/test/fixtures/task.rb | 3 + .../activerecord/test/fixtures/tasks.yml | 7 + .../rails/activerecord/test/fixtures/topic.rb | 20 + .../activerecord/test/fixtures/topics.yml | 22 + .../rails/activerecord/test/fixtures_test.rb | 345 +++ .../activerecord/test/inheritance_test.rb | 144 + .../rails/activerecord/test/lifecycle_test.rb | 116 + .../rails/activerecord/test/locking_test.rb | 46 + .../activerecord/test/method_scoping_test.rb | 416 +++ .../rails/activerecord/test/migration_test.rb | 491 ++++ .../test/mixin_nested_set_test.rb | 184 ++ vendor/rails/activerecord/test/mixin_test.rb | 512 ++++ .../rails/activerecord/test/modules_test.rb | 28 + .../activerecord/test/multiple_db_test.rb | 60 + vendor/rails/activerecord/test/pk_test.rb | 80 + .../rails/activerecord/test/readonly_test.rb | 107 + .../activerecord/test/reflection_test.rb | 153 + .../activerecord/test/schema_dumper_test.rb | 60 + .../test/schema_test_postgresql.rb | 64 + .../activerecord/test/synonym_test_oracle.rb | 17 + .../test/threaded_connections_test.rb | 45 + .../activerecord/test/transactions_test.rb | 216 ++ .../activerecord/test/unconnected_test.rb | 32 + .../activerecord/test/validations_test.rb | 1027 +++++++ vendor/rails/activesupport/CHANGELOG | 456 +++ vendor/rails/activesupport/README | 43 + vendor/rails/activesupport/Rakefile | 82 + vendor/rails/activesupport/install.rb | 30 + .../rails/activesupport/lib/active_support.rb | 41 + .../lib/active_support/binding_of_caller.rb | 84 + .../lib/active_support/breakpoint.rb | 523 ++++ .../lib/active_support/caching_tools.rb | 62 + .../lib/active_support/clean_logger.rb | 38 + .../lib/active_support/core_ext.rb | 1 + .../lib/active_support/core_ext/array.rb | 21 + .../core_ext/array/conversions.rb | 46 + .../lib/active_support/core_ext/blank.rb | 50 + .../lib/active_support/core_ext/cgi.rb | 5 + .../core_ext/cgi/escape_skipping_slashes.rb | 14 + .../lib/active_support/core_ext/class.rb | 3 + .../core_ext/class/attribute_accessors.rb | 44 + .../core_ext/class/inheritable_attributes.rb | 115 + .../active_support/core_ext/class/removal.rb | 24 + .../lib/active_support/core_ext/date.rb | 6 + .../core_ext/date/conversions.rb | 33 + .../lib/active_support/core_ext/enumerable.rb | 31 + .../lib/active_support/core_ext/exception.rb | 33 + .../lib/active_support/core_ext/hash.rb | 13 + .../core_ext/hash/conversions.rb | 44 + .../lib/active_support/core_ext/hash/diff.rb | 11 + .../core_ext/hash/indifferent_access.rb | 79 + .../lib/active_support/core_ext/hash/keys.rb | 53 + .../core_ext/hash/reverse_merge.rb | 25 + .../lib/active_support/core_ext/integer.rb | 7 + .../core_ext/integer/even_odd.rb | 24 + .../core_ext/integer/inflections.rb | 15 + .../lib/active_support/core_ext/kernel.rb | 4 + .../core_ext/kernel/agnostics.rb | 11 + .../core_ext/kernel/daemonizing.rb | 15 + .../core_ext/kernel/reporting.rb | 51 + .../core_ext/kernel/requires.rb | 24 + .../lib/active_support/core_ext/load_error.rb | 38 + .../lib/active_support/core_ext/logger.rb | 16 + .../lib/active_support/core_ext/module.rb | 5 + .../core_ext/module/attribute_accessors.rb | 44 + .../core_ext/module/delegation.rb | 16 + .../core_ext/module/inclusion.rb | 11 + .../core_ext/module/introspection.rb | 21 + .../active_support/core_ext/module/loading.rb | 13 + .../lib/active_support/core_ext/numeric.rb | 7 + .../active_support/core_ext/numeric/bytes.rb | 44 + .../active_support/core_ext/numeric/time.rb | 72 + .../lib/active_support/core_ext/object.rb | 2 + .../core_ext/object/extending.rb | 47 + .../active_support/core_ext/object/misc.rb | 34 + .../lib/active_support/core_ext/pathname.rb | 7 + .../core_ext/pathname/clean_within.rb | 14 + .../lib/active_support/core_ext/proc.rb | 12 + .../lib/active_support/core_ext/range.rb | 5 + .../core_ext/range/conversions.rb | 21 + .../lib/active_support/core_ext/string.rb | 13 + .../active_support/core_ext/string/access.rb | 58 + .../core_ext/string/conversions.rb | 19 + .../core_ext/string/inflections.rb | 64 + .../core_ext/string/iterators.rb | 17 + .../core_ext/string/starts_ends_with.rb | 20 + .../lib/active_support/core_ext/symbol.rb | 12 + .../lib/active_support/core_ext/time.rb | 7 + .../core_ext/time/calculations.rb | 189 ++ .../core_ext/time/conversions.rb | 37 + .../lib/active_support/dependencies.rb | 173 ++ .../lib/active_support/inflections.rb | 53 + .../lib/active_support/inflector.rb | 178 ++ .../activesupport/lib/active_support/json.rb | 37 + .../lib/active_support/json/encoders.rb | 25 + .../lib/active_support/json/encoders/core.rb | 65 + .../lib/active_support/option_merger.rb | 25 + .../lib/active_support/ordered_options.rb | 43 + .../lib/active_support/reloadable.rb | 30 + .../lib/active_support/values/time_zone.rb | 180 ++ .../lib/active_support/vendor/builder.rb | 13 + .../vendor/builder/blankslate.rb | 53 + .../active_support/vendor/builder/xmlbase.rb | 143 + .../vendor/builder/xmlevents.rb | 63 + .../vendor/builder/xmlmarkup.rb | 308 ++ .../lib/active_support/version.rb | 9 + .../lib/active_support/whiny_nil.rb | 38 + .../test/autoloading_fixtures/a/b.rb | 2 + .../test/autoloading_fixtures/a/c/d.rb | 2 + .../test/autoloading_fixtures/a/c/e/f.rb | 2 + .../test/autoloading_fixtures/e.rb | 2 + .../module_folder/nested_class.rb | 2 + .../module_folder/nested_sibling.rb | 2 + .../activesupport/test/caching_tools_test.rb | 81 + .../test/class_inheritable_attributes_test.rb | 141 + .../activesupport/test/clean_logger_test.rb | 82 + .../test/core_ext/array_ext_test.rb | 104 + .../activesupport/test/core_ext/blank_test.rb | 13 + .../test/core_ext/cgi_ext_test.rb | 15 + .../activesupport/test/core_ext/class_test.rb | 37 + .../test/core_ext/date_ext_test.rb | 17 + .../test/core_ext/enumerable_test.rb | 30 + .../test/core_ext/exception_test.rb | 65 + .../test/core_ext/hash_ext_test.rb | 241 ++ .../test/core_ext/integer_ext_test.rb | 38 + .../test/core_ext/kernel_test.rb | 44 + .../test/core_ext/load_error_tests.rb | 17 + .../test/core_ext/module_test.rb | 101 + .../test/core_ext/numeric_ext_test.rb | 58 + .../core_ext/object_and_class_ext_test.rb | 153 + .../test/core_ext/pathname_test.rb | 12 + .../activesupport/test/core_ext/proc_test.rb | 12 + .../test/core_ext/range_ext_test.rb | 16 + .../test/core_ext/string_ext_test.rb | 108 + .../test/core_ext/symbol_test.rb | 8 + .../test/core_ext/time_ext_test.rb | 209 ++ .../test/dependencies/check_warnings.rb | 2 + .../test/dependencies/mutual_one.rb | 4 + .../test/dependencies/mutual_two.rb | 4 + .../test/dependencies/raises_exception.rb | 3 + .../test/dependencies/service_one.rb | 5 + .../test/dependencies/service_two.rb | 2 + .../activesupport/test/dependencies_test.rb | 161 + .../activesupport/test/inflector_test.rb | 324 ++ vendor/rails/activesupport/test/json.rb | 57 + .../activesupport/test/option_merger_test.rb | 34 + .../test/ordered_options_test.rb | 55 + .../activesupport/test/reloadable_test.rb | 84 + .../activesupport/test/time_zone_test.rb | 92 + .../activesupport/test/whiny_nil_test.rb | 40 + vendor/rails/cleanlogs.sh | 1 + vendor/rails/pushgems.rb | 14 + vendor/rails/railties/CHANGELOG | 1080 +++++++ vendor/rails/railties/MIT-LICENSE | 20 + vendor/rails/railties/README | 183 ++ vendor/rails/railties/Rakefile | 320 ++ vendor/rails/railties/bin/about | 3 + vendor/rails/railties/bin/breakpointer | 3 + vendor/rails/railties/bin/console | 3 + vendor/rails/railties/bin/destroy | 3 + vendor/rails/railties/bin/generate | 3 + .../railties/bin/performance/benchmarker | 3 + .../rails/railties/bin/performance/profiler | 3 + vendor/rails/railties/bin/plugin | 3 + vendor/rails/railties/bin/process/reaper | 3 + vendor/rails/railties/bin/process/spawner | 3 + vendor/rails/railties/bin/rails | 19 + vendor/rails/railties/bin/runner | 3 + vendor/rails/railties/bin/server | 3 + .../railties/builtin/rails_info/rails/info.rb | 123 + .../rails_info/rails/info_controller.rb | 9 + .../builtin/rails_info/rails/info_helper.rb | 2 + .../rails_info/rails_info_controller.rb | 2 + vendor/rails/railties/configs/apache.conf | 40 + .../railties/configs/databases/mysql.yml | 47 + .../railties/configs/databases/oracle.yml | 30 + .../railties/configs/databases/postgresql.yml | 44 + .../railties/configs/databases/sqlite2.yml | 16 + .../railties/configs/databases/sqlite3.yml | 16 + vendor/rails/railties/configs/empty.log | 0 vendor/rails/railties/configs/lighttpd.conf | 53 + vendor/rails/railties/configs/routes.rb | 22 + .../rails/railties/dispatches/dispatch.fcgi | 24 + vendor/rails/railties/dispatches/dispatch.rb | 10 + vendor/rails/railties/dispatches/gateway.cgi | 97 + vendor/rails/railties/doc/README_FOR_APP | 2 + vendor/rails/railties/environments/boot.rb | 44 + .../railties/environments/development.rb | 21 + .../railties/environments/environment.rb | 53 + .../rails/railties/environments/production.rb | 18 + vendor/rails/railties/environments/test.rb | 19 + vendor/rails/railties/fresh_rakefile | 10 + vendor/rails/railties/helpers/application.rb | 4 + .../railties/helpers/application_helper.rb | 3 + vendor/rails/railties/helpers/test_helper.rb | 28 + vendor/rails/railties/html/404.html | 8 + vendor/rails/railties/html/500.html | 8 + vendor/rails/railties/html/favicon.ico | 0 vendor/rails/railties/html/images/rails.png | Bin 0 -> 1787 bytes vendor/rails/railties/html/index.html | 277 ++ .../railties/html/javascripts/application.js | 2 + .../railties/html/javascripts/controls.js | 815 ++++++ .../railties/html/javascripts/dragdrop.js | 913 ++++++ .../railties/html/javascripts/effects.js | 958 ++++++ .../railties/html/javascripts/prototype.js | 2006 +++++++++++++ vendor/rails/railties/html/robots.txt | 1 + .../rails/railties/lib/binding_of_caller.rb | 85 + vendor/rails/railties/lib/breakpoint.rb | 523 ++++ .../rails/railties/lib/breakpoint_client.rb | 196 ++ vendor/rails/railties/lib/code_statistics.rb | 107 + vendor/rails/railties/lib/commands.rb | 17 + vendor/rails/railties/lib/commands/about.rb | 2 + .../railties/lib/commands/breakpointer.rb | 1 + vendor/rails/railties/lib/commands/console.rb | 25 + vendor/rails/railties/lib/commands/destroy.rb | 6 + .../rails/railties/lib/commands/generate.rb | 6 + .../rails/railties/lib/commands/ncgi/listener | 86 + .../rails/railties/lib/commands/ncgi/tracker | 69 + .../lib/commands/performance/benchmarker.rb | 24 + .../lib/commands/performance/profiler.rb | 34 + vendor/rails/railties/lib/commands/plugin.rb | 871 ++++++ .../railties/lib/commands/process/reaper.rb | 130 + .../railties/lib/commands/process/spawner.rb | 94 + .../railties/lib/commands/process/spinner.rb | 57 + vendor/rails/railties/lib/commands/runner.rb | 27 + vendor/rails/railties/lib/commands/server.rb | 30 + .../railties/lib/commands/servers/lighttpd.rb | 92 + .../railties/lib/commands/servers/webrick.rb | 59 + vendor/rails/railties/lib/commands/update.rb | 4 + vendor/rails/railties/lib/console_app.rb | 27 + vendor/rails/railties/lib/console_sandbox.rb | 6 + .../railties/lib/console_with_helpers.rb | 23 + vendor/rails/railties/lib/dispatcher.rb | 117 + vendor/rails/railties/lib/fcgi_handler.rb | 207 ++ vendor/rails/railties/lib/initializer.rb | 622 ++++ vendor/rails/railties/lib/rails/version.rb | 9 + vendor/rails/railties/lib/rails_generator.rb | 43 + .../railties/lib/rails_generator/base.rb | 203 ++ .../railties/lib/rails_generator/commands.rb | 519 ++++ .../generators/applications/app/USAGE | 16 + .../applications/app/app_generator.rb | 157 + .../generators/components/controller/USAGE | 30 + .../controller/controller_generator.rb | 38 + .../controller/templates/controller.rb | 10 + .../controller/templates/functional_test.rb | 18 + .../components/controller/templates/helper.rb | 2 + .../controller/templates/view.rhtml | 2 + .../components/integration_test/USAGE | 14 + .../integration_test_generator.rb | 16 + .../templates/integration_test.rb | 10 + .../generators/components/mailer/USAGE | 18 + .../components/mailer/mailer_generator.rb | 32 + .../components/mailer/templates/fixture.rhtml | 3 + .../components/mailer/templates/mailer.rb | 13 + .../components/mailer/templates/unit_test.rb | 37 + .../components/mailer/templates/view.rhtml | 3 + .../generators/components/migration/USAGE | 14 + .../migration/migration_generator.rb | 7 + .../migration/templates/migration.rb | 7 + .../generators/components/model/USAGE | 19 + .../components/model/model_generator.rb | 34 + .../components/model/templates/fixtures.yml | 5 + .../components/model/templates/migration.rb | 11 + .../components/model/templates/model.rb | 2 + .../components/model/templates/unit_test.rb | 10 + .../generators/components/plugin/USAGE | 35 + .../components/plugin/plugin_generator.rb | 34 + .../components/plugin/templates/README | 4 + .../components/plugin/templates/Rakefile | 22 + .../components/plugin/templates/USAGE | 8 + .../components/plugin/templates/generator.rb | 8 + .../components/plugin/templates/init.rb | 1 + .../components/plugin/templates/install.rb | 1 + .../components/plugin/templates/plugin.rb | 1 + .../components/plugin/templates/tasks.rake | 4 + .../components/plugin/templates/unit_test.rb | 8 + .../generators/components/scaffold/USAGE | 32 + .../components/scaffold/scaffold_generator.rb | 184 ++ .../scaffold/templates/controller.rb | 58 + .../components/scaffold/templates/form.rhtml | 3 + .../scaffold/templates/form_scaffolding.rhtml | 1 + .../scaffold/templates/functional_test.rb | 98 + .../components/scaffold/templates/helper.rb | 2 + .../scaffold/templates/layout.rhtml | 13 + .../components/scaffold/templates/style.css | 74 + .../scaffold/templates/view_edit.rhtml | 9 + .../scaffold/templates/view_list.rhtml | 27 + .../scaffold/templates/view_new.rhtml | 8 + .../scaffold/templates/view_show.rhtml | 8 + .../components/session_migration/USAGE | 15 + .../session_migration_generator.rb | 12 + .../session_migration/templates/migration.rb | 15 + .../generators/components/web_service/USAGE | 28 + .../web_service/templates/api_definition.rb | 5 + .../web_service/templates/controller.rb | 8 + .../web_service/templates/functional_test.rb | 19 + .../web_service/web_service_generator.rb | 29 + .../railties/lib/rails_generator/lookup.rb | 210 ++ .../railties/lib/rails_generator/manifest.rb | 53 + .../railties/lib/rails_generator/options.rb | 140 + .../railties/lib/rails_generator/scripts.rb | 83 + .../lib/rails_generator/scripts/destroy.rb | 7 + .../lib/rails_generator/scripts/generate.rb | 7 + .../lib/rails_generator/scripts/update.rb | 12 + .../lib/rails_generator/simple_logger.rb | 46 + .../railties/lib/rails_generator/spec.rb | 44 + vendor/rails/railties/lib/railties_path.rb | 1 + .../rails/railties/lib/ruby_version_check.rb | 17 + vendor/rails/railties/lib/rubyprof_ext.rb | 35 + .../rails/railties/lib/tasks/databases.rake | 161 + .../railties/lib/tasks/documentation.rake | 81 + .../rails/railties/lib/tasks/framework.rake | 114 + vendor/rails/railties/lib/tasks/log.rake | 9 + vendor/rails/railties/lib/tasks/misc.rake | 4 + .../lib/tasks/pre_namespace_aliases.rake | 46 + vendor/rails/railties/lib/tasks/rails.rb | 8 + .../rails/railties/lib/tasks/statistics.rake | 17 + vendor/rails/railties/lib/tasks/testing.rake | 102 + vendor/rails/railties/lib/tasks/tmp.rake | 30 + vendor/rails/railties/lib/test_help.rb | 18 + vendor/rails/railties/lib/webrick_server.rb | 168 ++ vendor/rails/railties/test/dispatcher_test.rb | 92 + .../railties/test/fcgi_dispatcher_test.rb | 190 ++ .../fixtures/environment_with_constant.rb | 1 + .../fixtures/plugins/default/stubby/init.rb | 7 + .../default/stubby/lib/stubby_mixin.rb | 2 + .../missing_class/missing_class_generator.rb | 0 .../generators/working/working_generator.rb | 2 + .../rails/railties/test/initializer_test.rb | 33 + .../rails/railties/test/mocks/dispatcher.rb | 13 + vendor/rails/railties/test/mocks/fcgi.rb | 15 + vendor/rails/railties/test/plugin_test.rb | 72 + .../railties/test/rails_generator_test.rb | 105 + .../test/rails_info_controller_test.rb | 50 + vendor/rails/railties/test/rails_info_test.rb | 92 + .../railties/test/webrick_dispatcher_test.rb | 27 + vendor/rails/release.rb | 25 + 1138 files changed, 139586 insertions(+) create mode 100755 CHANGELOG create mode 100755 README create mode 100644 app/controllers/admin_controller.rb create mode 100644 app/controllers/application.rb create mode 100644 app/controllers/cache_sweeping_helper.rb create mode 100644 app/controllers/file_controller.rb create mode 100644 app/controllers/revision_sweeper.rb create mode 100644 app/controllers/web_sweeper.rb create mode 100644 app/controllers/wiki_controller.rb create mode 100644 app/helpers/application_helper.rb create mode 100644 app/helpers/wiki_helper.rb create mode 100644 app/models/author.rb create mode 100644 app/models/page.rb create mode 100644 app/models/page_observer.rb create mode 100644 app/models/page_set.rb create mode 100644 app/models/revision.rb create mode 100644 app/models/system.rb create mode 100644 app/models/web.rb create mode 100644 app/models/wiki.rb create mode 100644 app/models/wiki_file.rb create mode 100644 app/models/wiki_reference.rb create mode 100644 app/views/admin/create_system.rhtml create mode 100644 app/views/admin/create_web.rhtml create mode 100644 app/views/admin/edit_web.rhtml create mode 100644 app/views/file/file.rhtml create mode 100644 app/views/file/import.rhtml create mode 100644 app/views/layouts/default.rhtml create mode 100644 app/views/markdown_help.rhtml create mode 100644 app/views/mixed_help.rhtml create mode 100644 app/views/navigation.rhtml create mode 100644 app/views/rdoc_help.rhtml create mode 100644 app/views/textile_help.rhtml create mode 100644 app/views/wiki/_inbound_links.rhtml create mode 100644 app/views/wiki/authors.rhtml create mode 100644 app/views/wiki/edit.rhtml create mode 100644 app/views/wiki/export.rhtml create mode 100644 app/views/wiki/feeds.rhtml create mode 100644 app/views/wiki/list.rhtml create mode 100644 app/views/wiki/locked.rhtml create mode 100644 app/views/wiki/login.rhtml create mode 100644 app/views/wiki/new.rhtml create mode 100644 app/views/wiki/page.rhtml create mode 100644 app/views/wiki/print.rhtml create mode 100644 app/views/wiki/published.rhtml create mode 100644 app/views/wiki/recently_revised.rhtml create mode 100644 app/views/wiki/revision.rhtml create mode 100644 app/views/wiki/rollback.rhtml create mode 100644 app/views/wiki/rss_feed.rxml create mode 100644 app/views/wiki/search.rhtml create mode 100644 app/views/wiki/tex.rhtml create mode 100644 app/views/wiki/tex_web.rhtml create mode 100644 app/views/wiki/web_list.rhtml create mode 100644 app/views/wiki_words_help.rhtml create mode 100644 config/boot.rb create mode 100644 config/database.yml create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/test.rb create mode 100644 config/routes.rb create mode 100644 config/spam_patterns.txt create mode 100644 db/migrate/001_beta1_schema.rb create mode 100644 db/migrate/002_beta2_changes_bulk.rb create mode 100755 instiki create mode 100644 instiki.cmd create mode 100755 instiki.rb create mode 100644 lib/bluecloth_tweaked.rb create mode 100644 lib/chunks/category.rb create mode 100644 lib/chunks/chunk.rb create mode 100644 lib/chunks/engines.rb create mode 100644 lib/chunks/include.rb create mode 100644 lib/chunks/literal.rb create mode 100644 lib/chunks/nowiki.rb create mode 100644 lib/chunks/test.rb create mode 100644 lib/chunks/uri.rb create mode 100644 lib/chunks/wiki.rb create mode 100644 lib/diff.rb create mode 100644 lib/instiki_errors.rb create mode 100644 lib/native/win32/sqlite3.dll create mode 100644 lib/page_renderer.rb create mode 100644 lib/rdocsupport.rb create mode 100644 lib/redcloth.rb create mode 100644 lib/redcloth_for_tex.rb create mode 100644 lib/url_generator.rb create mode 100644 lib/wiki_content.rb create mode 100644 lib/wiki_words.rb create mode 100644 natives/osx/desktop_launcher/AppDelegate.h create mode 100644 natives/osx/desktop_launcher/AppDelegate.mm create mode 100644 natives/osx/desktop_launcher/Credits.html create mode 100644 natives/osx/desktop_launcher/English.lproj/InfoPlist.strings create mode 100644 natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib create mode 100644 natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib create mode 100644 natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib create mode 100644 natives/osx/desktop_launcher/Info.plist create mode 100644 natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj create mode 100644 natives/osx/desktop_launcher/Instiki_Prefix.pch create mode 100644 natives/osx/desktop_launcher/MakeDMG.sh create mode 100644 natives/osx/desktop_launcher/main.mm create mode 100644 natives/osx/desktop_launcher/version.plist create mode 100644 public/.htaccess create mode 100644 public/404.html create mode 100644 public/500.html create mode 100755 public/dispatch.cgi create mode 100755 public/dispatch.fcgi create mode 100755 public/dispatch.rb create mode 100644 public/favicon.ico create mode 100644 public/images/.images_go_here create mode 100644 public/images/bg_normal.gif create mode 100644 public/images/bg_protected.gif create mode 100644 public/javascripts/controls.js create mode 100644 public/javascripts/dragdrop.js create mode 100644 public/javascripts/edit_web.js create mode 100644 public/javascripts/effects.js create mode 100644 public/javascripts/prototype.js create mode 100644 public/javascripts/scriptaculous.js create mode 100644 public/javascripts/slider.js create mode 100644 public/robots.txt create mode 100644 public/stylesheets/instiki.css create mode 100755 rakefile.rb create mode 100755 script/benchmarker create mode 100755 script/breakpointer create mode 100755 script/console create mode 100755 script/destroy create mode 100755 script/generate create mode 100755 script/import_storage create mode 100755 script/profiler create mode 100755 script/reset_references create mode 100755 script/runner create mode 100755 script/server create mode 100644 test/fixtures/exported_markup.zip create mode 100644 test/fixtures/pages.yml create mode 100755 test/fixtures/rails.gif create mode 100644 test/fixtures/revisions.yml create mode 100644 test/fixtures/system.yml create mode 100644 test/fixtures/webs.yml create mode 100644 test/fixtures/wiki_references.yml create mode 100644 test/functional/admin_controller_test.rb create mode 100755 test/functional/application_test.rb create mode 100755 test/functional/file_controller_test.rb create mode 100644 test/functional/routes_test.rb create mode 100755 test/functional/wiki_controller_test.rb create mode 100644 test/test_helper.rb create mode 100755 test/unit/chunks/category_test.rb create mode 100755 test/unit/chunks/nowiki_test.rb create mode 100755 test/unit/chunks/wiki_test.rb create mode 100755 test/unit/diff_test.rb create mode 100644 test/unit/page_renderer_test.rb create mode 100644 test/unit/page_test.rb create mode 100755 test/unit/redcloth_for_tex_test.rb create mode 100755 test/unit/uri_test.rb create mode 100644 test/unit/web_test.rb create mode 100644 test/unit/wiki_file_test.rb create mode 100755 test/unit/wiki_words_test.rb create mode 100644 test/watir/e2e.rb create mode 100644 vendor/plugins/dnsbl_check/README create mode 100644 vendor/plugins/dnsbl_check/init.rb create mode 100644 vendor/plugins/dnsbl_check/lib/dnsbl_check.rb create mode 100644 vendor/plugins/rubyzip-0.9.1/ChangeLog create mode 100644 vendor/plugins/rubyzip-0.9.1/NEWS create mode 100644 vendor/plugins/rubyzip-0.9.1/README create mode 100755 vendor/plugins/rubyzip-0.9.1/Rakefile create mode 100644 vendor/plugins/rubyzip-0.9.1/TODO create mode 100755 vendor/plugins/rubyzip-0.9.1/install.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/lib/zip/ioextras.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/lib/zip/stdrubyext.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/lib/zip/tempfile_bugfixed.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/lib/zip/zip.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/lib/zip/zipfilesystem.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/lib/zip/ziprequire.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/samples/example.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/samples/example_filesystem.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/samples/gtkRubyzip.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/samples/qtzip.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/samples/write_simple.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/samples/zipfind.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/test/alltests.rb create mode 100644 vendor/plugins/rubyzip-0.9.1/test/data/file1.txt create mode 100644 vendor/plugins/rubyzip-0.9.1/test/data/file1.txt.deflatedData create mode 100644 vendor/plugins/rubyzip-0.9.1/test/data/file2.txt create mode 100755 vendor/plugins/rubyzip-0.9.1/test/data/notzippedruby.rb create mode 100644 vendor/plugins/rubyzip-0.9.1/test/data/rubycode.zip create mode 100644 vendor/plugins/rubyzip-0.9.1/test/data/rubycode2.zip create mode 100644 vendor/plugins/rubyzip-0.9.1/test/data/testDirectory.bin create mode 100644 vendor/plugins/rubyzip-0.9.1/test/data/zipWithDirs.zip create mode 100755 vendor/plugins/rubyzip-0.9.1/test/gentestfiles.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/test/ioextrastest.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/test/stdrubyexttest.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/test/zipfilesystemtest.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/test/ziprequiretest.rb create mode 100755 vendor/plugins/rubyzip-0.9.1/test/ziptest.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/constants.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/database.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/api.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/driver.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/driver/native/driver.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/errors.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/pragmas.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/resultset.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/statement.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/translator.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/value.rb create mode 100644 vendor/plugins/sqlite3-ruby/sqlite3/version.rb create mode 100644 vendor/rails/actionmailer/CHANGELOG create mode 100644 vendor/rails/actionmailer/MIT-LICENSE create mode 100755 vendor/rails/actionmailer/README create mode 100755 vendor/rails/actionmailer/Rakefile create mode 100644 vendor/rails/actionmailer/install.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/adv_attr_accessor.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/base.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/helpers.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/mail_helper.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/part.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/part_container.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/quoting.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/utils.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/text/format.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/address.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/attachments.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/base64.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/config.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/encode.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/facade.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/header.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/info.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/loader.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mail.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mailbox.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mbox.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/net.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/obsolete.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/parser.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/port.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/quoting.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/stringio.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/tmail.rb create mode 100755 vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/utils.rb create mode 100644 vendor/rails/actionmailer/lib/action_mailer/version.rb create mode 100644 vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper.rhtml create mode 100644 vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper_method.rhtml create mode 100644 vendor/rails/actionmailer/test/fixtures/helper_mailer/use_mail_helper.rhtml create mode 100644 vendor/rails/actionmailer/test/fixtures/helper_mailer/use_test_helper.rhtml create mode 100644 vendor/rails/actionmailer/test/fixtures/helpers/test_helper.rb create mode 100644 vendor/rails/actionmailer/test/fixtures/path.with.dots/multipart_with_template_path_with_dots.rhtml create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email10 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email11 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email12 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email13 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email2 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email3 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email4 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email5 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email6 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email7 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email8 create mode 100644 vendor/rails/actionmailer/test/fixtures/raw_email9 create mode 100644 vendor/rails/actionmailer/test/fixtures/templates/signed_up.rhtml create mode 100644 vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.rhtml create mode 100644 vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml create mode 100644 vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml create mode 100644 vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.yaml.rhtml create mode 100644 vendor/rails/actionmailer/test/fixtures/test_mailer/signed_up.rhtml create mode 100644 vendor/rails/actionmailer/test/mail_helper_test.rb create mode 100644 vendor/rails/actionmailer/test/mail_render_test.rb create mode 100755 vendor/rails/actionmailer/test/mail_service_test.rb create mode 100644 vendor/rails/actionmailer/test/quoting_test.rb create mode 100644 vendor/rails/actionmailer/test/tmail_test.rb create mode 100644 vendor/rails/actionpack/CHANGELOG create mode 100644 vendor/rails/actionpack/MIT-LICENSE create mode 100755 vendor/rails/actionpack/README create mode 100644 vendor/rails/actionpack/RUNNING_UNIT_TESTS create mode 100755 vendor/rails/actionpack/Rakefile create mode 100644 vendor/rails/actionpack/examples/.htaccess create mode 100644 vendor/rails/actionpack/examples/address_book/index.rhtml create mode 100644 vendor/rails/actionpack/examples/address_book/layout.rhtml create mode 100755 vendor/rails/actionpack/examples/address_book_controller.cgi create mode 100755 vendor/rails/actionpack/examples/address_book_controller.fcgi create mode 100644 vendor/rails/actionpack/examples/address_book_controller.rb create mode 100644 vendor/rails/actionpack/examples/address_book_controller.rbx create mode 100644 vendor/rails/actionpack/examples/benchmark.rb create mode 100755 vendor/rails/actionpack/examples/benchmark_with_ar.fcgi create mode 100755 vendor/rails/actionpack/examples/blog_controller.cgi create mode 100644 vendor/rails/actionpack/examples/debate/index.rhtml create mode 100644 vendor/rails/actionpack/examples/debate/new_topic.rhtml create mode 100644 vendor/rails/actionpack/examples/debate/topic.rhtml create mode 100755 vendor/rails/actionpack/examples/debate_controller.cgi create mode 100644 vendor/rails/actionpack/filler.txt create mode 100644 vendor/rails/actionpack/install.rb create mode 100755 vendor/rails/actionpack/lib/action_controller.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/assertions.rb create mode 100755 vendor/rails/actionpack/lib/action_controller/base.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/benchmarking.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/caching.rb create mode 100755 vendor/rails/actionpack/lib/action_controller/cgi_ext/cgi_ext.rb create mode 100755 vendor/rails/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/cgi_ext/cookie_performance_fix.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/cgi_ext/raw_post_data_fix.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/cgi_process.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/code_generation.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/components.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/cookies.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/dependencies.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/deprecated_assertions.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/deprecated_redirects.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/deprecated_request_methods.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/filters.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/flash.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/helpers.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/integration.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/layout.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/macros/auto_complete.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/macros/in_place_editing.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/mime_responds.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/mime_type.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/pagination.rb create mode 100755 vendor/rails/actionpack/lib/action_controller/request.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/rescue.rb create mode 100755 vendor/rails/actionpack/lib/action_controller/response.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/routing.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/scaffolding.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/session/active_record_store.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/session/drb_server.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/session/drb_store.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/session/mem_cache_store.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/session_management.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/streaming.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/rescues/_trace.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/rescues/layout.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/rescues/missing_template.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/rescues/routing_error.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/rescues/template_error.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/scaffolds/edit.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/scaffolds/layout.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/scaffolds/list.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/scaffolds/new.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/templates/scaffolds/show.rhtml create mode 100644 vendor/rails/actionpack/lib/action_controller/test_process.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/url_rewriter.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/vendor/html-scanner/html/version.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/vendor/xml_node.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/vendor/xml_simple.rb create mode 100644 vendor/rails/actionpack/lib/action_controller/verification.rb create mode 100644 vendor/rails/actionpack/lib/action_pack.rb create mode 100644 vendor/rails/actionpack/lib/action_pack/version.rb create mode 100644 vendor/rails/actionpack/lib/action_view.rb create mode 100644 vendor/rails/actionpack/lib/action_view/base.rb create mode 100644 vendor/rails/actionpack/lib/action_view/compiled_templates.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/active_record_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/asset_tag_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/benchmark_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/cache_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/capture_helper.rb create mode 100755 vendor/rails/actionpack/lib/action_view/helpers/date_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/debug_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/form_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/form_options_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/form_tag_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/java_script_macros_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/javascript_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/javascripts/controls.js create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/javascripts/dragdrop.js create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/javascripts/effects.js create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/javascripts/prototype.js create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/number_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/pagination_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/prototype_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/scriptaculous_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/tag_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/text_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/helpers/url_helper.rb create mode 100644 vendor/rails/actionpack/lib/action_view/partials.rb create mode 100644 vendor/rails/actionpack/lib/action_view/template_error.rb create mode 100644 vendor/rails/actionpack/test/abstract_unit.rb create mode 100644 vendor/rails/actionpack/test/active_record_unit.rb create mode 100644 vendor/rails/actionpack/test/activerecord/active_record_assertions_test.rb create mode 100644 vendor/rails/actionpack/test/activerecord/active_record_store_test.rb create mode 100644 vendor/rails/actionpack/test/activerecord/pagination_test.rb create mode 100644 vendor/rails/actionpack/test/controller/action_pack_assertions_test.rb create mode 100644 vendor/rails/actionpack/test/controller/addresses_render_test.rb create mode 100644 vendor/rails/actionpack/test/controller/base_test.rb create mode 100644 vendor/rails/actionpack/test/controller/benchmark_test.rb create mode 100644 vendor/rails/actionpack/test/controller/caching_filestore.rb create mode 100644 vendor/rails/actionpack/test/controller/capture_test.rb create mode 100755 vendor/rails/actionpack/test/controller/cgi_test.rb create mode 100644 vendor/rails/actionpack/test/controller/components_test.rb create mode 100644 vendor/rails/actionpack/test/controller/cookie_test.rb create mode 100644 vendor/rails/actionpack/test/controller/custom_handler_test.rb create mode 100644 vendor/rails/actionpack/test/controller/fake_controllers.rb create mode 100644 vendor/rails/actionpack/test/controller/filter_params_test.rb create mode 100644 vendor/rails/actionpack/test/controller/filters_test.rb create mode 100644 vendor/rails/actionpack/test/controller/flash_test.rb create mode 100644 vendor/rails/actionpack/test/controller/fragment_store_setting_test.rb create mode 100644 vendor/rails/actionpack/test/controller/helper_test.rb create mode 100644 vendor/rails/actionpack/test/controller/layout_test.rb create mode 100644 vendor/rails/actionpack/test/controller/mime_responds_test.rb create mode 100644 vendor/rails/actionpack/test/controller/mime_type_test.rb create mode 100644 vendor/rails/actionpack/test/controller/new_render_test.rb create mode 100644 vendor/rails/actionpack/test/controller/raw_post_test.rb create mode 100755 vendor/rails/actionpack/test/controller/redirect_test.rb create mode 100644 vendor/rails/actionpack/test/controller/render_test.rb create mode 100644 vendor/rails/actionpack/test/controller/request_test.rb create mode 100644 vendor/rails/actionpack/test/controller/routing_test.rb create mode 100644 vendor/rails/actionpack/test/controller/send_file_test.rb create mode 100644 vendor/rails/actionpack/test/controller/session_management_test.rb create mode 100644 vendor/rails/actionpack/test/controller/test_test.rb create mode 100644 vendor/rails/actionpack/test/controller/url_rewriter_test.rb create mode 100644 vendor/rails/actionpack/test/controller/verification_test.rb create mode 100644 vendor/rails/actionpack/test/controller/webservice_test.rb create mode 100644 vendor/rails/actionpack/test/fixtures/addresses/list.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/application_root/app/controllers/a_class_that_contains_a_controller/poorly_placed_controller.rb create mode 100644 vendor/rails/actionpack/test/fixtures/application_root/app/controllers/module_that_holds_controllers/nested_controller.rb create mode 100644 vendor/rails/actionpack/test/fixtures/application_root/app/models/a_class_that_contains_a_controller.rb create mode 100644 vendor/rails/actionpack/test/fixtures/companies.yml create mode 100644 vendor/rails/actionpack/test/fixtures/company.rb create mode 100644 vendor/rails/actionpack/test/fixtures/db_definitions/sqlite.sql create mode 100644 vendor/rails/actionpack/test/fixtures/developer.rb create mode 100644 vendor/rails/actionpack/test/fixtures/developers.yml create mode 100644 vendor/rails/actionpack/test/fixtures/developers_projects.yml create mode 100644 vendor/rails/actionpack/test/fixtures/dont_load.rb create mode 100644 vendor/rails/actionpack/test/fixtures/fun/games/hello_world.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/helpers/abc_helper.rb create mode 100644 vendor/rails/actionpack/test/fixtures/helpers/fun/games_helper.rb create mode 100644 vendor/rails/actionpack/test/fixtures/helpers/fun/pdf_helper.rb create mode 100644 vendor/rails/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/layout_tests/layouts/item.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/layout_tests/layouts/layout_test.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/layout_tests/layouts/third_party_template_library.mab create mode 100644 vendor/rails/actionpack/test/fixtures/layout_tests/views/hello.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/layouts/builder.rxml create mode 100644 vendor/rails/actionpack/test/fixtures/layouts/standard.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/layouts/talk_from_action.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/layouts/yield.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/multipart/binary_file create mode 100644 vendor/rails/actionpack/test/fixtures/multipart/large_text_file create mode 100644 vendor/rails/actionpack/test/fixtures/multipart/mixed_files create mode 100644 vendor/rails/actionpack/test/fixtures/multipart/mona_lisa.jpg create mode 100644 vendor/rails/actionpack/test/fixtures/multipart/single_parameter create mode 100644 vendor/rails/actionpack/test/fixtures/multipart/text_file create mode 100644 vendor/rails/actionpack/test/fixtures/project.rb create mode 100644 vendor/rails/actionpack/test/fixtures/projects.yml create mode 100644 vendor/rails/actionpack/test/fixtures/public/images/rails.png create mode 100644 vendor/rails/actionpack/test/fixtures/replies.yml create mode 100644 vendor/rails/actionpack/test/fixtures/reply.rb create mode 100644 vendor/rails/actionpack/test/fixtures/respond_to/all_types_with_layout.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/respond_to/all_types_with_layout.rjs create mode 100644 vendor/rails/actionpack/test/fixtures/respond_to/layouts/standard.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rjs create mode 100644 vendor/rails/actionpack/test/fixtures/respond_to/using_defaults.rxml create mode 100644 vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rjs create mode 100644 vendor/rails/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.rxml create mode 100644 vendor/rails/actionpack/test/fixtures/scope/test/modgreet.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/_customer.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/_customer_greeting.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/_hash_object.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/_partial_only.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/_person.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/action_talk_to_layout.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/block_content_for.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/capturing.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/content_for.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/delete_with_js.rjs create mode 100644 vendor/rails/actionpack/test/fixtures/test/dot.directory/render_file_with_ivar.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/enum_rjs_test.rjs create mode 100644 vendor/rails/actionpack/test/fixtures/test/erb_content_for.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/greeting.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/hello.rxml create mode 100644 vendor/rails/actionpack/test/fixtures/test/hello_world.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/hello_world.rxml create mode 100644 vendor/rails/actionpack/test/fixtures/test/hello_world_with_layout_false.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/hello_xml_world.rxml create mode 100644 vendor/rails/actionpack/test/fixtures/test/list.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/non_erb_block_content_for.rxml create mode 100644 vendor/rails/actionpack/test/fixtures/test/potential_conflicts.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/render_file_with_ivar.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/render_file_with_locals.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/render_to_string_test.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/test/update_element_with_capture.rhtml create mode 100644 vendor/rails/actionpack/test/fixtures/topic.rb create mode 100644 vendor/rails/actionpack/test/fixtures/topics.yml create mode 100644 vendor/rails/actionpack/test/template/active_record_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/asset_tag_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/benchmark_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/compiled_templates_test.rb create mode 100755 vendor/rails/actionpack/test/template/date_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/form_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/form_options_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/form_tag_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/java_script_macros_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/javascript_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/number_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/prototype_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/scriptaculous_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/tag_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/text_helper_test.rb create mode 100644 vendor/rails/actionpack/test/template/url_helper_test.rb create mode 100644 vendor/rails/actionpack/test/testing_sandbox.rb create mode 100644 vendor/rails/actionwebservice/CHANGELOG create mode 100644 vendor/rails/actionwebservice/MIT-LICENSE create mode 100644 vendor/rails/actionwebservice/README create mode 100644 vendor/rails/actionwebservice/Rakefile create mode 100644 vendor/rails/actionwebservice/TODO create mode 100644 vendor/rails/actionwebservice/examples/googlesearch/README create mode 100644 vendor/rails/actionwebservice/examples/googlesearch/autoloading/google_search_api.rb create mode 100644 vendor/rails/actionwebservice/examples/googlesearch/autoloading/google_search_controller.rb create mode 100644 vendor/rails/actionwebservice/examples/googlesearch/delegated/google_search_service.rb create mode 100644 vendor/rails/actionwebservice/examples/googlesearch/delegated/search_controller.rb create mode 100644 vendor/rails/actionwebservice/examples/googlesearch/direct/google_search_api.rb create mode 100644 vendor/rails/actionwebservice/examples/googlesearch/direct/search_controller.rb create mode 100644 vendor/rails/actionwebservice/examples/metaWeblog/README create mode 100644 vendor/rails/actionwebservice/examples/metaWeblog/apis/blogger_api.rb create mode 100644 vendor/rails/actionwebservice/examples/metaWeblog/apis/blogger_service.rb create mode 100644 vendor/rails/actionwebservice/examples/metaWeblog/apis/meta_weblog_api.rb create mode 100644 vendor/rails/actionwebservice/examples/metaWeblog/apis/meta_weblog_service.rb create mode 100644 vendor/rails/actionwebservice/examples/metaWeblog/controllers/xmlrpc_controller.rb create mode 100644 vendor/rails/actionwebservice/install.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/api.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/base.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/casting.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/client.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/client/base.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/client/soap_client.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/container.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/container/action_controller_container.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/container/delegated_container.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/container/direct_container.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/dispatcher.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/dispatcher/abstract.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/invocation.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/protocol.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/protocol/abstract.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/protocol/discovery.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/protocol/soap_protocol/marshaler.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/scaffolding.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/struct.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/support/class_inheritable_options.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/support/signature_types.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/test_invoke.rb create mode 100644 vendor/rails/actionwebservice/lib/action_web_service/version.rb create mode 100644 vendor/rails/actionwebservice/setup.rb create mode 100644 vendor/rails/actionwebservice/test/abstract_client.rb create mode 100644 vendor/rails/actionwebservice/test/abstract_dispatcher.rb create mode 100644 vendor/rails/actionwebservice/test/abstract_unit.rb create mode 100644 vendor/rails/actionwebservice/test/api_test.rb create mode 100644 vendor/rails/actionwebservice/test/apis/auto_load_api.rb create mode 100644 vendor/rails/actionwebservice/test/apis/broken_auto_load_api.rb create mode 100644 vendor/rails/actionwebservice/test/base_test.rb create mode 100644 vendor/rails/actionwebservice/test/casting_test.rb create mode 100644 vendor/rails/actionwebservice/test/client_soap_test.rb create mode 100644 vendor/rails/actionwebservice/test/client_xmlrpc_test.rb create mode 100644 vendor/rails/actionwebservice/test/container_test.rb create mode 100644 vendor/rails/actionwebservice/test/dispatcher_action_controller_soap_test.rb create mode 100644 vendor/rails/actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb create mode 100644 vendor/rails/actionwebservice/test/fixtures/db_definitions/mysql.sql create mode 100644 vendor/rails/actionwebservice/test/fixtures/users.yml create mode 100755 vendor/rails/actionwebservice/test/gencov create mode 100644 vendor/rails/actionwebservice/test/invocation_test.rb create mode 100755 vendor/rails/actionwebservice/test/run create mode 100644 vendor/rails/actionwebservice/test/scaffolded_controller_test.rb create mode 100644 vendor/rails/actionwebservice/test/struct_test.rb create mode 100644 vendor/rails/actionwebservice/test/test_invoke_test.rb create mode 100644 vendor/rails/activerecord/CHANGELOG create mode 100644 vendor/rails/activerecord/MIT-LICENSE create mode 100755 vendor/rails/activerecord/README create mode 100644 vendor/rails/activerecord/RUNNING_UNIT_TESTS create mode 100755 vendor/rails/activerecord/Rakefile create mode 100644 vendor/rails/activerecord/benchmarks/benchmark.rb create mode 100644 vendor/rails/activerecord/benchmarks/mysql_benchmark.rb create mode 100644 vendor/rails/activerecord/examples/associations.png create mode 100644 vendor/rails/activerecord/examples/associations.rb create mode 100644 vendor/rails/activerecord/examples/shared_setup.rb create mode 100644 vendor/rails/activerecord/examples/validation.rb create mode 100644 vendor/rails/activerecord/install.rb create mode 100755 vendor/rails/activerecord/lib/active_record.rb create mode 100644 vendor/rails/activerecord/lib/active_record/acts/list.rb create mode 100644 vendor/rails/activerecord/lib/active_record/acts/nested_set.rb create mode 100644 vendor/rails/activerecord/lib/active_record/acts/tree.rb create mode 100644 vendor/rails/activerecord/lib/active_record/aggregations.rb create mode 100755 vendor/rails/activerecord/lib/active_record/associations.rb create mode 100644 vendor/rails/activerecord/lib/active_record/associations/association_collection.rb create mode 100644 vendor/rails/activerecord/lib/active_record/associations/association_proxy.rb create mode 100644 vendor/rails/activerecord/lib/active_record/associations/belongs_to_association.rb create mode 100644 vendor/rails/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb create mode 100644 vendor/rails/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb create mode 100644 vendor/rails/activerecord/lib/active_record/associations/has_many_association.rb create mode 100644 vendor/rails/activerecord/lib/active_record/associations/has_many_through_association.rb create mode 100644 vendor/rails/activerecord/lib/active_record/associations/has_one_association.rb create mode 100755 vendor/rails/activerecord/lib/active_record/base.rb create mode 100644 vendor/rails/activerecord/lib/active_record/calculations.rb create mode 100755 vendor/rails/activerecord/lib/active_record/callbacks.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb create mode 100755 vendor/rails/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/db2_adapter.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/firebird_adapter.rb create mode 100755 vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/oracle_adapter.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb create mode 100644 vendor/rails/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb create mode 100644 vendor/rails/activerecord/lib/active_record/deprecated_associations.rb create mode 100644 vendor/rails/activerecord/lib/active_record/deprecated_finders.rb create mode 100755 vendor/rails/activerecord/lib/active_record/fixtures.rb create mode 100644 vendor/rails/activerecord/lib/active_record/locking.rb create mode 100644 vendor/rails/activerecord/lib/active_record/migration.rb create mode 100644 vendor/rails/activerecord/lib/active_record/observer.rb create mode 100644 vendor/rails/activerecord/lib/active_record/query_cache.rb create mode 100644 vendor/rails/activerecord/lib/active_record/reflection.rb create mode 100644 vendor/rails/activerecord/lib/active_record/schema.rb create mode 100644 vendor/rails/activerecord/lib/active_record/schema_dumper.rb create mode 100644 vendor/rails/activerecord/lib/active_record/timestamp.rb create mode 100644 vendor/rails/activerecord/lib/active_record/transactions.rb create mode 100755 vendor/rails/activerecord/lib/active_record/validations.rb create mode 100644 vendor/rails/activerecord/lib/active_record/vendor/db2.rb create mode 100644 vendor/rails/activerecord/lib/active_record/vendor/mysql.rb create mode 100644 vendor/rails/activerecord/lib/active_record/vendor/simple.rb create mode 100644 vendor/rails/activerecord/lib/active_record/version.rb create mode 100644 vendor/rails/activerecord/lib/active_record/wrappers/yaml_wrapper.rb create mode 100644 vendor/rails/activerecord/lib/active_record/wrappings.rb create mode 100644 vendor/rails/activerecord/test/aaa_create_tables_test.rb create mode 100755 vendor/rails/activerecord/test/abstract_unit.rb create mode 100644 vendor/rails/activerecord/test/active_schema_mysql.rb create mode 100644 vendor/rails/activerecord/test/adapter_test.rb create mode 100644 vendor/rails/activerecord/test/aggregations_test.rb create mode 100755 vendor/rails/activerecord/test/all.sh create mode 100644 vendor/rails/activerecord/test/ar_schema_test.rb create mode 100644 vendor/rails/activerecord/test/association_callbacks_test.rb create mode 100644 vendor/rails/activerecord/test/association_inheritance_reload.rb create mode 100644 vendor/rails/activerecord/test/associations_cascaded_eager_loading_test.rb create mode 100644 vendor/rails/activerecord/test/associations_extensions_test.rb create mode 100644 vendor/rails/activerecord/test/associations_go_eager_test.rb create mode 100644 vendor/rails/activerecord/test/associations_join_model_test.rb create mode 100755 vendor/rails/activerecord/test/associations_test.rb create mode 100755 vendor/rails/activerecord/test/base_test.rb create mode 100644 vendor/rails/activerecord/test/binary_test.rb create mode 100644 vendor/rails/activerecord/test/calculations_test.rb create mode 100644 vendor/rails/activerecord/test/callbacks_test.rb create mode 100644 vendor/rails/activerecord/test/class_inheritable_attributes_test.rb create mode 100644 vendor/rails/activerecord/test/column_alias_test.rb create mode 100644 vendor/rails/activerecord/test/connections/native_db2/connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_firebird/connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_mysql/connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_openbase/connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_oracle/connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_postgresql/connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_sqlite/connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_sqlite3/connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_sqlite3/in_memory_connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_sqlserver/connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_sqlserver_odbc/connection.rb create mode 100644 vendor/rails/activerecord/test/connections/native_sybase/connection.rb create mode 100644 vendor/rails/activerecord/test/copy_table_sqlite.rb create mode 100644 vendor/rails/activerecord/test/default_test_firebird.rb create mode 100644 vendor/rails/activerecord/test/defaults_test.rb create mode 100755 vendor/rails/activerecord/test/deprecated_associations_test.rb create mode 100755 vendor/rails/activerecord/test/deprecated_finder_test.rb create mode 100644 vendor/rails/activerecord/test/finder_test.rb create mode 100644 vendor/rails/activerecord/test/fixtures/accounts.yml create mode 100644 vendor/rails/activerecord/test/fixtures/author.rb create mode 100644 vendor/rails/activerecord/test/fixtures/author_favorites.yml create mode 100644 vendor/rails/activerecord/test/fixtures/authors.yml create mode 100644 vendor/rails/activerecord/test/fixtures/auto_id.rb create mode 100644 vendor/rails/activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char create mode 100644 vendor/rails/activerecord/test/fixtures/bad_fixtures/attr_with_spaces create mode 100644 vendor/rails/activerecord/test/fixtures/bad_fixtures/blank_line create mode 100644 vendor/rails/activerecord/test/fixtures/bad_fixtures/duplicate_attributes create mode 100644 vendor/rails/activerecord/test/fixtures/bad_fixtures/missing_value create mode 100644 vendor/rails/activerecord/test/fixtures/binary.rb create mode 100644 vendor/rails/activerecord/test/fixtures/categories.yml create mode 100644 vendor/rails/activerecord/test/fixtures/categories/special_categories.yml create mode 100644 vendor/rails/activerecord/test/fixtures/categories/subsubdir/arbitrary_filename.yml create mode 100644 vendor/rails/activerecord/test/fixtures/categories_ordered.yml create mode 100644 vendor/rails/activerecord/test/fixtures/categories_posts.yml create mode 100644 vendor/rails/activerecord/test/fixtures/categorization.rb create mode 100644 vendor/rails/activerecord/test/fixtures/categorizations.yml create mode 100644 vendor/rails/activerecord/test/fixtures/category.rb create mode 100644 vendor/rails/activerecord/test/fixtures/column_name.rb create mode 100644 vendor/rails/activerecord/test/fixtures/comment.rb create mode 100644 vendor/rails/activerecord/test/fixtures/comments.yml create mode 100644 vendor/rails/activerecord/test/fixtures/companies.yml create mode 100755 vendor/rails/activerecord/test/fixtures/company.rb create mode 100644 vendor/rails/activerecord/test/fixtures/company_in_module.rb create mode 100644 vendor/rails/activerecord/test/fixtures/computer.rb create mode 100644 vendor/rails/activerecord/test/fixtures/computers.yml create mode 100644 vendor/rails/activerecord/test/fixtures/course.rb create mode 100644 vendor/rails/activerecord/test/fixtures/courses.yml create mode 100644 vendor/rails/activerecord/test/fixtures/customer.rb create mode 100644 vendor/rails/activerecord/test/fixtures/customers.yml create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/db2.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/db2.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/db22.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/db22.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/firebird.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/firebird.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/firebird2.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/firebird2.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/mysql.drop.sql create mode 100755 vendor/rails/activerecord/test/fixtures/db_definitions/mysql.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/mysql2.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/mysql2.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/openbase.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/openbase.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/openbase2.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/openbase2.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/oracle.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/oracle.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/oracle2.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/oracle2.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/postgresql.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/postgresql.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/postgresql2.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/postgresql2.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/schema.rb create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sqlite.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sqlite.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sqlite2.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sqlite2.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver2.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sqlserver2.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sybase.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sybase.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sybase2.drop.sql create mode 100644 vendor/rails/activerecord/test/fixtures/db_definitions/sybase2.sql create mode 100644 vendor/rails/activerecord/test/fixtures/default.rb create mode 100644 vendor/rails/activerecord/test/fixtures/developer.rb create mode 100644 vendor/rails/activerecord/test/fixtures/developers.yml create mode 100644 vendor/rails/activerecord/test/fixtures/developers_projects.yml create mode 100644 vendor/rails/activerecord/test/fixtures/developers_projects/david_action_controller create mode 100644 vendor/rails/activerecord/test/fixtures/developers_projects/david_active_record create mode 100644 vendor/rails/activerecord/test/fixtures/developers_projects/jamis_active_record create mode 100644 vendor/rails/activerecord/test/fixtures/entrant.rb create mode 100644 vendor/rails/activerecord/test/fixtures/entrants.yml create mode 100644 vendor/rails/activerecord/test/fixtures/fk_test_has_fk.yml create mode 100644 vendor/rails/activerecord/test/fixtures/fk_test_has_pk.yml create mode 100644 vendor/rails/activerecord/test/fixtures/flowers.jpg create mode 100644 vendor/rails/activerecord/test/fixtures/funny_jokes.yml create mode 100644 vendor/rails/activerecord/test/fixtures/joke.rb create mode 100644 vendor/rails/activerecord/test/fixtures/keyboard.rb create mode 100644 vendor/rails/activerecord/test/fixtures/legacy_thing.rb create mode 100644 vendor/rails/activerecord/test/fixtures/legacy_things.yml create mode 100644 vendor/rails/activerecord/test/fixtures/migrations/1_people_have_last_names.rb create mode 100644 vendor/rails/activerecord/test/fixtures/migrations/2_we_need_reminders.rb create mode 100644 vendor/rails/activerecord/test/fixtures/migrations/3_innocent_jointable.rb create mode 100644 vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb create mode 100644 vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb create mode 100644 vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/3_foo.rb create mode 100644 vendor/rails/activerecord/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb create mode 100644 vendor/rails/activerecord/test/fixtures/mixin.rb create mode 100644 vendor/rails/activerecord/test/fixtures/mixins.yml create mode 100644 vendor/rails/activerecord/test/fixtures/movie.rb create mode 100644 vendor/rails/activerecord/test/fixtures/movies.yml create mode 100644 vendor/rails/activerecord/test/fixtures/naked/csv/accounts.csv create mode 100644 vendor/rails/activerecord/test/fixtures/naked/yml/accounts.yml create mode 100644 vendor/rails/activerecord/test/fixtures/naked/yml/companies.yml create mode 100644 vendor/rails/activerecord/test/fixtures/naked/yml/courses.yml create mode 100644 vendor/rails/activerecord/test/fixtures/order.rb create mode 100644 vendor/rails/activerecord/test/fixtures/people.yml create mode 100644 vendor/rails/activerecord/test/fixtures/person.rb create mode 100644 vendor/rails/activerecord/test/fixtures/post.rb create mode 100644 vendor/rails/activerecord/test/fixtures/posts.yml create mode 100644 vendor/rails/activerecord/test/fixtures/project.rb create mode 100644 vendor/rails/activerecord/test/fixtures/projects.yml create mode 100644 vendor/rails/activerecord/test/fixtures/reader.rb create mode 100644 vendor/rails/activerecord/test/fixtures/readers.yml create mode 100755 vendor/rails/activerecord/test/fixtures/reply.rb create mode 100644 vendor/rails/activerecord/test/fixtures/subject.rb create mode 100644 vendor/rails/activerecord/test/fixtures/subscriber.rb create mode 100644 vendor/rails/activerecord/test/fixtures/subscribers/first create mode 100644 vendor/rails/activerecord/test/fixtures/subscribers/second create mode 100644 vendor/rails/activerecord/test/fixtures/tag.rb create mode 100644 vendor/rails/activerecord/test/fixtures/tagging.rb create mode 100644 vendor/rails/activerecord/test/fixtures/taggings.yml create mode 100644 vendor/rails/activerecord/test/fixtures/tags.yml create mode 100644 vendor/rails/activerecord/test/fixtures/task.rb create mode 100644 vendor/rails/activerecord/test/fixtures/tasks.yml create mode 100755 vendor/rails/activerecord/test/fixtures/topic.rb create mode 100644 vendor/rails/activerecord/test/fixtures/topics.yml create mode 100755 vendor/rails/activerecord/test/fixtures_test.rb create mode 100755 vendor/rails/activerecord/test/inheritance_test.rb create mode 100755 vendor/rails/activerecord/test/lifecycle_test.rb create mode 100644 vendor/rails/activerecord/test/locking_test.rb create mode 100644 vendor/rails/activerecord/test/method_scoping_test.rb create mode 100644 vendor/rails/activerecord/test/migration_test.rb create mode 100644 vendor/rails/activerecord/test/mixin_nested_set_test.rb create mode 100644 vendor/rails/activerecord/test/mixin_test.rb create mode 100644 vendor/rails/activerecord/test/modules_test.rb create mode 100644 vendor/rails/activerecord/test/multiple_db_test.rb create mode 100644 vendor/rails/activerecord/test/pk_test.rb create mode 100755 vendor/rails/activerecord/test/readonly_test.rb create mode 100644 vendor/rails/activerecord/test/reflection_test.rb create mode 100644 vendor/rails/activerecord/test/schema_dumper_test.rb create mode 100644 vendor/rails/activerecord/test/schema_test_postgresql.rb create mode 100644 vendor/rails/activerecord/test/synonym_test_oracle.rb create mode 100644 vendor/rails/activerecord/test/threaded_connections_test.rb create mode 100644 vendor/rails/activerecord/test/transactions_test.rb create mode 100755 vendor/rails/activerecord/test/unconnected_test.rb create mode 100755 vendor/rails/activerecord/test/validations_test.rb create mode 100644 vendor/rails/activesupport/CHANGELOG create mode 100644 vendor/rails/activesupport/README create mode 100644 vendor/rails/activesupport/Rakefile create mode 100644 vendor/rails/activesupport/install.rb create mode 100644 vendor/rails/activesupport/lib/active_support.rb create mode 100644 vendor/rails/activesupport/lib/active_support/binding_of_caller.rb create mode 100755 vendor/rails/activesupport/lib/active_support/breakpoint.rb create mode 100644 vendor/rails/activesupport/lib/active_support/caching_tools.rb create mode 100644 vendor/rails/activesupport/lib/active_support/clean_logger.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/array.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/array/conversions.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/blank.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/cgi.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/cgi/escape_skipping_slashes.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/class.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/class/removal.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/date.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/date/conversions.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/enumerable.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/exception.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/hash.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/hash/conversions.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/hash/diff.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/hash/keys.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/integer.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/integer/even_odd.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/integer/inflections.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/kernel.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/kernel/agnostics.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/kernel/daemonizing.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/kernel/reporting.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/kernel/requires.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/load_error.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/logger.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/module.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/module/delegation.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/module/inclusion.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/module/introspection.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/module/loading.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/numeric.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/numeric/bytes.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/numeric/time.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/object.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/object/extending.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/object/misc.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/pathname.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/pathname/clean_within.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/proc.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/range.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/range/conversions.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/string.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/string/access.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/string/conversions.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/string/inflections.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/string/iterators.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/string/starts_ends_with.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/symbol.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/time.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/time/calculations.rb create mode 100644 vendor/rails/activesupport/lib/active_support/core_ext/time/conversions.rb create mode 100644 vendor/rails/activesupport/lib/active_support/dependencies.rb create mode 100644 vendor/rails/activesupport/lib/active_support/inflections.rb create mode 100644 vendor/rails/activesupport/lib/active_support/inflector.rb create mode 100644 vendor/rails/activesupport/lib/active_support/json.rb create mode 100644 vendor/rails/activesupport/lib/active_support/json/encoders.rb create mode 100644 vendor/rails/activesupport/lib/active_support/json/encoders/core.rb create mode 100644 vendor/rails/activesupport/lib/active_support/option_merger.rb create mode 100644 vendor/rails/activesupport/lib/active_support/ordered_options.rb create mode 100644 vendor/rails/activesupport/lib/active_support/reloadable.rb create mode 100644 vendor/rails/activesupport/lib/active_support/values/time_zone.rb create mode 100644 vendor/rails/activesupport/lib/active_support/vendor/builder.rb create mode 100644 vendor/rails/activesupport/lib/active_support/vendor/builder/blankslate.rb create mode 100644 vendor/rails/activesupport/lib/active_support/vendor/builder/xmlbase.rb create mode 100644 vendor/rails/activesupport/lib/active_support/vendor/builder/xmlevents.rb create mode 100644 vendor/rails/activesupport/lib/active_support/vendor/builder/xmlmarkup.rb create mode 100644 vendor/rails/activesupport/lib/active_support/version.rb create mode 100644 vendor/rails/activesupport/lib/active_support/whiny_nil.rb create mode 100644 vendor/rails/activesupport/test/autoloading_fixtures/a/b.rb create mode 100644 vendor/rails/activesupport/test/autoloading_fixtures/a/c/d.rb create mode 100644 vendor/rails/activesupport/test/autoloading_fixtures/a/c/e/f.rb create mode 100644 vendor/rails/activesupport/test/autoloading_fixtures/e.rb create mode 100644 vendor/rails/activesupport/test/autoloading_fixtures/module_folder/nested_class.rb create mode 100644 vendor/rails/activesupport/test/autoloading_fixtures/module_folder/nested_sibling.rb create mode 100644 vendor/rails/activesupport/test/caching_tools_test.rb create mode 100644 vendor/rails/activesupport/test/class_inheritable_attributes_test.rb create mode 100644 vendor/rails/activesupport/test/clean_logger_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/array_ext_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/blank_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/cgi_ext_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/class_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/date_ext_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/enumerable_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/exception_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/hash_ext_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/integer_ext_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/kernel_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/load_error_tests.rb create mode 100644 vendor/rails/activesupport/test/core_ext/module_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/numeric_ext_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/object_and_class_ext_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/pathname_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/proc_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/range_ext_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/string_ext_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/symbol_test.rb create mode 100644 vendor/rails/activesupport/test/core_ext/time_ext_test.rb create mode 100644 vendor/rails/activesupport/test/dependencies/check_warnings.rb create mode 100644 vendor/rails/activesupport/test/dependencies/mutual_one.rb create mode 100644 vendor/rails/activesupport/test/dependencies/mutual_two.rb create mode 100644 vendor/rails/activesupport/test/dependencies/raises_exception.rb create mode 100644 vendor/rails/activesupport/test/dependencies/service_one.rb create mode 100644 vendor/rails/activesupport/test/dependencies/service_two.rb create mode 100644 vendor/rails/activesupport/test/dependencies_test.rb create mode 100644 vendor/rails/activesupport/test/inflector_test.rb create mode 100644 vendor/rails/activesupport/test/json.rb create mode 100644 vendor/rails/activesupport/test/option_merger_test.rb create mode 100644 vendor/rails/activesupport/test/ordered_options_test.rb create mode 100644 vendor/rails/activesupport/test/reloadable_test.rb create mode 100644 vendor/rails/activesupport/test/time_zone_test.rb create mode 100644 vendor/rails/activesupport/test/whiny_nil_test.rb create mode 100755 vendor/rails/cleanlogs.sh create mode 100755 vendor/rails/pushgems.rb create mode 100644 vendor/rails/railties/CHANGELOG create mode 100644 vendor/rails/railties/MIT-LICENSE create mode 100644 vendor/rails/railties/README create mode 100644 vendor/rails/railties/Rakefile create mode 100644 vendor/rails/railties/bin/about create mode 100644 vendor/rails/railties/bin/breakpointer create mode 100644 vendor/rails/railties/bin/console create mode 100644 vendor/rails/railties/bin/destroy create mode 100644 vendor/rails/railties/bin/generate create mode 100644 vendor/rails/railties/bin/performance/benchmarker create mode 100644 vendor/rails/railties/bin/performance/profiler create mode 100644 vendor/rails/railties/bin/plugin create mode 100644 vendor/rails/railties/bin/process/reaper create mode 100644 vendor/rails/railties/bin/process/spawner create mode 100755 vendor/rails/railties/bin/rails create mode 100644 vendor/rails/railties/bin/runner create mode 100644 vendor/rails/railties/bin/server create mode 100644 vendor/rails/railties/builtin/rails_info/rails/info.rb create mode 100644 vendor/rails/railties/builtin/rails_info/rails/info_controller.rb create mode 100644 vendor/rails/railties/builtin/rails_info/rails/info_helper.rb create mode 100644 vendor/rails/railties/builtin/rails_info/rails_info_controller.rb create mode 100755 vendor/rails/railties/configs/apache.conf create mode 100644 vendor/rails/railties/configs/databases/mysql.yml create mode 100644 vendor/rails/railties/configs/databases/oracle.yml create mode 100644 vendor/rails/railties/configs/databases/postgresql.yml create mode 100644 vendor/rails/railties/configs/databases/sqlite2.yml create mode 100644 vendor/rails/railties/configs/databases/sqlite3.yml create mode 100644 vendor/rails/railties/configs/empty.log create mode 100644 vendor/rails/railties/configs/lighttpd.conf create mode 100644 vendor/rails/railties/configs/routes.rb create mode 100755 vendor/rails/railties/dispatches/dispatch.fcgi create mode 100755 vendor/rails/railties/dispatches/dispatch.rb create mode 100644 vendor/rails/railties/dispatches/gateway.cgi create mode 100644 vendor/rails/railties/doc/README_FOR_APP create mode 100644 vendor/rails/railties/environments/boot.rb create mode 100644 vendor/rails/railties/environments/development.rb create mode 100644 vendor/rails/railties/environments/environment.rb create mode 100644 vendor/rails/railties/environments/production.rb create mode 100644 vendor/rails/railties/environments/test.rb create mode 100755 vendor/rails/railties/fresh_rakefile create mode 100644 vendor/rails/railties/helpers/application.rb create mode 100644 vendor/rails/railties/helpers/application_helper.rb create mode 100644 vendor/rails/railties/helpers/test_helper.rb create mode 100644 vendor/rails/railties/html/404.html create mode 100644 vendor/rails/railties/html/500.html create mode 100644 vendor/rails/railties/html/favicon.ico create mode 100644 vendor/rails/railties/html/images/rails.png create mode 100644 vendor/rails/railties/html/index.html create mode 100644 vendor/rails/railties/html/javascripts/application.js create mode 100644 vendor/rails/railties/html/javascripts/controls.js create mode 100644 vendor/rails/railties/html/javascripts/dragdrop.js create mode 100644 vendor/rails/railties/html/javascripts/effects.js create mode 100644 vendor/rails/railties/html/javascripts/prototype.js create mode 100644 vendor/rails/railties/html/robots.txt create mode 100644 vendor/rails/railties/lib/binding_of_caller.rb create mode 100644 vendor/rails/railties/lib/breakpoint.rb create mode 100644 vendor/rails/railties/lib/breakpoint_client.rb create mode 100644 vendor/rails/railties/lib/code_statistics.rb create mode 100644 vendor/rails/railties/lib/commands.rb create mode 100644 vendor/rails/railties/lib/commands/about.rb create mode 100644 vendor/rails/railties/lib/commands/breakpointer.rb create mode 100644 vendor/rails/railties/lib/commands/console.rb create mode 100644 vendor/rails/railties/lib/commands/destroy.rb create mode 100755 vendor/rails/railties/lib/commands/generate.rb create mode 100644 vendor/rails/railties/lib/commands/ncgi/listener create mode 100644 vendor/rails/railties/lib/commands/ncgi/tracker create mode 100644 vendor/rails/railties/lib/commands/performance/benchmarker.rb create mode 100644 vendor/rails/railties/lib/commands/performance/profiler.rb create mode 100644 vendor/rails/railties/lib/commands/plugin.rb create mode 100644 vendor/rails/railties/lib/commands/process/reaper.rb create mode 100644 vendor/rails/railties/lib/commands/process/spawner.rb create mode 100644 vendor/rails/railties/lib/commands/process/spinner.rb create mode 100644 vendor/rails/railties/lib/commands/runner.rb create mode 100644 vendor/rails/railties/lib/commands/server.rb create mode 100644 vendor/rails/railties/lib/commands/servers/lighttpd.rb create mode 100644 vendor/rails/railties/lib/commands/servers/webrick.rb create mode 100644 vendor/rails/railties/lib/commands/update.rb create mode 100644 vendor/rails/railties/lib/console_app.rb create mode 100644 vendor/rails/railties/lib/console_sandbox.rb create mode 100644 vendor/rails/railties/lib/console_with_helpers.rb create mode 100644 vendor/rails/railties/lib/dispatcher.rb create mode 100644 vendor/rails/railties/lib/fcgi_handler.rb create mode 100644 vendor/rails/railties/lib/initializer.rb create mode 100644 vendor/rails/railties/lib/rails/version.rb create mode 100644 vendor/rails/railties/lib/rails_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/base.rb create mode 100644 vendor/rails/railties/lib/rails_generator/commands.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/applications/app/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/applications/app/app_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/controller/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/controller/controller_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/controller.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/functional_test.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/helper.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/controller/templates/view.rhtml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/integration_test/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/integration_test/integration_test_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/integration_test/templates/integration_test.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/mailer/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/mailer/mailer_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/fixture.rhtml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/mailer.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/mailer/templates/view.rhtml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/migration/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/migration/migration_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/migration/templates/migration.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/model/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/model/model_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/model/templates/fixtures.yml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/model/templates/migration.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/model/templates/model.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/model/templates/unit_test.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/plugin/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/plugin/plugin_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/README create mode 100755 vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/Rakefile create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/init.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/install.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/plugin.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/tasks.rake create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/plugin/templates/unit_test.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/scaffold_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/form.rhtml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/form_scaffolding.rhtml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/functional_test.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/helper.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/layout.rhtml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/style.css create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_edit.rhtml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_new.rhtml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/scaffold/templates/view_show.rhtml create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/session_migration/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/session_migration/session_migration_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/session_migration/templates/migration.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/web_service/USAGE create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/api_definition.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/controller.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/web_service/templates/functional_test.rb create mode 100644 vendor/rails/railties/lib/rails_generator/generators/components/web_service/web_service_generator.rb create mode 100644 vendor/rails/railties/lib/rails_generator/lookup.rb create mode 100644 vendor/rails/railties/lib/rails_generator/manifest.rb create mode 100644 vendor/rails/railties/lib/rails_generator/options.rb create mode 100644 vendor/rails/railties/lib/rails_generator/scripts.rb create mode 100644 vendor/rails/railties/lib/rails_generator/scripts/destroy.rb create mode 100644 vendor/rails/railties/lib/rails_generator/scripts/generate.rb create mode 100644 vendor/rails/railties/lib/rails_generator/scripts/update.rb create mode 100644 vendor/rails/railties/lib/rails_generator/simple_logger.rb create mode 100644 vendor/rails/railties/lib/rails_generator/spec.rb create mode 100644 vendor/rails/railties/lib/railties_path.rb create mode 100644 vendor/rails/railties/lib/ruby_version_check.rb create mode 100644 vendor/rails/railties/lib/rubyprof_ext.rb create mode 100644 vendor/rails/railties/lib/tasks/databases.rake create mode 100644 vendor/rails/railties/lib/tasks/documentation.rake create mode 100644 vendor/rails/railties/lib/tasks/framework.rake create mode 100644 vendor/rails/railties/lib/tasks/log.rake create mode 100644 vendor/rails/railties/lib/tasks/misc.rake create mode 100644 vendor/rails/railties/lib/tasks/pre_namespace_aliases.rake create mode 100644 vendor/rails/railties/lib/tasks/rails.rb create mode 100644 vendor/rails/railties/lib/tasks/statistics.rake create mode 100644 vendor/rails/railties/lib/tasks/testing.rake create mode 100644 vendor/rails/railties/lib/tasks/tmp.rake create mode 100644 vendor/rails/railties/lib/test_help.rb create mode 100644 vendor/rails/railties/lib/webrick_server.rb create mode 100644 vendor/rails/railties/test/dispatcher_test.rb create mode 100644 vendor/rails/railties/test/fcgi_dispatcher_test.rb create mode 100644 vendor/rails/railties/test/fixtures/environment_with_constant.rb create mode 100644 vendor/rails/railties/test/fixtures/plugins/default/stubby/init.rb create mode 100644 vendor/rails/railties/test/fixtures/plugins/default/stubby/lib/stubby_mixin.rb create mode 100644 vendor/rails/railties/test/generators/missing_class/missing_class_generator.rb create mode 100644 vendor/rails/railties/test/generators/working/working_generator.rb create mode 100644 vendor/rails/railties/test/initializer_test.rb create mode 100644 vendor/rails/railties/test/mocks/dispatcher.rb create mode 100644 vendor/rails/railties/test/mocks/fcgi.rb create mode 100644 vendor/rails/railties/test/plugin_test.rb create mode 100644 vendor/rails/railties/test/rails_generator_test.rb create mode 100644 vendor/rails/railties/test/rails_info_controller_test.rb create mode 100644 vendor/rails/railties/test/rails_info_test.rb create mode 100644 vendor/rails/railties/test/webrick_dispatcher_test.rb create mode 100755 vendor/rails/release.rb diff --git a/CHANGELOG b/CHANGELOG new file mode 100755 index 00000000..a0c56d89 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,270 @@ + * TRUNK: + + - ANTISPAM: + - updated and included spam_patterns.txt + - included dnsbl_check - DNS Blackhole Lists check + [thanks to joost from http://www.spacebabies.nl ] + + - BUGFIXES: + - fix PDF output not to contain garbage chars [Jesse Newland] + - fixed the pages and authors display for single webs + - web list does not show a link to a published version if it has none + [Jesse Newland] + - Fixed bug that failed to expire cached diff view of a page + - Fixed rendering of WikiLinks in included pages in published or export + mode + - lots of small bugfixes and changes + + - UPDATES: + - Rails 1.2 tested and packaged with instiki + - updated RubyZip to 0.9.1 + - updated RedCloth to 3.0.4 + - updated packaged sqlite3-ruby to 1.2.0 + + - FEATURES: + - Stylesheet tweaks + - visual display if webs are pass-protected (div background) + - Linux packaging + +------------------------------------------------------------------------------ + * 0.11.0: + - SQL-based backend (ActiveRecord) + - File uploads (finally) + - Upgraded to Rails 1.0.0 + - Replaced internal link generator with routing + - Fixed --daemon option + - Removed Rubygem and native OS X distributions + - Improved HTML diff + - More accurate "See Changes" +------------------------------------------------------------------------------ + * 0.10.2: + - Upgraded to Rails 0.13.1 + - Fixed HTML export + - Added layout=no option to the export_html action (it exports page + contents processed by the markup engine, but without the default layout - + so that they can be wrapped in some other layout) + - tag can span several lines (before it was applied when both + opening and closing tags were on the same line only) + - Resolved the "endless redirection loop" condition and otherwise improved + handling of errors in the rendering engines + - Fixed rendering of Markdown hyperlinks such as + [Text](http://something.com/foo) +------------------------------------------------------------------------------ + * 0.10.1: + - Upgraded Rails to 0.12.0 + - Upgraded rubyzip to version 0.5.8 + - BlueCloth is back (RedCloth didn't do pure Markdown well enough) + - Handling of line breaks in Textile is as in 0.9 (inserts
tag) + - Fixed HTML export (to enclose the output in tags, include the + stylesheet etc) + - Corrected some compatibility issues with storages from earlier Instiki + versions + - Some other bug fixes +------------------------------------------------------------------------------ + * 0.10.0: + - Ported to ActionPack + - RedCloth 3.0.3 + - BlueCloth is phased out, Markdown is rendered by RedCloth + - Mix markup option understands both Textile and Markdown on the same page + - Instiki can serve static content (such as HTML or plain-text files) from + ./public directory + - Much friendlier admin interface + - Wiki link syntax doesn't conflict with Textile hyperlink syntax. + Therefore "textile link":LinkToSomePlace will not look insane. + - RSS feeds accept query parameters, such as + http://localhost:2500/wiki/rss_with_headlines?start=2005-02-18&end=2005-02-19&limit=10 + - RSS feed with page contents for a password-protected web behaves as + follows: if the web is published, RSS feed links to the published version + of the web. otherwise, the feed is not available + Madeleine will check every hour if there are new commands in the log or + 24 hours have passed since last snapshot, and take snapshot if either of + these conditions is true. Madeleine will also not log read-only + operations, resulting in a better performance + - Wiki extracts (to HTML and plain text) will leave only the last extract + file in ./storage + - Wiki search handles multibyte (UTF-8) characters correctly + - Local hyperlinks in published pages point to published pages [Michael + DeHaan] + - Fixed a bug that sometimes caused all past revisions of a page to be + "forgotten" on restart + - Fixed parsing of URIs with a port number (http://someplace.org:8080) + - Instiki will not fork itself on a *nix, unless explicitly asked to + - Instiki can bind to IPs other than 127.0.0.1 (command-line option) + - Revisions that do not change anything on the page are rejected + - Automated tests for all controller actions + - category: lines are presented as links to "All Pages" for relevant + categories + - Search looks at page titles, as well as content + - Multiple other usability enhancements and bug fixes +------------------------------------------------------------------------------ + * 0.9.2: + - Rollback takes the user to an edit form. The form has to be submitted for + the change to take place. + - Changed to use inline style on published pages + - Fixed "forward in time" on the last revision before current page + - Instiki won't log bogus error messages when creating a new Wiki + - Fixed deprecation warning for Object.id (introduced in Ruby 1.8.2) + - Madeleine upgraded to 0.7.1 + - Madeleine snapshots are compressed + - Packaged as a gem +------------------------------------------------------------------------------ + * 0.9.1: + - Added performance improvements for updating existing pages + - Fixed IP logging and RSS feeds behind proxies [With help from Guan Yang] + - Fixed default storage directory (borked running on Windows) + [Spotted by Curt Hibbs] +------------------------------------------------------------------------------ + * 0.9.0: + - Added aliased links such as [[HomePage|that nice home page]] [Mark Reid] + - Added include other page content with [[!include TableOfContents]] + [Mark Reid] + - Added delete orphan pages from the Edit Web screen [by inspiration from + Simon Arnaud] + - Added logging of IP address for authors (who's behind the rollback wars) + - Added Categories pages through backlinks (use "categories: news, instiki" + on start of line) [Mark Reid] + - Added option to use bracket-style wiki links only (and hence ban + WikiWords) + - Added command-line option to specify different storage path + - Added print view without navigation + - Added character and page (2275 characters including spaces) counter + (important for student papers) + - Off by default, activate it on the Edit Web screen + - Added LaTeX/PDF integration on Textile installations with pdflatex + installed on system (EXPERIMENTAL) + - Use the home page as a table of contents with a unordered list to control + sections + - Added limit of 15 to the number of pages included in RSS feed + - Moved static parts of stylesheet to separate file [Lau T?rnskov] + - Fixed better semantics for revision movement [Ryan Singer] + - Fixed color diffs to work much better [Xen/Mertz/Atkins] + - Fixed performance problems for All Pages list [Dennis Mertz] + - Fixed lots of rendering bugs [Mark Reid] + - Upgraded to RedCloth 2.0.11 [integrating the fine work of Dennis Mertz] +------------------------------------------------------------------------------ + * 0.8.9: + - Added color diffs to see changes between revisions [Bill Atkins] + They're aren't quite perfect yet as new paragraphs split the tags + (hence 0.8.9, not 0.9.0) + - Added redirect to edit if content of page generates an error + (so the page doesn't become unusable on bugs in the markup engines) + - Fixed update Web with different address bug [Denis Metz] + - Fixed a bunch of wiki word rendering issues by doing wiki word detection + and replacment at once + - Upgraded to BlueCloth 0.0.3b (should fix loads of problems on Markdown + wikis) +------------------------------------------------------------------------------ + * 0.8.5: + - Instiki can now serve as a CMS by running a password-protected web with a + published front + - Added version check at startup (Instiki needs Ruby 1.8.1) +------------------------------------------------------------------------------ + * 0.8.1: + - Actually included RedCloth 2.0.7 in the release +------------------------------------------------------------------------------ + * 0.8.0: + - NOTE: Single-web wikis created in versions prior to 0.8.0 have "instiki" + as their system password + - Accepts wiki words in bracket style. + Examples: [[wiki word]], [[c]], [[We could'nt have done it!]] + - Accepts camel-case wiki words in all latin, greek, cyrillian, and + armenian unicode characters + - Many thanks to Guan Yang for building the higher- and lower-case lookup + tables. And thanks to Simon Arnaud for the initial patch that got the + work started + - Changed charset to UTF-8 + - Cut down on command-line options and replaced them with an per-web config + screen + - Added option to extend the stylesheet on a per-web basis to tweak the + look in details + - Added simple color options for variety + - Added option to add/remove password protection on each web + - Added the wiki name of the author locking a given page (instead of just + "someone") + - Removed single/multi-web distinction -- all Instikis are now multi-web + - Load libraries from an unshifted load path, so that old installed + libraries doesn't clash [Emiel van de Laar] + - Keeps the author cookie forever, so you don't have to enter your name + again and again + - Fixed XHTML so it validates [Bruce D'Arcus] + - Authors are no longer listed under orphan pages + - Added export to markup (great for backups, potentially for switching wiki + engine) + - Don't link wiki words that proceeds from either /, = or ? + (http://c2.com/cgi/wiki?WikiWikiClones, + /show/HomePage, cgi.pl?show=WikiWord without escaping) + - Accessing an unexisting page redirects to a different url (/new/PageName) + - Increased snapshot time to just once a day (cuts down on disk storage + requirements) + - Made RDoc support work better with 1.8.1 [Mauricio Fern?ndez] + - Added convinient redirect from /wiki/ to /wiki/show/HomePage + - Fixed BlueCloth bug with backticks at start of line + - Updated to RedCloth 2.0.7 (and linked to the new Textile reference) +------------------------------------------------------------------------------ + * 0.7.0: + - Added Markdown (BlueCloth) and RDoc [Mauricio Fern?ndez] as command-line + markup choices + - Added wanted and orphan page lists to All pages (only show up if there's + actually orphan or wanted pages) + - Added ISO-8859-1 as XML encoding in RSS feeds (makes FeedReader among + others happy for special entities) + - Added proper links in the RSS feed (but the body links are still + relative, which NNW and others doesn't grok) + - Added access keys: E => Edit, H => HomePage, A => All Pages, + U => Recently Revised, X => Export + - Added password-login through URL (so you can subscribe to feed on a + protected web) + - Added web passwords to the feed links for protected webs, so they work + without manual login + - Added the web name in small letters above all pages within a web + - Polished authors and recently revised + - Updated to RedCloth 2.0.6 + - Changed content type for RSS feeds to text/xml (makes Mozilla Aggreg8 + happy) + - Changed searching to be case insensitive + - Changed HomePage to display the name of the web instead + - Changed exported HTML pages to be valid XHTML (which can be preprocessed + by XSLT) + - Fixed broken recently revised +------------------------------------------------------------------------------ + * 0.6.0: + - Fixed Windows compatibility [Florian] + - Fixed bug that would prevent Madeleine from taking snapshots in Daemon + mode + - Added export entire web as HTML in a zip file + - Added RSS feeds + - Added proper getops support for the growing number of options [Florian] + - Added safe mode that forbids style options in RedCloth [Florian] + - Updated RedCloth to 2.0.5 +------------------------------------------------------------------------------ + * 0.5.0: + - NOTE: 0.5.0 is NOT compatible with databases from earlier versions + - Added revisions + - Added multiple webs + - Added password protection for webs on multi-web setups + - Added the notion of authors (that are saved in a cookie) + - Added command-line option for not running as a Daemon on Unix +------------------------------------------------------------------------------ + * 0.3.1: + - Added option to escape wiki words with \ +------------------------------------------------------------------------------ + * 0.3.0: + - Brought all files into common style (including Textile help on the edit + page) + - Added page locking (if someone already is editing a page there's a + warning) + - Added daemon abilities on Unix (keep Instiki running after you close the + terminal) + - Made port 2500 the default port, so Instiki can be launched by + dobbelt-click + - Added Textile cache to speed-up rendering of large pages + - Made WikiWords look like "Wiki Words" + - Updated RedCloth to 2.0.4 +------------------------------------------------------------------------------ + * 0.2.5: + - Upgraded to RedCloth 2.0.2 and Madeleine 0.6.1, which means the + - Windows problems are gone. Also fixed a problem with wikiwords + - that used part of other wikiwords. +------------------------------------------------------------------------------ + * 0.2.0: + - First public release diff --git a/README b/README new file mode 100755 index 00000000..ffcab202 --- /dev/null +++ b/README @@ -0,0 +1,113 @@ +===What is Instiki? + +Admitted, it's YetAnotherWikiClone[http://c2.com/cgi/wiki?WikiWikiClones], but with a strong focus +on simplicity of installation and running: + +Step 1. Download +Step 2. Run "instiki" + +If you are on Windows: +"Step 3. Chuckle... "There's no step three!" (TM)" + +You're now running a perfectly suitable wiki on port 2500 +that'll present you with one-step setup, followed by a textarea for the home page +on http://localhost:2500 + +Instiki lowers the barriers of interest for when you might consider +using a wiki. It's so simple to get running that you'll find yourself +using it for anything -- taking notes, brainstorming, organizing a +gathering. + +Having said all that, if you are not on Windows, in this version of Instiki it is a somewhat different story. +Since the author has no Linux or Mac at hand, and Instiki is moving to a SQL-based backend, this is what it takes +to install (until somebody sends a patch to properly package Instiki for all those other platforms): + +3. Kill "instiki" +4. Install SQLite 3 database engine from http://www.sqlite.org/ +5. Install SQLite 3 driver for Ruby from http://sqlite-ruby.rubyforge.org/ +6. Install Rake from http://rake.rubyforge.org/ +7. Execute rm -f db/*.db +8. Execute 'rake environment RAILS_ENV=production migrate' +9. Make an embarrassed sigh (as I do while writing this) +10. Run 'instiki' again +11. Pat yourself on the shoulder for being such a talented geek +12. At least, there is no step twelve! (TM) + +===Features: +* Regular expression search: Find deep stuff really fast +* Revisions: Follow the changes on every page from birth. Rollback to an earlier rev +* Export to HTML or markup in a zip: Take the entire wiki with you home or for reference +* RSS feeds to track recently revised pages +* Multiple webs: Create separate wikis with their own namespace +* Password-protected webs: Keep it private +* Authors: Each revision is associated with an author, so you can see who changed what +* Reference tracker: Which other pages are pointing to the current? +* Speed: Using Madelein[http://madeleine.sourceforge.net] for persistence (all pages are in memory) +* Three markup choices: Textile[http://www.textism.com/tools/textile] + (default / RedCloth[http://www.whytheluckystiff.net/ruby/redcloth]), + Markdown (BlueCloth[http://bluecloth.rubyforge.org]), and RDoc[http://rdoc.sourceforge.net/doc] +* Embedded webserver: Through WEBrick[http://www.webrick.org] +* Internationalization: Wiki words in any latin, greek, cyrillian, or armenian characters +* Color diffs: Track changes through revisions +* Definitely can run on SQLite and MySQL +* May be able to run on Postgres, Oracle, DB2 and SqlServer. If you try this, and it works + (or, it doesn't, but you make it work) please write about it on Instiki.org. + +===Command-line options: +* Run "ruby instiki --help" + +===History: + * See CHANGELOG + +===Migrating Instiki 0.10.2 storage to Instiki 0.11.0 database +1. Install Instiki 0.11 and check that it works (you should be able to create a web, edit and save a HomePage) +2. Execute + ruby script\import_storage \ + -t /full/path/to/instiki0.10/storage \ + -i /full/path/to/instiki0.10/installation \ + -d sqlite (or mysql, or postgres, depending on what you use) \ + -o instiki_import.sql + for example (Windows): + ruby script\import_storage -t c:\instiki-0.10.2\storage\2500 -i c:\instiki-0.10.2 -d sqlite -o instiki_import.sql +3. This will produce instiki_import.sql file in the current working directory. + Open it in a text editor and inspect carefully. +4. Connect to your production database (e.g., 'sqlite3 db\prod.db'), + and have it execute instiki_import.sql (e.g., '.read instiki_import.sql') +5. Execute ruby script\reset_references + (this script parses all pages for crosslinks between them, so it may take a few minutes) +6. Restart Instiki +7. Go over some pages, especially those with a lot of complex markup, and see if anything is broken. + +The most common migration problem is this: if you open All Pages and see a lot of orphaned pages, +you forgot to run ruby script\reset_references after importing the data. + +===Upgrading from Instiki-AR Beta 1 +In Beta 2, we switch to ActiveRecord:Migrations. Therefore: +1. Back up your production database. +2. Open command-line session to your database and execute: + create table schema_info (version integer(11)); + insert into schema_info (version) values (1); +3. Go back to the shell, change directory to the new Instiki and execute "rake migrate". + +Step 2 creates a table that tells to ActiveRecord:Migrations that the current version +of this database is 1 (corresponding to Beta 1), and step 3 makes it up-to-date with +the current version of Instiki. + +===Download the latest release from: +* http://rubyforge.org/project/showfiles.php?group_id=186 + +===Visit the "official" Instiki wiki: +* http://instiki.org + +===License: +* same as Ruby's + +--- +Authors:: + +Versions 0.0 to 0.9.1:: David Heinemeier Hansson +Email:: david@loudthinking.com +Weblog:: http://www.loudthinking.com + +From 0.9.2 onwards:: Alexey Verkhovsky +Email:: alex@verk.info diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb new file mode 100644 index 00000000..4390359d --- /dev/null +++ b/app/controllers/admin_controller.rb @@ -0,0 +1,94 @@ +require 'application' + +class AdminController < ApplicationController + + layout 'default' + cache_sweeper :web_sweeper + + def create_system + if @wiki.setup? + flash[:error] = + "Wiki has already been created in '#{@wiki.storage_path}'. " + + "Shut down Instiki and delete this directory if you want to recreate it from scratch." + + "\n\n" + + "(WARNING: this will destroy content of your current wiki)." + redirect_home(@wiki.webs.keys.first) + elsif @params['web_name'] + # form submitted -> create a wiki + @wiki.setup(@params['password'], @params['web_name'], @params['web_address']) + flash[:info] = "Your new wiki '#{@params['web_name']}' is created!\n" + + "Please edit its home page and press Submit when finished." + redirect_to :web => @params['web_address'], :controller => 'wiki', :action => 'new', + :id => 'HomePage' + else + # no form submitted -> go to template + end + end + + def create_web + if @params['address'] + # form submitted + if @wiki.authenticate(@params['system_password']) + begin + @wiki.create_web(@params['name'], @params['address']) + flash[:info] = "New web '#{@params['name']}' successfully created." + redirect_to :web => @params['address'], :controller => 'wiki', :action => 'new', + :id => 'HomePage' + rescue Instiki::ValidationError => e + @error = e.message + # and re-render the form again + end + else + redirect_to :controller => 'wiki', :action => 'index' + end + else + # no form submitted -> render template + end + end + + def edit_web + system_password = @params['system_password'] + if system_password + # form submitted + if wiki.authenticate(system_password) + begin + wiki.edit_web( + @web.address, @params['address'], @params['name'], + @params['markup'].intern, + @params['color'], @params['additional_style'], + @params['safe_mode'] ? true : false, + @params['password'].empty? ? nil : @params['password'], + @params['published'] ? true : false, + @params['brackets_only'] ? true : false, + @params['count_pages'] ? true : false, + @params['allow_uploads'] ? true : false, + @params['max_upload_size'] + ) + flash[:info] = "Web '#{@params['address']}' was successfully updated" + redirect_home(@params['address']) + rescue Instiki::ValidationError => e + logger.warn e.message + @error = e.message + # and re-render the same template again + end + else + @error = password_error(system_password) + # and re-render the same template again + end + else + # no form submitted - go to template + end + end + + def remove_orphaned_pages + if wiki.authenticate(@params['system_password_orphaned']) + wiki.remove_orphaned_pages(@web_name) + flash[:info] = 'Orphaned pages removed' + redirect_to :controller => 'wiki', :web => @web_name, :action => 'list' + else + flash[:error] = password_error(@params['system_password_orphaned']) + redirect_to :controller => 'admin', :web => @web_name, :action => 'edit_web' + end + end + +end diff --git a/app/controllers/application.rb b/app/controllers/application.rb new file mode 100644 index 00000000..59c43eb0 --- /dev/null +++ b/app/controllers/application.rb @@ -0,0 +1,190 @@ +# The filters added to this controller will be run for all controllers in the application. +# Likewise will all the methods added be available for all controllers. +class ApplicationController < ActionController::Base +# require 'dnsbl_check' + before_filter :dnsbl_check, :connect_to_model, :check_authorization, :setup_url_generator, :set_content_type_header, :set_robots_metatag + after_filter :remember_location, :teardown_url_generator + + # For injecting a different wiki model implementation. Intended for use in tests + def self.wiki=(the_wiki) + # a global variable is used here because Rails reloads controller and model classes in the + # development environment; therefore, storing it as a class variable does not work + # class variable is, anyway, not much different from a global variable + #$instiki_wiki_service = the_wiki + logger.debug("Wiki service: #{the_wiki.to_s}") + end + + def self.wiki + Wiki.new + end + + protected + + def check_authorization + if in_a_web? and authorization_needed? and not authorized? + redirect_to :controller => 'wiki', :action => 'login', :web => @web_name + return false + end + end + + def connect_to_model + @action_name = @params['action'] || 'index' + @web_name = @params['web'] + @wiki = wiki + @author = cookies['author'] || 'AnonymousCoward' + if @web_name + @web = @wiki.webs[@web_name] + if @web.nil? + render(:status => 404, :text => "Unknown web '#{@web_name}'") + return false + end + end + end + + FILE_TYPES = { + '.exe' => 'application/octet-stream', + '.gif' => 'image/gif', + '.jpg' => 'image/jpeg', + '.pdf' => 'application/pdf', + '.png' => 'image/png', + '.txt' => 'text/plain', + '.zip' => 'application/zip' + } unless defined? FILE_TYPES + + DISPOSITION = { + 'application/octet-stream' => 'attachment', + 'image/gif' => 'inline', + 'image/jpeg' => 'inline', + 'application/pdf' => 'inline', + 'image/png' => 'inline', + 'text/plain' => 'inline', + 'application/zip' => 'attachment' + } unless defined? DISPOSITION + + def determine_file_options_for(file_name, original_options = {}) + original_options[:type] ||= (FILE_TYPES[File.extname(file_name)] or 'application/octet-stream') + original_options[:disposition] ||= (DISPOSITION[original_options[:type]] or 'attachment') + original_options[:stream] ||= false + original_options + end + + def send_file(file, options = {}) + determine_file_options_for(file, options) + super(file, options) + end + + def password_check(password) + if password == @web.password + cookies['web_address'] = password + true + else + false + end + end + + def password_error(password) + if password.nil? or password.empty? + 'Please enter the password.' + else + 'You entered a wrong password. Please enter the right one.' + end + end + + def redirect_home(web = @web_name) + if web + redirect_to_page('HomePage', web) + else + redirect_to_url '/' + end + end + + def redirect_to_page(page_name = @page_name, web = @web_name) + redirect_to :web => web, :controller => 'wiki', :action => 'show', + :id => (page_name or 'HomePage') + end + + def remember_location + if @request.method == :get and + @response.headers['Status'] == '200 OK' and not + %w(locked save back file pic import).include?(action_name) + @session[:return_to] = @request.request_uri + logger.debug "Session ##{session.object_id}: remembered URL '#{@session[:return_to]}'" + end + end + + def rescue_action_in_public(exception) + render :status => 500, :text => <<-EOL + +

Internal Error

+

An application error occurred while processing your request.

+ + + EOL + end + + def return_to_last_remembered + # Forget the redirect location + redirect_target, @session[:return_to] = @session[:return_to], nil + tried_home, @session[:tried_home] = @session[:tried_home], false + + # then try to redirect to it + if redirect_target.nil? + if tried_home + raise 'Application could not render the index page' + else + logger.debug("Session ##{session.object_id}: no remembered redirect location, trying home") + redirect_home + end + else + logger.debug("Session ##{session.object_id}: " + + "redirect to the last remembered URL #{redirect_target}") + redirect_to_url(redirect_target) + end + end + + def set_content_type_header + if %w(rss_with_content rss_with_headlines).include?(action_name) + @response.headers['Content-Type'] = 'text/xml; charset=UTF-8' + else + @response.headers['Content-Type'] = 'text/html; charset=UTF-8' + end + end + + def set_robots_metatag + if controller_name == 'wiki' and %w(show published).include? action_name + @robots_metatag_value = 'index,follow' + else + @robots_metatag_value = 'noindex,nofollow' + end + end + + def setup_url_generator + PageRenderer.setup_url_generator(UrlGenerator.new(self)) + end + + def teardown_url_generator + PageRenderer.teardown_url_generator + end + + def wiki + self.class.wiki + end + + private + + def in_a_web? + not @web_name.nil? + end + + def authorization_needed? + not %w( login authenticate published rss_with_content rss_with_headlines ).include?(action_name) + end + + def authorized? + @web.nil? or + @web.password.nil? or + cookies['web_address'] == @web.password or + password_check(@params['password']) + end + +end diff --git a/app/controllers/cache_sweeping_helper.rb b/app/controllers/cache_sweeping_helper.rb new file mode 100644 index 00000000..2bf3e9f8 --- /dev/null +++ b/app/controllers/cache_sweeping_helper.rb @@ -0,0 +1,23 @@ +module CacheSweepingHelper + + def expire_cached_page(web, page_name) + expire_action :controller => 'wiki', :web => web.address, + :action => %w(show published), :id => page_name + expire_action :controller => 'wiki', :web => web.address, + :action => %w(show published), :id => page_name, :mode => 'diff' + end + + def expire_cached_summary_pages(web) + categories = WikiReference.find(:all, :conditions => "link_type = 'C'") + %w(recently_revised list).each do |action| + expire_action :controller => 'wiki', :web => web.address, :action => action + categories.each do |category| + expire_action :controller => 'wiki', :web => web.address, :action => action, :category => category.referenced_name + end + end + + expire_action :controller => 'wiki', :web => web.address, :action => 'authors' + expire_fragment :controller => 'wiki', :web => web.address, :action => %w(rss_with_headlines rss_with_content) + end + +end \ No newline at end of file diff --git a/app/controllers/file_controller.rb b/app/controllers/file_controller.rb new file mode 100644 index 00000000..865450aa --- /dev/null +++ b/app/controllers/file_controller.rb @@ -0,0 +1,100 @@ +# Controller responsible for serving files and pictures. + +require 'zip/zip' + +class FileController < ApplicationController + + layout 'default' + + before_filter :check_allow_uploads + + def file + @file_name = params['id'] + if @params['file'] + # form supplied + new_file = @web.wiki_files.create(@params['file']) + if new_file.valid? + flash[:info] = "File '#{@file_name}' successfully uploaded" + return_to_last_remembered + else + # pass the file with errors back into the form + @file = new_file + render + end + else + # no form supplied, this is a request to download the file + file = WikiFile.find_by_file_name(@file_name) + if file + send_data(file.content, determine_file_options_for(@file_name, :filename => @file_name)) + else + @file = WikiFile.new(:file_name => @file_name) + render + end + end + end + + def cancel_upload + return_to_last_remembered + end + + def import + if @params['file'] + @problems = [] + import_file_name = "#{@web.address}-import-#{Time.now.strftime('%Y-%m-%d-%H-%M-%S')}.zip" + import_from_archive(@params['file'].path) + if @problems.empty? + flash[:info] = 'Import successfully finished' + else + flash[:error] = 'Import finished, but some pages were not imported:
  • ' + + @problems.join('
  • ') + '
  • ' + end + return_to_last_remembered + else + # to template + end + end + + protected + + def check_allow_uploads + render(:status => 404, :text => "Web #{@params['web'].inspect} not found") and return false unless @web + if @web.allow_uploads? + return true + else + render :status => 403, :text => 'File uploads are blocked by the webmaster' + return false + end + end + + private + + def import_from_archive(archive) + logger.info "Importing pages from #{archive}" + zip = Zip::ZipInputStream.open(archive) + while (entry = zip.get_next_entry) do + ext_length = File.extname(entry.name).length + page_name = entry.name[0..-(ext_length + 1)] + page_content = entry.get_input_stream.read + logger.info "Processing page '#{page_name}'" + begin + existing_page = @wiki.read_page(@web.address, page_name) + if existing_page + if existing_page.content == page_content + logger.info "Page '#{page_name}' with the same content already exists. Skipping." + next + else + logger.info "Page '#{page_name}' already exists. Adding a new revision to it." + wiki.revise_page(@web.address, page_name, page_content, Time.now, @author, PageRenderer.new) + end + else + wiki.write_page(@web.address, page_name, page_content, Time.now, @author, PageRenderer.new) + end + rescue => e + logger.error(e) + @problems << "#{page_name} : #{e.message}" + end + end + logger.info "Import from #{archive} finished" + end + +end diff --git a/app/controllers/revision_sweeper.rb b/app/controllers/revision_sweeper.rb new file mode 100644 index 00000000..1db2d2c6 --- /dev/null +++ b/app/controllers/revision_sweeper.rb @@ -0,0 +1,29 @@ +require_dependency 'cache_sweeping_helper' + +class RevisionSweeper < ActionController::Caching::Sweeper + + include CacheSweepingHelper + + observe Revision, Page + + def after_save(record) + if record.is_a?(Revision) + expire_caches(record.page) + end + end + + def after_delete(record) + if record.is_a?(Page) + expire_caches(record) + end + end + + private + + def expire_caches(page) + expire_cached_summary_pages(page.web) + pages_to_expire = ([page.name] + WikiReference.pages_that_reference(page.name)).uniq + pages_to_expire.each { |page_name| expire_cached_page(page.web, page_name) } + end + +end diff --git a/app/controllers/web_sweeper.rb b/app/controllers/web_sweeper.rb new file mode 100644 index 00000000..38e8f3da --- /dev/null +++ b/app/controllers/web_sweeper.rb @@ -0,0 +1,14 @@ +require_dependency 'cache_sweeping_helper' + +class WebSweeper < ActionController::Caching::Sweeper + + include CacheSweepingHelper + + observe Web + + def after_save(record) + web = record + web.pages.each { |page| expire_cached_page(web, page.name) } + expire_cached_summary_pages(web) + end +end diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb new file mode 100644 index 00000000..6bf52f7c --- /dev/null +++ b/app/controllers/wiki_controller.rb @@ -0,0 +1,429 @@ +require 'fileutils' +require 'redcloth_for_tex' +require 'parsedate' +require 'zip/zip' + +class WikiController < ApplicationController + + before_filter :load_page + caches_action :show, :published, :authors, :recently_revised, :list + cache_sweeper :revision_sweeper + + layout 'default', :except => [:rss_feed, :rss_with_content, :rss_with_headlines, :tex, :export_tex, :export_html] + + def index + if @web_name + redirect_home + elsif not @wiki.setup? + redirect_to :controller => 'admin', :action => 'create_system' + elsif @wiki.webs.length == 1 + redirect_home @wiki.webs.values.first.address + else + redirect_to :action => 'web_list' + end + end + + # Outside a single web -------------------------------------------------------- + + def authenticate + if password_check(@params['password']) + redirect_home + else + flash[:info] = password_error(@params['password']) + redirect_to :action => 'login', :web => @web_name + end + end + + def login + # to template + end + + def web_list + @webs = wiki.webs.values.sort_by { |web| web.name } + end + + + # Within a single web --------------------------------------------------------- + + def authors + @page_names_by_author = @web.page_names_by_author + @authors = @page_names_by_author.keys.sort + end + + def export_html + stylesheet = File.read(File.join(RAILS_ROOT, 'public', 'stylesheets', 'instiki.css')) + export_pages_as_zip('html') do |page| + + renderer = PageRenderer.new(page.revisions.last) + rendered_page = <<-EOL + + + + #{page.plain_name} in #{@web.name} + + + + + + + #{renderer.display_content_for_export} + + + + EOL + rendered_page + end + end + + def export_markup + export_pages_as_zip(@web.markup) { |page| page.content } + end + + def export_pdf + file_name = "#{@web.address}-tex-#{@web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}" + file_path = File.join(@wiki.storage_path, file_name) + + export_web_to_tex "#{file_path}.tex" unless FileTest.exists? "#{file_path}.tex" + convert_tex_to_pdf "#{file_path}.tex" + send_file "#{file_path}.pdf" + end + + def export_tex + file_name = "#{@web.address}-tex-#{@web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}.tex" + file_path = File.join(@wiki.storage_path, file_name) + export_web_to_tex(file_path) unless FileTest.exists?(file_path) + send_file file_path + end + + def feeds + @rss_with_content_allowed = rss_with_content_allowed? + # show the template + end + + def list + parse_category + @page_names_that_are_wanted = @pages_in_category.wanted_pages + @pages_that_are_orphaned = @pages_in_category.orphaned_pages + end + + def recently_revised + parse_category + @pages_by_revision = @pages_in_category.by_revision + @pages_by_day = Hash.new { |h, day| h[day] = [] } + @pages_by_revision.each do |page| + day = Date.new(page.revised_at.year, page.revised_at.month, page.revised_at.day) + @pages_by_day[day] << page + end + end + + def rss_with_content + if rss_with_content_allowed? + render_rss(hide_description = false, *parse_rss_params) + else + render_text 'RSS feed with content for this web is blocked for security reasons. ' + + 'The web is password-protected and not published', '403 Forbidden' + end + end + + def rss_with_headlines + render_rss(hide_description = true, *parse_rss_params) + end + + def search + @query = @params['query'] + @title_results = @web.select { |page| page.name =~ /#{@query}/i }.sort + @results = @web.select { |page| page.content =~ /#{@query}/i }.sort + all_pages_found = (@results + @title_results).uniq + if all_pages_found.size == 1 + redirect_to_page(all_pages_found.first.name) + end + end + + # Within a single page -------------------------------------------------------- + + def cancel_edit + @page.unlock + redirect_to_page(@page_name) + end + + def edit + if @page.nil? + redirect_home + elsif @page.locked?(Time.now) and not @params['break_lock'] + redirect_to :web => @web_name, :action => 'locked', :id => @page_name + else + @page.lock(Time.now, @author) + end + end + + def locked + # to template + end + + def new + # to template + end + + def pdf + page = wiki.read_page(@web_name, @page_name) + safe_page_name = @page.name.gsub(/\W/, '') + file_name = "#{safe_page_name}-#{@web.address}-#{@page.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}" + file_path = File.join(@wiki.storage_path, file_name) + + export_page_to_tex("#{file_path}.tex") unless FileTest.exists?("#{file_path}.tex") + # NB: this is _very_ slow + convert_tex_to_pdf("#{file_path}.tex") + send_file "#{file_path}.pdf" + end + + def print + if @page.nil? + redirect_home + end + @link_mode ||= :show + @renderer = PageRenderer.new(@page.revisions.last) + # to template + end + + def published + if not @web.published? + render(:text => "Published version of web '#{@web_name}' is not available", :status => 404) + return + end + + @page_name ||= 'HomePage' + @page ||= wiki.read_page(@web_name, @page_name) + render(:text => "Page '#{@page_name}' not found", :status => 404) and return unless @page + + @renderer = PageRenderer.new(@page.revisions.last) + end + + def revision + get_page_and_revision + @show_diff = (@params[:mode] == 'diff') + @renderer = PageRenderer.new(@revision) + end + + def rollback + get_page_and_revision + end + + def save + render(:status => 404, :text => 'Undefined page name') and return if @page_name.nil? + + author_name = @params['author'] + author_name = 'AnonymousCoward' if author_name =~ /^\s*$/ + cookies['author'] = { :value => author_name, :expires => Time.utc(2030) } + + begin + filter_spam(@params['content']) + if @page + wiki.revise_page(@web_name, @page_name, @params['content'], Time.now, + Author.new(author_name, remote_ip), PageRenderer.new) + @page.unlock + else + wiki.write_page(@web_name, @page_name, @params['content'], Time.now, + Author.new(author_name, remote_ip), PageRenderer.new) + end + redirect_to_page @page_name + rescue => e + flash[:error] = e + logger.error e + flash[:content] = @params['content'] + if @page + @page.unlock + redirect_to :action => 'edit', :web => @web_name, :id => @page_name + else + redirect_to :action => 'new', :web => @web_name, :id => @page_name + end + end + end + + def show + if @page + begin + @renderer = PageRenderer.new(@page.revisions.last) + @show_diff = (@params[:mode] == 'diff') + render_action 'page' + # TODO this rescue should differentiate between errors due to rendering and errors in + # the application itself (for application errors, it's better not to rescue the error at all) + rescue => e + logger.error e + flash[:error] = e.message + if in_a_web? + redirect_to :action => 'edit', :web => @web_name, :id => @page_name + else + raise e + end + end + else + if not @page_name.nil? and not @page_name.empty? + redirect_to :web => @web_name, :action => 'new', :id => @page_name + else + render_text 'Page name is not specified', '404 Not Found' + end + end + end + + def tex + @tex_content = RedClothForTex.new(@page.content).to_tex + end + + protected + + def load_page + @page_name = @params['id'] + @page = @wiki.read_page(@web_name, @page_name) if @page_name + end + + private + + def convert_tex_to_pdf(tex_path) + # TODO remove earlier PDF files with the same prefix + # TODO handle gracefully situation where pdflatex is not available + begin + wd = Dir.getwd + Dir.chdir(File.dirname(tex_path)) + logger.info `pdflatex --interaction=nonstopmode #{File.basename(tex_path)}` + ensure + Dir.chdir(wd) + end + end + + def export_page_to_tex(file_path) + tex + File.open(file_path, 'w') { |f| f.write(render_to_string(:template => 'wiki/tex', :layout => false)) } + end + + def export_pages_as_zip(file_type, &block) + + file_prefix = "#{@web.address}-#{file_type}-" + timestamp = @web.revised_at.strftime('%Y-%m-%d-%H-%M-%S') + file_path = File.join(@wiki.storage_path, file_prefix + timestamp + '.zip') + tmp_path = "#{file_path}.tmp" + + Zip::ZipOutputStream.open(tmp_path) do |zip_out| + @web.select.by_name.each do |page| + zip_out.put_next_entry("#{CGI.escape(page.name)}.#{file_type}") + zip_out.puts(block.call(page)) + end + # add an index file, if exporting to HTML + if file_type.to_s.downcase == 'html' + zip_out.put_next_entry 'index.html' + zip_out.puts "" + + "" + end + end + FileUtils.rm_rf(Dir[File.join(@wiki.storage_path, file_prefix + '*.zip')]) + FileUtils.mv(tmp_path, file_path) + send_file file_path + end + + def export_web_to_tex(file_path) + @tex_content = table_of_contents(@web.page('HomePage').content, render_tex_web) + File.open(file_path, 'w') { |f| f.write(render_to_string(:template => 'wiki/tex_web', :layout => nil)) } + end + + def get_page_and_revision + if @params['rev'] + @revision_number = @params['rev'].to_i + else + @revision_number = @page.revisions.length + end + @revision = @page.revisions[@revision_number - 1] + end + + def parse_category + @categories = WikiReference.list_categories.sort + @category = @params['category'] + if @category + @set_name = "category '#{@category}'" + pages = WikiReference.pages_in_category(@category).sort.map { |page_name| @web.page(page_name) } + @pages_in_category = PageSet.new(@web, pages) + else + # no category specified, return all pages of the web + @pages_in_category = @web.select_all.by_name + @set_name = 'the web' + end + end + + def parse_rss_params + if @params.include? 'limit' + limit = @params['limit'].to_i rescue nil + limit = nil if limit == 0 + else + limit = 15 + end + start_date = Time.local(*ParseDate::parsedate(@params['start'])) rescue nil + end_date = Time.local(*ParseDate::parsedate(@params['end'])) rescue nil + [ limit, start_date, end_date ] + end + + def remote_ip + ip = @request.remote_ip + logger.info(ip) + ip + end + + def render_rss(hide_description = false, limit = 15, start_date = nil, end_date = nil) + if limit && !start_date && !end_date + @pages_by_revision = @web.select.by_revision.first(limit) + else + @pages_by_revision = @web.select.by_revision + @pages_by_revision.reject! { |page| page.revised_at < start_date } if start_date + @pages_by_revision.reject! { |page| page.revised_at > end_date } if end_date + end + + @hide_description = hide_description + @link_action = @web.password ? 'published' : 'show' + + render :action => 'rss_feed' + end + + def render_tex_web + @web.select.by_name.inject({}) do |tex_web, page| + tex_web[page.name] = RedClothForTex.new(page.content).to_tex + tex_web + end + end + + def rss_with_content_allowed? + @web.password.nil? or @web.published? + end + + def truncate(text, length = 30, truncate_string = '...') + if text.length > length then text[0..(length - 3)] + truncate_string else text end + end + + def filter_spam(content) + @@spam_patterns ||= load_spam_patterns + @@spam_patterns.each do |pattern| + raise "Your edit was blocked by spam filtering" if content =~ pattern + end + end + + def load_spam_patterns + spam_patterns_file = "#{RAILS_ROOT}/config/spam_patterns.txt" + if File.exists?(spam_patterns_file) + File.readlines(spam_patterns_file).inject([]) { |patterns, line| patterns << Regexp.new(line.chomp, Regexp::IGNORECASE) } + else + [] + end + end + + +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 00000000..67d7ae86 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,93 @@ +# The methods added to this helper will be available to all templates in the application. +module ApplicationHelper + + # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container + # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and + # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values + # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. + # + # Examples (call, result): + # html_options([["Dollar", "$"], ["Kroner", "DKK"]]) + # \n + # + # html_options([ "VISA", "Mastercard" ], "Mastercard") + # \n + # + # html_options({ "Basic" => "$20", "Plus" => "$40" }, "$40") + # \n + def html_options(container, selected = nil) + container = container.to_a if Hash === container + + html_options = container.inject([]) do |options, element| + if element.is_a? Array + if element.last != selected + options << "" + else + options << "" + end + else + options << ((element != selected) ? "" : "") + end + end + + html_options.join("\n") + end + + # Creates a hyperlink to a Wiki page, without checking if the page exists or not + def link_to_existing_page(page, text = nil, html_options = {}) + link_to( + text || page.plain_name, + {:web => @web.address, :action => 'show', :id => page.name, :only_path => true}, + html_options) + end + + # Creates a hyperlink to a Wiki page, or to a "new page" form if the page doesn't exist yet + def link_to_page(page_name, web = @web, text = nil, options = {}) + raise 'Web not defined' if web.nil? + UrlGenerator.new(@controller).make_link(page_name, web, text, + options.merge(:base_url => "#{base_url}/#{web.address}")) + end + + def author_link(page, options = {}) + UrlGenerator.new(@controller).make_link(page.author.name, page.web, nil, options) + end + + def base_url + home_page_url = url_for :controller => 'admin', :action => 'create_system', :only_path => true + home_page_url.sub(%r-/create_system/?$-, '') + end + + # Creates a menu of categories + def categories_menu + if @categories.empty? + '' + else + "
    \n" + + 'Categories:' + + '[' + link_to_unless_current('Any', :web => @web.address, :action => @action_name) + "]\n" + + @categories.map { |c| + link_to_unless_current(c, :web => @web.address, :action => @action_name, :category => c) + }.join(', ') + "\n" + + '
    ' + end + end + + # Performs HTML escaping on text, but keeps linefeeds intact (by replacing them with
    ) + def escape_preserving_linefeeds(text) + h(text).gsub(/\n/, '
    ') + end + + def format_date(date, include_time = true) + # Must use DateTime because Time doesn't support %e on at least some platforms + if include_time + DateTime.new(date.year, date.mon, date.day, date.hour, date.min, date.sec).strftime("%B %e, %Y %H:%M:%S") + else + DateTime.new(date.year, date.mon, date.day).strftime("%B %e, %Y") + end + end + + def rendered_content(page) + PageRenderer.new(page.revisions.last).display_content + end + +end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb new file mode 100644 index 00000000..a5b97f29 --- /dev/null +++ b/app/helpers/wiki_helper.rb @@ -0,0 +1,89 @@ +module WikiHelper + + def navigation_menu_for_revision + menu = [] + menu << forward + menu << back_for_revision if @revision_number > 1 + menu << current_revision + menu << see_or_hide_changes_for_revision if @revision_number > 1 + menu << rollback + menu + end + + def navigation_menu_for_page + menu = [] + menu << edit_page + menu << edit_web if @page.name == "HomePage" + if @page.revisions.length > 1 + menu << back_for_page + menu << see_or_hide_changes_for_page + end + menu + end + + def edit_page + link_text = (@page.name == "HomePage" ? 'Edit Page' : 'Edit') + link_to(link_text, {:web => @web.address, :action => 'edit', :id => @page.name}, + {:class => 'navlink', :accesskey => 'E', :name => 'edit'}) + end + + def edit_web + link_to('Edit Web', {:web => @web.address, :action => 'edit_web'}, + {:class => 'navlink', :accesskey => 'W', :name => 'edit_web'}) + end + + def forward + if @revision_number < @page.revisions.length - 1 + link_to('Forward in time', + {:web => @web.address, :action => 'revision', :id => @page.name, :rev => @revision_number + 1}, + {:class => 'navlink', :accesskey => 'F', :name => 'to_next_revision'}) + + " (#{@revision.page.revisions.length - @revision_number} more) " + else + link_to('Forward in time', {:web => @web.address, :action => 'show', :id => @page.name}, + {:class => 'navlink', :accesskey => 'F', :name => 'to_next_revision'}) + + " (to current)" + end + end + + def back_for_revision + link_to('Back in time', + {:web => @web.address, :action => 'revision', :id => @page.name, :rev => @revision_number - 1}, + {:class => 'navlink', :name => 'to_previous_revision'}) + + " (#{@revision_number - 1} more)" + end + + def back_for_page + link_to('Back in time', + {:web => @web.address, :action => 'revision', :id => @page.name, + :rev => @page.revisions.length - 1}, + {:class => 'navlink', :accesskey => 'B', :name => 'to_previous_revision'}) + + " (#{@page.revisions.length - 1} #{@page.revisions.length - 1 == 1 ? 'revision' : 'revisions'})" + end + + def current_revision + link_to('See current', {:web => @web.address, :action => 'show', :id => @page.name}, + {:class => 'navlink', :name => 'to_current_revision'}) + end + + def see_or_hide_changes_for_revision + link_to(@show_diff ? 'Hide changes' : 'See changes', + {:web => @web.address, :action => 'revision', :id => @page.name, :rev => @revision_number, + :mode => (@show_diff ? nil : 'diff') }, + {:class => 'navlink', :accesskey => 'C', :name => 'see_changes'}) + end + + def see_or_hide_changes_for_page + link_to(@show_diff ? 'Hide changes' : 'See changes', + {:web => @web.address, :action => 'show', :id => @page.name, :mode => (@show_diff ? nil : 'diff') }, + {:class => 'navlink', :accesskey => 'C', :name => 'see_changes'}) + end + + def rollback + link_to('Rollback', + {:web => @web.address, :action => 'rollback', :id => @page.name, :rev => @revision_number}, + {:class => 'navlink', :name => 'rollback'}) + end + + + +end \ No newline at end of file diff --git a/app/models/author.rb b/app/models/author.rb new file mode 100644 index 00000000..be8a5cf7 --- /dev/null +++ b/app/models/author.rb @@ -0,0 +1,18 @@ +class Author < String + attr_accessor :ip + attr_reader :name + def initialize(name, ip = nil) + @ip = ip + super(name) + end + + def name=(value) + self.gsub!(/.+/, value) + end + + alias_method :name, :to_s + + def <=>(other) + name <=> other.to_s + end +end \ No newline at end of file diff --git a/app/models/page.rb b/app/models/page.rb new file mode 100644 index 00000000..c5f48d43 --- /dev/null +++ b/app/models/page.rb @@ -0,0 +1,121 @@ +class Page < ActiveRecord::Base + belongs_to :web + has_many :revisions, :order => 'id' + has_many :wiki_references, :order => 'referenced_name' + has_one :current_revision, :class_name => 'Revision', :order => 'id DESC' + + def revise(content, time, author, renderer) + revisions_size = new_record? ? 0 : revisions.size + if (revisions_size > 0) and content == current_revision.content + raise Instiki::ValidationError.new( + "You have tried to save page '#{name}' without changing its content") + end + + author = Author.new(author.to_s) unless author.is_a?(Author) + + # Try to render content to make sure that markup engine can take it, + renderer.revision = Revision.new( + :page => self, :content => content, :author => author, :revised_at => time) + renderer.display_content(update_references = true) + + # A user may change a page, look at it and make some more changes - several times. + # Not to record every such iteration as a new revision, if the previous revision was done + # by the same author, not more than 30 minutes ago, then update the last revision instead of + # creating a new one + if (revisions_size > 0) && continous_revision?(time, author) + current_revision.update_attributes(:content => content, :revised_at => time) + else + revisions.create(:content => content, :author => author, :revised_at => time) + end + save + self + end + + def rollback(revision_number, time, author_ip, renderer) + roll_back_revision = self.revisions[revision_number] + if roll_back_revision.nil? + raise Instiki::ValidationError.new("Revision #{revision_number} not found") + end + author = Author.new(roll_back_revision.author.name, author_ip) + revise(roll_back_revision.content, time, author, renderer) + end + + def revisions? + revisions.size > 1 + end + + def previous_revision(revision) + revision_index = revisions.each_with_index do |rev, index| + if rev.id == revision.id + break index + else + nil + end + end + if revision_index.nil? or revision_index == 0 + nil + else + revisions[revision_index - 1] + end + end + + def references + web.select.pages_that_reference(name) + end + + def wiki_words + wiki_references.select { |ref| ref.wiki_word? }.map { |ref| ref.referenced_name } + end + + def linked_from + web.select.pages_that_link_to(name) + end + + def included_from + web.select.pages_that_include(name) + end + + # Returns the original wiki-word name as separate words, so "MyPage" becomes "My Page". + def plain_name + web.brackets_only? ? name : WikiWords.separate(name) + end + + LOCKING_PERIOD = 30.minutes + + def lock(time, locked_by) + update_attributes(:locked_at => time, :locked_by => locked_by) + end + + def lock_duration(time) + ((time - locked_at) / 60).to_i unless locked_at.nil? + end + + def unlock + update_attribute(:locked_at, nil) + end + + def locked?(comparison_time) + locked_at + LOCKING_PERIOD > comparison_time unless locked_at.nil? + end + + def to_param + name + end + + private + + def continous_revision?(time, author) + (current_revision.author == author) && (revised_at + 30.minutes > time) + end + + # Forward method calls to the current revision, so the page responds to all revision calls + def method_missing(method_id, *args, &block) + method_name = method_id.to_s + # Perform a hand-off to AR::Base#method_missing + if @attributes.include?(method_name) or md = /(=|\?|_before_type_cast)$/.match(method_name) + super(method_id, *args, &block) + else + current_revision.send(method_id) + end + end +end diff --git a/app/models/page_observer.rb b/app/models/page_observer.rb new file mode 100644 index 00000000..aab72fa8 --- /dev/null +++ b/app/models/page_observer.rb @@ -0,0 +1,15 @@ +# This class maintains the state of wiki references for newly created or newly deleted pages +class PageObserver < ActiveRecord::Observer + + def after_create(page) + WikiReference.update_all("link_type = '#{WikiReference::LINKED_PAGE}'", + ['referenced_name = ?', page.name]) + end + + def before_destroy(page) + WikiReference.delete_all ['page_id = ?', page.id] + WikiReference.update_all("link_type = '#{WikiReference::WANTED_PAGE}'", + ['referenced_name = ?', page.name]) + end + +end \ No newline at end of file diff --git a/app/models/page_set.rb b/app/models/page_set.rb new file mode 100644 index 00000000..4ac08c00 --- /dev/null +++ b/app/models/page_set.rb @@ -0,0 +1,92 @@ +# Container for a set of pages with methods for manipulation. + +class PageSet < Array + attr_reader :web + + def initialize(web, pages = nil, condition = nil) + @web = web + # if pages is not specified, make a list of all pages in the web + if pages.nil? + super(web.pages) + # otherwise use specified pages and condition to produce a set of pages + elsif condition.nil? + super(pages) + else + super(pages.select { |page| condition[page] }) + end + end + + def most_recent_revision + self.map { |page| page.revised_at }.max || Time.at(0) + end + + def by_name + PageSet.new(@web, sort_by { |page| page.name }) + end + + alias :sort :by_name + + def by_revision + PageSet.new(@web, sort_by { |page| page.revised_at }).reverse + end + + def pages_that_reference(page_name) + all_referring_pages = WikiReference.pages_that_reference(page_name) + self.select { |page| all_referring_pages.include?(page.name) } + end + + def pages_that_link_to(page_name) + all_linking_pages = WikiReference.pages_that_link_to(page_name) + self.select { |page| all_linking_pages.include?(page.name) } + end + + def pages_that_include(page_name) + all_including_pages = WikiReference.pages_that_include(page_name) + self.select { |page| all_including_pages.include?(page.name) } + end + + def pages_authored_by(author) + all_pages_authored_by_the_author = + Page.connection.select_all(sanitize_sql([ + "SELECT page_id FROM revision WHERE author = '?'", author])) + self.select { |page| page.authors.include?(author) } + end + + def characters + self.inject(0) { |chars,page| chars += page.content.size } + end + + # Returns all the orphaned pages in this page set. That is, + # pages in this set for which there is no reference in the web. + # The HomePage and author pages are always assumed to have + # references and so cannot be orphans + # Pages that refer to themselves and have no links from outside are oprphans. + def orphaned_pages + never_orphans = web.authors + ['HomePage'] + self.select { |page| + if never_orphans.include? page.name + false + else + references = pages_that_reference(page.name) + references.empty? or references == [page] + end + } + end + + # Returns all the wiki words in this page set for which + # there are no pages in this page set's web + def wanted_pages + wiki_words - web.select.names + end + + def names + self.map { |page| page.name } + end + + def wiki_words + self.inject([]) { |wiki_words, page| + wiki_words + page.wiki_words + }.flatten.uniq.sort + end + +end diff --git a/app/models/revision.rb b/app/models/revision.rb new file mode 100644 index 00000000..3af2384d --- /dev/null +++ b/app/models/revision.rb @@ -0,0 +1,4 @@ +class Revision < ActiveRecord::Base + belongs_to :page + composed_of :author, :mapping => [ %w(author name), %w(ip ip) ] +end diff --git a/app/models/system.rb b/app/models/system.rb new file mode 100644 index 00000000..7ac1ad08 --- /dev/null +++ b/app/models/system.rb @@ -0,0 +1,4 @@ +class System < ActiveRecord::Base + set_table_name 'system' + validates_presence_of :password +end \ No newline at end of file diff --git a/app/models/web.rb b/app/models/web.rb new file mode 100644 index 00000000..d1d50268 --- /dev/null +++ b/app/models/web.rb @@ -0,0 +1,147 @@ +class Web < ActiveRecord::Base + has_many :pages + has_many :wiki_files + + def wiki + Wiki.new + end + + def settings_changed?(markup, safe_mode, brackets_only) + self.markup != markup || + self.safe_mode != safe_mode || + self.brackets_only != brackets_only + end + + def add_page(name, content, time, author, renderer) + page = page(name) || Page.new(:web => self, :name => name) + page.revise(content, time, author, renderer) + end + + def authors + connection.select_all( + 'SELECT DISTINCT r.author AS author ' + + 'FROM revisions r ' + + 'JOIN pages p ON p.id = r.page_id ' + + 'WHERE p.web_id = ' + self.id.to_s + + 'ORDER by 1 ' + ).collect { |row| row['author'] } + end + + def categories + select.map { |page| page.categories }.flatten.uniq.sort + end + + def page(name) + pages.find(:first, :conditions => ['name = ?', name]) + end + + def last_page + return Page.find(:first, :order => 'id desc', :conditions => ['web_id = ?', self.id]) + end + + def has_page?(name) + Page.count(['web_id = ? AND name = ?', id, name]) > 0 + end + + def has_file?(file_name) + WikiFile.find_by_file_name(file_name) != nil + end + + def markup + read_attribute('markup').to_sym + end + + def page_names_by_author + connection.select_all( + 'SELECT DISTINCT r.author AS author, p.name AS page_name ' + + 'FROM revisions r ' + + 'JOIN pages p ON r.page_id = p.id ' + + "WHERE p.web_id = #{self.id} " + + 'ORDER by p.name' + ).inject({}) { |result, row| + author, page_name = row['author'], row['page_name'] + result[author] = [] unless result.has_key?(author) + result[author] << page_name + result + } + end + + def remove_pages(pages_to_be_removed) + pages_to_be_removed.each { |p| p.destroy } + end + + def revised_at + select.most_recent_revision + end + + def select(&condition) + PageSet.new(self, pages, condition) + end + + def select_all + PageSet.new(self, pages, nil) + end + + def to_param + address + end + + def create_files_directory + return unless allow_uploads == 1 + dummy_file = self.wiki_files.build(:file_name => '0', :description => '0', :content => '0') + dir = File.dirname(dummy_file.content_path) + begin + require 'fileutils' + FileUtils.mkdir_p dir + dummy_file.save + dummy_file.destroy + rescue => e + logger.error("Failed create files directory for #{self.address}: #{e}") + raise "Instiki could not create directory to store uploaded files. " + + "Please make sure that Instiki is allowed to create directory " + + "#{File.expand_path(dir)} and add files to it." + end + end + + private + + # Returns an array of all the wiki words in any current revision + def wiki_words + pages.inject([]) { |wiki_words, page| wiki_words << page.wiki_words }.flatten.uniq + end + + # Returns an array of all the page names on this web + def page_names + pages.map { |p| p.name } + end + + protected + before_save :sanitize_markup + after_save :create_files_directory + before_validation :validate_address + validates_uniqueness_of :address + validates_length_of :color, :in => 3..6 + + def sanitize_markup + self.markup = markup.to_s + end + + def validate_address + unless address == CGI.escape(address) + self.errors.add(:address, 'should contain only valid URI characters') + raise Instiki::ValidationError.new("#{self.class.human_attribute_name('address')} #{errors.on(:address)}") + end + end + + def default_web? + defined? DEFAULT_WEB and self.address == DEFAULT_WEB + end + + def files_path + if default_web? + "#{RAILS_ROOT}/public/files" + else + "#{RAILS_ROOT}/public/#{self.address}/files" + end + end +end diff --git a/app/models/wiki.rb b/app/models/wiki.rb new file mode 100644 index 00000000..46073065 --- /dev/null +++ b/app/models/wiki.rb @@ -0,0 +1,92 @@ +class Wiki + + cattr_accessor :storage_path, :logger + self.storage_path = "#{RAILS_ROOT}/storage/" + self.logger = RAILS_DEFAULT_LOGGER + + def authenticate(password) + password == (system.password || 'instiki') + end + + def create_web(name, address, password = nil) + @webs = nil + Web.create(:name => name, :address => address, :password => password) + end + + def delete_web(address) + web = Web.find_by_address(address) + unless web.nil? + web.destroy + @webs = nil + end + end + + def edit_web(old_address, new_address, name, markup, color, additional_style, safe_mode = false, + password = nil, published = false, brackets_only = false, count_pages = false, + allow_uploads = true, max_upload_size = nil) + + if not (web = Web.find_by_address(old_address)) + raise Instiki::ValidationError.new("Web with address '#{old_address}' does not exist") + end + + web.update_attributes(:address => new_address, :name => name, :markup => markup, :color => color, + :additional_style => additional_style, :safe_mode => safe_mode, :password => password, :published => published, + :brackets_only => brackets_only, :count_pages => count_pages, :allow_uploads => allow_uploads, :max_upload_size => max_upload_size) + @webs = nil + raise Instiki::ValidationError.new("There is already a web with address '#{new_address}'") unless web.errors.on(:address).nil? + web + end + + def read_page(web_address, page_name) + self.class.logger.debug "Reading page '#{page_name}' from web '#{web_address}'" + web = Web.find_by_address(web_address) + if web.nil? + self.class.logger.debug "Web '#{web_address}' not found" + return nil + else + page = web.pages.find(:first, :conditions => ['name = ?', page_name]) + self.class.logger.debug "Page '#{page_name}' #{page.nil? ? 'not' : ''} found" + return page + end + end + + def remove_orphaned_pages(web_address) + web = Web.find_by_address(web_address) + web.remove_pages(web.select.orphaned_pages) + end + + def revise_page(web_address, page_name, content, revised_at, author, renderer) + page = read_page(web_address, page_name) + page.revise(content, revised_at, author, renderer) + end + + def rollback_page(web_address, page_name, revision_number, time, author_id = nil) + page = read_page(web_address, page_name) + page.rollback(revision_number, time, author_id) + end + + def setup(password, web_name, web_address) + system.update_attribute(:password, password) + create_web(web_name, web_address) + end + + def system + @system ||= (System.find(:first) || System.create) + end + + def setup? + Web.count > 0 + end + + def webs + @webs ||= Web.find(:all).inject({}) { |webs, web| webs.merge(web.address => web) } + end + + def storage_path + self.class.storage_path + end + + def write_page(web_address, page_name, content, written_on, author, renderer) + Web.find_by_address(web_address).add_page(page_name, content, written_on, author, renderer) + end +end \ No newline at end of file diff --git a/app/models/wiki_file.rb b/app/models/wiki_file.rb new file mode 100644 index 00000000..ba122662 --- /dev/null +++ b/app/models/wiki_file.rb @@ -0,0 +1,64 @@ +class WikiFile < ActiveRecord::Base + belongs_to :web + + before_save :write_content_to_file + before_destroy :delete_content_file + + validates_presence_of %w( web file_name ) + validates_length_of :file_name, :within=>1..50 + validates_length_of :description, :maximum=>255 + + def self.find_by_file_name(file_name) + find(:first, :conditions => ['file_name = ?', file_name]) + end + + SANE_FILE_NAME = /^[a-zA-Z0-9\-_\. ]*$/ + def validate + if file_name + if file_name !~ SANE_FILE_NAME + errors.add("file_name", "is invalid. Only latin characters, digits, dots, underscores, " + + "dashes and spaces are accepted") + elsif file_name == '.' or file_name == '..' + errors.add("file_name", "cannot be '.' or '..'") + end + end + + if @web and @content + if (@content.size > @web.max_upload_size.kilobytes) + errors.add("content", "size (#{(@content.size / 1024.0).round} kilobytes) exceeds " + + "the maximum (#{web.max_upload_size} kilobytes) set for this wiki") + end + end + + errors.add("content", "is empty") if @content.nil? or @content.empty? + end + + def content=(content) + if content.respond_to? :read + @content = content.read + else + @content = content + end + end + + def content + @content ||= ( File.open(content_path, 'rb') { |f| f.read } ) + end + + def content_path + web.files_path + '/' + file_name + end + + def write_content_to_file + web.create_files_directory unless File.exists?(web.files_path) + File.open(self.content_path, 'wb') { |f| f.write(@content) } + end + + def delete_content_file + require 'fileutils' + FileUtils.rm_f(content_path) if File.exists?(content_path) + end + + + +end diff --git a/app/models/wiki_reference.rb b/app/models/wiki_reference.rb new file mode 100644 index 00000000..c326e8ad --- /dev/null +++ b/app/models/wiki_reference.rb @@ -0,0 +1,82 @@ +class WikiReference < ActiveRecord::Base + + LINKED_PAGE = 'L' + WANTED_PAGE = 'W' + INCLUDED_PAGE = 'I' + CATEGORY = 'C' + AUTHOR = 'A' + FILE = 'F' + WANTED_FILE = 'E' + + belongs_to :page + validates_inclusion_of :link_type, :in => [LINKED_PAGE, WANTED_PAGE, INCLUDED_PAGE, CATEGORY, AUTHOR, FILE, WANTED_FILE] + + # FIXME all finders below MUST restrict their results to pages belonging to a particular web + + def self.link_type(web, page_name) + web.has_page?(page_name) ? LINKED_PAGE : WANTED_PAGE + end + + def self.pages_that_reference(page_name) + query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' + + 'WHERE wiki_references.referenced_name = ?' + + "AND wiki_references.link_type in ('#{LINKED_PAGE}', '#{WANTED_PAGE}', '#{INCLUDED_PAGE}')" + names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] } + end + + def self.pages_that_link_to(page_name) + query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' + + 'WHERE wiki_references.referenced_name = ? ' + + "AND wiki_references.link_type in ('#{LINKED_PAGE}', '#{WANTED_PAGE}')" + names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] } + end + + def self.pages_that_include(page_name) + query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' + + 'WHERE wiki_references.referenced_name = ? ' + + "AND wiki_references.link_type = '#{INCLUDED_PAGE}'" + names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] } + end + + def self.pages_in_category(category) + query = + 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' + + 'WHERE wiki_references.referenced_name = ? ' + + "AND wiki_references.link_type = '#{CATEGORY}'" + names = connection.select_all(sanitize_sql([query, category])).map { |row| row['name'] } + end + + def self.list_categories + query = "SELECT DISTINCT referenced_name FROM wiki_references WHERE link_type = '#{CATEGORY}'" + connection.select_all(query).map { |row| row['referenced_name'] } + end + + def wiki_word? + linked_page? or wanted_page? + end + + def wiki_link? + linked_page? or wanted_page? or file? or wanted_file? + end + + def linked_page? + link_type == LINKED_PAGE + end + + def wanted_page? + link_type == WANTED_PAGE + end + + def included_page? + link_type == INCLUDED_PAGE + end + + def file? + link_type == FILE + end + + def wanted_file? + link_type == WANTED_FILE + end + +end diff --git a/app/views/admin/create_system.rhtml b/app/views/admin/create_system.rhtml new file mode 100644 index 00000000..f5d5ff7f --- /dev/null +++ b/app/views/admin/create_system.rhtml @@ -0,0 +1,86 @@ +<% @title = "Instiki Setup"; @content_width = 500 %> + +

    + Congratulations on succesfully installing and starting Instiki. + Since this is the first time Instiki has been run on this port, + you'll need to do a brief one-time setup. +

    + +<%= form_tag({ :controller => 'admin', :action => 'create_system' }, + { 'id' => 'setup', 'method' => 'post', 'onSubmit' => 'return validateSetup()', + 'accept-charset' => 'utf-8' }) +%> +
      +
    1. + +

      Name and address for your first web

      +
      + The name of the web is included in the title on all pages. + The address is the base path that all pages within the web live beneath. + Ex: the address "rails" gives URLs like /rails/show/HomePage. + The address can only consist of letters and digits. +
      +
      + Name: +    + Address: +
      +
    2. + +
    3. +

      Password for creating and changing webs

      +
      + Administrative access allows you to make new webs and change existing ones. +
      +
      Everyone with this password will be able to do this, so pick it carefully!
      +
      + Password: +    + Verify: +
      +
    4. +
    + +

    + +

    +<%= end_form_tag %> + + diff --git a/app/views/admin/create_web.rhtml b/app/views/admin/create_web.rhtml new file mode 100644 index 00000000..5b9e3f7e --- /dev/null +++ b/app/views/admin/create_web.rhtml @@ -0,0 +1,72 @@ +<% @title = "New Wiki Web"; @content_width = 500 %> + +

    + Each web serves as an isolated name space for wiki pages, + so different subjects or projects can write about different MuppetShows. +

    + +<%= form_tag({ :controller => 'admin', :action => 'create_web' }, + { 'id' => 'setup', 'method' => 'post', + 'onSubmit' => 'cleanAddress(); return validateSetup()', + 'accept-charset' => 'utf-8' }) +%> + +
      +
    1. +

      Name and address for your new web

      +
      + The name of the web is included in the title on all pages. + The address is the base path that all pages within the web live beneath. + Ex: the address "rails" gives URLs like /rails/show/HomePage. + The address can only consist of letters and digits. +
      +
      + Name: +    + Address: +
      +
    2. +
    + + +

    + + Enter system password + + and + + +

    + +<%= end_form_tag %> + + diff --git a/app/views/admin/edit_web.rhtml b/app/views/admin/edit_web.rhtml new file mode 100644 index 00000000..3062892f --- /dev/null +++ b/app/views/admin/edit_web.rhtml @@ -0,0 +1,136 @@ +<% @title = "Edit Web" %> + +<%= form_tag({ :controller => 'admin', :action => 'edit_web', :web => @web.address }, + { 'id' => 'setup', 'method' => 'post', + 'onSubmit' => 'cleanAddress(); return validateSetup()', + 'accept-charset' => 'utf-8' }) +%> + +

    Name and address

    +
    + The name of the web is included in the title on all pages. + The address is the base path that all pages within the web live beneath. + Ex: the address "rails" gives URLs like /rails/show/HomePage. +
    + +
    + Name:    + Address: + (Letters and digits only) +
    + +

    Specialize

    +
    + Markup: + + +    + + Color: + +
    +

    + + /> + Safe mode + - strip HTML tags and stylesheet options from the content of all pages +
    + /> + Brackets only + - require all wiki words to be as [[wiki word]], WikiWord links won't be created +
    + /> + Count pages +
    + + /> + Allow uploads of no more than + + kbytes + - + allow users to upload pictures and other files and include them on wiki pages + +
    +
    +

    + + + Stylesheet tweaks >> + + - add or change styles used by this web; styles defined here take precedence over + instiki.css. Hint: View HTML source of a page you want to style to find ID names on individual + tags. +
    + +
    + +

    Password protection for this web (<%= @web.name %>)

    +
    + This is the password that visitors need to view and edit this web. + Setting the password to nothing will remove the password protection. +
    +
    + Password: +    + Verify: +
    + +

    Publish read-only version of this web (<%= @web.name %>)

    +
    + You can turn on a read-only version of this web that's accessible even when the regular web + is password protected. + The published version is accessible through URLs like /wiki/published/HomePage. +
    +
    + /> + Publish this web +
    + +

    + + Enter system password + + and + +

    + ...or forget changes and <%= link_to 'create a new web', :action => 'create_web' %> +
    +

    + +<%= end_form_tag %> + +
    +

    Other administrative tasks

    + +<%= form_tag({:controller => 'admin', :web => @web.address, :action => 'remove_orphaned_pages'}, + { :id => 'remove_orphaned_pages', + :onSubmit => "return checkSystemPassword(document.getElementById('system_password_orphaned').value)", + 'accept-charset' => 'utf-8' }) +%> +

    + + Clean up by entering system password + + and + + +

    +<%= end_form_tag %> + +<%= javascript_include_tag 'edit_web' %> diff --git a/app/views/file/file.rhtml b/app/views/file/file.rhtml new file mode 100644 index 00000000..9f67b9b8 --- /dev/null +++ b/app/views/file/file.rhtml @@ -0,0 +1,33 @@ +<% + @title = "Upload #{h @file_name}" + @hide_navigation = false +%> + +<%= error_messages_for 'file' %> + +<%= form_tag({ :controller => 'file', :web => @web_name, :action => 'file' }, + { 'multipart' => true , 'accept-charset' => 'utf-8' }) %> + <%= hidden_field 'file', 'file_name' %> +
    + Content of <%= h @file_name %> to upload (required): +
    + +
    + + Please note that the file you are uploadng will be named <%= h @file_name %> on the wiki - + regardless of how it is named on your computer. To change the wiki name of the file, please go + <%= link_to :back %> and edit the wiki page that refers to the file. + +
    +
    + Description (optional): +
    + <%= text_field "file", "description", "size" => 40 %> +
    +
    + as + <%= text_field_tag :author, @author, + :onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;", + :onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %> +
    +<%= end_form_tag %> \ No newline at end of file diff --git a/app/views/file/import.rhtml b/app/views/file/import.rhtml new file mode 100644 index 00000000..910ccef4 --- /dev/null +++ b/app/views/file/import.rhtml @@ -0,0 +1,23 @@ +

    +<%= form_tag({}, { 'multipart' => true, 'accept-charset' => 'utf-8' }) %> +

    + File to upload: +
    + +

    +

    + System password: +
    + +

    +

    + as + + <% if @page %> + | <%= link_to 'Cancel', :web => @web.address, :action => 'file'%> (unlocks page) + <% end %> + +

    +<%= end_form_tag %> +

    \ No newline at end of file diff --git a/app/views/layouts/default.rhtml b/app/views/layouts/default.rhtml new file mode 100644 index 00000000..67b85e06 --- /dev/null +++ b/app/views/layouts/default.rhtml @@ -0,0 +1,79 @@ + + + + + <% if @page and (@page.name == 'HomePage') and (%w( show published print ).include?(@action_name)) %> + <%= h @web.name %> + <% elsif @web %> + <%= @title %> in <%= h @web.name %> + <% else %> + <%= @title %> + <% end %> + <%= @show_diff ? ' (changes)' : '' %> + + + + + + + + <%= stylesheet_link_tag 'instiki' unless @inline_style %> + + + + <% if @web %> + <%= auto_discovery_link_tag(:rss, :controller => 'wiki', :web => @web.address, :action => 'rss_with_headlines') %> + <%= auto_discovery_link_tag(:rss, :controller => 'wiki', :web => @web.address, :action => 'rss_with_content') %> + <% end %> + + + + +
    +
    +

    + <% if @page and (@page.name == 'HomePage') and %w( show published print ).include?(@action_name) %> + <%= h(@web.name) + (@show_diff ? ' (changes)' : '') %> + <% elsif @web %> + <%= @web.name %>
    + <%= @title %> + <% else %> + <%= @title %> + <% end %> +

    + +<%= render 'navigation' unless @web.nil? || @hide_navigation %> + +<% if @flash[:info] %> +
    <%= escape_preserving_linefeeds @flash[:info] %>
    +<% end %> + +<% if @error or @flash[:error] %> +
    <%= escape_preserving_linefeeds(@error || @flash[:error]) %>
    +<% end %> + +<%= @content_for_layout %> + +<% if @show_footer %> + +<% end %> + +
    + +
    + + + diff --git a/app/views/markdown_help.rhtml b/app/views/markdown_help.rhtml new file mode 100644 index 00000000..067be08d --- /dev/null +++ b/app/views/markdown_help.rhtml @@ -0,0 +1,12 @@ +

    Markdown formatting tips (advanced)

    + + + + + + + + + + +
    _your text_your text
    **your text**your text
    `my code`my code
    * Bulleted list
    * Second item
    • Bulleted list
    • Second item
    1. Numbered list
    1. Second item
    1. Numbered list
    2. Second item
    [link name](URL)link name
    ***Horizontal ruler
    <http://url>
    <email@add.com>
    Auto-linked
    ![Alt text](URL)Image
    diff --git a/app/views/mixed_help.rhtml b/app/views/mixed_help.rhtml new file mode 100644 index 00000000..58503f54 --- /dev/null +++ b/app/views/mixed_help.rhtml @@ -0,0 +1,7 @@ +<%= render 'textile_help' %> + +

    Markdown

    +

    + In addition to Textile, this wiki also understands + Markdown. +

    \ No newline at end of file diff --git a/app/views/navigation.rhtml b/app/views/navigation.rhtml new file mode 100644 index 00000000..5735b472 --- /dev/null +++ b/app/views/navigation.rhtml @@ -0,0 +1,28 @@ +<% +def list_item(text, link_options, description, accesskey = nil) + link_options[:controller] = 'wiki' + link_options[:web] = @web.address + link_to_unless_current(text, link_options, :title => description, :accesskey => accesskey) { + content_tag('b', text, 'title' => description, 'class' => 'navOn') + } +end +%> + + \ No newline at end of file diff --git a/app/views/rdoc_help.rhtml b/app/views/rdoc_help.rhtml new file mode 100644 index 00000000..1afaff5c --- /dev/null +++ b/app/views/rdoc_help.rhtml @@ -0,0 +1,12 @@ +

    RDoc formatting tips (advanced)

    + + + + + + + + + + +
    _your text_your text
    *your text*your text
    * Bulleted list
    * Second item
    • Bulleted list
    • Second item
    1. Numbered list
    2. Second item
    1. Numbered list
    2. Second item
    +my_code+my_code
    ---Horizontal ruler
    [[URL linkname]]linkname
    http://url
    mailto:e@add.com
    Auto-linked
    imageURLImage
    diff --git a/app/views/textile_help.rhtml b/app/views/textile_help.rhtml new file mode 100644 index 00000000..3d8400b3 --- /dev/null +++ b/app/views/textile_help.rhtml @@ -0,0 +1,24 @@ +

    Textile formatting tips (advanced)

    + + + + + + + + + + +
    _your text_your text
    *your text*your text
    %{color:red}hello%hello
    * Bulleted list
    * Second item
    • Bulleted list
    • Second item
    # Numbered list
    # Second item
    1. Numbered list
    2. Second item
    "linkname":URLlinkname
    |a|table|row|
    |b|table|row|
    Table
    http://url
    email@address.com
    Auto-linked
    !imageURL!Image
    + + diff --git a/app/views/wiki/_inbound_links.rhtml b/app/views/wiki/_inbound_links.rhtml new file mode 100644 index 00000000..4c5e4e12 --- /dev/null +++ b/app/views/wiki/_inbound_links.rhtml @@ -0,0 +1,13 @@ +<% unless @page.linked_from.empty? %> + + | Linked from: + <%= @page.linked_from.collect { |referring_page| link_to_existing_page referring_page }.join(", ") %> + +<% end %> + +<% unless @page.included_from.empty? %> + + | Included from: + <%= @page.included_from.collect { |referring_page| link_to_existing_page referring_page }.join(", ") %> + +<% end %> diff --git a/app/views/wiki/authors.rhtml b/app/views/wiki/authors.rhtml new file mode 100644 index 00000000..57f529ee --- /dev/null +++ b/app/views/wiki/authors.rhtml @@ -0,0 +1,11 @@ +<% @title = 'Authors' %> + +
      + <% for author in @authors %> +
    • + <%= link_to_page author %> + co- or authored: + <%= @page_names_by_author[author].collect { |page_name| link_to_page(page_name) }.sort.join ', ' %> +
    • + <% end %> +
    diff --git a/app/views/wiki/edit.rhtml b/app/views/wiki/edit.rhtml new file mode 100644 index 00000000..ad7df15d --- /dev/null +++ b/app/views/wiki/edit.rhtml @@ -0,0 +1,40 @@ +<% + @title = "Editing #{@page.name}" + @content_width = 720 + @hide_navigation = true +%> + +
    + <%= render("#{@web.markup}_help") %> + <%= render 'wiki_words_help' %> +
    + +
    + <%= form_tag({ :action => 'save', :web => @web.address, :id => @page.name }, + { 'id' => 'editForm', 'method' => 'post', 'onSubmit' => 'cleanAuthorName()', + 'accept-charset' => 'utf-8' }) %> + + +
    + as + <%= text_field_tag :author, @author, + :onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;", + :onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %> + | + + <%= link_to('Cancel', {:web => @web.address, :action => 'cancel_edit', :id => @page.name}, + {:accesskey => 'c'}) %> + (unlocks page) + +
    + <%= end_form_tag %> +
    + + diff --git a/app/views/wiki/export.rhtml b/app/views/wiki/export.rhtml new file mode 100644 index 00000000..d8bc3f02 --- /dev/null +++ b/app/views/wiki/export.rhtml @@ -0,0 +1,12 @@ +<% @title = "Export" %> + +

    You can export all the pages in this web as a zip file in either HTML (with working links and all) or the pure markup (to import in another wiki).

    + +
      +
    • <%= link_to 'HTML', :web => @web.address, :action => 'export_html' %>
    • +
    • <%= link_to "Markup (#{@web.markup.to_s.capitalize})", :web => @web.address, :action => 'export_markup' %>
    • +<% if OPTIONS[:pdflatex] && @web.markup == :textile %> +
    • <%= link_to 'TeX', :web => @web.address, :action => 'export_tex' %>
    • +
    • <%= link_to 'PDF', :web => @web.address, :action => 'export_pdf' %>
    • +<% end %> +
    diff --git a/app/views/wiki/feeds.rhtml b/app/views/wiki/feeds.rhtml new file mode 100644 index 00000000..389d6983 --- /dev/null +++ b/app/views/wiki/feeds.rhtml @@ -0,0 +1,14 @@ +<% @title = "Feeds" %> + +

    You can subscribe to this wiki by RSS and get either just the headlines of the pages that change or the entire page.

    + +
      +
    • + <% if @rss_with_content_allowed %> + <%= link_to 'Full content (RSS 2.0)', :web => @web.address, :action => :rss_with_content %> + <% end %> +
    • +
    • + <%= link_to 'Headlines (RSS 2.0)', :web => @web.address, :action => :rss_with_headlines %> +
    • +
    diff --git a/app/views/wiki/list.rhtml b/app/views/wiki/list.rhtml new file mode 100644 index 00000000..e1b584be --- /dev/null +++ b/app/views/wiki/list.rhtml @@ -0,0 +1,64 @@ +<% @title = "All Pages" %> + +<%= categories_menu unless @categories.empty? %> + +
    +<% unless @pages_that_are_orphaned.empty? && @page_names_that_are_wanted.empty? %> +

    + All Pages +
    All pages in <%= @set_name %> listed alphabetically +

    +<% end %> + +
      + <% @pages_in_category.each do |page| %> +
    • + <%= link_to_existing_page page, truncate(page.plain_name, 35) %> +
    • +<% end %>
    + +<% if @web.count_pages? %> + <% total_chars = @pages_in_category.characters %> +

    All content: <%= total_chars %> chars / approx. <%= sprintf("%-.1f", (total_chars / 2275 )) %> printed pages

    +<% end %> +
    + +
    +<% unless @page_names_that_are_wanted.empty? %> +

    + Wanted Pages +
    + + Unexisting pages that other pages in <%= @set_name %> reference + +

    + +
      + <% @page_names_that_are_wanted.each do |wanted_page_name| %> +
    • + <%= link_to_page(wanted_page_name, @web, truncate(WikiWords.separate(wanted_page_name), 35)) %> + wanted by + <%= @web.select.pages_that_reference(wanted_page_name).collect { |referring_page| + link_to_existing_page referring_page + }.join(", ") + %> +
    • + <% end %> +
    +<% end %> + +<% unless @pages_that_are_orphaned.empty? %> +

    + Orphaned Pages +
    Pages in <%= @set_name %> that no other page reference +

    + +
      + <% @pages_that_are_orphaned.each do |orphan_page| %> +
    • + <%= link_to_existing_page orphan_page, truncate(orphan_page.plain_name, 35) %> +
    • + <% end %> +
    +<% end %> +
    diff --git a/app/views/wiki/locked.rhtml b/app/views/wiki/locked.rhtml new file mode 100644 index 00000000..ee0cf814 --- /dev/null +++ b/app/views/wiki/locked.rhtml @@ -0,0 +1,23 @@ +<% @title = "#{@page.plain_name} is locked" %> + +

    + <%= link_to_page(@page.locked_by) %> + <% if @page.lock_duration(Time.now) == 0 %> + just started editing this page. + <% else %> + has been editing this page for <%= @page.lock_duration(Time.now) %> minutes. + <% end %> +

    + +

    + <%= link_to 'Edit the page anyway', + {:web => @web_name, :action => 'edit', :id => @page.name, :params => {'break_lock' => '1'} }, + {:accesskey => 'E'} + %> + + <%= link_to 'Cancel', + {:web => @web_name, :action => 'show', :id => @page.name}, + {:accesskey => 'C'} + %> + +

    diff --git a/app/views/wiki/login.rhtml b/app/views/wiki/login.rhtml new file mode 100644 index 00000000..15582524 --- /dev/null +++ b/app/views/wiki/login.rhtml @@ -0,0 +1,22 @@ +<% @title = "#{@web_name} Login" %><% @hide_navigation = true %> + +

    +<%= form_tag({ :controller => 'wiki', :action => 'authenticate', :web => @web.address}, + { 'name' => 'loginForm', 'id' => 'loginForm', 'method' => 'post', 'accept-charset' => 'utf-8' }) %> +

    + This web is password-protected. Please enter the password. + <% if @web.published? %> + If you don't have the password, you can view this wiki as a <%= link_to 'read-only version', :action => 'published', :id => 'HomePage' %>. + <% end %> +

    +

    + Password: + + +

    +<%= end_form_tag %> +

    + + diff --git a/app/views/wiki/new.rhtml b/app/views/wiki/new.rhtml new file mode 100644 index 00000000..cda26081 --- /dev/null +++ b/app/views/wiki/new.rhtml @@ -0,0 +1,33 @@ +<% + @title = "Creating #{WikiWords.separate(@page_name)}" + @content_width = 720 + @hide_navigation = true +%> + +
    + <%= render("#{@web.markup}_help") %> + <%= render 'wiki_words_help' %> +
    + +
    + <%= form_tag({ :action => 'save', :web => @web.address, :id => @page_name }, + { 'id' => 'editForm', 'method' => 'post', 'onSubmit' => 'cleanAuthorName();', 'accept-charset' => 'utf-8' }) %> + + +
    + as + <%= text_field_tag :author, @author, + :onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;", + :onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %> +
    + <%= end_form_tag %> +
    + + diff --git a/app/views/wiki/page.rhtml b/app/views/wiki/page.rhtml new file mode 100644 index 00000000..725f1184 --- /dev/null +++ b/app/views/wiki/page.rhtml @@ -0,0 +1,51 @@ +<% + @title = @page.plain_name + @title += ' (changes)' if @show_diff + @show_footer = true +%> + +
    + <% if @show_diff %> +

    + + Showing changes from revision #<%= @page.revisions.size - 1 %> to #<%= @page.revisions.size %>: + Added | Removed + +

    + <%= @renderer.display_diff %> + <% else %> + <%= @renderer.display_content %> + <% end %> +
    + + + + diff --git a/app/views/wiki/print.rhtml b/app/views/wiki/print.rhtml new file mode 100644 index 00000000..177e92f1 --- /dev/null +++ b/app/views/wiki/print.rhtml @@ -0,0 +1,14 @@ +<% + @title = @page.plain_name + @hide_navigation = true + @style_additions = ".newWikiWord { background-color: white; font-style: italic; }" + @inline_style = true +%> + +<%= @renderer.display_content_for_export %> + + diff --git a/app/views/wiki/published.rhtml b/app/views/wiki/published.rhtml new file mode 100644 index 00000000..b394583c --- /dev/null +++ b/app/views/wiki/published.rhtml @@ -0,0 +1,9 @@ +<% + @title = @page.plain_name + @hide_navigation = false + @style_additions = ".newWikiWord { background-color: white; font-style: italic; }" + @inline_style = true + @show_footer = true +%> + +<%= @renderer.display_published %> diff --git a/app/views/wiki/recently_revised.rhtml b/app/views/wiki/recently_revised.rhtml new file mode 100644 index 00000000..f4411fba --- /dev/null +++ b/app/views/wiki/recently_revised.rhtml @@ -0,0 +1,19 @@ +<% @title = "Recently Revised" %> + +<%= categories_menu %> + +<% @pages_by_day.keys.sort.reverse.each do |day| %> +

    <%= format_date(day, include_time = false) %>

    +
      + <% for page in @pages_by_day[day] %> +
    • + <%= link_to_existing_page page %> + +
    • + <% end %> +
    +<% end %> diff --git a/app/views/wiki/revision.rhtml b/app/views/wiki/revision.rhtml new file mode 100644 index 00000000..2c0bcefe --- /dev/null +++ b/app/views/wiki/revision.rhtml @@ -0,0 +1,28 @@ +<% + @title = "#{@page.plain_name} (Rev ##{@revision_number}#{@show_diff ? ', changes' : ''})" +%> + + +
    + <% if @show_diff %> +

    + + Showing changes from revision #<%= @revision_number - 1 %> to #<%= @revision_number %>: + Added | Removed + +

    + <%= @renderer.display_diff %> + <% else %> + <%= @renderer.display_content %> + <% end %> +
    + + + + diff --git a/app/views/wiki/rollback.rhtml b/app/views/wiki/rollback.rhtml new file mode 100644 index 00000000..0e4cbea2 --- /dev/null +++ b/app/views/wiki/rollback.rhtml @@ -0,0 +1,39 @@ +<% + @title = "Rollback to #{@page.plain_name} Rev ##{@revision_number}" + @content_width = 720 + @hide_navigation = true +%> + +<%= "

    Please correct the error that caused this error in rendering:
    #{@params["msg"]}

    " if @params["msg"] %> + +
    + <%= render("#{@web.markup}_help") %> + <%= render 'wiki_words_help' %> +
    + +
    + <%= form_tag({:web => @web.address, :action => 'save', :id => @page.name}, + { :id => 'editForm', :method => 'post', :onSubmit => 'cleanAuthorName();', + 'accept-charset' => 'utf-8' }) %> + +
    + as + + | + + <%= link_to('Cancel', {:web => @web.address, :action => 'cancel_edit', :id => @page.name}, + {:accesskey => 'c'}) %> + (unlocks page) + +
    + <%= end_form_tag %> +
    + + diff --git a/app/views/wiki/rss_feed.rxml b/app/views/wiki/rss_feed.rxml new file mode 100644 index 00000000..84e29dbe --- /dev/null +++ b/app/views/wiki/rss_feed.rxml @@ -0,0 +1,21 @@ +xml.rss('version' => '2.0') do + xml.channel do + xml.title(@web.name) + xml.link(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => 'HomePage')) + xml.description('An Instiki wiki') + xml.language('en-us') + xml.ttl('40') + + for page in @pages_by_revision + xml.item do + xml.title(page.plain_name) + unless @hide_description + xml.description(rendered_content(page)) + end + xml.pubDate(page.revised_at.getgm.strftime('%a, %d %b %Y %H:%M:%S Z')) + xml.guid(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => page.name)) + xml.link(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => page.name)) + end + end + end +end diff --git a/app/views/wiki/search.rhtml b/app/views/wiki/search.rhtml new file mode 100644 index 00000000..789f867c --- /dev/null +++ b/app/views/wiki/search.rhtml @@ -0,0 +1,38 @@ +<% @title = "Search results for \"#{h @params["query"]}\"" %> + +<% unless @title_results.empty? %> +

    <%= @title_results.length %> page(s) containing search string in the page name:

    +
      + <% for page in @title_results %> +
    • + <%= link_to page.plain_name, :web => @web.address, :action => 'show', :id => page.name %> +
    • + <% end %> +
    +<% end %> + + +<% unless @results.empty? %> +

    <%= @results.length %> page(s) containing search string in the page text:

    +
      + <% for page in @results %> +
    • + <%= link_to page.plain_name, :web => @web.address, :action => 'show', :id => page.name %> +
    • + <% end %> +
    +<% end %> + +<% if (@results + @title_results).empty? %> +

    No pages contain "<%= h @params["query"] %>"

    +

    + Perhaps you should try expanding your query. Remember that Instiki searches for entire + phrases, so if you search for "all that jazz" it will not match pages that contain these + words in separation—only as a sentence fragment. +

    +

    + If you're a high-tech computer wizard, you might even want try constructing a Ruby regular + expression. That's actually what Instiki uses, so go right ahead and flex your + "[a-z]*Leet?RegExpSkill(s|z)" +

    +<% end %> diff --git a/app/views/wiki/tex.rhtml b/app/views/wiki/tex.rhtml new file mode 100644 index 00000000..ea9a06c6 --- /dev/null +++ b/app/views/wiki/tex.rhtml @@ -0,0 +1,23 @@ +\documentclass[12pt,titlepage]{article} + +\usepackage[danish]{babel} %danske tekster +\usepackage[OT1]{fontenc} %rigtige danske bogstaver... +\usepackage{a4} +\usepackage{graphicx} +\usepackage{ucs} +\usepackage[utf8x]{inputenc} +\input epsf + +%------------------------------------------------------------------- + +\begin{document} + +\sloppy + +%------------------------------------------------------------------- + +\section*{<%= @page.name %>} + +<%= @tex_content %> + +\end{document} \ No newline at end of file diff --git a/app/views/wiki/tex_web.rhtml b/app/views/wiki/tex_web.rhtml new file mode 100644 index 00000000..45953c52 --- /dev/null +++ b/app/views/wiki/tex_web.rhtml @@ -0,0 +1,35 @@ +\documentclass[12pt,titlepage]{article} + +\usepackage{fancyhdr} +\pagestyle{fancy} + +\fancyhead[LE,RO]{} +\fancyhead[LO,RE]{\nouppercase{\bfseries \leftmark}} +\fancyfoot[C]{\thepage} + +\usepackage[danish]{babel} %danske tekster +\usepackage{a4} +\usepackage{graphicx} +\usepackage{ucs} +\usepackage[utf8]{inputenc} +\input epsf + + +%------------------------------------------------------------------- + +\title{<%= @web_name %>} + +\begin{document} + +\maketitle + +\tableofcontents +\pagebreak + +\sloppy + +%------------------------------------------------------------------- + +<%= @tex_content %> + +\end{document} \ No newline at end of file diff --git a/app/views/wiki/web_list.rhtml b/app/views/wiki/web_list.rhtml new file mode 100644 index 00000000..4f9608b0 --- /dev/null +++ b/app/views/wiki/web_list.rhtml @@ -0,0 +1,25 @@ +<% @title = "Wiki webs" %> +
    + +<% @webs.each do |web| %> + + <% if web.password %>
    + <% else %>
    <% end %> + + <%= link_to_page 'HomePage', web, web.name, :mode => 'show' %> + <% if web.published? %> + (<%= link_to_page 'HomePage', web, 'published version', :mode => 'publish' %>) + <% end %> + + + +

    +<% end %> + diff --git a/app/views/wiki_words_help.rhtml b/app/views/wiki_words_help.rhtml new file mode 100644 index 00000000..b283b407 --- /dev/null +++ b/app/views/wiki_words_help.rhtml @@ -0,0 +1,9 @@ +

    Wiki words

    +

    + Two or more uppercase words stuck together (camel case) or any phrase surrounded by double + brackets is a wiki word. A camel-case wiki word can be escaped by putting \ in front of it. +

    +

    + Wiki words: HomePage, ThreeWordsTogether, [[C++]], [[Let's play again!]]
    + Not wiki words: IBM, School +

    diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 00000000..b829644d --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,17 @@ +unless defined?(RAILS_ROOT) + root_path = File.join(File.dirname(__FILE__), '..') + unless RUBY_PLATFORM =~ /mswin32/ + require 'pathname' + root_path = Pathname.new(root_path).cleanpath.to_s + end + RAILS_ROOT = root_path +end + +if File.directory?("#{RAILS_ROOT}/vendor/rails") + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" +else + require 'rubygems' + require 'initializer' +end + +Rails::Initializer.run(:set_load_path) \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 00000000..015390e1 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,105 @@ + +# "Out of the box", Instiki stores it's data in sqlite3 database. Other options are listed below. + +development: + adapter: sqlite3 + database: db/development.db.sqlite3 + +test: + adapter: sqlite3 + database: db/test.db.sqlite3 + +production: + adapter: sqlite3 + database: db/production.db.sqlite3 + +# MySQL (default setup). Versions 4.1 and 5.0 are recommended. +# +# Install the MySQL driver: +# gem install mysql +# On MacOS X: +# gem install mysql -- --include=/usr/local/lib +# On Windows: +# There is no gem for Windows. Install mysql.so from RubyForApache. +# http://rubyforge.org/projects/rubyforapache +# +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html + + +# Get the fast C bindings: +# gem install mysql +# (on OS X: gem install mysql -- --include=/usr/local/lib) +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html + +mysql_example: + adapter: mysql + database: instiki_development + username: root + password: + socket: /path/to/your/mysql.sock + +# Connect on a TCP socket. If omitted, the adapter will connect on the +# domain socket given by socket instead. +#host: localhost +#port: 3306 + +# Warning: The database defined as 'test' will be erased and +# re-generated from your development database when you run 'rake'. +# Do not set this db to the same as development or production. +mysql_example: + adapter: mysql + database: instiki_test + username: root + password: + socket: /path/to/your/mysql.sock + +# PostgreSQL versions 7.4 - 8.2 +# +# Get the C bindings: +# gem install postgres +# or use the pure-Ruby bindings (the only know way on Windows): +# gem install postgres-pr + +postgresql_example: + adapter: postgresql + database: instiki_development + username: instiki + password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. + #host: remote-database + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Character set encoding. The server defaults to sql_ascii. + #encoding: UTF8 + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # info, notice, warning, error, log, fatal, or panic + # The server defaults to notice. + #min_messages: warning + + +# SQLite version 2.x +# gem install sqlite-ruby +sqlite_example: + adapter: sqlite + database: db/development.sqlite2 + + +# SQLite version 3.x +# gem install sqlite3-ruby +sqlite3_example: + adapter: sqlite3 + database: db/development.sqlite3 + +# In-memory SQLite 3 database. Useful for tests. +sqlite3_in_memory_example: + adapter: sqlite3 + database: ":memory:" diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 00000000..24e89a88 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,29 @@ +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + # Skip frameworks you're not going to use + config.frameworks -= [ :action_web_service, :action_mailer ] + + # Use the database for sessions instead of the file system + # (create the session table with 'rake create_sessions_table') + config.action_controller.session_store = :active_record_store + + # Enable page/fragment caching by setting a file-based store + # (remember to create the caching directory and make it readable to the application) + #config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache" + + # Activate observers that should always be running + config.active_record.observers = :page_observer + + # Use Active Record's schema dumper instead of SQL when creating the test database + # (enables use of different database adapters for development and test environments) + config.active_record.schema_format = :ruby + + config.load_paths << "#{RAILS_ROOT}/vendor/plugins/sqlite3-ruby" +end + +# Instiki-specific configuration below +require_dependency 'instiki_errors' + +require 'jcode' diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 00000000..a151c3ba --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,17 @@ +# In the development environment your application's code is reloaded on +# every request. This slows down response time but is perfect for development +# since you don't have to restart the webserver when you make code changes. +config.cache_classes = false + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Enable the breakpoint server that script/breakpointer connects to +config.breakpoint_server = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false + +# Don't care if the mailer can't send +config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 00000000..f2b9ed68 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,17 @@ +# The production environment is meant for finished, "live" apps. +# Code is not reloaded between requests +config.cache_classes = true + +# Use a different logger for distributed setups +# config.logger = SyslogLogger.new + + +# Full error reports are disabled and caching is turned on +config.action_controller.consider_all_requests_local = false +config.action_controller.perform_caching = true + +# Enable serving of images, stylesheets, and javascripts from an asset server +# config.action_controller.asset_host = "http://assets.example.com" + +# Disable delivery errors if you bad email addresses should just be ignored +# config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 00000000..66b823dc --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,23 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! +config.cache_classes = true + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false + +# Tell ActionMailer not to deliver emails to the real world. +# The :test delivery method accumulates sent emails in the +# ActionMailer::Base.deliveries array. +config.action_mailer.delivery_method = :test + +# Overwrite the default settings for fixtures in tests. See Fixtures +# for more details about these settings. +# config.transactional_fixtures = true +# config.instantiated_fixtures = false +# config.pre_loaded_fixtures = false \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 00000000..a5d49bab --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,38 @@ +# Create a route to DEFAULT_WEB, if such is specified; also register a generic route +def connect_to_web(map, generic_path, generic_routing_options) + if defined? DEFAULT_WEB + explicit_path = generic_path.gsub(/:web\/?/, '') + explicit_routing_options = generic_routing_options.merge(:web => DEFAULT_WEB) + map.connect(explicit_path, explicit_routing_options) + end + map.connect(generic_path, generic_routing_options) +end + +ActionController::Routing::Routes.draw do |map| + map.connect 'create_system', :controller => 'admin', :action => 'create_system' + map.connect 'create_web', :controller => 'admin', :action => 'create_web' + map.connect 'remove_orphaned_pages', :controller => 'admin', :action => 'remove_orphaned_pages' + map.connect 'delete_web', :controller => 'admin', :action => 'delete_web' + map.connect 'web_list', :controller => 'wiki', :action => 'web_list' + + connect_to_web map, ':web/edit_web', :controller => 'admin', :action => 'edit_web' + connect_to_web map, ':web/files/:id', :controller => 'file', :action => 'file' + connect_to_web map, ':web/import/:id', :controller => 'file', :action => 'import' + connect_to_web map, ':web/login', :controller => 'wiki', :action => 'login' + connect_to_web map, ':web/web_list', :controller => 'wiki', :action => 'web_list' + connect_to_web map, ':web/show/diff/:id', :controller => 'wiki', :action => 'show', :mode => 'diff' + connect_to_web map, ':web/revision/diff/:id', :controller => 'wiki', :action => 'revision', :mode => 'diff' + connect_to_web map, ':web/list', :controller => 'wiki', :action => 'list' + connect_to_web map, ':web/list/:category', :controller => 'wiki', :action => 'list' + connect_to_web map, ':web/recently_revised', :controller => 'wiki', :action => 'recently_revised' + connect_to_web map, ':web/recently_revised/:category', :controller => 'wiki', :action => 'recently_revised' + connect_to_web map, ':web/:action/:id', :controller => 'wiki' + connect_to_web map, ':web/:action', :controller => 'wiki' + connect_to_web map, ':web', :controller => 'wiki', :action => 'index' + + if defined? DEFAULT_WEB + map.connect '', :controller => 'wiki', :web => DEFAULT_WEB, :action => 'index' + else + map.connect '', :controller => 'wiki', :action => 'index' + end +end diff --git a/config/spam_patterns.txt b/config/spam_patterns.txt new file mode 100644 index 00000000..5c12addc --- /dev/null +++ b/config/spam_patterns.txt @@ -0,0 +1,97 @@ +.*\[\/link\] +.*\[\/url\] +51wisdom +acupuncturealliance +acyclovir +Adipex +adultfriend +airline +allegra +ampicill +anafranil +atenolol +attacke\.ch +autocorp +awardspace +blogspot\.com +bravehost\.com +butalbital +buy cheap +buy computer +buy-online +calling-phone-cards +casino +celexa +cialis +computer-exchange\.com +Cool website! +debt\s*consolidation +diazepam +display:\s*none +domaindlx\.com +equity\s*loan +Erectol +Feel free to visit my page +fortunecity +fuck +funpic\.de +gambling +Good job man +gucci +guestbook +hamburger +hold-em +holdem +home\s*loan +hoodia +http://[A-Za-z0-9_\.]+\.cn +hydrocodone +I am really excited +I really like your site +igotfree +ketoconazole +lust cartoon +mijneigenweblog +Mortage +my homepage +myspace +naked +netfirms\.com +nice site +overflow:\s*auto +paxil +pbwiki\.com +penis +Phentermine +phpbbforfree +pochta\.ru +poker +porn +prohosting +protonix +rapidforum +replica +ringtone +rolex +serotonin +singtaotor +slot\s*machin +soma +super site +texas +thepussies +tits +Tramadol +versace +viagra +vuitton +websamba\.com +xanax +xoomer +xrumer +Your site is great! +zoloft +\.iwarp\. +\.tripod\.com +\[link\= +\[url\= diff --git a/db/migrate/001_beta1_schema.rb b/db/migrate/001_beta1_schema.rb new file mode 100644 index 00000000..8985aa95 --- /dev/null +++ b/db/migrate/001_beta1_schema.rb @@ -0,0 +1,56 @@ +class Beta1Schema < ActiveRecord::Migration + def self.up + create_table "pages", :force => true do |t| + t.column "created_at", :datetime, :null => false + t.column "updated_at", :datetime, :null => false + t.column "web_id", :integer, :default => 0, :null => false + t.column "locked_by", :string, :limit => 60 + t.column "name", :string, :limit => 60 + t.column "locked_at", :datetime + end + + create_table "revisions", :force => true do |t| + t.column "created_at", :datetime, :null => false + t.column "updated_at", :datetime, :null => false + t.column "revised_at", :datetime, :null => false + t.column "page_id", :integer, :default => 0, :null => false + t.column "content", :text, :default => "", :null => false + t.column "author", :string, :limit => 60 + t.column "ip", :string, :limit => 60 + end + + create_table "system", :force => true do |t| + t.column "password", :string, :limit => 60 + end + + create_table "webs", :force => true do |t| + t.column "created_at", :datetime, :null => false + t.column "updated_at", :datetime, :null => false + t.column "name", :string, :limit => 60, :default => "", :null => false + t.column "address", :string, :limit => 60, :default => "", :null => false + t.column "password", :string, :limit => 60 + t.column "additional_style", :string + t.column "allow_uploads", :integer, :default => 1 + t.column "published", :integer, :default => 0 + t.column "count_pages", :integer, :default => 0 + t.column "markup", :string, :limit => 50, :default => "textile" + t.column "color", :string, :limit => 6, :default => "008B26" + t.column "max_upload_size", :integer, :default => 100 + t.column "safe_mode", :integer, :default => 0 + t.column "brackets_only", :integer, :default => 0 + end + + create_table "wiki_references", :force => true do |t| + t.column "created_at", :datetime, :null => false + t.column "updated_at", :datetime, :null => false + t.column "page_id", :integer, :default => 0, :null => false + t.column "referenced_name", :string, :limit => 60, :default => "", :null => false + t.column "link_type", :string, :limit => 1, :default => "", :null => false + end + end + + def self.down + raise 'Initial schema - cannot be further reverted' + end + +end diff --git a/db/migrate/002_beta2_changes_bulk.rb b/db/migrate/002_beta2_changes_bulk.rb new file mode 100644 index 00000000..b0b94486 --- /dev/null +++ b/db/migrate/002_beta2_changes_bulk.rb @@ -0,0 +1,36 @@ +class Beta2ChangesBulk < ActiveRecord::Migration + def self.up + add_index "revisions", "page_id" + add_index "revisions", "created_at" + add_index "revisions", "author" + + create_table "sessions", :force => true do |t| + t.column "session_id", :string + t.column "data", :text + t.column "updated_at", :datetime + end + add_index "sessions", "session_id" + + create_table "wiki_files", :force => true do |t| + t.column "created_at", :datetime, :null => false + t.column "updated_at", :datetime, :null => false + t.column "web_id", :integer, :null => false + t.column "file_name", :string, :null => false + t.column "description", :string, :null => false + end + + add_index "wiki_references", "page_id" + add_index "wiki_references", "referenced_name" + end + + def self.down + remove_index "wiki_references", "referenced_name" + remove_index "wiki_references", "page_id" + drop_table "wiki_files" + remove_index "sessions", "session_id" + drop_table "sessions" + remove_index "revisions", "author" + remove_index "revisions", "created_at" + remove_index "revisions", "page_id" + end +end diff --git a/instiki b/instiki new file mode 100755 index 00000000..db95d002 --- /dev/null +++ b/instiki @@ -0,0 +1,7 @@ +#!/bin/sh + +cd $(dirname $0) + +export LD_LIBRARY_PATH=./lib/native/linux-x86:$LD_LIBRARY_PATH +ruby script/server + diff --git a/instiki.cmd b/instiki.cmd new file mode 100644 index 00000000..00461495 --- /dev/null +++ b/instiki.cmd @@ -0,0 +1,2 @@ +set PATH=.\lib\native\win32;%PATH% +ruby.exe script\server -e production \ No newline at end of file diff --git a/instiki.rb b/instiki.rb new file mode 100755 index 00000000..93f6ea51 --- /dev/null +++ b/instiki.rb @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +load File.dirname(__FILE__) + '/script/server' diff --git a/lib/bluecloth_tweaked.rb b/lib/bluecloth_tweaked.rb new file mode 100644 index 00000000..b91622f1 --- /dev/null +++ b/lib/bluecloth_tweaked.rb @@ -0,0 +1,1127 @@ +#!/usr/bin/env ruby +# +# Bluecloth is a Ruby implementation of Markdown, a text-to-HTML conversion +# tool. +# +# == Synopsis +# +# doc = BlueCloth::new " +# ## Test document ## +# +# Just a simple test. +# " +# +# puts doc.to_html +# +# == Authors +# +# * Michael Granger +# +# == Contributors +# +# * Martin Chase - Peer review, helpful suggestions +# * Florian Gross - Filter options, suggestions +# +# == Copyright +# +# Original version: +# Copyright (c) 2003-2004 John Gruber +# +# All rights reserved. +# +# Ruby port: +# Copyright (c) 2004 The FaerieMUD Consortium. +# +# BlueCloth is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# BlueCloth is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# == To-do +# +# * Refactor some of the larger uglier methods that have to do their own +# brute-force scanning because of lack of Perl features in Ruby's Regexp +# class. Alternately, could add a dependency on 'pcre' and use most Perl +# regexps. +# +# * Put the StringScanner in the render state for thread-safety. +# +# == Version +# +# $Id: bluecloth.rb,v 1.3 2004/05/02 15:56:33 webster132 Exp $ +# + +require 'digest/md5' +require 'logger' +require 'strscan' + + +### BlueCloth is a Ruby implementation of Markdown, a text-to-HTML conversion +### tool. +class BlueCloth < String + + ### Exception class for formatting errors. + class FormatError < RuntimeError + + ### Create a new FormatError with the given source +str+ and an optional + ### message about the +specific+ error. + def initialize( str, specific=nil ) + if specific + msg = "Bad markdown format near %p: %s" % [ str, specific ] + else + msg = "Bad markdown format near %p" % str + end + + super( msg ) + end + end + + + # Release Version + Version = '0.0.3' + + # SVN Revision + SvnRev = %q$Rev: 37 $ + + # SVN Id tag + SvnId = %q$Id: bluecloth.rb,v 1.3 2004/05/02 15:56:33 webster132 Exp $ + + # SVN URL + SvnUrl = %q$URL: svn+ssh://cvs.faeriemud.org/var/svn/BlueCloth/trunk/lib/bluecloth.rb $ + + + # Rendering state struct. Keeps track of URLs, titles, and HTML blocks + # midway through a render. I prefer this to the globals of the Perl version + # because globals make me break out in hives. Or something. + RenderState = Struct::new( "RenderState", :urls, :titles, :html_blocks, :log ) + + # Tab width for #detab! if none is specified + TabWidth = 4 + + # The tag-closing string -- set to '>' for HTML + EmptyElementSuffix = "/>"; + + # Table of MD5 sums for escaped characters + EscapeTable = {} + '\\`*_{}[]()#.!'.split(//).each {|char| + hash = Digest::MD5::hexdigest( char ) + + EscapeTable[ char ] = { + :md5 => hash, + :md5re => Regexp::new( hash ), + :re => Regexp::new( '\\\\' + Regexp::escape(char) ), + } + } + + + ################################################################# + ### I N S T A N C E M E T H O D S + ################################################################# + + ### Create a new BlueCloth string. + def initialize( content="", *restrictions ) + @log = Logger::new( $deferr ) + @log.level = $DEBUG ? + Logger::DEBUG : + ($VERBOSE ? Logger::INFO : Logger::WARN) + @scanner = nil + + # Add any restrictions, and set the line-folding attribute to reflect + # what happens by default. + restrictions.flatten.each {|r| __send__("#{r}=", true) } + @fold_lines = true + + super( content ) + + @log.debug "String is: %p" % self + end + + + ###### + public + ###### + + # Filters for controlling what gets output for untrusted input. (But really, + # you're filtering bad stuff out of untrusted input at submission-time via + # untainting, aren't you?) + attr_accessor :filter_html, :filter_styles + + # RedCloth-compatibility accessor. Line-folding is part of Markdown syntax, + # so this isn't used by anything. + attr_accessor :fold_lines + + + ### Render Markdown-formatted text in this string object as HTML and return + ### it. The parameter is for compatibility with RedCloth, and is currently + ### unused, though that may change in the future. + def to_html( lite=false ) + + # Create a StringScanner we can reuse for various lexing tasks + @scanner = StringScanner::new( '' ) + + # Make a structure to carry around stuff that gets placeholdered out of + # the source. + rs = RenderState::new( {}, {}, {} ) + + # Make a copy of the string with normalized line endings, tabs turned to + # spaces, and a couple of guaranteed newlines at the end + text = self.gsub( /\r\n?/, "\n" ).detab + text += "\n\n" + @log.debug "Normalized line-endings: %p" % text + + # Filter HTML if we're asked to do so + if self.filter_html + text.gsub!( "<", "<" ) + text.gsub!( ">", ">" ) + @log.debug "Filtered HTML: %p" % text + end + + # Simplify blank lines + text.gsub!( /^ +$/, '' ) + @log.debug "Tabs -> spaces/blank lines stripped: %p" % text + + # Replace HTML blocks with placeholders + text = hide_html_blocks( text, rs ) + @log.debug "Hid HTML blocks: %p" % text + @log.debug "Render state: %p" % rs + + # Strip link definitions, store in render state + text = strip_link_definitions( text, rs ) + @log.debug "Stripped link definitions: %p" % text + @log.debug "Render state: %p" % rs + + # Escape meta-characters + text = escape_special_chars( text ) + @log.debug "Escaped special characters: %p" % text + + # Transform block-level constructs + text = apply_block_transforms( text, rs ) + @log.debug "After block-level transforms: %p" % text + + # Now swap back in all the escaped characters + text = unescape_special_chars( text ) + @log.debug "After unescaping special characters: %p" % text + + return text + end + + + ### Convert tabs in +str+ to spaces. + def detab( tabwidth=TabWidth ) + copy = self.dup + copy.detab!( tabwidth ) + return copy + end + + + ### Convert tabs to spaces in place and return self if any were converted. + def detab!( tabwidth=TabWidth ) + newstr = self.split( /\n/ ).collect {|line| + line.gsub( /(.*?)\t/ ) do + $1 + ' ' * (tabwidth - $1.length % tabwidth) + end + }.join("\n") + self.replace( newstr ) + end + + + ####### + #private + ####### + + ### Do block-level transforms on a copy of +str+ using the specified render + ### state +rs+ and return the results. + def apply_block_transforms( str, rs ) + # Port: This was called '_runBlockGamut' in the original + + @log.debug "Applying block transforms to:\n %p" % str + text = transform_headers( str, rs ) + text = transform_hrules( text, rs ) + text = transform_lists( text, rs ) + text = transform_code_blocks( text, rs ) + text = transform_block_quotes( text, rs ) + text = transform_auto_links( text, rs ) + text = hide_html_blocks( text, rs ) + + text = form_paragraphs( text, rs ) + + @log.debug "Done with block transforms:\n %p" % text + return text + end + + + ### Apply Markdown span transforms to a copy of the specified +str+ with the + ### given render state +rs+ and return it. + def apply_span_transforms( str, rs ) + @log.debug "Applying span transforms to:\n %p" % str + + str = transform_code_spans( str, rs ) + str = encode_html( str ) + str = transform_images( str, rs ) + str = transform_anchors( str, rs ) + str = transform_italic_and_bold( str, rs ) + + # Hard breaks + str.gsub!( / {2,}\n/, " + #
    + # tags for inner block must be indented. + #
    + #
    + StrictBlockRegex = %r{ + ^ # Start of line + <(#{BlockTagPattern}) # Start tag: \2 + \b # word break + (.*\n)*? # Any number of lines, minimal match + # Matching end tag + [ ]* # trailing spaces + (?=\n+|\Z) # End of line or document + }ix + + # More-liberal block-matching + LooseBlockRegex = %r{ + ^ # Start of line + <(#{BlockTagPattern}) # start tag: \2 + \b # word break + (.*\n)*? # Any number of lines, minimal match + .* # Anything + Matching end tag + [ ]* # trailing spaces + (?=\n+|\Z) # End of line or document + }ix + + # Special case for
    . + HruleBlockRegex = %r{ + ( # $1 + \A\n? # Start of doc + optional \n + | # or + .*\n\n # anything + blank line + ) + ( # save in $2 + [ ]* # Any spaces +
    ])*? # Attributes + /?> # Tag close + (?=\n\n|\Z) # followed by a blank line or end of document + ) + }ix + + ### Replace all blocks of HTML in +str+ that start in the left margin with + ### tokens. + def hide_html_blocks( str, rs ) + @log.debug "Hiding HTML blocks in %p" % str + + # Tokenizer proc to pass to gsub + tokenize = lambda {|match| + key = Digest::MD5::hexdigest( match ) + rs.html_blocks[ key ] = match + @log.debug "Replacing %p with %p" % + [ match, key ] + "\n\n#{key}\n\n" + } + + rval = str.dup + + @log.debug "Finding blocks with the strict regex..." + rval.gsub!( StrictBlockRegex, &tokenize ) + + @log.debug "Finding blocks with the loose regex..." + rval.gsub!( LooseBlockRegex, &tokenize ) + + @log.debug "Finding hrules..." + rval.gsub!( HruleBlockRegex ) {|match| $1 + tokenize[$2] } + + return rval + end + + + # Link defs are in the form: ^[id]: url "optional title" + LinkRegex = %r{ + ^[ ]*\[(.+)\]: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (\S+) # url = $2 + [ ]* + \n? # maybe one newline + [ ]* + (?: + # Titles are delimited by "quotes" or (parens). + ["(] + (.+?) # title = $3 + [")] # Matching ) or " + [ ]* + )? # title is optional + (?:\n+|\Z) + }x + + ### Strip link definitions from +str+, storing them in the given RenderState + ### +rs+. + def strip_link_definitions( str, rs ) + str.gsub( LinkRegex ) {|match| + id, url, title = $1, $2, $3 + + rs.urls[ id.downcase ] = encode_html( url ) + unless title.nil? + rs.titles[ id.downcase ] = title.gsub( /"/, """ ) + end + "" + } + end + + + ### Escape special characters in the given +str+ + def escape_special_chars( str ) + @log.debug " Escaping special characters" + text = '' + + tokenize_html( str ) {|token, str| + @log.debug " Adding %p token %p" % [ token, str ] + case token + + # Within tags, encode * and _ + when :tag + text += str. + gsub( /\*/, EscapeTable['*'][:md5] ). + gsub( /_/, EscapeTable['_'][:md5] ) + + # Encode backslashed stuff in regular text + when :text + text += encode_backslash_escapes( str ) + else + raise TypeError, "Unknown token type %p" % token + end + } + + @log.debug " Text with escapes is now: %p" % text + return text + end + + + ### Swap escaped special characters in a copy of the given +str+ and return + ### it. + def unescape_special_chars( str ) + EscapeTable.each {|char, hash| + @log.debug "Unescaping escaped %p with %p" % + [ char, hash[:md5re] ] + str.gsub!( hash[:md5re], char ) + } + + return str + end + + + ### Return a copy of the given +str+ with any backslashed special character + ### in it replaced with MD5 placeholders. + def encode_backslash_escapes( str ) + # Make a copy with any double-escaped backslashes encoded + text = str.gsub( /\\\\/, EscapeTable['\\'][:md5] ) + + EscapeTable.each_pair {|char, esc| + next if char == '\\' + text.gsub!( esc[:re], esc[:md5] ) + } + + return text + end + + + ### Transform any Markdown-style horizontal rules in a copy of the specified + ### +str+ and return it. + def transform_hrules( str, rs ) + @log.debug " Transforming horizontal rules" + str.gsub( /^( ?[\-\*] ?){3,}$/, "\n\n%s\n} % [ + list_type, + transform_list_items( list, rs ), + list_type, + ] + } + end + + + # Pattern for transforming list items + ListItemRegexp = %r{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + (\*|\d+\.) [ ]+ # list marker = $3 + ((?m:.+?) # list item text = $4 + (\n{1,2})) + (?= \n* (\z | \2 (\*|\d+\.) [ ]+)) + }x + + ### Transform list items in a copy of the given +str+ and return it. + def transform_list_items( str, rs ) + @log.debug " Transforming list items" + + # Trim trailing blank lines + str = str.sub( /\n{2,}\z/, "\n" ) + + str.gsub( ListItemRegexp ) {|line| + @log.debug " Found item line %p" % line + leading_line, item = $1, $4 + + if leading_line or /\n{2,}/.match( item ) + @log.debug " Found leading line or item has a blank" + item = apply_block_transforms( outdent(item), rs ) + else + # Recursion for sub-lists + @log.debug " Recursing for sublist" + item = transform_lists( outdent(item), rs ).chomp + item = apply_span_transforms( item, rs ) + end + + %{
  • %s
  • \n} % item + } + end + + + # Pattern for matching codeblocks + CodeBlockRegexp = %r{ + (.?) # $1 = preceding character + :\n+ # colon + NL delimiter + ( # $2 = the code block + (?: + (?:[ ]{#{TabWidth}} | \t) # a tab or tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,#{TabWidth}}\S)|\Z) # Lookahead for non-space at + # line-start, or end of doc + }x + + ### Transform Markdown-style codeblocks in a copy of the specified +str+ and + ### return it. + def transform_code_blocks( str, rs ) + @log.debug " Transforming code blocks" + + str.gsub( CodeBlockRegexp ) {|block| + prevchar, codeblock = $1, $2 + + @log.debug " prevchar = %p" % prevchar + + # Generated the codeblock + %{%s\n\n
    %s\n
    \n\n} % [ + (prevchar.empty? || /\s/ =~ prevchar) ? "" : "#{prevchar}:", + encode_code( outdent(codeblock), rs ).rstrip, + ] + } + end + + + # Pattern for matching Markdown blockquote blocks + BlockQuoteRegexp = %r{ + (?: + ^[ ]*>[ ]? # '>' at the start of a line + .+\n # rest of the first line + (?:.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + }x + + ### Transform Markdown-style blockquotes in a copy of the specified +str+ + ### and return it. + def transform_block_quotes( str, rs ) + @log.debug " Transforming block quotes" + + str.gsub( BlockQuoteRegexp ) {|quote| + @log.debug "Making blockquote from %p" % quote + quote.gsub!( /^[ ]*>[ ]?/, '' ) + %{
    \n%s\n
    \n\n} % + apply_block_transforms( quote, rs ). + gsub( /^/, " " * TabWidth ) + } + end + + + AutoAnchorURLRegexp = /<((https?|ftp):[^'">\s]+)>/ + AutoAnchorEmailRegexp = %r{ + < + ( + [-.\w]+ + \@ + [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ + ) + > + }x + + ### Transform URLs in a copy of the specified +str+ into links and return + ### it. + def transform_auto_links( str, rs ) + @log.debug " Transforming auto-links" + str.gsub( AutoAnchorURLRegexp, %{\\1}). + gsub( AutoAnchorEmailRegexp ) {|addr| + encode_email_address( unescape_special_chars($1) ) + } + end + + + # Encoder functions to turn characters of an email address into encoded + # entities. + Encoders = [ + lambda {|char| "&#%03d;" % char}, + lambda {|char| "&#x%X;" % char}, + lambda {|char| char.chr }, + ] + + ### Transform a copy of the given email +addr+ into an escaped version safer + ### for posting publicly. + def encode_email_address( addr ) + + rval = '' + ("mailto:" + addr).each_byte {|b| + case b + when ?: + rval += ":" + when ?@ + rval += Encoders[ rand(2) ][ b ] + else + r = rand(100) + rval += ( + r > 90 ? Encoders[2][ b ] : + r < 45 ? Encoders[1][ b ] : + Encoders[0][ b ] + ) + end + } + + return %{%s} % [ rval, rval.sub(/.+?:/, '') ] + end + + + # Regex for matching Setext-style headers + SetextHeaderRegexp = %r{ + (.+) # The title text ($1) + \n + ([\-=])+ # Match a line of = or -. Save only one in $2. + [ ]*\n+ + }x + + # Regexp for matching ATX-style headers + AtxHeaderRegexp = %r{ + ^(\#{1,6}) # $1 = string of #'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #'s (not counted) + \n+ + }x + + ### Apply Markdown header transforms to a copy of the given +str+ amd render + ### state +rs+ and return the result. + def transform_headers( str, rs ) + @log.debug " Transforming headers" + + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + # + str. + gsub( SetextHeaderRegexp ) {|m| + @log.debug "Found setext-style header" + title, hdrchar = $1, $2 + title = apply_span_transforms( title, rs ) + + case hdrchar + when '=' + %[

    #{title}

    \n\n] + when '-' + %[

    #{title}

    \n\n] + else + title + end + }. + + gsub( AtxHeaderRegexp ) {|m| + @log.debug "Found ATX-style header" + hdrchars, title = $1, $2 + title = apply_span_transforms( title, rs ) + + level = hdrchars.length + %{%s\n\n} % [ level, title, level ] + } + end + + + ### Wrap all remaining paragraph-looking text in a copy of +str+ inside

    + ### tags and return it. + def form_paragraphs( str, rs ) + @log.debug " Forming paragraphs" + grafs = str. + sub( /\A\n+/, '' ). + sub( /\n+\z/, '' ). + split( /\n{2,}/ ) + + rval = grafs.collect {|graf| + + # Unhashify HTML blocks if this is a placeholder + if rs.html_blocks.key?( graf ) + rs.html_blocks[ graf ] + + # Otherwise, wrap in

    tags + else + apply_span_transforms(graf, rs). + sub( /^[ ]*/, '

    ' ) + '

    ' + end + }.join( "\n\n" ) + + @log.debug " Formed paragraphs: %p" % rval + return rval + end + + + # Pattern to match the linkid part of an anchor tag for reference-style + # links. + RefLinkIdRegex = %r{ + [ ]? # Optional leading space + (?:\n[ ]*)? # Optional newline + spaces + \[ + (.*?) # Id = $1 + \] + }x + + InlineLinkRegex = %r{ + \( # Literal paren + [ ]* # Zero or more spaces + (.*?) # URI = $1 + [ ]* # Zero or more spaces + (?: # + ([\"\']) # Opening quote char = $2 + (.*?) # Title = $3 + \2 # Matching quote char + )? # Title is optional + \) + }x + + ### Apply Markdown anchor transforms to a copy of the specified +str+ with + ### the given render state +rs+ and return it. + def transform_anchors( str, rs ) + @log.debug " Transforming anchors" + @scanner.string = str.dup + text = '' + + # Scan the whole string + until @scanner.empty? + + if @scanner.scan( /\[/ ) + link = ''; linkid = '' + depth = 1 + startpos = @scanner.pos + @log.debug " Found a bracket-open at %d" % startpos + + # Scan the rest of the tag, allowing unlimited nested []s. If + # the scanner runs out of text before the opening bracket is + # closed, append the text and return (wasn't a valid anchor). + while depth.nonzero? + linktext = @scanner.scan_until( /\]|\[/ ) + + if linktext + @log.debug " Found a bracket at depth %d: %p" % + [ depth, linktext ] + link += linktext + + # Decrement depth for each closing bracket + depth += ( linktext[-1, 1] == ']' ? -1 : 1 ) + @log.debug " Depth is now #{depth}" + + # If there's no more brackets, it must not be an anchor, so + # just abort. + else + @log.debug " Missing closing brace, assuming non-link." + link += @scanner.rest + @scanner.terminate + return text + '[' + link + end + end + link.slice!( -1 ) # Trim final ']' + @log.debug " Found leading link %p" % link + + # Look for a reference-style second part + if @scanner.scan( RefLinkIdRegex ) + linkid = @scanner[1] + linkid = link.dup if linkid.empty? + linkid.downcase! + @log.debug " Found a linkid: %p" % linkid + + # If there's a matching link in the link table, build an + # anchor tag for it. + if rs.urls.key?( linkid ) + @log.debug " Found link key in the link table: %p" % + rs.urls[linkid] + url = escape_md( rs.urls[linkid] ) + + text += %{#{link}} + + # If the link referred to doesn't exist, just append the raw + # source to the result + else + @log.debug " Linkid %p not found in link table" % linkid + @log.debug " Appending original string instead: %p" % + @scanner.string[ startpos-1 .. @scanner.pos ] + text += @scanner.string[ startpos-1 .. @scanner.pos ] + end + + # ...or for an inline style second part + elsif @scanner.scan( InlineLinkRegex ) + url = @scanner[1] + title = @scanner[3] + @log.debug " Found an inline link to %p" % url + + text += %{#{link}} + + # No linkid part: just append the first part as-is. + else + @log.debug "No linkid, so no anchor. Appending literal text." + text += @scanner.string[ startpos-1 .. @scanner.pos-1 ] + end # if linkid + + # Plain text + else + @log.debug " Scanning to the next link from %p" % @scanner.rest + text += @scanner.scan( /[^\[]+/ ) + end + + end # until @scanner.empty? + + return text + end + + # Pattern to match strong emphasis in Markdown text + BoldRegexp = %r{ (\*\*|__) (?=\S) (.+?\S) \1 }x + + # Pattern to match normal emphasis in Markdown text + ItalicRegexp = %r{ (\*|_) (?=\S) (.+?\S) \1 }x + + ### Transform italic- and bold-encoded text in a copy of the specified +str+ + ### and return it. + def transform_italic_and_bold( str, rs ) + @log.debug " Transforming italic and bold" + + str. + gsub( BoldRegexp, %{\\2} ). + gsub( ItalicRegexp, %{\\2} ) + end + + + ### Transform backticked spans into spans. + def transform_code_spans( str, rs ) + @log.debug " Transforming code spans" + + # Set up the string scanner and just return the string unless there's at + # least one backtick. + @scanner.string = str.dup + unless @scanner.exist?( /`/ ) + @scanner.terminate + @log.debug "No backticks found for code span in %p" % str + return str + end + + @log.debug "Transforming code spans in %p" % str + + # Build the transformed text anew + text = '' + + # Scan to the end of the string + until @scanner.empty? + + # Scan up to an opening backtick + if pre = @scanner.scan_until( /.?(?=`)/m ) + text += pre + @log.debug "Found backtick at %d after '...%s'" % + [ @scanner.pos, text[-10, 10] ] + + # Make a pattern to find the end of the span + opener = @scanner.scan( /`+/ ) + len = opener.length + closer = Regexp::new( opener ) + @log.debug "Scanning for end of code span with %p" % closer + + # Scan until the end of the closing backtick sequence. Chop the + # backticks off the resultant string, strip leading and trailing + # whitespace, and encode any enitites contained in it. + codespan = @scanner.scan_until( closer ) or + raise FormatError::new( @scanner.rest[0,20], + "No %p found before end" % opener ) + + @log.debug "Found close of code span at %d: %p" % + [ @scanner.pos - len, codespan ] + codespan.slice!( -len, len ) + text += "%s" % + encode_code( codespan.strip, rs ) + + # If there's no more backticks, just append the rest of the string + # and move the scan pointer to the end + else + text += @scanner.rest + @scanner.terminate + end + end + + return text + end + + + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + InlineImageRegexp = %r{ + ( # Whole match = $1 + !\[ (.*?) \] # alt text = $2 + \([ ]* (\S+) [ ]* # source url = $3 + ( # title = $4 + (["']) # quote char = $5 + .*? + \5 # matching quote + [ ]* + )? # title is optional + \) + ) + }xs #" + + + # Reference-style images + ReferenceImageRegexp = %r{ + ( # Whole match = $1 + !\[ (.*?) \] # Alt text = $2 + [ ]? # Optional space + (?:\n[ ]*)? # One optional newline + spaces + \[ (.*?) \] # id = $3 + ) + }xs + + ### Turn image markup into image tags. + def transform_images( str, rs ) + @log.debug " Transforming images" % str + + # Handle reference-style labeled images: ![alt text][id] + str. + gsub( ReferenceImageRegexp ) {|match| + whole, alt, linkid = $1, $2, $3.downcase + @log.debug "Matched %p" % match + res = nil + + # for shortcut links like ![this][]. + linkid = alt.downcase if linkid.empty? + + if rs.urls.key?( linkid ) + url = escape_md( rs.urls[linkid] ) + @log.debug "Found url '%s' for linkid '%s' " % + [ url, linkid ] + + # Build the tag + result = %{%s}, '>' ). + gsub( CodeEscapeRegexp ) {|match| EscapeTable[match][:md5]} + end + + + + ################################################################# + ### U T I L I T Y F U N C T I O N S + ################################################################# + + ### Escape any markdown characters in a copy of the given +str+ and return + ### it. + def escape_md( str ) + str. + gsub( /\*/, '*' ). + gsub( /_/, '_' ) + end + + + # Matching constructs for tokenizing X/HTML + HTMLCommentRegexp = %r{ }mx + XMLProcInstRegexp = %r{ <\? .*? \?> }mx + MetaTag = Regexp::union( HTMLCommentRegexp, XMLProcInstRegexp ) + + HTMLTagOpenRegexp = %r{ < [a-z/!$] [^<>]* }mx + HTMLTagCloseRegexp = %r{ > }x + HTMLTagPart = Regexp::union( HTMLTagOpenRegexp, HTMLTagCloseRegexp ) + + ### Break the HTML source in +str+ into a series of tokens and return + ### them. The tokens are just 2-element Array tuples with a type and the + ### actual content. If this function is called with a block, the type and + ### text parts of each token will be yielded to it one at a time as they are + ### extracted. + def tokenize_html( str ) + depth = 0 + tokens = [] + @scanner.string = str.dup + type, token = nil, nil + + until @scanner.empty? + @log.debug "Scanning from %p" % @scanner.rest + + # Match comments and PIs without nesting + if (( token = @scanner.scan(MetaTag) )) + type = :tag + + # Do nested matching for HTML tags + elsif (( token = @scanner.scan(HTMLTagOpenRegexp) )) + tagstart = @scanner.pos + @log.debug " Found the start of a plain tag at %d" % tagstart + + # Start the token with the opening angle + depth = 1 + type = :tag + + # Scan the rest of the tag, allowing unlimited nested <>s. If + # the scanner runs out of text before the tag is closed, raise + # an error. + while depth.nonzero? + + # Scan either an opener or a closer + chunk = @scanner.scan( HTMLTagPart ) or + raise "Malformed tag at character %d: %p" % + [ tagstart, token + @scanner.rest ] + + @log.debug " Found another part of the tag at depth %d: %p" % + [ depth, chunk ] + + token += chunk + + # If the last character of the token so far is a closing + # angle bracket, decrement the depth. Otherwise increment + # it for a nested tag. + depth += ( token[-1, 1] == '>' ? -1 : 1 ) + @log.debug " Depth is now #{depth}" + end + + # Match text segments + else + @log.debug " Looking for a chunk of text" + type = :text + + # Scan forward, always matching at least one character to move + # the pointer beyond any non-tag '<'. + token = @scanner.scan_until( /[^<]+/m ) + end + + @log.debug " type: %p, token: %p" % [ type, token ] + + # If a block is given, feed it one token at a time. Add the token to + # the token list to be returned regardless. + if block_given? + yield( type, token ) + end + tokens << [ type, token ] + end + + return tokens + end + + + ### Return a copy of +str+ with angle brackets and ampersands HTML-encoded. + def encode_html( str ) + str.gsub( /&(?!#?[x]?(?:[0-9a-f]+|\w{1,8});)/i, "&" ). + gsub( %r{<(?![a-z/?\$!])}i, "<" ) + end + + + ### Return one level of line-leading tabs or spaces from a copy of +str+ and + ### return it. + def outdent( str ) + str.gsub( /^(\t|[ ]{1,#{TabWidth}})/, '') + end + +end # class BlueCloth + diff --git a/lib/chunks/category.rb b/lib/chunks/category.rb new file mode 100644 index 00000000..d08d8636 --- /dev/null +++ b/lib/chunks/category.rb @@ -0,0 +1,33 @@ +require 'chunks/chunk' + +# The category chunk looks for "category: news" on a line by +# itself and parses the terms after the ':' as categories. +# Other classes can search for Category chunks within +# rendered content to find out what categories this page +# should be in. +# +# Category lines can be hidden using ':category: news', for example +class Category < Chunk::Abstract + CATEGORY_PATTERN = /^(:)?category\s*:(.*)$/i + def self.pattern() CATEGORY_PATTERN end + + attr_reader :hidden, :list + +def initialize(match_data, content) + super(match_data, content) + @hidden = match_data[1] + @list = match_data[2].split(',').map { |c| c.strip } + @unmask_text = '' + if @hidden + @unmask_text = '' + else + category_urls = @list.map { |category| url(category) }.join(', ') + @unmask_text = '
    category: ' + category_urls + '
    ' + end + end + + # TODO move presentation of page metadata to controller/view + def url(category) + %{#{category}} + end +end diff --git a/lib/chunks/chunk.rb b/lib/chunks/chunk.rb new file mode 100644 index 00000000..18de7d0c --- /dev/null +++ b/lib/chunks/chunk.rb @@ -0,0 +1,79 @@ +require 'uri/common' + +# A chunk is a pattern of text that can be protected +# and interrogated by a renderer. Each Chunk class has a +# +pattern+ that states what sort of text it matches. +# Chunks are initalized by passing in the result of a +# match by its pattern. + +module Chunk + class Abstract + + # automatically construct the array of derivatives of Chunk::Abstract + @derivatives = [] + + class << self + attr_reader :derivatives + end + + def self::inherited( klass ) + Abstract::derivatives << klass + end + + # the class name part of the mask strings + def self.mask_string + self.to_s.delete(':').downcase + end + + # a regexp that matches all chunk_types masks + def Abstract::mask_re(chunk_types) + chunk_classes = chunk_types.map{|klass| klass.mask_string}.join("|") + /chunk(-?\d+)(#{chunk_classes})chunk/ + end + + attr_reader :text, :unmask_text, :unmask_mode + + def initialize(match_data, content) + @text = match_data[0] + @content = content + @unmask_mode = :normal + end + + # Find all the chunks of the given type in content + # Each time the pattern is matched, create a new + # chunk for it, and replace the occurance of the chunk + # in this content with its mask. + def self.apply_to(content) + content.gsub!( self.pattern ) do |match| + new_chunk = self.new($~, content) + content.add_chunk(new_chunk) + new_chunk.mask + end + end + + # should contain only [a-z0-9] + def mask + @mask ||= "chunk#{self.object_id}#{self.class.mask_string}chunk" + end + + def unmask + @content.sub!(mask, @unmask_text) + end + + def rendered? + @unmask_mode == :normal + end + + def escaped? + @unmask_mode == :escape + end + + def revert + @content.sub!(mask, @text) + # unregister + @content.delete_chunk(self) + end + + end + +end diff --git a/lib/chunks/engines.rb b/lib/chunks/engines.rb new file mode 100644 index 00000000..3b7b5bbe --- /dev/null +++ b/lib/chunks/engines.rb @@ -0,0 +1,62 @@ +$: << File.dirname(__FILE__) + "../../lib" + +require_dependency 'chunks/chunk' + +# The markup engines are Chunks that call the one of RedCloth +# or RDoc to convert text. This markup occurs when the chunk is required +# to mask itself. +module Engines + class AbstractEngine < Chunk::Abstract + + # Create a new chunk for the whole content and replace it with its mask. + def self.apply_to(content) + new_chunk = self.new(content) + content.replace(new_chunk.mask) + end + + private + + # Never create engines by constructor - use apply_to instead + def initialize(content) + @content = content + end + + end + + class Textile < AbstractEngine + def mask + require_dependency 'redcloth' + redcloth = RedCloth.new(@content, [:hard_breaks] + @content.options[:engine_opts]) + redcloth.filter_html = false + redcloth.no_span_caps = false + redcloth.to_html(:textile) + end + end + + class Markdown < AbstractEngine + def mask + require_dependency 'bluecloth_tweaked' + BlueCloth.new(@content, @content.options[:engine_opts]).to_html + end + end + + class Mixed < AbstractEngine + def mask + require_dependency 'redcloth' + redcloth = RedCloth.new(@content, @content.options[:engine_opts]) + redcloth.filter_html = false + redcloth.no_span_caps = false + redcloth.to_html + end + end + + class RDoc < AbstractEngine + def mask + require_dependency 'rdocsupport' + RDocSupport::RDocFormatter.new(@content).to_html + end + end + + MAP = { :textile => Textile, :markdown => Markdown, :mixed => Mixed, :rdoc => RDoc } + MAP.default = Textile +end diff --git a/lib/chunks/include.rb b/lib/chunks/include.rb new file mode 100644 index 00000000..ac9b9bd8 --- /dev/null +++ b/lib/chunks/include.rb @@ -0,0 +1,49 @@ +require 'chunks/wiki' + +# Includes the contents of another page for rendering. +# The include command looks like this: "[[!include PageName]]". +# It is a WikiReference since it refers to another page (PageName) +# and the wiki content using this command must be notified +# of changes to that page. +# If the included page could not be found, a warning is displayed. + +class Include < WikiChunk::WikiReference + + INCLUDE_PATTERN = /\[\[!include\s+(.*?)\]\]\s*/i + def self.pattern() INCLUDE_PATTERN end + + def initialize(match_data, content) + super + @page_name = match_data[1].strip + rendering_mode = content.options[:mode] || :show + @unmask_text = get_unmask_text_avoiding_recursion_loops(rendering_mode) + end + + private + + def get_unmask_text_avoiding_recursion_loops(rendering_mode) + if refpage + # TODO This way of instantiating a renderer is ugly. + renderer = PageRenderer.new(refpage.current_revision) + if renderer.wiki_includes.include?(@content.page_name) + # this will break the recursion + @content.delete_chunk(self) + return "Recursive include detected; #{@page_name} --> #{@content.page_name} " + + "--> #{@page_name}\n" + else + included_content = + case rendering_mode + when :show then renderer.display_content + when :publish then renderer.display_published + when :export then renderer.display_content_for_export + else raise "Unsupported rendering mode #{@mode.inspect}" + end + @content.merge_chunks(included_content) + return included_content.pre_rendered + end + else + return "Could not include #{@page_name}\n" + end + end + +end diff --git a/lib/chunks/literal.rb b/lib/chunks/literal.rb new file mode 100644 index 00000000..09da4005 --- /dev/null +++ b/lib/chunks/literal.rb @@ -0,0 +1,31 @@ +require 'chunks/chunk' + +# These are basic chunks that have a pattern and can be protected. +# They are used by rendering process to prevent wiki rendering +# occuring within literal areas such as and
     blocks
    +# and within HTML tags.
    +module Literal
    +
    +  class AbstractLiteral < Chunk::Abstract
    +
    +    def initialize(match_data, content)
    +      super
    +      @unmask_text = @text
    +    end
    +
    +  end
    +
    +  # A literal chunk that protects 'code' and 'pre' tags from wiki rendering.
    +  class Pre < AbstractLiteral
    +    PRE_BLOCKS = "a|pre|code"
    +    PRE_PATTERN = Regexp.new('<('+PRE_BLOCKS+')\b[^>]*?>.*?', Regexp::MULTILINE)
    +    def self.pattern() PRE_PATTERN end
    +  end 
    +
    +  # A literal chunk that protects HTML tags from wiki rendering.
    +  class Tags < AbstractLiteral
    +    TAGS = "a|img|em|strong|div|span|table|td|th|ul|ol|li|dl|dt|dd"
    +    TAGS_PATTERN = Regexp.new('<(?:'+TAGS+')[^>]*?>', Regexp::MULTILINE) 
    +    def self.pattern() TAGS_PATTERN  end
    +  end
    +end
    diff --git a/lib/chunks/nowiki.rb b/lib/chunks/nowiki.rb
    new file mode 100644
    index 00000000..ef99ec0b
    --- /dev/null
    +++ b/lib/chunks/nowiki.rb
    @@ -0,0 +1,28 @@
    +require 'chunks/chunk'
    +
    +# This chunks allows certain parts of a wiki page to be hidden from the
    +# rest of the rendering pipeline. It should be run at the beginning
    +# of the pipeline in `wiki_content.rb`.
    +#
    +# An example use of this chunk is to markup double brackets or
    +# auto URI links:
    +#  Here are [[double brackets]] and a URI: www.uri.org
    +#
    +# The contents of the chunks will not be processed by any other chunk
    +# so the `www.uri.org` and the double brackets will appear verbatim.
    +#
    +# Author: Mark Reid 
    +# Created: 8th June 2004
    +class NoWiki < Chunk::Abstract
    +
    +  NOWIKI_PATTERN = Regexp.new('(.*?)', Regexp::MULTILINE)
    +  def self.pattern() NOWIKI_PATTERN end
    +
    +  attr_reader :plain_text
    +
    +  def initialize(match_data, content)
    +    super
    +    @plain_text = @unmask_text = match_data[1]
    +  end
    +
    +end
    diff --git a/lib/chunks/test.rb b/lib/chunks/test.rb
    new file mode 100644
    index 00000000..73af8142
    --- /dev/null
    +++ b/lib/chunks/test.rb
    @@ -0,0 +1,18 @@
    +require 'test/unit'
    +
    +class ChunkTest < Test::Unit::TestCase
    +
    +  # Asserts a number of tests for the given type and text.
    +  def match(type, test_text, expected)
    +    pattern = type.pattern
    +    assert_match(pattern, test_text)
    +    pattern =~ test_text   # Previous assertion guarantees match
    +    chunk = type.new($~)
    +    
    +    # Test if requested parts are correct.
    +    for method_sym, value in expected do
    +      assert_respond_to(chunk, method_sym)
    +      assert_equal(value, chunk.method(method_sym).call, "Checking value of '#{method_sym}'")
    +    end
    +  end
    +end
    diff --git a/lib/chunks/uri.rb b/lib/chunks/uri.rb
    new file mode 100644
    index 00000000..1a208535
    --- /dev/null
    +++ b/lib/chunks/uri.rb
    @@ -0,0 +1,182 @@
    +require 'chunks/chunk'
    +
    +# This wiki chunk matches arbitrary URIs, using patterns from the Ruby URI modules.
    +# It parses out a variety of fields that could be used by renderers to format
    +# the links in various ways (shortening domain names, hiding email addresses)
    +# It matches email addresses and host.com.au domains without schemes (http://)
    +# but adds these on as required.
    +#
    +# The heuristic used to match a URI is designed to err on the side of caution.
    +# That is, it is more likely to not autolink a URI than it is to accidently
    +# autolink something that is not a URI. The reason behind this is it is easier
    +# to force a URI link by prefixing 'http://' to it than it is to escape and
    +# incorrectly marked up non-URI.
    +#
    +# I'm using a part of the [ISO 3166-1 Standard][iso3166] for country name suffixes.
    +# The generic names are from www.bnoack.com/data/countrycode2.html)
    +#   [iso3166]: http://geotags.com/iso3166/
    +
    +class URIChunk < Chunk::Abstract
    +  include URI::REGEXP::PATTERN
    +
    +  # this condition is to get rid of pesky warnings in tests
    +  unless defined? URIChunk::INTERNET_URI_REGEXP
    +
    +    GENERIC = 'aero|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org'
    +    
    +    COUNTRY = 'ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|az|ba|bb|bd|be|' + 
    +      'bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cf|cd|cg|ch|ci|ck|cl|' + 
    +      'cm|cn|co|cr|cs|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|fi|' + 
    +      'fj|fk|fm|fo|fr|fx|ga|gb|gd|ge|gf|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|' + 
    +      'hk|hm|hn|hr|ht|hu|id|ie|il|in|io|iq|ir|is|it|jm|jo|jp|ke|kg|kh|ki|km|kn|' + 
    +      'kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|' + 
    +      'mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nt|' + 
    +      'nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|' + 
    +      'sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|' + 
    +      'tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|' + 
    +      'ws|ye|yt|yu|za|zm|zr|zw'
    +    # These are needed otherwise HOST will match almost anything
    +    TLDS = "(?:#{GENERIC}|#{COUNTRY})"
    +    
    +    # Redefine USERINFO so that it must have non-zero length
    +    USERINFO = "(?:[#{UNRESERVED};:&=+$,]|#{ESCAPED})+"
    +  
    +    # unreserved_no_ending = alphanum | mark, but URI_ENDING [)!] excluded
    +    UNRESERVED_NO_ENDING = "-_.~*'(#{ALNUM}"  
    +
    +    # this ensures that query or fragment do not end with URI_ENDING
    +    # and enable us to use a much simpler self.pattern Regexp
    +
    +    # uric_no_ending = reserved | unreserved_no_ending | escaped
    +    URIC_NO_ENDING = "(?:[#{UNRESERVED_NO_ENDING}#{RESERVED}]|#{ESCAPED})"
    +    # query = *uric
    +    QUERY = "#{URIC_NO_ENDING}*"
    +    # fragment = *uric
    +    FRAGMENT = "#{URIC_NO_ENDING}*"
    +
    +    # DOMLABEL is defined in the ruby uri library, TLDS is defined above
    +    INTERNET_HOSTNAME = "(?:#{DOMLABEL}\\.)+#{TLDS}" 
    +
    +    # Correct a typo bug in ruby 1.8.x lib/uri/common.rb 
    +    PORT = '\\d*'
    +
    +    INTERNET_URI =
    +        "(?:(#{SCHEME}):/{0,2})?" +   # Optional scheme:        (\1)
    +        "(?:(#{USERINFO})@)?" +       # Optional userinfo@      (\2)
    +        "(#{INTERNET_HOSTNAME})" +    # Mandatory hostname      (\3)
    +        "(?::(#{PORT}))?" +           # Optional :port          (\4)
    +        "(#{ABS_PATH})?"  +           # Optional absolute path  (\5)
    +        "(?:\\?(#{QUERY}))?" +        # Optional ?query         (\6)
    +        "(?:\\#(#{FRAGMENT}))?"  +    # Optional #fragment      (\7)
    +        '(?=\.?(?:\s|\)|\z))'         # ends only with optional dot + space or ")" 
    +                                      # or end of the string
    +
    +    SUSPICIOUS_PRECEDING_CHARACTER = '(!|\"\:|\"|\\\'|\]\()?'  # any of !, ":, ", ', ](
    +  
    +    INTERNET_URI_REGEXP = 
    +        Regexp.new(SUSPICIOUS_PRECEDING_CHARACTER + INTERNET_URI, Regexp::EXTENDED, 'N')
    +
    +  end
    +
    +  def URIChunk.pattern
    +    INTERNET_URI_REGEXP
    +  end
    +
    +  attr_reader :user, :host, :port, :path, :query, :fragment, :link_text
    +  
    +  def self.apply_to(content)
    +    content.gsub!( self.pattern ) do |matched_text|
    +      chunk = self.new($~, content)
    +      if chunk.avoid_autolinking?
    +        # do not substitute nor register the chunk
    +        matched_text
    +      else
    +        content.add_chunk(chunk)
    +        chunk.mask
    +      end
    +    end
    +  end
    +
    +  def initialize(match_data, content)
    +    super
    +    @link_text = match_data[0]
    +    @suspicious_preceding_character = match_data[1]
    +    @original_scheme, @user, @host, @port, @path, @query, @fragment = match_data[2..-1]
    +    treat_trailing_character
    +    @unmask_text = "#{link_text}"
    +  end
    +
    +  def avoid_autolinking?
    +    not @suspicious_preceding_character.nil?
    +  end
    +
    +  def treat_trailing_character
    +    # If the last character matched by URI pattern is in ! or ), this may be part of the markup,
    +    # not a URL. We should handle it as such. It is possible to do it by a regexp, but 
    +    # much easier to do programmatically
    +    last_char = @link_text[-1..-1]
    +    if last_char == ')' or last_char == '!'
    +      @trailing_punctuation = last_char
    +      @link_text.chop!
    +      [@original_scheme, @user, @host, @port, @path, @query, @fragment].compact.last.chop!
    +    else 
    +      @trailing_punctuation = nil
    +    end
    +  end
    +
    +  def scheme
    +    @original_scheme or (@user ? 'mailto' : 'http')
    +  end
    +
    +  def scheme_delimiter
    +    scheme == 'mailto' ? ':' : '://'
    +  end
    +
    +  def user_delimiter
    +     '@' unless @user.nil?
    +  end
    +
    +  def port_delimiter
    +     ':' unless @port.nil?
    +  end
    +
    +  def query_delimiter
    +     '?' unless @query.nil?
    +  end
    +
    +  def uri
    +    [scheme, scheme_delimiter, user, user_delimiter, host, port_delimiter, port, path, 
    +      query_delimiter, query].compact.join
    +  end
    +
    +end
    +
    +# uri with mandatory scheme but less restrictive hostname, like
    +# http://localhost:2500/blah.html
    +class LocalURIChunk < URIChunk
    +
    +  unless defined? LocalURIChunk::LOCAL_URI_REGEXP
    +    # hostname can be just a simple word like 'localhost'
    +    ANY_HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?"
    +    
    +    # The basic URI expression as a string
    +    # Scheme and hostname are mandatory
    +    LOCAL_URI =
    +        "(?:(#{SCHEME})://)+" +       # Mandatory scheme://     (\1)
    +        "(?:(#{USERINFO})@)?" +       # Optional userinfo@      (\2)
    +        "(#{ANY_HOSTNAME})" +         # Mandatory hostname      (\3)
    +        "(?::(#{PORT}))?" +           # Optional :port          (\4)
    +        "(#{ABS_PATH})?"  +           # Optional absolute path  (\5)
    +        "(?:\\?(#{QUERY}))?" +        # Optional ?query         (\6)
    +        "(?:\\#(#{FRAGMENT}))?" +     # Optional #fragment      (\7)
    +        '(?=\.?(?:\s|\)|\z))'         # ends only with optional dot + space or ")" 
    +                                      # or end of the string
    +  
    +    LOCAL_URI_REGEXP = Regexp.new(SUSPICIOUS_PRECEDING_CHARACTER + LOCAL_URI, Regexp::EXTENDED, 'N')
    +  end
    +
    +  def LocalURIChunk.pattern
    +    LOCAL_URI_REGEXP
    +  end
    +
    +end
    diff --git a/lib/chunks/wiki.rb b/lib/chunks/wiki.rb
    new file mode 100644
    index 00000000..617e6da5
    --- /dev/null
    +++ b/lib/chunks/wiki.rb
    @@ -0,0 +1,143 @@
    +require 'wiki_words'
    +require 'chunks/chunk'
    +require 'chunks/wiki'
    +require 'cgi'
    +
    +# Contains all the methods for finding and replacing wiki related links.
    +module WikiChunk
    +  include Chunk
    +
    +  # A wiki reference is the top-level class for anything that refers to
    +  # another wiki page.
    +  class WikiReference < Chunk::Abstract
    +
    +    # Name of the referenced page
    +    attr_reader :page_name
    +    
    +    # the referenced page
    +    def refpage
    +      @content.web.page(@page_name)
    +    end
    +  
    +  end
    +
    +  # A wiki link is the top-level class for links that refers to
    +  # another wiki page.
    +  class WikiLink < WikiReference
    + 
    +    attr_reader :link_text, :link_type
    +
    +    def initialize(match_data, content)
    +      super
    +      @link_type = :show
    +    end
    +
    +    def self.apply_to(content)
    +      content.gsub!( self.pattern ) do |matched_text|
    +        chunk = self.new($~, content)
    +        if chunk.textile_url?
    +          # do not substitute
    +          matched_text
    +        else
    +          content.add_chunk(chunk)
    +          chunk.mask
    +        end
    +      end
    +    end
    +
    +    def textile_url?
    +      not @textile_link_suffix.nil?
    +    end
    +
    +    # replace any sequence of whitespace characters with a single space
    +    def normalize_whitespace(line)
    +      line.gsub(/\s+/, ' ')
    +    end
    +  
    +  end
    +
    +  # This chunk matches a WikiWord. WikiWords can be escaped
    +  # by prepending a '\'. When this is the case, the +escaped_text+
    +  # method will return the WikiWord instead of the usual +nil+.
    +  # The +page_name+ method returns the matched WikiWord.
    +  class Word < WikiLink
    +
    +    attr_reader :escaped_text
    +    
    +    unless defined? WIKI_WORD
    +      WIKI_WORD = Regexp.new('(":)?(\\\\)?(' + WikiWords::WIKI_WORD_PATTERN + ')\b', 0, "utf-8")
    +    end
    +
    +    def self.pattern
    +      WIKI_WORD
    +    end
    +
    +    def initialize(match_data, content)
    +      super
    +      @textile_link_suffix, @escape, @page_name = match_data[1..3]
    +      if @escape 
    +        @unmask_mode = :escape
    +        @escaped_text = @page_name
    +      else
    +        @escaped_text = nil
    +      end
    +      @link_text = WikiWords.separate(@page_name)
    +      @unmask_text = (@escaped_text || @content.page_link(@page_name, @link_text, @link_type))
    +    end
    +
    +  end
    +
    +  # This chunk handles [[bracketted wiki words]] and 
    +  # [[AliasedWords|aliased wiki words]]. The first part of an
    +  # aliased wiki word must be a WikiWord. If the WikiWord
    +  # is aliased, the +link_text+ field will contain the
    +  # alias, otherwise +link_text+ will contain the entire
    +  # contents within the double brackets.
    +  #
    +  # NOTE: This chunk must be tested before WikiWord since
    +  #       a WikiWords can be a substring of a WikiLink. 
    +  class Link < WikiLink
    +    
    +    unless defined? WIKI_LINK
    +      WIKI_LINK = /(":)?\[\[\s*([^\]\s][^\]]+?)\s*\]\]/
    +      LINK_TYPE_SEPARATION = Regexp.new('^(.+):((file)|(pic))$', 0, 'utf-8')
    +      ALIAS_SEPARATION = Regexp.new('^(.+)\|(.+)$', 0, 'utf-8')
    +    end    
    +        
    +    def self.pattern() WIKI_LINK end
    +
    +    def initialize(match_data, content)
    +      super
    +      @textile_link_suffix = match_data[1]
    +      @link_text = @page_name = normalize_whitespace(match_data[2])
    +      separate_link_type
    +      separate_alias
    +      @unmask_text = @content.page_link(@page_name, @link_text, @link_type)
    +    end
    +
    +    private
    +
    +    # if link wihin the brackets has a form of [[filename:file]] or [[filename:pic]], 
    +    # this means a link to a picture or a file
    +    def separate_link_type
    +      link_type_match = LINK_TYPE_SEPARATION.match(@page_name)
    +      if link_type_match
    +        @link_text = @page_name = link_type_match[1]
    +        @link_type = link_type_match[2..3].compact[0].to_sym
    +      end
    +    end
    +
    +    # link text may be different from page name. this will look like [[actual page|link text]]
    +    def separate_alias
    +      alias_match = ALIAS_SEPARATION.match(@page_name)
    +      if alias_match
    +        @page_name = normalize_whitespace(alias_match[1])
    +        @link_text = alias_match[2]
    +      end
    +      # note that [[filename|link text:file]] is also supported
    +    end  
    +  
    +  end
    +
    +  
    +end
    diff --git a/lib/diff.rb b/lib/diff.rb
    new file mode 100644
    index 00000000..1e3fe298
    --- /dev/null
    +++ b/lib/diff.rb
    @@ -0,0 +1,316 @@
    +module HTMLDiff
    +
    +  Match = Struct.new(:start_in_old, :start_in_new, :size)
    +  class Match
    +    def end_in_old
    +      self.start_in_old + self.size
    +    end
    +    
    +    def end_in_new
    +      self.start_in_new + self.size
    +    end
    +  end
    +  
    +  Operation = Struct.new(:action, :start_in_old, :end_in_old, :start_in_new, :end_in_new)
    +
    +  class DiffBuilder
    +
    +    def initialize(old_version, new_version)
    +      @old_version, @new_version = old_version, new_version
    +      @content = []
    +    end
    +
    +    def build
    +      split_inputs_to_words
    +      index_new_words
    +      operations.each { |op| perform_operation(op) }
    +      return @content.join
    +    end
    +
    +    def split_inputs_to_words
    +      @old_words = convert_html_to_list_of_words(explode(@old_version))
    +      @new_words = convert_html_to_list_of_words(explode(@new_version))
    +    end
    +
    +    def index_new_words
    +      @word_indices = Hash.new { |h, word| h[word] = [] }
    +      @new_words.each_with_index { |word, i| @word_indices[word] << i }
    +    end
    +
    +    def operations
    +      position_in_old = position_in_new = 0
    +      operations = []
    +      
    +      matches = matching_blocks
    +      # an empty match at the end forces the loop below to handle the unmatched tails
    +      # I'm sure it can be done more gracefully, but not at 23:52
    +      matches << Match.new(@old_words.length, @new_words.length, 0)
    +      
    +      matches.each_with_index do |match, i|
    +        match_starts_at_current_position_in_old = (position_in_old == match.start_in_old)
    +        match_starts_at_current_position_in_new = (position_in_new == match.start_in_new)
    +        
    +        action_upto_match_positions = 
    +          case [match_starts_at_current_position_in_old, match_starts_at_current_position_in_new]
    +          when [false, false]
    +            :replace
    +          when [true, false]
    +            :insert
    +          when [false, true]
    +            :delete
    +          else
    +            # this happens if the first few words are same in both versions
    +            :none
    +          end
    +
    +        if action_upto_match_positions != :none
    +          operation_upto_match_positions = 
    +              Operation.new(action_upto_match_positions, 
    +                  position_in_old, match.start_in_old, 
    +                  position_in_new, match.start_in_new)
    +          operations << operation_upto_match_positions
    +        end
    +        if match.size != 0
    +          match_operation = Operation.new(:equal, 
    +              match.start_in_old, match.end_in_old, 
    +              match.start_in_new, match.end_in_new)
    +          operations << match_operation
    +        end
    +
    +        position_in_old = match.end_in_old
    +        position_in_new = match.end_in_new
    +      end
    +      
    +      operations
    +    end
    +
    +    def matching_blocks
    +      matching_blocks = []
    +      recursively_find_matching_blocks(0, @old_words.size, 0, @new_words.size, matching_blocks)
    +      matching_blocks
    +    end
    +
    +    def recursively_find_matching_blocks(start_in_old, end_in_old, start_in_new, end_in_new, matching_blocks)
    +      match = find_match(start_in_old, end_in_old, start_in_new, end_in_new)
    +      if match
    +        if start_in_old < match.start_in_old and start_in_new < match.start_in_new
    +          recursively_find_matching_blocks(
    +              start_in_old, match.start_in_old, start_in_new, match.start_in_new, matching_blocks) 
    +        end
    +        matching_blocks << match
    +        if match.end_in_old < end_in_old and match.end_in_new < end_in_new
    +          recursively_find_matching_blocks(
    +              match.end_in_old, end_in_old, match.end_in_new, end_in_new, matching_blocks)
    +        end
    +      end
    +    end
    +
    +    def find_match(start_in_old, end_in_old, start_in_new, end_in_new)
    +
    +      best_match_in_old = start_in_old
    +      best_match_in_new = start_in_new
    +      best_match_size = 0
    +      
    +      match_length_at = Hash.new { |h, index| h[index] = 0 }
    +      
    +      start_in_old.upto(end_in_old - 1) do |index_in_old|
    +
    +        new_match_length_at = Hash.new { |h, index| h[index] = 0 }
    +
    +        @word_indices[@old_words[index_in_old]].each do |index_in_new|
    +          next  if index_in_new < start_in_new
    +          break if index_in_new >= end_in_new
    +
    +          new_match_length = match_length_at[index_in_new - 1] + 1
    +          new_match_length_at[index_in_new] = new_match_length
    +
    +          if new_match_length > best_match_size
    +            best_match_in_old = index_in_old - new_match_length + 1
    +            best_match_in_new = index_in_new - new_match_length + 1
    +            best_match_size = new_match_length
    +          end
    +        end
    +        match_length_at = new_match_length_at
    +      end
    +
    +#      best_match_in_old, best_match_in_new, best_match_size = add_matching_words_left(
    +#          best_match_in_old, best_match_in_new, best_match_size, start_in_old, start_in_new)
    +#      best_match_in_old, best_match_in_new, match_size = add_matching_words_right(
    +#          best_match_in_old, best_match_in_new, best_match_size, end_in_old, end_in_new)
    +
    +      return (best_match_size != 0 ? Match.new(best_match_in_old, best_match_in_new, best_match_size) : nil)
    +    end
    +
    +    def add_matching_words_left(match_in_old, match_in_new, match_size, start_in_old, start_in_new)
    +      while match_in_old > start_in_old and 
    +            match_in_new > start_in_new and 
    +            @old_words[match_in_old - 1] == @new_words[match_in_new - 1]
    +        match_in_old -= 1
    +        match_in_new -= 1
    +        match_size += 1
    +      end
    +      [match_in_old, match_in_new, match_size]
    +    end
    +
    +    def add_matching_words_right(match_in_old, match_in_new, match_size, end_in_old, end_in_new)
    +      while match_in_old + match_size < end_in_old and 
    +            match_in_new + match_size < end_in_new and
    +            @old_words[match_in_old + match_size] == @new_words[match_in_new + match_size]
    +        match_size += 1
    +      end
    +      [match_in_old, match_in_new, match_size]
    +    end
    +    
    +    VALID_METHODS = [:replace, :insert, :delete, :equal]
    +
    +    def perform_operation(operation)
    +      @operation = operation
    +      self.send operation.action, operation
    +    end
    +
    +    def replace(operation)
    +      delete(operation, 'diffmod')
    +      insert(operation, 'diffmod')
    +    end
    +    
    +    def insert(operation, tagclass = 'diffins')
    +      insert_tag('ins', tagclass, @new_words[operation.start_in_new...operation.end_in_new])
    +    end
    +    
    +    def delete(operation, tagclass = 'diffdel')
    +       insert_tag('del', tagclass, @old_words[operation.start_in_old...operation.end_in_old])
    +    end
    +    
    +    def equal(operation)
    +      # no tags to insert, simply copy the matching words from one of the versions
    +      @content += @new_words[operation.start_in_new...operation.end_in_new]
    +    end
    +  
    +    def opening_tag?(item)
    +      item =~ %r!^\s*<[^>]+>\s*$!
    +    end
    +
    +    def closing_tag?(item)
    +      item =~ %r!^\s*]+>\s*$!
    +    end
    +
    +    def tag?(item)
    +      opening_tag?(item) or closing_tag?(item)
    +    end
    +
    +    def extract_consecutive_words(words, &condition)
    +      index_of_first_tag = nil
    +      words.each_with_index do |word, i| 
    +        if !condition.call(word)
    +          index_of_first_tag = i
    +          break
    +        end
    +      end
    +      if index_of_first_tag
    +        return words.slice!(0...index_of_first_tag)
    +      else
    +        return words.slice!(0..words.length)
    +      end
    +    end
    +
    +    # This method encloses words within a specified tag (ins or del), and adds this into @content, 
    +    # with a twist: if there are words contain tags, it actually creates multiple ins or del, 
    +    # so that they don't include any ins or del. This handles cases like
    +    # old: '

    a

    ' + # new: '

    ab

    c' + # diff result: '

    ab

    c

    ' + # this still doesn't guarantee valid HTML (hint: think about diffing a text containing ins or + # del tags), but handles correctly more cases than the earlier version. + # + # P.S.: Spare a thought for people who write HTML browsers. They live in this ... every day. + + def insert_tag(tagname, cssclass, words) + loop do + break if words.empty? + non_tags = extract_consecutive_words(words) { |word| not tag?(word) } + @content << wrap_text(non_tags.join, tagname, cssclass) unless non_tags.empty? + + break if words.empty? + @content += extract_consecutive_words(words) { |word| tag?(word) } + end + end + + def wrap_text(text, tagname, cssclass) + %(<#{tagname} class="#{cssclass}">#{text}) + end + + def explode(sequence) + sequence.is_a?(String) ? sequence.split(//) : sequence + end + + def end_of_tag?(char) + char == '>' + end + + def start_of_tag?(char) + char == '<' + end + + def whitespace?(char) + char =~ /\s/ + end + + def convert_html_to_list_of_words(x, use_brackets = false) + mode = :char + current_word = '' + words = [] + + explode(x).each do |char| + case mode + when :tag + if end_of_tag? char + current_word << (use_brackets ? ']' : '>') + words << current_word + current_word = '' + if whitespace?(char) + mode = :whitespace + else + mode = :char + end + else + current_word << char + end + when :char + if start_of_tag? char + words << current_word unless current_word.empty? + current_word = (use_brackets ? '[' : '<') + mode = :tag + elsif /\s/.match char + words << current_word unless current_word.empty? + current_word = char + mode = :whitespace + else + current_word << char + end + when :whitespace + if start_of_tag? char + words << current_word unless current_word.empty? + current_word = (use_brackets ? '[' : '<') + mode = :tag + elsif /\s/.match char + current_word << char + else + words << current_word unless current_word.empty? + current_word = char + mode = :char + end + else + raise "Unknown mode #{mode.inspect}" + end + end + words << current_word unless current_word.empty? + words + end + + end # of class Diff Builder + + def diff(a, b) + DiffBuilder.new(a, b).build + end + +end diff --git a/lib/instiki_errors.rb b/lib/instiki_errors.rb new file mode 100644 index 00000000..0737ab46 --- /dev/null +++ b/lib/instiki_errors.rb @@ -0,0 +1,15 @@ +# Model methods that want to rollback transactions gracefully +# (i.e, returning the user back to the form from which the request was posted) +# should raise Instiki::ValidationError. +# +# E.g. if a model object does +# raise "Foo: '#{foo}' is not equal to Bar: '#{bar}'" if (foo != bar) +# +# then the operation is not committed; Rails returns the user to the page +# where s/he was entering foo and bar, and the error message will be displayed +# on the page + +module Instiki + class ValidationError < StandardError + end +end \ No newline at end of file diff --git a/lib/native/win32/sqlite3.dll b/lib/native/win32/sqlite3.dll new file mode 100644 index 0000000000000000000000000000000000000000..98c50c4d23a0d29b3de032a923ff4e49229d4fa0 GIT binary patch literal 250368 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 z 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 z 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

    dG0ue&ZS4;Bm-{wC>iK--R=A)YR0x7GiUvfe?Z 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 Ws}zPjUYdvjtpr2;_4 update_references) + end + + def display_content_for_export + render :mode => :export + end + + def display_published + @display_published ||= render(:mode => :publish) + end + + def display_diff + previous_revision = @revision.page.previous_revision(@revision) + if previous_revision + rendered_previous_revision = WikiContent.new(previous_revision, @@url_generator).render! + diff(rendered_previous_revision, display_content) + else + display_content + end + end + + # Returns an array of all the WikiIncludes present in the content of this revision. + def wiki_includes + unless @wiki_includes_cache + chunks = display_content.find_chunks(Include) + @wiki_includes_cache = chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq + end + @wiki_includes_cache + end + + # Returns an array of all the WikiReferences present in the content of this revision. + def wiki_references + unless @wiki_references_cache + chunks = display_content.find_chunks(WikiChunk::WikiReference) + @wiki_references_cache = chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq + end + @wiki_references_cache + end + + # Returns an array of all the WikiWords present in the content of this revision. + def wiki_words + @wiki_words_cache ||= find_wiki_words(display_content) + end + + def find_wiki_words(rendering_result) + wiki_links = rendering_result.find_chunks(WikiChunk::WikiLink) + # Exclude backslash-escaped wiki words, such as \WikiWord, as well as links to files + # and pictures, such as [[foo.txt:file]] or [[foo.jpg:pic]] + wiki_links.delete_if { |link| link.escaped? or [:pic, :file].include?(link.link_type) } + # convert to the list of unique page names + wiki_links.map { |link| ( link.page_name ) }.uniq + end + + # Returns an array of all the WikiWords present in the content of this revision. + # that already exists as a page in the web. + def existing_pages + wiki_words.select { |wiki_word| @revision.page.web.page(wiki_word) } + end + + # Returns an array of all the WikiWords present in the content of this revision + # that *doesn't* already exists as a page in the web. + def unexisting_pages + wiki_words - existing_pages + end + + private + + def render(options = {}) + rendering_result = WikiContent.new(@revision, @@url_generator, options).render! + update_references(rendering_result) if options[:update_references] + rendering_result + end + + def update_references(rendering_result) + WikiReference.delete_all ['page_id = ?', @revision.page_id] + + references = @revision.page.wiki_references + + wiki_words = find_wiki_words(rendering_result) + # TODO it may be desirable to save links to files and pictures as WikiReference objects + # present version doesn't do it + + wiki_words.each do |referenced_name| + # Links to self are always considered linked + if referenced_name == @revision.page.name + link_type = WikiReference::LINKED_PAGE + else + link_type = WikiReference.link_type(@revision.page.web, referenced_name) + end + references.create :referenced_name => referenced_name, :link_type => link_type + end + + include_chunks = rendering_result.find_chunks(Include) + includes = include_chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq + includes.each do |included_page_name| + references.create :referenced_name => included_page_name, + :link_type => WikiReference::INCLUDED_PAGE + end + + categories = rendering_result.find_chunks(Category).map { |cat| cat.list }.flatten + categories.each do |category| + references.create :referenced_name => category, :link_type => WikiReference::CATEGORY + end + end +end diff --git a/lib/rdocsupport.rb b/lib/rdocsupport.rb new file mode 100644 index 00000000..0aaf842f --- /dev/null +++ b/lib/rdocsupport.rb @@ -0,0 +1,152 @@ +begin + require "rdoc/markup/simple_markup" + require 'rdoc/markup/simple_markup/to_html' +rescue LoadError + # use old version if available + require 'markup/simple_markup' + require 'markup/simple_markup/to_html' +end + +module RDocSupport + +# A simple +rdoc+ markup class which recognizes some additional +# formatting commands suitable for Wiki use. +class RDocMarkup < SM::SimpleMarkup + def initialize + super() + + pre = '(?:\\s|^|\\\\)' + + # links of the form + # [[ description with spaces]] + add_special(/((\\)?\[\[\S+?\s+.+?\]\])/,:TIDYLINK) + + # and external references + add_special(/((\\)?(link:|anchor:|http:|mailto:|ftp:|img:|www\.)\S+\w\/?)/, + :HYPERLINK) + + #
    + add_special(%r{(#{pre}
    )}, :BR) + + # and

    ...
    + add_html("center", :CENTER) + end + + def convert(text, handler) + super.sub(/^

    \n/, '').sub(/<\/p>$/, '') + end +end + +# Handle special hyperlinking requirments for RDoc formatted +# entries. Requires RDoc + +class HyperLinkHtml < SM::ToHtml + + # Initialize the HyperLinkHtml object. + # [path] location of the node + # [site] object representing the whole site (typically of class + # +Site+) + def initialize + super() + add_tag(:CENTER, "

    ", "
    ") + end + + # handle
    + def handle_special_BR(special) + return "<br/>" if special.text[0,1] == '\\' + special.text + end + + # We're invoked with a potential external hyperlink. + # [mailto:] just gets inserted. + # [http:] links are checked to see if they + # reference an image. If so, that image gets inserted + # using an tag. Otherwise a conventional
    + # is used. + # [img:] insert a tag + # [link:] used to insert arbitrary references + # [anchor:] used to create an anchor + def handle_special_HYPERLINK(special) + text = special.text.strip + return text[1..-1] if text[0,1] == '\\' + url = special.text.strip + if url =~ /([A-Za-z]+):(.*)/ + type = $1 + path = $2 + else + type = "http" + path = url + url = "http://#{url}" + end + + case type + when "http" + if url =~ /\.(gif|png|jpg|jpeg|bmp)$/ + "" + else + "#{url.sub(%r{^\w+:/*}, '')}" + end + when "img" + "" + when "link" + "#{path}" + when "anchor" + "" + else + "#{url.sub(%r{^\w+:/*}, '')}" + end + end + + # Here's a hyperlink where the label is different to the URL + # [[url label that may contain spaces]] + # + + def handle_special_TIDYLINK(special) + text = special.text.strip + return text[1..-1] if text[0,1] == '\\' + unless text =~ /\[\[(\S+?)\s+(.+?)\]\]/ + return text + end + url = $1 + label = $2 + label = RDocFormatter.new(label).to_html + label = label.split.select{|x| x =~ /\S/}. + map{|x| x.chomp}.join(' ') + + case url + when /link:(\S+)/ + return %{#{label}} + when /img:(\S+)/ + return %{#{label}} + when /rubytalk:(\S+)/ + return %{#{label}} + when /rubygarden:(\S+)/ + return %{#{label}} + when /c2:(\S+)/ + return %{#{label}} + when /isbn:(\S+)/ + return %{#{label}} + end + + unless url =~ /\w+?:/ + url = "http://#{url}" + end + + "#{label}" + end +end + +class RDocFormatter + def initialize(text) + @text = text + end + + def to_html + markup = RDocMarkup.new + h = HyperLinkHtml.new + markup.convert(@text, h) + end +end + +end \ No newline at end of file diff --git a/lib/redcloth.rb b/lib/redcloth.rb new file mode 100644 index 00000000..1228af6e --- /dev/null +++ b/lib/redcloth.rb @@ -0,0 +1,1130 @@ +# vim:ts=4:sw=4: +# = RedCloth - Textile and Markdown Hybrid for Ruby +# +# Homepage:: http://whytheluckystiff.net/ruby/redcloth/ +# Author:: why the lucky stiff (http://whytheluckystiff.net/) +# Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.) +# License:: BSD +# +# (see http://hobix.com/textile/ for a Textile Reference.) +# +# Based on (and also inspired by) both: +# +# PyTextile: http://diveintomark.org/projects/textile/textile.py.txt +# Textism for PHP: http://www.textism.com/tools/textile/ +# +# + +# = RedCloth +# +# RedCloth is a Ruby library for converting Textile and/or Markdown +# into HTML. You can use either format, intermingled or separately. +# You can also extend RedCloth to honor your own custom text stylings. +# +# RedCloth users are encouraged to use Textile if they are generating +# HTML and to use Markdown if others will be viewing the plain text. +# +# == What is Textile? +# +# Textile is a simple formatting style for text +# documents, loosely based on some HTML conventions. +# +# == Sample Textile Text +# +# h2. This is a title +# +# h3. This is a subhead +# +# This is a bit of paragraph. +# +# bq. This is a blockquote. +# +# = Writing Textile +# +# A Textile document consists of paragraphs. Paragraphs +# can be specially formatted by adding a small instruction +# to the beginning of the paragraph. +# +# h[n]. Header of size [n]. +# bq. Blockquote. +# # Numeric list. +# * Bulleted list. +# +# == Quick Phrase Modifiers +# +# Quick phrase modifiers are also included, to allow formatting +# of small portions of text within a paragraph. +# +# \_emphasis\_ +# \_\_italicized\_\_ +# \*strong\* +# \*\*bold\*\* +# ??citation?? +# -deleted text- +# +inserted text+ +# ^superscript^ +# ~subscript~ +# @code@ +# %(classname)span% +# +# ==notextile== (leave text alone) +# +# == Links +# +# To make a hypertext link, put the link text in "quotation +# marks" followed immediately by a colon and the URL of the link. +# +# Optional: text in (parentheses) following the link text, +# but before the closing quotation mark, will become a Title +# attribute for the link, visible as a tool tip when a cursor is above it. +# +# Example: +# +# "This is a link (This is a title) ":http://www.textism.com +# +# Will become: +# +# This is a link +# +# == Images +# +# To insert an image, put the URL for the image inside exclamation marks. +# +# Optional: text that immediately follows the URL in (parentheses) will +# be used as the Alt text for the image. Images on the web should always +# have descriptive Alt text for the benefit of readers using non-graphical +# browsers. +# +# Optional: place a colon followed by a URL immediately after the +# closing ! to make the image into a link. +# +# Example: +# +# !http://www.textism.com/common/textist.gif(Textist)! +# +# Will become: +# +# Textist +# +# With a link: +# +# !/common/textist.gif(Textist)!:http://textism.com +# +# Will become: +# +# Textist +# +# == Defining Acronyms +# +# HTML allows authors to define acronyms via the tag. The definition appears as a +# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing, +# this should be used at least once for each acronym in documents where they appear. +# +# To quickly define an acronym in Textile, place the full text in (parentheses) +# immediately following the acronym. +# +# Example: +# +# ACLU(American Civil Liberties Union) +# +# Will become: +# +# ACLU +# +# == Adding Tables +# +# In Textile, simple tables can be added by seperating each column by +# a pipe. +# +# |a|simple|table|row| +# |And|Another|table|row| +# +# Attributes are defined by style definitions in parentheses. +# +# table(border:1px solid black). +# (background:#ddd;color:red). |{}| | | | +# +# == Using RedCloth +# +# RedCloth is simply an extension of the String class, which can handle +# Textile formatting. Use it like a String and output HTML with its +# RedCloth#to_html method. +# +# doc = RedCloth.new " +# +# h2. Test document +# +# Just a simple test." +# +# puts doc.to_html +# +# By default, RedCloth uses both Textile and Markdown formatting, with +# Textile formatting taking precedence. If you want to turn off Markdown +# formatting, to boost speed and limit the processor: +# +# class RedCloth::Textile.new( str ) + +class RedCloth < String + + VERSION = '3.0.4' + DEFAULT_RULES = [:textile, :markdown] + + # + # Two accessor for setting security restrictions. + # + # This is a nice thing if you're using RedCloth for + # formatting in public places (e.g. Wikis) where you + # don't want users to abuse HTML for bad things. + # + # If +:filter_html+ is set, HTML which wasn't + # created by the Textile processor will be escaped. + # + # If +:filter_styles+ is set, it will also disable + # the style markup specifier. ('{color: red}') + # + attr_accessor :filter_html, :filter_styles + + # + # Accessor for toggling hard breaks. + # + # If +:hard_breaks+ is set, single newlines will + # be converted to HTML break tags. This is the + # default behavior for traditional RedCloth. + # + attr_accessor :hard_breaks + + # Accessor for toggling lite mode. + # + # In lite mode, block-level rules are ignored. This means + # that tables, paragraphs, lists, and such aren't available. + # Only the inline markup for bold, italics, entities and so on. + # + # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] ) + # r.to_html + # #=> "And then? She fell!" + # + attr_accessor :lite_mode + + # + # Accessor for toggling span caps. + # + # Textile places `span' tags around capitalized + # words by default, but this wreaks havoc on Wikis. + # If +:no_span_caps+ is set, this will be + # suppressed. + # + attr_accessor :no_span_caps + + # + # Establishes the markup predence. Available rules include: + # + # == Textile Rules + # + # The following textile rules can be set individually. Or add the complete + # set of rules with the single :textile rule, which supplies the rule set in + # the following precedence: + # + # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/) + # block_textile_table:: Textile table block structures + # block_textile_lists:: Textile list structures + # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.) + # inline_textile_image:: Textile inline images + # inline_textile_link:: Textile inline links + # inline_textile_span:: Textile inline spans + # glyphs_textile:: Textile entities (such as em-dashes and smart quotes) + # + # == Markdown + # + # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/) + # block_markdown_setext:: Markdown setext headers + # block_markdown_atx:: Markdown atx headers + # block_markdown_rule:: Markdown horizontal rules + # block_markdown_bq:: Markdown blockquotes + # block_markdown_lists:: Markdown lists + # inline_markdown_link:: Markdown links + attr_accessor :rules + + # Returns a new RedCloth object, based on _string_ and + # enforcing all the included _restrictions_. + # + # r = RedCloth.new( "h1. A bold man", [:filter_html] ) + # r.to_html + # #=>"

    A <b>bold</b> man

    " + # + def initialize( string, restrictions = [] ) + restrictions.each { |r| method( "#{ r }=" ).call( true ) } + super( string ) + end + + # + # Generates HTML from the Textile contents. + # + # r = RedCloth.new( "And then? She *fell*!" ) + # r.to_html( true ) + # #=>"And then? She fell!" + # + def to_html( *rules ) + rules = DEFAULT_RULES if rules.empty? + # make our working copy + text = self.dup + + @urlrefs = {} + @shelf = [] + textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists, + :block_textile_prefix, :inline_textile_image, :inline_textile_link, + :inline_textile_code, :inline_textile_span, :glyphs_textile] + markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule, + :block_markdown_bq, :block_markdown_lists, + :inline_markdown_reflink, :inline_markdown_link] + @rules = rules.collect do |rule| + case rule + when :markdown + markdown_rules + when :textile + textile_rules + else + rule + end + end.flatten + + # standard clean up + incoming_entities text + clean_white_space text + + # start processor + @pre_list = [] + rip_offtags text + no_textile text + hard_break text + unless @lite_mode + refs text + blocks text + end + inline text + smooth_offtags text + + retrieve text + + text.gsub!( /<\/?notextile>/, '' ) + text.gsub!( /x%x%/, '&' ) + clean_html text if filter_html + text.strip! + text + + end + + ####### + private + ####### + # + # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents. + # (from PyTextile) + # + TEXTILE_TAGS = + + [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230], + [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249], + [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217], + [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732], + [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]]. + + collect! do |a, b| + [a.chr, ( b.zero? and "" or "&#{ b };" )] + end + + # + # Regular expressions to convert to HTML. + # + A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/ + A_VLGN = /[\-^~]/ + C_CLAS = '(?:\([^)]+\))' + C_LNGE = '(?:\[[^\]]+\])' + C_STYL = '(?:\{[^}]+\})' + S_CSPN = '(?:\\\\\d+)' + S_RSPN = '(?:/\d+)' + A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)" + S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)" + C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)" + # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ) + PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' ) + PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' ) + PUNCT_Q = Regexp::quote( '*-_+^~%' ) + HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)' + + # Text markup tags, don't conflict with block tags + SIMPLE_HTML_TAGS = [ + 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code', + 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br', + 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo' + ] + + QTAGS = [ + ['**', 'b'], + ['*', 'strong'], + ['??', 'cite', :limit], + ['-', 'del', :limit], + ['__', 'i'], + ['_', 'em', :limit], + ['%', 'span', :limit], + ['+', 'ins', :limit], + ['^', 'sup'], + ['~', 'sub'] + ] + QTAGS.collect! do |rc, ht, rtype| + rcq = Regexp::quote rc + re = + case rtype + when :limit + /(\W) + (#{rcq}) + (#{C}) + (?::(\S+?))? + (\S.*?\S|\S) + #{rcq} + (?=\W)/x + else + /(#{rcq}) + (#{C}) + (?::(\S+))? + (\S.*?\S|\S) + #{rcq}/xm + end + [rc, ht, re, rtype] + end + + # Elements to handle + GLYPHS = [ + # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing + [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1’' ], # single closing + [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '’' ], # single closing + [ /\'/, '‘' ], # single opening + [ //, '>' ], # greater-than + # [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing + [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1”' ], # double closing + [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '”' ], # double closing + [ /"/, '“' ], # double opening + [ /\b( )?\.{3}/, '\1…' ], # ellipsis + [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '\1' ], # 3+ uppercase acronym + [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^\2\3', :no_span_caps ], # 3+ uppercase caps + [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash + [ /\s->\s/, ' → ' ], # right arrow + [ /\s-\s/, ' – ' ], # en dash + [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign + [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark + [ /\b ?[(\[]R[\])]/i, '®' ], # registered + [ /\b ?[(\[]C[\])]/i, '©' ] # copyright + ] + + H_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right', + '<>' => 'justify' + } + + V_ALGN_VALS = { + '^' => 'top', + '-' => 'middle', + '~' => 'bottom' + } + + # + # Flexible HTML escaping + # + def htmlesc( str, mode ) + str.gsub!( '&', '&' ) + str.gsub!( '"', '"' ) if mode != :NoQuotes + str.gsub!( "'", ''' ) if mode == :Quotes + str.gsub!( '<', '<') + str.gsub!( '>', '>') + end + + # Search and replace for Textile glyphs (quotes, dashes, other symbols) + def pgl( text ) + GLYPHS.each do |re, resub, tog| + next if tog and method( tog ).call + text.gsub! re, resub + end + end + + # Parses Textile attribute lists and builds an HTML attribute string + def pba( text_in, element = "" ) + + return '' unless text_in + + style = [] + text = text_in.dup + if element == 'td' + colspan = $1 if text =~ /\\(\d+)/ + rowspan = $1 if text =~ /\/(\d+)/ + style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN + end + + style << "#{ $1 };" if not filter_styles and + text.sub!( /\{([^}]*)\}/, '' ) + + lang = $1 if + text.sub!( /\[([^)]+?)\]/, '' ) + + cls = $1 if + text.sub!( /\(([^()]+?)\)/, '' ) + + style << "padding-left:#{ $1.length }em;" if + text.sub!( /([(]+)/, '' ) + + style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' ) + + style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN + + cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/ + + atts = '' + atts << " style=\"#{ style.join }\"" unless style.empty? + atts << " class=\"#{ cls }\"" unless cls.to_s.empty? + atts << " lang=\"#{ lang }\"" if lang + atts << " id=\"#{ id }\"" if id + atts << " colspan=\"#{ colspan }\"" if colspan + atts << " rowspan=\"#{ rowspan }\"" if rowspan + + atts + end + + TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m + + # Parses a Textile table block, building HTML from the result. + def block_textile_table( text ) + text.gsub!( TABLE_RE ) do |matches| + + tatts, fullrow = $~[1..2] + tatts = pba( tatts, 'table' ) + tatts = shelve( tatts ) if tatts + rows = [] + + fullrow. + split( /\|$/m ). + delete_if { |x| x.empty? }. + each do |row| + + ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m + + cells = [] + row.split( '|' ).each do |cell| + ctyp = 'd' + ctyp = 'h' if cell =~ /^_/ + + catts = '' + catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/ + + unless cell.strip.empty? + catts = shelve( catts ) if catts + cells << "\t\t\t#{ cell }" + end + end + ratts = shelve( ratts ) if ratts + rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t" + end + "\t\n#{ rows.join( "\n" ) }\n\t\n\n" + end + end + + LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m + LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m + + # Parses Textile lists and generates HTML + def block_textile_lists( text ) + text.gsub!( LISTS_RE ) do |match| + lines = match.split( /\n/ ) + last_line = -1 + depth = [] + lines.each_with_index do |line, line_id| + if line =~ LISTS_CONTENT_RE + tl,atts,content = $~[1..3] + if depth.last + if depth.last.length > tl.length + (depth.length - 1).downto(0) do |i| + break if depth[i].length == tl.length + lines[line_id - 1] << "\n\t\n\t" + depth.pop + end + end + if depth.last and depth.last.length == tl.length + lines[line_id - 1] << '' + end + end + unless depth.last == tl + depth << tl + atts = pba( atts ) + atts = shelve( atts ) if atts + lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t
  • #{ content }" + else + lines[line_id] = "\t\t
  • #{ content }" + end + last_line = line_id + + else + last_line = line_id + end + if line_id - last_line > 1 or line_id == lines.length - 1 + depth.delete_if do |v| + lines[last_line] << "
  • \n\t" + end + end + end + lines.join( "\n" ) + end + end + + CODE_RE = /(\W) + @ + (?:\|(\w+?)\|)? + (.+?) + @ + (?=\W)/x + + def inline_textile_code( text ) + text.gsub!( CODE_RE ) do |m| + before,lang,code,after = $~[1..4] + lang = " lang=\"#{ lang }\"" if lang + rip_offtags( "#{ before }#{ code }
    #{ after }" ) + end + end + + def lT( text ) + text =~ /\#$/ ? 'o' : 'u' + end + + def hard_break( text ) + text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
    " ) if hard_breaks + end + + BLOCKS_GROUP_RE = /\n{2,}(?! )/m + + def blocks( text, deep_code = false ) + text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk| + plain = blk !~ /\A[#*> ]/ + + # skip blocks that are complex HTML + if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1 + blk + else + # search for indentation levels + blk.strip! + if blk.empty? + blk + else + code_blk = nil + blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk| + flush_left iblk + blocks iblk, plain + iblk.gsub( /^(\S)/, "\t\\1" ) + if plain + code_blk = iblk; "" + else + iblk + end + end + + block_applied = 0 + @rules.each do |rule_name| + block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) ) + end + if block_applied.zero? + if deep_code + blk = "\t
    #{ blk }
    " + else + blk = "\t

    #{ blk }

    " + end + end + # hard_break blk + blk + "\n#{ code_blk }" + end + end + + end.join( "\n\n" ) ) + end + + def textile_bq( tag, atts, cite, content ) + cite, cite_title = check_refs( cite ) + cite = " cite=\"#{ cite }\"" if cite + atts = shelve( atts ) if atts + "\t\n\t\t#{ content }

    \n\t" + end + + def textile_p( tag, atts, cite, content ) + atts = shelve( atts ) if atts + "\t<#{ tag }#{ atts }>#{ content }" + end + + alias textile_h1 textile_p + alias textile_h2 textile_p + alias textile_h3 textile_p + alias textile_h4 textile_p + alias textile_h5 textile_p + alias textile_h6 textile_p + + def textile_fn_( tag, num, atts, cite, content ) + atts << " id=\"fn#{ num }\"" + content = "#{ num } #{ content }" + atts = shelve( atts ) if atts + "\t#{ content }

    " + end + + BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m + + def block_textile_prefix( text ) + if text =~ BLOCK_RE + tag,tagpre,num,atts,cite,content = $~[1..6] + atts = pba( atts ) + + # pass to prefix handler + if respond_to? "textile_#{ tag }", true + text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) ) + elsif respond_to? "textile_#{ tagpre }_", true + text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) ) + end + end + end + + SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m + def block_markdown_setext( text ) + if text =~ SETEXT_RE + tag = if $2 == "="; "h1"; else; "h2"; end + blk, cont = "<#{ tag }>#{ $1 }", $' + blocks cont + text.replace( blk + cont ) + end + end + + ATX_RE = /\A(\#{1,6}) # $1 = string of #'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #'s (not counted) + $/x + def block_markdown_atx( text ) + if text =~ ATX_RE + tag = "h#{ $1.length }" + blk, cont = "<#{ tag }>#{ $2 }\n\n", $' + blocks cont + text.replace( blk + cont ) + end + end + + MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m + + def block_markdown_bq( text ) + text.gsub!( MARKDOWN_BQ_RE ) do |blk| + blk.gsub!( /^ *> ?/, '' ) + flush_left blk + blocks blk + blk.gsub!( /^(\S)/, "\t\\1" ) + "
    \n#{ blk }\n
    \n\n" + end + end + + MARKDOWN_RULE_RE = /^(#{ + ['*', '-', '_'].collect { |ch| '( ?' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' ) + })$/ + + def block_markdown_rule( text ) + text.gsub!( MARKDOWN_RULE_RE ) do |blk| + "
    " + end + end + + # XXX TODO XXX + def block_markdown_lists( text ) + end + + def inline_textile_span( text ) + QTAGS.each do |qtag_rc, ht, qtag_re, rtype| + text.gsub!( qtag_re ) do |m| + + case rtype + when :limit + sta,qtag,atts,cite,content = $~[1..5] + else + qtag,atts,cite,content = $~[1..4] + sta = '' + end + atts = pba( atts ) + atts << " cite=\"#{ cite }\"" if cite + atts = shelve( atts ) if atts + + "#{ sta }<#{ ht }#{ atts }>#{ content }" + + end + end + end + + LINK_RE = / + ([\s\[{(]|[#{PUNCT}])? # $pre + " # start + (#{C}) # $atts + ([^"]+?) # $text + \s? + (?:\(([^)]+?)\)(?="))? # $title + ": + (\S+?) # $url + (\/)? # $slash + ([^\w\/;]*?) # $post + (?=<|\s|$) + /x + + def inline_textile_link( text ) + text.gsub!( LINK_RE ) do |m| + pre,atts,text,title,url,slash,post = $~[1..7] + + url, url_title = check_refs( url ) + title ||= url_title + + atts = pba( atts ) + atts = " href=\"#{ url }#{ slash }\"#{ atts }" + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) if atts + + "#{ pre }#{ text }#{ post }" + end + end + + MARKDOWN_REFLINK_RE = / + \[([^\[\]]+)\] # $text + [ ]? # opt. space + (?:\n[ ]*)? # one optional newline followed by spaces + \[(.*?)\] # $id + /x + + def inline_markdown_reflink( text ) + text.gsub!( MARKDOWN_REFLINK_RE ) do |m| + text, id = $~[1..2] + + if id.empty? + url, title = check_refs( text ) + else + url, title = check_refs( id ) + end + + atts = " href=\"#{ url }\"" + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) + + "#{ text }" + end + end + + MARKDOWN_LINK_RE = / + \[([^\[\]]+)\] # $text + \( # open paren + [ \t]* # opt space + ? # $href + [ \t]* # opt space + (?: # whole title + (['"]) # $quote + (.*?) # $title + \3 # matching quote + )? # title is optional + \) + /x + + def inline_markdown_link( text ) + text.gsub!( MARKDOWN_LINK_RE ) do |m| + text, url, quote, title = $~[1..4] + + atts = " href=\"#{ url }\"" + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) + + "#{ text }" + end + end + + TEXTILE_REFS_RE = /(^ *)\[([^\n]+?)\](#{HYPERLINK})(?=\s|$)/ + MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m + + def refs( text ) + @rules.each do |rule_name| + method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/ + end + end + + def refs_textile( text ) + text.gsub!( TEXTILE_REFS_RE ) do |m| + flag, url = $~[2..3] + @urlrefs[flag.downcase] = [url, nil] + nil + end + end + + def refs_markdown( text ) + text.gsub!( MARKDOWN_REFS_RE ) do |m| + flag, url = $~[2..3] + title = $~[6] + @urlrefs[flag.downcase] = [url, title] + nil + end + end + + def check_refs( text ) + ret = @urlrefs[text.downcase] if text + ret || [text, nil] + end + + IMAGE_RE = / + (

    |.|^) # start of line? + \! # opening + (\<|\=|\>)? # optional alignment atts + (#{C}) # optional style,class atts + (?:\. )? # optional dot-space + ([^\s(!]+?) # presume this is the src + \s? # optional space + (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title + \! # closing + (?::#{ HYPERLINK })? # optional href + /x + + def inline_textile_image( text ) + text.gsub!( IMAGE_RE ) do |m| + stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8] + atts = pba( atts ) + atts = " src=\"#{ url }\"#{ atts }" + atts << " title=\"#{ title }\"" if title + atts << " alt=\"#{ title }\"" + # size = @getimagesize($url); + # if($size) $atts.= " $size[3]"; + + href, alt_title = check_refs( href ) if href + url, url_title = check_refs( url ) + + out = '' + out << "" if href + out << "" + out << "#{ href_a1 }#{ href_a2 }" if href + + if algn + algn = h_align( algn ) + if stln == "

    " + out = "

    #{ out }" + else + out = "#{ stln }

    #{ out }
    " + end + else + out = stln + out + end + + out + end + end + + def shelve( val ) + @shelf << val + " :redsh##{ @shelf.length }:" + end + + def retrieve( text ) + @shelf.each_with_index do |r, i| + text.gsub!( " :redsh##{ i + 1 }:", r ) + end + end + + def incoming_entities( text ) + ## turn any incoming ampersands into a dummy character for now. + ## This uses a negative lookahead for alphanumerics followed by a semicolon, + ## implying an incoming html entity, to be skipped + + text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" ) + end + + def no_textile( text ) + text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/, + '\1\2\3' ) + text.gsub!( /^ *==([^=]+.*?)==/m, + '\1\2\3' ) + end + + def clean_white_space( text ) + # normalize line breaks + text.gsub!( /\r\n/, "\n" ) + text.gsub!( /\r/, "\n" ) + text.gsub!( /\t/, ' ' ) + text.gsub!( /^ +$/, '' ) + text.gsub!( /\n{3,}/, "\n\n" ) + text.gsub!( /"$/, "\" " ) + + # if entire document is indented, flush + # to the left side + flush_left text + end + + def flush_left( text ) + indt = 0 + if text =~ /^ / + while text !~ /^ {#{indt}}\S/ + indt += 1 + end unless text.empty? + if indt.nonzero? + text.gsub!( /^ {#{indt}}/, '' ) + end + end + end + + def footnote_ref( text ) + text.gsub!( /\b\[([0-9]+?)\](\s)?/, + '\1\2' ) + end + + OFFTAGS = /(code|pre|kbd|notextile)/ + OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi + OFFTAG_OPEN = /<#{ OFFTAGS }/ + OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/ + HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m + ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m + + def glyphs_textile( text, level = 0 ) + if text !~ HASTAG_MATCH + pgl text + footnote_ref text + else + codepre = 0 + text.gsub!( ALLTAG_MATCH ) do |line| + ## matches are off if we're between ,
     etc.
    +                if $1
    +                    if line =~ OFFTAG_OPEN
    +                        codepre += 1
    +                    elsif line =~ OFFTAG_CLOSE
    +                        codepre -= 1
    +                        codepre = 0 if codepre < 0
    +                    end 
    +                elsif codepre.zero?
    +                    glyphs_textile( line, level + 1 )
    +                else
    +                    htmlesc( line, :NoQuotes )
    +                end
    +                # p [level, codepre, line]
    +
    +                line
    +            end
    +        end
    +    end
    +
    +    def rip_offtags( text )
    +        if text =~ /<.*>/
    +            ## strip and encode 
     content
    +            codepre, used_offtags = 0, {}
    +            text.gsub!( OFFTAG_MATCH ) do |line|
    +                if $3
    +                    offtag, aftertag = $4, $5
    +                    codepre += 1
    +                    used_offtags[offtag] = true
    +                    if codepre - used_offtags.length > 0
    +                        htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
    +                        @pre_list.last << line
    +                        line = ""
    +                    else
    +                        htmlesc( aftertag, :NoQuotes ) if aftertag and not used_offtags['notextile']
    +                        line = ""
    +                        @pre_list << "#{ $3 }#{ aftertag }"
    +                    end
    +                elsif $1 and codepre > 0
    +                    if codepre - used_offtags.length > 0
    +                        htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
    +                        @pre_list.last << line
    +                        line = ""
    +                    end
    +                    codepre -= 1 unless codepre.zero?
    +                    used_offtags = {} if codepre.zero?
    +                end 
    +                line
    +            end
    +        end
    +        text
    +    end
    +
    +    def smooth_offtags( text )
    +        unless @pre_list.empty?
    +            ## replace 
     content
    +            text.gsub!( // ) { @pre_list[$1.to_i] }
    +        end
    +    end
    +
    +    def inline( text ) 
    +        [/^inline_/, /^glyphs_/].each do |meth_re|
    +            @rules.each do |rule_name|
    +                method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
    +            end
    +        end
    +    end
    +
    +    def h_align( text ) 
    +        H_ALGN_VALS[text]
    +    end
    +
    +    def v_align( text ) 
    +        V_ALGN_VALS[text]
    +    end
    +
    +    def textile_popup_help( name, windowW, windowH )
    +        ' ' + name + '
    ' + end + + # HTML cleansing stuff + BASIC_TAGS = { + 'a' => ['href', 'title'], + 'img' => ['src', 'alt', 'title'], + 'br' => [], + 'i' => nil, + 'u' => nil, + 'b' => nil, + 'pre' => nil, + 'kbd' => nil, + 'code' => ['lang'], + 'cite' => nil, + 'strong' => nil, + 'em' => nil, + 'ins' => nil, + 'sup' => nil, + 'sub' => nil, + 'del' => nil, + 'table' => nil, + 'tr' => nil, + 'td' => ['colspan', 'rowspan'], + 'th' => nil, + 'ol' => nil, + 'ul' => nil, + 'li' => nil, + 'p' => nil, + 'h1' => nil, + 'h2' => nil, + 'h3' => nil, + 'h4' => nil, + 'h5' => nil, + 'h6' => nil, + 'blockquote' => ['cite'] + } + + def clean_html( text, tags = BASIC_TAGS ) + text.gsub!( /]*)>/ ) do + raw = $~ + tag = raw[2].downcase + if tags.has_key? tag + pcs = [tag] + tags[tag].each do |prop| + ['"', "'", ''].each do |q| + q2 = ( q != '' ? q : '\s' ) + if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i + attrv = $1 + next if prop == 'src' and attrv =~ %r{^(?!http)\w+:} + pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\"" + break + end + end + end if tags[tag] + "<#{raw[1]}#{pcs.join " "}>" + else + " " + end + end + end +end + diff --git a/lib/redcloth_for_tex.rb b/lib/redcloth_for_tex.rb new file mode 100644 index 00000000..6bfb6a0f --- /dev/null +++ b/lib/redcloth_for_tex.rb @@ -0,0 +1,736 @@ +# This is RedCloth (http://www.whytheluckystiff.net/ruby/redcloth/) +# converted by David Heinemeier Hansson to emit Tex + +class String + # Flexible HTML escaping + def texesc!( mode ) + gsub!( '&', '\\\\&' ) + gsub!( '%', '\%' ) + gsub!( '$', '\$' ) + gsub!( '~', '$\sim$' ) + end +end + + +def table_of_contents(text, pages) + text.gsub( /^([#*]+? .*?)$(?![^#*])/m ) do |match| + lines = match.split( /\n/ ) + last_line = -1 + depth = [] + lines.each_with_index do |line, line_id| + if line =~ /^([#*]+) (.*)$/m + tl,content = $~[1..2] + content.gsub! /[\[\]]/, "" + content.strip! + + if depth.last + if depth.last.length > tl.length + (depth.length - 1).downto(0) do |i| + break if depth[i].length == tl.length + lines[line_id - 1] << "" # "\n\t\\end{#{ lT( depth[i] ) }}\n\t" + depth.pop + end + end + if !depth.last.nil? && !tl.length.nil? && depth.last.length == tl.length + lines[line_id - 1] << '' + end + end + + depth << tl unless depth.last == tl + + subsection_depth = [depth.length - 1, 2].min + + lines[line_id] = "\n\\#{ "sub" * subsection_depth }section{#{ content }}" + lines[line_id] += "\n#{pages[content]}" if pages.keys.include?(content) + + lines[line_id] = "\\pagebreak\n#{lines[line_id]}" if subsection_depth == 0 + + last_line = line_id + + elsif line =~ /^\s+\S/ + last_line = line_id + elsif line_id - last_line < 2 and line =~ /^\S/ + last_line = line_id + end + if line_id - last_line > 1 or line_id == lines.length - 1 + depth.delete_if do |v| + lines[last_line] << "" # "\n\t\\end{#{ lT( v ) }}" + end + end + end + lines.join( "\n" ) + end +end + +class RedClothForTex < String + + VERSION = '2.0.7' + + # + # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents. + # (from PyTextile) + # + TEXTILE_TAGS = + + [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230], + [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249], + [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217], + [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732], + [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]]. + + collect! do |a, b| + [a.chr, ( b.zero? and "" or "&#{ b };" )] + end + + # + # Regular expressions to convert to HTML. + # + A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/ + A_VLGN = /[\-^~]/ + C_CLAS = '(?:\([^)]+\))' + C_LNGE = '(?:\[[^\]]+\])' + C_STYL = '(?:\{[^}]+\})' + S_CSPN = '(?:\\\\\d+)' + S_RSPN = '(?:/\d+)' + A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)" + S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)" + C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)" + # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ) + PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' ) + HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(\s|$)' + + GLYPHS = [ + # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing + [ /([^\s\[{(>])\'/, '\1’' ], # single closing + [ /\'(?=\s|s\b|[#{PUNCT}])/, '’' ], # single closing + [ /\'/, '‘' ], # single opening + # [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing + [ /([^\s\[{(>])"/, '\1”' ], # double closing + [ /"(?=\s|[#{PUNCT}])/, '”' ], # double closing + [ /"/, '“' ], # double opening + [ /\b( )?\.{3}/, '\1…' ], # ellipsis + [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '\1' ], # 3+ uppercase acronym + [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]{2,})([^\2\3' ], # 3+ uppercase caps + [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash + [ /\s->\s/, ' → ' ], # en dash + [ /\s-\s/, ' – ' ], # en dash + [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign + [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark + [ /\b ?[(\[]R[\])]/i, '®' ], # registered + [ /\b ?[(\[]C[\])]/i, '©' ] # copyright + ] + + I_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right' + } + + H_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right', + '<>' => 'justify' + } + + V_ALGN_VALS = { + '^' => 'top', + '-' => 'middle', + '~' => 'bottom' + } + + QTAGS = [ + ['**', 'bf'], + ['*', 'bf'], + ['??', 'cite'], + ['-', 'del'], + ['__', 'underline'], + ['_', 'em'], + ['%', 'span'], + ['+', 'ins'], + ['^', 'sup'], + ['~', 'sub'] + ] + + def self.available? + if not defined? @@available + begin + @@available = system "pdflatex -version" + rescue Errno::ENOENT + @@available = false + end + end + @@available + end + + # + # Two accessor for setting security restrictions. + # + # This is a nice thing if you're using RedCloth for + # formatting in public places (e.g. Wikis) where you + # don't want users to abuse HTML for bad things. + # + # If +:filter_html+ is set, HTML which wasn't + # created by the Textile processor will be escaped. + # + # If +:filter_styles+ is set, it will also disable + # the style markup specifier. ('{color: red}') + # + attr_accessor :filter_html, :filter_styles + + # + # Accessor for toggling line folding. + # + # If +:fold_lines+ is set, single newlines will + # not be converted to break tags. + # + attr_accessor :fold_lines + + def initialize( string, restrictions = [] ) + restrictions.each { |r| method( "#{ r }=" ).call( true ) } + super( string ) + end + + # + # Generate tex. + # + def to_tex( lite = false ) + + # make our working copy + text = self.dup + + @urlrefs = {} + @shelf = [] + + # incoming_entities text + fix_entities text + clean_white_space text + + get_refs text + + no_textile text + + unless lite + lists text + table text + end + + glyphs text + + unless lite + fold text + block text + end + + retrieve text + encode_entities text + + text.gsub!(/\[\[(.*?)\]\]/, "\\1") + text.gsub!(/_/, "\\_") + text.gsub!( /<\/?notextile>/, '' ) + # text.gsub!( /x%x%/, '&' ) + # text.gsub!( /
    /, "
    \n" ) + text.strip! + text + + end + + def pgl( text ) + GLYPHS.each do |re, resub| + text.gsub! re, resub + end + end + + def pba( text_in, element = "" ) + + return '' unless text_in + + style = [] + text = text_in.dup + if element == 'td' + colspan = $1 if text =~ /\\(\d+)/ + rowspan = $1 if text =~ /\/(\d+)/ + style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN + end + + style << "#{ $1 };" if not @filter_styles and + text.sub!( /\{([^}]*)\}/, '' ) + + lang = $1 if + text.sub!( /\[([^)]+?)\]/, '' ) + + cls = $1 if + text.sub!( /\(([^()]+?)\)/, '' ) + + style << "padding-left:#{ $1.length }em;" if + text.sub!( /([(]+)/, '' ) + + style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' ) + + style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN + + cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/ + + atts = '' + atts << " style=\"#{ style.join }\"" unless style.empty? + atts << " class=\"#{ cls }\"" unless cls.to_s.empty? + atts << " lang=\"#{ lang }\"" if lang + atts << " id=\"#{ id }\"" if id + atts << " colspan=\"#{ colspan }\"" if colspan + atts << " rowspan=\"#{ rowspan }\"" if rowspan + + atts + end + + def table( text ) + text << "\n\n" + text.gsub!( /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)\n\n/m ) do |matches| + + tatts, fullrow = $~[1..2] + tatts = pba( tatts, 'table' ) + rows = [] + + fullrow. + split( /\|$/m ). + delete_if { |x| x.empty? }. + each do |row| + + ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m + + cells = [] + row.split( '|' ).each do |cell| + ctyp = 'd' + ctyp = 'h' if cell =~ /^_/ + + catts = '' + catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. )(.*)/ + + unless cell.strip.empty? + cells << "\t\t\t#{ cell }" + end + end + rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t" + end + "\t\n#{ rows.join( "\n" ) }\n\t\n\n" + end + end + + def lists( text ) + text.gsub!( /^([#*]+?#{C} .*?)$(?![^#*])/m ) do |match| + lines = match.split( /\n/ ) + last_line = -1 + depth = [] + lines.each_with_index do |line, line_id| + if line =~ /^([#*]+)(#{A}#{C}) (.*)$/m + tl,atts,content = $~[1..3] + if depth.last + if depth.last.length > tl.length + (depth.length - 1).downto(0) do |i| + break if depth[i].length == tl.length + lines[line_id - 1] << "\n\t\\end{#{ lT( depth[i] ) }}\n\t" + depth.pop + end + end + if !depth.last.nil? && !tl.length.nil? && depth.last.length == tl.length + lines[line_id - 1] << '' + end + end + unless depth.last == tl + depth << tl + atts = pba( atts ) + lines[line_id] = "\t\\begin{#{ lT(tl) }}\n\t\\item #{ content }" + else + lines[line_id] = "\t\t\\item #{ content }" + end + last_line = line_id + + elsif line =~ /^\s+\S/ + last_line = line_id + elsif line_id - last_line < 2 and line =~ /^\S/ + last_line = line_id + end + if line_id - last_line > 1 or line_id == lines.length - 1 + depth.delete_if do |v| + lines[last_line] << "\n\t\\end{#{ lT( v ) }}" + end + end + end + lines.join( "\n" ) + end + end + + def lT( text ) + text =~ /\#$/ ? 'enumerate' : 'itemize' + end + + def fold( text ) + text.gsub!( /(.+)\n(?![#*\s|])/, "\\1\\\\\\\\" ) + # text.gsub!( /(.+)\n(?![#*\s|])/, "\\1#{ @fold_lines ? ' ' : '
    ' }" ) + end + + def block( text ) + pre = false + find = ['bq','h[1-6]','fn\d+'] + + regexp_cue = [] + + lines = text.split( /\n/ ) + [' '] + new_text = + lines.collect do |line| + pre = true if line =~ /<(pre|notextile)>/i + find.each do |tag| + line.gsub!( /^(#{ tag })(#{A}#{C})\.(?::(\S+))? (.*)$/ ) do |m| + tag,atts,cite,content = $~[1..4] + + atts = pba( atts ) + + if tag =~ /fn(\d+)/ + # tag = 'p'; + # atts << " id=\"fn#{ $1 }\"" + regexp_cue << [ /footnote\{#{$1}}/, "footnote{#{content}}" ] + content = "" + end + + if tag =~ /h([1-6])/ + section_type = "sub" * [$1.to_i - 1, 2].min + start = "\t\\#{section_type}section*{" + tend = "}" + end + + if tag == "bq" + cite = check_refs( cite ) + cite = " cite=\"#{ cite }\"" if cite + start = "\t\\begin{quotation}\n\\noindent {\\em "; + tend = "}\n\t\\end{quotation}"; + end + + "#{ start }#{ content }#{ tend }" + end unless pre + end + + #line.gsub!( /^(?!\t|<\/?pre|<\/?notextile|<\/?code|$| )(.*)/, "\t

    \\1

    " ) + + #line.gsub!( "
    ", "\n" ) if pre + # pre = false if line =~ /<\/(pre|notextile)>/i + + line + end.join( "\n" ) + text.replace( new_text ) + regexp_cue.each { |pair| text.gsub!(pair.first, pair.last) } + end + + def span( text ) + QTAGS.each do |tt, ht| + ttr = Regexp::quote( tt ) + text.gsub!( + + /(^|\s|\>|[#{PUNCT}{(\[]) + #{ttr} + (#{C}) + (?::(\S+?))? + ([^\s#{ttr}]+?(?:[^\n]|\n(?!\n))*?) + ([#{PUNCT}]*?) + #{ttr} + (?=[\])}]|[#{PUNCT}]+?|<|\s|$)/xm + + ) do |m| + + start,atts,cite,content,tend = $~[1..5] + atts = pba( atts ) + atts << " cite=\"#{ cite }\"" if cite + + "#{ start }{\\#{ ht } #{ content }#{ tend }}" + + end + end + end + + def links( text ) + text.gsub!( / + ([\s\[{(]|[#{PUNCT}])? # $pre + " # start + (#{C}) # $atts + ([^"]+?) # $text + \s? + (?:\(([^)]+?)\)(?="))? # $title + ": + (\S+?) # $url + (\/)? # $slash + ([^\w\/;]*?) # $post + (?=\s|$) + /x ) do |m| + pre,atts,text,title,url,slash,post = $~[1..7] + + url.gsub!(/(\\)(.)/, '\2') + url = check_refs( url ) + + atts = pba( atts ) + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) if atts + + "#{ pre }\\textit{#{ text }} \\footnote{\\texttt{\\textless #{ url }#{ slash }" + + "\\textgreater}#{ post }}" + end + end + + def get_refs( text ) + text.gsub!( /(^|\s)\[(.+?)\]((?:http:\/\/|javascript:|ftp:\/\/|\/)\S+?)(?=\s|$)/ ) do |m| + flag, url = $~[1..2] + @urlrefs[flag] = url + end + end + + def check_refs( text ) + @urlrefs[text] || text + end + + def image( text ) + text.gsub!( / + \! # opening + (\<|\=|\>)? # optional alignment atts + (#{C}) # optional style,class atts + (?:\. )? # optional dot-space + ([^\s(!]+?) # presume this is the src + \s? # optional space + (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title + \! # closing + (?::#{ HYPERLINK })? # optional href + /x ) do |m| + algn,atts,url,title,href,href_a1,href_a2 = $~[1..7] + atts = pba( atts ) + atts << " align=\"#{ i_align( algn ) }\"" if algn + atts << " title=\"#{ title }\"" if title + atts << " alt=\"#{ title }\"" + # size = @getimagesize($url); + # if($size) $atts.= " $size[3]"; + + href = check_refs( href ) if href + url = check_refs( url ) + + out = '' + out << "" if href + out << "" + out << "#{ href_a1 }#{ href_a2 }" if href + + out + end + end + + def code( text ) + text.gsub!( / + (?:^|([\s\(\[{])) # 1 open bracket? + @ # opening + (?:\|(\w+?)\|)? # 2 language + (\S(?:[^\n]|\n(?!\n))*?) # 3 code + @ # closing + (?:$|([\]})])| + (?=[#{PUNCT}]{1,2}| + \s)) # 4 closing bracket? + /x ) do |m| + before,lang,code,after = $~[1..4] + lang = " language=\"#{ lang }\"" if lang + "#{ before }#{ code }
    #{ after }" + end + end + + def shelve( val ) + @shelf << val + " <#{ @shelf.length }>" + end + + def retrieve( text ) + @shelf.each_with_index do |r, i| + text.gsub!( " <#{ i + 1 }>", r ) + end + end + + def incoming_entities( text ) + ## turn any incoming ampersands into a dummy character for now. + ## This uses a negative lookahead for alphanumerics followed by a semicolon, + ## implying an incoming html entity, to be skipped + + text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" ) + end + + def encode_entities( text ) + ## Convert high and low ascii to entities. + # if $-K == "UTF-8" + # encode_high( text ) + # else + text.texesc!( :NoQuotes ) + # end + end + + def fix_entities( text ) + ## de-entify any remaining angle brackets or ampersands + text.gsub!( "\&", "&" ) + text.gsub!( "\%", "%" ) + end + + def clean_white_space( text ) + text.gsub!( /\r\n/, "\n" ) + text.gsub!( /\t/, '' ) + text.gsub!( /\n{3,}/, "\n\n" ) + text.gsub!( /\n *\n/, "\n\n" ) + text.gsub!( /"$/, "\" " ) + end + + def no_textile( text ) + text.gsub!( /(^|\s)==(.*?)==(\s|$)?/, + '\1\2\3' ) + end + + def footnote_ref( text ) + text.gsub!( /\[([0-9]+?)\](\s)?/, + '\footnote{\1}\2') + #'\1\2' ) + end + + def inline( text ) + image text + links text + code text + span text + end + + def glyphs_deep( text ) + codepre = 0 + offtags = /(?:code|pre|kbd|notextile)/ + if text !~ /<.*>/ + # pgl text + footnote_ref text + else + used_offtags = {} + text.gsub!( /(?:[^<].*?(?=<[^\n]*?>|$)|<[^\n]*?>+)/m ) do |line| + tagline = ( line =~ /^<.*>/ ) + + ## matches are off if we're between ,
     etc.
    +          if tagline
    +            if line =~ /<(#{ offtags })>/i
    +              codepre += 1
    +              used_offtags[$1] = true
    +              line.texesc!( :NoQuotes ) if codepre - used_offtags.length > 0
    +            elsif line =~ /<\/(#{ offtags })>/i
    +              line.texesc!( :NoQuotes ) if codepre - used_offtags.length > 0
    +              codepre -= 1 unless codepre.zero?
    +              used_offtags = {} if codepre.zero?
    +            elsif @filter_html or codepre > 0
    +              line.texesc!( :NoQuotes )
    +              ## line.gsub!( /<(\/?#{ offtags })>/, '<\1>' )
    +            end 
    +            ## do htmlspecial if between 
    +          elsif codepre > 0
    +            line.texesc!( :NoQuotes )
    +            ## line.gsub!( /<(\/?#{ offtags })>/, '<\1>' )
    +          elsif not tagline
    +            inline line
    +            glyphs_deep line
    +          end
    +          
    +          line
    +        end
    +      end
    +    end
    +    
    +    def glyphs( text ) 
    +      text.gsub!( /"\z/, "\" " )
    +      ## if no html, do a simple search and replace...
    +      if text !~ /<.*>/
    +        inline text
    +      end
    +      glyphs_deep text
    +    end
    +    
    +    def i_align( text )
    +      I_ALGN_VALS[text]
    +    end
    +    
    +    def h_align( text ) 
    +      H_ALGN_VALS[text]
    +    end
    +    
    +    def v_align( text ) 
    +      V_ALGN_VALS[text]
    +    end
    +    
    +    def encode_high( text )
    +      ## mb_encode_numericentity($text, $cmap, $charset);
    +    end
    +    
    +    def decode_high( text )
    +      ## mb_decode_numericentity($text, $cmap, $charset);
    +    end
    +    
    +    def textile_popup_help( name, helpvar, windowW, windowH )
    +        ' ' + name + '
    ' + end + + CMAP = [ + 160, 255, 0, 0xffff, + 402, 402, 0, 0xffff, + 913, 929, 0, 0xffff, + 931, 937, 0, 0xffff, + 945, 969, 0, 0xffff, + 977, 978, 0, 0xffff, + 982, 982, 0, 0xffff, + 8226, 8226, 0, 0xffff, + 8230, 8230, 0, 0xffff, + 8242, 8243, 0, 0xffff, + 8254, 8254, 0, 0xffff, + 8260, 8260, 0, 0xffff, + 8465, 8465, 0, 0xffff, + 8472, 8472, 0, 0xffff, + 8476, 8476, 0, 0xffff, + 8482, 8482, 0, 0xffff, + 8501, 8501, 0, 0xffff, + 8592, 8596, 0, 0xffff, + 8629, 8629, 0, 0xffff, + 8656, 8660, 0, 0xffff, + 8704, 8704, 0, 0xffff, + 8706, 8707, 0, 0xffff, + 8709, 8709, 0, 0xffff, + 8711, 8713, 0, 0xffff, + 8715, 8715, 0, 0xffff, + 8719, 8719, 0, 0xffff, + 8721, 8722, 0, 0xffff, + 8727, 8727, 0, 0xffff, + 8730, 8730, 0, 0xffff, + 8733, 8734, 0, 0xffff, + 8736, 8736, 0, 0xffff, + 8743, 8747, 0, 0xffff, + 8756, 8756, 0, 0xffff, + 8764, 8764, 0, 0xffff, + 8773, 8773, 0, 0xffff, + 8776, 8776, 0, 0xffff, + 8800, 8801, 0, 0xffff, + 8804, 8805, 0, 0xffff, + 8834, 8836, 0, 0xffff, + 8838, 8839, 0, 0xffff, + 8853, 8853, 0, 0xffff, + 8855, 8855, 0, 0xffff, + 8869, 8869, 0, 0xffff, + 8901, 8901, 0, 0xffff, + 8968, 8971, 0, 0xffff, + 9001, 9002, 0, 0xffff, + 9674, 9674, 0, 0xffff, + 9824, 9824, 0, 0xffff, + 9827, 9827, 0, 0xffff, + 9829, 9830, 0, 0xffff, + 338, 339, 0, 0xffff, + 352, 353, 0, 0xffff, + 376, 376, 0, 0xffff, + 710, 710, 0, 0xffff, + 732, 732, 0, 0xffff, + 8194, 8195, 0, 0xffff, + 8201, 8201, 0, 0xffff, + 8204, 8207, 0, 0xffff, + 8211, 8212, 0, 0xffff, + 8216, 8218, 0, 0xffff, + 8218, 8218, 0, 0xffff, + 8220, 8222, 0, 0xffff, + 8224, 8225, 0, 0xffff, + 8240, 8240, 0, 0xffff, + 8249, 8250, 0, 0xffff, + 8364, 8364, 0, 0xffff + ] + end diff --git a/lib/url_generator.rb b/lib/url_generator.rb new file mode 100644 index 00000000..b5415e63 --- /dev/null +++ b/lib/url_generator.rb @@ -0,0 +1,121 @@ +class AbstractUrlGenerator + + def initialize(controller) + raise 'Controller cannot be nil' if controller.nil? + @controller = controller + end + + # Create a link for the given page (or file) name and link text based + # on the render mode in options and whether the page (file) exists + # in the web. + def make_link(name, web, text = nil, options = {}) + text = CGI.escapeHTML(text || WikiWords.separate(name)) + mode = (options[:mode] || :show).to_sym + link_type = (options[:link_type] || :show).to_sym + + if (link_type == :show) + known_page = web.has_page?(name) + else + known_page = web.has_file?(name) + end + + case link_type + when :show + page_link(mode, name, text, web.address, known_page) + when :file + file_link(mode, name, text, web.address, known_page) + when :pic + pic_link(mode, name, text, web.address, known_page) + else + raise "Unknown link type: #{link_type}" + end + end + +end + +class UrlGenerator < AbstractUrlGenerator + + private + + def file_link(mode, name, text, web_address, known_file) + case mode + when :export + if known_file + %{#{text}} + else + %{#{text}} + end + when :publish + if known_file + href = @controller.url_for :controller => 'file', :web => web_address, :action => 'file', + :id => name + %{#{text}} + else + %{#{text}} + end + else + href = @controller.url_for :controller => 'file', :web => web_address, :action => 'file', + :id => name + if known_file + %{#{text}} + else + %{#{text}?} + end + end + end + + def page_link(mode, name, text, web_address, known_page) + case mode + when :export + if known_page + %{#{text}} + else + %{#{text}} + end + when :publish + if known_page + href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'published', + :id => name + %{#{text}} + else + %{#{text}} + end + else + if known_page + href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'show', + :id => name + %{#{text}} + else + href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'new', + :id => name + %{#{text}?} + end + end + end + + def pic_link(mode, name, text, web_address, known_pic) + case mode + when :export + if known_pic + %{#{text}} + else + %{#{text}} + end + when :publish + if known_pic + %{#{text}} + else + %{#{text}} + end + else + href = @controller.url_for :controller => 'file', :web => web_address, :action => 'file', + :id => name + if known_pic + %{#{text}} + else + %{#{text}?} + end + end + end + +end diff --git a/lib/wiki_content.rb b/lib/wiki_content.rb new file mode 100644 index 00000000..3e348837 --- /dev/null +++ b/lib/wiki_content.rb @@ -0,0 +1,202 @@ +require 'cgi' +require_dependency 'chunks/engines' +require_dependency 'chunks/category' +require_dependency 'chunks/include' +require_dependency 'chunks/wiki' +require_dependency 'chunks/literal' +require_dependency 'chunks/uri' +require_dependency 'chunks/nowiki' + +# Wiki content is just a string that can process itself with a chain of +# actions. The actions can modify wiki content so that certain parts of +# it are protected from being rendered by later actions. +# +# When wiki content is rendered, it can be interrogated to find out +# which chunks were rendered. This means things like categories, wiki +# links, can be determined. +# +# Exactly how wiki content is rendered is determined by a number of +# settings that are optionally passed in to a constructor. The current +# options are: +# * :engine +# => The structural markup engine to use (Textile, Markdown, RDoc) +# * :engine_opts +# => A list of options to pass to the markup engines (safe modes, etc) +# * :pre_engine_actions +# => A list of render actions or chunks to be processed before the +# markup engine is applied. By default this is: +# Category, Include, URIChunk, WikiChunk::Link, WikiChunk::Word +# * :post_engine_actions +# => A list of render actions or chunks to apply after the markup +# engine. By default these are: +# Literal::Pre, Literal::Tags +# * :mode +# => How should the content be rendered? For normal display (show), +# publishing (:publish) or export (:export)? + +module ChunkManager + attr_reader :chunks_by_type, :chunks_by_id, :chunks, :chunk_id + + ACTIVE_CHUNKS = [ NoWiki, Category, WikiChunk::Link, URIChunk, LocalURIChunk, + WikiChunk::Word ] + + HIDE_CHUNKS = [ Literal::Pre, Literal::Tags ] + + MASK_RE = { + ACTIVE_CHUNKS => Chunk::Abstract.mask_re(ACTIVE_CHUNKS), + HIDE_CHUNKS => Chunk::Abstract.mask_re(HIDE_CHUNKS) + } + + def init_chunk_manager + @chunks_by_type = Hash.new + Chunk::Abstract::derivatives.each{|chunk_type| + @chunks_by_type[chunk_type] = Array.new + } + @chunks_by_id = Hash.new + @chunks = [] + @chunk_id = 0 + end + + def add_chunk(c) + @chunks_by_type[c.class] << c + @chunks_by_id[c.object_id] = c + @chunks << c + @chunk_id += 1 + end + + def delete_chunk(c) + @chunks_by_type[c.class].delete(c) + @chunks_by_id.delete(c.object_id) + @chunks.delete(c) + end + + def merge_chunks(other) + other.chunks.each{|c| add_chunk(c)} + end + + def scan_chunkid(text) + text.scan(MASK_RE[ACTIVE_CHUNKS]){|a| yield a[0] } + end + + def find_chunks(chunk_type) + @chunks.select { |chunk| chunk.kind_of?(chunk_type) and chunk.rendered? } + end + +end + +# A simplified version of WikiContent. Useful to avoid recursion problems in +# WikiContent.new +class WikiContentStub < String + + attr_reader :options + include ChunkManager + + def initialize(content, options) + super(content) + @options = options + init_chunk_manager + end + + # Detects the mask strings contained in the text of chunks of type chunk_types + # and yields the corresponding chunk ids + # example: content = "chunk123categorychunk
    chunk456categorychunk
    " + # inside_chunks(Literal::Pre) ==> yield 456 + def inside_chunks(chunk_types) + chunk_types.each{|chunk_type| chunk_type.apply_to(self) } + + chunk_types.each{|chunk_type| @chunks_by_type[chunk_type].each{|hide_chunk| + scan_chunkid(hide_chunk.text){|id| yield id } + } + } + end +end + +class WikiContent < String + + include ChunkManager + + DEFAULT_OPTS = { + :active_chunks => ACTIVE_CHUNKS, + :engine => Engines::Textile, + :engine_opts => [], + :mode => :show + }.freeze + + attr_reader :web, :options, :revision, :not_rendered, :pre_rendered + + # Create a new wiki content string from the given one. + # The options are explained at the top of this file. + def initialize(revision, url_generator, options = {}) + @revision = revision + @url_generator = url_generator + @web = @revision.page.web + + @options = DEFAULT_OPTS.dup.merge(options) + @options[:engine] = Engines::MAP[@web.markup] + @options[:engine_opts] = [:filter_html, :filter_styles] if @web.safe_mode? + @options[:active_chunks] = (ACTIVE_CHUNKS - [WikiChunk::Word] ) if @web.brackets_only? + + @not_rendered = @pre_rendered = nil + + super(@revision.content) + init_chunk_manager + build_chunks + @not_rendered = String.new(self) + end + + # Call @web.page_link using current options. + def page_link(name, text, link_type) + @options[:link_type] = (link_type || :show) + @url_generator.make_link(name, @web, text, @options) + end + + def build_chunks + # create and mask Includes and "active_chunks" chunks + Include.apply_to(self) + @options[:active_chunks].each{|chunk_type| chunk_type.apply_to(self)} + + # Handle hiding contexts like "pre" and "code" etc.. + # The markup (textile, rdoc etc) can produce such contexts with its own syntax. + # To reveal them, we work on a copy of the content. + # The copy is rendered and used to detect the chunks that are inside protecting context + # These chunks are reverted on the original content string. + + copy = WikiContentStub.new(self, @options) + @options[:engine].apply_to(copy) + + copy.inside_chunks(HIDE_CHUNKS) do |id| + @chunks_by_id[id.to_i].revert + end + end + + def pre_render! + unless @pre_rendered + @chunks_by_type[Include].each{|chunk| chunk.unmask } + @pre_rendered = String.new(self) + end + @pre_rendered + end + + def render! + pre_render! + @options[:engine].apply_to(self) + # unmask in one go. $~[1] is the chunk id + gsub!(MASK_RE[ACTIVE_CHUNKS]) do + chunk = @chunks_by_id[$~[1].to_i] + if chunk.nil? + # if we match a chunkmask that existed in the original content string + # just keep it as it is + $~[0] + else + chunk.unmask_text + end + end + self + end + + def page_name + @revision.page.name + end + +end + diff --git a/lib/wiki_words.rb b/lib/wiki_words.rb new file mode 100644 index 00000000..8f2b154f --- /dev/null +++ b/lib/wiki_words.rb @@ -0,0 +1,23 @@ +# Contains all the methods for finding and replacing wiki words +module WikiWords + # In order of appearance: Latin, greek, cyrillian, armenian + I18N_HIGHER_CASE_LETTERS = + "ÀÃ?ÂÃÄÅĀĄĂÆÇĆČĈĊĎÄ?ÈÉÊËĒĘĚĔĖĜĞĠĢĤĦÌÃ?ÃŽÃ?ĪĨĬĮİIJĴĶÅ?ĽĹĻĿÑŃŇŅŊÒÓÔÕÖØŌÅ?ŎŒŔŘŖŚŠŞŜȘŤŢŦȚÙÚÛÜŪŮŰŬŨŲŴÃ?ŶŸŹŽŻ" + + "ΑΒΓΔΕΖΗΘΙΚΛΜÎ?ΞΟΠΡΣΤΥΦΧΨΩ" + + "ΆΈΉΊΌΎÎ?ѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎÒ?ҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾÓ?ÓƒÓ…Ó‡Ó‰Ó‹Ó?Ó?ӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӸЖ" + + "Ô±Ô²Ô³Ô´ÔµÔ¶Ô·Ô¸Ô¹ÔºÔ»Ô¼Ô½Ô¾Ô¿Õ€Õ?Õ‚ÕƒÕ„Õ…Õ†Õ‡ÕˆÕ‰ÕŠÕ‹ÕŒÕ?Õ?Õ?Õ‘Õ’Õ“Õ”Õ•Õ–" + + I18N_LOWER_CASE_LETTERS = + "àáâãäåÄ?ąăæçćÄ?ĉċÄ?đèéêëēęěĕėƒÄ?ğġģĥħìíîïīĩĭįıijĵķĸłľĺļŀñńňņʼnŋòóôõöøÅ?Å‘Å?œŕřŗśšşÅ?șťţŧțùúûüūůűŭũųŵýÿŷžżźÞþßſÃ?ð" + + "άέήίΰαβγδεζηθικλμνξοπÏ?ςστυφχψωϊϋόÏ?ÏŽÎ?" + + "абвгдежзийклмнопрÑ?туфхцчшщъыьÑ?ÑŽÑ?Ñ?ёђѓєѕіїјљћќÑ?ўџѡѣѥѧѩѫѭѯѱѳѵѷѹѻѽѿÒ?Ò‹Ò?Ò?Ò‘Ò“Ò•Ò—Ò™Ò›Ò?ҟҡңҥҧҩҫҭүұҳҵҷҹһҽҿӀӂӄӆӈӊӌӎӑӓӕӗәӛÓ?ÓŸÓ¡Ó£Ó¥Ó§Ó©Ó«Ó­Ó¯Ó±Ó³ÓµÓ¹" + + "Õ¡Õ¢Õ£Õ¤Õ¥Õ¦Õ§Õ¨Õ©ÕªÕ«Õ¬Õ­Õ®Õ¯Õ°Õ±Õ²Õ³Õ´ÕµÕ¶Õ·Õ¸Õ¹ÕºÕ»Õ¼Õ½Õ¾Õ¿Ö€Ö?Ö‚ÖƒÖ„Ö…Ö†Ö‡" + + WIKI_WORD_PATTERN = '[A-Z' + I18N_HIGHER_CASE_LETTERS + '][a-z' + I18N_LOWER_CASE_LETTERS + ']+[A-Z' + I18N_HIGHER_CASE_LETTERS + ']\w+' + CAMEL_CASED_WORD_BORDER = /([a-z#{I18N_LOWER_CASE_LETTERS}])([A-Z#{I18N_HIGHER_CASE_LETTERS}])/u + + def self.separate(wiki_word) + wiki_word.gsub(CAMEL_CASED_WORD_BORDER, '\1 \2') + end + +end diff --git a/natives/osx/desktop_launcher/AppDelegate.h b/natives/osx/desktop_launcher/AppDelegate.h new file mode 100644 index 00000000..c2b8f4f1 --- /dev/null +++ b/natives/osx/desktop_launcher/AppDelegate.h @@ -0,0 +1,18 @@ +/* AppDelegate */ + +#import + +@interface AppDelegate : NSObject +{ + IBOutlet NSMenu* statusMenu; + NSTask* serverCommand; + int processID; + BOOL shouldOpenUntitled; + + NSNetService* service; +} +- (IBAction)about:(id)sender; +- (IBAction)goToHomepage:(id)sender; +- (IBAction)goToInstikiOrg:(id)sender; +- (IBAction)quit:(id)sender; +@end diff --git a/natives/osx/desktop_launcher/AppDelegate.mm b/natives/osx/desktop_launcher/AppDelegate.mm new file mode 100644 index 00000000..be3339cc --- /dev/null +++ b/natives/osx/desktop_launcher/AppDelegate.mm @@ -0,0 +1,109 @@ +#include +#include +#import "AppDelegate.h" + +int launch_ruby (char const* cmd) +{ + int pId, parentID = getpid(); + if((pId = fork()) == 0) // child + { + NSLog(@"set child (%d) to pgrp %d", getpid(), parentID); + setpgrp(0, parentID); + system(cmd); + return 0; + } + else // parent + { + NSLog(@"started child process: %d", pId); + return pId; + } +} + +@implementation AppDelegate + +- (NSString*)storageDirectory +{ + NSString* dir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Application Support/Instiki"]; + [[NSFileManager defaultManager] createDirectoryAtPath:dir attributes:nil]; + return dir; +} + +- (void)awakeFromNib +{ + setpgrp(0, getpid()); + + if([[[[NSBundle mainBundle] infoDictionary] objectForKey:@"LSUIElement"] isEqualToString:@"1"]) + { + NSStatusBar* bar = [NSStatusBar systemStatusBar]; + NSStatusItem* item = [[bar statusItemWithLength:NSVariableStatusItemLength] retain]; + [item setTitle:@"Wiki"]; + [item setHighlightMode:YES]; + [item setMenu:statusMenu]; + } + + NSBundle* bundle = [NSBundle bundleForClass:[self class]]; + NSString* ruby = [bundle pathForResource:@"ruby" ofType:nil]; + NSString* script = [[bundle resourcePath] stringByAppendingPathComponent:@"rb_src/instiki.rb"]; + if(ruby && script) + { + NSString* cmd = [NSString stringWithFormat: + @"%@ -I '%@' -I '%@' '%@' -s --storage='%@'", + ruby, + [[bundle resourcePath] stringByAppendingPathComponent:@"lib/ruby/1.8"], + [[bundle resourcePath] stringByAppendingPathComponent:@"lib/ruby/1.8/powerpc-darwin"], + script, + [self storageDirectory] + ]; + NSLog(@"starting %@", cmd); + processID = launch_ruby([cmd UTF8String]); + } + + /* public the service using rendezvous */ + service = [[NSNetService alloc] + initWithDomain:@"" // default domain + type:@"_http._tcp." + name:[NSString stringWithFormat:@"%@'s Instiki", NSFullUserName()] + port:2500]; + [service publish]; +} + +- (void)applicationWillTerminate:(NSNotification*)aNotification +{ + [service stop]; + [service release]; + + kill(0, SIGTERM); +} + +- (IBAction)about:(id)sender +{ + [NSApp activateIgnoringOtherApps:YES]; + [NSApp orderFrontStandardAboutPanel:self]; +} + +- (IBAction)goToHomepage:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://localhost:2500/"]]; +} + +- (IBAction)goToInstikiOrg:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.instiki.org/"]]; +} + +- (BOOL)applicationShouldOpenUntitledFile:(NSApplication*)sender +{ + return shouldOpenUntitled ?: (shouldOpenUntitled = YES, NO); +} + +- (BOOL)applicationOpenUntitledFile:(NSApplication*)theApplication +{ + return [self goToHomepage:self], YES; +} + +- (IBAction)quit:(id)sender +{ + [NSApp terminate:self]; +} + +@end diff --git a/natives/osx/desktop_launcher/Credits.html b/natives/osx/desktop_launcher/Credits.html new file mode 100644 index 00000000..99ff9627 --- /dev/null +++ b/natives/osx/desktop_launcher/Credits.html @@ -0,0 +1,16 @@ +
    +
    Engineering:
    +
    Some people
    + +
    Human Interface Design:
    +
    Some other people
    + +
    Testing:
    +
    Hopefully not nobody
    + +
    Documentation:
    +
    Whoever
    + +
    With special thanks to:
    +
    Mom
    +
    \ No newline at end of file diff --git a/natives/osx/desktop_launcher/English.lproj/InfoPlist.strings b/natives/osx/desktop_launcher/English.lproj/InfoPlist.strings new file mode 100644 index 0000000000000000000000000000000000000000..9ead5aa9d514c3266b4e65a6561c8825fe84c0ca GIT binary patch literal 550 zcmbu5PfNo<5XIkF@H+&LL2M8&B3_JG1i2d#1;PsUMucrsy)6{NA?Z$r$>cw9v|5k z>rOZ7!7o8(N>KP92D;{)cqZ#E=q`9)O*N%@MXUj~_+=<1)6iBb9|xT$mURV z;{;O_Hrzgq~P-b + + + + IBDocumentLocation + 109 6 356 240 0 0 1440 878 + IBEditorPositions + + 206 + 112 300 116 87 0 0 1440 878 + 29 + 241 316 70 44 0 0 1440 878 + + IBFramework Version + 349.0 + IBOpenObjects + + 206 + 29 + + IBSystem Version + 7H63 + + diff --git a/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib b/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib new file mode 100644 index 0000000000000000000000000000000000000000..e78d3042493ce2739c2a7124d7dd40337f78021f GIT binary patch literal 1607 zcmb7E-)kdP6h7IuHZ)DQf{GvueUn8H^hx`aY?rcmXrhkqGD$AU*kqJ$Gi(q)H!59&&T;cfRxEe&>v5 z=#3Rp^{7kCCL8;Ur*fS0nW9m+QYyboD)g#JO$JXmPZy2Y0{=@FOwJvidMR5p@+&J1t71~iu|-Splyy;}XLbr*iR~9CX|fBS zc65}dViSXJI(*#9<#NE+*T*$V#5p9f@~&&%0L~|oJUVuQv3AhrpR{{Ci++WIcFxUP z5kaP4d(>LCUUuA?Ktd2}FblBu{>jM+w2*kVXvD?6MLfrME97!VA`aOLO|wR{kFFEb zSg4cAa?^B|g=q25U)9=K3B$24%wr8SBO0-T~Ph+A&o<9}HNFkxz>B(MK?uYVrC=Wv!4*Q;7GA%on z>PC=iA8V!zMmsfQu&@8!RzBnT7j;UFIA z6tYv4xJ}DO$<7NT`*C|;n=d(TmAH#8vf=pHRnx6TQhmd;Nh1O7bur)P>L|GAl|~kd zmmm%_VnQPl!(rs&+uDR``;QCf&qP&}eA*y%w1|7#-CZP0R|oTjxjx?B)g8`Gc{t&| z7mG)iu%n?9*HME>+&~+$HRo+-FwD`#k3v)lI3t89Y=6420@mQUQZ|>X3m{ySO{m4nBpt!=S7F zVDOXLWbi;eV(_i{lfiwpKd-{q)o%=bPS4Rv!P!Acj)$a`Ms^iy?^5+2B W_&~FTD+^A=F&AC4Nmd=b`~CrC`o$># literal 0 HcmV?d00001 diff --git a/natives/osx/desktop_launcher/Info.plist b/natives/osx/desktop_launcher/Info.plist new file mode 100644 index 00000000..c2c9f3bf --- /dev/null +++ b/natives/osx/desktop_launcher/Info.plist @@ -0,0 +1,13 @@ +{ + CFBundleDevelopmentRegion = English; + CFBundleExecutable = Instiki; + CFBundleIconFile = ""; + CFBundleIdentifier = "com.nextangle.instiki"; + CFBundleInfoDictionaryVersion = "6.0"; + CFBundlePackageType = APPL; + CFBundleSignature = WIKI; + CFBundleVersion = "0.9.0"; + LSUIElement = 1; + NSMainNibFile = MainMenu; + NSPrincipalClass = NSApplication; +} \ No newline at end of file diff --git a/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj b/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj new file mode 100644 index 00000000..f71cb7bc --- /dev/null +++ b/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj @@ -0,0 +1,592 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 39; + objects = { + 080E96DDFE201D6D7F000001 = { + children = ( + 174B2765065CE31400ED6208, + 174B2766065CE31400ED6208, + ); + isa = PBXGroup; + name = Classes; + refType = 4; + sourceTree = ""; + }; + 089C165CFE840E0CC02AAC07 = { + children = ( + 089C165DFE840E0CC02AAC07, + ); + isa = PBXVariantGroup; + name = InfoPlist.strings; + refType = 4; + sourceTree = ""; + }; + 089C165DFE840E0CC02AAC07 = { + fileEncoding = 10; + isa = PBXFileReference; + lastKnownFileType = text.plist.strings; + name = English; + path = English.lproj/InfoPlist.strings; + refType = 4; + sourceTree = ""; + }; +//080 +//081 +//082 +//083 +//084 +//100 +//101 +//102 +//103 +//104 + 1058C7A0FEA54F0111CA2CBB = { + children = ( + 1058C7A1FEA54F0111CA2CBB, + ); + isa = PBXGroup; + name = "Linked Frameworks"; + refType = 4; + sourceTree = ""; + }; + 1058C7A1FEA54F0111CA2CBB = { + fallbackIsa = PBXFileReference; + isa = PBXFrameworkReference; + lastKnownFileType = wrapper.framework; + name = Cocoa.framework; + path = /System/Library/Frameworks/Cocoa.framework; + refType = 0; + sourceTree = ""; + }; + 1058C7A2FEA54F0111CA2CBB = { + children = ( + 29B97325FDCFA39411CA2CEA, + 29B97324FDCFA39411CA2CEA, + ); + isa = PBXGroup; + name = "Other Frameworks"; + refType = 4; + sourceTree = ""; + }; +//100 +//101 +//102 +//103 +//104 +//170 +//171 +//172 +//173 +//174 + 174B2765065CE31400ED6208 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = sourcecode.cpp.objcpp; + path = AppDelegate.mm; + refType = 4; + sourceTree = ""; + }; + 174B2766065CE31400ED6208 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = sourcecode.c.h; + path = AppDelegate.h; + refType = 4; + sourceTree = ""; + }; + 174B2767065CE31400ED6208 = { + fileRef = 174B2765065CE31400ED6208; + isa = PBXBuildFile; + settings = { + }; + }; + 174B2768065CE31400ED6208 = { + fileRef = 174B2766065CE31400ED6208; + isa = PBXBuildFile; + settings = { + }; + }; + 17BF6FD9067536EB003F37D6 = { + children = ( + 63B86D2F0673A5D300807E13, + 63B86D1A0673A5B200807E13, + 63B86D100673A58400807E13, + ); + isa = PBXGroup; + name = "Instiki Source"; + refType = 4; + sourceTree = ""; + }; + 17C1C5CD065D3A3C003526E7 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.html; + path = Credits.html; + refType = 4; + sourceTree = ""; + }; + 17C1C5CE065D3A3C003526E7 = { + fileRef = 17C1C5CD065D3A3C003526E7; + isa = PBXBuildFile; + settings = { + }; + }; + 17C1C6E2065D458D003526E7 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.script.sh; + path = MakeDMG.sh; + refType = 4; + sourceTree = ""; + }; + 17F6C11106629574007E0BD0 = { + isa = PBXFileReference; + lastKnownFileType = "compiled.mach-o.executable"; + name = ruby; + path = /usr/local/bin/ruby; + refType = 0; + sourceTree = ""; + }; + 17F6C11206629574007E0BD0 = { + fileRef = 17F6C11106629574007E0BD0; + isa = PBXBuildFile; + settings = { + }; + }; + 17F6C113066295D0007E0BD0 = { + isa = PBXFileReference; + lastKnownFileType = folder; + name = ruby; + path = /usr/local/lib/ruby; + refType = 0; + sourceTree = ""; + }; + 17F6C3A90662960F007E0BD0 = { + buildActionMask = 2147483647; + dstPath = lib; + dstSubfolderSpec = 7; + files = ( + 17F6C3CF066296B5007E0BD0, + ); + isa = PBXCopyFilesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 17F6C3CF066296B5007E0BD0 = { + fileRef = 17F6C113066295D0007E0BD0; + isa = PBXBuildFile; + settings = { + }; + }; + 17F6C3D2066296E4007E0BD0 = { + children = ( + 17F6C11106629574007E0BD0, + 17F6C113066295D0007E0BD0, + ); + isa = PBXGroup; + name = "Ruby 1.8"; + refType = 4; + sourceTree = ""; + }; +//170 +//171 +//172 +//173 +//174 +//190 +//191 +//192 +//193 +//194 + 19C28FACFE9D520D11CA2CBB = { + children = ( + 8D1107320486CEB800E47090, + ); + isa = PBXGroup; + name = Products; + refType = 4; + sourceTree = ""; + }; +//190 +//191 +//192 +//193 +//194 +//290 +//291 +//292 +//293 +//294 + 29B97313FDCFA39411CA2CEA = { + buildSettings = { + }; + buildStyles = ( + 4A9504CCFFE6A4B311CA0CBA, + 4A9504CDFFE6A4B311CA0CBA, + ); + hasScannedForEncodings = 1; + isa = PBXProject; + mainGroup = 29B97314FDCFA39411CA2CEA; + projectDirPath = ""; + targets = ( + 8D1107260486CEB800E47090, + ); + }; + 29B97314FDCFA39411CA2CEA = { + children = ( + 080E96DDFE201D6D7F000001, + 29B97315FDCFA39411CA2CEA, + 29B97317FDCFA39411CA2CEA, + 29B97323FDCFA39411CA2CEA, + 19C28FACFE9D520D11CA2CBB, + 17C1C6E2065D458D003526E7, + ); + isa = PBXGroup; + name = Instiki; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97315FDCFA39411CA2CEA = { + children = ( + 32CA4F630368D1EE00C91783, + 29B97316FDCFA39411CA2CEA, + ); + isa = PBXGroup; + name = "Other Sources"; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97316FDCFA39411CA2CEA = { + fileEncoding = 30; + isa = PBXFileReference; + lastKnownFileType = sourcecode.cpp.objcpp; + path = main.mm; + refType = 4; + sourceTree = ""; + }; + 29B97317FDCFA39411CA2CEA = { + children = ( + 17BF6FD9067536EB003F37D6, + 17F6C3D2066296E4007E0BD0, + 8D1107310486CEB800E47090, + 089C165CFE840E0CC02AAC07, + 29B97318FDCFA39411CA2CEA, + 17C1C5CD065D3A3C003526E7, + ); + isa = PBXGroup; + name = Resources; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97318FDCFA39411CA2CEA = { + children = ( + 29B97319FDCFA39411CA2CEA, + ); + isa = PBXVariantGroup; + name = MainMenu.nib; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97319FDCFA39411CA2CEA = { + isa = PBXFileReference; + lastKnownFileType = wrapper.nib; + name = English; + path = English.lproj/MainMenu.nib; + refType = 4; + sourceTree = ""; + }; + 29B97323FDCFA39411CA2CEA = { + children = ( + 1058C7A0FEA54F0111CA2CBB, + 1058C7A2FEA54F0111CA2CBB, + ); + isa = PBXGroup; + name = Frameworks; + path = ""; + refType = 4; + sourceTree = ""; + }; + 29B97324FDCFA39411CA2CEA = { + fallbackIsa = PBXFileReference; + isa = PBXFrameworkReference; + lastKnownFileType = wrapper.framework; + name = AppKit.framework; + path = /System/Library/Frameworks/AppKit.framework; + refType = 0; + sourceTree = ""; + }; + 29B97325FDCFA39411CA2CEA = { + fallbackIsa = PBXFileReference; + isa = PBXFrameworkReference; + lastKnownFileType = wrapper.framework; + name = Foundation.framework; + path = /System/Library/Frameworks/Foundation.framework; + refType = 0; + sourceTree = ""; + }; +//290 +//291 +//292 +//293 +//294 +//320 +//321 +//322 +//323 +//324 + 32CA4F630368D1EE00C91783 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = sourcecode.c.h; + path = Instiki_Prefix.pch; + refType = 4; + sourceTree = ""; + }; +//320 +//321 +//322 +//323 +//324 +//4A0 +//4A1 +//4A2 +//4A3 +//4A4 + 4A9504CCFFE6A4B311CA0CBA = { + buildRules = ( + ); + buildSettings = { + COPY_PHASE_STRIP = NO; + DEBUGGING_SYMBOLS = YES; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + OPTIMIZATION_CFLAGS = "-O0"; + ZERO_LINK = YES; + }; + isa = PBXBuildStyle; + name = Development; + }; + 4A9504CDFFE6A4B311CA0CBA = { + buildRules = ( + ); + buildSettings = { + COPY_PHASE_STRIP = YES; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + ZERO_LINK = NO; + }; + isa = PBXBuildStyle; + name = Deployment; + }; +//4A0 +//4A1 +//4A2 +//4A3 +//4A4 +//630 +//631 +//632 +//633 +//634 + 63B86D0F0673A53100807E13 = { + buildActionMask = 2147483647; + dstPath = rb_src; + dstSubfolderSpec = 7; + files = ( + 63B86D310673A5D600807E13, + 63B86D1C0673A5B600807E13, + 63B86D120673A59100807E13, + ); + isa = PBXCopyFilesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 63B86D100673A58400807E13 = { + explicitFileType = folder; + fileEncoding = 4; + isa = PBXFileReference; + name = app; + path = /Users/duff/Source/rb_src/instiki/app; + refType = 0; + sourceTree = ""; + }; + 63B86D120673A59100807E13 = { + fileRef = 63B86D100673A58400807E13; + isa = PBXBuildFile; + settings = { + }; + }; + 63B86D1A0673A5B200807E13 = { + explicitFileType = folder; + fileEncoding = 4; + isa = PBXFileReference; + name = libraries; + path = /Users/duff/Source/rb_src/instiki/libraries; + refType = 0; + sourceTree = ""; + }; + 63B86D1C0673A5B600807E13 = { + fileRef = 63B86D1A0673A5B200807E13; + isa = PBXBuildFile; + settings = { + }; + }; + 63B86D2F0673A5D300807E13 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.script.ruby; + name = instiki.rb; + path = /Users/duff/Source/rb_src/instiki/instiki.rb; + refType = 0; + sourceTree = ""; + }; + 63B86D310673A5D600807E13 = { + fileRef = 63B86D2F0673A5D300807E13; + isa = PBXBuildFile; + settings = { + }; + }; +//630 +//631 +//632 +//633 +//634 +//8D0 +//8D1 +//8D2 +//8D3 +//8D4 + 8D1107260486CEB800E47090 = { + buildPhases = ( + 8D1107270486CEB800E47090, + 8D1107290486CEB800E47090, + 8D11072C0486CEB800E47090, + 8D11072E0486CEB800E47090, + 17F6C3A90662960F007E0BD0, + 63B86D0F0673A53100807E13, + ); + buildRules = ( + ); + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ""; + GCC_ENABLE_TRIGRAPHS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = Instiki_Prefix.pch; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = NO; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = NO; + GCC_WARN_UNKNOWN_PRAGMAS = NO; + HEADER_SEARCH_PATHS = ""; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + LIBRARY_SEARCH_PATHS = ""; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = Instiki; + SECTORDER_FLAGS = ""; + WARNING_CFLAGS = "-Wmost -Wno-four-char-constants -Wno-unknown-pragmas"; + WRAPPER_EXTENSION = app; + }; + dependencies = ( + ); + isa = PBXNativeTarget; + name = Instiki; + productInstallPath = "$(HOME)/Applications"; + productName = Instiki; + productReference = 8D1107320486CEB800E47090; + productType = "com.apple.product-type.application"; + }; + 8D1107270486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D1107280486CEB800E47090, + 174B2768065CE31400ED6208, + ); + isa = PBXHeadersBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D1107280486CEB800E47090 = { + fileRef = 32CA4F630368D1EE00C91783; + isa = PBXBuildFile; + settings = { + }; + }; + 8D1107290486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D11072A0486CEB800E47090, + 8D11072B0486CEB800E47090, + 17C1C5CE065D3A3C003526E7, + 17F6C11206629574007E0BD0, + ); + isa = PBXResourcesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D11072A0486CEB800E47090 = { + fileRef = 29B97318FDCFA39411CA2CEA; + isa = PBXBuildFile; + settings = { + }; + }; + 8D11072B0486CEB800E47090 = { + fileRef = 089C165CFE840E0CC02AAC07; + isa = PBXBuildFile; + settings = { + }; + }; + 8D11072C0486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D11072D0486CEB800E47090, + 174B2767065CE31400ED6208, + ); + isa = PBXSourcesBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D11072D0486CEB800E47090 = { + fileRef = 29B97316FDCFA39411CA2CEA; + isa = PBXBuildFile; + settings = { + ATTRIBUTES = ( + ); + }; + }; + 8D11072E0486CEB800E47090 = { + buildActionMask = 2147483647; + files = ( + 8D11072F0486CEB800E47090, + ); + isa = PBXFrameworksBuildPhase; + runOnlyForDeploymentPostprocessing = 0; + }; + 8D11072F0486CEB800E47090 = { + fileRef = 1058C7A1FEA54F0111CA2CBB; + isa = PBXBuildFile; + settings = { + }; + }; + 8D1107310486CEB800E47090 = { + fileEncoding = 4; + isa = PBXFileReference; + lastKnownFileType = text.plist; + path = Info.plist; + refType = 4; + sourceTree = ""; + }; + 8D1107320486CEB800E47090 = { + explicitFileType = wrapper.application; + includeInIndex = 0; + isa = PBXFileReference; + path = Instiki.app; + refType = 3; + sourceTree = BUILT_PRODUCTS_DIR; + }; + }; + rootObject = 29B97313FDCFA39411CA2CEA; +} diff --git a/natives/osx/desktop_launcher/Instiki_Prefix.pch b/natives/osx/desktop_launcher/Instiki_Prefix.pch new file mode 100644 index 00000000..4212f5ef --- /dev/null +++ b/natives/osx/desktop_launcher/Instiki_Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'Instiki' target in the 'Instiki' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/natives/osx/desktop_launcher/MakeDMG.sh b/natives/osx/desktop_launcher/MakeDMG.sh new file mode 100644 index 00000000..d1cddeb1 --- /dev/null +++ b/natives/osx/desktop_launcher/MakeDMG.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +hdiutil create -size 12m -fs HFS+ -volname Instiki -ov /tmp/Instiki_12MB.dmg +hdiutil mount /tmp/Instiki_12MB.dmg +# strip ~/ruby/instiki/natives/osx/build/Instiki.app/Contents/MacOS/Instiki +ditto ~/ruby/instiki/natives/osx/desktop_launcher/build/Instiki.app /Volumes/Instiki/Instiki.app +hdiutil unmount /Volumes/Instiki +hdiutil convert -format UDZO -o /tmp/Instiki.dmg /tmp/Instiki_12MB.dmg +hdiutil internet-enable -yes /tmp/Instiki.dmg diff --git a/natives/osx/desktop_launcher/main.mm b/natives/osx/desktop_launcher/main.mm new file mode 100644 index 00000000..0eb41cfe --- /dev/null +++ b/natives/osx/desktop_launcher/main.mm @@ -0,0 +1,14 @@ +// +// main.mm +// Instiki +// +// Created by Allan Odgaard on Thu May 20 2004. +// Copyright (c) 2004 MacroMates. All rights reserved. +// + +#import + +int main (int argc, char const* argv[]) +{ + return NSApplicationMain(argc, argv); +} diff --git a/natives/osx/desktop_launcher/version.plist b/natives/osx/desktop_launcher/version.plist new file mode 100644 index 00000000..a2932018 --- /dev/null +++ b/natives/osx/desktop_launcher/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 17 + CFBundleShortVersionString + 0.1 + CFBundleVersion + 0.1 + ProjectName + NibPBTemplates + SourceVersion + 1150000 + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 00000000..d3c99834 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,40 @@ +# General Apache options +AddHandler fastcgi-script .fcgi +AddHandler cgi-script .cgi +Options +FollowSymLinks +ExecCGI + +# If you don't want Rails to look in certain directories, +# use the following rewrite rules so that Apache won't rewrite certain requests +# +# Example: +# RewriteCond %{REQUEST_URI} ^/notrails.* +# RewriteRule .* - [L] + +# Redirect all requests not available on the filesystem to Rails +# By default the cgi dispatcher is used which is very slow +# +# For better performance replace the dispatcher with the fastcgi one +# +# Example: +# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] +RewriteEngine On + +# If your Rails application is accessed via an Alias directive, +# then you MUST also set the RewriteBase in this htaccess file. +# +# Example: +# Alias /myrailsapp /path/to/myrailsapp/public +# RewriteBase /myrailsapp + +RewriteRule ^$ index.html [QSA] +RewriteRule ^([^.]+)$ $1.html [QSA] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ dispatch.cgi [QSA,L] + +# In case Rails experiences terminal errors +# Instead of displaying this message you can supply a file here which will be rendered instead +# +# Example: +# ErrorDocument 500 /500.html + +ErrorDocument 500 "

    Application error

    Rails application failed to start properly" \ No newline at end of file diff --git a/public/404.html b/public/404.html new file mode 100644 index 00000000..0e184561 --- /dev/null +++ b/public/404.html @@ -0,0 +1,8 @@ + + + +

    File not found

    +

    Change this error message for pages not found in public/404.html

    + + \ No newline at end of file diff --git a/public/500.html b/public/500.html new file mode 100644 index 00000000..a1001a00 --- /dev/null +++ b/public/500.html @@ -0,0 +1,8 @@ + + + +

    Application error (Apache)

    +

    Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html

    + + \ No newline at end of file diff --git a/public/dispatch.cgi b/public/dispatch.cgi new file mode 100755 index 00000000..ce705d36 --- /dev/null +++ b/public/dispatch.cgi @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch \ No newline at end of file diff --git a/public/dispatch.fcgi b/public/dispatch.fcgi new file mode 100755 index 00000000..664dbbbe --- /dev/null +++ b/public/dispatch.fcgi @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby +# +# You may specify the path to the FastCGI crash log (a log of unhandled +# exceptions which forced the FastCGI instance to exit, great for debugging) +# and the number of requests to process before running garbage collection. +# +# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log +# and the GC period is nil (turned off). A reasonable number of requests +# could range from 10-100 depending on the memory footprint of your app. +# +# Example: +# # Default log path, normal GC behavior. +# RailsFCGIHandler.process! +# +# # Default log path, 50 requests between GC. +# RailsFCGIHandler.process! nil, 50 +# +# # Custom log path, normal GC behavior. +# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' +# +require File.dirname(__FILE__) + "/../config/environment" +require 'fcgi_handler' + +RailsFCGIHandler.process! diff --git a/public/dispatch.rb b/public/dispatch.rb new file mode 100755 index 00000000..ce705d36 --- /dev/null +++ b/public/dispatch.rb @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e61366585aa57740c9ca3d84a74e84be8f8bf3cb GIT binary patch literal 4710 zcmeHKA#WQo6n^LA*6Y%xIW%ct3#_=+D^TvygWaDpP%h_&(WeA?s+e{maV+0KG43`0fUjz~<)b}P%QRk>^R5$)m=b3K#b$IX2Cu)$vmaBr|6(lX^ULgXJdsDk?{xCvoOYjoqUpt44F}&;fA5RR z^PB^wH=uSNwzFbBeoj31bwxS@IYCx!Om|;Qy-A->v^!tubMYKHoW+&k^r5NuWaF&! z^_Ad^x8p)P3pgD2w#7a>TjVsqv}|+>iXUs(s#$Vb{*kQf|ul+sv2R%}}N&o-= literal 0 HcmV?d00001 diff --git a/public/images/.images_go_here b/public/images/.images_go_here new file mode 100644 index 00000000..e69de29b diff --git a/public/images/bg_normal.gif b/public/images/bg_normal.gif new file mode 100644 index 0000000000000000000000000000000000000000..76c0f48412774324350cf744c73748d17aa53ecc GIT binary patch literal 48 xcmZ?wbhEHbWMyDwXkcJCbLPzd|Nj+#vM_*v4u}BBFfg(AFi+pMLY#}i8URCA3()`o literal 0 HcmV?d00001 diff --git a/public/images/bg_protected.gif b/public/images/bg_protected.gif new file mode 100644 index 0000000000000000000000000000000000000000..2030f6f0618990cae1d7efd38bf0135dd686edeb GIT binary patch literal 48 xcmZ?wbhEHbWMyDwXkcLYf9A~p|Nj+#vM_*v4u}BBFfg(AFi+pMLY#}i8URXA30) && (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) + return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else this.hide(); + }, + + markPrevious: function() { + if(this.index > 0) this.index-- + else this.index = this.entryCount-1; + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + + var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + var lastTokenPos = this.findLastToken(); + if (lastTokenPos != -1) { + var newValue = this.element.value.substr(0, lastTokenPos + 1); + var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value; + } else { + this.element.value = value; + } + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.firstChild); + + if(this.update.firstChild && this.update.firstChild.childNodes) { + this.entryCount = + this.update.firstChild.childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + + this.index = 0; + this.render(); + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + if(this.getToken().length>=this.options.minChars) { + this.startIndicator(); + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + }, + + getToken: function() { + var tokenPos = this.findLastToken(); + if (tokenPos != -1) + var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + else + var ret = this.element.value; + + return /\n/.test(ret) ? '' : ret; + }, + + findLastToken: function() { + var lastTokenPos = -1; + + for (var i=0; i lastTokenPos) + lastTokenPos = thisTokenPos; + } + return lastTokenPos; + } +} + +Ajax.Autocompleter = Class.create(); +Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } + +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(); +Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
  • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
  • "); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("
  • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
  • "); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + return "
      " + ret.join('') + "
    "; + } + }, options || {}); + } +}); + +// AJAX in-place editor +// +// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor + +Ajax.InPlaceEditor = Class.create(); +Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; +Ajax.InPlaceEditor.prototype = { + initialize: function(element, url, options) { + this.url = url; + this.element = $(element); + + this.options = Object.extend({ + okText: "ok", + cancelText: "cancel", + savingText: "Saving...", + clickToEditText: "Click to edit", + okText: "ok", + rows: 1, + onComplete: function(transport, element) { + new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); + }, + onFailure: function(transport) { + alert("Error communicating with the server: " + transport.responseText.stripTags()); + }, + callback: function(form) { + return Form.serialize(form); + }, + handleLineBreaks: true, + loadingText: 'Loading...', + savingClassName: 'inplaceeditor-saving', + loadingClassName: 'inplaceeditor-loading', + formClassName: 'inplaceeditor-form', + highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, + highlightendcolor: "#FFFFFF", + externalControl: null, + ajaxOptions: {} + }, options || {}); + + if(!this.options.formId && this.element.id) { + this.options.formId = this.element.id + "-inplaceeditor"; + if ($(this.options.formId)) { + // there's already a form with that name, don't specify an id + this.options.formId = null; + } + } + + if (this.options.externalControl) { + this.options.externalControl = $(this.options.externalControl); + } + + this.originalBackground = Element.getStyle(this.element, 'background-color'); + if (!this.originalBackground) { + this.originalBackground = "transparent"; + } + + this.element.title = this.options.clickToEditText; + + this.onclickListener = this.enterEditMode.bindAsEventListener(this); + this.mouseoverListener = this.enterHover.bindAsEventListener(this); + this.mouseoutListener = this.leaveHover.bindAsEventListener(this); + Event.observe(this.element, 'click', this.onclickListener); + Event.observe(this.element, 'mouseover', this.mouseoverListener); + Event.observe(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.observe(this.options.externalControl, 'click', this.onclickListener); + Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + }, + enterEditMode: function() { + if (this.saving) return; + if (this.editing) return; + this.editing = true; + this.onEnterEditMode(); + if (this.options.externalControl) { + Element.hide(this.options.externalControl); + } + Element.hide(this.element); + this.createForm(); + this.element.parentNode.insertBefore(this.form, this.element); + Field.focus(this.editField); + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + }, + createForm: function() { + this.form = document.createElement("form"); + this.form.id = this.options.formId; + Element.addClassName(this.form, this.options.formClassName) + this.form.onsubmit = this.onSubmit.bind(this); + + this.createEditField(); + + if (this.options.textarea) { + var br = document.createElement("br"); + this.form.appendChild(br); + } + + okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = this.options.okText; + this.form.appendChild(okButton); + + cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = this.onclickCancel.bind(this); + this.form.appendChild(cancelLink); + }, + hasHTMLLineBreaks: function(string) { + if (!this.options.handleLineBreaks) return false; + return string.match(/
    /i); + }, + convertHTMLLineBreaks: function(string) { + return string.replace(/
    /gi, "\n").replace(//gi, "\n").replace(/<\/p>/gi, "\n").replace(/

    /gi, ""); + }, + createEditField: function() { + var text; + if(this.options.loadTextURL) { + text = this.options.loadingText; + } else { + text = this.getText(); + } + + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { + this.options.textarea = false; + var textField = document.createElement("input"); + textField.type = "text"; + textField.name = "value"; + textField.value = text; + textField.style.backgroundColor = this.options.highlightcolor; + var size = this.options.size || this.options.cols || 0; + if (size != 0) textField.size = size; + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement("textarea"); + textArea.name = "value"; + textArea.value = this.convertHTMLLineBreaks(text); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + this.editField = textArea; + } + + if(this.options.loadTextURL) { + this.loadExternalText(); + } + this.form.appendChild(this.editField); + }, + getText: function() { + return this.element.innerHTML; + }, + loadExternalText: function() { + Element.addClassName(this.form, this.options.loadingClassName); + this.editField.disabled = true; + new Ajax.Request( + this.options.loadTextURL, + Object.extend({ + asynchronous: true, + onComplete: this.onLoadedExternalText.bind(this) + }, this.options.ajaxOptions) + ); + }, + onLoadedExternalText: function(transport) { + Element.removeClassName(this.form, this.options.loadingClassName); + this.editField.disabled = false; + this.editField.value = transport.responseText.stripTags(); + }, + onclickCancel: function() { + this.onComplete(); + this.leaveEditMode(); + return false; + }, + onFailure: function(transport) { + this.options.onFailure(transport); + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + this.oldInnerHTML = null; + } + return false; + }, + onSubmit: function() { + // onLoading resets these so we need to save them away for the Ajax call + var form = this.form; + var value = this.editField.value; + + // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... + // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... + // to be displayed indefinitely + this.onLoading(); + + new Ajax.Updater( + { + success: this.element, + // don't update on failure (this could be an option) + failure: null + }, + this.url, + Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this) + }, this.options.ajaxOptions) + ); + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + return false; + }, + onLoading: function() { + this.saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + showSaving: function() { + this.oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + Element.addClassName(this.element, this.options.savingClassName); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + }, + removeForm: function() { + if(this.form) { + if (this.form.parentNode) Element.remove(this.form); + this.form = null; + } + }, + enterHover: function() { + if (this.saving) return; + this.element.style.backgroundColor = this.options.highlightcolor; + if (this.effect) { + this.effect.cancel(); + } + Element.addClassName(this.element, this.options.hoverClassName) + }, + leaveHover: function() { + if (this.options.backgroundColor) { + this.element.style.backgroundColor = this.oldBackground; + } + Element.removeClassName(this.element, this.options.hoverClassName) + if (this.saving) return; + this.effect = new Effect.Highlight(this.element, { + startcolor: this.options.highlightcolor, + endcolor: this.options.highlightendcolor, + restorecolor: this.originalBackground + }); + }, + leaveEditMode: function() { + Element.removeClassName(this.element, this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + if (this.options.externalControl) { + Element.show(this.options.externalControl); + } + this.editing = false; + this.saving = false; + this.oldInnerHTML = null; + this.onLeaveEditMode(); + }, + onComplete: function(transport) { + this.leaveEditMode(); + this.options.onComplete.bind(this)(transport, this.element); + }, + onEnterEditMode: function() {}, + onLeaveEditMode: function() {}, + dispose: function() { + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + } + this.leaveEditMode(); + Event.stopObserving(this.element, 'click', this.onclickListener); + Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); + Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + } +}; \ No newline at end of file diff --git a/public/javascripts/dragdrop.js b/public/javascripts/dragdrop.js new file mode 100644 index 00000000..5445d748 --- /dev/null +++ b/public/javascripts/dragdrop.js @@ -0,0 +1,516 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// Element.Class part Copyright (c) 2005 by Rick Olson +// +// See scriptaculous.js for full license. + +/*--------------------------------------------------------------------------*/ + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==element }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null + }, arguments[1] || {}); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if((typeof containment == 'object') && + (containment.constructor == Array)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + isContained: function(element, drop) { + var parentNode = element.parentNode; + return drop._containers.detect(function(c) { return parentNode == c }); + }, + + isAffected: function(pX, pY, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.Class.has_any(element, drop.accept))) && + Position.within(drop.element, pX, pY) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.Class.remove(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(this.last_active) this.deactivate(this.last_active); + if(drop.hoverclass) + Element.Class.add(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(event, element) { + if(!this.drops.length) return; + var pX = Event.pointerX(event); + var pY = Event.pointerY(event); + Position.prepare(); + + var i = this.drops.length-1; do { + var drop = this.drops[i]; + if(this.isAffected(pX, pY, element, drop)) { + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + if(drop.greedy) { + this.activate(drop); + return; + } + } + } while (i--); + + if(this.last_active) this.deactivate(this.last_active); + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active)) + if (this.last_active.onDrop) + this.last_active.onDrop(element, this.last_active.element, event); + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +} + +var Draggables = { + observers: [], + addObserver: function(observer) { + this.observers.push(observer); + }, + removeObserver: function(element) { // element instead of obsever fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + }, + notify: function(eventName, draggable) { // 'onStart', 'onEnd' + this.observers.invoke(eventName, draggable); + } +} + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create(); +Draggable.prototype = { + initialize: function(element) { + var options = Object.extend({ + handle: false, + starteffect: function(element) { + new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); + }, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur}); + }, + endeffect: function(element) { + new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); + }, + zindex: 1000, + revert: false + }, arguments[1] || {}); + + this.element = $(element); + if(options.handle && (typeof options.handle == 'string')) + this.handle = Element.Class.childrenWith(this.element, options.handle)[0]; + + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + Element.makePositioned(this.element); // fix IE + + this.offsetX = 0; + this.offsetY = 0; + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + this.originalX = this.element.offsetLeft; + this.originalY = this.element.offsetTop; + + this.options = options; + + this.active = false; + this.dragging = false; + + this.eventMouseDown = this.startDrag.bindAsEventListener(this); + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.update.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + this.registerEvents(); + }, + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + this.unregisterEvents(); + }, + registerEvents: function() { + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + }, + unregisterEvents: function() { + //if(!this.active) return; + //Event.stopObserving(document, "mouseup", this.eventMouseUp); + //Event.stopObserving(document, "mousemove", this.eventMouseMove); + //Event.stopObserving(document, "keypress", this.eventKeypress); + }, + currentLeft: function() { + return parseInt(this.element.style.left || '0'); + }, + currentTop: function() { + return parseInt(this.element.style.top || '0') + }, + startDrag: function(event) { + if(Event.isLeftClick(event)) { + + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if(src.tagName && ( + src.tagName=='INPUT' || + src.tagName=='SELECT' || + src.tagName=='BUTTON' || + src.tagName=='TEXTAREA')) return; + + // this.registerEvents(); + this.active = true; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.element); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); + Event.stop(event); + } + }, + finishDrag: function(event, success) { + // this.unregisterEvents(); + + this.active = false; + this.dragging = false; + + if(this.options.ghosting) { + Position.relativize(this.element); + Element.remove(this._clone); + this._clone = null; + } + + if(success) Droppables.fire(event, this.element); + Draggables.notify('onEnd', this); + + var revert = this.options.revert; + if(revert && typeof revert == 'function') revert = revert(this.element); + + if(revert && this.options.reverteffect) { + this.options.reverteffect(this.element, + this.currentTop()-this.originalTop, + this.currentLeft()-this.originalLeft); + } else { + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + + Droppables.reset(); + }, + keyPress: function(event) { + if(this.active) { + if(event.keyCode==Event.KEY_ESC) { + this.finishDrag(event, false); + Event.stop(event); + } + } + }, + endDrag: function(event) { + if(this.active && this.dragging) { + this.finishDrag(event, true); + Event.stop(event); + } + this.active = false; + this.dragging = false; + }, + draw: function(event) { + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.element); + offsets[0] -= this.currentLeft(); + offsets[1] -= this.currentTop(); + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = (pointer[0] - offsets[0] - this.offsetX) + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = (pointer[1] - offsets[1] - this.offsetY) + "px"; + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + update: function(event) { + if(this.active) { + if(!this.dragging) { + var style = this.element.style; + this.dragging = true; + + if(Element.getStyle(this.element,'position')=='') + style.position = "relative"; + + if(this.options.zindex) { + this.options.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + Draggables.notify('onStart', this); + if(this.options.starteffect) this.options.starteffect(this.element); + } + + Droppables.show(event, this.element); + this.draw(event); + if(this.options.change) this.options.change(this); + + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + + Event.stop(event); + } + } +} + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create(); +SortableObserver.prototype = { + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +} + +var Sortable = { + sortables: new Array(), + options: function(element){ + element = $(element); + return this.sortables.detect(function(s) { return s.element == element }); + }, + destroy: function(element){ + element = $(element); + this.sortables.findAll(function(s) { return s.element == element }).each(function(s){ + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + }); + this.sortables = this.sortables.reject(function(s) { return s.element == element }); + }, + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, // fixme: unimplemented + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + hoverclass: null, + ghosting: false, + format: null, + onChange: function() {}, + onUpdate: function() {} + }, arguments[1] || {}); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass, + onHover: Sortable.onHover, + greedy: !options.dropOnEmpty + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // make it so + + // drop on empty handling + if(options.dropOnEmpty) { + Droppables.add(element, + {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false}); + options.droppables.push(element); + } + + (this.findElements(element, options) || []).each( function(e) { + // handles are per-draggable + var handle = options.handle ? + Element.Class.childrenWith(e, options.handle)[0] : e; + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + options.droppables.push(e); + }); + + // keep reference + this.sortables.push(options); + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + if(!element.hasChildNodes()) return null; + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName==options.tag.toUpperCase() && + (!options.only || (Element.Class.has(e, options.only)))) + elements.push(e); + if(options.tree) { + var grandchildren = this.findElements(e, options); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : null); + }, + + onHover: function(element, dropon, overlap) { + if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon) { + if(element.parentNode!=dropon) { + dropon.appendChild(element); + } + }, + + unmark: function() { + if(Sortable._marker) Element.hide(Sortable._marker); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = $('dropmarker') || document.createElement('DIV'); + Element.hide(Sortable._marker); + Element.Class.add(Sortable._marker, 'dropmarker'); + Sortable._marker.style.position = 'absolute'; + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.style.top = offsets[1] + 'px'; + if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; + Sortable._marker.style.left = offsets[0] + 'px'; + Element.show(Sortable._marker); + }, + + serialize: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format || /^[^_]*_(.*)$/ + }, arguments[1] || {}); + return $(this.findElements(element, options) || []).collect( function(item) { + return (encodeURIComponent(options.name) + "[]=" + + encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : '')); + }).join("&"); + } +} \ No newline at end of file diff --git a/public/javascripts/edit_web.js b/public/javascripts/edit_web.js new file mode 100644 index 00000000..38b1ba49 --- /dev/null +++ b/public/javascripts/edit_web.js @@ -0,0 +1,55 @@ +function proposeAddress() { + document.getElementById('address').value = + document.getElementById('name').value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); +} + +function cleanAddress() { + document.getElementById('address').value = + document.getElementById('address').value.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); +} + +function checkSystemPassword(password) { + if (password == "") { + alert("You must enter the system password"); + return false; + } else { + return true; + } +} + +function validateEditWebForm() { + if (!checkSystemPassword(document.getElementById('system_password').value)) { + return false; + } + if (document.getElementById('name').value == "") { + alert("You must pick a name for the web"); + return false; + } + if (document.getElementById('address').value == "") { + alert("You must pick an address for the web"); + return false; + } + if (document.getElementById('password').value != "" && + document.getElementById('password').value != document.getElementById('password_check').value) { + alert("The password and its verification doesn't match"); + return false; + } + return true; +} + +// overriding auto-complete by form managers +// code by Chris Holland, lifted from +// http://chrisholland.blogspot.com/2004/11/banks-protect-privacy-disable.html +function overrideAutocomplete() { + if (document.getElementsByTagName) { + var inputElements = document.getElementsByTagName("input"); + for (i=0; inputElements[i]; i++) { + if (inputElements[i].className && (inputElements[i].className.indexOf("disableAutoComplete") != -1)) { + inputElements[i].setAttribute("autocomplete","off"); + }//if current input element has the disableAutoComplete class set. + }//loop thru input elements + } +} + +// This line is executed when the script is loaded +overrideAutocomplete(); diff --git a/public/javascripts/effects.js b/public/javascripts/effects.js new file mode 100644 index 00000000..7e65d922 --- /dev/null +++ b/public/javascripts/effects.js @@ -0,0 +1,1101 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// See scriptaculous.js for full license. + +Object.debug = function(obj) { + var info = []; + + if(typeof obj in ["string","number"]) { + return obj; + } else { + for(property in obj) + if(typeof obj[property]!="function") + info.push(property + ' => ' + + (typeof obj[property] == "string" ? + '"' + obj[property] + '"' : + obj[property])); + } + + return ("'" + obj + "' #" + typeof obj + + ": {" + info.join(", ") + "}"); +} + + +/*--------------------------------------------------------------------------*/ + +var Builder = { + NODEMAP: { + AREA: 'map', + CAPTION: 'table', + COL: 'table', + COLGROUP: 'table', + LEGEND: 'fieldset', + OPTGROUP: 'select', + OPTION: 'select', + PARAM: 'object', + TBODY: 'table', + TD: 'table', + TFOOT: 'table', + TH: 'table', + THEAD: 'table', + TR: 'table' + }, + // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken, + // due to a Firefox bug + node: function(elementName) { + elementName = elementName.toUpperCase(); + + // try innerHTML approach + var parentTag = this.NODEMAP[elementName] || 'div'; + var parentElement = document.createElement(parentTag); + parentElement.innerHTML = "<" + elementName + ">"; + var element = parentElement.firstChild || null; + + // see if browser added wrapping tags + if(element && (element.tagName != elementName)) + element = element.getElementsByTagName(elementName)[0]; + + // fallback to createElement approach + if(!element) element = document.createElement(elementName); + + // abort if nothing could be created + if(!element) return; + + // attributes (or text) + if(arguments[1]) + if(this._isStringOrNumber(arguments[1]) || + (arguments[1] instanceof Array)) { + this._children(element, arguments[1]); + } else { + var attrs = this._attributes(arguments[1]); + if(attrs.length) { + parentElement.innerHTML = "<" +elementName + " " + + attrs + ">"; + element = parentElement.firstChild || null; + // workaround firefox 1.0.X bug + if(!element) { + element = document.createElement(elementName); + for(attr in arguments[1]) + element[attr == 'class' ? 'className' : attr] = arguments[1][attr]; + } + if(element.tagName != elementName) + element = parentElement.getElementsByTagName(elementName)[0]; + } + } + + // text, or array of children + if(arguments[2]) + this._children(element, arguments[2]); + + return element; + }, + _text: function(text) { + return document.createTextNode(text); + }, + _attributes: function(attributes) { + var attrs = []; + for(attribute in attributes) + attrs.push((attribute=='className' ? 'class' : attribute) + + '="' + attributes[attribute].toString().escapeHTML() + '"'); + return attrs.join(" "); + }, + _children: function(element, children) { + if(typeof children=='object') { // array can hold nodes and text + children.flatten().each( function(e) { + if(typeof e=='object') + element.appendChild(e) + else + if(Builder._isStringOrNumber(e)) + element.appendChild(Builder._text(e)); + }); + } else + if(Builder._isStringOrNumber(children)) + element.appendChild(Builder._text(children)); + }, + _isStringOrNumber: function(param) { + return(typeof param=='string' || typeof param=='number'); + } +} + +/* ------------- element ext -------------- */ + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + color = "#"; + if(this.slice(0,4) == "rgb(") { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if(this.slice(0,1) == '#') { + if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if(this.length==7) color = this.toLowerCase(); + } + } + return(color.length==7 ? color : (arguments[0] || this)); +} + +Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { + var children = $(element).childNodes; + var text = ""; + var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i"); + + for (var i = 0; i < children.length; i++) { + if(children[i].nodeType==3) { + text+=children[i].nodeValue; + } else { + if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) + text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); + } + } + + return text; +} + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.style.fontSize = (percent/100) + "em"; + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); +} + +Element.getOpacity = function(element){ + var opacity; + if (opacity = Element.getStyle(element, "opacity")) + return parseFloat(opacity); + if (opacity = (Element.getStyle(element, "filter") || '').match(/alpha\(opacity=(.*)\)/)) + if(opacity[1]) return parseFloat(opacity[1]) / 100; + return 1.0; +} + +Element.setOpacity = function(element, value){ + element= $(element); + var els = element.style; + if (value == 1){ + els.opacity = '0.999999'; + if(/MSIE/.test(navigator.userAgent)) + els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,''); + } else { + if(value < 0.00001) value = 0; + els.opacity = value; + if(/MSIE/.test(navigator.userAgent)) + els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + + "alpha(opacity="+value*100+")"; + } +} + +Element.getInlineOpacity = function(element){ + element= $(element); + var op; + op = element.style.opacity; + if (typeof op != "undefined" && op != "") return op; + return ""; +} + +Element.setInlineOpacity = function(element, value){ + element= $(element); + var els = element.style; + els.opacity = value; +} + +/*--------------------------------------------------------------------------*/ + +Element.Class = { + // Element.toggleClass(element, className) toggles the class being on/off + // Element.toggleClass(element, className1, className2) toggles between both classes, + // defaulting to className1 if neither exist + toggle: function(element, className) { + if(Element.Class.has(element, className)) { + Element.Class.remove(element, className); + if(arguments.length == 3) Element.Class.add(element, arguments[2]); + } else { + Element.Class.add(element, className); + if(arguments.length == 3) Element.Class.remove(element, arguments[2]); + } + }, + + // gets space-delimited classnames of an element as an array + get: function(element) { + return $(element).className.split(' '); + }, + + // functions adapted from original functions by Gavin Kistner + remove: function(element) { + element = $(element); + var removeClasses = arguments; + $R(1,arguments.length-1).each( function(index) { + element.className = + element.className.split(' ').reject( + function(klass) { return (klass == removeClasses[index]) } ).join(' '); + }); + }, + + add: function(element) { + element = $(element); + for(var i = 1; i < arguments.length; i++) { + Element.Class.remove(element, arguments[i]); + element.className += (element.className.length > 0 ? ' ' : '') + arguments[i]; + } + }, + + // returns true if all given classes exist in said element + has: function(element) { + element = $(element); + if(!element || !element.className) return false; + var regEx; + for(var i = 1; i < arguments.length; i++) { + if((typeof arguments[i] == 'object') && + (arguments[i].constructor == Array)) { + for(var j = 0; j < arguments[i].length; j++) { + regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)"); + if(!regEx.test(element.className)) return false; + } + } else { + regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)"); + if(!regEx.test(element.className)) return false; + } + } + return true; + }, + + // expects arrays of strings and/or strings as optional paramters + // Element.Class.has_any(element, ['classA','classB','classC'], 'classD') + has_any: function(element) { + element = $(element); + if(!element || !element.className) return false; + var regEx; + for(var i = 1; i < arguments.length; i++) { + if((typeof arguments[i] == 'object') && + (arguments[i].constructor == Array)) { + for(var j = 0; j < arguments[i].length; j++) { + regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)"); + if(regEx.test(element.className)) return true; + } + } else { + regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)"); + if(regEx.test(element.className)) return true; + } + } + return false; + }, + + childrenWith: function(element, className) { + var children = $(element).getElementsByTagName('*'); + var elements = new Array(); + + for (var i = 0; i < children.length; i++) + if (Element.Class.has(children[i], className)) + elements.push(children[i]); + + return elements; + } +} + +/*--------------------------------------------------------------------------*/ + +var Effect = { + tagifyText: function(element) { + var tagifyStyle = "position:relative"; + if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1"; + element = $(element); + $A(element.childNodes).each( function(child) { + if(child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + Builder.node('span',{style: tagifyStyle}, + character == " " ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if(((typeof element == 'object') || + (typeof element == 'function')) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || {}); + var speed = options.speed; + var delay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: delay + index * speed })); + }); + } +}; + +var Effect2 = Effect; // deprecated + +/* ------------- transitions ------------- */ + +Effect.Transitions = {} + +Effect.Transitions.linear = function(pos) { + return pos; +} +Effect.Transitions.sinoidal = function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; +} +Effect.Transitions.reverse = function(pos) { + return 1-pos; +} +Effect.Transitions.flicker = function(pos) { + return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; +} +Effect.Transitions.wobble = function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; +} +Effect.Transitions.pulse = function(pos) { + return (Math.floor(pos*10) % 2 == 0 ? + (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10))); +} +Effect.Transitions.none = function(pos) { + return 0; +} +Effect.Transitions.full = function(pos) { + return 1; +} + +/* ------------- core effects ------------- */ + +Effect.Queue = { + effects: [], + interval: null, + add: function(effect) { + var timestamp = new Date().getTime(); + + switch(effect.options.queue) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + this.effects.push(effect); + if(!this.interval) + this.interval = setInterval(this.loop.bind(this), 40); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if(this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + this.effects.invoke('loop', timePos); + } +} + +Effect.Base = function() {}; +Effect.Base.prototype = { + position: null, + setOptions: function(options) { + this.options = Object.extend({ + transition: Effect.Transitions.sinoidal, + duration: 1.0, // seconds + fps: 25.0, // max. 25fps due to Effect.Queue implementation + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, options || {}); + }, + start: function(options) { + this.setOptions(options || {}); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn + (this.options.duration*1000); + this.event('beforeStart'); + if(!this.options.sync) Effect.Queue.add(this); + }, + loop: function(timePos) { + if(timePos >= this.startOn) { + if(timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if(this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); + var frame = Math.round(pos * this.options.fps * this.options.duration); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + render: function(pos) { + if(this.state == 'idle') { + this.state = 'running'; + this.event('beforeSetup'); + if(this.setup) this.setup(); + this.event('afterSetup'); + } + if(this.options.transition) pos = this.options.transition(pos); + pos *= (this.options.to-this.options.from); + pos += this.options.from; + this.position = pos; + this.event('beforeUpdate'); + if(this.update) this.update(pos); + this.event('afterUpdate'); + }, + cancel: function() { + if(!this.options.sync) Effect.Queue.remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if(this.options[eventName]) this.options[eventName](this); + } +} + +Effect.Parallel = Class.create(); +Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if(effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + // make this work on IE on elements without 'layout' + if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) + this.element.style.zoom = 1; + var options = Object.extend({ + from: Element.getOpacity(this.element) || 0.0, + to: 1.0 + }, arguments[1] || {}); + this.start(options); + }, + update: function(position) { + Element.setOpacity(this.element, position); + } +}); + +Effect.MoveBy = Class.create(); +Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), { + initialize: function(element, toTop, toLeft) { + this.element = $(element); + this.toTop = toTop; + this.toLeft = toLeft; + this.start(arguments[3]); + }, + setup: function() { + // Bug in Opera: Opera returns the "real" position of a static element or + // relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your stylesheets + // (to 0 if you do not need them) + + Element.makePositioned(this.element); + this.originalTop = parseFloat(Element.getStyle(this.element,'top') || '0'); + this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0'); + }, + update: function(position) { + var topd = this.toTop * position + this.originalTop; + var leftd = this.toLeft * position + this.originalLeft; + this.setPosition(topd, leftd); + }, + setPosition: function(topd, leftd) { + this.element.style.top = topd + "px"; + this.element.style.left = leftd + "px"; + } +}); + +Effect.Scale = Class.create(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + initialize: function(element, percent) { + this.element = $(element) + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || {}); + this.start(options); + }, + setup: function() { + var effect = this; + + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = Element.getStyle(this.element,'position'); + + effect.originalStyle = {}; + ['top','left','width','height','fontSize'].each( function(k) { + effect.originalStyle[k] = effect.element.style[k]; + }); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = Element.getStyle(this.element,'font-size') || "100%"; + ['em','px','%'].each( function(fontSizeType) { + if(fontSize.indexOf(fontSizeType)>0) { + effect.fontSize = parseFloat(fontSize); + effect.fontSizeType = fontSizeType; + } + }); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if(this.options.scaleMode=='box') + this.dims = [this.element.clientHeight, this.element.clientWidth]; + if(this.options.scaleMode=='content') + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if(!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if(this.options.scaleContent && this.fontSize) + this.element.style.fontSize = this.fontSize*currentScale + this.fontSizeType; + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) { + var effect = this; + ['top','left','width','height','fontSize'].each( function(k) { + effect.element.style[k] = effect.originalStyle[k]; + }); + } + }, + setDimensions: function(height, width) { + var els = this.element.style; + if(this.options.scaleX) els.width = width + 'px'; + if(this.options.scaleY) els.height = height + 'px'; + if(this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if(this.elementPositioning == 'absolute') { + if(this.options.scaleY) els.top = this.originalTop-topd + "px"; + if(this.options.scaleX) els.left = this.originalLeft-leftd + "px"; + } else { + if(this.options.scaleY) els.top = -topd + "px"; + if(this.options.scaleX) els.left = -leftd + "px"; + } + } + } +}); + +Effect.Highlight = Class.create(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ + startcolor: "#ffff99" + }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Disable background image during the effect + this.oldBgImage = this.element.style.backgroundImage; + this.element.style.backgroundImage = "none"; + if(!this.options.endcolor) + this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff'); + if (typeof this.options.restorecolor == "undefined") + this.options.restorecolor = this.element.style.backgroundColor; + // init color calculations + this.colors_base = [ + parseInt(this.options.startcolor.slice(1,3),16), + parseInt(this.options.startcolor.slice(3,5),16), + parseInt(this.options.startcolor.slice(5),16) ]; + this.colors_delta = [ + parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0], + parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1], + parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]]; + }, + update: function(position) { + var effect = this; var colors = $R(0,2).map( function(i){ + return Math.round(effect.colors_base[i]+(effect.colors_delta[i]*position)) + }); + this.element.style.backgroundColor = "#" + + colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart(); + }, + finish: function() { + this.element.style.backgroundColor = this.options.restorecolor; + this.element.style.backgroundImage = this.oldBgImage; + } +}); + +Effect.ScrollTo = Class.create(); +Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + this.start(arguments[1] || {}); + }, + setup: function() { + Position.prepare(); + var offsets = Position.cumulativeOffset(this.element); + var max = window.innerHeight ? + window.height - window.innerHeight : + document.body.scrollHeight - + (document.documentElement.clientHeight ? + document.documentElement.clientHeight : document.body.clientHeight); + this.scrollStart = Position.deltaY; + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; + }, + update: function(position) { + Position.prepare(); + window.scrollTo(Position.deltaX, + this.scrollStart + (position*this.delta)); + } +}); + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + var oldOpacity = Element.getInlineOpacity(element); + var options = Object.extend({ + from: Element.getOpacity(element) || 1.0, + to: 0.0, + afterFinishInternal: function(effect) + { if (effect.options.to == 0) { + Element.hide(effect.element); + Element.setInlineOpacity(effect.element, oldOpacity); + } + } + }, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Appear = function(element) { + var options = Object.extend({ + from: (Element.getStyle(element, "display") == "none" ? 0.0 : Element.getOpacity(element) || 0.0), + to: 1.0, + beforeSetup: function(effect) + { Element.setOpacity(effect.element, effect.options.from); + Element.show(effect.element); } + }, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Puff = function(element) { + element = $(element); + var oldOpacity = Element.getInlineOpacity(element); + var oldPosition = element.style.position; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) + { effect.effects[0].element.style.position = 'absolute'; }, + afterFinishInternal: function(effect) + { Element.hide(effect.effects[0].element); + effect.effects[0].element.style.position = oldPosition; + Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } + }, arguments[1] || {}) + ); +} + +Effect.BlindUp = function(element) { + element = $(element); + Element.makeClipping(element); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) + { + Element.hide(effect.element); + Element.undoClipping(effect.element); + } + }, arguments[1] || {}) + ); +} + +Effect.BlindDown = function(element) { + element = $(element); + var oldHeight = element.style.height; + var elementDimensions = Element.getDimensions(element); + return new Effect.Scale(element, 100, + Object.extend({ scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + Element.makeClipping(effect.element); + effect.element.style.height = "0px"; + Element.show(effect.element); + }, + afterFinishInternal: function(effect) { + Element.undoClipping(effect.element); + effect.element.style.height = oldHeight; + } + }, arguments[1] || {}) + ); +} + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = Element.getInlineOpacity(element); + return new Effect.Appear(element, { + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + Element.makePositioned(effect.element); + Element.makeClipping(effect.element); + }, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); + Element.undoPositioned(effect.element); + Element.setInlineOpacity(effect.element, oldOpacity); + } + }) + } + }); +} + +Effect.DropOut = function(element) { + element = $(element); + var oldTop = element.style.top; + var oldLeft = element.style.left; + var oldOpacity = Element.getInlineOpacity(element); + return new Effect.Parallel( + [ new Effect.MoveBy(element, 100, 0, { sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + Element.makePositioned(effect.effects[0].element); }, + afterFinishInternal: function(effect) { + Element.hide(effect.effects[0].element); + Element.undoPositioned(effect.effects[0].element); + effect.effects[0].element.style.left = oldLeft; + effect.effects[0].element.style.top = oldTop; + Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } + }, arguments[1] || {})); +} + +Effect.Shake = function(element) { + element = $(element); + var oldTop = element.style.top; + var oldLeft = element.style.left; + return new Effect.MoveBy(element, 0, 20, + { duration: 0.05, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, -40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, 40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, -40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, 40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, -20, + { duration: 0.05, afterFinishInternal: function(effect) { + Element.undoPositioned(effect.element); + effect.element.style.left = oldLeft; + effect.element.style.top = oldTop; + }}) }}) }}) }}) }}) }}); +} + +Effect.SlideDown = function(element) { + element = $(element); + Element.cleanWhitespace(element); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.firstChild.style.bottom; + var elementDimensions = Element.getDimensions(element); + return new Effect.Scale(element, 100, + Object.extend({ scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + Element.makePositioned(effect.element.firstChild); + if (window.opera) effect.element.firstChild.style.top = ""; + Element.makeClipping(effect.element); + element.style.height = '0'; + Element.show(element); + }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.style.bottom = + (effect.originalHeight - effect.element.clientHeight) + 'px'; }, + afterFinishInternal: function(effect) { + Element.undoClipping(effect.element); + Element.undoPositioned(effect.element.firstChild); + effect.element.firstChild.style.bottom = oldInnerBottom; } + }, arguments[1] || {}) + ); +} + +Effect.SlideUp = function(element) { + element = $(element); + Element.cleanWhitespace(element); + var oldInnerBottom = element.firstChild.style.bottom; + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function(effect) { + Element.makePositioned(effect.element.firstChild); + if (window.opera) effect.element.firstChild.style.top = ""; + Element.makeClipping(effect.element); + Element.show(element); + }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.style.bottom = + (effect.originalHeight - effect.element.clientHeight) + 'px'; }, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); + Element.undoPositioned(effect.element.firstChild); + effect.element.firstChild.style.bottom = oldInnerBottom; } + }, arguments[1] || {}) + ); +} + +Effect.Squish = function(element) { + // Bug in opera makes the TD containing this element expand for a instance after finish + return new Effect.Scale(element, window.opera ? 1 : 0, + { restoreAfterFinish: true, + beforeSetup: function(effect) { + Element.makeClipping(effect.element); }, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); } + }); +} + +Effect.Grow = function(element) { + element = $(element); + var options = arguments[1] || {}; + + var elementDimensions = Element.getDimensions(element); + var originalWidth = elementDimensions.width; + var originalHeight = elementDimensions.height; + var oldTop = element.style.top; + var oldLeft = element.style.left; + var oldHeight = element.style.height; + var oldWidth = element.style.width; + var oldOpacity = Element.getInlineOpacity(element); + + var direction = options.direction || 'center'; + var moveTransition = options.moveTransition || Effect.Transitions.sinoidal; + var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal; + var opacityTransition = options.opacityTransition || Effect.Transitions.full; + + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = originalWidth; + initialMoveY = moveY = 0; + moveX = -originalWidth; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = originalHeight; + moveY = -originalHeight; + break; + case 'bottom-right': + initialMoveX = originalWidth; + initialMoveY = originalHeight; + moveX = -originalWidth; + moveY = -originalHeight; + break; + case 'center': + initialMoveX = originalWidth / 2; + initialMoveY = originalHeight / 2; + moveX = -originalWidth / 2; + moveY = -originalHeight / 2; + break; + } + + return new Effect.MoveBy(element, initialMoveY, initialMoveX, { + duration: 0.01, + beforeSetup: function(effect) { + Element.hide(effect.element); + Element.makeClipping(effect.element); + Element.makePositioned(effect.element); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }), + new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.style.height = 0; + Element.show(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + var el = effect.effects[0].element; + var els = el.style; + Element.undoClipping(el); + Element.undoPositioned(el); + els.top = oldTop; + els.left = oldLeft; + els.height = oldHeight; + els.width = originalWidth; + Element.setInlineOpacity(el, oldOpacity); + } + }, options) + ) + } + }); +} + +Effect.Shrink = function(element) { + element = $(element); + var options = arguments[1] || {}; + + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + var oldTop = element.style.top; + var oldLeft = element.style.left; + var oldHeight = element.style.height; + var oldWidth = element.style.width; + var oldOpacity = Element.getInlineOpacity(element); + + var direction = options.direction || 'center'; + var moveTransition = options.moveTransition || Effect.Transitions.sinoidal; + var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal; + var opacityTransition = options.opacityTransition || Effect.Transitions.none; + + var moveX, moveY; + + switch (direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = originalWidth; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = originalHeight; + break; + case 'bottom-right': + moveX = originalWidth; + moveY = originalHeight; + break; + case 'center': + moveX = originalWidth / 2; + moveY = originalHeight / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: scaleTransition, restoreAfterFinish: true}), + new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + Element.makePositioned(effect.effects[0].element); + Element.makeClipping(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + var el = effect.effects[0].element; + var els = el.style; + Element.hide(el); + Element.undoClipping(el); + Element.undoPositioned(el); + els.top = oldTop; + els.left = oldLeft; + els.height = oldHeight; + els.width = oldWidth; + Element.setInlineOpacity(el, oldOpacity); + } + }, options) + ); +} + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || {}; + var oldOpacity = Element.getInlineOpacity(element); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 3.0, from: 0, + afterFinishInternal: function(effect) { Element.setInlineOpacity(effect.element, oldOpacity); } + }, options), {transition: reverser})); +} + +Effect.Fold = function(element) { + element = $(element); + var originalTop = element.style.top; + var originalLeft = element.style.left; + var originalWidth = element.style.width; + var originalHeight = element.style.height; + Element.makeClipping(element); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); + effect.element.style.top = originalTop; + effect.element.style.left = originalLeft; + effect.element.style.width = originalWidth; + effect.element.style.height = originalHeight; + } }); + }}, arguments[1] || {})); +} diff --git a/public/javascripts/prototype.js b/public/javascripts/prototype.js new file mode 100644 index 00000000..120f4cb9 --- /dev/null +++ b/public/javascripts/prototype.js @@ -0,0 +1,1724 @@ +/* Prototype JavaScript framework, version 1.4.0_rc0 + * (c) 2005 Sam Stephenson + * + * THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff + * against the source tree, available from the Prototype darcs repository. + * + * Prototype is freely distributable under the terms of an MIT-style license. + * + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.4.0_rc0', + + emptyFunction: function() {}, + K: function(x) {return x} +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.inspect = function(object) { + try { + if (object == undefined) return 'undefined'; + if (object == null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } +} + +Function.prototype.bind = function(object) { + var __method = this; + return function() { + return __method.apply(object, arguments); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this; + return function(event) { + return __method.call(object, event || window.event); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +var Try = { + these: function() { + var returnValue; + + for (var i = 0; i < arguments.length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } + } + } +} + +/*--------------------------------------------------------------------------*/ + +function $() { + var elements = new Array(); + + for (var i = 0; i < arguments.length; i++) { + var element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + + if (arguments.length == 1) + return element; + + elements.push(element); + } + + return elements; +} +Object.extend(String.prototype, { + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + }, + + toQueryParams: function() { + var pairs = this.match(/^\??(.*)$/)[1].split('&'); + return pairs.inject({}, function(params, pairString) { + var pair = pairString.split('='); + params[pair[0]] = pair[1]; + return params; + }); + }, + + toArray: function() { + return this.split(''); + }, + + camelize: function() { + var oStringList = this.split('-'); + if (oStringList.length == 1) return oStringList[0]; + + var camelizedString = this.indexOf('-') == 0 + ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) + : oStringList[0]; + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }, + + inspect: function() { + return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'"; + } +}); + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + if (!(result &= (iterator || Prototype.K)(value, index))) + throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = true; + this.each(function(value, index) { + if (result &= (iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function (iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.collect(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value >= (result || value)) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value <= (result || value)) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.collect(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.collect(Prototype.K); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + iterator(value = collections.pluck(index)); + return value; + }); + }, + + inspect: function() { + return '#'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.length; i++) + iterator(this[i]); + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != undefined || value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0; i < this.length; i++) + if (this[i] == object) return i; + return false; + }, + + reverse: function() { + var result = []; + for (var i = this.length; i > 0; i--) + result.push(this[i-1]); + return result; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); +var Hash = { + _each: function(iterator) { + for (key in this) { + var value = this[key]; + if (typeof value == 'function') continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject($H(this), function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + return pair.map(encodeURIComponent).join('='); + }).join('&'); + }, + + inspect: function() { + return '#'; + } +} + +function $H(object) { + var hash = Object.extend({}, object || {}); + Object.extend(hash, Enumerable); + Object.extend(hash, Hash); + return hash; +} +var Range = Class.create(); +Object.extend(Range.prototype, Enumerable); +Object.extend(Range.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new Range(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')}, + function() {return new XMLHttpRequest()} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responderToAdd) { + if (!this.include(responderToAdd)) + this.responders.push(responderToAdd); + }, + + unregister: function(responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (responder[callback] && typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { + } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + parameters: '' + } + Object.extend(this.options, options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + + try { + this.url = url; + if (this.options.method == 'get') + this.url += '?' + parameters; + + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = this.onStateChange.bind(this); + setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + } + }, + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version]; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', + 'application/x-www-form-urlencoded'); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState != 1) + this.respondToReadyState(this.transport.readyState); + }, + + evalJSON: function() { + try { + var json = this.transport.getResponseHeader('X-JSON'), object; + object = eval(json); + return object; + } catch (e) { + } + }, + + respondToReadyState: function(readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; + } +}); + +Ajax.Updater = Class.create(); +Ajax.Updater.ScriptFragment = '(?:)((\n|.)*?)(?:<\/script>)'; + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.containers = { + success: container.success ? $(container.success) : $(container), + failure: container.failure ? $(container.failure) : + (container.success ? null : $(container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, object) { + this.updateContent(); + onComplete(transport, object); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + + var match = new RegExp(Ajax.Updater.ScriptFragment, 'img'); + var response = this.transport.responseText.replace(match, ''); + var scripts = this.transport.responseText.match(match); + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + receiver.innerHTML = response; + } + } + + if (this.responseIsSuccess()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + + if (this.options.evalScripts && scripts) { + match = new RegExp(Ajax.Updater.ScriptFragment, 'im'); + setTimeout((function() { + for (var i = 0; i < scripts.length; i++) + eval(scripts[i].match(match)[1]); + }).bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +document.getElementsByClassName = function(className, parentElement) { + var children = (document.body || $(parentElement)).getElementsByTagName('*'); + return $A(children).inject([], function(elements, child) { + if (Element.hasClassName(child, className)) + elements.push(child); + return elements; + }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) { + var Element = new Object(); +} + +Object.extend(Element, { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + Element[Element.visible(element) ? 'hide' : 'show'](element); + } + }, + + hide: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = 'none'; + } + }, + + show: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = ''; + } + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).include(className); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).add(className); + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).remove(className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + scrollTo: function(element) { + element = $(element); + var x = element.x ? element.x : element.offsetLeft, + y = element.y ? element.y : element.offsetTop; + window.scrollTo(x, y); + }, + + getStyle: function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + } + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + + return value == 'auto' ? null : value; + }, + + getDimensions: function(element) { + element = $(element); + if (Element.getStyle(element, 'display') != 'none') + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = ''; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = 'none'; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return; + element._overflow = element.style.overflow; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + }, + + undoClipping: function(element) { + element = $(element); + if (element._overflow) return; + element.style.overflow = element._overflow; + element._overflow = undefined; + } +}); + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content; + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + if (this.element.tagName.toLowerCase() == 'tbody') { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
    '; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse().each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set(this.toArray().concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set(this.select(function(className) { + return className != classNameToRemove; + })); + }, + + toString: function() { + return this.toArray().join(' '); + } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + for (var i = 0; i < arguments.length; i++) + if ($(arguments[i]).value == '') return false; + return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + $(element).focus(); + $(element).select(); + } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); + } + + return queryComponents.join('&'); + }, + + getElements: function(form) { + var form = $(form); + var elements = new Array(); + + for (tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); + } + return elements; + }, + + getInputs: function(form, typeName, name) { + var form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); + } + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + focusFirstElement: function(form) { + var form = $(form); + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + if (element.type != 'hidden' && !element.disabled) { + Field.activate(element); + break; + } + } + }, + + reset: function(form) { + $(form).reset(); + } +} + +Form.Element = { + serialize: function(element) { + var element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return encodeURIComponent(parameter[0]) + '=' + + encodeURIComponent(parameter[1]); + }, + + getValue: function(element) { + var element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return Form.Element.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + } + return false; + }, + + inputSelector: function(element) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value; + if (!value && !('value' in opt)) + value = opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = new Array(); + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) { + var optValue = opt.value; + if (!optValue && !('value' in opt)) + optValue = opt.text; + value.push(optValue); + } + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + element.target = this; + element.prev_onclick = element.onclick || Prototype.emptyFunction; + element.onclick = function() { + this.prev_onclick(); + this.target.onElementEvent(); + } + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + element.target = this; + element.prev_onchange = element.onchange || Prototype.emptyFunction; + element.onchange = function() { + this.prev_onchange(); + this.target.onElementEvent(); + } + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px';; + element.style.left = left + 'px';; + element.style.width = width + 'px';; + element.style.height = height + 'px';; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +} \ No newline at end of file diff --git a/public/javascripts/scriptaculous.js b/public/javascripts/scriptaculous.js new file mode 100644 index 00000000..cd0e3417 --- /dev/null +++ b/public/javascripts/scriptaculous.js @@ -0,0 +1,47 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +var Scriptaculous = { + Version: '1.5_rc3', + require: function(libraryName) { + // inserting via DOM fails in Safari 2.0, so brute force approach + document.write(''); + }, + load: function() { + if((typeof Prototype=='undefined') || + parseFloat(Prototype.Version.split(".")[0] + "." + + Prototype.Version.split(".")[1]) < 1.4) + throw("script.aculo.us requires the Prototype JavaScript framework >= 1.4.0"); + var scriptTags = document.getElementsByTagName("script"); + for(var i=0;i this.maximum) sliderValue = this.maximum; + if(sliderValue < this.minimum) sliderValue = this.minimum; + var offsetDiff = (sliderValue - (this.value||this.minimum)) * this.increment; + + if(this.isVertical()){ + this.setCurrentTop(offsetDiff + this.currentTop()); + } else { + this.setCurrentLeft(offsetDiff + this.currentLeft()); + } + this.value = sliderValue; + this.updateFinished(); + }, + minimumOffset: function(){ + return(this.isVertical() ? + this.trackTop() + this.alignY : + this.trackLeft() + this.alignX); + }, + maximumOffset: function(){ + return(this.isVertical() ? + this.trackTop() + this.alignY + (this.maximum - this.minimum) * this.increment : + this.trackLeft() + this.alignX + (this.maximum - this.minimum) * this.increment); + }, + isVertical: function(){ + return (this.axis == 'vertical'); + }, + startDrag: function(event) { + if(Event.isLeftClick(event)) { + if(!this.disabled){ + this.active = true; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.handle); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + } + Event.stop(event); + } + }, + update: function(event) { + if(this.active) { + if(!this.dragging) { + var style = this.handle.style; + this.dragging = true; + if(style.position=="") style.position = "relative"; + style.zIndex = this.options.zindex; + } + this.draw(event); + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + Event.stop(event); + } + }, + draw: function(event) { + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.handle); + + offsets[0] -= this.currentLeft(); + offsets[1] -= this.currentTop(); + + // Adjust for the pointer's position on the handle + pointer[0] -= this.offsetX; + pointer[1] -= this.offsetY; + var style = this.handle.style; + + if(this.isVertical()){ + if(pointer[1] > this.maximumOffset()) + pointer[1] = this.maximumOffset(); + if(pointer[1] < this.minimumOffset()) + pointer[1] = this.minimumOffset(); + + // Increment by values + if(this.values){ + this.value = this.getNearestValue(Math.round((pointer[1] - this.minimumOffset()) / this.increment) + this.minimum); + pointer[1] = this.trackTop() + this.alignY + (this.value - this.minimum) * this.increment; + } else { + this.value = Math.round((pointer[1] - this.minimumOffset()) / this.increment) + this.minimum; + } + style.top = pointer[1] - offsets[1] + "px"; + } else { + if(pointer[0] > this.maximumOffset()) pointer[0] = this.maximumOffset(); + if(pointer[0] < this.minimumOffset()) pointer[0] = this.minimumOffset(); + // Increment by values + if(this.values){ + this.value = this.getNearestValue(Math.round((pointer[0] - this.minimumOffset()) / this.increment) + this.minimum); + pointer[0] = this.trackLeft() + this.alignX + (this.value - this.minimum) * this.increment; + } else { + this.value = Math.round((pointer[0] - this.minimumOffset()) / this.increment) + this.minimum; + } + style.left = (pointer[0] - offsets[0]) + "px"; + } + if(this.options.onSlide) this.options.onSlide(this.value); + }, + endDrag: function(event) { + if(this.active && this.dragging) { + this.finishDrag(event, true); + Event.stop(event); + } + this.active = false; + this.dragging = false; + }, + finishDrag: function(event, success) { + this.active = false; + this.dragging = false; + this.handle.style.zIndex = this.originalZ; + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + this.updateFinished(); + }, + updateFinished: function() { + if(this.options.onChange) this.options.onChange(this.value); + }, + keyPress: function(event) { + if(this.active && !this.disabled) { + switch(event.keyCode) { + case Event.KEY_ESC: + this.finishDrag(event, false); + Event.stop(event); + break; + } + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + } + } +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..4ab9e89f --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file \ No newline at end of file diff --git a/public/stylesheets/instiki.css b/public/stylesheets/instiki.css new file mode 100644 index 00000000..e676f3b9 --- /dev/null +++ b/public/stylesheets/instiki.css @@ -0,0 +1,320 @@ +body { +background-color:#FFF; +color:#333; +font-family:Verdana, Arial, Helvetica, sans-serif; +font-size:90%; +line-height:1.3em; +} + +#Container { +float:none; +margin:0 15%; +text-align:center; +} + +#Content { +border-top:none; +float:left; +margin:0; +padding:0.3em; +text-align:left; +width:100%; +} + +a:visited { +color:#666; +} + +h1,h2,h3 { +color:#333; +font-family:georgia, verdana, sans-serif; +} + +h1 { +font-size:200%; +} + +h2 { +font-size:130%; +} + +h3 { +font-size:120%; +} + +h1#pageName { +line-height:1em; +margin:0.2em 0 0; +padding:0; +} + +h1#pageName small { +color:#444; +font-size:35%; +line-height:1em; +padding:0; +} + +a.nav,a.nav:link,a.nav:visited { +background-color:#FFF; +color:#000; +} + +table { +border:double #000; +border-collapse:collapse; +} + +td { +border:thin solid #888; +} + +li { +margin-bottom:0.5em; +} + +.newWikiWord { +background-color:#DDD; +} + +.newWikiWord a:hover { +background-color:#FFF; +} + +form#navigationSearchForm { +display:inline; +} + +form#navigationSearchForm input { +font-size:80%; +} + +.navigation { +color:#999; +font-size:90%; +margin-top:0.3em; +} + +.navigation a:hover { +background-color:#000; +color:#FFF; +text-decoration:none; +} + +.navigation a { +color:#000; +font-weight:bold; +} + +.navigation small a { +font-size:90%; +font-weight:normal; +} + +.navOn { +color:#444; +font-weight:bold; +text-decoration:none; +} + +div.help { +font-family:verdana, arial, helvetica, sans-serif; +font-size:70%; +} + +div.inputBox { +background-color:#EEE; +font-family:verdana, arial, helvetica, sans-serif; +font-size:80%; +margin-bottom:1.5em; +padding:0.3em; +} + +blockquote { +display:block; +font-size:90%; +font-style:italic; +line-height:1.5em; +margin:0 0 1.5em; +padding:0 2.5em; +} + +pre { +background-color:#DDD; +font-size:90%; +overflow:auto; +padding:1em; +} + +ol.setup { +font-family:georgia, verdana, sans-serif; +font-size:110%; +margin-top:1em; +padding-left:1.5em; +} + +.byline { +color:#999; +font-size:65%; +font-style:italic; +margin-bottom:1em; +padding-top:1px; +} + +.diffdel,del.diffmod { +background-color:#FAA; +} + +.diffins,ins.diffmod { +background-color:#AFA; +} + +#footer { +color:#999; +font-size:60%; +font-style:italic; +line-height:1.2em; +padding-top:2em; +text-align:right; +} + +#footer a:link,#footer a:visited { +color:#888; +font-style:italic; +} + +div.web_normal { + padding:4px; +} + +div.web_protected { + padding:4px; + background-color:#DDD; +} + +div.inputFieldWithPrompt { +margin:0.75em 0; +} + +div.errorExplanation { +background-color:#FFA; +color:#900; +font-style:italic; +font-weight:bold; +margin:1.5em 0; +padding:1em; +width:100%; +} + +div.errorExplanation h2 { +display:none; +} + +div.errorExplanation ul { +border:none; +margin:0.5em 0 0 2em; +padding:0; +} + +div.fieldWithErrors input { +border:1px solid #900; +} + +div.info { +background-color:#DDD; +color:#060; +font-weight:bold; +margin-top:0.5em; +padding:0.5em; +width:100%; +} + +div#editFormButtons { +margin:0.5em 0 0; +} + +div#editFormButtons span { +white-space:nowrap; +} + +div#editForm textarea#content { +height:400px; +width:70%; +} + +div#MarkupHelp { +float:right; +margin-top:0.5em; +width:25%; +} + +div#MarkupHelp table { +border-bottom:3px solid #BBB; +border-left:3px solid #999; +border-right:3px solid #BBB; +border-top:3px solid #999; +margin-bottom:0; +} + +div#MarkupHelp td { +border:1px solid #999; +border-width:1px 0; +font-size:80%; +margin:0; +padding:0.2em; +vertical-align:top; +white-space:nowrap; +} + +div#MarkupHelp td.arrow { +color:#999; +padding:0 0.75em; +} + +div#MarkupHelp h3 { +font-size:90%; +font-weight:bold; +margin:0 0 5px; +padding:5px 0 0; +} + +div#MarkupHelp p { +font-size:70%; +} + +div.rightHandSide { +border-left:1px dotted #ccc; +float:right; +font-size:80%; +margin-left:0.7em; +padding-left:1.5em; +width:25%; +} + +.newsList { +margin-top:1.5em; +} + +.newsList p { +margin-bottom:2.5em; +} + +.property { +color:#999; +font-size:80%; +} + +a,li span { +color:#000; +} + +a:hover,a.nav:hover { +background-color:#000; +color:#FFF; +} + +div.errorExplanation p,div.errorExplanation li { +border:none; +margin:0; +padding:0; +} \ No newline at end of file diff --git a/rakefile.rb b/rakefile.rb new file mode 100755 index 00000000..cffd19f0 --- /dev/null +++ b/rakefile.rb @@ -0,0 +1,10 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake. + +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' \ No newline at end of file diff --git a/script/benchmarker b/script/benchmarker new file mode 100755 index 00000000..4a0ea231 --- /dev/null +++ b/script/benchmarker @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby + +if ARGV.empty? + puts "Usage: benchmarker times 'Person.expensive_way' 'Person.another_expensive_way' ..." + exit +end + +require File.dirname(__FILE__) + '/../config/environment' +require 'benchmark' +include Benchmark + +# Don't include compilation in the benchmark +ARGV[1..-1].each { |expression| eval(expression) } + +bm(6) do |x| + ARGV[1..-1].each_with_index do |expression, idx| + x.report("##{idx + 1}") { ARGV[0].to_i.times { eval(expression) } } + end +end \ No newline at end of file diff --git a/script/breakpointer b/script/breakpointer new file mode 100755 index 00000000..6375616b --- /dev/null +++ b/script/breakpointer @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require 'rubygems' +require_gem 'rails' +require 'breakpoint_client' diff --git a/script/console b/script/console new file mode 100755 index 00000000..e23abda3 --- /dev/null +++ b/script/console @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby +irb = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb' + +require 'optparse' +options = { :sandbox => false, :irb => irb } +OptionParser.new do |opt| + opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |options[:sandbox]| } + opt.on("--irb=[#{irb}]", 'Invoke a different irb.') { |options[:irb]| } + opt.parse!(ARGV) +end + +libs = " -r irb/completion" +libs << " -r #{File.dirname(__FILE__)}/../config/environment" +libs << " -r console_sandbox" if options[:sandbox] + +ENV['RAILS_ENV'] = ARGV.first || 'development' +if options[:sandbox] + puts "Loading #{ENV['RAILS_ENV']} environment in sandbox." + puts "Any modifications you make will be rolled back on exit." +else + puts "Loading #{ENV['RAILS_ENV']} environment." +end +exec "#{options[:irb]} #{libs} --prompt-mode simple" diff --git a/script/destroy b/script/destroy new file mode 100755 index 00000000..46cc786e --- /dev/null +++ b/script/destroy @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/environment' +require 'rails_generator' +require 'rails_generator/scripts/destroy' + +ARGV.shift if ['--help', '-h'].include?(ARGV[0]) +Rails::Generator::Scripts::Destroy.new.run(ARGV) diff --git a/script/generate b/script/generate new file mode 100755 index 00000000..26447804 --- /dev/null +++ b/script/generate @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/environment' +require 'rails_generator' +require 'rails_generator/scripts/generate' + +ARGV.shift if ['--help', '-h'].include?(ARGV[0]) +Rails::Generator::Scripts::Generate.new.run(ARGV) diff --git a/script/import_storage b/script/import_storage new file mode 100755 index 00000000..7c53af9e --- /dev/null +++ b/script/import_storage @@ -0,0 +1,228 @@ +#!/usr/bin/env ruby + +require 'optparse' + +OPTIONS = { + :instiki_root => nil, + :storage => nil, + :database => 'mysql' +} + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: ruby #{script_name} [options]" + + opts.separator "" + + opts.on("-t", "--storage /full/path/to/storage", String, + "Full path to your storage, ", + "such as /home/joe/instiki/storage/2500", + "It should be the directory that ", + "contains .snapshot files.") do |storage| + OPTIONS[:storage] = storage + end + + opts.separator "" + + opts.on("-i", "--instiki /full/path/to/instiki", String, + "Full path to your Instiki 0.10 installation, ", + "such as /home/joe/instiki-0.10.2") do |instiki| + OPTIONS[:instiki] = instiki + end + + opts.separator "" + + opts.on("-o", "--outfile /full/path/to/output_file", String, + "Full path (including filename!) to where ", + "you want the SQL output placed, such as ", + "/home/joe/instiki.sql") do |outfile| + OPTIONS[:outfile] = outfile + end + + opts.on("-d", "--database {mysql|sqlite|postgres}", String, + "Target database (they have slightly different syntax)", + "default: mysql") do |database| + OPTIONS[:database] = database + end + + opts.separator "" + + opts.on_tail("-h", "--help", + "Show this help message.") { puts opts; exit } + + opts.parse! +end + +if OPTIONS[:instiki].nil? or OPTIONS[:storage].nil? or OPTIONS[:outfile].nil? + $stderr.puts "Please specify full paths to Instiki 0.10 installation and storage," + $stderr.puts "as well as the path to the output file" + $stderr.puts + puts ARGV.options + exit -1 +end + +if FileTest.exists? OPTIONS[:outfile] + $stderr.puts "Output file #{OPTIONS[:outfile]} already exists!" + $stderr.puts "Please specify a new file" + $stderr.puts + puts ARGV.options + exit -1 +end + +raise "Directory #{OPTIONS[:instiki]} not found" unless File.directory?(OPTIONS[:instiki]) +raise "Directory #{OPTIONS[:storage]} not found" unless File.directory?(OPTIONS[:storage]) + +expected_page_rb_path = File.join(OPTIONS[:instiki], 'app/models/page.rb') +raise "Instiki installation not found in #{OPTIONS[:instiki]}" unless File.file?(expected_page_rb_path) + +expected_snapshot_pattern = File.join(OPTIONS[:storage], '*.snapshot') +raise "No snapshots found in #{expected_snapshot_pattern}" if Dir[expected_snapshot_pattern].empty? + +INSTIKI_ROOT = File.expand_path(OPTIONS[:instiki]) + +ADDITIONAL_LOAD_PATHS = %w( + app/models + lib + vendor/madeleine-0.7.1/lib + vendor/RedCloth-3.0.3/lib + vendor/RedCloth-3.0.4/lib + vendor/rubyzip-0.5.8/lib +).map { |dir| "#{File.expand_path(File.join(INSTIKI_ROOT, dir))}" +}.delete_if { |dir| not File.exist?(dir) } + +# Prepend to $LOAD_PATH +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } + +require 'webrick' +require 'wiki_service' + +# substitute an extremely expensive method with something cheap. +class Revision + alias :__display_content :display_content + def display_content + return self + end +end + +class Time + def ansi + strftime('%Y-%m-%d %H:%M:%S') + end +end + +def sql_insert(table, hash) + columns = hash.keys + + values = columns.map { |column| hash[column] } + values = values.map do |value| + if value.nil? + 'NULL' + else + if (value == false or value == true) and OPTIONS[:database] == 'mysql' + value = value ? '1' : '0' + end + + case OPTIONS[:database] + when 'mysql', 'postgres' + value = value.to_s.gsub("'", "\\\\'") + when 'sqlite' + value = value.to_s.gsub("'", "''") + else + raise "Unsupported database option #{OPTIONS[:database]}" + end + "'#{value.gsub("\r\n", "\n")}'" + end + end + + output = "INSERT INTO #{table} (" + output << columns.join(", ") + + output << ") VALUES (" + output << values.join(", ") + output << ");" + output +end + +def delete_all(outfile) + %w(wiki_references revisions pages system webs).each { |table| outfile.puts "DELETE FROM #{table};" } +end + +def next_id(key) + $ids ||= {} + if $ids[key].nil? + $ids[key] = 1 + else + $ids[key] = $ids[key] + 1 + end + $ids[key] +end + +def current_id(key) + $ids[key] or raise "No curent ID for #{key.inspect}" +end + +WikiService.storage_path = OPTIONS[:storage] +wiki = WikiService.instance + +File.open(OPTIONS[:outfile], 'w') { |outfile| + + outfile.puts "BEGIN;" + delete_all(outfile) + outfile.puts "COMMIT;" + + wiki.webs.each_pair do |web_name, web| + outfile.puts "BEGIN;" + outfile.puts sql_insert(:webs, { + :id => next_id(:web), + :name => web.name, + :address => web.address, + :password => web.password, + :additional_style => web.additional_style, + :allow_uploads => web.allow_uploads, + :published => web.published, + :count_pages => web.count_pages, + :markup => web.markup, + :color => web.color, + :max_upload_size => web.max_upload_size, + :safe_mode => web.safe_mode, + :brackets_only => web.brackets_only, + :created_at => web.pages.values.map { |p| p.revisions.first.created_at }.min.ansi, + :updated_at => web.pages.values.map { |p| p.revisions.last.created_at }.max.ansi + }) + outfile.puts "COMMIT;" + + puts "Web #{web_name} has #{web.pages.keys.size} pages" + web.pages.each_pair do |page_name, page| + + outfile.puts "BEGIN;" + + outfile.puts sql_insert(:pages, { + :id => next_id(:page), + :web_id => current_id(:web), + :locked_by => page.locked_by, + :name => page.name, + :created_at => page.revisions.first.created_at.ansi, + :updated_at => page.revisions.last.created_at.ansi + }) + + puts " Page #{page_name} has #{page.revisions.size} revisions" + page.revisions.each_with_index do |rev, i| + + outfile.puts sql_insert(:revisions, { + :id => next_id(:revision), + :page_id => current_id(:page), + :content => rev.content, + :author => rev.author.to_s, + :ip => (rev.author.is_a?(Author) ? rev.author.ip : 'N/A'), + :created_at => rev.created_at.ansi, + :updated_at => rev.created_at.ansi, + :revised_at => rev.created_at.ansi + }) + puts " Revision #{i} created at #{rev.created_at.ansi}" + end + + outfile.puts "COMMIT;" + + end + end +} diff --git a/script/profiler b/script/profiler new file mode 100755 index 00000000..77c9fbef --- /dev/null +++ b/script/profiler @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +if ARGV.empty? + $stderr.puts "Usage: profiler 'Person.expensive_method(10)' [times]" + exit(1) +end + +# Keep the expensive require out of the profile. +$stderr.puts 'Loading Rails...' +require File.dirname(__FILE__) + '/../config/environment' + +# Define a method to profile. +if ARGV[1] and ARGV[1].to_i > 1 + eval "def profile_me() #{ARGV[1]}.times { #{ARGV[0]} } end" +else + eval "def profile_me() #{ARGV[0]} end" +end + +# Use the ruby-prof extension if available. Fall back to stdlib profiler. +begin + require 'prof' + $stderr.puts 'Using the ruby-prof extension.' + Prof.clock_mode = Prof::GETTIMEOFDAY + Prof.start + profile_me + results = Prof.stop + require 'rubyprof_ext' + Prof.print_profile(results, $stderr) +rescue LoadError + $stderr.puts 'Using the standard Ruby profiler.' + Profiler__.start_profile + profile_me + Profiler__.stop_profile + Profiler__.print_profile($stderr) +end diff --git a/script/reset_references b/script/reset_references new file mode 100755 index 00000000..dd9ffb4a --- /dev/null +++ b/script/reset_references @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby + +ENV['RAILS_ENV'] = ARGV.first || 'development' + +$stderr.puts "Loading Rails for #{ENV['RAILS_ENV']} environment..." +require File.dirname(__FILE__) + '/../config/environment' + +class StubUrlGenerator + def make_link(*args) + 'StubLink' + end +end + +PageRenderer.setup_url_generator(StubUrlGenerator.new) +WikiReference.delete_all + +Web.find_all.each do |web| + web.pages.find(:all, :order => 'name').each do |page| + $stderr.puts "Processing page '#{page.name}'" + begin + PageRenderer.new(page.current_revision).display_content(update_references = true) + rescue => e + puts e + puts e.backtrace + end + end +end + diff --git a/script/runner b/script/runner new file mode 100755 index 00000000..9c8bbb13 --- /dev/null +++ b/script/runner @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'optparse' + +options = { :environment => "development" } + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: runner 'puts Person.find(1).name' [options]" + + opts.separator "" + + opts.on("-e", "--environment=name", String, + "Specifies the environment for the runner to operate under (test/development/production).", + "Default: development") { |options[:environment]| } + + opts.separator "" + + opts.on("-h", "--help", + "Show this help message.") { puts opts; exit } + + opts.parse! +end + +ENV["RAILS_ENV"] = options[:environment] + +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../config/environment' +eval(ARGV.first) \ No newline at end of file diff --git a/script/server b/script/server new file mode 100755 index 00000000..27a7989a --- /dev/null +++ b/script/server @@ -0,0 +1,49 @@ +#!/usr/bin/env ruby + +require 'webrick' +require 'optparse' + +OPTIONS = { + :port => 2500, + :ip => "0.0.0.0", + :environment => "production", + :server_root => File.expand_path(File.dirname(__FILE__) + "/../public/"), + :server_type => WEBrick::SimpleServer +} + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: ruby #{script_name} [options]" + + opts.separator "" + + opts.on("-p", "--port=port", Integer, + "Runs Instiki on the specified port.", + "Default: 2500") { |OPTIONS[:port]| } + opts.on("-b", "--binding=ip", String, + "Binds Instiki to the specified ip.", + "Default: 0.0.0.0") { |OPTIONS[:ip]| } + opts.on("-e", "--environment=name", String, + "Specifies the environment to run this server under (test/development/production).", + "Default: development") { |OPTIONS[:environment]| } + opts.on("-d", "--daemon", + "Make Instiki run as a Daemon (only works if fork is available -- meaning on *nix)." + ) { OPTIONS[:server_type] = WEBrick::Daemon } + + opts.separator "" + + opts.on("-h", "--help", + "Show this help message.") { puts opts; exit } + + opts.parse! +end + +ENV["RAILS_ENV"] = OPTIONS[:environment] +require File.dirname(__FILE__) + "/../config/environment" +require 'webrick_server' + +OPTIONS['working_directory'] = File.expand_path(RAILS_ROOT) + +puts "=> Instiki started on http://#{OPTIONS[:ip]}:#{OPTIONS[:port]}" +puts "=> Ctrl-C to shutdown; call with --help for options" if OPTIONS[:server_type] == WEBrick::SimpleServer +DispatchServlet.dispatch(OPTIONS) diff --git a/test/fixtures/exported_markup.zip b/test/fixtures/exported_markup.zip new file mode 100644 index 0000000000000000000000000000000000000000..565834b05c3195cf64b6d222d9a221ace664552f GIT binary patch literal 283 zcmWIWW@h1H009YxauYUt(H&wyHV6vsE_VdlWZ6krB0GKnzYb_UQC21W)25CsJ2 mjtcNb=*H#%kRFI`7a$8}HL|w?yjj^ms+fSV0Z6xkI1B*8r#CYI literal 0 HcmV?d00001 diff --git a/test/fixtures/pages.yml b/test/fixtures/pages.yml new file mode 100644 index 00000000..65fba487 --- /dev/null +++ b/test/fixtures/pages.yml @@ -0,0 +1,55 @@ +home_page: + id: 1 + created_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %> + updated_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %> + web_id: 1 + name: HomePage + +my_way: + id: 2 + created_at: <%= 9.days.ago.to_formatted_s(:db) %> + updated_at: <%= 9.days.ago.to_formatted_s(:db) %> + web_id: 1 + name: MyWay + +smart_engine: + id: 3 + created_at: <%= 8.days.ago.to_formatted_s(:db) %> + updated_at: <%= 8.days.ago.to_formatted_s(:db) %> + web_id: 1 + name: SmartEngine + +that_way: + id: 4 + created_at: <%= 7.days.ago.to_formatted_s(:db) %> + updated_at: <%= 7.days.ago.to_formatted_s(:db) %> + web_id: 1 + name: ThatWay + +no_wiki_word: + id: 5 + created_at: <%= 6.days.ago.to_formatted_s(:db) %> + updated_at: <%= 6.days.ago.to_formatted_s(:db) %> + web_id: 1 + name: NoWikiWord + +first_page: + id: 6 + created_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %> + updated_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %> + web_id: 1 + name: FirstPage + +oak: + id: 7 + created_at: <%= 5.days.ago.to_formatted_s(:db) %> + updated_at: <%= 5.days.ago.to_formatted_s(:db) %> + web_id: 1 + name: Oak + +elephant: + id: 8 + created_at: <%= 10.minutes.ago.to_formatted_s(:db) %> + updated_at: <%= 10.minutes.ago.to_formatted_s(:db) %> + web_id: 1 + name: Elephant \ No newline at end of file diff --git a/test/fixtures/rails.gif b/test/fixtures/rails.gif new file mode 100755 index 0000000000000000000000000000000000000000..58960ee4f9b9e619ab880c2a9dc2685ada0a9253 GIT binary patch literal 8533 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/revisions.yml b/test/fixtures/revisions.yml new file mode 100644 index 00000000..d65c2bd2 --- /dev/null +++ b/test/fixtures/revisions.yml @@ -0,0 +1,83 @@ +home_page_first_revision: + id: 1 + created_at: <%= Time.local(2004, 4, 4, 15, 50).to_formatted_s(:db) %> + updated_at: <%= Time.local(2004, 4, 4, 15, 50).to_formatted_s(:db) %> + revised_at: <%= Time.local(2004, 4, 4, 15, 50).to_formatted_s(:db) %> + page_id: 1 + content: First revision of the HomePage end + author: AnAuthor + ip: 127.0.0.1 + +my_way_first_revision: + id: 2 + created_at: <%= 9.days.ago.to_formatted_s(:db) %> + updated_at: <%= 9.days.ago.to_formatted_s(:db) %> + revised_at: <%= 9.days.ago.to_formatted_s(:db) %> + page_id: 2 + content: MyWay + author: Me + +smart_engine_first_revision: + id: 3 + created_at: <%= 8.days.ago.to_formatted_s(:db) %> + updated_at: <%= 8.days.ago.to_formatted_s(:db) %> + revised_at: <%= 8.days.ago.to_formatted_s(:db) %> + page_id: 3 + content: SmartEngine + author: Me + +that_way_first_revision: + id: 4 + created_at: <%= 7.days.ago.to_formatted_s(:db) %> + updated_at: <%= 7.days.ago.to_formatted_s(:db) %> + revised_at: <%= 7.days.ago.to_formatted_s(:db) %> + page_id: 4 + content: ThatWay + author: Me + +no_wiki_word_first_revision: + id: 5 + created_at: <%= 6.days.ago.to_formatted_s(:db) %> + updated_at: <%= 6.days.ago.to_formatted_s(:db) %> + revised_at: <%= 6.days.ago.to_formatted_s(:db) %> + page_id: 5 + content: hey you + author: Me + +home_page_second_revision: + id: 6 + created_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %> + updated_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %> + revised_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %> + page_id: 1 + content: HisWay would be MyWay in kinda ThatWay in HisWay though MyWay \OverThere -- see SmartEngine in that SmartEngineGUI + author: DavidHeinemeierHansson + +first_page_first_revision: + id: 7 + created_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %> + updated_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %> + revised_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %> + page_id: 6 + content: HisWay would be MyWay in kinda ThatWay in HisWay though MyWay \\OverThere -- see SmartEngine in that SmartEngineGUI + author: DavidHeinemeierHansson + +oak_first_revision: + id: 8 + created_at: <%= 5.days.ago.to_formatted_s(:db) %> + updated_at: <%= 5.days.ago.to_formatted_s(:db) %> + revised_at: <%= 5.days.ago.to_formatted_s(:db) %> + page_id: 7 + content: "All about oak.\ncategory: trees" + author: TreeHugger + ip: 127.0.0.2 + +elephant_first_revision: + id: 9 + created_at: <%= 10.minutes.ago.to_formatted_s(:db) %> + updated_at: <%= 10.minutes.ago.to_formatted_s(:db) %> + revised_at: <%= 10.minutes.ago.to_formatted_s(:db) %> + page_id: 8 + content: "All about elephants.\ncategory: animals" + author: Guest + ip: 127.0.0.2 diff --git a/test/fixtures/system.yml b/test/fixtures/system.yml new file mode 100644 index 00000000..1b17f2fc --- /dev/null +++ b/test/fixtures/system.yml @@ -0,0 +1,2 @@ +system: + password: test_password diff --git a/test/fixtures/webs.yml b/test/fixtures/webs.yml new file mode 100644 index 00000000..05437295 --- /dev/null +++ b/test/fixtures/webs.yml @@ -0,0 +1,15 @@ +test_wiki: + id: 1 + created_at: 2004-08-01 + updated_at: 2005-08-01 + name: wiki1 + address: wiki1 + markup: textile + +instiki: + id: 2 + created_at: 2004-08-01 + updated_at: 2005-08-01 + name: Instiki + address: instiki + markup: textile \ No newline at end of file diff --git a/test/fixtures/wiki_references.yml b/test/fixtures/wiki_references.yml new file mode 100644 index 00000000..542a3013 --- /dev/null +++ b/test/fixtures/wiki_references.yml @@ -0,0 +1,112 @@ +my_way_1: + id: 1 + page_id: 2 + referenced_name: MyWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +smart_engine_1: + id: 2 + page_id: 3 + referenced_name: SmartEngine + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +that_way_1: + id: 3 + page_id: 4 + referenced_name: ThatWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +home_page_1: + id: 4 + page_id: 1 + referenced_name: HisWay + link_type: W + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +home_page_2: + id: 5 + page_id: 1 + referenced_name: MyWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +home_page_3: + id: 6 + page_id: 1 + referenced_name: ThatWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +home_page_4: + id: 7 + page_id: 1 + referenced_name: SmartEngine + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +first_page_1: + id: 8 + page_id: 6 + referenced_name: HisWay + link_type: W + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +first_page_2: + id: 9 + page_id: 6 + referenced_name: MyWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +first_page_3: + id: 10 + page_id: 6 + referenced_name: ThatWay + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +first_page_4: + id: 11 + page_id: 6 + referenced_name: OverThere + link_type: W + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +first_page_5: + id: 12 + page_id: 6 + referenced_name: SmartEngine + link_type: L + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +oak_1: + id: 13 + page_id: 7 + referenced_name: trees + link_type: C + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + +elephant_1: + id: 14 + page_id: 8 + referenced_name: animals + link_type: C + created_at: <%= Time.now.to_formatted_s(:db) %> + updated_at: <%= Time.now.to_formatted_s(:db) %> + \ No newline at end of file diff --git a/test/functional/admin_controller_test.rb b/test/functional/admin_controller_test.rb new file mode 100644 index 00000000..6df919f3 --- /dev/null +++ b/test/functional/admin_controller_test.rb @@ -0,0 +1,234 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') +require 'admin_controller' + +# Raise errors beyond the default web-based presentation +class AdminController; def rescue_action(e) logger.error(e); raise e end; end + +class AdminControllerTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system, :wiki_references + + def setup + @controller = AdminController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @wiki = Wiki.new + @oak = pages(:oak) + @elephant = pages(:elephant) + @web = webs(:test_wiki) + @home = @page = pages(:home_page) + end + + def test_create_system_form_displayed + use_blank_wiki + process('create_system') + assert_response :success + end + + def test_create_system_form_submitted + use_blank_wiki + assert !@wiki.setup? + + process('create_system', 'password' => 'a_password', 'web_name' => 'My Wiki', + 'web_address' => 'my_wiki') + + assert_redirected_to :web => 'my_wiki', :controller => 'wiki', :action => 'new', + :id => 'HomePage' + assert @wiki.setup? + assert_equal 'a_password', @wiki.system[:password] + assert_equal 1, @wiki.webs.size + new_web = @wiki.webs['my_wiki'] + assert_equal 'My Wiki', new_web.name + assert_equal 'my_wiki', new_web.address + end + + def test_create_system_form_submitted_and_wiki_already_initialized + wiki_before = @wiki + old_size = @wiki.webs.size + assert @wiki.setup? + + process 'create_system', 'password' => 'a_password', 'web_name' => 'My Wiki', + 'web_address' => 'my_wiki' + + assert_redirected_to :web => @wiki.webs.keys.first, :action => 'show', :id => 'HomePage' + assert_equal wiki_before, @wiki + # and no new web should be created either + assert_equal old_size, @wiki.webs.size + assert_flash_has :error + end + + def test_create_system_no_form_and_wiki_already_initialized + assert @wiki.setup? + process('create_system') + assert_redirected_to :web => @wiki.webs.keys.first, :action => 'show', :id => 'HomePage' + assert_flash_has :error + end + + + def test_create_web + @wiki.system.update_attribute(:password, 'pswd') + + process 'create_web', 'system_password' => 'pswd', 'name' => 'Wiki Two', 'address' => 'wiki2' + + assert_redirected_to :web => 'wiki2', :action => 'new', :id => 'HomePage' + wiki2 = @wiki.webs['wiki2'] + assert wiki2 + assert_equal 'Wiki Two', wiki2.name + assert_equal 'wiki2', wiki2.address + end + + def test_create_web_default_password + @wiki.system.update_attribute(:password, nil) + + process 'create_web', 'system_password' => 'instiki', 'name' => 'Wiki Two', 'address' => 'wiki2' + + assert_redirected_to :web => 'wiki2', :action => 'new', :id => 'HomePage' + end + + def test_create_web_failed_authentication + @wiki.system.update_attribute(:password, 'pswd') + + process 'create_web', 'system_password' => 'wrong', 'name' => 'Wiki Two', 'address' => 'wiki2' + + assert_redirected_to :web => nil, :action => 'index' + assert_nil @wiki.webs['wiki2'] + end + + def test_create_web_no_form_submitted + @wiki.system.update_attribute(:password, 'pswd') + process 'create_web' + assert_response :success + end + + + def test_edit_web_no_form + process 'edit_web', 'web' => 'wiki1' + # this action simply renders a form + assert_response :success + end + + def test_edit_web_form_submitted + @wiki.system.update_attribute(:password, 'pswd') + + process('edit_web', 'system_password' => 'pswd', + 'web' => 'wiki1', 'address' => 'renamed_wiki1', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'safe_mode' => 'on', 'password' => 'new_password', 'published' => 'on', + 'brackets_only' => 'on', 'count_pages' => 'on', 'allow_uploads' => 'on', + 'max_upload_size' => '300') + + assert_redirected_to :web => 'renamed_wiki1', :action => 'show', :id => 'HomePage' + @web = Web.find(@web.id) + assert_equal 'renamed_wiki1', @web.address + assert_equal 'Renamed Wiki1', @web.name + assert_equal :markdown, @web.markup + assert_equal 'blue', @web.color + assert @web.safe_mode? + assert_equal 'new_password', @web.password + assert @web.published? + assert @web.brackets_only? + assert @web.count_pages? + assert @web.allow_uploads? + assert_equal 300, @web.max_upload_size + end + + def test_edit_web_opposite_values + @wiki.system.update_attribute(:password, 'pswd') + + process('edit_web', 'system_password' => 'pswd', + 'web' => 'wiki1', 'address' => 'renamed_wiki1', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'password' => 'new_password') + # safe_mode, published, brackets_only, count_pages, allow_uploads not set + # and should become false + + assert_redirected_to :web => 'renamed_wiki1', :action => 'show', :id => 'HomePage' + @web = Web.find(@web.id) + assert !@web.safe_mode? + assert !@web.published? + assert !@web.brackets_only? + assert !@web.count_pages? + assert !@web.allow_uploads? + end + + def test_edit_web_wrong_password + process('edit_web', 'system_password' => 'wrong', + 'web' => 'wiki1', 'address' => 'renamed_wiki1', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'password' => 'new_password') + + #returns to the same form + assert_response :success + assert @response.has_template_object?('error') + end + + def test_edit_web_rename_to_already_existing_web_name + @wiki.system.update_attribute(:password, 'pswd') + + @wiki.create_web('Another', 'another') + process('edit_web', 'system_password' => 'pswd', + 'web' => 'wiki1', 'address' => 'another', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'password' => 'new_password') + + #returns to the same form + assert_response :success + assert @response.has_template_object?('error') + end + + def test_edit_web_empty_password + process('edit_web', 'system_password' => '', + 'web' => 'wiki1', 'address' => 'renamed_wiki1', 'name' => 'Renamed Wiki1', + 'markup' => 'markdown', 'color' => 'blue', 'additional_style' => 'whatever', + 'password' => 'new_password') + + #returns to the same form + assert_response :success + assert @response.has_template_object?('error') + end + + + def test_remove_orphaned_pages + @wiki.system.update_attribute(:password, 'pswd') + page_order = [@home, pages(:my_way), @oak, pages(:smart_engine), pages(:that_way)] + orphan_page_linking_to_oak = @wiki.write_page('wiki1', 'Pine', + "Refers to [[Oak]].\n" + + "category: trees", + Time.now, Author.new('TreeHugger', '127.0.0.2'), test_renderer) + + r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password_orphaned' => 'pswd') + + assert_redirected_to :controller => 'wiki', :web => 'wiki1', :action => 'list' + @web.pages(true) + assert_equal page_order, @web.select.sort, + "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" + + # Oak is now orphan, second pass should remove it + r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password_orphaned' => 'pswd') + assert_redirected_to :controller => 'wiki', :web => 'wiki1', :action => 'list' + @web.pages(true) + page_order.delete(@oak) + assert_equal page_order, @web.select.sort, + "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" + + # third pass does not destroy HomePage + r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password_orphaned' => 'pswd') + assert_redirected_to :action => 'list' + @web.pages(true) + assert_equal page_order, @web.select.sort, + "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" + end + + def test_remove_orphaned_pages_empty_or_wrong_password + @wiki.system[:password] = 'pswd' + + process('remove_orphaned_pages', 'web' => 'wiki1') + assert_redirected_to(:controller => 'admin', :action => 'edit_web', :web => 'wiki1') + assert @response.flash[:error] + + process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password_orphaned' => 'wrong') + assert_redirected_to(:controller => 'admin', :action => 'edit_web', :web => 'wiki1') + assert @response.flash[:error] + end +end diff --git a/test/functional/application_test.rb b/test/functional/application_test.rb new file mode 100755 index 00000000..c32f8b23 --- /dev/null +++ b/test/functional/application_test.rb @@ -0,0 +1,30 @@ +# Unit tests for ApplicationController (the abstract controller class) + +require File.dirname(__FILE__) + '/../test_helper' +require 'wiki_controller' +require 'rexml/document' + +# Need some concrete class to test the abstract class features +class WikiController; def rescue_action(e) logger.error(e); raise e end; end + +class ApplicationTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system + + def setup + @controller = WikiController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @wiki = Wiki.new + end + + def test_utf8_header + get :show, :web => 'wiki1', :id => 'HomePage' + assert_equal 'text/html; charset=UTF-8', @response.headers['Content-Type'] + end + + def test_connect_to_model_unknown_wiki + get :show, :web => 'unknown_wiki', :id => 'HomePage' + assert_response :missing + end + +end diff --git a/test/functional/file_controller_test.rb b/test/functional/file_controller_test.rb new file mode 100755 index 00000000..98288864 --- /dev/null +++ b/test/functional/file_controller_test.rb @@ -0,0 +1,114 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' +require 'file_controller' +require 'fileutils' +require 'stringio' + +# Raise errors beyond the default web-based presentation +class FileController; def rescue_action(e) logger.error(e); raise e end; end + +class FileControllerTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system + + def setup + @controller = FileController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @web = webs(:test_wiki) + @wiki = Wiki.new + WikiFile.delete_all + require 'fileutils' + FileUtils.rm_rf("#{RAILS_ROOT}/public/wiki1/files/*") + end + + def test_file_upload_form + get :file, :web => 'wiki1', :id => 'new_file.txt' + assert_success + assert_rendered_file 'file/file' + end + + def test_file_download_text_file + @web.wiki_files.create(:file_name => 'foo.txt', :description => 'Text file', + :content => "Contents of the file") + + r = get :file, :web => 'wiki1', :id => 'foo.txt' + + assert_success(bypass_body_parsing = true) + assert_equal "Contents of the file", r.body + assert_equal 'text/plain', r.headers['Content-Type'] + end + + def test_file_download_pdf_file + @web.wiki_files.create(:file_name => 'foo.pdf', :description => 'PDF file', + :content => "aaa\nbbb\n") + + r = get :file, :web => 'wiki1', :id => 'foo.pdf' + + assert_success(bypass_body_parsing = true) + assert_equal "aaa\nbbb\n", r.body + assert_equal 'application/pdf', r.headers['Content-Type'] + end + + def test_pic_download_gif + pic = File.open("#{RAILS_ROOT}/test/fixtures/rails.gif", 'rb') { |f| f.read } + @web.wiki_files.create(:file_name => 'rails.gif', :description => 'An image', :content => pic) + + r = get :file, :web => 'wiki1', :id => 'rails.gif' + + assert_success(bypass_body_parsing = true) + assert_equal 'image/gif', r.headers['Content-Type'] + assert_equal pic.size, r.body.size + assert_equal pic, r.body + end + + def test_pic_unknown_pic + r = get :file, :web => 'wiki1', :id => 'non-existant.gif' + + assert_success + assert_rendered_file 'file/file' + end + + def test_pic_upload_end_to_end + # edit and re-render home page so that it has an "unknown file" link to 'rails-e2e.gif' + PageRenderer.setup_url_generator(StubUrlGenerator.new) + renderer = PageRenderer.new + @wiki.revise_page('wiki1', 'HomePage', '[[rails-e2e.gif:pic]]', + Time.now, 'AnonymousBrave', renderer) + assert_equal "

    rails-e2e.gif" + + "?

    ", + renderer.display_content + + # rails-e2e.gif is unknown to the system, so pic action goes to the file [upload] form + r = get :file, :web => 'wiki1', :id => 'rails-e2e.gif' + assert_success + assert_rendered_file 'file/file' + + # User uploads the picture + picture = File.read("#{RAILS_ROOT}/test/fixtures/rails.gif") + r = post :file, :web => 'wiki1', + :file => {:file_name => 'rails-e2e.gif', :content => StringIO.new(picture)} + assert_redirected_to({}) + assert @web.has_file?('rails-e2e.gif') + assert_equal(picture, WikiFile.find_by_file_name('rails-e2e.gif').content) + end + + def test_import + r = post :import, :web => 'wiki1', :file => uploaded_file("#{RAILS_ROOT}/test/fixtures/exported_markup.zip") + assert_redirect + assert @web.has_page?('ImportedPage') + end + + def uploaded_file(path, content_type="application/octet-stream", filename=nil) + filename ||= File.basename(path) + t = Tempfile.new(filename) + FileUtils.copy_file(path, t.path) + (class << t; self; end;).class_eval do + alias local_path path + define_method(:original_filename) { filename } + define_method(:content_type) { content_type } + end + return t + end + +end diff --git a/test/functional/routes_test.rb b/test/functional/routes_test.rb new file mode 100644 index 00000000..4ba2cec5 --- /dev/null +++ b/test/functional/routes_test.rb @@ -0,0 +1,51 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' + +require 'action_controller/routing' + +class RoutesTest < Test::Unit::TestCase + + def test_parse_uri + assert_routing('', :controller => 'wiki', :action => 'index') + assert_routing('x', :controller => 'wiki', :action => 'index', :web => 'x') + assert_routing('x/y', :controller => 'wiki', :web => 'x', :action => 'y') + assert_routing('x/y/z', :controller => 'wiki', :web => 'x', :action => 'y', :id => 'z') + assert_recognizes({:web => 'x', :controller => 'wiki', :action => 'y'}, 'x/y/') + assert_recognizes({:web => 'x', :controller => 'wiki', :action => 'y', :id => 'z'}, 'x/y/z/') + end + + def test_parse_uri_interestng_cases + assert_routing('_veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long_web_/an_action/HomePage', + :web => '_veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery-long_web_', + :controller => 'wiki', + :action => 'an_action', :id => 'HomePage' + ) + assert_recognizes({:controller => 'wiki', :action => 'index'}, '///') + end + + def test_parse_uri_liberal_with_pagenames + + assert_routing('web/show/%24HOME_PAGE', + :controller => 'wiki', :web => 'web', :action => 'show', :id => '$HOME_PAGE') + + assert_routing('web/show/HomePage%3Farg1%3Dvalue1%26arg2%3Dvalue2', + :controller => 'wiki', :web => 'web', :action => 'show', + :id => 'HomePage?arg1=value1&arg2=value2') + + assert_routing('web/files/abc.zip', + :web => 'web', :controller => 'file', :action => 'file', :id => 'abc.zip') + assert_routing('web/import', :web => 'web', :controller => 'file', :action => 'import') + # default option is wiki + assert_recognizes({:controller => 'wiki', :web => 'unknown_path', :action => 'index', }, + 'unknown_path') + end + + def test_cases_broken_by_routes +# assert_routing('web/show/Page+With+Spaces', +# :controller => 'wiki', :web => 'web', :action => 'show', :id => 'Page With Spaces') +# assert_routing('web/show/HomePage%2Fsomething_else', +# :controller => 'wiki', :web => 'web', :action => 'show', :id => 'HomePage/something_else') + end + +end diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb new file mode 100755 index 00000000..3edf75b4 --- /dev/null +++ b/test/functional/wiki_controller_test.rb @@ -0,0 +1,676 @@ +#!/usr/bin/env ruby + +# Uncomment the line below to enable pdflatex tests; don't forget to comment them again +# commiting to SVN +# $INSTIKI_TEST_PDFLATEX = true + +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') +require 'wiki_controller' +require 'rexml/document' +require 'tempfile' +require 'zip/zipfilesystem' + +# Raise errors beyond the default web-based presentation +class WikiController; def rescue_action(e) logger.error(e); raise e end; end + +class WikiControllerTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system, :wiki_references + + def setup + @controller = WikiController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @wiki = Wiki.new + @web = webs(:test_wiki) + @home = @page = pages(:home_page) + @oak = pages(:oak) + @elephant = pages(:elephant) + end + + def test_authenticate + set_web_property :password, 'pswd' + + get :authenticate, :web => 'wiki1', :password => 'pswd' + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' + assert_equal ['pswd'], @response.cookies['web_address'] + end + + def test_authenticate_wrong_password + set_web_property :password, 'pswd' + + r = process('authenticate', 'web' => 'wiki1', 'password' => 'wrong password') + assert_redirected_to :action => 'login', :web => 'wiki1' + assert_nil r.cookies['web_address'] + end + + def test_authors + @wiki.write_page('wiki1', 'BreakSortingOrder', + "This page breaks the accidentally correct sorting order of authors", + Time.now, Author.new('BreakingTheOrder', '127.0.0.2'), test_renderer) + + r = process('authors', 'web' => 'wiki1') + + assert_success + assert_equal %w(AnAuthor BreakingTheOrder DavidHeinemeierHansson Guest Me TreeHugger), + r.template_objects['authors'] + page_names_by_author = r.template_objects['page_names_by_author'] + assert_equal r.template_objects['authors'], page_names_by_author.keys.sort + assert_equal %w(FirstPage HomePage), page_names_by_author['DavidHeinemeierHansson'] + end + + def test_cancel_edit + @oak.lock(Time.now, 'Locky') + assert @oak.locked?(Time.now) + + r = process('cancel_edit', 'web' => 'wiki1', 'id' => 'Oak') + + assert_redirected_to :action => 'show', :id => 'Oak' + assert !Page.find(@oak.id).locked?(Time.now) + end + + def test_edit + r = process 'edit', 'web' => 'wiki1', 'id' => 'HomePage' + assert_success + assert_equal @wiki.read_page('wiki1', 'HomePage'), r.template_objects['page'] + end + + def test_edit_page_locked_page + @home.lock(Time.now, 'Locky') + process 'edit', 'web' => 'wiki1', 'id' => 'HomePage' + assert_redirected_to :action => 'locked' + end + + def test_edit_page_break_lock + @home.lock(Time.now, 'Locky') + process 'edit', 'web' => 'wiki1', 'id' => 'HomePage', 'break_lock' => 'y' + assert_success + @home = Page.find(@home.id) + assert @home.locked?(Time.now) + end + + def test_edit_unknown_page + process 'edit', 'web' => 'wiki1', 'id' => 'UnknownPage', 'break_lock' => 'y' + assert_redirected_to :controller => 'wiki', :action => 'show', :web => 'wiki1', + :id => 'HomePage' + end + + def test_edit_page_with_special_symbols + @wiki.write_page('wiki1', 'With : Special /> symbols', + 'This page has special symbols in the name', Time.now, Author.new('Special', '127.0.0.3'), + test_renderer) + + r = process 'edit', 'web' => 'wiki1', 'id' => 'With : Special /> symbols' + assert_success + xml = REXML::Document.new(r.body) + form = REXML::XPath.first(xml, '//form') + assert_equal '/wiki1/save/With+%3A+Special+%2F%3E+symbols', form.attributes['action'] + end + + def test_export_html + # rollback homepage to a version that is easier to match + @home.rollback(0, Time.now, 'Rick', test_renderer) + r = process 'export_html', 'web' => 'wiki1' + + assert_success(bypass_body_parsing = true) + assert_equal 'application/zip', r.headers['Content-Type'] + assert_match /attachment; filename="wiki1-html-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.zip"/, + r.headers['Content-Disposition'] + assert_equal 'PK', r.body[0..1], 'Content is not a zip file' + + # Tempfile doesn't know how to open files with binary flag, hence the two-step process + Tempfile.open('instiki_export_file') { |f| @tempfile_path = f.path } + begin + File.open(@tempfile_path, 'wb') { |f| f.write(r.body); @exported_file = f.path } + Zip::ZipFile.open(@exported_file) do |zip| + assert_equal %w(Elephant.html FirstPage.html HomePage.html MyWay.html NoWikiWord.html Oak.html SmartEngine.html ThatWay.html index.html), zip.dir.entries('.').sort + assert_match /.*/, + zip.file.read('Elephant.html').gsub(/\s+/, ' ') + assert_match /.*/, + zip.file.read('Oak.html').gsub(/\s+/, ' ') + assert_match /.*/, + zip.file.read('HomePage.html').gsub(/\s+/, ' ') + assert_equal ' ', zip.file.read('index.html').gsub(/\s+/, ' ') + end + ensure + File.delete(@tempfile_path) if File.exist?(@tempfile_path) + end + end + + def test_export_html_no_layout + r = process 'export_html', 'web' => 'wiki1', 'layout' => 'no' + + assert_success(bypass_body_parsing = true) + assert_equal 'application/zip', r.headers['Content-Type'] + assert_match /attachment; filename="wiki1-html-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.zip"/, + r.headers['Content-Disposition'] + assert_equal 'PK', r.body[0..1], 'Content is not a zip file' + end + + def test_export_markup + r = process 'export_markup', 'web' => 'wiki1' + + assert_success(bypass_body_parsing = true) + assert_equal 'application/zip', r.headers['Content-Type'] + assert_match /attachment; filename="wiki1-textile-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.zip"/, + r.headers['Content-Disposition'] + assert_equal 'PK', r.body[0..1], 'Content is not a zip file' + end + + + if ENV['INSTIKI_TEST_LATEX'] or defined? $INSTIKI_TEST_PDFLATEX + + def test_export_pdf + r = process 'export_pdf', 'web' => 'wiki1' + assert_success(bypass_body_parsing = true) + assert_equal 'application/pdf', r.headers['Content-Type'] + assert_match /attachment; filename="wiki1-tex-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.pdf"/, + r.headers['Content-Disposition'] + assert_equal '%PDF', r.body[0..3] + assert_equal "EOF\n", r.body[-4..-1] + end + + else + puts 'Warning: tests involving pdflatex are very slow, therefore they are disabled by default.' + puts ' Set environment variable INSTIKI_TEST_PDFLATEX or global Ruby variable' + puts ' $INSTIKI_TEST_PDFLATEX to enable them.' + end + + def test_export_tex + r = process 'export_tex', 'web' => 'wiki1' + + assert_success(bypass_body_parsing = true) + assert_equal 'application/octet-stream', r.headers['Content-Type'] + assert_match /attachment; filename="wiki1-tex-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.tex"/, + r.headers['Content-Disposition'] + assert_equal '\documentclass', r.body[0..13], 'Content is not a TeX file' + end + + def test_feeds + process('feeds', 'web' => 'wiki1') + end + + def test_index + # delete extra web fixture + webs(:instiki).destroy + process('index') + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' + end + + def test_index_multiple_webs + @wiki.create_web('Test Wiki 2', 'wiki2') + process('index') + assert_redirected_to :action => 'web_list' + end + + def test_index_multiple_webs_web_explicit + @wiki.create_web('Test Wiki 2', 'wiki2') + process('index', 'web' => 'wiki2') + assert_redirected_to :web => 'wiki2', :action => 'show', :id => 'HomePage' + end + + def test_index_wiki_not_initialized + use_blank_wiki + process('index') + assert_redirected_to :controller => 'admin', :action => 'create_system' + end + + + def test_list + r = process('list', 'web' => 'wiki1') + + assert_equal ['animals', 'trees'], r.template_objects['categories'] + assert_nil r.template_objects['category'] + assert_equal [@elephant, pages(:first_page), @home, pages(:my_way), pages(:no_wiki_word), + @oak, pages(:smart_engine), pages(:that_way)], + r.template_objects['pages_in_category'] + end + + + def test_locked + @home.lock(Time.now, 'Locky') + r = process('locked', 'web' => 'wiki1', 'id' => 'HomePage') + assert_success + assert_equal @home, r.template_objects['page'] + end + + + def test_login + r = process 'login', 'web' => 'wiki1' + assert_success + # this action goes straight to the templates + end + + + def test_new + r = process('new', 'id' => 'NewPage', 'web' => 'wiki1') + assert_success + assert_equal 'AnonymousCoward', r.template_objects['author'] + assert_equal 'NewPage', r.template_objects['page_name'] + end + + + if ENV['INSTIKI_TEST_LATEX'] or defined? $INSTIKI_TEST_PDFLATEX + + def test_pdf + assert RedClothForTex.available?, 'Cannot do test_pdf when pdflatex is not available' + r = process('pdf', 'web' => 'wiki1', 'id' => 'HomePage') + assert_success(bypass_body_parsing = true) + + assert_equal '%PDF', r.body[0..3] + assert_equal "EOF\n", r.body[-4..-1] + + assert_equal 'application/pdf', r.headers['Content-Type'] + assert_match /attachment; filename="HomePage-wiki1-\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d.pdf"/, + r.headers['Content-Disposition'] + end + + end + + + def test_print + r = process('print', 'web' => 'wiki1', 'id' => 'HomePage') + + assert_success + assert_equal :show, r.template_objects['link_mode'] + end + + + def test_published + set_web_property :published, true + + r = process('published', 'web' => 'wiki1', 'id' => 'HomePage') + + assert_success + assert_equal @home, r.template_objects['page'] + end + + + def test_published_web_not_published + set_web_property :published, false + + r = process('published', 'web' => 'wiki1', 'id' => 'HomePage') + + assert_response :missing + end + + def test_published_should_render_homepage_if_no_page_specified + set_web_property :published, true + + r = process('published', 'web' => 'wiki1') + + assert_success + assert_equal @home, r.template_objects['page'] + end + + + def test_recently_revised + r = process('recently_revised', 'web' => 'wiki1') + assert_success + + assert_equal %w(animals trees), r.template_objects['categories'] + assert_nil r.template_objects['category'] + all_pages = @elephant, pages(:first_page), @home, pages(:my_way), pages(:no_wiki_word), + @oak, pages(:smart_engine), pages(:that_way) + assert_equal all_pages, r.template_objects['pages_in_category'] + + pages_by_day = r.template_objects['pages_by_day'] + assert_not_nil pages_by_day + pages_by_day_size = pages_by_day.keys.inject(0) { |sum, day| sum + pages_by_day[day].size } + assert_equal all_pages.size, pages_by_day_size + all_pages.each do |page| + day = Date.new(page.revised_at.year, page.revised_at.month, page.revised_at.day) + assert pages_by_day[day].include?(page) + end + + assert_equal 'the web', r.template_objects['set_name'] + end + + def test_recently_revised_with_categorized_page + page2 = @wiki.write_page('wiki1', 'Page2', + "Page2 contents.\n" + + "category: categorized", + Time.now, Author.new('AnotherAuthor', '127.0.0.2'), test_renderer) + + r = process('recently_revised', 'web' => 'wiki1') + assert_success + + assert_equal %w(animals categorized trees), r.template_objects['categories'] + # no category is specified in params + assert_nil r.template_objects['category'] + assert_equal [@elephant, pages(:first_page), @home, pages(:my_way), pages(:no_wiki_word), @oak, page2, pages(:smart_engine), pages(:that_way)], r.template_objects['pages_in_category'], + "Pages are not as expected: " + + r.template_objects['pages_in_category'].map {|p| p.name}.inspect + assert_equal 'the web', r.template_objects['set_name'] + end + + def test_recently_revised_with_categorized_page_multiple_categories + r = process('recently_revised', 'web' => 'wiki1') + assert_success + + assert_equal ['animals', 'trees'], r.template_objects['categories'] + # no category is specified in params + assert_nil r.template_objects['category'] + assert_equal [@elephant, pages(:first_page), @home, pages(:my_way), pages(:no_wiki_word), @oak, pages(:smart_engine), pages(:that_way)], r.template_objects['pages_in_category'], + "Pages are not as expected: " + + r.template_objects['pages_in_category'].map {|p| p.name}.inspect + assert_equal 'the web', r.template_objects['set_name'] + end + + def test_recently_revised_with_specified_category + r = process('recently_revised', 'web' => 'wiki1', 'category' => 'animals') + assert_success + + assert_equal ['animals', 'trees'], r.template_objects['categories'] + # no category is specified in params + assert_equal 'animals', r.template_objects['category'] + assert_equal [@elephant], r.template_objects['pages_in_category'] + assert_equal "category 'animals'", r.template_objects['set_name'] + end + + + def test_revision + r = process 'revision', 'web' => 'wiki1', 'id' => 'HomePage', 'rev' => '1' + + assert_success + assert_equal @home, r.template_objects['page'] + assert_equal @home.revisions[0], r.template_objects['revision'] + end + + + def test_rollback + # rollback shows a form where a revision can be edited. + # its assigns the same as or revision + r = process 'rollback', 'web' => 'wiki1', 'id' => 'HomePage', 'rev' => '1' + + assert_success + assert_equal @home, r.template_objects['page'] + assert_equal @home.revisions[0], r.template_objects['revision'] + end + + def test_rss_with_content + r = process 'rss_with_content', 'web' => 'wiki1' + + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal [@elephant, @oak, pages(:no_wiki_word), pages(:that_way), pages(:smart_engine), pages(:my_way), pages(:first_page), @home], pages, + "Pages are not as expected: #{pages.map {|p| p.name}.inspect}" + assert !r.template_objects['hide_description'] + end + + def test_rss_with_content_when_blocked + @web.update_attributes(:password => 'aaa', :published => false) + @web = Web.find(@web.id) + + r = process 'rss_with_content', 'web' => 'wiki1' + + assert_equal 403, r.response_code + end + + + def test_rss_with_headlines + @title_with_spaces = @wiki.write_page('wiki1', 'Title With Spaces', + 'About spaces', 1.hour.ago, Author.new('TreeHugger', '127.0.0.2'), test_renderer) + + @request.host = 'localhost' + @request.port = 8080 + + r = process 'rss_with_headlines', 'web' => 'wiki1' + + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal [@elephant, @title_with_spaces, @oak, pages(:no_wiki_word), pages(:that_way), pages(:smart_engine), pages(:my_way), pages(:first_page), @home], pages, "Pages are not as expected: #{pages.map {|p| p.name}.inspect}" + assert r.template_objects['hide_description'] + + xml = REXML::Document.new(r.body) + + expected_page_links = + ['http://localhost:8080/wiki1/show/Elephant', + 'http://localhost:8080/wiki1/show/Title+With+Spaces', + 'http://localhost:8080/wiki1/show/Oak', + 'http://localhost:8080/wiki1/show/NoWikiWord', + 'http://localhost:8080/wiki1/show/ThatWay', + 'http://localhost:8080/wiki1/show/SmartEngine', + 'http://localhost:8080/wiki1/show/MyWay', + 'http://localhost:8080/wiki1/show/FirstPage', + 'http://localhost:8080/wiki1/show/HomePage', + ] + + assert_template_xpath_match '/rss/channel/link', + 'http://localhost:8080/wiki1/show/HomePage' + assert_template_xpath_match '/rss/channel/item/guid', expected_page_links + assert_template_xpath_match '/rss/channel/item/link', expected_page_links + end + + def test_rss_switch_links_to_published + @web.update_attributes(:password => 'aaa', :published => true) + @web = Web.find(@web.id) + + @request.host = 'foo.bar.info' + @request.port = 80 + + r = process 'rss_with_headlines', 'web' => 'wiki1' + + assert_success + xml = REXML::Document.new(r.body) + + expected_page_links = + ['http://foo.bar.info/wiki1/published/Elephant', + 'http://foo.bar.info/wiki1/published/Oak', + 'http://foo.bar.info/wiki1/published/NoWikiWord', + 'http://foo.bar.info/wiki1/published/ThatWay', + 'http://foo.bar.info/wiki1/published/SmartEngine', + 'http://foo.bar.info/wiki1/published/MyWay', + 'http://foo.bar.info/wiki1/published/FirstPage', + 'http://foo.bar.info/wiki1/published/HomePage'] + + assert_template_xpath_match '/rss/channel/link', + 'http://foo.bar.info/wiki1/published/HomePage' + assert_template_xpath_match '/rss/channel/item/guid', expected_page_links + assert_template_xpath_match '/rss/channel/item/link', expected_page_links + end + + def test_rss_with_params + setup_wiki_with_30_pages + + r = process 'rss_with_headlines', 'web' => 'wiki1' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 15, pages.size, 15 + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'limit' => '5' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 5, pages.size + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'limit' => '25' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 25, pages.size + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'limit' => 'all' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 38, pages.size + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'start' => '1976-10-16' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 23, pages.size + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'end' => '1976-10-16' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 15, pages.size + + r = process 'rss_with_headlines', 'web' => 'wiki1', 'start' => '1976-10-01', 'end' => '1976-10-06' + assert_success + pages = r.template_objects['pages_by_revision'] + assert_equal 5, pages.size + end + + def test_rss_title_with_ampersand + # was ticket:143 + @wiki.write_page('wiki1', 'Title&With&Ampersands', + 'About spaces', 1.hour.ago, Author.new('NitPicker', '127.0.0.3'), test_renderer) + + r = process 'rss_with_headlines', 'web' => 'wiki1' + + assert r.body.include?('Home Page') + assert r.body.include?('Title&With&Ampersands') + end + + def test_rss_timestamp + new_page = @wiki.write_page('wiki1', 'PageCreatedAtTheBeginningOfCtime', + 'Created on 1 Jan 1970 at 0:00:00 Z', Time.at(0), Author.new('NitPicker', '127.0.0.3'), + test_renderer) + + r = process 'rss_with_headlines', 'web' => 'wiki1' + assert_template_xpath_match '/rss/channel/item/pubDate[9]', "Thu, 01 Jan 1970 00:00:00 Z" + end + + def test_save + r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => 'Contents of a new page', + 'author' => 'AuthorOfNewPage' + + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'NewPage' + assert_equal ['AuthorOfNewPage'], r.cookies['author'].value + assert_equal Time.utc(2030), r.cookies['author'].expires + new_page = @wiki.read_page('wiki1', 'NewPage') + assert_equal 'Contents of a new page', new_page.content + assert_equal 'AuthorOfNewPage', new_page.author + end + + def test_save_new_revision_of_existing_page + @home.lock(Time.now, 'Batman') + current_revisions = @home.revisions.size + + r = process 'save', 'web' => 'wiki1', 'id' => 'HomePage', 'content' => 'Revised HomePage', + 'author' => 'Batman' + + assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' + assert_equal ['Batman'], r.cookies['author'].value + home_page = @wiki.read_page('wiki1', 'HomePage') + assert_equal current_revisions+1, home_page.revisions.size + assert_equal 'Revised HomePage', home_page.content + assert_equal 'Batman', home_page.author + assert !home_page.locked?(Time.now) + end + + def test_save_new_revision_identical_to_last + revisions_before = @home.revisions.size + @home.lock(Time.now, 'AnAuthor') + + r = process 'save', {'web' => 'wiki1', 'id' => 'HomePage', + 'content' => @home.revisions.last.content.dup, + 'author' => 'SomeOtherAuthor'}, {:return_to => '/wiki1/show/HomePage'} + + assert_redirected_to :action => 'edit', :web => 'wiki1', :id => 'HomePage' + assert_flash_has :error + assert r.flash[:error].kind_of?(Instiki::ValidationError) + + revisions_after = @home.revisions.size + assert_equal revisions_before, revisions_after + @home = Page.find(@home.id) + assert !@home.locked?(Time.now), 'HomePage should be unlocked if an edit was unsuccessful' + end + + def test_save_blank_author + r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => 'Contents of a new page', + 'author' => '' + new_page = @wiki.read_page('wiki1', 'NewPage') + assert_equal 'AnonymousCoward', new_page.author + + r = process 'save', 'web' => 'wiki1', 'id' => 'AnotherPage', 'content' => 'Contents of a new page', + 'author' => ' ' + + another_page = @wiki.read_page('wiki1', 'AnotherPage') + assert_equal 'AnonymousCoward', another_page.author + end + + + def test_search + r = process 'search', 'web' => 'wiki1', 'query' => '\s[A-Z]ak' + + assert_redirected_to :action => 'show', :id => 'Oak' + end + + def test_search_multiple_results + r = process 'search', 'web' => 'wiki1', 'query' => 'All about' + + assert_success + assert_equal 'All about', r.template_objects['query'] + assert_equal [@elephant, @oak], r.template_objects['results'] + assert_equal [], r.template_objects['title_results'] + end + + def test_search_by_content_and_title + r = process 'search', 'web' => 'wiki1', 'query' => '(Oak|Elephant)' + + assert_success + assert_equal '(Oak|Elephant)', r.template_objects['query'] + assert_equal [@elephant, @oak], r.template_objects['results'] + assert_equal [@elephant, @oak], r.template_objects['title_results'] + end + + def test_search_zero_results + r = process 'search', 'web' => 'wiki1', 'query' => 'non-existant text' + + assert_success + assert_equal [], r.template_objects['results'] + assert_equal [], r.template_objects['title_results'] + end + + def test_show_page + r = process('show', 'id' => 'Oak', 'web' => 'wiki1') + assert_success + assert_tag :content => /All about oak/ + end + + def test_show_page_with_multiple_revisions + @wiki.write_page('wiki1', 'HomePage', 'Second revision of the HomePage end', Time.now, + Author.new('AnotherAuthor', '127.0.0.2'), test_renderer) + + r = process('show', 'id' => 'HomePage', 'web' => 'wiki1') + + assert_success + assert_match /Second revision of the end/, r.body + end + + def test_show_page_nonexistant_page + process('show', 'id' => 'UnknownPage', 'web' => 'wiki1') + assert_redirected_to :web => 'wiki1', :action => 'new', :id => 'UnknownPage' + end + + def test_show_no_page + r = process('show', 'id' => '', 'web' => 'wiki1') + assert_response :missing + + r = process('show', 'web' => 'wiki1') + assert_response :missing + end + + def test_tex + r = process('tex', 'web' => 'wiki1', 'id' => 'HomePage') + assert_success + + assert_equal "\\documentclass[12pt,titlepage]{article}\n\n\\usepackage[danish]{babel} " + + "%danske tekster\n\\usepackage[OT1]{fontenc} %rigtige danske bogstaver...\n" + + "\\usepackage{a4}\n\\usepackage{graphicx}\n\\usepackage{ucs}\n\\usepackage[utf8x]" + + "{inputenc}\n\\input epsf \n\n%----------------------------------------------------" + + "---------------\n\n\\begin{document}\n\n\\sloppy\n\n%-----------------------------" + + "--------------------------------------\n\n\\section*{HomePage}\n\nHisWay would be " + + "MyWay in kinda ThatWay in HisWay though MyWay \\OverThere -- see SmartEngine in that " + + "SmartEngineGUI\n\n\\end{document}", r.body + end + + + def test_web_list + another_wiki = @wiki.create_web('Another Wiki', 'another_wiki') + + r = process('web_list') + + assert_success + assert_equal [another_wiki, webs(:instiki), @web], r.template_objects['webs'] + end + +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..3afd1ae6 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,168 @@ +ENV['RAILS_ENV'] = 'test' + +# Expand the path to environment so that Ruby does not load it multiple times +# File.expand_path can be removed if Ruby 1.9 is in use. +require File.expand_path(File.dirname(__FILE__) + '/../config/environment') +require 'application' + +require 'test/unit' +require 'active_record/fixtures' +require 'action_controller/test_process' +require 'action_web_service/test_invoke' +require 'breakpoint' +require 'wiki_content' +require 'url_generator' + +Test::Unit::TestCase.pre_loaded_fixtures = false +Test::Unit::TestCase.use_transactional_fixtures = true +Test::Unit::TestCase.use_instantiated_fixtures = false +Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" + +# activate PageObserver +PageObserver.instance + +class Test::Unit::TestCase + def create_fixtures(*table_names) + Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures", table_names) + end + + # Add more helper methods to be used by all tests here... + def set_web_property(property, value) + @web.update_attribute(property, value) + @page = Page.find(@page.id) + @wiki.webs[@web.name] = @web + end + + def setup_wiki_with_30_pages + ActiveRecord::Base.silence do + (1..30).each do |i| + @wiki.write_page('wiki1', "page#{i}", "Test page #{i}\ncategory: test", + Time.local(1976, 10, i, 12, 00, 00), Author.new('Dema', '127.0.0.2'), + test_renderer) + end + end + @web = Web.find(@web.id) + end + + def test_renderer(revision = nil) + PageRenderer.setup_url_generator(StubUrlGenerator.new) + PageRenderer.new(revision) + end + + def use_blank_wiki + Revision.destroy_all + Page.destroy_all + Web.destroy_all + end +end + +# This module is to be included in unit tests that involve matching chunks. +# It provides a easy way to test whether a chunk matches a particular string +# and any the values of any fields that should be set after a match. +class ContentStub < String + include ChunkManager + def initialize(str) + super + init_chunk_manager + end + def page_link(*); end +end + +module ChunkMatch + + # Asserts a number of tests for the given type and text. + def match(chunk_type, test_text, expected_chunk_state) + if chunk_type.respond_to? :pattern + assert_match(chunk_type.pattern, test_text) + end + + content = ContentStub.new(test_text) + chunk_type.apply_to(content) + + # Test if requested parts are correct. + expected_chunk_state.each_pair do |a_method, expected_value| + assert content.chunks.last.kind_of?(chunk_type) + assert_respond_to(content.chunks.last, a_method) + assert_equal(expected_value, content.chunks.last.send(a_method.to_sym), + "Wrong #{a_method} value") + end + end + + # Asserts that test_text doesn't match the chunk_type + def no_match(chunk_type, test_text) + if chunk_type.respond_to? :pattern + assert_no_match(chunk_type.pattern, test_text) + end + end +end + +class StubUrlGenerator < AbstractUrlGenerator + + def initialize + super(:doesnt_need_controller) + end + + def file_link(mode, name, text, web_name, known_file) + link = CGI.escape(name) + case mode + when :export + if known_file then %{#{text}} + else %{#{text}} end + when :publish + if known_file then %{#{text}} + else %{#{text}} end + else + if known_file + %{#{text}} + else + %{#{text}?} + end + end + end + + def page_link(mode, name, text, web_address, known_page) + link = CGI.escape(name) + case mode.to_sym + when :export + if known_page then %{#{text}} + else %{#{text}} end + when :publish + if known_page then %{#{text}} + else %{#{text}} end + else + if known_page + %{#{text}} + else + %{#{text}?} + end + end + end + + def pic_link(mode, name, text, web_name, known_pic) + link = CGI.escape(name) + case mode.to_sym + when :export + if known_pic then %{#{text}} + else %{#{text}} end + when :publish + if known_pic then %{#{text}} + else %{#{text}} end + else + if known_pic then %{#{text}} + else %{#{text}?} end + end + end +end + +module Test + module Unit + module Assertions + def assert_success(bypass_body_parsing = false) + assert_response :success + unless bypass_body_parsing + assert_nothing_raised(@response.body) { REXML::Document.new(@response.body) } + end + end + end + end +end diff --git a/test/unit/chunks/category_test.rb b/test/unit/chunks/category_test.rb new file mode 100755 index 00000000..6bc7627f --- /dev/null +++ b/test/unit/chunks/category_test.rb @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../test_helper' +require 'chunks/category' + +class CategoryTest < Test::Unit::TestCase + include ChunkMatch + + def test_single_category + match(Category, 'category: test', :list => ['test'], :hidden => nil) + match(Category, 'category : chunk test ', :list => ['chunk test'], :hidden => nil) + match(Category, ':category: test', :list => ['test'], :hidden => ':') + end + + def test_multiple_categories + match(Category, 'category: test, multiple', :list => ['test', 'multiple'], :hidden => nil) + match(Category, 'category : chunk test , multi category,regression test case ', + :list => ['chunk test','multi category','regression test case'], :hidden => nil + ) + end + +end diff --git a/test/unit/chunks/nowiki_test.rb b/test/unit/chunks/nowiki_test.rb new file mode 100755 index 00000000..8af5a645 --- /dev/null +++ b/test/unit/chunks/nowiki_test.rb @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../test_helper' +require 'chunks/nowiki' + +class NoWikiTest < Test::Unit::TestCase + include ChunkMatch + + def test_simple_nowiki + match(NoWiki, 'This sentence contains [[raw text]]. Do not touch!', + :plain_text => '[[raw text]]' + ) + end + +end diff --git a/test/unit/chunks/wiki_test.rb b/test/unit/chunks/wiki_test.rb new file mode 100755 index 00000000..82c2546c --- /dev/null +++ b/test/unit/chunks/wiki_test.rb @@ -0,0 +1,98 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../test_helper' +require 'chunks/wiki' + +class WikiTest < Test::Unit::TestCase + + include ChunkMatch + + def test_simple + match(WikiChunk::Word, 'This is a WikiWord okay?', :page_name => 'WikiWord') + end + + def test_escaped + # escape is only implemented in WikiChunk::Word + match(WikiChunk::Word, 'Do not link to an \EscapedWord', + :page_name => 'EscapedWord', :escaped_text => 'EscapedWord' + ) + end + + def test_simple_brackets + match(WikiChunk::Link, 'This is a [[bracketted link]]', :page_name => 'bracketted link') + end + + def test_void_brackets + # double brackets woith only spaces inside are not a WikiLink + no_match(WikiChunk::Link, "This [[ ]] are [[]] no [[ \t ]] links") + end + + def test_brackets_strip_spaces + match(WikiChunk::Link, + "This is a [[Sperberg-McQueen \t ]] link with trailing spaces to strip", + :page_name => 'Sperberg-McQueen') + match(WikiChunk::Link, + "This is a [[ \t Sperberg-McQueen]] link with leading spaces to strip", + :page_name => 'Sperberg-McQueen') + match(WikiChunk::Link, + 'This is a [[ Sperberg-McQueen ]] link with spaces around it to strip', + :page_name => 'Sperberg-McQueen') + match(WikiChunk::Link, + 'This is a [[ Sperberg McQueen ]] link with spaces inside and around it', + :page_name => 'Sperberg McQueen') + end + + def test_complex_brackets + match(WikiChunk::Link, 'This is a tricky link [[Sperberg-McQueen]]', + :page_name => 'Sperberg-McQueen') + end + + def test_include_chunk_pattern + content = 'This is a [[!include pagename]] and [[!include WikiWord]] but [[blah]]' + recognized_includes = content.scan(Include.pattern).collect { |m| m[0] } + assert_equal %w(pagename WikiWord), recognized_includes + end + + def test_textile_link + textile_link = ContentStub.new('"Here is a special link":SpecialLink') + WikiChunk::Word.apply_to(textile_link) + assert_equal '"Here is a special link":SpecialLink', textile_link + assert textile_link.chunks.empty? + end + + def test_file_types + # only link + assert_link_parsed_as 'only text', 'only text', :show, '[[only text]]' + # link and text + assert_link_parsed_as 'page name', 'link text', :show, '[[page name|link text]]' + # link and type (file) + assert_link_parsed_as 'foo.tar.gz', 'foo.tar.gz', :file, '[[foo.tar.gz:file]]' + # link and type (pic) + assert_link_parsed_as 'foo.tar.gz', 'foo.tar.gz', :pic, '[[foo.tar.gz:pic]]' + # link, text and type + assert_link_parsed_as 'foo.tar.gz', 'FooTar', :file, '[[foo.tar.gz|FooTar:file]]' + + # NEGATIVE TEST CASES + + # empty page name + assert_link_parsed_as '|link text?', '|link text?', :file, '[[|link text?:file]]' + # empty link text + assert_link_parsed_as 'page name?|', 'page name?|', :file, '[[page name?|:file]]' + # empty link type + assert_link_parsed_as 'page name', 'link?:', :show, '[[page name|link?:]]' + # unknown link type + assert_link_parsed_as 'page name:create_system', 'page name:create_system', :show, + '[[page name:create_system]]' + end + + def assert_link_parsed_as(expected_page_name, expected_link_text, expected_link_type, link) + link_to_file = ContentStub.new(link) + WikiChunk::Link.apply_to(link_to_file) + chunk = link_to_file.chunks.last + assert chunk + assert_equal expected_page_name, chunk.page_name + assert_equal expected_link_text, chunk.link_text + assert_equal expected_link_type, chunk.link_type + end + +end diff --git a/test/unit/diff_test.rb b/test/unit/diff_test.rb new file mode 100755 index 00000000..67a8a71b --- /dev/null +++ b/test/unit/diff_test.rb @@ -0,0 +1,110 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') +require 'diff' + +class DiffTest < Test::Unit::TestCase + + include HTMLDiff + + def setup + @builder = DiffBuilder.new('old', 'new') + end + + def test_start_of_tag + assert @builder.start_of_tag?('<') + assert(!@builder.start_of_tag?('>')) + assert(!@builder.start_of_tag?('a')) + end + + def test_end_of_tag + assert @builder.end_of_tag?('>') + assert(!@builder.end_of_tag?('<')) + assert(!@builder.end_of_tag?('a')) + end + + def test_whitespace + assert @builder.whitespace?(" ") + assert @builder.whitespace?("\n") + assert @builder.whitespace?("\r") + assert(!@builder.whitespace?("a")) + end + + def test_convert_html_to_list_of_words_simple + assert_equal( + ['the', ' ', 'original', ' ', 'text'], + @builder.convert_html_to_list_of_words('the original text')) + end + + def test_convert_html_to_list_of_words_should_separate_endlines + assert_equal( + ['a', "\n", 'b', "\r", 'c'], + @builder.convert_html_to_list_of_words("a\nb\rc")) + end + + def test_convert_html_to_list_of_words_should_not_compress_whitespace + assert_equal( + ['a', ' ', 'b', ' ', 'c', "\r \n ", 'd'], + @builder.convert_html_to_list_of_words("a b c\r \n d")) + end + + def test_convert_html_to_list_of_words_should_handle_tags_well + assert_equal( + ['

    ', 'foo', ' ', 'bar', '

    '], + @builder.convert_html_to_list_of_words("

    foo bar

    ")) + end + + def test_convert_html_to_list_of_words_interesting + assert_equal( + ['

    ', 'this', ' ', 'is', '

    ', "\r\n", '

    ', 'the', ' ', 'new', ' ', 'string', + '

    ', "\r\n", '

    ', 'around', ' ', 'the', ' ', 'world', '

    '], + @builder.convert_html_to_list_of_words( + "

    this is

    \r\n

    the new string

    \r\n

    around the world

    ")) + end + + def test_html_diff_simple + a = 'this was the original string' + b = 'this is the new string' + assert_equal('this wasis the ' + + 'originalnew string', + diff(a, b)) + end + + def test_html_diff_with_multiple_paragraphs + a = "

    this was the original string

    " + b = "

    this is

    \r\n

    the new string

    \r\n

    around the world

    " + + # Some of this expected result is accidental to implementation. + # At least it's well-formed and more or less correct. + assert_equal( + "

    this wasis

    "+ + "\r\n

    the " + + "originalnew" + + " string

    \r\n" + + "

    around the world

    ", + diff(a, b)) + end + + # FIXME this test fails (ticket #67, http://dev.instiki.org/ticket/67) + def test_html_diff_preserves_endlines_in_pre + a = "
    \na\nb\nc\n
    " + b = "
    \n
    " + assert_equal( + "
    \na\nb\nc\n
    ", + diff(a, b)) + end + + def test_html_diff_with_tags + a = "" + b = "
    foo
    " + assert_equal '
    foo
    ', diff(a, b) + end + + def test_diff_for_tag_change + a = "x" + b = "x" + # FIXME sad, but true - this case produces an invalid XML. If handle this you can, strong your foo is. + assert_equal 'x', diff(a, b) + end + +end diff --git a/test/unit/page_renderer_test.rb b/test/unit/page_renderer_test.rb new file mode 100644 index 00000000..54f5990e --- /dev/null +++ b/test/unit/page_renderer_test.rb @@ -0,0 +1,389 @@ +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') + +class PageRendererTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system, :wiki_references + + def setup + @wiki = Wiki.new + @web = webs(:test_wiki) + @page = pages(:home_page) + @revision = revisions(:home_page_second_revision) + end + + def test_wiki_word_linking + @web.add_page('SecondPage', 'Yo, yo. Have you EverBeenHated', + Time.now, 'DavidHeinemeierHansson', test_renderer) + + assert_equal('

    Yo, yo. Have you Ever Been Hated' + + '?

    ', + rendered_content(@web.page("SecondPage"))) + + @web.add_page('EverBeenHated', 'Yo, yo. Have you EverBeenHated', Time.now, + 'DavidHeinemeierHansson', test_renderer) + assert_equal('

    Yo, yo. Have you Ever Been Hated

    ', + rendered_content(@web.page("SecondPage"))) + end + + def test_wiki_words + assert_equal %w( HisWay MyWay SmartEngine SmartEngineGUI ThatWay ), + test_renderer(@revision).wiki_words.sort + + @wiki.write_page('wiki1', 'NoWikiWord', 'hey you!', Time.now, 'Me', test_renderer) + assert_equal [], test_renderer(@wiki.read_page('wiki1', 'NoWikiWord').revisions.last).wiki_words + end + + def test_existing_pages + assert_equal %w( MyWay SmartEngine ThatWay ), test_renderer(@revision).existing_pages.sort + end + + def test_unexisting_pages + assert_equal %w( HisWay SmartEngineGUI ), test_renderer(@revision).unexisting_pages.sort + end + + def test_content_with_wiki_links + assert_equal '

    His Way? ' + + 'would be My Way in kinda ' + + 'That Way in ' + + 'His Way? ' + + 'though My Way OverThere—see ' + + 'Smart Engine in that ' + + 'Smart Engine GUI' + + '?

    ', + test_renderer(@revision).display_content + end + + def test_markdown + set_web_property :markup, :markdown + + assert_markup_parsed_as( + %{

    My Headline

    \n\n

    that } + + %{Smart Engine GUI?

    }, + "My Headline\n===========\n\nthat SmartEngineGUI") + + code_block = [ + 'This is a code block:', + '', + ' def a_method(arg)', + ' return ThatWay', + '', + 'Nice!' + ].join("\n") + + assert_markup_parsed_as( + %{

    This is a code block:

    \n\n
    def a_method(arg)\n} +
    +        %{return ThatWay\n
    \n\n

    Nice!

    }, + code_block) + end + + def test_markdown_hyperlink_with_slash + # in response to a bug, see http://dev.instiki.org/attachment/ticket/177 + set_web_property :markup, :markdown + + assert_markup_parsed_as( + '

    text

    ', + '[text](http://example/with/slash)') + end + + def test_mixed_formatting + textile_and_markdown = [ + 'Markdown heading', + '================', + '', + 'h2. Textile heading', + '', + '*some* **text** _with_ -styles-', + '', + '* list 1', + '* list 2' + ].join("\n") + + set_web_property :markup, :markdown + assert_markup_parsed_as( + "

    Markdown heading

    \n\n" + + "

    h2. Textile heading

    \n\n" + + "

    some text with -styles-

    \n\n" + + "
      \n
    • list 1
    • \n
    • list 2
    • \n
    ", + textile_and_markdown) + + set_web_property :markup, :textile + assert_markup_parsed_as( + "

    Markdown heading
    ================

    \n\n\n\t

    Textile heading

    " + + "\n\n\n\t

    some text with styles

    " + + "\n\n\n\t
      \n\t
    • list 1
    • \n\t\t
    • list 2
    • \n\t
    ", + textile_and_markdown) + + set_web_property :markup, :mixed + assert_markup_parsed_as( + "

    Markdown heading

    \n\n\n\t

    Textile heading

    \n\n\n\t" + + "

    some text with styles

    \n\n\n\t" + + "
      \n\t
    • list 1
    • \n\t\t
    • list 2
    • \n\t
    ", + textile_and_markdown) + end + + def test_rdoc + set_web_property :markup, :rdoc + + @revision = Revision.new(:page => @page, :content => '+hello+ that SmartEngineGUI', + :author => Author.new('DavidHeinemeierHansson')) + + assert_equal "hello that Smart Engine GUI" + + "?\n\n", + test_renderer(@revision).display_content + end + + def test_content_with_auto_links + assert_markup_parsed_as( + '

    http://www.loudthinking.com/ ' + + 'points to That Way from ' + + 'david@loudthinking.com

    ', + 'http://www.loudthinking.com/ points to ThatWay from david@loudthinking.com') + + end + + def test_content_with_aliased_links + assert_markup_parsed_as( + '

    Would a clever motor' + + ' go by any other name?

    ', + 'Would a [[SmartEngine|clever motor]] go by any other name?') + end + + def test_content_with_wikiword_in_em + assert_markup_parsed_as( + '

    should we go ' + + 'That Way or This Way?' + + '

    ', + '_should we go ThatWay or ThisWay _') + end + + def test_content_with_wikiword_in_tag + assert_markup_parsed_as( + '

    That is some Stylish Emphasis

    ', + 'That is some Stylish Emphasis') + end + + def test_content_with_escaped_wikiword + # there should be no wiki link + assert_markup_parsed_as('

    WikiWord

    ', '\WikiWord') + end + + def test_content_with_pre_blocks + assert_markup_parsed_as( + '

    A class SmartEngine end would not mark up

    CodeBlocks

    ', + 'A class SmartEngine end would not mark up
    CodeBlocks
    ') + end + + def test_content_with_autolink_in_parentheses + assert_markup_parsed_as( + '

    The W3C body (' + + 'http://www.w3c.org) sets web standards

    ', + 'The W3C body (http://www.w3c.org) sets web standards') + end + + def test_content_with_link_in_parentheses + assert_markup_parsed_as( + '

    (What is a wiki?)

    ', + '("What is a wiki?":http://wiki.org/wiki.cgi?WhatIsWiki)') + end + + def test_content_with_image_link + assert_markup_parsed_as( + '

    This is a Textile image link.

    ', + 'This !http://hobix.com/sample.jpg! is a Textile image link.') + end + + def test_content_with_inlined_img_tag + assert_markup_parsed_as( + '

    This is an inline image link.

    ', + 'This is an inline image link.') + + assert_markup_parsed_as( + '

    This is an inline image link.

    ', + 'This is an inline image link.') + end + + def test_nowiki_tag + assert_markup_parsed_as( + '

    Do not mark up [[this text]] or http://www.thislink.com.

    ', + 'Do not mark up [[this text]] ' + + 'or http://www.thislink.com.') + end + + def test_multiline_nowiki_tag + assert_markup_parsed_as( + "

    Do not mark \n up [[this text]] \nand http://this.url.com but markup " + + 'this?

    ', + "Do not mark \n up [[this text]] \n" + + "and http://this.url.com but markup [[this]]") + end + + def test_content_with_bracketted_wiki_word + set_web_property :brackets_only, true + assert_markup_parsed_as( + '

    This is a WikiWord and a tricky name ' + + 'Sperberg-McQueen?.

    ', + 'This is a WikiWord and a tricky name [[Sperberg-McQueen]].') + end + + def test_content_for_export + assert_equal '

    His Way would be ' + + 'My Way in kinda ' + + 'That Way in ' + + 'His Way though ' + + 'My Way OverThere—see ' + + 'Smart Engine in that ' + + 'Smart Engine GUI

    ', + test_renderer(@revision).display_content_for_export + end + + def test_double_replacing + @revision.content = "VersionHistory\r\n\r\ncry VersionHistory" + assert_equal '

    Version History' + + "?

    \n\n\n\t

    cry " + + 'Version History?' + + '

    ', + test_renderer(@revision).display_content + + @revision.content = "f\r\nVersionHistory\r\n\r\ncry VersionHistory" + assert_equal "

    f
    Version History" + + "?

    \n\n\n\t

    cry " + + "Version History?" + + "

    ", + test_renderer(@revision).display_content + end + + def test_difficult_wiki_words + @revision.content = "[[It's just awesome GUI!]]" + assert_equal "

    It's just awesome GUI!" + + "?

    ", + test_renderer(@revision).display_content + end + + def test_revisions_diff + Revision.create(:page => @page, :content => 'What a blue and lovely morning', + :author => Author.new('DavidHeinemeierHansson'), :revised_at => Time.now) + Revision.create(:page => @page, :content => 'What a red and lovely morning today', + :author => Author.new('DavidHeinemeierHansson'), :revised_at => Time.now) + + assert_equal "

    What a bluered" + + " and lovely morning today

    ", test_renderer(@page.revisions.last).display_diff + end + + def test_link_to_file + assert_markup_parsed_as( + '

    doc.pdf?

    ', + '[[doc.pdf:file]]') + end + + def test_link_to_pic + WikiFile.delete_all + require 'fileutils' + FileUtils.rm_rf("#{RAILS_ROOT}/public/wiki1/files/*") + @web.wiki_files.create(:file_name => 'square.jpg', :description => 'Square', :content => 'never mind') + assert_markup_parsed_as( + '

    Square

    ', + '[[square.jpg|Square:pic]]') + assert_markup_parsed_as( + '

    square.jpg

    ', + '[[square.jpg:pic]]') + end + + def test_link_to_non_existant_pic + assert_markup_parsed_as( + '

    NonExistant?' + + '

    ', + '[[NonExistant.jpg|NonExistant:pic]]') + assert_markup_parsed_as( + '

    NonExistant.jpg?' + + '

    ', + '[[NonExistant.jpg:pic]]') + end + + def test_wiki_link_with_colon + assert_markup_parsed_as( + '

    With:Colon?

    ', + '[[With:Colon]]') + end + + def test_list_with_tildas + list_with_tildas = <<-EOL + * "a":~b + * c~ d + EOL + + assert_markup_parsed_as( + "
      \n\t
    • a
    • \n\t\t
    • c~ d
    • \n\t
    ", + list_with_tildas) + end + + def test_textile_image_in_mixed_wiki + set_web_property :markup, :mixed + assert_markup_parsed_as( + "

    \"\"\nss

    ", + "!http://google.com!\r\nss") + end + + + def test_references_creation_links + new_page = @web.add_page('NewPage', 'HomePage NewPage', + Time.local(2004, 4, 4, 16, 50), 'AlexeyVerkhovsky', test_renderer) + + references = new_page.wiki_references(true) + assert_equal 2, references.size + assert_equal 'HomePage', references[0].referenced_name + assert_equal WikiReference::LINKED_PAGE, references[0].link_type + assert_equal 'NewPage', references[1].referenced_name + assert_equal WikiReference::LINKED_PAGE, references[1].link_type + end + + def test_references_creation_includes + new_page = @web.add_page('NewPage', '[[!include IncludedPage]]', + Time.local(2004, 4, 4, 16, 50), 'AlexeyVerkhovsky', test_renderer) + + references = new_page.wiki_references(true) + assert_equal 1, references.size + assert_equal 'IncludedPage', references[0].referenced_name + assert_equal WikiReference::INCLUDED_PAGE, references[0].link_type + end + + def test_references_creation_categories + new_page = @web.add_page('NewPage', "Foo\ncategory: NewPageCategory", + Time.local(2004, 4, 4, 16, 50), 'AlexeyVerkhovsky', test_renderer) + + references = new_page.wiki_references(true) + assert_equal 1, references.size + assert_equal 'NewPageCategory', references[0].referenced_name + assert_equal WikiReference::CATEGORY, references[0].link_type + end + + def test_rendering_included_page_under_different_modes + included = @web.add_page('Included', 'link to HomePage', Time.now, 'AnAuthor', test_renderer) + main = @web.add_page('Main', '[[!include Included]]', Time.now, 'AnAuthor', test_renderer) + + assert_equal '

    link to Home Page

    ', + test_renderer(main).display_content + assert_equal '

    link to Home Page

    ', + test_renderer(main).display_published + assert_equal '

    link to Home Page

    ', + test_renderer(main).display_content_for_export + end + + private + + def add_sample_pages + @in_love = @web.add_page('EverBeenInLove', 'Who am I me', + Time.local(2004, 4, 4, 16, 50), 'DavidHeinemeierHansson', test_renderer) + @hated = @web.add_page('EverBeenHated', 'I am me EverBeenHated', + Time.local(2004, 4, 4, 16, 51), 'DavidHeinemeierHansson', test_renderer) + end + + def assert_markup_parsed_as(expected_output, input) + revision = Revision.new(:page => @page, :content => input, :author => Author.new('AnAuthor')) + assert_equal expected_output, test_renderer(revision).display_content, 'Rendering output not as expected' + end + + def rendered_content(page) + test_renderer(page.revisions.last).display_content + end + +end \ No newline at end of file diff --git a/test/unit/page_test.rb b/test/unit/page_test.rb new file mode 100644 index 00000000..5513cd48 --- /dev/null +++ b/test/unit/page_test.rb @@ -0,0 +1,122 @@ +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') + +class PageTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system + + def setup + @page = pages(:first_page) + end + + + def test_lock + assert !@page.locked?(Time.local(2004, 4, 4, 16, 50)) + + @page.lock(Time.local(2004, 4, 4, 16, 30), "DavidHeinemeierHansson") + + assert @page.locked?(Time.local(2004, 4, 4, 16, 50)) + assert !@page.locked?(Time.local(2004, 4, 4, 17, 1)) + + @page.unlock + + assert !@page.locked?(Time.local(2004, 4, 4, 16, 50)) + end + + def test_lock_duration + @page.lock(Time.local(2004, 4, 4, 16, 30), "DavidHeinemeierHansson") + + assert_equal 15, @page.lock_duration(Time.local(2004, 4, 4, 16, 45)) + end + + def test_plain_name + assert_equal "First Page", @page.plain_name + end + + def test_revise + @page.revise('HisWay would be MyWay in kinda lame', Time.local(2004, 4, 4, 16, 55), + 'MarianneSyhler', test_renderer) + @page.reload + + assert_equal 2, @page.revisions.length, 'Should have two revisions' + assert_equal 'MarianneSyhler', @page.current_revision.author.to_s, + 'Mary should be the author now' + assert_equal 'DavidHeinemeierHansson', @page.revisions.first.author.to_s, + 'David was the first author' + end + + def test_revise_continous_revision + @page.revise('HisWay would be MyWay in kinda lame', Time.local(2004, 4, 4, 16, 55), + 'MarianneSyhler', test_renderer) + @page.reload + assert_equal 2, @page.revisions.length + assert_equal 'HisWay would be MyWay in kinda lame', @page.content + + # consecutive revision by the same author within 30 minutes doesn't create a new revision + @page.revise('HisWay would be MyWay in kinda update', Time.local(2004, 4, 4, 16, 57), + 'MarianneSyhler', test_renderer) + @page.reload + assert_equal 2, @page.revisions.length + assert_equal 'HisWay would be MyWay in kinda update', @page.content + assert_equal Time.local(2004, 4, 4, 16, 57), @page.revised_at + + # but consecutive revision by another author results in a new revision + @page.revise('HisWay would be MyWay in the house', Time.local(2004, 4, 4, 16, 58), + 'DavidHeinemeierHansson', test_renderer) + @page.reload + assert_equal 3, @page.revisions.length + assert_equal 'HisWay would be MyWay in the house', @page.content + + # consecutive update after 30 minutes since the last one also creates a new revision, + # even when it is by the same author + @page.revise('HisWay would be MyWay in my way', Time.local(2004, 4, 4, 17, 30), + 'DavidHeinemeierHansson', test_renderer) + @page.reload + assert_equal 4, @page.revisions.length + end + + def test_revise_content_unchanged + last_revision_before = @page.current_revision + revisions_number_before = @page.revisions.size + + assert_raises(Instiki::ValidationError) { + @page.revise(@page.current_revision.content, Time.now, 'AlexeyVerkhovsky', test_renderer) + } + + assert_equal last_revision_before, @page.current_revision(true) + assert_equal revisions_number_before, @page.revisions.size + end + + def test_revise_changes_references_from_wanted_to_linked_for_new_pages + web = Web.find(1) + new_page = Page.new(:web => web, :name => 'NewPage') + new_page.revise('Reference to WantedPage, and to WantedPage2', Time.now, 'AlexeyVerkhovsky', + test_renderer) + + references = new_page.wiki_references(true) + assert_equal 2, references.size + assert_equal 'WantedPage', references[0].referenced_name + assert_equal WikiReference::WANTED_PAGE, references[0].link_type + assert_equal 'WantedPage2', references[1].referenced_name + assert_equal WikiReference::WANTED_PAGE, references[1].link_type + + wanted_page = Page.new(:web => web, :name => 'WantedPage') + wanted_page.revise('And here it is!', Time.now, 'AlexeyVerkhovsky', test_renderer) + + # link type stored for NewPage -> WantedPage reference should change from WANTED to LINKED + # reference NewPage -> WantedPage2 should remain the same + references = new_page.wiki_references(true) + assert_equal 2, references.size + assert_equal 'WantedPage', references[0].referenced_name + assert_equal WikiReference::LINKED_PAGE, references[0].link_type + assert_equal 'WantedPage2', references[1].referenced_name + assert_equal WikiReference::WANTED_PAGE, references[1].link_type + end + + def test_rollback + @page.revise("spot two", Time.now, "David", test_renderer) + @page.revise("spot three", Time.now + 2000, "David", test_renderer) + assert_equal 3, @page.revisions(true).length, "Should have three revisions" + @page.current_revision(true) + @page.rollback(0, Time.now, '127.0.0.1', test_renderer) + assert_equal "HisWay would be MyWay in kinda ThatWay in HisWay though MyWay \\\\OverThere -- see SmartEngine in that SmartEngineGUI", @page.current_revision(true).content + end +end diff --git a/test/unit/redcloth_for_tex_test.rb b/test/unit/redcloth_for_tex_test.rb new file mode 100755 index 00000000..3556beaf --- /dev/null +++ b/test/unit/redcloth_for_tex_test.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' +require 'redcloth_for_tex' + +class RedClothForTexTest < Test::Unit::TestCase + def test_basics + assert_equal '{\bf First Page}', RedClothForTex.new("*First Page*").to_tex + assert_equal '{\em First Page}', RedClothForTex.new("_First Page_").to_tex + assert_equal "\\begin{itemize}\n\t\\item A\n\t\t\\item B\n\t\t\\item C\n\t\\end{itemize}", RedClothForTex.new("* A\n* B\n* C").to_tex + end + + def test_blocks + assert_equal '\section*{hello}', RedClothForTex.new("h1. hello").to_tex + assert_equal '\subsection*{hello}', RedClothForTex.new("h2. hello").to_tex + end + + def test_table_of_contents + +source = < 'Abe', 'B' => 'Babe')) + end + + def test_entities + assert_equal "Beck \\& Fowler are 100\\% cool", RedClothForTex.new("Beck & Fowler are 100% cool").to_tex + end + + def test_bracket_links + assert_equal "such a Horrible Day, but I won't be Made Useless", RedClothForTex.new("such a [[Horrible Day]], but I won't be [[Made Useless]]").to_tex + end + + def test_footnotes_on_abbreviations + assert_equal( + "such a Horrible Day\\footnote{1}, but I won't be Made Useless", + RedClothForTex.new("such a [[Horrible Day]][1], but I won't be [[Made Useless]]").to_tex + ) + end + + def test_subsection_depth + assert_equal "\\subsubsection*{Hello}", RedClothForTex.new("h4. Hello").to_tex + end +end diff --git a/test/unit/uri_test.rb b/test/unit/uri_test.rb new file mode 100755 index 00000000..29326c3b --- /dev/null +++ b/test/unit/uri_test.rb @@ -0,0 +1,217 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' +require 'chunks/uri' + +class URITest < Test::Unit::TestCase + include ChunkMatch + + def test_non_matches + assert_conversion_does_not_apply(URIChunk, 'There is no URI here') + assert_conversion_does_not_apply(URIChunk, + 'One gemstone is the garnet:reddish in colour, like ruby') + end + + def test_simple_uri + # Simplest case + match(URIChunk, 'http://www.example.com', + :scheme =>'http', :host =>'www.example.com', :path => nil, + :link_text => 'http://www.example.com' + ) + # With trailing slash + match(URIChunk, 'http://www.example.com/', + :scheme =>'http', :host =>'www.example.com', :path => '/', + :link_text => 'http://www.example.com/' + ) + # Without http:// + match(URIChunk, 'www.example.com', + :scheme =>'http', :host =>'www.example.com', :link_text => 'www.example.com' + ) + # two parts + match(URIChunk, 'example.com', + :scheme =>'http',:host =>'example.com', :link_text => 'example.com' + ) + # "unusual" base domain (was a bug in an early version) + match(URIChunk, 'http://example.com.au/', + :scheme =>'http', :host =>'example.com.au', :link_text => 'http://example.com.au/' + ) + # "unusual" base domain without http:// + match(URIChunk, 'example.com.au', + :scheme =>'http', :host =>'example.com.au', :link_text => 'example.com.au' + ) + # Another "unusual" base domain + match(URIChunk, 'http://www.example.co.uk/', + :scheme =>'http', :host =>'www.example.co.uk', + :link_text => 'http://www.example.co.uk/' + ) + match(URIChunk, 'example.co.uk', + :scheme =>'http', :host =>'example.co.uk', :link_text => 'example.co.uk' + ) + # With some path at the end + match(URIChunk, 'http://moinmoin.wikiwikiweb.de/HelpOnNavigation', + :scheme => 'http', :host => 'moinmoin.wikiwikiweb.de', :path => '/HelpOnNavigation', + :link_text => 'http://moinmoin.wikiwikiweb.de/HelpOnNavigation' + ) + # With some path at the end, and withot http:// prefix + match(URIChunk, 'moinmoin.wikiwikiweb.de/HelpOnNavigation', + :scheme => 'http', :host => 'moinmoin.wikiwikiweb.de', :path => '/HelpOnNavigation', + :link_text => 'moinmoin.wikiwikiweb.de/HelpOnNavigation' + ) + # With a port number + match(URIChunk, 'http://www.example.com:80', + :scheme =>'http', :host =>'www.example.com', :port => '80', :path => nil, + :link_text => 'http://www.example.com:80') + # With a port number and a path + match(URIChunk, 'http://www.example.com.tw:80/HelpOnNavigation', + :scheme =>'http', :host =>'www.example.com.tw', :port => '80', :path => '/HelpOnNavigation', + :link_text => 'http://www.example.com.tw:80/HelpOnNavigation') + # With a query + match(URIChunk, 'http://www.example.com.tw:80/HelpOnNavigation?arg=val', + :scheme =>'http', :host =>'www.example.com.tw', :port => '80', :path => '/HelpOnNavigation', + :query => 'arg=val', + :link_text => 'http://www.example.com.tw:80/HelpOnNavigation?arg=val') + # Query with two arguments + match(URIChunk, 'http://www.example.com.tw:80/HelpOnNavigation?arg=val&arg2=val2', + :scheme =>'http', :host =>'www.example.com.tw', :port => '80', :path => '/HelpOnNavigation', + :query => 'arg=val&arg2=val2', + :link_text => 'http://www.example.com.tw:80/HelpOnNavigation?arg=val&arg2=val2') + # HTTPS + match(URIChunk, 'https://www.example.com', + :scheme =>'https', :host =>'www.example.com', :port => nil, :path => nil, :query => nil, + :link_text => 'https://www.example.com') + # FTP + match(URIChunk, 'ftp://www.example.com', + :scheme =>'ftp', :host =>'www.example.com', :port => nil, :path => nil, :query => nil, + :link_text => 'ftp://www.example.com') + # mailto + match(URIChunk, 'mailto:jdoe123@example.com', + :scheme =>'mailto', :host =>'example.com', :port => nil, :path => nil, :query => nil, + :user => 'jdoe123', :link_text => 'mailto:jdoe123@example.com') + # something nonexistant + match(URIChunk, 'foobar://www.example.com', + :scheme =>'foobar', :host =>'www.example.com', :port => nil, :path => nil, :query => nil, + :link_text => 'foobar://www.example.com') + + # Soap opera (the most complex case imaginable... well, not really, there should be more evil) + match(URIChunk, 'http://www.example.com.tw:80/~jdoe123/Help%20Me%20?arg=val&arg2=val2', + :scheme =>'http', :host =>'www.example.com.tw', :port => '80', + :path => '/~jdoe123/Help%20Me%20', :query => 'arg=val&arg2=val2', + :link_text => 'http://www.example.com.tw:80/~jdoe123/Help%20Me%20?arg=val&arg2=val2') + + # from 0.9 bug reports + match(URIChunk, 'http://www2.pos.to/~tosh/ruby/rdtool/en/doc/rd-draft.html', + :scheme =>'http', :host => 'www2.pos.to', + :path => '/~tosh/ruby/rdtool/en/doc/rd-draft.html') + + match(URIChunk, 'http://support.microsoft.com/default.aspx?scid=kb;en-us;234562', + :scheme =>'http', :host => 'support.microsoft.com', :path => '/default.aspx', + :query => 'scid=kb;en-us;234562') + end + + def test_email_uri + match(URIChunk, 'mail@example.com', + :user => 'mail', :host => 'example.com', :link_text => 'mail@example.com' + ) + end + + def test_non_email + # The @ is part of the normal text, but 'example.com' is marked up. + match(URIChunk, 'Not an email: @example.com', :user => nil, :uri => 'http://example.com') + end + + def test_textile_image + assert_conversion_does_not_apply(URIChunk, + 'This !http://hobix.com/sample.jpg! is a Textile image link.') + end + + def test_textile_link + assert_conversion_does_not_apply(URIChunk, + 'This "hobix (hobix)":http://hobix.com/sample.jpg is a Textile link.') + # just to be sure ... + match(URIChunk, 'This http://hobix.com/sample.jpg should match', + :link_text => 'http://hobix.com/sample.jpg') + end + + def test_inline_html + assert_conversion_does_not_apply(URIChunk, '') + assert_conversion_does_not_apply(URIChunk, "") + end + + def test_non_uri + # "so" is a valid country code; "libproxy.so" is a valid url + match(URIChunk, 'libproxy.so', :link_text => 'libproxy.so') + + assert_conversion_does_not_apply URIChunk, 'httpd.conf' + assert_conversion_does_not_apply URIChunk, 'ld.so.conf' + assert_conversion_does_not_apply URIChunk, 'index.jpeg' + assert_conversion_does_not_apply URIChunk, 'index.jpg' + assert_conversion_does_not_apply URIChunk, 'file.txt' + assert_conversion_does_not_apply URIChunk, 'file.doc' + assert_conversion_does_not_apply URIChunk, 'file.pdf' + assert_conversion_does_not_apply URIChunk, 'file.png' + assert_conversion_does_not_apply URIChunk, 'file.ps' + end + + def test_uri_in_text + match(URIChunk, 'Go to: http://www.example.com/', :host => 'www.example.com', :path =>'/') + match(URIChunk, 'http://www.example.com/ is a link.', :host => 'www.example.com') + match(URIChunk, + 'Email david@loudthinking.com', + :scheme =>'mailto', :user =>'david', :host =>'loudthinking.com') + # check that trailing punctuation is not included in the hostname + match(URIChunk, 'Hey dude, http://fake.link.com.', :scheme => 'http', :host => 'fake.link.com') + # this is a textile link, no match please. + assert_conversion_does_not_apply(URIChunk, '"link":http://fake.link.com.') + end + + def test_uri_in_parentheses + match(URIChunk, 'URI (http://brackets.com.de) in brackets', :host => 'brackets.com.de') + match(URIChunk, 'because (as shown at research.net) the results', :host => 'research.net') + match(URIChunk, + 'A wiki (http://wiki.org/wiki.cgi?WhatIsWiki) page', + :scheme => 'http', :host => 'wiki.org', :path => '/wiki.cgi', :query => 'WhatIsWiki' + ) + end + + def test_uri_list_item + match( + URIChunk, + '* http://www.btinternet.com/~mail2minh/SonyEricssonP80xPlatform.sis', + :path => '/~mail2minh/SonyEricssonP80xPlatform.sis' + ) + end + + def test_interesting_uri_with__comma + # Counter-intuitively, this URL matches, but the query part includes the trailing comma. + # It has no way to know that the query does not include the comma. + match( + URIChunk, + "This text contains a URL http://someplace.org:8080/~person/stuff.cgi?arg=val, doesn't it?", + :scheme => 'http', :host => 'someplace.org', :port => '8080', :path => '/~person/stuff.cgi', + :query => 'arg=val,') + end + + def test_local_urls + # normal + match(LocalURIChunk, 'http://perforce:8001/toto.html', + :scheme => 'http', :host => 'perforce', + :port => '8001', :link_text => 'http://perforce:8001/toto.html') + + # in parentheses + match(LocalURIChunk, 'URI (http://localhost:2500) in brackets', + :host => 'localhost', :port => '2500') + match(LocalURIChunk, 'because (as shown at http://perforce:8001) the results', + :host => 'perforce', :port => '8001') + match(LocalURIChunk, + 'A wiki (http://localhost:2500/wiki.cgi?WhatIsWiki) page', + :scheme => 'http', :host => 'localhost', :path => '/wiki.cgi', + :port => '2500', :query => 'WhatIsWiki') + end + + def assert_conversion_does_not_apply(chunk_type, str) + processed_str = ContentStub.new(str.dup) + chunk_type.apply_to(processed_str) + assert_equal(str, processed_str) + end + +end diff --git a/test/unit/web_test.rb b/test/unit/web_test.rb new file mode 100644 index 00000000..62c3935e --- /dev/null +++ b/test/unit/web_test.rb @@ -0,0 +1,105 @@ +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') + +class WebTest < Test::Unit::TestCase + fixtures :webs, :pages, :revisions, :system, :wiki_references + + def setup + @web = webs(:instiki) + end + + def test_pages_by_revision + add_sample_pages + assert_equal 'EverBeenHated', @web.select.by_revision.first.name + end + + def test_pages_by_match + add_sample_pages + assert_equal 2, @web.select { |page| page.content =~ /me/i }.length + assert_equal 1, @web.select { |page| page.content =~ /Who/i }.length + assert_equal 0, @web.select { |page| page.content =~ /none/i }.length + end + + def test_002_references + add_sample_pages + assert_equal 1, @web.select.pages_that_reference('EverBeenHated').length + assert_equal 0, @web.select.pages_that_reference('EverBeenInLove').length + end + + def test_delete + add_sample_pages + assert_equal 2, @web.pages.length + @web.remove_pages([ @web.page('EverBeenInLove') ]) + assert_equal 1, @web.pages(true).length + end + + def test_initialize + web = Web.new(:name => 'Wiki2', :address => 'wiki2', :password => '123') + + assert_equal 'Wiki2', web.name + assert_equal 'wiki2', web.address + assert_equal '123', web.password + + # new web should be set for maximum features enabled + assert_equal :textile, web.markup + assert_equal '008B26', web.color + assert !web.safe_mode? + assert_equal([], web.pages) + assert web.allow_uploads? + assert_nil web.additional_style + assert !web.published? + assert !web.brackets_only? + assert !web.count_pages? + assert_equal 100, web.max_upload_size + end + + def test_initialize_invalid_name + assert_raises(Instiki::ValidationError) { + Web.create(:name => 'Wiki2', :address => "wiki\234", :password => '123') + } + end + + def test_new_page_linked_from_mother_page + # this was a bug in revision 204 + home = @web.add_page('HomePage', 'This page refers to AnotherPage', + Time.local(2004, 4, 4, 16, 50), 'Alexey Verkhovsky', test_renderer) + @web.add_page('AnotherPage', 'This is \AnotherPage', + Time.local(2004, 4, 4, 16, 51), 'Alexey Verkhovsky', test_renderer) + + @web.pages(true) + assert_equal [home], @web.select.pages_that_link_to('AnotherPage') + end + + def test_001_orphaned_pages + add_sample_pages + home = @web.add_page('HomePage', + 'This is a home page, it should not be an orphan', + Time.local(2004, 4, 4, 16, 50), 'AlexeyVerkhovsky', test_renderer) + author = @web.add_page('AlexeyVerkhovsky', + 'This is an author page, it should not be an orphan', + Time.local(2004, 4, 4, 16, 50), 'AlexeyVerkhovsky', test_renderer) + self_linked = @web.add_page('SelfLinked', + 'I am SelfLinked and link to EverBeenInLove', + Time.local(2004, 4, 4, 16, 50), 'AnonymousCoward', test_renderer) + + # page that links to itself, and nobody else links to it must be an orphan + assert_equal ['EverBeenHated', 'SelfLinked'], + @web.select.orphaned_pages.collect{ |page| page.name }.sort + end + + def test_page_names_by_author + page_names_by_author = webs(:test_wiki).page_names_by_author + assert_equal %w(AnAuthor DavidHeinemeierHansson Guest Me TreeHugger), + page_names_by_author.keys.sort + assert_equal %w(FirstPage HomePage), page_names_by_author['DavidHeinemeierHansson'] + assert_equal %w(Oak), page_names_by_author['TreeHugger'] + end + + private + + def add_sample_pages + @in_love = @web.add_page('EverBeenInLove', 'Who am I me', + Time.local(2004, 4, 4, 16, 50), 'DavidHeinemeierHansson', test_renderer) + @hated = @web.add_page('EverBeenHated', 'I am me EverBeenHated', + Time.local(2004, 4, 4, 16, 51), 'DavidHeinemeierHansson', test_renderer) + end +end diff --git a/test/unit/wiki_file_test.rb b/test/unit/wiki_file_test.rb new file mode 100644 index 00000000..233a73fa --- /dev/null +++ b/test/unit/wiki_file_test.rb @@ -0,0 +1,84 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'fileutils' + +class WikiFileTest < Test::Unit::TestCase + include FileUtils + fixtures :webs, :pages, :revisions, :system, :wiki_references + + def setup + @web = webs(:test_wiki) + mkdir_p("#{RAILS_ROOT}/public/wiki1/files/") + rm_rf("#{RAILS_ROOT}/public/wiki1/files/*") + WikiFile.delete_all + end + + def test_basic_store_and_retrieve_ascii_file + @web.wiki_files.create(:file_name => 'binary_file', :description => 'Binary file', :content => "\001\002\003") + binary = WikiFile.find_by_file_name('binary_file') + assert_equal "\001\002\003", binary.content + end + + def test_basic_store_and_retrieve_binary_file + @web.wiki_files.create(:file_name => 'text_file', :description => 'Text file', :content => "abc") + text = WikiFile.find_by_file_name('text_file') + assert_equal "abc", text.content + end + + def test_storing_an_image + rails_gif = File.open("#{RAILS_ROOT}/test/fixtures/rails.gif", 'rb') { |f| f.read } + assert_equal rails_gif.size, File.size("#{RAILS_ROOT}/test/fixtures/rails.gif") + + @web.wiki_files.create(:file_name => 'rails.gif', :description => 'Rails logo', :content => rails_gif) + + rails_gif_from_db = WikiFile.find_by_file_name('rails.gif') + assert_equal rails_gif.size, rails_gif_from_db.content.size + assert_equal rails_gif, rails_gif_from_db.content + end + + def test_mandatory_fields_validations + assert_validation(:file_name, '', :fail) + assert_validation(:file_name, nil, :fail) + assert_validation(:content, '', :fail) + assert_validation(:content, nil, :fail) + end + + def test_upload_size_validation + assert_validation(:content, 'a' * 100.kilobytes, :pass) + assert_validation(:content, 'a' * (100.kilobytes + 1), :fail) + end + + def test_file_name_size_validation + assert_validation(:file_name, '', :fail) + assert_validation(:file_name, 'a', :pass) + assert_validation(:file_name, 'a' * 50, :pass) + assert_validation(:file_name, 'a' * 51, :fail) + end + + def test_file_name_pattern_validation + assert_validation(:file_name, ".. Accep-table File.name", :pass) + assert_validation(:file_name, "/bad", :fail) + assert_validation(:file_name, "~bad", :fail) + assert_validation(:file_name, "..\bad", :fail) + assert_validation(:file_name, "\001bad", :fail) + assert_validation(:file_name, ".", :fail) + assert_validation(:file_name, "..", :fail) + end + + def test_find_by_file_name + assert_equal @file1, WikiFile.find_by_file_name('file1.txt') + assert_nil WikiFile.find_by_file_name('unknown_file') + end + + def assert_validation(field, value, expected_result) + values = {:file_name => '0', :description => '0', :content => '0'} + raise "WikiFile has no attribute named #{field.inspect}" unless values.has_key?(field) + values[field] = value + + new_object = @web.wiki_files.create(values) + if expected_result == :pass then assert(new_object.valid?, new_object.errors.inspect) + elsif expected_result == :fail then assert(!new_object.valid?) + else raise "Unknown value of expected_result: #{expected_result.inspect}" + end + end + +end diff --git a/test/unit/wiki_words_test.rb b/test/unit/wiki_words_test.rb new file mode 100755 index 00000000..f90a8d12 --- /dev/null +++ b/test/unit/wiki_words_test.rb @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.dirname(__FILE__) + '/../test_helper') +require 'wiki_words' + +class WikiWordsTest < Test::Unit::TestCase + + def test_utf8_characters_in_wiki_word + assert_equal "Æåle Øen", WikiWords.separate("ÆåleØen") + assert_equal "ÆÅØle Øen", WikiWords.separate("ÆÅØleØen") + assert_equal "Æe ÅØle Øen", WikiWords.separate("ÆeÅØleØen") + assert_equal "Legetøj", WikiWords.separate("Legetøj") + end +end diff --git a/test/watir/e2e.rb b/test/watir/e2e.rb new file mode 100644 index 00000000..58cbdd20 --- /dev/null +++ b/test/watir/e2e.rb @@ -0,0 +1,370 @@ +require 'fileutils' +require 'cgi' +require 'test/unit' +require 'rexml/document' + +INSTIKI_ROOT = File.expand_path(File.dirname(__FILE__) + "/../..") +require(File.expand_path(File.dirname(__FILE__) + "/../../config/environment")) + +# TODO Create tests for: +# * exporting HTML +# * exporting markup +# * include tag + +# Use instiki/../watir, if such a directory exists; This can be a CVS HEAD version of Watir. +# Otherwise Watir has to be installed in ruby/lib. +$:.unshift INSTIKI_ROOT + '/../watir' if File.exists?(INSTIKI_ROOT + '/../watir/watir.rb') +require 'watir' + +INSTIKI_PORT = 2501 +HOME = "http://localhost:#{INSTIKI_PORT}" + +class E2EInstikiTest < Test::Unit::TestCase + + def startup + @@instiki = InstikiController.start + + sleep 8 + @@ie = Watir::IE.start(HOME) + @@ie.set_fast_speed if (ARGV & ['-d', '--demo', '-demo', 'demo']).empty? + + setup_web + setup_home_page + + @@ie + end + + def self.shutdown + @@ie.close if defined? @@ie + @@instiki.stop + end + + def ie + if defined? @@ie + @@ie + else + startup + end + end + + def setup + ie.goto HOME + ie + end + + # Numbers like _00010_ determine the sequence in which the test cases are executed by Test::Unit + # Unfortunately, this sequence is important. + + def test_00010_home_page_contents + check_main_menu + check_bottom_menu + check_footnote + end + + def test_00020_add_a_page + # Add reference to a non-existant wiki page + enter_markup('HomePage', '[[Another Wiki Page]]') + assert_equal '?', ie.link(:url, url(:new, 'Another Wiki Page')).text + + # Edit the first revision of a page + enter_markup('Another Wiki Page', 'First revision of Another Wiki Page, linked from HomePage') + + # Check contents of the new page + assert_equal url(:show, 'Another Wiki Page'), ie.url + assert_match /First revision of Another Wiki Page, linked from Home Page/, ie.text + assert_match /Linked from: Home Page/, ie.text + + # There must be three links to HomePage - main menu, contents of the page and navigation bar + links_to_homepage = ie.links.to_a.select { |link| link.text == 'Home Page' } + assert_equal 3, links_to_homepage.size + links_to_homepage.each { |link| assert_equal url(:show, 'HomePage'), link.href } + + # Check also the "created on ... by ..." footnote + assert_match Regexp.new('Created on ' + date_pattern + ' by Anonymous Coward\?'), ie.text + end + + def test_00030_edit_page + enter_markup('TestEditPage', 'Test Edit Page, revision 1') + assert_match /Test Edit Page, revision 1/, ie.text + + # subsequent revision by the anonymous author + enter_markup('TestEditPage', 'Test Edit Page, revision 1, altered') + assert_match /Test Edit Page, revision 1, altered/, ie.text + assert_match Regexp.new('Created on ' + date_pattern + ' by Anonymous Coward\?'), ie.text + + # revision by a named author + enter_markup('TestEditPage', 'Test Edit Page, revision 2', 'Author') + assert_match /Test Edit Page, revision 2/, ie.text + assert_match Regexp.new('Revised on ' + date_pattern + ' by Author\?'), ie.text + + link_to_previous_revision = ie.link(:name, 'to_previous_revision') + assert_equal url(:revision, 'TestEditPage', 1), link_to_previous_revision.href + assert_equal 'Back in time', link_to_previous_revision.text + assert_match /Edit \| Back in time \(1 revision\) \| See changes/, ie.text + + # another anonymous revision + enter_markup('TestEditPage', 'Test Edit Page, revision 3') + assert_match /Test Edit Page, revision 3/, ie.text + assert_match /Edit \| Back in time \(2 revisions\) \| See changes /, ie.text + end + + def test_00040_traversing_revisions + ie.goto url(:revision, 'TestEditPage', 2) + assert_match /Test Edit Page, revision 2/, ie.text + assert_match(Regexp.new( + 'Forward in time \(to current\) \| Back in time \(1 more\) \| See current \| See changes \| Rollback'), + ie.text) + + ie.link(:name, 'to_previous_revision').click + assert_match /Test Edit Page, revision 1, altered/, ie.text + assert_match /Forward in time \(2 more\) \| See current \| Rollback/, ie.text + + ie.link(:name, 'to_next_revision').click + assert_match /Test Edit Page, revision 2/, ie.text + + ie.link(:name, 'to_next_revision').click + assert_match /Test Edit Page, revision 3/, ie.text + end + + def test_00050_rollback + ie.goto url(:revision, 'TestEditPage', 2) + assert_match /Test Edit Page, revision 2/, ie.text + ie.link(:name, 'rollback').click + assert_equal url(:rollback, 'TestEditPage', 2), ie.url + assert_equal 'Test Edit Page, revision 2', ie.text_field(:name, 'content').value + + ie.text_field(:name, 'content').set('Test Edit Page, revision 2, rolled back') + ie.button(:value, 'Update').click + + assert_equal url(:show, 'TestEditPage'), ie.url + assert_match /Test Edit Page, revision 2, rolled back/, ie.text + end + + def test_0060_see_changes + ie.goto url(:show, 'TestEditPage') + + assert_match /Test Edit Page, revision 2, rolled back/, ie.text + + ie.link(:text, 'See changes').click + + assert_match /Showing changes from revision #2 to #3: Added \| Removed/, ie.text + assert_match /Test Edit Page, revision 22, rolled back/, ie.text + + ie.link(:text, 'Hide changes').click + + assert_match /Test Edit Page, revision 2, rolled back/, ie.text + end + + def test_0070_all_pages + # create a wanted page, and unlink Another Wiki Page from Home Page + # (to see that it doesn't show up in the orphans, regardless) + enter_markup('Another Wiki Page', 'Reference to a NonExistantPage') + + ie.link(:text, 'All Pages').click + + page_links = ie.links.map { |l| l.text } + expected_page_links = ['Another Wiki Page', 'Home Page', 'Test Edit Page', '?', + 'Another Wiki Page', 'Test Edit Page'] + assert_equal expected_page_links, page_links[-6..-1] + links_sequence = + 'All Pages.*Another Wiki Page.*Home Page.*Test Edit Page.*' + + 'Wanted Pages.*Non Existant Page\? wanted by Another Wiki Page.*'+ + 'Orphaned Pages.*Test Edit Page.*' + assert_match Regexp.new(links_sequence, Regexp::MULTILINE), ie.text + # and before that, we have the tail of the main menu + + +require 'breakpoint'; breakpoint + + + assert_equal 'Export', page_links[-7] + end + + def test_0080_recently_revised + ie.link(:text, 'Recently Revised').click + + links = ie.links.map { |l| l.text } + assert_equal ['Another Wiki Page', '?', 'Test Edit Page', '?', 'Home Page', '?'], links[-6..-1] + + expected_text = + 'Another Wiki Page.*by Anonymous Coward\?.*' + + 'Test Edit Page.*by Anonymous Coward\?.*' + + 'Home Page.*by Anonymous Coward\?.*' + assert_match Regexp.new(expected_text, Regexp::MULTILINE), ie.text + end + + def test_0090_authors + # create a revision of TestEditPage, and a corresponding author page + enter_markup('TestEditPage', '3rd revision of this page', 'Another Author') + ie.link(:afterText, 'Another Author').click + assert_equal url(:new, 'Another Author'), ie.url + enter_markup('Another Author', 'Email me at another_author@foo.bar.com', 'Another Author') + + ie.link(:text, 'Authors').click + + expected_authors = + 'Anonymous Coward\? co- or authored: Another Wiki Page, Home Page, Test Edit Page.*' + + 'Another Author co- or authored: Another Author, Test Edit Page.*' + + 'Author\? co- or authored: Test Edit Page' + assert_match Regexp.new(expected_authors, Regexp::MULTILINE), ie.text + + ie.link(:text, 'Another Author').click + assert_equal url(:show, 'Another Author'), ie.url + ie.back + ie.link(:text, 'Test Edit Page').click + assert_equal url(:show, 'TestEditPage'), ie.url + end + + def test_0100_feeds + ie.link(:text, 'Feeds').click + assert_equal url(:rss_with_content), ie.link(:text, 'Full content (RSS 2.0)').href + assert_equal url(:rss_with_headlines), ie.link(:text, 'Headlines (RSS 2.0)').href + + ie.link(:text, 'Full content (RSS 2.0)').click + assert_nothing_raised { REXML::Document.new ie.text } + + ie.back + ie.link(:text, 'Headlines (RSS 2.0)').click + assert_nothing_raised { REXML::Document.new ie.text } + end + + private + + def bp + require 'breakpoint' + breakpoint + end + + def check_main_menu + assert_equal HOME + '/wiki/list', ie.link(:text, 'All Pages').href + assert_equal HOME + '/wiki/recently_revised', ie.link(:text, 'Recently Revised').href + assert_equal HOME + '/wiki/authors', ie.link(:text, 'Authors').href + assert_equal HOME + '/wiki/feeds', ie.link(:text, 'Feeds').href + assert_equal HOME + '/wiki/export', ie.link(:text, 'Export').href + end + + def check_bottom_menu + assert_equal url(:edit, 'HomePage'), ie.link(:text, 'Edit Page').href + assert_equal HOME + '/wiki/edit_web', ie.link(:text, 'Edit Web').href + assert_equal url(:print, 'HomePage'), ie.link(:text, 'Print').href + end + + def check_footnote + assert_match /This site is running on Instiki/, ie.text + assert_equal 'http://instiki.org/', ie.link(:text, 'Instiki').href + assert_match /Powered by Ruby on Rails/, ie.text + assert_equal 'http://rubyonrails.com/', ie.link(:text, 'Ruby on Rails').href + end + + def date_pattern + '(January|February|March|April|May|June|July|August|September|October|November|December) ' + + '\d\d?, \d\d\d\d \d\d:\d\d:\d\d' + end + + def enter_markup(page, content, author = nil) + ie.goto url(:show, page) + if ie.url == url(:show, page) + ie.link(:name, 'edit').click + assert_equal url(:edit, page), ie.url + else + assert_equal url(:new, page), ie.url + end + + ie.text_field(:name, 'content').set(content) + ie.text_field(:name, 'author').set(author || '') + ie.button(:value, 'Submit').click + + assert_equal url(:show, page), ie.url + end + + def setup_web + assert_equal 'Wiki', ie.text_field(:name, 'web_name').value + assert_equal 'wiki', ie.text_field(:name, 'web_address').value + assert_equal '', ie.text_field(:name, 'password').value + assert_equal '', ie.text_field(:name, 'password_check').value + + ie.text_field(:name, 'password').set('123') + ie.text_field(:name, 'password_check').set('123') + ie.button(:value, 'Setup').click + assert_equal url(:new, 'HomePage'), ie.url + end + + def setup_home_page + ie.text_field(:name, 'content').set('Homepage of a test wiki') + ie.button(:value, 'Submit').click + assert_equal url(:show, 'HomePage'), ie.url + end + + def url(operation, page_name = nil, revision = nil) + page_name = CGI.escape(page_name) if page_name + case operation + when :edit, :new, :show, :print, :revision, :rollback + "#{HOME}/wiki/#{operation}/#{page_name}" + (revision ? "?rev=#{revision}" : '') + when :rss_with_content, :rss_with_headlines + "#{HOME}/wiki/#{operation}" + else + raise "Unsupported operation: '#{operation}" + end + end + +end + +class InstikiController + + attr_reader :process_id + + def self.start + startup_info = [68].pack('lx64') + process_info = [0, 0, 0, 0].pack('llll') + + clear_database + startup_command = + "ruby #{RAILS_ROOT}/instiki.rb --port #{INSTIKI_PORT} --environment development" + + result = Win32API.new('kernel32.dll', 'CreateProcess', 'pplllllppp', 'L').call( + nil, + startup_command, + 0, 0, 1, 0, 0, '.', startup_info, process_info) + + # TODO print the error code, or better yet a text message + raise "Failed to start Instiki." if result == 0 + + process_id = process_info.unpack('llll')[2] + return self.new(process_id) + end + + def self.clear_database + ENV['RAILS_ENV'] = 'development' + require INSTIKI_ROOT + '/config/environment.rb' + [WikiReference, Revision, Page, WikiFile, Web, System].each { |entity| entity.delete_all } + end + + def initialize(pid) + @process_id = pid + end + + def stop + right_to_terminate_process = 1 + handle = Win32API.new('kernel32.dll', 'OpenProcess', 'lil', 'l').call( + right_to_terminate_process, 0, @process_id) + Win32API.new('kernel32.dll', 'TerminateProcess', 'll', 'L').call(handle, 0) + end + +end + +begin + require 'test/unit/ui/console/testrunner' + Test::Unit::UI::Console::TestRunner.new(E2EInstikiTest.suite).start +rescue => e + $stderr.puts 'Unhandled error during test execution' + $stderr.puts e.message + $stderr.puts e.backtrace +ensure + begin + E2EInstikiTest::shutdown + rescue => e + $stderr.puts 'Error during shutdown' + $stderr.puts e.message + $stderr.puts e.backtrace + end +end diff --git a/vendor/plugins/dnsbl_check/README b/vendor/plugins/dnsbl_check/README new file mode 100644 index 00000000..dcbfb8d7 --- /dev/null +++ b/vendor/plugins/dnsbl_check/README @@ -0,0 +1,35 @@ +This plugin checks if the client is listed in RBLs (Real-time Blackhole Lists). +These are lists of IP addresses misbehaving. There are many RBLs, some are more +aggressive than others. More information at http://en.wikipedia.org/wiki/DNSBL + +This filter will result in one DNS request for every blocklist that you have +configured. This might be problematic for sites under heavy load, although this +plugin has been used on high-traffic sites without any problem. One DNS +request takes a few miliseconds to complete, after all. + + +INSTALLATION + +1. Download dnsbl_check-(version).tar.gz. You agree to the license. +2. Go to your application's 'vendor/plugins' directory +3. Untar (un-winzip) the above file: tar xvfz dnsbl_check.tar.gz +4. Restart your application. + + +VERSION HISTORY + +0.1 18 June 2006 Initial release +0.2 10 June 2006 Renamed to dnsbl_check, bugfix +0.3 20 June 2006 Removed sorbs from distribution, was not supposed to be included (too aggressive) +0.4 18 July 2006 Explicit return false added, moved to a per-controller basis (not global anymore) +1.0 16 August 2006 Renamed 0.4 to 1.0. I have been using the plugin very succesfully for months now. +1.1 17 October 2006 Multithreaded version +1.2 23 October 2006 Using the native Ruby resolver library for better multithreaded support +1.2.1 25 October 2006 Accepts a wider range of dns responses +1.2.2 11 December 2006 dnsbls are seemingly under attack, added code to cope with failing service + + +MORE INFORMATION + +http://spacebabies.nl/dnsbl_check/ +joost@spacebabies.nl diff --git a/vendor/plugins/dnsbl_check/init.rb b/vendor/plugins/dnsbl_check/init.rb new file mode 100644 index 00000000..19da77fd --- /dev/null +++ b/vendor/plugins/dnsbl_check/init.rb @@ -0,0 +1 @@ +ActionController::Base.send :include, DNSBL_Check diff --git a/vendor/plugins/dnsbl_check/lib/dnsbl_check.rb b/vendor/plugins/dnsbl_check/lib/dnsbl_check.rb new file mode 100644 index 00000000..b891aa8b --- /dev/null +++ b/vendor/plugins/dnsbl_check/lib/dnsbl_check.rb @@ -0,0 +1,58 @@ +# This plugin checks if the client is listed in DNSBLs (DNS Blackhole Lists). +# These are lists of IP addresses misbehaving. There are many DNSBLs, some are more +# aggressive than others. More information at http://en.wikipedia.org/wiki/DNSBL +# +# This plugin will perform one DNS request per client per blocklist. +# This plugin will deny service to clients those blocklists have listed. +# Whether any of this is acceptable is up to you. +# +# mailto:joost@spacebabies.nl +# License: MIT License, like Rails. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Version 1.2 +# http://www.spacebabies.nl/dnsbl_check +require 'resolv' + +module DNSBL_Check + $dnsbl_passed ||= [] + DNSBLS = %w{list.dsbl.org bl.spamcop.net sbl-xbl.spamhaus.org} + + private + # Filter to check if the client is listed. This will be run before all requests. + def dnsbl_check + return true if $dnsbl_passed.include? request.remote_addr + + passed = true + threads = [] + request.remote_addr =~ /(\d+).(\d+).(\d+).(\d+)/ + + # Check the remote address against each dnsbl in a separate thread + DNSBLS.each do |dnsbl| + threads << Thread.new("#$4.#$3.#$2.#$1.#{dnsbl}") do |host| + logger.warn("Checking DNSBL #{host}") + addr = Resolv.getaddress("#{host}") rescue '' + if addr[0,7]=="127.0.0" + logger.info("#{request.remote_addr} found using DNSBL #{host}") + passed = false + end + end + end + threads.each {|thread| thread.join(2)} # join threads, but use timeout to kill blocked ones + + # Add client ip to global passed cache if no dnsbls objected. else deny service. + if passed + $dnsbl_passed = $dnsbl_passed[0,49].unshift request.remote_addr + logger.warn("#{request.remote_addr} added to DNSBL passed cache") + else + render :text => 'Access denied', :status => 403 + return false + end + end +end diff --git a/vendor/plugins/rubyzip-0.9.1/ChangeLog b/vendor/plugins/rubyzip-0.9.1/ChangeLog new file mode 100644 index 00000000..1e281443 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/ChangeLog @@ -0,0 +1,1081 @@ +2006-07-01 10:04 thomas + + * Rakefile: Don't autorequire zip/zip - autorequire is deprecated. + +2006-06-30 09:28 thomas + + * Rakefile: [no log message] + + * NEWS, lib/zip/zip.rb: Bumped version number and reformatted NEWS + a bit. + +2006-06-29 22:49 technorama + + * lib/zip/zip.rb, NEWS: documentation additions + +2006-04-30 06:25 technorama + + * TODO, lib/zip/zip.rb, test/ziptest.rb: add documentation and test + for new ZipFile::extract + + * lib/zip/zip.rb: add some of the API suggestions from sf.net + #1281314 + + * lib/zip/zip.rb: apply patch for bug #1446926 + + * lib/zip/zip.rb: apply patch for bug #1459902 + +2006-04-26 17:17 technorama + + * lib/zip/zip.rb: add ZipFile @restore_*, documentation update + +2006-04-07 21:13 technorama + + * test/: gentestfiles.rb, zipfilesystemtest.rb, ziptest.rb: + additional tests + +2006-03-28 04:11 technorama + + * lib/zip/zip.rb: start of unix_uid, unix_gid, restore_* support + + * lib/zip/zip.rb: follow_symlinks is now optional + + * lib/zip/zip.rb: add eof? methods + + * test/ziptest.rb: eof? tests + +2006-02-26 09:57 technorama + + * README: add to authors + + * TODO: [no log message] + +2006-02-25 12:04 thomas + + * lib/zip/zip.rb, test/ziptest.rb: Did away with ZipStreamableFile. + +2006-02-23 08:03 technorama + + * lib/zip/zip.rb: unix file permissions. symlink support. rework + ZipEntry and delegate classes. reduce memory usage during + decompression. + +2006-02-22 23:44 technorama + + * lib/zip/zipfilesystem.rb: update permissionInt for mkdir + +2006-02-04 10:42 thomas + + * lib/zip/: ioextras.rb, zip.rb: Merged patch from oss-ruby. + +2005-11-19 16:17 thomas + + * lib/zip/zip.rb: [no log message] + +2005-11-08 08:23 thomas + + * lib/zip/ioextras.rb: Accepted patch from oss-ruby + +2005-10-07 09:54 thomas + + * TODO: [no log message] + +2005-09-06 21:19 thomas + + * lib/zip/zip.rb: [no log message] + + * NEWS: [no log message] + + * lib/zip/zip.rb, test/gentestfiles.rb, test/ziptest.rb: Fixed + problem on windows - tempfile has to be set to binmode again when + it is reopened + +2005-09-04 16:45 thomas + + * Rakefile: [no log message] + + * TODO: [no log message] + + * test/ziptest.rb: [no log message] + +2005-09-03 10:27 thomas + + * NEWS: [no log message] + + * TODO, lib/zip/zip.rb: [no log message] + + * lib/zip/ioextras.rb, lib/zip/zip.rb, test/ziptest.rb: Merged + patch from oss-ruby at technorama.net + + * test/ziptest.rb: Added failing test that shows that read and gets + don't mix currently + +2005-08-29 08:50 thomas + + * lib/zip/: ioextras.rb, zip.rb: [no log message] + + * NEWS, lib/zip/zip.rb: [no log message] + + * lib/zip/zip.rb: [no log message] + + * lib/zip/zip.rb: [no log message] + +2005-08-07 14:27 thomas + + * lib/zip/zip.rb, NEWS: [no log message] + +2005-08-06 11:12 thomas + + * lib/zip/: ioextras.rb, zip.rb: [no log message] + +2005-08-03 18:54 thomas + + * lib/zip/zip.rb: Read/write in chunks to preserve memory + +2005-07-02 15:08 thomas + + * lib/zip/zip.rb: Applied received patch concerning FreeBSD 4.5 + issue + +2005-04-03 16:52 thomas + + * samples/.cvsignore: [no log message] + + * samples/: qtzip.rb, zipdialogui.ui: Added a qt example + +2005-03-31 21:58 thomas + + * lib/zip/zip.rb, test/ziptest.rb: [no log message] + + * test/zipfilesystemtest.rb: [no log message] + +2005-03-17 18:17 thomas + + * Rakefile: [no log message] + + * NEWS, README, lib/zip/zip.rb: [no log message] + + * install.rb: Fixed install.rb + +2005-03-03 18:38 thomas + + * Rakefile: [no log message] + +2005-02-27 16:23 thomas + + * lib/zip/ziprequire.rb: Added documentation to ziprequire + + * README, TODO, lib/zip/ziprequire.rb: Added documentation to + ziprequire + + * Rakefile, test/ziptest.rb: [no log message] + +2005-02-19 21:30 thomas + + * lib/zip/ioextras.rb, lib/zip/stdrubyext.rb, + lib/zip/tempfile_bugfixed.rb, lib/zip/zip.rb, + lib/zip/ziprequire.rb, test/ioextrastest.rb, + test/stdrubyexttest.rb, test/zipfilesystemtest.rb, + test/ziprequiretest.rb, test/ziptest.rb: Added more rdoc and + changed the remaining tests to Test::Unit + + * lib/zip/: ioextras.rb, zip.rb: Added documentation to + ZipInputStream and ZipOutputStream + +2005-02-18 10:27 thomas + + * README: [no log message] + +2005-02-17 23:21 thomas + + * README, Rakefile: Added ppackage (publish package) task to + Rakefile + + * README, Rakefile, TODO: Added pdoc (publish doc) task to Rakefile + + * README, Rakefile, TODO, lib/zip/stdrubyext.rb, lib/zip/zip.rb, + lib/zip/zipfilesystem.rb: Added a bunch of documentation + + * test/ziptest.rb: [no log message] + +2005-02-16 20:04 thomas + + * NEWS, README, Rakefile: Improved documentation and added rdoc + task to Rakefile + + * NEWS, Rakefile, lib/zip/zip.rb: [no log message] + + * Rakefile, samples/example.rb, samples/example_filesystem.rb, + samples/gtkRubyzip.rb, samples/write_simple.rb, + samples/zipfind.rb, test/.cvsignore, test/gentestfiles.rb: + Improvements to Rakefile + +2005-02-15 23:35 thomas + + * NEWS, TODO: [no log message] + + * Rakefile, rubyzip.gemspec: Now uses Rake to build gem + + * Rakefile: [no log message] + + * lib/zip/zip.rb, test/.cvsignore, test/ziptest.rb, NEWS: Fixed + compatibility issue with ruby 1.8.2. Migrated test suite to + Test::Unit + + * NEWS, lib/zip/ioextras.rb, lib/zip/stdrubyext.rb, + lib/zip/tempfile_bugfixed.rb, lib/zip/zip.rb, + lib/zip/zipfilesystem.rb, lib/zip/ziprequire.rb, test/.cvsignore, + test/file1.txt, test/file1.txt.deflatedData, test/file2.txt, + test/gentestfiles.rb, test/ioextrastest.rb, + test/notzippedruby.rb, test/rubycode.zip, test/rubycode2.zip, + test/stdrubyexttest.rb, test/testDirectory.bin, + test/zipWithDirs.zip, test/zipfilesystemtest.rb, + test/ziprequiretest.rb, test/ziptest.rb, test/data/.cvsignore, + test/data/file1.txt, test/data/file1.txt.deflatedData, + test/data/file2.txt, test/data/notzippedruby.rb, + test/data/rubycode.zip, test/data/rubycode2.zip, + test/data/testDirectory.bin, test/data/zipWithDirs.zip: Changed + directory structure + +2005-02-13 22:44 thomas + + * Rakefile, TODO: [no log message] + + * rubyzip.gemspec: [no log message] + + * install.rb: Made install.rb independent of the current path + (fixes bug reported by Drew Robinson) + +2004-12-12 11:22 thomas + + * NEWS, TODO, samples/write_simple.rb: Fixed 'version needed to + extract'-field wrong in local headers + +2004-05-02 15:17 thomas + + * rubyzip.gemspec: Added gemspec contributed by Chad Fowler + +2004-04-02 07:25 thomas + + * NEWS: Fix for FreeBSD 4.9 + +2004-03-29 00:28 thomas + + * NEWS: [no log message] + +2004-03-28 17:59 thomas + + * NEWS: [no log message] + +2004-03-27 16:09 thomas + + * test/stdrubyexttest.rb: Patch for stdrubyext.rb from Nobu Nakada + + * test/: ioextrastest.rb, stdrubyexttest.rb: converted some files + to unix line-endings + +2004-03-25 16:34 thomas + + * NEWS, install.rb: Significantly reduced memory footprint when + modifying zip files + +2004-03-16 18:20 thomas + + * install.rb, test/alltests.rb, test/ioextrastest.rb, + test/stdrubyexttest.rb, test/ziptest.rb: IO utility classes moved + to new file ioextras.rb. Tests moved to new file ioextrastest.rb + +2004-02-27 13:21 thomas + + * NEWS: Optimization to avoid decompression and recompression + +2004-01-30 16:17 thomas + + * NEWS: [no log message] + + * README, test/zipfilesystemtest.rb, test/ziptest.rb: Applied + extra-field patch + +2003-12-13 16:57 thomas + + * TODO: [no log message] + +2003-12-10 00:25 thomas + + * test/ziptest.rb: (Temporary) fix to bug reported by Takashi Sano + +2003-08-23 09:42 thomas + + * test/ziptest.rb, NEWS: Fixed ZipFile.get_ouput_stream bug - data + was never written to zip + +2003-08-21 16:05 thomas + + * install.rb: [no log message] + + * alltests.rb, stdrubyexttest.rb, zipfilesystemtest.rb, + ziprequiretest.rb, ziptest.rb, test/alltests.rb, + test/stdrubyexttest.rb, test/zipfilesystemtest.rb, + test/ziprequiretest.rb, test/ziptest.rb: Moved all test ruby + files to test/ + + * NEWS, install.rb, stdrubyext.rb, stdrubyexttest.rb, zip.rb, + zipfilesystem.rb, zipfilesystemtest.rb, ziprequire.rb, + ziprequiretest.rb, ziptest.rb, samples/example.rb, + samples/example_filesystem.rb, samples/gtkRubyzip.rb, + samples/zipfind.rb: Moved all production source files to zip/ so + they are in the same dir as when they are installed + + * NEWS, TODO, alltests.rb: [no log message] + + * filearchive.rb, filearchivetest.rb, fileutils.rb: Removed + filearchive.rb, filearchivetest.rb and fileutils.rb + + * samples/.cvsignore, samples/example_filesystem.rb, zip.rb, + samples/example_filesystem.rb: Added + samples/example_filesystem.rb. Fixed Tempfile creation for + entries created with get_output_stream where entries were in a + subdirectory + + * zip.rb, ziptest.rb: Fixed mkdir bug. ZipFile.mkdir didn't work if + the zipfile doesn't exist already + + * ziptest.rb: [no log message] + + * TODO, zipfilesystemtest.rb: Globbing test placeholder commented + out + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented ZipFsDir.new + and open + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented DirFsIterator + and tests + +2003-08-20 22:50 thomas + + * NEWS, TODO: [no log message] + + * zipfilesystemtest.rb: [no log message] + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsDir.foreach, ZipFsDir.entries now reimplemented in terms of + it + + * README: [no log message] + + * zipfilesystem.rb, zipfilesystemtest.rb: [no log message] + + * zipfilesystem.rb: All access from ZipFsFile and ZipFsDir to + ZipFile is now routed through ZipFileNameMapper which has the + single responsibility of mapping entry/filenames + + * alltests.rb, stdrubyext.rb, stdrubyexttest.rb: Added + stdrubyexttest.rb and added test test_select_map + + * zipfilesystem.rb: ZipFsDir was in the wrong module. ZipFileSystem + now has a ctor that creates ZipFsDir and ZipFsFile instances, + instead of creating them lazily. It then passes the dir instance + to the file instance and vice versa + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: ZipFsFile.open + honours chdir + + * stdrubyext.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, + ziptest.rb: Fixed ZipEntry::parent_as_string. Implemented + ZipFsDir.chdir, pwd and entries including test + +2003-08-19 15:44 thomas + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsDir.mkdir + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsDir.delete (and aliases rmdir and unlink) + + * zipfilesystem.rb, zipfilesystemtest.rb: Another dummy + implementation and commented out a test for select() which can be + added later + +2003-08-18 20:40 thomas + + * ziptest.rb: Honoured 1.8.0 Object.to_a deprecation warning + + * zip.rb, ziptest.rb, samples/example.rb, samples/zipfind.rb: + Converted a few more names to ruby underscore style that I missed + with the automated processing the first time around + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, ziptest.rb: + Implemented Zip::ZipFile.get_output_stream + +2003-08-17 18:28 thomas + + * README, install.rb, stdrubyext.rb, zipfilesystem.rb, + zipfilesystemtest.rb: Updated README with Documentation section. + Updated install.rb. Fixed three tests that failed on 1.8.0. + +2003-08-14 05:40 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Added empty + implementations of atime and ctime + +2003-08-13 17:08 thomas + + * simpledist.rb: Moved simpledist to a separate repository called + 'misc' + + * NEWS: [no log message] + + * stdrubyext.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, + ziprequire.rb, ziprequiretest.rb, ziptest.rb, samples/example.rb, + samples/gtkRubyzip.rb, samples/zipfind.rb: Changed all method + names to the ruby convention underscore style + + * alltests.rb, zipfilesystem.rb, zipfilesystemtest.rb: Implemented + a lot more of the stat methods. Mostly with dummy implementations + that return values that indicate that these features aren't + supported + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented more methods + and tests in zipfilesystem. Mostly empty methods as permissions + and file types other than files and directories are not supported + + * install.rb, stdrubyext.rb, zip.rb, zipfilesystem.rb, + zipfilesystemtest.rb: Addd file stdrubyext.rb and moved the + modifications to std ruby classes to it. Refactored the ZipFsStat + tests and ZipFsStat. Added Module.forwardMessages and used it to + implement the forwarding of calls in ZipFsStat + + * zipfilesystem.rb, zipfilesystemtest.rb: Added + Zip::ZipFsFile::ZipFsStat and started implementing it and its + methods + + * zipfilesystem.rb, zipfilesystemtest.rb, ziptest.rb: Updated and + added missing copyright notices + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: zipfilesystem.rb + is becoming big and not everyone will want to use that code. + Therefore zip.rb no longer requires it. Instead you must require + zipfilesystem.rb itself if you want to use it + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented dummy + permission test methods + + * TODO, zip.rb, ziptest.rb: Merged from patch from Kristoffer + Lunden. Fixed more 1.8.0 incompatibilites - tests run on 1.8.0 + now + +2003-08-12 19:18 thomas + + * zip.rb: Get rid of 1.8.0 warning + + * ziptest.rb: ruby 1.8.0 compatibility fix + + * NEWS, zip.rb: ruby-zlib 0.6.0 compatibility fix + +2002-12-22 20:12 thomas + + * zip.rb: [no log message] + +2002-09-16 22:11 thomas + + * NEWS: [no log message] + +2002-09-15 17:16 thomas + + * samples/zipfind.rb: [no log message] + + * samples/zipfind.rb: [no log message] + +2002-09-14 22:59 thomas + + * samples/zipfind.rb: Added simple zipfind script + +2002-09-13 23:53 thomas + + * TODO: Added TODO about openmode for zip entries binary/ascii + + * NEWS: ziptest now runs without errors with ruby-1.7.2-4 (Andy's + latest build) + + * zip.rb, ziprequiretest.rb, ziptest.rb: ziptest now runs without + errors with ruby-1.7.2-4 (Andy's latest build) + +2002-09-12 00:20 thomas + + * zipfilesystemtest.rb: Improved ZipFsFile.delete/unlink test + + * test/.cvsignore: [no log message] + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.delete/unlink + +2002-09-11 22:22 thomas + + * alltests.rb: [no log message] + + * NEWS, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: Fixed + AbstractInputStream.each_line ignored its aSeparator argument. + Implemented more ZipFsFile methods + + * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: ZipFileSystem is + now a module instead of a class, and is mixed into ZipFile, + instead of being made available as a property fileSystem + +2002-09-10 23:45 thomas + + * NEWS: Updated NEWS file + + * zip.rb: [no log message] + + * NEWS, zip.rb, ziptest.rb: Fix bug: rewind should reset lineno. + Fix bug: Deflater.read uses separate buffer from produceInput + (feeding gets/readline etc) + +2002-09-09 23:48 thomas + + * .cvsignore: [no log message] + +2002-09-09 22:55 uid26649 + + * zip.rb, ziptest.rb: Implemented ZipInputStream.rewind and + AbstractInputStream.lineno. Tests for both + +2002-09-09 20:31 thomas + + * zip.rb, ziptest.rb: ZipInputStream and ZipOutstream (thru their + AbstractInputStream and AbstractOutputStream now lie about being + kind_of?(IO) + +2002-09-08 16:38 thomas + + * zipfilesystemtest.rb: [no log message] + + * filearchive.rb, filearchivetest.rb, zip.rb, ziptest.rb: Moved + String additions from filearchive.rb to zip.rb (and moved tests + along too to ziptest.rb). Added ZipEntry.parentAsString and + ZipEntrySet.parent + + * ziptest.rb: Implemented ZipEntrySetTest.testDup and testCompound + + * TODO, zip.rb, ziptest.rb: Replaced Array with EntrySet for + keeping entries in a zip file. Tagged repository before this + commit, so this change can be rolled back, if it stinks + +2002-09-07 20:21 thomas + + * zip.rb, ziptest.rb: Implemented ZipEntry.<=> + + * ziptest.rb: Removed unused code + +2002-08-11 15:14 thomas + + * zip.rb, ziptest.rb: Made some changes to accomodate ruby 1.7.2 + +2002-07-27 15:25 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented ZipFsFile.new + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.pipe + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.link + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.symlink + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.readlink, wrapped ZipFileSystem class in Zip module + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.zero? + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented test for + ZipFsFile.directory? + +2002-07-26 23:56 thomas + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.socket? + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.join + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.ftype + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.blockdev? + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.size? (slightly different from size) + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.split + + * zipfilesystem.rb, zipfilesystemtest.rb: Implemented + ZipFsFile.symlink? + + * alltests.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: + Implemented ZipFsFile.mtime + + * zipfilesystem.rb, zipfilesystemtest.rb: Implement ZipFsFile.file? + + * zip.rb, ziptest.rb: Implemented ZipEntry.file? + + * alltests.rb, filearchive.rb, filearchivetest.rb, zip.rb, + zipfilesystem.rb, zipfilesystemtest.rb, ziprequire.rb, + ziptest.rb: Implemented ZipFileSystem::ZipFsFile.size + + * zipfilesystem.rb, zipfilesystemtest.rb: [no log message] + + * test/zipWithDirs.zip: Changed zipWithDirs.zip so all the entries + in it have unix file endings + + * alltests.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: + Started implementing ZipFileSystem + + * test/zipWithDirs.zip: Added a zip file for testing with a + directory structure + +2002-07-22 21:40 thomas + + * TODO: [no log message] + + * TODO: [no log message] + +2002-07-21 18:20 thomas + + * NEWS: [no log message] + + * TODO: Updated TODO with a refactoring idea for FileArchive + + * filearchive.rb, filearchivetest.rb: Added some FileArchiveAdd + tests and cleaned up some of the FileArchive tests. extract and + add now have individual test fixtures. + + * filearchive.rb, filearchivetest.rb: Added tests for extract + called with regex src arg and Enumerable src arg + + * filearchivetest.rb: Added test for continueOnExistsProc when + extracting from a file archive + +2002-07-20 17:13 thomas + + * TODO, filearchivetest.rb, fileutils.rb, ziptest.rb, + test/.cvsignore: Added (failing) tests for FileArchive.add, added + code for creating test files for FileArchive.add tests. Added + fileutils.rb, which is borrowed from ruby 1.7.2 + + * filearchive.rb, filearchivetest.rb: [no log message] + + * filearchivetest.rb: Added tests for String extensions + + * alltests.rb, ziprequiretest.rb, ziptest.rb: [no log message] + + * install.rb: [no log message] + + * TODO: Updated TODO + + * filearchive.rb, filearchivetest.rb: All FileArchive.extract tests + run + +2002-07-19 23:11 thomas + + * filearchive.rb, filearchivetest.rb: [no log message] + + * filearchivetest.rb: [no log message] + + * filearchive.rb, filearchivetest.rb: [no log message] + + * filearchive.rb, filearchivetest.rb, zip.rb: [no log message] + +2002-07-08 13:41 thomas + + * TODO: [no log message] + +2002-06-11 19:47 thomas + + * filearchive.rb, filearchivetest.rb, zip.rb, ziptest.rb: [no log + message] + +2002-05-25 00:41 thomas + + * simpledist.rb: Added hackish script for creating dist files + +2002-04-30 21:22 thomas + + * TODO: [no log message] + + * filearchive.rb, filearchivetest.rb: [no log message] + + * filearchive.rb, filearchivetest.rb: Improved testing and wrote + some of the skeleton of extract. Still to do: Fix glob, so it + returns a hashmap instead of a list. The map will need to map the + full entry name to the last part of the name (which is only + really interesting for recursively extracted entries, otherwise + it is just the name). Glob.expandPathList should also output + directories with a trailing slash, which is doesn't right now. + + * filearchive.rb, filearchivetest.rb: Implemented the first few + tests for FileArchive + +2002-04-24 22:06 thomas + + * ziprequire.rb, ziprequiretest.rb: Appended copyright message to + ziprequire.rb and ziprequiretest.rb + + * zip.rb: Made ZipEntry tolerate invalid dates + +2002-04-21 00:57 thomas + + * NEWS, TODO, zip.rb, ziptest.rb: Read and write entry modification + date/time correctly + +2002-04-20 02:44 thomas + + * ziprequiretest.rb, test/rubycode2.zip: improved ZipRequireTest + + * ziprequire.rb: Made a warning go away + + * ziprequire.rb, ziprequiretest.rb, test/notzippedruby.rb, + test/rubycode.zip: Fixed a bug in ziprequire. Added + ziprequiretest.rb and test data files + +2002-04-19 22:43 thomas + + * zip.rb, ziptest.rb: Added recursion support to Glob module + +2002-04-18 21:37 thomas + + * NEWS, TODO, zip.rb, ziptest.rb: Added Glob module and GlobTest + unit test suite. This module provides the functionality to expand + a 'glob pattern' given a list of files - Next step is to use this + module in ZipFile + +2002-04-01 22:55 thomas + + * NEWS: [no log message] + + * TODO, zip.rb, ziprequire.rb: Added ziprequire.rb which contains a + proof-of-concept implementation of a require implementation that + can load ruby modules from a zip file. Needs unit tests and + polish. + +2002-03-31 01:13 thomas + + * README: [no log message] + +2002-03-30 16:14 thomas + + * TODO: [no log message] + + * .cvsignore, README, zip.rb: Added rdoc markup (only #:nodoc:all + modifiers) to zip.rb. Made README 'RDoc compliant' + +2002-03-29 23:29 thomas + + * TODO: [no log message] + + * example.rb, samples/.cvsignore, samples/example.rb, + samples/gtkRubyzip.rb: Moved example.rb to samples/. Added + another sample gtkRubyzip.rb + + * NEWS, TODO, TODO: [no log message] + + * .cvsignore, file1.txt, file1.txt.deflatedData, testDirectory.bin, + ziptest.rb, test/.cvsignore, test/file1.txt, + test/file1.txt.deflatedData, test/file2.txt, + test/testDirectory.bin: Added test/ directory and moved the + manually created test data files into it. Changed ziptest.rb so + it runs in test/ directory + + * TODO: [no log message] + + * NEWS, zip.rb, ziptest.rb: Don't decompress and recompress zip + entries when changing zip file + + * zip.rb: Performance optimization: Only write new ZipFile, if it + has been changed. The test suite runs in half the time now. + +2002-03-28 22:12 thomas + + * TODO: [no log message] + +2002-03-23 17:31 thomas + + * TODO: [no log message] + +2002-03-22 22:47 thomas + + * NEWS: [no log message] + + * NEWS, TODO: [no log message] + + * ziptest.rb: Found the tests that didn't use blocks to make sure + input streams are closed as soon as they arent used anymore and + got rid of the GC.start + + * ziptest.rb: All tests run on windows ruby 1.6.6 + + * zip.rb, ziptest.rb: Windows fixes: Fixed ZipFile.initialize which + needed to open zipfile file in binary mode. Added another + workaround for the return value from File.open(name) where name + is the name of a directory - ruby returns different exceptions in + linux, win/cygwin and windows. A number of tests failed because + in windows you cant delete a file that is open. Fixed by changing + ziptest.rb to use ZipInputStream.getInputStream with blocks a few + places. There is a hack in CommanZipFileFixture.setup where the + GC is explicitly invoked. Should be fixed with blocks instead. + The only currently failing test fails because the test data + creation fails to add a comment to 4entry.zip, because echo eats + the remainder of the line including the pipe character and the + following zip -z 4 entry.zip command + +2002-03-21 22:18 thomas + + * NEWS: [no log message] + + * NEWS, README, TODO, install.rb: Added install.rb + + * ziptest.rb: [no log message] + + * NEWS, TODO: [no log message] + + * .cvsignore, TODO, zip.rb, ziptest.rb: Added + test_extractDirectoryExistsAsFileOverwrite and fixed to pass + + * zip.rb, ziptest.rb: Extraction of directory entries is now + supported + +2002-03-20 21:59 thomas + + * NEWS: [no log message] + + * COPYING, README, README.txt: Removed COPYING, renamed README.txt + to README. Updated README + + * example.rb: Fixed example.rb added example that shows zip file + manipulation with Zip::ZipFile + + * .cvsignore: [no log message] + + * TODO, zip.rb, ziptest.rb: Directories can now be added (not + recursively, the directory entry itself. Directories are + recognized by a empty entries with a trailing /. The purpose of + storing them explicitly in the zip file is to be able to store + permission and ownership information + + * TODO, zip.rb, ziptest.rb: zip.rb depended on ftools but it was + only included in ziptest.rb + + * zip.rb, ziptest.rb: ZipError is now a subclass of StandardError + instead of RuntimeError. ZipError now has several subclasses. + +2002-03-19 22:26 thomas + + * TODO: [no log message] + + * TODO, ziptest.rb: Unit test ZipFile.getInputStream with block + + * TODO, zip.rb, ziptest.rb: Unit test for adding new entry with + name that already exists in archive, and fixed to pass test + + * TODO, zip.rb, ziptest.rb: Added unit tests for rename to existing + entry + + * TODO: [no log message] + + * TODO, zip.rb, ziptest.rb: Unit test calling ZipFile.extract with + block + +2002-03-18 21:06 thomas + + * TODO: [no log message] + + * zip.rb, ziptest.rb: ZipFile#commit now reinitializes ZipFile. + + * TODO, zip.rb, ziptest.rb: Refactoring: + + Collapsed ZipEntry and ZipStreamableZipEntry into ZipEntry. + + Collapsed BasicZipFile and ZipFile into ZipFile. + + * zip.rb: Removed method that was never called + +2002-03-17 22:33 thomas + + * TODO: [no log message] + + * ziptest.rb: Run tests with =true as default + + * NEWS, TODO, zip.rb, ziptest.rb: Now runs with -w switch without + warnings + + * .cvsignore: [no log message] + + * zip.rb, ziptest.rb: Down to one failing test + + * zip.rb, ziptest.rb: [no log message] + + * TODO, zip.rb, ziptest.rb: [no log message] + +2002-02-25 19:42 thomas + + * TODO: Added more todos + +2002-02-23 15:51 thomas + + * zip.rb: [no log message] + + * zip.rb, ziptest.rb: [no log message] + + * zip.rb, ziptest.rb: [no log message] + +2002-02-03 18:47 thomas + + * ziptest.rb: [no log message] + +2002-02-02 15:58 thomas + + * example.rb, zip.rb, ziptest.rb: [no log message] + + * .cvsignore: [no log message] + + * example.rb, zip.rb, ziptest.rb: Renamed SimpleZipFile to + BasicZipFile + + * TODO: [no log message] + + * ziptest.rb: More test cases - all of them failing, so now there + are 18 failing test cases. Three more test cases to implement, + then it is time for the production code + +2002-02-01 21:49 thomas + + * ziptest.rb: [no log message] + + * ziptest.rb: Also run SimpleZipFile tests for ZipFile. + + * example.rb, zip.rb, ziptest.rb: ZipFile renamed to SimpleZipFile. + The new ZipFile will have many more methods that are useful for + managing archives. + +2002-01-29 20:30 thomas + + * TODO: [no log message] + +2002-01-26 00:18 thomas + + * NEWS: [no log message] + + * ziptest.rb: In unit test: work around ruby/cygwin weirdness. You + get an Errno::EEXISTS instead of an Errno::EISDIR if you try to + open a file for writing that is a directory. + + * ziptest.rb: Fixed test that failed on windows because of CRLF + line ending + +2002-01-25 23:58 thomas + + * ziptest.rb: [no log message] + + * .cvsignore, example.rb, zip.rb: Fixed bug reading from empty + deflated entry in zip file + + * .cvsignore: [no log message] + + * ziptest.rb: [no log message] + + * NEWS, README.txt, zip.rb, ziptest.rb: Zip write support is now + fully functional in the form of ZipOutputStream. + + * zip.rb, ziptest.rb: [no log message] + + * zip.rb, ziptest.rb: [no log message] + +2002-01-20 16:00 thomas + + * zip.rb, ziptest.rb: Added Deflater and DeflaterTest. + + * .cvsignore: [no log message] + + * .cvsignore: Added .cvsignore file + + * zip.rb, ziptest.rb: Added ZipEntry.writeCDirEntry and misc minor + fixes + +2002-01-19 23:28 thomas + + * example.rb, zip.rb, ziptest.rb: NOTICE: Not all tests run!! + + ZipOutputStream in progress + + Wrapped rubyzip in namespace module Zip. + +2002-01-17 18:52 thomas + + * ziptest.rb: Fail nicely if the user doesn't have info-zip + compatible zip in the path + +2002-01-10 18:02 thomas + + * zip.rb: Adjusted chunk size to 32k after a few perf measurements + +2002-01-09 22:10 thomas + + * README.txt: License now same as rubys, not just GPL + +2002-01-06 00:19 thomas + + * README.txt: [no log message] + +2002-01-05 23:09 thomas + + * NEWS, README.txt, NEWS: Updated NEWS file + + * README.txt, zip.rb, ziptest.rb, zlib.c.diff: Added tests for + decompressors and a tests for ZipLocalEntry, + ZipCentralDirectoryEntry and ZipCentralDirectory for handling of + corrupt data + + * file1.txt.deflatedData: deflated data extracted from a zip file. + contains file1.txt + + * zip.rb: Changed references to Inflate to Zlib::inflate for + compatibility with ruby-zlib-0.5 + + * README.txt, zip.rb, ziptest.rb: [no log message] + + * example.rb, NEWS: [no log message] + + * COPYING, README.txt: [no log message] + + * ziptest.rb: Fixed problem with test file creation + + * README.txt: Updated README.txt + + * zip.rb, ziptest.rb: ZipFile now works + +2002-01-04 21:51 thomas + + * testDirectory.bin, zip.rb, ziptest.rb: + ZipCentralDirectoryEntryTest now runs + + * ziptest.rb: Changed + ZIpLocalNEtryTest::test_ReadLocalEntryHeaderOfFirstTestZipEntry + so it works on both unix too. It only worked on windows because + the test made assumptions about the compressed size and crc of an + entry, but that differs depending on the OS because of the CRLF + thing. + + * README.txt: Added note about zlib.c patch + +2002-01-02 18:48 thomas + + * README.txt, example.rb, file1.txt, zip.rb, ziptest.rb, + zlib.c.diff: initial + + * README.txt, example.rb, file1.txt, zip.rb, ziptest.rb, + zlib.c.diff: Initial revision + diff --git a/vendor/plugins/rubyzip-0.9.1/NEWS b/vendor/plugins/rubyzip-0.9.1/NEWS new file mode 100644 index 00000000..0f46f727 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/NEWS @@ -0,0 +1,144 @@ += Version 0.9.1 + +Added symlink support and support for unix file permissions. Reduced +memory usage during decompression. + +New methods ZipFile::[follow_symlinks, restore_times, restore_permissions, restore_ownership]. +New methods ZipEntry::unix_perms, ZipInputStream::eof?. +Added documentation and test for new ZipFile::extract. +Added some of the API suggestions from sf.net #1281314. +Applied patch for sf.net bug #1446926. +Applied patch for sf.net bug #1459902. +Rework ZipEntry and delegate classes. + += Version 0.5.12 + +Fixed problem with writing binary content to a ZipFile in MS Windows. + += Version 0.5.11 + +Fixed name clash file method copy_stream from fileutils.rb. Fixed +problem with references to constant CHUNK_SIZE. +ZipInputStream/AbstractInputStream read is now buffered like ruby IO's +read method, which means that read and gets etc can be mixed. The +unbuffered read method has been renamed to sysread. + += Version 0.5.10 + +Fixed method name resolution problem with FileUtils::copy_stream and +IOExtras::copy_stream. + += Version 0.5.9 + +Fixed serious memory consumption issue + += Version 0.5.8 + +Fixed install script. + += Version 0.5.7 + +install.rb no longer assumes it is being run from the toplevel source +dir. Directory structure changed to reflect common ruby library +project structure. Migrated from RubyUnit to Test::Unit format. Now +uses Rake to build source packages and gems and run unit tests. + += Version 0.5.6 + +Fix for FreeBSD 4.9 which returns Errno::EFBIG instead of +Errno::EINVAL for some invalid seeks. Fixed 'version needed to +extract'-field incorrect in local headers. + += Version 0.5.5 + +Fix for a problem with writing zip files that concerns only ruby 1.8.1. + += Version 0.5.4 + +Significantly reduced memory footprint when modifying zip files. + += Version 0.5.3 + +Added optimization to avoid decompressing and recompressing individual +entries when modifying a zip archive. + += Version 0.5.2 + +Fixed ZipFile corruption bug in ZipFile class. Added basic unix +extra-field support. + += Version 0.5.1 + +Fixed ZipFile.get_output_stream bug. + += Version 0.5.0 + +List of changes: +* Ruby 1.8.0 and ruby-zlib 0.6.0 compatibility +* Changed method names from camelCase to rubys underscore style. +* Installs to zip/ subdir instead of directly to site_ruby +* Added ZipFile.directory and ZipFile.file - each method return an +object that can be used like Dir and File only for the contents of the +zip file. +* Added sample application zipfind which works like Find.find, only +Zip::ZipFind.find traverses into zip archives too. + +Bug fixes: +* AbstractInputStream.each_line with non-default separator + + += Version 0.5.0a + +Source reorganized. Added ziprequire, which can be used to load ruby +modules from a zip file, in a fashion similar to jar files in +Java. Added gtkRubyzip, another sample application. Implemented +ZipInputStream.lineno and ZipInputStream.rewind + +Bug fixes: + +* Read and write date and time information correctly for zip entries. +* Fixed read() using separate buffer, causing mix of gets/readline/read to +cause problems. + += Version 0.4.2 + +Performance optimizations. Test suite runs in half the time. + += Version 0.4.1 + +Windows compatibility fixes. + += Version 0.4.0 + +Zip::ZipFile is now mutable and provides a more convenient way of +modifying zip archives than Zip::ZipOutputStream. Operations for +adding, extracting, renaming, replacing and removing entries to zip +archives are now available. + +Runs without warnings with -w switch. + +Install script install.rb added. + + += Version 0.3.1 + +Rudimentary support for writing zip archives. + + += Version 0.2.2 + +Fixed and extended unit test suite. Updated to work with ruby/zlib +0.5. It doesn't work with earlier versions of ruby/zlib. + + += Version 0.2.0 + +Class ZipFile added. Where ZipInputStream is used to read the +individual entries in a zip file, ZipFile reads the central directory +in the zip archive, so you can get to any entry in the zip archive +without having to skipping through all the preceeding entries. + + += Version 0.1.0 + +First working version of ZipInputStream. diff --git a/vendor/plugins/rubyzip-0.9.1/README b/vendor/plugins/rubyzip-0.9.1/README new file mode 100644 index 00000000..81ec4c5d --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/README @@ -0,0 +1,72 @@ += rubyzip + +rubyzip is a ruby library for reading and writing zip files. + += Install + +If you have rubygems you can install rubyzip directly from the gem +repository + + gem install rubyzip + +Otherwise obtain the source (see below) and run + + ruby install.rb + +To run the unit tests you need to have test::unit installed + + rake test + + += Documentation + +There is more than one way to access or create a zip archive with +rubyzip. The basic API is modeled after the classes in +java.util.zip from the Java SDK. This means there are classes such +as Zip::ZipInputStream, Zip::ZipOutputStream and +Zip::ZipFile. Zip::ZipInputStream provides a basic interface for +iterating through the entries in a zip archive and reading from the +entries in the same way as from a regular File or IO +object. ZipOutputStream is the corresponding basic output +facility. Zip::ZipFile provides a mean for accessing the archives +central directory and provides means for accessing any entry without +having to iterate through the archive. Unlike Java's +java.util.zip.ZipFile rubyzip's Zip::ZipFile is mutable, which means +it can be used to change zip files as well. + +Another way to access a zip archive with rubyzip is to use rubyzip's +Zip::ZipFileSystem API. Using this API files can be read from and +written to the archive in much the same manner as ruby's builtin +classes allows files to be read from and written to the file system. + +rubyzip also features the +zip/ziprequire.rb[link:files/lib/zip/ziprequire_rb.html] module which +allows ruby to load ruby modules from zip archives. + +For details about the specific behaviour of classes and methods refer +to the test suite. Finally you can generate the rdoc documentation or +visit http://rubyzip.sourceforge.net. + += License + +rubyzip is distributed under the same license as ruby. See +http://www.ruby-lang.org/en/LICENSE.txt + + += Website and Project Home + +http://rubyzip.sourceforge.net + +http://sourceforge.net/projects/rubyzip + +== Download (tarballs and gems) + +http://sourceforge.net/project/showfiles.php?group_id=43107&package_id=35377 + += Authors + +Thomas Sondergaard (thomas at sondergaard.cc) + +Technorama Ltd. (oss-ruby-zip at technorama.net) + +extra-field support contributed by Tatsuki Sugiura (sugi at nemui.org) diff --git a/vendor/plugins/rubyzip-0.9.1/Rakefile b/vendor/plugins/rubyzip-0.9.1/Rakefile new file mode 100755 index 00000000..c6581cf6 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/Rakefile @@ -0,0 +1,110 @@ +# Rakefile for RubyGems -*- ruby -*- + +require 'rubygems' +require 'rake/clean' +require 'rake/testtask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/rdoctask' +require 'rake/contrib/sshpublisher' +require 'net/ftp' + +PKG_NAME = 'rubyzip' +PKG_VERSION = File.read('lib/zip/zip.rb').match(/\s+VERSION\s*=\s*'(.*)'/)[1] + +PKG_FILES = FileList.new + +PKG_FILES.add %w{ README NEWS TODO ChangeLog install.rb Rakefile } +PKG_FILES.add %w{ samples/*.rb } +PKG_FILES.add %w{ test/*.rb } +PKG_FILES.add %w{ test/data/* } +PKG_FILES.exclude "test/data/generated" +PKG_FILES.add %w{ lib/**/*.rb } + +def clobberFromCvsIgnore(path) + CLOBBER.add File.readlines(path+'/.cvsignore').map { + |f| File.join(path, f.chomp) + } rescue StandardError +end + +clobberFromCvsIgnore '.' +clobberFromCvsIgnore 'samples' +clobberFromCvsIgnore 'test' +clobberFromCvsIgnore 'test/data' + +task :default => [:test] + +desc "Run unit tests" +task :test do + ruby %{-C test alltests.rb} +end + +# Shortcuts for test targets +task :ut => [:test] + +spec = Gem::Specification.new do |s| + s.name = PKG_NAME + s.version = PKG_VERSION + s.author = "Thomas Sondergaard" + s.email = "thomas(at)sondergaard.cc" + s.homepage = "http://rubyzip.sourceforge.net/" + s.platform = Gem::Platform::RUBY + s.summary = "rubyzip is a ruby module for reading and writing zip files" + s.files = PKG_FILES.to_a + s.require_path = 'lib' +end + +Rake::GemPackageTask.new(spec) do |pkg| + pkg.need_zip = true + pkg.need_tar = true +end + +Rake::RDocTask.new do |rd| + rd.main = "README" + rd.rdoc_files.add %W{ lib/zip/*.rb README NEWS TODO ChangeLog } + rd.options << "--title 'rubyzip documentation' --webcvs http://cvs.sourceforge.net/viewcvs.py/rubyzip/rubyzip/" +# rd.options << "--all" +end + +desc "Publish documentation" +task :pdoc => [:rdoc] do + Rake::SshFreshDirPublisher. + new("thomas@rubyzip.sourceforge.net", "/home/groups/r/ru/rubyzip/htdocs", "html").upload +end + +desc "Publish package" +task :ppackage => [:package] do + Net::FTP.open("upload.sourceforge.net", + "ftp", + ENV['USER']+"@"+ENV['HOSTNAME']) { + |ftpclient| + ftpclient.passive = true + ftpclient.chdir "incoming" + Dir['pkg/*.{tgz,zip,gem}'].each { + |e| + ftpclient.putbinaryfile(e, File.basename(e)) + } + } +end + +desc "Generate the ChangeLog file" +task :ChangeLog do + puts "Updating ChangeLog" + system %{cvs2cl} +end + +desc "Make a release" +task :release => [:tag_release, :pdoc, :ppackage] do +end + +desc "Make a release tag" +task :tag_release do + tag = "release-#{PKG_VERSION.gsub('.','-')}" + + puts "Checking for tag '#{tag}'" + if (Regexp.new("^\\s+#{tag}") =~ `cvs log README`) + abort "Tag '#{tag}' already exists" + end + puts "Tagging module with '#{tag}'" + system("cvs tag #{tag}") +end diff --git a/vendor/plugins/rubyzip-0.9.1/TODO b/vendor/plugins/rubyzip-0.9.1/TODO new file mode 100644 index 00000000..e24cde57 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/TODO @@ -0,0 +1,16 @@ + +* ZipInputStream: Support zip-files with trailing data descriptors +* Adjust rdoc stylesheet to advertise inherited methods if possible +* Suggestion: Add ZipFile/ZipInputStream example that demonstrates extracting all entries. +* Suggestion: ZipFile#extract destination should default to "." +* Suggestion: ZipEntry should have extract(), get_input_stream() methods etc +* SUggestion: ZipInputStream/ZipOutputStream should accept an IO object in addition to a filename. +* (is buffering used anywhere with write?) +* Inflater.sysread should pass the buffer to produce_input. +* Implement ZipFsDir.glob +* ZipFile.checkIntegrity method +* non-MSDOS permission attributes +** See mail from Ned Konz to ruby-talk subj. "Re: SV: [ANN] Archive 0.2" +* Packager version, required unpacker version in zip headers +** See mail from Ned Konz to ruby-talk subj. "Re: SV: [ANN] Archive 0.2" +* implement storing attributes and ownership information diff --git a/vendor/plugins/rubyzip-0.9.1/install.rb b/vendor/plugins/rubyzip-0.9.1/install.rb new file mode 100755 index 00000000..405e2b0b --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/install.rb @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +files = %w{ stdrubyext.rb ioextras.rb zip.rb zipfilesystem.rb ziprequire.rb tempfile_bugfixed.rb } + +INSTALL_DIR = File.join(CONFIG["sitelibdir"], "zip") +File.makedirs(INSTALL_DIR) + +SOURCE_DIR = File.join(File.dirname($0), "lib/zip") + +files.each { + |filename| + installPath = File.join(INSTALL_DIR, filename) + File::install(File.join(SOURCE_DIR, filename), installPath, 0644, true) +} diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/ioextras.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/ioextras.rb new file mode 100755 index 00000000..c458bb58 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/ioextras.rb @@ -0,0 +1,155 @@ +module IOExtras #:nodoc: + + CHUNK_SIZE = 32768 + + RANGE_ALL = 0..-1 + + def self.copy_stream(ostream, istream) + s = '' + ostream.write(istream.read(CHUNK_SIZE, s)) until istream.eof? + end + + + # Implements kind_of? in order to pretend to be an IO object + module FakeIO + def kind_of?(object) + object == IO || super + end + end + + # Implements many of the convenience methods of IO + # such as gets, getc, readline and readlines + # depends on: input_finished?, produce_input and read + module AbstractInputStream + include Enumerable + include FakeIO + + def initialize + super + @lineno = 0 + @outputBuffer = "" + end + + attr_accessor :lineno + + def read(numberOfBytes = nil, buf = nil) + tbuf = nil + + if @outputBuffer.length > 0 + if numberOfBytes <= @outputBuffer.length + tbuf = @outputBuffer.slice!(0, numberOfBytes) + else + numberOfBytes -= @outputBuffer.length if (numberOfBytes) + rbuf = sysread(numberOfBytes, buf) + tbuf = @outputBuffer + tbuf << rbuf if (rbuf) + @outputBuffer = "" + end + else + tbuf = sysread(numberOfBytes, buf) + end + + return nil unless (tbuf) + + if buf + buf.replace(tbuf) + else + buf = tbuf + end + + buf + end + + def readlines(aSepString = $/) + retVal = [] + each_line(aSepString) { |line| retVal << line } + return retVal + end + + def gets(aSepString=$/) + @lineno = @lineno.next + return read if aSepString == nil + aSepString="#{$/}#{$/}" if aSepString == "" + + bufferIndex=0 + while ((matchIndex = @outputBuffer.index(aSepString, bufferIndex)) == nil) + bufferIndex=@outputBuffer.length + if input_finished? + return @outputBuffer.empty? ? nil : flush + end + @outputBuffer << produce_input + end + sepIndex=matchIndex + aSepString.length + return @outputBuffer.slice!(0...sepIndex) + end + + def flush + retVal=@outputBuffer + @outputBuffer="" + return retVal + end + + def readline(aSepString = $/) + retVal = gets(aSepString) + raise EOFError if retVal == nil + return retVal + end + + def each_line(aSepString = $/) + while true + yield readline(aSepString) + end + rescue EOFError + end + + alias_method :each, :each_line + end + + + # Implements many of the output convenience methods of IO. + # relies on << + module AbstractOutputStream + include FakeIO + + def write(data) + self << data + data.to_s.length + end + + + def print(*params) + self << params.to_s << $\.to_s + end + + def printf(aFormatString, *params) + self << sprintf(aFormatString, *params) + end + + def putc(anObject) + self << case anObject + when Fixnum then anObject.chr + when String then anObject + else raise TypeError, "putc: Only Fixnum and String supported" + end + anObject + end + + def puts(*params) + params << "\n" if params.empty? + params.flatten.each { + |element| + val = element.to_s + self << val + self << "\n" unless val[-1,1] == "\n" + } + end + + end + +end # IOExtras namespace module + + + +# Copyright (C) 2002-2004 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/stdrubyext.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/stdrubyext.rb new file mode 100755 index 00000000..833365db --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/stdrubyext.rb @@ -0,0 +1,111 @@ +unless Enumerable.method_defined?(:inject) + module Enumerable #:nodoc:all + def inject(n = 0) + each { |value| n = yield(n, value) } + n + end + end +end + +module Enumerable #:nodoc:all + # returns a new array of all the return values not equal to nil + # This implementation could be faster + def select_map(&aProc) + map(&aProc).reject { |e| e.nil? } + end +end + +unless Object.method_defined?(:object_id) + class Object #:nodoc:all + # Using object_id which is the new thing, so we need + # to make that work in versions prior to 1.8.0 + alias object_id id + end +end + +unless File.respond_to?(:read) + class File # :nodoc:all + # singleton method read does not exist in 1.6.x + def self.read(fileName) + open(fileName) { |f| f.read } + end + end +end + +class String #:nodoc:all + def starts_with(aString) + rindex(aString, 0) == 0 + end + + def ends_with(aString) + index(aString, -aString.size) + end + + def ensure_end(aString) + ends_with(aString) ? self : self + aString + end + + def lchop + slice(1, length) + end +end + +class Time #:nodoc:all + + #MS-DOS File Date and Time format as used in Interrupt 21H Function 57H: + # + # Register CX, the Time: + # Bits 0-4 2 second increments (0-29) + # Bits 5-10 minutes (0-59) + # bits 11-15 hours (0-24) + # + # Register DX, the Date: + # Bits 0-4 day (1-31) + # bits 5-8 month (1-12) + # bits 9-15 year (four digit year minus 1980) + + + def to_binary_dos_time + (sec/2) + + (min << 5) + + (hour << 11) + end + + def to_binary_dos_date + (day) + + (month << 5) + + ((year - 1980) << 9) + end + + # Dos time is only stored with two seconds accuracy + def dos_equals(other) + to_i/2 == other.to_i/2 + end + + def self.parse_binary_dos_format(binaryDosDate, binaryDosTime) + second = 2 * ( 0b11111 & binaryDosTime) + minute = ( 0b11111100000 & binaryDosTime) >> 5 + hour = (0b1111100000000000 & binaryDosTime) >> 11 + day = ( 0b11111 & binaryDosDate) + month = ( 0b111100000 & binaryDosDate) >> 5 + year = ((0b1111111000000000 & binaryDosDate) >> 9) + 1980 + begin + return Time.local(year, month, day, hour, minute, second) + end + end +end + +class Module #:nodoc:all + def forward_message(forwarder, *messagesToForward) + methodDefs = messagesToForward.map { + |msg| + "def #{msg}; #{forwarder}(:#{msg}); end" + } + module_eval(methodDefs.join("\n")) + end +end + + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/tempfile_bugfixed.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/tempfile_bugfixed.rb new file mode 100755 index 00000000..6cf8e0af --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/tempfile_bugfixed.rb @@ -0,0 +1,195 @@ +# +# tempfile - manipulates temporary files +# +# $Id: tempfile_bugfixed.rb,v 1.2 2005/02/19 20:30:33 thomas Exp $ +# + +require 'delegate' +require 'tmpdir' + +module BugFix #:nodoc:all + +# A class for managing temporary files. This library is written to be +# thread safe. +class Tempfile < DelegateClass(File) + MAX_TRY = 10 + @@cleanlist = [] + + # Creates a temporary file of mode 0600 in the temporary directory + # whose name is basename.pid.n and opens with mode "w+". A Tempfile + # object works just like a File object. + # + # If tmpdir is omitted, the temporary directory is determined by + # Dir::tmpdir provided by 'tmpdir.rb'. + # When $SAFE > 0 and the given tmpdir is tainted, it uses + # /tmp. (Note that ENV values are tainted by default) + def initialize(basename, tmpdir=Dir::tmpdir) + if $SAFE > 0 and tmpdir.tainted? + tmpdir = '/tmp' + end + + lock = nil + n = failure = 0 + + begin + Thread.critical = true + + begin + tmpname = sprintf('%s/%s%d.%d', tmpdir, basename, $$, n) + lock = tmpname + '.lock' + n += 1 + end while @@cleanlist.include?(tmpname) or + File.exist?(lock) or File.exist?(tmpname) + + Dir.mkdir(lock) + rescue + failure += 1 + retry if failure < MAX_TRY + raise "cannot generate tempfile `%s'" % tmpname + ensure + Thread.critical = false + end + + @data = [tmpname] + @clean_proc = Tempfile.callback(@data) + ObjectSpace.define_finalizer(self, @clean_proc) + + @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600) + @tmpname = tmpname + @@cleanlist << @tmpname + @data[1] = @tmpfile + @data[2] = @@cleanlist + + super(@tmpfile) + + # Now we have all the File/IO methods defined, you must not + # carelessly put bare puts(), etc. after this. + + Dir.rmdir(lock) + end + + # Opens or reopens the file with mode "r+". + def open + @tmpfile.close if @tmpfile + @tmpfile = File.open(@tmpname, 'r+') + @data[1] = @tmpfile + __setobj__(@tmpfile) + end + + def _close # :nodoc: + @tmpfile.close if @tmpfile + @data[1] = @tmpfile = nil + end + protected :_close + + # Closes the file. If the optional flag is true, unlinks the file + # after closing. + # + # If you don't explicitly unlink the temporary file, the removal + # will be delayed until the object is finalized. + def close(unlink_now=false) + if unlink_now + close! + else + _close + end + end + + # Closes and unlinks the file. + def close! + _close + @clean_proc.call + ObjectSpace.undefine_finalizer(self) + end + + # Unlinks the file. On UNIX-like systems, it is often a good idea + # to unlink a temporary file immediately after creating and opening + # it, because it leaves other programs zero chance to access the + # file. + def unlink + # keep this order for thread safeness + File.unlink(@tmpname) if File.exist?(@tmpname) + @@cleanlist.delete(@tmpname) if @@cleanlist + end + alias delete unlink + + if RUBY_VERSION > '1.8.0' + def __setobj__(obj) + @_dc_obj = obj + end + else + def __setobj__(obj) + @obj = obj + end + end + + # Returns the full path name of the temporary file. + def path + @tmpname + end + + # Returns the size of the temporary file. As a side effect, the IO + # buffer is flushed before determining the size. + def size + if @tmpfile + @tmpfile.flush + @tmpfile.stat.size + else + 0 + end + end + alias length size + + class << self + def callback(data) # :nodoc: + pid = $$ + lambda{ + if pid == $$ + path, tmpfile, cleanlist = *data + + print "removing ", path, "..." if $DEBUG + + tmpfile.close if tmpfile + + # keep this order for thread safeness + File.unlink(path) if File.exist?(path) + cleanlist.delete(path) if cleanlist + + print "done\n" if $DEBUG + end + } + end + + # If no block is given, this is a synonym for new(). + # + # If a block is given, it will be passed tempfile as an argument, + # and the tempfile will automatically be closed when the block + # terminates. In this case, open() returns nil. + def open(*args) + tempfile = new(*args) + + if block_given? + begin + yield(tempfile) + ensure + tempfile.close + end + + nil + else + tempfile + end + end + end +end + +end # module BugFix +if __FILE__ == $0 +# $DEBUG = true + f = Tempfile.new("foo") + f.print("foo\n") + f.close + f.open + p f.gets # => "foo\n" + f.close! +end diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/zip.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/zip.rb new file mode 100755 index 00000000..19d90f51 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/zip.rb @@ -0,0 +1,1847 @@ +require 'delegate' +require 'singleton' +require 'tempfile' +require 'ftools' +require 'stringio' +require 'zlib' +require 'zip/stdrubyext' +require 'zip/ioextras' + +if Tempfile.superclass == SimpleDelegator + require 'zip/tempfile_bugfixed' + Tempfile = BugFix::Tempfile +end + +module Zlib #:nodoc:all + if ! const_defined? :MAX_WBITS + MAX_WBITS = Zlib::Deflate.MAX_WBITS + end +end + +module Zip + + VERSION = '0.9.1' + + RUBY_MINOR_VERSION = RUBY_VERSION.split(".")[1].to_i + + RUNNING_ON_WINDOWS = /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM + + # Ruby 1.7.x compatibility + # In ruby 1.6.x and 1.8.0 reading from an empty stream returns + # an empty string the first time and then nil. + # not so in 1.7.x + EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST = RUBY_MINOR_VERSION != 7 + + # ZipInputStream is the basic class for reading zip entries in a + # zip file. It is possible to create a ZipInputStream object directly, + # passing the zip file name to the constructor, but more often than not + # the ZipInputStream will be obtained from a ZipFile (perhaps using the + # ZipFileSystem interface) object for a particular entry in the zip + # archive. + # + # A ZipInputStream inherits IOExtras::AbstractInputStream in order + # to provide an IO-like interface for reading from a single zip + # entry. Beyond methods for mimicking an IO-object it contains + # the method get_next_entry for iterating through the entries of + # an archive. get_next_entry returns a ZipEntry object that describes + # the zip entry the ZipInputStream is currently reading from. + # + # Example that creates a zip archive with ZipOutputStream and reads it + # back again with a ZipInputStream. + # + # require 'zip/zip' + # + # Zip::ZipOutputStream::open("my.zip") { + # |io| + # + # io.put_next_entry("first_entry.txt") + # io.write "Hello world!" + # + # io.put_next_entry("adir/first_entry.txt") + # io.write "Hello again!" + # } + # + # + # Zip::ZipInputStream::open("my.zip") { + # |io| + # + # while (entry = io.get_next_entry) + # puts "Contents of #{entry.name}: '#{io.read}'" + # end + # } + # + # java.util.zip.ZipInputStream is the original inspiration for this + # class. + + class ZipInputStream + include IOExtras::AbstractInputStream + + # Opens the indicated zip file. An exception is thrown + # if the specified offset in the specified filename is + # not a local zip entry header. + def initialize(filename, offset = 0) + super() + @archiveIO = File.open(filename, "rb") + @archiveIO.seek(offset, IO::SEEK_SET) + @decompressor = NullDecompressor.instance + @currentEntry = nil + end + + def close + @archiveIO.close + end + + # Same as #initialize but if a block is passed the opened + # stream is passed to the block and closed when the block + # returns. + def ZipInputStream.open(filename) + return new(filename) unless block_given? + + zio = new(filename) + yield zio + ensure + zio.close if zio + end + + # Returns a ZipEntry object. It is necessary to call this + # method on a newly created ZipInputStream before reading from + # the first entry in the archive. Returns nil when there are + # no more entries. + + def get_next_entry + @archiveIO.seek(@currentEntry.next_header_offset, + IO::SEEK_SET) if @currentEntry + open_entry + end + + # Rewinds the stream to the beginning of the current entry + def rewind + return if @currentEntry.nil? + @lineno = 0 + @archiveIO.seek(@currentEntry.localHeaderOffset, + IO::SEEK_SET) + open_entry + end + + # Modeled after IO.sysread + def sysread(numberOfBytes = nil, buf = nil) + @decompressor.sysread(numberOfBytes, buf) + end + + def eof + @outputBuffer.empty? && @decompressor.eof + end + alias :eof? :eof + + protected + + def open_entry + @currentEntry = ZipEntry.read_local_entry(@archiveIO) + if (@currentEntry == nil) + @decompressor = NullDecompressor.instance + elsif @currentEntry.compression_method == ZipEntry::STORED + @decompressor = PassThruDecompressor.new(@archiveIO, + @currentEntry.size) + elsif @currentEntry.compression_method == ZipEntry::DEFLATED + @decompressor = Inflater.new(@archiveIO) + else + raise ZipCompressionMethodError, + "Unsupported compression method #{@currentEntry.compression_method}" + end + flush + return @currentEntry + end + + def produce_input + @decompressor.produce_input + end + + def input_finished? + @decompressor.input_finished? + end + end + + + + class Decompressor #:nodoc:all + CHUNK_SIZE=32768 + def initialize(inputStream) + super() + @inputStream=inputStream + end + end + + class Inflater < Decompressor #:nodoc:all + def initialize(inputStream) + super + @zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) + @outputBuffer="" + @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST + end + + def sysread(numberOfBytes = nil, buf = nil) + readEverything = (numberOfBytes == nil) + while (readEverything || @outputBuffer.length < numberOfBytes) + break if internal_input_finished? + @outputBuffer << internal_produce_input(buf) + end + return value_when_finished if @outputBuffer.length==0 && input_finished? + endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes + return @outputBuffer.slice!(0...endIndex) + end + + def produce_input + if (@outputBuffer.empty?) + return internal_produce_input + else + return @outputBuffer.slice!(0...(@outputBuffer.length)) + end + end + + # to be used with produce_input, not read (as read may still have more data cached) + # is data cached anywhere other than @outputBuffer? the comment above may be wrong + def input_finished? + @outputBuffer.empty? && internal_input_finished? + end + alias :eof :input_finished? + alias :eof? :input_finished? + + private + + def internal_produce_input(buf = nil) + retried = 0 + begin + @zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE, buf)) + rescue Zlib::BufError + raise if (retried >= 5) # how many times should we retry? + retried += 1 + retry + end + end + + def internal_input_finished? + @zlibInflater.finished? + end + + # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ? + def value_when_finished # mimic behaviour of ruby File object. + return nil if @hasReturnedEmptyString + @hasReturnedEmptyString=true + return "" + end + end + + class PassThruDecompressor < Decompressor #:nodoc:all + def initialize(inputStream, charsToRead) + super inputStream + @charsToRead = charsToRead + @readSoFar = 0 + @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST + end + + # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ? + def sysread(numberOfBytes = nil, buf = nil) + if input_finished? + hasReturnedEmptyStringVal=@hasReturnedEmptyString + @hasReturnedEmptyString=true + return "" unless hasReturnedEmptyStringVal + return nil + end + + if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead) + numberOfBytes = @charsToRead-@readSoFar + end + @readSoFar += numberOfBytes + @inputStream.read(numberOfBytes, buf) + end + + def produce_input + sysread(Decompressor::CHUNK_SIZE) + end + + def input_finished? + (@readSoFar >= @charsToRead) + end + alias :eof :input_finished? + alias :eof? :input_finished? + end + + class NullDecompressor #:nodoc:all + include Singleton + def sysread(numberOfBytes = nil, buf = nil) + nil + end + + def produce_input + nil + end + + def input_finished? + true + end + + def eof + true + end + alias :eof? :eof + end + + class NullInputStream < NullDecompressor #:nodoc:all + include IOExtras::AbstractInputStream + end + + class ZipEntry + STORED = 0 + DEFLATED = 8 + + FSTYPE_FAT = 0 + FSTYPE_AMIGA = 1 + FSTYPE_VMS = 2 + FSTYPE_UNIX = 3 + FSTYPE_VM_CMS = 4 + FSTYPE_ATARI = 5 + FSTYPE_HPFS = 6 + FSTYPE_MAC = 7 + FSTYPE_Z_SYSTEM = 8 + FSTYPE_CPM = 9 + FSTYPE_TOPS20 = 10 + FSTYPE_NTFS = 11 + FSTYPE_QDOS = 12 + FSTYPE_ACORN = 13 + FSTYPE_VFAT = 14 + FSTYPE_MVS = 15 + FSTYPE_BEOS = 16 + FSTYPE_TANDEM = 17 + FSTYPE_THEOS = 18 + FSTYPE_MAC_OSX = 19 + FSTYPE_ATHEOS = 30 + + FSTYPES = { + FSTYPE_FAT => 'FAT'.freeze, + FSTYPE_AMIGA => 'Amiga'.freeze, + FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze, + FSTYPE_UNIX => 'Unix'.freeze, + FSTYPE_VM_CMS => 'VM/CMS'.freeze, + FSTYPE_ATARI => 'Atari ST'.freeze, + FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze, + FSTYPE_MAC => 'Macintosh'.freeze, + FSTYPE_Z_SYSTEM => 'Z-System'.freeze, + FSTYPE_CPM => 'CP/M'.freeze, + FSTYPE_TOPS20 => 'TOPS-20'.freeze, + FSTYPE_NTFS => 'NTFS'.freeze, + FSTYPE_QDOS => 'SMS/QDOS'.freeze, + FSTYPE_ACORN => 'Acorn RISC OS'.freeze, + FSTYPE_VFAT => 'Win32 VFAT'.freeze, + FSTYPE_MVS => 'MVS'.freeze, + FSTYPE_BEOS => 'BeOS'.freeze, + FSTYPE_TANDEM => 'Tandem NSK'.freeze, + FSTYPE_THEOS => 'Theos'.freeze, + FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze, + FSTYPE_ATHEOS => 'AtheOS'.freeze, + }.freeze + + attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method, + :name, :size, :localHeaderOffset, :zipfile, :fstype, :externalFileAttributes, :gp_flags, :header_signature + + attr_accessor :follow_symlinks + attr_accessor :restore_times, :restore_permissions, :restore_ownership + attr_accessor :unix_uid, :unix_gid, :unix_perms + + attr_reader :ftype, :filepath # :nodoc: + + def initialize(zipfile = "", name = "", comment = "", extra = "", + compressed_size = 0, crc = 0, + compression_method = ZipEntry::DEFLATED, size = 0, + time = Time.now) + super() + if name.starts_with("/") + raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /" + end + @localHeaderOffset = 0 + @internalFileAttributes = 1 + @externalFileAttributes = 0 + @version = 52 # this library's version + @ftype = nil # unspecified or unknown + @filepath = nil + if Zip::RUNNING_ON_WINDOWS + @fstype = FSTYPE_FAT + else + @fstype = FSTYPE_UNIX + end + @zipfile, @comment, @compressed_size, @crc, @extra, @compression_method, + @name, @size = zipfile, comment, compressed_size, crc, + extra, compression_method, name, size + @time = time + + @follow_symlinks = false + + @restore_times = true + @restore_permissions = false + @restore_ownership = false + +# BUG: need an extra field to support uid/gid's + @unix_uid = nil + @unix_gid = nil + @unix_perms = nil +# @posix_acl = nil +# @ntfs_acl = nil + + if name_is_directory? + @ftype = :directory + else + @ftype = :file + end + + unless ZipExtraField === @extra + @extra = ZipExtraField.new(@extra.to_s) + end + end + + def time + if @extra["UniversalTime"] + @extra["UniversalTime"].mtime + else + # Atandard time field in central directory has local time + # under archive creator. Then, we can't get timezone. + @time + end + end + alias :mtime :time + + def time=(aTime) + unless @extra.member?("UniversalTime") + @extra.create("UniversalTime") + end + @extra["UniversalTime"].mtime = aTime + @time = aTime + end + + # Returns +true+ if the entry is a directory. + def directory? + raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype + @ftype == :directory + end + alias :is_directory :directory? + + # Returns +true+ if the entry is a file. + def file? + raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype + @ftype == :file + end + + # Returns +true+ if the entry is a symlink. + def symlink? + raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype + @ftype == :link + end + + def name_is_directory? #:nodoc:all + (%r{\/$} =~ @name) != nil + end + + def local_entry_offset #:nodoc:all + localHeaderOffset + local_header_size + end + + def local_header_size #:nodoc:all + LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.local_size : 0) + end + + def cdir_header_size #:nodoc:all + CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + + (@extra ? @extra.c_dir_size : 0) + (@comment ? @comment.size : 0) + end + + def next_header_offset #:nodoc:all + local_entry_offset + self.compressed_size + end + + # Extracts entry to file destPath (defaults to @name). + def extract(destPath = @name, &onExistsProc) + onExistsProc ||= proc { false } + + if directory? + create_directory(destPath, &onExistsProc) + elsif file? + write_file(destPath, &onExistsProc) + elsif symlink? + create_symlink(destPath, &onExistsProc) + else + raise RuntimeError, "unknown file type #{self.inspect}" + end + + self + end + + def to_s + @name + end + + protected + + def ZipEntry.read_zip_short(io) # :nodoc: + io.read(2).unpack('v')[0] + end + + def ZipEntry.read_zip_long(io) # :nodoc: + io.read(4).unpack('V')[0] + end + public + + LOCAL_ENTRY_SIGNATURE = 0x04034b50 + LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30 + LOCAL_ENTRY_TRAILING_DESCRIPTOR_LENGTH = 4+4+4 + + def read_local_entry(io) #:nodoc:all + @localHeaderOffset = io.tell + staticSizedFieldsBuf = io.read(LOCAL_ENTRY_STATIC_HEADER_LENGTH) + unless (staticSizedFieldsBuf.size==LOCAL_ENTRY_STATIC_HEADER_LENGTH) + raise ZipError, "Premature end of file. Not enough data for zip entry local header" + end + + @header_signature , + @version , + @fstype , + @gp_flags , + @compression_method, + lastModTime , + lastModDate , + @crc , + @compressed_size , + @size , + nameLength , + extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv') + + unless (@header_signature == LOCAL_ENTRY_SIGNATURE) + raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'" + end + set_time(lastModDate, lastModTime) + + @name = io.read(nameLength) + extra = io.read(extraLength) + + if (extra && extra.length != extraLength) + raise ZipError, "Truncated local zip entry header" + else + if ZipExtraField === @extra + @extra.merge(extra) + else + @extra = ZipExtraField.new(extra) + end + end + end + + def ZipEntry.read_local_entry(io) + entry = new(io.path) + entry.read_local_entry(io) + return entry + rescue ZipError + return nil + end + + def write_local_entry(io) #:nodoc:all + @localHeaderOffset = io.tell + + io << + [LOCAL_ENTRY_SIGNATURE , + 0 , + 0 , # @gp_flags , + @compression_method , + @time.to_binary_dos_time , # @lastModTime , + @time.to_binary_dos_date , # @lastModDate , + @crc , + @compressed_size , + @size , + @name ? @name.length : 0, + @extra? @extra.local_length : 0 ].pack('VvvvvvVVVvv') + io << @name + io << (@extra ? @extra.to_local_bin : "") + end + + CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50 + CDIR_ENTRY_STATIC_HEADER_LENGTH = 46 + + def read_c_dir_entry(io) #:nodoc:all + staticSizedFieldsBuf = io.read(CDIR_ENTRY_STATIC_HEADER_LENGTH) + unless (staticSizedFieldsBuf.size == CDIR_ENTRY_STATIC_HEADER_LENGTH) + raise ZipError, "Premature end of file. Not enough data for zip cdir entry header" + end + + @header_signature , + @version , # version of encoding software + @fstype , # filesystem type + @versionNeededToExtract, + @gp_flags , + @compression_method , + lastModTime , + lastModDate , + @crc , + @compressed_size , + @size , + nameLength , + extraLength , + commentLength , + diskNumberStart , + @internalFileAttributes, + @externalFileAttributes, + @localHeaderOffset , + @name , + @extra , + @comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV') + + unless (@header_signature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE) + raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'" + end + set_time(lastModDate, lastModTime) + + @name = io.read(nameLength) + if ZipExtraField === @extra + @extra.merge(io.read(extraLength)) + else + @extra = ZipExtraField.new(io.read(extraLength)) + end + @comment = io.read(commentLength) + unless (@comment && @comment.length == commentLength) + raise ZipError, "Truncated cdir zip entry header" + end + + case @fstype + when FSTYPE_UNIX + @unix_perms = (@externalFileAttributes >> 16) & 07777 + + case (@externalFileAttributes >> 28) + when 04 + @ftype = :directory + when 010 + @ftype = :file + when 012 + @ftype = :link + else + raise ZipInternalError, "unknown file type #{'0%o' % (@externalFileAttributes >> 28)}" + end + else + if name_is_directory? + @ftype = :directory + else + @ftype = :file + end + end + end + + def ZipEntry.read_c_dir_entry(io) #:nodoc:all + entry = new(io.path) + entry.read_c_dir_entry(io) + return entry + rescue ZipError + return nil + end + + def file_stat(path) # :nodoc: + if @follow_symlinks + return File::stat(path) + else + return File::lstat(path) + end + end + + def get_extra_attributes_from_path(path) # :nodoc: + unless Zip::RUNNING_ON_WINDOWS + stat = file_stat(path) + @unix_uid = stat.uid + @unix_gid = stat.gid + @unix_perms = stat.mode & 07777 + end + end + + def set_extra_attributes_on_path(destPath) # :nodoc: + return unless (file? or directory?) + + case @fstype + when FSTYPE_UNIX + # BUG: does not update timestamps into account + # ignore setuid/setgid bits by default. honor if @restore_ownership + unix_perms_mask = 01777 + unix_perms_mask = 07777 if (@restore_ownership) + File::chmod(@unix_perms & unix_perms_mask, destPath) if (@restore_permissions && @unix_perms) + File::chown(@unix_uid, @unix_gid, destPath) if (@restore_ownership && @unix_uid && @unix_gid && Process::egid == 0) + # File::utimes() + end + end + + def write_c_dir_entry(io) #:nodoc:all + case @fstype + when FSTYPE_UNIX + ft = nil + case @ftype + when :file + ft = 010 + @unix_perms ||= 0644 + when :directory + ft = 004 + @unix_perms ||= 0755 + when :symlink + ft = 012 + @unix_perms ||= 0755 + else + raise ZipInternalError, "unknown file type #{self.inspect}" + end + + @externalFileAttributes = (ft << 12 | (@unix_perms & 07777)) << 16 + end + + io << + [CENTRAL_DIRECTORY_ENTRY_SIGNATURE, + @version , # version of encoding software + @fstype , # filesystem type + 0 , # @versionNeededToExtract , + 0 , # @gp_flags , + @compression_method , + @time.to_binary_dos_time , # @lastModTime , + @time.to_binary_dos_date , # @lastModDate , + @crc , + @compressed_size , + @size , + @name ? @name.length : 0 , + @extra ? @extra.c_dir_length : 0 , + @comment ? comment.length : 0 , + 0 , # disk number start + @internalFileAttributes , # file type (binary=0, text=1) + @externalFileAttributes , # native filesystem attributes + @localHeaderOffset , + @name , + @extra , + @comment ].pack('VCCvvvvvVVVvvvvvVV') + + io << @name + io << (@extra ? @extra.to_c_dir_bin : "") + io << @comment + end + + def == (other) + return false unless other.class == self.class + # Compares contents of local entry and exposed fields + (@compression_method == other.compression_method && + @crc == other.crc && + @compressed_size == other.compressed_size && + @size == other.size && + @name == other.name && + @extra == other.extra && + @filepath == other.filepath && + self.time.dos_equals(other.time)) + end + + def <=> (other) + return to_s <=> other.to_s + end + + # Returns an IO like object for the given ZipEntry. + # Warning: may behave weird with symlinks. + def get_input_stream(&aProc) + if @ftype == :directory + return yield(NullInputStream.instance) if block_given? + return NullInputStream.instance + elsif @filepath + case @ftype + when :file + return File.open(@filepath, "rb", &aProc) + + when :symlink + linkpath = File::readlink(@filepath) + stringio = StringIO.new(linkpath) + return yield(stringio) if block_given? + return stringio + else + raise "unknown @ftype #{@ftype}" + end + else + zis = ZipInputStream.new(@zipfile, localHeaderOffset) + zis.get_next_entry + if block_given? + begin + return yield(zis) + ensure + zis.close + end + else + return zis + end + end + end + + def gather_fileinfo_from_srcpath(srcPath) # :nodoc: + stat = file_stat(srcPath) + case stat.ftype + when 'file' + if name_is_directory? + raise ArgumentError, + "entry name '#{newEntry}' indicates directory entry, but "+ + "'#{srcPath}' is not a directory" + end + @ftype = :file + when 'directory' + if ! name_is_directory? + @name += "/" + end + @ftype = :directory + when 'link' + if name_is_directory? + raise ArgumentError, + "entry name '#{newEntry}' indicates directory entry, but "+ + "'#{srcPath}' is not a directory" + end + @ftype = :symlink + else + raise RuntimeError, "unknown file type: #{srcPath.inspect} #{stat.inspect}" + end + + @filepath = srcPath + get_extra_attributes_from_path(@filepath) + end + + def write_to_zip_output_stream(aZipOutputStream) #:nodoc:all + if @ftype == :directory + aZipOutputStream.put_next_entry(self) + elsif @filepath + aZipOutputStream.put_next_entry(self) + get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) } + else + aZipOutputStream.copy_raw_entry(self) + end + end + + def parent_as_string + entry_name = name.chomp("/") + slash_index = entry_name.rindex("/") + slash_index ? entry_name.slice(0, slash_index+1) : nil + end + + def get_raw_input_stream(&aProc) + File.open(@zipfile, "rb", &aProc) + end + + private + + def set_time(binaryDosDate, binaryDosTime) + @time = Time.parse_binary_dos_format(binaryDosDate, binaryDosTime) + rescue ArgumentError + puts "Invalid date/time in zip entry" + end + + def write_file(destPath, continueOnExistsProc = proc { false }) + if File.exists?(destPath) && ! yield(self, destPath) + raise ZipDestinationFileExistsError, + "Destination '#{destPath}' already exists" + end + File.open(destPath, "wb") do |os| + get_input_stream do |is| + set_extra_attributes_on_path(destPath) + + buf = '' + while buf = is.sysread(Decompressor::CHUNK_SIZE, buf) + os << buf + end + end + end + end + + def create_directory(destPath) + if File.directory? destPath + return + elsif File.exists? destPath + if block_given? && yield(self, destPath) + File.rm_f destPath + else + raise ZipDestinationFileExistsError, + "Cannot create directory '#{destPath}'. "+ + "A file already exists with that name" + end + end + Dir.mkdir destPath + set_extra_attributes_on_path(destPath) + end + +# BUG: create_symlink() does not use &onExistsProc + def create_symlink(destPath) + stat = nil + begin + stat = File::lstat(destPath) + rescue Errno::ENOENT + end + + io = get_input_stream + linkto = io.read + + if stat + if stat.symlink? + if File::readlink(destPath) == linkto + return + else + raise ZipDestinationFileExistsError, + "Cannot create symlink '#{destPath}'. "+ + "A symlink already exists with that name" + end + else + raise ZipDestinationFileExistsError, + "Cannot create symlink '#{destPath}'. "+ + "A file already exists with that name" + end + end + + File::symlink(linkto, destPath) + end + end + + + # ZipOutputStream is the basic class for writing zip files. It is + # possible to create a ZipOutputStream object directly, passing + # the zip file name to the constructor, but more often than not + # the ZipOutputStream will be obtained from a ZipFile (perhaps using the + # ZipFileSystem interface) object for a particular entry in the zip + # archive. + # + # A ZipOutputStream inherits IOExtras::AbstractOutputStream in order + # to provide an IO-like interface for writing to a single zip + # entry. Beyond methods for mimicking an IO-object it contains + # the method put_next_entry that closes the current entry + # and creates a new. + # + # Please refer to ZipInputStream for example code. + # + # java.util.zip.ZipOutputStream is the original inspiration for this + # class. + + class ZipOutputStream + include IOExtras::AbstractOutputStream + + attr_accessor :comment + + # Opens the indicated zip file. If a file with that name already + # exists it will be overwritten. + def initialize(fileName) + super() + @fileName = fileName + @outputStream = File.new(@fileName, "wb") + @entrySet = ZipEntrySet.new + @compressor = NullCompressor.instance + @closed = false + @currentEntry = nil + @comment = nil + end + + # Same as #initialize but if a block is passed the opened + # stream is passed to the block and closed when the block + # returns. + def ZipOutputStream.open(fileName) + return new(fileName) unless block_given? + zos = new(fileName) + yield zos + ensure + zos.close if zos + end + + # Closes the stream and writes the central directory to the zip file + def close + return if @closed + finalize_current_entry + update_local_headers + write_central_directory + @outputStream.close + @closed = true + end + + # Closes the current entry and opens a new for writing. + # +entry+ can be a ZipEntry object or a string. + def put_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION) + raise ZipError, "zip stream is closed" if @closed + newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@fileName, entry.to_s) + init_next_entry(newEntry, level) + @currentEntry=newEntry + end + + def copy_raw_entry(entry) + entry = entry.dup + raise ZipError, "zip stream is closed" if @closed + raise ZipError, "entry is not a ZipEntry" if !entry.kind_of?(ZipEntry) + finalize_current_entry + @entrySet << entry + src_pos = entry.local_entry_offset + entry.write_local_entry(@outputStream) + @compressor = NullCompressor.instance + @outputStream << entry.get_raw_input_stream { + |is| + is.seek(src_pos, IO::SEEK_SET) + is.read(entry.compressed_size) + } + @compressor = NullCompressor.instance + @currentEntry = nil + end + + private + def finalize_current_entry + return unless @currentEntry + finish + @currentEntry.compressed_size = @outputStream.tell - @currentEntry.localHeaderOffset - + @currentEntry.local_header_size + @currentEntry.size = @compressor.size + @currentEntry.crc = @compressor.crc + @currentEntry = nil + @compressor = NullCompressor.instance + end + + def init_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION) + finalize_current_entry + @entrySet << entry + entry.write_local_entry(@outputStream) + @compressor = get_compressor(entry, level) + end + + def get_compressor(entry, level) + case entry.compression_method + when ZipEntry::DEFLATED then Deflater.new(@outputStream, level) + when ZipEntry::STORED then PassThruCompressor.new(@outputStream) + else raise ZipCompressionMethodError, + "Invalid compression method: '#{entry.compression_method}'" + end + end + + def update_local_headers + pos = @outputStream.tell + @entrySet.each { + |entry| + @outputStream.pos = entry.localHeaderOffset + entry.write_local_entry(@outputStream) + } + @outputStream.pos = pos + end + + def write_central_directory + cdir = ZipCentralDirectory.new(@entrySet, @comment) + cdir.write_to_stream(@outputStream) + end + + protected + + def finish + @compressor.finish + end + + public + # Modeled after IO.<< + def << (data) + @compressor << data + end + end + + + class Compressor #:nodoc:all + def finish + end + end + + class PassThruCompressor < Compressor #:nodoc:all + def initialize(outputStream) + super() + @outputStream = outputStream + @crc = Zlib::crc32 + @size = 0 + end + + def << (data) + val = data.to_s + @crc = Zlib::crc32(val, @crc) + @size += val.size + @outputStream << val + end + + attr_reader :size, :crc + end + + class NullCompressor < Compressor #:nodoc:all + include Singleton + + def << (data) + raise IOError, "closed stream" + end + + attr_reader :size, :compressed_size + end + + class Deflater < Compressor #:nodoc:all + def initialize(outputStream, level = Zlib::DEFAULT_COMPRESSION) + super() + @outputStream = outputStream + @zlibDeflater = Zlib::Deflate.new(level, -Zlib::MAX_WBITS) + @size = 0 + @crc = Zlib::crc32 + end + + def << (data) + val = data.to_s + @crc = Zlib::crc32(val, @crc) + @size += val.size + @outputStream << @zlibDeflater.deflate(data) + end + + def finish + until @zlibDeflater.finished? + @outputStream << @zlibDeflater.finish + end + end + + attr_reader :size, :crc + end + + + class ZipEntrySet #:nodoc:all + include Enumerable + + def initialize(anEnumerable = []) + super() + @entrySet = {} + anEnumerable.each { |o| push(o) } + end + + def include?(entry) + @entrySet.include?(entry.to_s) + end + + def <<(entry) + @entrySet[entry.to_s] = entry + end + alias :push :<< + + def size + @entrySet.size + end + alias :length :size + + def delete(entry) + @entrySet.delete(entry.to_s) ? entry : nil + end + + def each(&aProc) + @entrySet.values.each(&aProc) + end + + def entries + @entrySet.values + end + + # deep clone + def dup + newZipEntrySet = ZipEntrySet.new(@entrySet.values.map { |e| e.dup }) + end + + def == (other) + return false unless other.kind_of?(ZipEntrySet) + return @entrySet == other.entrySet + end + + def parent(entry) + @entrySet[entry.parent_as_string] + end + + def glob(pattern, flags = File::FNM_PATHNAME|File::FNM_DOTMATCH) + entries.select { + |entry| + File.fnmatch(pattern, entry.name.chomp('/'), flags) + } + end + +#TODO attr_accessor :auto_create_directories + protected + attr_accessor :entrySet + end + + + class ZipCentralDirectory + include Enumerable + + END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50 + MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE = 65536 + 18 + STATIC_EOCD_SIZE = 22 + + attr_reader :comment + + # Returns an Enumerable containing the entries. + def entries + @entrySet.entries + end + + def initialize(entries = ZipEntrySet.new, comment = "") #:nodoc: + super() + @entrySet = entries.kind_of?(ZipEntrySet) ? entries : ZipEntrySet.new(entries) + @comment = comment + end + + def write_to_stream(io) #:nodoc: + offset = io.tell + @entrySet.each { |entry| entry.write_c_dir_entry(io) } + write_e_o_c_d(io, offset) + end + + def write_e_o_c_d(io, offset) #:nodoc: + io << + [END_OF_CENTRAL_DIRECTORY_SIGNATURE, + 0 , # @numberOfThisDisk + 0 , # @numberOfDiskWithStartOfCDir + @entrySet? @entrySet.size : 0 , + @entrySet? @entrySet.size : 0 , + cdir_size , + offset , + @comment ? @comment.length : 0 ].pack('VvvvvVVv') + io << @comment + end + private :write_e_o_c_d + + def cdir_size #:nodoc: + # does not include eocd + @entrySet.inject(0) { |value, entry| entry.cdir_header_size + value } + end + private :cdir_size + + def read_e_o_c_d(io) #:nodoc: + buf = get_e_o_c_d(io) + @numberOfThisDisk = ZipEntry::read_zip_short(buf) + @numberOfDiskWithStartOfCDir = ZipEntry::read_zip_short(buf) + @totalNumberOfEntriesInCDirOnThisDisk = ZipEntry::read_zip_short(buf) + @size = ZipEntry::read_zip_short(buf) + @sizeInBytes = ZipEntry::read_zip_long(buf) + @cdirOffset = ZipEntry::read_zip_long(buf) + commentLength = ZipEntry::read_zip_short(buf) + @comment = buf.read(commentLength) + raise ZipError, "Zip consistency problem while reading eocd structure" unless buf.size == 0 + end + + def read_central_directory_entries(io) #:nodoc: + begin + io.seek(@cdirOffset, IO::SEEK_SET) + rescue Errno::EINVAL + raise ZipError, "Zip consistency problem while reading central directory entry" + end + @entrySet = ZipEntrySet.new + @size.times { + @entrySet << ZipEntry.read_c_dir_entry(io) + } + end + + def read_from_stream(io) #:nodoc: + read_e_o_c_d(io) + read_central_directory_entries(io) + end + + def get_e_o_c_d(io) #:nodoc: + begin + io.seek(-MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE, IO::SEEK_END) + rescue Errno::EINVAL + io.seek(0, IO::SEEK_SET) + rescue Errno::EFBIG # FreeBSD 4.9 raise Errno::EFBIG instead of Errno::EINVAL + io.seek(0, IO::SEEK_SET) + end + + # 'buf = io.read' substituted with lump of code to work around FreeBSD 4.5 issue + retried = false + buf = nil + begin + buf = io.read + rescue Errno::EFBIG # FreeBSD 4.5 may raise Errno::EFBIG + raise if (retried) + retried = true + + io.seek(0, IO::SEEK_SET) + retry + end + + sigIndex = buf.rindex([END_OF_CENTRAL_DIRECTORY_SIGNATURE].pack('V')) + raise ZipError, "Zip end of central directory signature not found" unless sigIndex + buf=buf.slice!((sigIndex+4)...(buf.size)) + def buf.read(count) + slice!(0, count) + end + return buf + end + + # For iterating over the entries. + def each(&proc) + @entrySet.each(&proc) + end + + # Returns the number of entries in the central directory (and + # consequently in the zip archive). + def size + @entrySet.size + end + + def ZipCentralDirectory.read_from_stream(io) #:nodoc: + cdir = new + cdir.read_from_stream(io) + return cdir + rescue ZipError + return nil + end + + def == (other) #:nodoc: + return false unless other.kind_of?(ZipCentralDirectory) + @entrySet.entries.sort == other.entries.sort && comment == other.comment + end + end + + + class ZipError < StandardError ; end + + class ZipEntryExistsError < ZipError; end + class ZipDestinationFileExistsError < ZipError; end + class ZipCompressionMethodError < ZipError; end + class ZipEntryNameError < ZipError; end + class ZipInternalError < ZipError; end + + # ZipFile is modeled after java.util.zip.ZipFile from the Java SDK. + # The most important methods are those inherited from + # ZipCentralDirectory for accessing information about the entries in + # the archive and methods such as get_input_stream and + # get_output_stream for reading from and writing entries to the + # archive. The class includes a few convenience methods such as + # #extract for extracting entries to the filesystem, and #remove, + # #replace, #rename and #mkdir for making simple modifications to + # the archive. + # + # Modifications to a zip archive are not committed until #commit or + # #close is called. The method #open accepts a block following + # the pattern from File.open offering a simple way to + # automatically close the archive when the block returns. + # + # The following example opens zip archive my.zip + # (creating it if it doesn't exist) and adds an entry + # first.txt and a directory entry a_dir + # to it. + # + # require 'zip/zip' + # + # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) { + # |zipfile| + # zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" } + # zipfile.mkdir("a_dir") + # } + # + # The next example reopens my.zip writes the contents of + # first.txt to standard out and deletes the entry from + # the archive. + # + # require 'zip/zip' + # + # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) { + # |zipfile| + # puts zipfile.read("first.txt") + # zipfile.remove("first.txt") + # } + # + # ZipFileSystem offers an alternative API that emulates ruby's + # interface for accessing the filesystem, ie. the File and Dir classes. + + class ZipFile < ZipCentralDirectory + + CREATE = 1 + + attr_reader :name + + # default -> false + attr_accessor :restore_ownership + # default -> false + attr_accessor :restore_permissions + # default -> true + attr_accessor :restore_times + + # Opens a zip archive. Pass true as the second parameter to create + # a new archive if it doesn't exist already. + def initialize(fileName, create = nil) + super() + @name = fileName + @comment = "" + if (File.exists?(fileName)) + File.open(name, "rb") { |f| read_from_stream(f) } + elsif (create) + @entrySet = ZipEntrySet.new + else + raise ZipError, "File #{fileName} not found" + end + @create = create + @storedEntries = @entrySet.dup + + @restore_ownership = false + @restore_permissions = false + @restore_times = true + end + + # Same as #new. If a block is passed the ZipFile object is passed + # to the block and is automatically closed afterwards just as with + # ruby's builtin File.open method. + def ZipFile.open(fileName, create = nil) + zf = ZipFile.new(fileName, create) + if block_given? + begin + yield zf + ensure + zf.close + end + else + zf + end + end + + # Returns the zip files comment, if it has one + attr_accessor :comment + + # Iterates over the contents of the ZipFile. This is more efficient + # than using a ZipInputStream since this methods simply iterates + # through the entries in the central directory structure in the archive + # whereas ZipInputStream jumps through the entire archive accessing the + # local entry headers (which contain the same information as the + # central directory). + def ZipFile.foreach(aZipFileName, &block) + ZipFile.open(aZipFileName) { + |zipFile| + zipFile.each(&block) + } + end + + # Returns an input stream to the specified entry. If a block is passed + # the stream object is passed to the block and the stream is automatically + # closed afterwards just as with ruby's builtin File.open method. + def get_input_stream(entry, &aProc) + get_entry(entry).get_input_stream(&aProc) + end + + # Returns an output stream to the specified entry. If a block is passed + # the stream object is passed to the block and the stream is automatically + # closed afterwards just as with ruby's builtin File.open method. + def get_output_stream(entry, &aProc) + newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s) + if newEntry.directory? + raise ArgumentError, + "cannot open stream to directory entry - '#{newEntry}'" + end + zipStreamableEntry = ZipStreamableStream.new(newEntry) + @entrySet << zipStreamableEntry + zipStreamableEntry.get_output_stream(&aProc) + end + + # Returns the name of the zip archive + def to_s + @name + end + + # Returns a string containing the contents of the specified entry + def read(entry) + get_input_stream(entry) { |is| is.read } + end + + # Convenience method for adding the contents of a file to the archive + def add(entry, srcPath, &continueOnExistsProc) + continueOnExistsProc ||= proc { false } + check_entry_exists(entry, continueOnExistsProc, "add") + newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s) + newEntry.gather_fileinfo_from_srcpath(srcPath) + @entrySet << newEntry + end + + # Removes the specified entry. + def remove(entry) + @entrySet.delete(get_entry(entry)) + end + + # Renames the specified entry. + def rename(entry, newName, &continueOnExistsProc) + foundEntry = get_entry(entry) + check_entry_exists(newName, continueOnExistsProc, "rename") + foundEntry.name=newName + end + + # Replaces the specified entry with the contents of srcPath (from + # the file system). + def replace(entry, srcPath) + check_file(srcPath) + add(remove(entry), srcPath) + end + + # Extracts entry to file destPath. + def extract(entry, destPath, &onExistsProc) + onExistsProc ||= proc { false } + foundEntry = get_entry(entry) + foundEntry.extract(destPath, &onExistsProc) + end + + # Commits changes that has been made since the previous commit to + # the zip archive. + def commit + return if ! commit_required? + on_success_replace(name) { + |tmpFile| + ZipOutputStream.open(tmpFile) { + |zos| + + @entrySet.each { |e| e.write_to_zip_output_stream(zos) } + zos.comment = comment + } + true + } + initialize(name) + end + + # Closes the zip file committing any changes that has been made. + def close + commit + end + + # Returns true if any changes has been made to this archive since + # the previous commit + def commit_required? + return @entrySet != @storedEntries || @create == ZipFile::CREATE + end + + # Searches for entry with the specified name. Returns nil if + # no entry is found. See also get_entry + def find_entry(entry) + @entrySet.detect { + |e| + e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "") + } + end + + # Searches for an entry just as find_entry, but throws Errno::ENOENT + # if no entry is found. + def get_entry(entry) + selectedEntry = find_entry(entry) + unless selectedEntry + raise Errno::ENOENT, entry + end + selectedEntry.restore_ownership = @restore_ownership + selectedEntry.restore_permissions = @restore_permissions + selectedEntry.restore_times = @restore_times + + return selectedEntry + end + + # Creates a directory + def mkdir(entryName, permissionInt = 0755) + if find_entry(entryName) + raise Errno::EEXIST, "File exists - #{entryName}" + end + @entrySet << ZipStreamableDirectory.new(@name, entryName.to_s.ensure_end("/"), nil, permissionInt) + end + + private + + def is_directory(newEntry, srcPath) + srcPathIsDirectory = File.directory?(srcPath) + if newEntry.is_directory && ! srcPathIsDirectory + raise ArgumentError, + "entry name '#{newEntry}' indicates directory entry, but "+ + "'#{srcPath}' is not a directory" + elsif ! newEntry.is_directory && srcPathIsDirectory + newEntry.name += "/" + end + return newEntry.is_directory && srcPathIsDirectory + end + + def check_entry_exists(entryName, continueOnExistsProc, procedureName) + continueOnExistsProc ||= proc { false } + if @entrySet.detect { |e| e.name == entryName } + if continueOnExistsProc.call + remove get_entry(entryName) + else + raise ZipEntryExistsError, + procedureName+" failed. Entry #{entryName} already exists" + end + end + end + + def check_file(path) + unless File.readable? path + raise Errno::ENOENT, path + end + end + + def on_success_replace(aFilename) + tmpfile = get_tempfile + tmpFilename = tmpfile.path + tmpfile.close + if yield tmpFilename + File.move(tmpFilename, name) + end + end + + def get_tempfile + tempFile = Tempfile.new(File.basename(name), File.dirname(name)) + tempFile.binmode + tempFile + end + + end + + class ZipStreamableDirectory < ZipEntry + def initialize(zipfile, entry, srcPath = nil, permissionInt = nil) + super(zipfile, entry) + + @ftype = :directory + entry.get_extra_attributes_from_path(srcPath) if (srcPath) + @unix_perms = permissionInt if (permissionInt) + end + end + + class ZipStreamableStream < DelegateClass(ZipEntry) #nodoc:all + def initialize(entry) + super(entry) + @tempFile = Tempfile.new(File.basename(name), File.dirname(zipfile)) + @tempFile.binmode + end + + def get_output_stream + if block_given? + begin + yield(@tempFile) + ensure + @tempFile.close + end + else + @tempFile + end + end + + def get_input_stream + if ! @tempFile.closed? + raise StandardError, "cannot open entry for reading while its open for writing - #{name}" + end + @tempFile.open # reopens tempfile from top + @tempFile.binmode + if block_given? + begin + yield(@tempFile) + ensure + @tempFile.close + end + else + @tempFile + end + end + + def write_to_zip_output_stream(aZipOutputStream) + aZipOutputStream.put_next_entry(self) + get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) } + end + end + + class ZipExtraField < Hash + ID_MAP = {} + + # Meta class for extra fields + class Generic + def self.register_map + if self.const_defined?(:HEADER_ID) + ID_MAP[self.const_get(:HEADER_ID)] = self + end + end + + def self.name + self.to_s.split("::")[-1] + end + + # return field [size, content] or false + def initial_parse(binstr) + if ! binstr + # If nil, start with empty. + return false + elsif binstr[0,2] != self.class.const_get(:HEADER_ID) + $stderr.puts "Warning: weired extra feild header ID. skip parsing" + return false + end + [binstr[2,2].unpack("v")[0], binstr[4..-1]] + end + + def ==(other) + self.class != other.class and return false + each { |k, v| + v != other[k] and return false + } + true + end + + def to_local_bin + s = pack_for_local + self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s + end + + def to_c_dir_bin + s = pack_for_c_dir + self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s + end + end + + # Info-ZIP Additional timestamp field + class UniversalTime < Generic + HEADER_ID = "UT" + register_map + + def initialize(binstr = nil) + @ctime = nil + @mtime = nil + @atime = nil + @flag = nil + binstr and merge(binstr) + end + attr_accessor :atime, :ctime, :mtime, :flag + + def merge(binstr) + binstr == "" and return + size, content = initial_parse(binstr) + size or return + @flag, mtime, atime, ctime = content.unpack("CVVV") + mtime and @mtime ||= Time.at(mtime) + atime and @atime ||= Time.at(atime) + ctime and @ctime ||= Time.at(ctime) + end + + def ==(other) + @mtime == other.mtime && + @atime == other.atime && + @ctime == other.ctime + end + + def pack_for_local + s = [@flag].pack("C") + @flag & 1 != 0 and s << [@mtime.to_i].pack("V") + @flag & 2 != 0 and s << [@atime.to_i].pack("V") + @flag & 4 != 0 and s << [@ctime.to_i].pack("V") + s + end + + def pack_for_c_dir + s = [@flag].pack("C") + @flag & 1 == 1 and s << [@mtime.to_i].pack("V") + s + end + end + + # Info-ZIP Extra for UNIX uid/gid + class IUnix < Generic + HEADER_ID = "Ux" + register_map + + def initialize(binstr = nil) + @uid = 0 + @gid = 0 + binstr and merge(binstr) + end + attr_accessor :uid, :gid + + def merge(binstr) + binstr == "" and return + size, content = initial_parse(binstr) + # size: 0 for central direcotry. 4 for local header + return if(! size || size == 0) + uid, gid = content.unpack("vv") + @uid ||= uid + @gid ||= gid + end + + def ==(other) + @uid == other.uid && + @gid == other.gid + end + + def pack_for_local + [@uid, @gid].pack("vv") + end + + def pack_for_c_dir + "" + end + end + + ## start main of ZipExtraField < Hash + def initialize(binstr = nil) + binstr and merge(binstr) + end + + def merge(binstr) + binstr == "" and return + i = 0 + while i < binstr.length + id = binstr[i,2] + len = binstr[i+2,2].to_s.unpack("v")[0] + if id && ID_MAP.member?(id) + field_name = ID_MAP[id].name + if self.member?(field_name) + self[field_name].mergea(binstr[i, len+4]) + else + field_obj = ID_MAP[id].new(binstr[i, len+4]) + self[field_name] = field_obj + end + elsif id + unless self["Unknown"] + s = "" + class << s + alias_method :to_c_dir_bin, :to_s + alias_method :to_local_bin, :to_s + end + self["Unknown"] = s + end + if ! len || len+4 > binstr[i..-1].length + self["Unknown"] << binstr[i..-1] + break; + end + self["Unknown"] << binstr[i, len+4] + end + i += len+4 + end + end + + def create(name) + field_class = nil + ID_MAP.each { |id, klass| + if klass.name == name + field_class = klass + break + end + } + if ! field_class + raise ZipError, "Unknown extra field '#{name}'" + end + self[name] = field_class.new() + end + + def to_local_bin + s = "" + each { |k, v| + s << v.to_local_bin + } + s + end + alias :to_s :to_local_bin + + def to_c_dir_bin + s = "" + each { |k, v| + s << v.to_c_dir_bin + } + s + end + + def c_dir_length + to_c_dir_bin.length + end + def local_length + to_local_bin.length + end + alias :c_dir_size :c_dir_length + alias :local_size :local_length + alias :length :local_length + alias :size :local_length + end # end ZipExtraField + +end # Zip namespace module + + + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/zipfilesystem.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/zipfilesystem.rb new file mode 100755 index 00000000..3fa3748c --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/zipfilesystem.rb @@ -0,0 +1,609 @@ +require 'zip/zip' + +module Zip + + # The ZipFileSystem API provides an API for accessing entries in + # a zip archive that is similar to ruby's builtin File and Dir + # classes. + # + # Requiring 'zip/zipfilesystem' includes this module in ZipFile + # making the methods in this module available on ZipFile objects. + # + # Using this API the following example creates a new zip file + # my.zip containing a normal entry with the name + # first.txt, a directory entry named mydir + # and finally another normal entry named second.txt + # + # require 'zip/zipfilesystem' + # + # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) { + # |zipfile| + # zipfile.file.open("first.txt", "w") { |f| f.puts "Hello world" } + # zipfile.dir.mkdir("mydir") + # zipfile.file.open("mydir/second.txt", "w") { |f| f.puts "Hello again" } + # } + # + # Reading is as easy as writing, as the following example shows. The + # example writes the contents of first.txt from zip archive + # my.zip to standard out. + # + # require 'zip/zipfilesystem' + # + # Zip::ZipFile.open("my.zip") { + # |zipfile| + # puts zipfile.file.read("first.txt") + # } + + module ZipFileSystem + + def initialize # :nodoc: + mappedZip = ZipFileNameMapper.new(self) + @zipFsDir = ZipFsDir.new(mappedZip) + @zipFsFile = ZipFsFile.new(mappedZip) + @zipFsDir.file = @zipFsFile + @zipFsFile.dir = @zipFsDir + end + + # Returns a ZipFsDir which is much like ruby's builtin Dir (class) + # object, except it works on the ZipFile on which this method is + # invoked + def dir + @zipFsDir + end + + # Returns a ZipFsFile which is much like ruby's builtin File (class) + # object, except it works on the ZipFile on which this method is + # invoked + def file + @zipFsFile + end + + # Instances of this class are normally accessed via the accessor + # ZipFile::file. An instance of ZipFsFile behaves like ruby's + # builtin File (class) object, except it works on ZipFile entries. + # + # The individual methods are not documented due to their + # similarity with the methods in File + class ZipFsFile + + attr_writer :dir +# protected :dir + + class ZipFsStat + def initialize(zipFsFile, entryName) + @zipFsFile = zipFsFile + @entryName = entryName + end + + def forward_invoke(msg) + @zipFsFile.send(msg, @entryName) + end + + def kind_of?(t) + super || t == ::File::Stat + end + + forward_message :forward_invoke, :file?, :directory?, :pipe?, :chardev? + forward_message :forward_invoke, :symlink?, :socket?, :blockdev? + forward_message :forward_invoke, :readable?, :readable_real? + forward_message :forward_invoke, :writable?, :writable_real? + forward_message :forward_invoke, :executable?, :executable_real? + forward_message :forward_invoke, :sticky?, :owned?, :grpowned? + forward_message :forward_invoke, :setuid?, :setgid? + forward_message :forward_invoke, :zero? + forward_message :forward_invoke, :size, :size? + forward_message :forward_invoke, :mtime, :atime, :ctime + + def blocks; nil; end + + def get_entry + @zipFsFile.__send__(:get_entry, @entryName) + end + private :get_entry + + def gid + e = get_entry + if e.extra.member? "IUnix" + e.extra["IUnix"].gid || 0 + else + 0 + end + end + + def uid + e = get_entry + if e.extra.member? "IUnix" + e.extra["IUnix"].uid || 0 + else + 0 + end + end + + def ino; 0; end + + def dev; 0; end + + def rdev; 0; end + + def rdev_major; 0; end + + def rdev_minor; 0; end + + def ftype + if file? + return "file" + elsif directory? + return "directory" + else + raise StandardError, "Unknown file type" + end + end + + def nlink; 1; end + + def blksize; nil; end + + def mode + e = get_entry + if e.fstype == 3 + e.externalFileAttributes >> 16 + else + 33206 # 33206 is equivalent to -rw-rw-rw- + end + end + end + + def initialize(mappedZip) + @mappedZip = mappedZip + end + + def get_entry(fileName) + if ! exists?(fileName) + raise Errno::ENOENT, "No such file or directory - #{fileName}" + end + @mappedZip.find_entry(fileName) + end + private :get_entry + + def unix_mode_cmp(fileName, mode) + begin + e = get_entry(fileName) + e.fstype == 3 && ((e.externalFileAttributes >> 16) & mode ) != 0 + rescue Errno::ENOENT + false + end + end + private :unix_mode_cmp + + def exists?(fileName) + expand_path(fileName) == "/" || @mappedZip.find_entry(fileName) != nil + end + alias :exist? :exists? + + # Permissions not implemented, so if the file exists it is accessible + alias owned? exists? + alias grpowned? exists? + + def readable?(fileName) + unix_mode_cmp(fileName, 0444) + end + alias readable_real? readable? + + def writable?(fileName) + unix_mode_cmp(fileName, 0222) + end + alias writable_real? writable? + + def executable?(fileName) + unix_mode_cmp(fileName, 0111) + end + alias executable_real? executable? + + def setuid?(fileName) + unix_mode_cmp(fileName, 04000) + end + + def setgid?(fileName) + unix_mode_cmp(fileName, 02000) + end + + def sticky?(fileName) + unix_mode_cmp(fileName, 01000) + end + + def umask(*args) + ::File.umask(*args) + end + + def truncate(fileName, len) + raise StandardError, "truncate not supported" + end + + def directory?(fileName) + entry = @mappedZip.find_entry(fileName) + expand_path(fileName) == "/" || (entry != nil && entry.directory?) + end + + def open(fileName, openMode = "r", &block) + case openMode + when "r" + @mappedZip.get_input_stream(fileName, &block) + when "w" + @mappedZip.get_output_stream(fileName, &block) + else + raise StandardError, "openmode '#{openMode} not supported" unless openMode == "r" + end + end + + def new(fileName, openMode = "r") + open(fileName, openMode) + end + + def size(fileName) + @mappedZip.get_entry(fileName).size + end + + # Returns nil for not found and nil for directories + def size?(fileName) + entry = @mappedZip.find_entry(fileName) + return (entry == nil || entry.directory?) ? nil : entry.size + end + + def chown(ownerInt, groupInt, *filenames) + filenames.each { |fileName| + e = get_entry(fileName) + unless e.extra.member?("IUnix") + e.extra.create("IUnix") + end + e.extra["IUnix"].uid = ownerInt + e.extra["IUnix"].gid = groupInt + } + filenames.size + end + + def chmod (modeInt, *filenames) + filenames.each { |fileName| + e = get_entry(fileName) + e.fstype = 3 # force convertion filesystem type to unix + e.externalFileAttributes = modeInt << 16 + } + filenames.size + end + + def zero?(fileName) + sz = size(fileName) + sz == nil || sz == 0 + rescue Errno::ENOENT + false + end + + def file?(fileName) + entry = @mappedZip.find_entry(fileName) + entry != nil && entry.file? + end + + def dirname(fileName) + ::File.dirname(fileName) + end + + def basename(fileName) + ::File.basename(fileName) + end + + def split(fileName) + ::File.split(fileName) + end + + def join(*fragments) + ::File.join(*fragments) + end + + def utime(modifiedTime, *fileNames) + fileNames.each { |fileName| + get_entry(fileName).time = modifiedTime + } + end + + def mtime(fileName) + @mappedZip.get_entry(fileName).mtime + end + + def atime(fileName) + e = get_entry(fileName) + if e.extra.member? "UniversalTime" + e.extra["UniversalTime"].atime + else + nil + end + end + + def ctime(fileName) + e = get_entry(fileName) + if e.extra.member? "UniversalTime" + e.extra["UniversalTime"].ctime + else + nil + end + end + + def pipe?(filename) + false + end + + def blockdev?(filename) + false + end + + def chardev?(filename) + false + end + + def symlink?(fileName) + false + end + + def socket?(fileName) + false + end + + def ftype(fileName) + @mappedZip.get_entry(fileName).directory? ? "directory" : "file" + end + + def readlink(fileName) + raise NotImplementedError, "The readlink() function is not implemented" + end + + def symlink(fileName, symlinkName) + raise NotImplementedError, "The symlink() function is not implemented" + end + + def link(fileName, symlinkName) + raise NotImplementedError, "The link() function is not implemented" + end + + def pipe + raise NotImplementedError, "The pipe() function is not implemented" + end + + def stat(fileName) + if ! exists?(fileName) + raise Errno::ENOENT, fileName + end + ZipFsStat.new(self, fileName) + end + + alias lstat stat + + def readlines(fileName) + open(fileName) { |is| is.readlines } + end + + def read(fileName) + @mappedZip.read(fileName) + end + + def popen(*args, &aProc) + File.popen(*args, &aProc) + end + + def foreach(fileName, aSep = $/, &aProc) + open(fileName) { |is| is.each_line(aSep, &aProc) } + end + + def delete(*args) + args.each { + |fileName| + if directory?(fileName) + raise Errno::EISDIR, "Is a directory - \"#{fileName}\"" + end + @mappedZip.remove(fileName) + } + end + + def rename(fileToRename, newName) + @mappedZip.rename(fileToRename, newName) { true } + end + + alias :unlink :delete + + def expand_path(aPath) + @mappedZip.expand_path(aPath) + end + end + + # Instances of this class are normally accessed via the accessor + # ZipFile::dir. An instance of ZipFsDir behaves like ruby's + # builtin Dir (class) object, except it works on ZipFile entries. + # + # The individual methods are not documented due to their + # similarity with the methods in Dir + class ZipFsDir + + def initialize(mappedZip) + @mappedZip = mappedZip + end + + attr_writer :file + + def new(aDirectoryName) + ZipFsDirIterator.new(entries(aDirectoryName)) + end + + def open(aDirectoryName) + dirIt = new(aDirectoryName) + if block_given? + begin + yield(dirIt) + return nil + ensure + dirIt.close + end + end + dirIt + end + + def pwd; @mappedZip.pwd; end + alias getwd pwd + + def chdir(aDirectoryName) + unless @file.stat(aDirectoryName).directory? + raise Errno::EINVAL, "Invalid argument - #{aDirectoryName}" + end + @mappedZip.pwd = @file.expand_path(aDirectoryName) + end + + def entries(aDirectoryName) + entries = [] + foreach(aDirectoryName) { |e| entries << e } + entries + end + + def foreach(aDirectoryName) + unless @file.stat(aDirectoryName).directory? + raise Errno::ENOTDIR, aDirectoryName + end + path = @file.expand_path(aDirectoryName).ensure_end("/") + + subDirEntriesRegex = Regexp.new("^#{path}([^/]+)$") + @mappedZip.each { + |fileName| + match = subDirEntriesRegex.match(fileName) + yield(match[1]) unless match == nil + } + end + + def delete(entryName) + unless @file.stat(entryName).directory? + raise Errno::EINVAL, "Invalid argument - #{entryName}" + end + @mappedZip.remove(entryName) + end + alias rmdir delete + alias unlink delete + + def mkdir(entryName, permissionInt = 0755) + @mappedZip.mkdir(entryName, permissionInt) + end + + def chroot(*args) + raise NotImplementedError, "The chroot() function is not implemented" + end + + end + + class ZipFsDirIterator # :nodoc:all + include Enumerable + + def initialize(arrayOfFileNames) + @fileNames = arrayOfFileNames + @index = 0 + end + + def close + @fileNames = nil + end + + def each(&aProc) + raise IOError, "closed directory" if @fileNames == nil + @fileNames.each(&aProc) + end + + def read + raise IOError, "closed directory" if @fileNames == nil + @fileNames[(@index+=1)-1] + end + + def rewind + raise IOError, "closed directory" if @fileNames == nil + @index = 0 + end + + def seek(anIntegerPosition) + raise IOError, "closed directory" if @fileNames == nil + @index = anIntegerPosition + end + + def tell + raise IOError, "closed directory" if @fileNames == nil + @index + end + end + + # All access to ZipFile from ZipFsFile and ZipFsDir goes through a + # ZipFileNameMapper, which has one responsibility: ensure + class ZipFileNameMapper # :nodoc:all + include Enumerable + + def initialize(zipFile) + @zipFile = zipFile + @pwd = "/" + end + + attr_accessor :pwd + + def find_entry(fileName) + @zipFile.find_entry(expand_to_entry(fileName)) + end + + def get_entry(fileName) + @zipFile.get_entry(expand_to_entry(fileName)) + end + + def get_input_stream(fileName, &aProc) + @zipFile.get_input_stream(expand_to_entry(fileName), &aProc) + end + + def get_output_stream(fileName, &aProc) + @zipFile.get_output_stream(expand_to_entry(fileName), &aProc) + end + + def read(fileName) + @zipFile.read(expand_to_entry(fileName)) + end + + def remove(fileName) + @zipFile.remove(expand_to_entry(fileName)) + end + + def rename(fileName, newName, &continueOnExistsProc) + @zipFile.rename(expand_to_entry(fileName), expand_to_entry(newName), + &continueOnExistsProc) + end + + def mkdir(fileName, permissionInt = 0755) + @zipFile.mkdir(expand_to_entry(fileName), permissionInt) + end + + # Turns entries into strings and adds leading / + # and removes trailing slash on directories + def each + @zipFile.each { + |e| + yield("/"+e.to_s.chomp("/")) + } + end + + def expand_path(aPath) + expanded = aPath.starts_with("/") ? aPath : @pwd.ensure_end("/") + aPath + expanded.gsub!(/\/\.(\/|$)/, "") + expanded.gsub!(/[^\/]+\/\.\.(\/|$)/, "") + expanded.empty? ? "/" : expanded + end + + private + + def expand_to_entry(aPath) + expand_path(aPath).lchop + end + end + end + + class ZipFile + include ZipFileSystem + end +end + +# Copyright (C) 2002, 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/lib/zip/ziprequire.rb b/vendor/plugins/rubyzip-0.9.1/lib/zip/ziprequire.rb new file mode 100755 index 00000000..5a4c4d48 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/lib/zip/ziprequire.rb @@ -0,0 +1,90 @@ +# With ziprequire you can load ruby modules from a zip file. This means +# ruby's module include path can include zip-files. +# +# The following example creates a zip file with a single entry +# log/simplelog.rb that contains a single function +# simpleLog: +# +# require 'zip/zipfilesystem' +# +# Zip::ZipFile.open("my.zip", true) { +# |zf| +# zf.file.open("log/simplelog.rb", "w") { +# |f| +# f.puts "def simpleLog(v)" +# f.puts ' Kernel.puts "INFO: #{v}"' +# f.puts "end" +# } +# } +# +# To use the ruby module stored in the zip archive simply require +# zip/ziprequire and include the my.zip zip +# file in the module search path. The following command shows one +# way to do this: +# +# ruby -rzip/ziprequire -Imy.zip -e " require 'log/simplelog'; simpleLog 'Hello world' " + +#$: << 'data/rubycode.zip' << 'data/rubycode2.zip' + + +require 'zip/zip' + +class ZipList #:nodoc:all + def initialize(zipFileList) + @zipFileList = zipFileList + end + + def get_input_stream(entry, &aProc) + @zipFileList.each { + |zfName| + Zip::ZipFile.open(zfName) { + |zf| + begin + return zf.get_input_stream(entry, &aProc) + rescue Errno::ENOENT + end + } + } + raise Errno::ENOENT, + "No matching entry found in zip files '#{@zipFileList.join(', ')}' "+ + " for '#{entry}'" + end +end + + +module Kernel #:nodoc:all + alias :oldRequire :require + + def require(moduleName) + zip_require(moduleName) || oldRequire(moduleName) + end + + def zip_require(moduleName) + return false if already_loaded?(moduleName) + get_resource(ensure_rb_extension(moduleName)) { + |zis| + eval(zis.read); $" << moduleName + } + return true + rescue Errno::ENOENT => ex + return false + end + + def get_resource(resourceName, &aProc) + zl = ZipList.new($:.grep(/\.zip$/)) + zl.get_input_stream(resourceName, &aProc) + end + + def already_loaded?(moduleName) + moduleRE = Regexp.new("^"+moduleName+"(\.rb|\.so|\.dll|\.o)?$") + $".detect { |e| e =~ moduleRE } != nil + end + + def ensure_rb_extension(aString) + aString.sub(/(\.rb)?$/i, ".rb") + end +end + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/samples/example.rb b/vendor/plugins/rubyzip-0.9.1/samples/example.rb new file mode 100755 index 00000000..741afa76 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/example.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby + +$: << "../lib" +system("zip example.zip example.rb gtkRubyzip.rb") + +require 'zip/zip' + +####### Using ZipInputStream alone: ####### + +Zip::ZipInputStream.open("example.zip") { + |zis| + entry = zis.get_next_entry + print "First line of '#{entry.name} (#{entry.size} bytes): " + puts "'#{zis.gets.chomp}'" + entry = zis.get_next_entry + print "First line of '#{entry.name} (#{entry.size} bytes): " + puts "'#{zis.gets.chomp}'" +} + + +####### Using ZipFile to read the directory of a zip file: ####### + +zf = Zip::ZipFile.new("example.zip") +zf.each_with_index { + |entry, index| + + puts "entry #{index} is #{entry.name}, size = #{entry.size}, compressed size = #{entry.compressed_size}" + # use zf.get_input_stream(entry) to get a ZipInputStream for the entry + # entry can be the ZipEntry object or any object which has a to_s method that + # returns the name of the entry. +} + + +####### Using ZipOutputStream to write a zip file: ####### + +Zip::ZipOutputStream.open("exampleout.zip") { + |zos| + zos.put_next_entry("the first little entry") + zos.puts "Hello hello hello hello hello hello hello hello hello" + + zos.put_next_entry("the second little entry") + zos.puts "Hello again" + + # Use rubyzip or your zip client of choice to verify + # the contents of exampleout.zip +} + +####### Using ZipFile to change a zip file: ####### + +Zip::ZipFile.open("exampleout.zip") { + |zf| + zf.add("thisFile.rb", "example.rb") + zf.rename("thisFile.rb", "ILikeThisName.rb") + zf.add("Again", "example.rb") +} + +# Lets check +Zip::ZipFile.open("exampleout.zip") { + |zf| + puts "Changed zip file contains: #{zf.entries.join(', ')}" + zf.remove("Again") + puts "Without 'Again': #{zf.entries.join(', ')}" +} + +# For other examples, look at zip.rb and ziptest.rb + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/samples/example_filesystem.rb b/vendor/plugins/rubyzip-0.9.1/samples/example_filesystem.rb new file mode 100755 index 00000000..867e8d4f --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/example_filesystem.rb @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby + +$: << "../lib" + +require 'zip/zipfilesystem' +require 'ftools' + +EXAMPLE_ZIP = "filesystem.zip" + +File.delete(EXAMPLE_ZIP) if File.exists?(EXAMPLE_ZIP) + +Zip::ZipFile.open(EXAMPLE_ZIP, Zip::ZipFile::CREATE) { + |zf| + zf.file.open("file1.txt", "w") { |os| os.write "first file1.txt" } + zf.dir.mkdir("dir1") + zf.dir.chdir("dir1") + zf.file.open("file1.txt", "w") { |os| os.write "second file1.txt" } + puts zf.file.read("file1.txt") + puts zf.file.read("../file1.txt") + zf.dir.chdir("..") + zf.file.open("file2.txt", "w") { |os| os.write "first file2.txt" } + puts "Entries: #{zf.entries.join(', ')}" +} + +Zip::ZipFile.open(EXAMPLE_ZIP) { + |zf| + puts "Entries from reloaded zip: #{zf.entries.join(', ')}" +} + +# For other examples, look at zip.rb and ziptest.rb + +# Copyright (C) 2003 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/samples/gtkRubyzip.rb b/vendor/plugins/rubyzip-0.9.1/samples/gtkRubyzip.rb new file mode 100755 index 00000000..5d91829d --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/gtkRubyzip.rb @@ -0,0 +1,86 @@ +#!/usr/bin/env ruby + +$: << "../lib" + +$VERBOSE = true + +require 'gtk' +require 'zip/zip' + +class MainApp < Gtk::Window + def initialize + super() + set_usize(400, 256) + set_title("rubyzip") + signal_connect(Gtk::Window::SIGNAL_DESTROY) { Gtk.main_quit } + + box = Gtk::VBox.new(false, 0) + add(box) + + @zipfile = nil + @buttonPanel = ButtonPanel.new + @buttonPanel.openButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + show_file_selector + } + @buttonPanel.extractButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + puts "Not implemented!" + } + box.pack_start(@buttonPanel, false, false, 0) + + sw = Gtk::ScrolledWindow.new + sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) + box.pack_start(sw, true, true, 0) + + @clist = Gtk::CList.new(["Name", "Size", "Compression"]) + @clist.set_selection_mode(Gtk::SELECTION_BROWSE) + @clist.set_column_width(0, 120) + @clist.set_column_width(1, 120) + @clist.signal_connect(Gtk::CList::SIGNAL_SELECT_ROW) { + |w, row, column, event| + @selected_row = row + } + sw.add(@clist) + end + + class ButtonPanel < Gtk::HButtonBox + attr_reader :openButton, :extractButton + def initialize + super + set_layout(Gtk::BUTTONBOX_START) + set_spacing(0) + @openButton = Gtk::Button.new("Open archive") + @extractButton = Gtk::Button.new("Extract entry") + pack_start(@openButton) + pack_start(@extractButton) + end + end + + def show_file_selector + @fileSelector = Gtk::FileSelection.new("Open zip file") + @fileSelector.show + @fileSelector.ok_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + open_zip(@fileSelector.filename) + @fileSelector.destroy + } + @fileSelector.cancel_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) { + @fileSelector.destroy + } + end + + def open_zip(filename) + @zipfile = Zip::ZipFile.open(filename) + @clist.clear + @zipfile.each { + |entry| + @clist.append([ entry.name, + entry.size.to_s, + (100.0*entry.compressedSize/entry.size).to_s+"%" ]) + } + end +end + +mainApp = MainApp.new() + +mainApp.show_all + +Gtk.main diff --git a/vendor/plugins/rubyzip-0.9.1/samples/qtzip.rb b/vendor/plugins/rubyzip-0.9.1/samples/qtzip.rb new file mode 100755 index 00000000..3d76bd18 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/qtzip.rb @@ -0,0 +1,101 @@ +#!/usr/bin/env ruby + +$VERBOSE=true + +$: << "../lib" + +require 'Qt' +system('rbuic -o zipdialogui.rb zipdialogui.ui') +require 'zipdialogui.rb' +require 'zip/zip' + + + +a = Qt::Application.new(ARGV) + +class ZipDialog < ZipDialogUI + + + def initialize() + super() + connect(child('add_button'), SIGNAL('clicked()'), + self, SLOT('add_files()')) + connect(child('extract_button'), SIGNAL('clicked()'), + self, SLOT('extract_files()')) + end + + def zipfile(&proc) + Zip::ZipFile.open(@zip_filename, &proc) + end + + def each(&proc) + Zip::ZipFile.foreach(@zip_filename, &proc) + end + + def refresh() + lv = child("entry_list_view") + lv.clear + each { + |e| + lv.insert_item(Qt::ListViewItem.new(lv, e.name, e.size.to_s)) + } + end + + + def load(zipfile) + @zip_filename = zipfile + refresh + end + + def add_files + l = Qt::FileDialog.getOpenFileNames(nil, nil, self) + zipfile { + |zf| + l.each { + |path| + zf.add(File.basename(path), path) + } + } + refresh + end + + def extract_files + selected_items = [] + unselected_items = [] + lv_item = entry_list_view.first_child + while (lv_item) + if entry_list_view.is_selected(lv_item) + selected_items << lv_item.text(0) + else + unselected_items << lv_item.text(0) + end + lv_item = lv_item.next_sibling + end + puts "selected_items.size = #{selected_items.size}" + puts "unselected_items.size = #{unselected_items.size}" + items = selected_items.size > 0 ? selected_items : unselected_items + puts "items.size = #{items.size}" + + d = Qt::FileDialog.get_existing_directory(nil, self) + if (!d) + puts "No directory chosen" + else + zipfile { |zf| items.each { |e| zf.extract(e, File.join(d, e)) } } + end + + end + + slots 'add_files()', 'extract_files()' +end + +if !ARGV[0] + puts "usage: #{$0} zipname" + exit +end + +zd = ZipDialog.new +zd.load(ARGV[0]) + +a.mainWidget = zd +zd.show() +a.exec() diff --git a/vendor/plugins/rubyzip-0.9.1/samples/write_simple.rb b/vendor/plugins/rubyzip-0.9.1/samples/write_simple.rb new file mode 100755 index 00000000..5a1f26b1 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/write_simple.rb @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +$: << "../lib" + +require 'zip/zip' + +include Zip + +ZipOutputStream.open('simple.zip') { + |zos| + ze = zos.put_next_entry 'entry.txt' + zos.puts "Hello world" +} diff --git a/vendor/plugins/rubyzip-0.9.1/samples/zipfind.rb b/vendor/plugins/rubyzip-0.9.1/samples/zipfind.rb new file mode 100755 index 00000000..54ad936e --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/samples/zipfind.rb @@ -0,0 +1,74 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +$: << "../lib" + +require 'zip/zip' +require 'find' + +module Zip + module ZipFind + def self.find(path, zipFilePattern = /\.zip$/i) + Find.find(path) { + |fileName| + yield(fileName) + if zipFilePattern.match(fileName) && File.file?(fileName) + begin + Zip::ZipFile.foreach(fileName) { + |zipEntry| + yield(fileName + File::SEPARATOR + zipEntry.to_s) + } + rescue Errno::EACCES => ex + puts ex + end + end + } + end + + def self.find_file(path, fileNamePattern, zipFilePattern = /\.zip$/i) + self.find(path, zipFilePattern) { + |fileName| + yield(fileName) if fileNamePattern.match(fileName) + } + end + + end +end + +if __FILE__ == $0 + module ZipFindConsoleRunner + + PATH_ARG_INDEX = 0; + FILENAME_PATTERN_ARG_INDEX = 1; + ZIPFILE_PATTERN_ARG_INDEX = 2; + + def self.run(args) + check_args(args) + Zip::ZipFind.find_file(args[PATH_ARG_INDEX], + args[FILENAME_PATTERN_ARG_INDEX], + args[ZIPFILE_PATTERN_ARG_INDEX]) { + |fileName| + report_entry_found fileName + } + end + + def self.check_args(args) + if (args.size != 3) + usage + exit + end + end + + def self.usage + puts "Usage: #{$0} PATH ZIPFILENAME_PATTERN FILNAME_PATTERN" + end + + def self.report_entry_found(fileName) + puts fileName + end + + end + + ZipFindConsoleRunner.run(ARGV) +end diff --git a/vendor/plugins/rubyzip-0.9.1/test/alltests.rb b/vendor/plugins/rubyzip-0.9.1/test/alltests.rb new file mode 100755 index 00000000..691349af --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/alltests.rb @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +require 'stdrubyexttest' +require 'ioextrastest' +require 'ziptest' +require 'zipfilesystemtest' +require 'ziprequiretest' diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt b/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt new file mode 100644 index 00000000..23ea2f73 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt @@ -0,0 +1,46 @@ + +AUTOMAKE_OPTIONS = gnu + +EXTRA_DIST = test.zip + +CXXFLAGS= -g + +noinst_LIBRARIES = libzipios.a + +bin_PROGRAMS = test_zip test_izipfilt test_izipstream +# test_flist + +libzipios_a_SOURCES = backbuffer.h fcol.cpp fcol.h \ + fcol_common.h fcolexceptions.cpp fcolexceptions.h \ + fileentry.cpp fileentry.h flist.cpp \ + flist.h flistentry.cpp flistentry.h \ + flistscanner.h ifiltstreambuf.cpp ifiltstreambuf.h \ + inflatefilt.cpp inflatefilt.h izipfilt.cpp \ + izipfilt.h izipstream.cpp izipstream.h \ + zipfile.cpp zipfile.h ziphead.cpp \ + ziphead.h flistscanner.ll + +# test_flist_SOURCES = test_flist.cpp + +test_izipfilt_SOURCES = test_izipfilt.cpp + +test_izipstream_SOURCES = test_izipstream.cpp + +test_zip_SOURCES = test_zip.cpp + +# Notice that libzipios.a is not specified as -L. -lzipios +# If it was, automake would not include it as a dependency. + +# test_flist_LDADD = libzipios.a + +test_izipfilt_LDADD = libzipios.a -lz + +test_zip_LDADD = libzipios.a -lz + +test_izipstream_LDADD = libzipios.a -lz + + + +flistscanner.cc : flistscanner.ll + $(LEX) -+ -PFListScanner -o$@ $^ + diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt.deflatedData b/vendor/plugins/rubyzip-0.9.1/test/data/file1.txt.deflatedData new file mode 100644 index 0000000000000000000000000000000000000000..bfbb4f42c009154e9e3304855c31813275b709d6 GIT binary patch literal 482 zcmV<80UiE@RMBpmFcf{>{fa9!51TglfJv3cnzTZrO$4cwhiS+$rdV}s6dQHj*U#Vt z3=5f`xX0%l?mad@^t@d^Mn6{hdb5q!PZ{3gi);W^yKNff%Q)Lw#4v5bKfDIG+wJa? z=pnns-~~V`F15*%_4Ca zj^EboH)XZqO6tya0#)-~Treih@%_}yQ1_j5gZaJAdUeEVT>IuDsQSN`rbJ4Y7;mF@ zf!i26zX>!yBbTKhhP8Aj^y*W$=fmwAo%K2sJ)!HNmwM3k8J!jDh3C2&Q7T4?A^j^} z9r3Ik8+;@B3zEjD19@fmrW#RnN-n8r3f5ArlwiSXCJQF% zdpJoZSw_p{^uI6; YT71LBFMz*PQb9>fNlr&oR8>Ys3VC(y$N&HU literal 0 HcmV?d00001 diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/file2.txt b/vendor/plugins/rubyzip-0.9.1/test/data/file2.txt new file mode 100644 index 00000000..cc9ef6ad --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/data/file2.txt @@ -0,0 +1,1504 @@ +#!/usr/bin/env ruby + +$VERBOSE = true + +require 'rubyunit' +require 'zip' + +include Zip + +Dir.chdir "test" + +class AbstractInputStreamTest < RUNIT::TestCase + # AbstractInputStream subclass that provides a read method + + TEST_LINES = [ "Hello world#{$/}", + "this is the second line#{$/}", + "this is the last line"] + TEST_STRING = TEST_LINES.join + class TestAbstractInputStream + include AbstractInputStream + def initialize(aString) + @contents = aString + @readPointer = 0 + end + + def read(charsToRead) + retVal=@contents[@readPointer, charsToRead] + @readPointer+=charsToRead + return retVal + end + + def produceInput + read(100) + end + + def inputFinished? + @contents[@readPointer] == nil + end + end + + def setup + @io = TestAbstractInputStream.new(TEST_STRING) + end + + def test_gets + assert_equals(TEST_LINES[0], @io.gets) + assert_equals(TEST_LINES[1], @io.gets) + assert_equals(TEST_LINES[2], @io.gets) + assert_equals(nil, @io.gets) + end + + def test_getsMultiCharSeperator + assert_equals("Hell", @io.gets("ll")) + assert_equals("o world#{$/}this is the second l", @io.gets("d l")) + end + + def test_each_line + lineNumber=0 + @io.each_line { + |line| + assert_equals(TEST_LINES[lineNumber], line) + lineNumber+=1 + } + end + + def test_readlines + assert_equals(TEST_LINES, @io.readlines) + end + + def test_readline + test_gets + begin + @io.readline + fail "EOFError expected" + rescue EOFError + end + end +end + +class ZipEntryTest < RUNIT::TestCase + TEST_ZIPFILE = "someZipFile.zip" + TEST_COMMENT = "a comment" + TEST_COMPRESSED_SIZE = 1234 + TEST_CRC = 325324 + TEST_EXTRA = "Some data here" + TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED + TEST_NAME = "entry name" + TEST_SIZE = 8432 + TEST_ISDIRECTORY = false + + def test_constructorAndGetters + entry = ZipEntry.new(TEST_ZIPFILE, + TEST_NAME, + TEST_COMMENT, + TEST_EXTRA, + TEST_COMPRESSED_SIZE, + TEST_CRC, + TEST_COMPRESSIONMETHOD, + TEST_SIZE) + + assert_equals(TEST_COMMENT, entry.comment) + assert_equals(TEST_COMPRESSED_SIZE, entry.compressedSize) + assert_equals(TEST_CRC, entry.crc) + assert_equals(TEST_EXTRA, entry.extra) + assert_equals(TEST_COMPRESSIONMETHOD, entry.compressionMethod) + assert_equals(TEST_NAME, entry.name) + assert_equals(TEST_SIZE, entry.size) + assert_equals(TEST_ISDIRECTORY, entry.isDirectory) + end + + def test_equality + entry1 = ZipEntry.new("file.zip", "name", "isNotCompared", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry2 = ZipEntry.new("file.zip", "name", "isNotComparedXXX", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry3 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extra", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry4 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 123, 1234, + ZipEntry::DEFLATED, 10000) + entry5 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 1234, + ZipEntry::DEFLATED, 10000) + entry6 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::DEFLATED, 10000) + entry7 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::STORED, 10000) + entry8 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", + "something extraXX", 12, 123, + ZipEntry::STORED, 100000) + + assert_equals(entry1, entry1) + assert_equals(entry1, entry2) + + assert(entry2 != entry3) + assert(entry3 != entry4) + assert(entry4 != entry5) + assert(entry5 != entry6) + assert(entry6 != entry7) + assert(entry7 != entry8) + + assert(entry7 != "hello") + assert(entry7 != 12) + end +end + +module IOizeString + attr_reader :tell + + def read(count = nil) + @tell ||= 0 + count = size unless count + retVal = slice(@tell, count) + @tell += count + return retVal + end + + def seek(index, offset) + @tell ||= 0 + case offset + when IO::SEEK_END + newPos = size + index + when IO::SEEK_SET + newPos = index + when IO::SEEK_CUR + newPos = @tell + index + else + raise "Error in test method IOizeString::seek" + end + if (newPos < 0 || newPos >= size) + raise Errno::EINVAL + else + @tell=newPos + end + end + + def reset + @tell = 0 + end +end + +class ZipLocalEntryTest < RUNIT::TestCase + def test_readLocalEntryHeaderOfFirstTestZipEntry + File.open(TestZipFile::TEST_ZIP3.zipName) { + |file| + entry = ZipEntry.readLocalEntry(file) + + assert_equal("", entry.comment) + # Differs from windows and unix because of CR LF + # assert_equal(480, entry.compressedSize) + # assert_equal(0x2a27930f, entry.crc) + # extra field is 21 bytes long + # probably contains some unix attrutes or something + # disabled: assert_equal(nil, entry.extra) + assert_equal(ZipEntry::DEFLATED, entry.compressionMethod) + assert_equal(TestZipFile::TEST_ZIP3.entryNames[0], entry.name) + assert_equal(File.size(TestZipFile::TEST_ZIP3.entryNames[0]), entry.size) + assert(! entry.isDirectory) + } + end + + def test_readLocalEntryFromNonZipFile + File.open("ziptest.rb") { + |file| + assert_equals(nil, ZipEntry.readLocalEntry(file)) + } + end + + def test_readLocalEntryFromTruncatedZipFile + zipFragment="" + File.open(TestZipFile::TEST_ZIP2.zipName) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes + zipFragment.extend(IOizeString).reset + entry = ZipEntry.new + entry.readLocalEntry(zipFragment) + fail "ZipError expected" + rescue ZipError + end + + def test_writeEntry + entry = ZipEntry.new("file.zip", "entryName", "my little comment", + "thisIsSomeExtraInformation", 100, 987654, + ZipEntry::DEFLATED, 400) + writeToFile("localEntryHeader.bin", "centralEntryHeader.bin", entry) + entryReadLocal, entryReadCentral = readFromFile("localEntryHeader.bin", "centralEntryHeader.bin") + compareLocalEntryHeaders(entry, entryReadLocal) + compareCDirEntryHeaders(entry, entryReadCentral) + end + + private + def compareLocalEntryHeaders(entry1, entry2) + assert_equals(entry1.compressedSize , entry2.compressedSize) + assert_equals(entry1.crc , entry2.crc) + assert_equals(entry1.extra , entry2.extra) + assert_equals(entry1.compressionMethod, entry2.compressionMethod) + assert_equals(entry1.name , entry2.name) + assert_equals(entry1.size , entry2.size) + assert_equals(entry1.localHeaderOffset, entry2.localHeaderOffset) + end + + def compareCDirEntryHeaders(entry1, entry2) + compareLocalEntryHeaders(entry1, entry2) + assert_equals(entry1.comment, entry2.comment) + end + + def writeToFile(localFileName, centralFileName, entry) + File.open(localFileName, "wb") { |f| entry.writeLocalEntry(f) } + File.open(centralFileName, "wb") { |f| entry.writeCDirEntry(f) } + end + + def readFromFile(localFileName, centralFileName) + localEntry = nil + cdirEntry = nil + File.open(localFileName, "rb") { |f| localEntry = ZipEntry.readLocalEntry(f) } + File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.readCDirEntry(f) } + return [localEntry, cdirEntry] + end +end + + +module DecompressorTests + # expects @refText and @decompressor + + def test_readEverything + assert_equals(@refText, @decompressor.read) + end + + def test_readInChunks + chunkSize = 5 + while (decompressedChunk = @decompressor.read(chunkSize)) + assert_equals(@refText.slice!(0, chunkSize), decompressedChunk) + end + assert_equals(0, @refText.size) + end +end + +class InflaterTest < RUNIT::TestCase + include DecompressorTests + + def setup + @file = File.new("file1.txt.deflatedData", "rb") + @refText="" + File.open("file1.txt") { |f| @refText = f.read } + @decompressor = Inflater.new(@file) + end + + def teardown + @file.close + end +end + + +class PassThruDecompressorTest < RUNIT::TestCase + include DecompressorTests + TEST_FILE="file1.txt" + def setup + @file = File.new(TEST_FILE) + @refText="" + File.open(TEST_FILE) { |f| @refText = f.read } + @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) + end + + def teardown + @file.close + end +end + + +module AssertEntry + def assertNextEntry(filename, zis) + assertEntry(filename, zis, zis.getNextEntry.name) + end + + def assertEntry(filename, zis, entryName) + assert_equals(filename, entryName) + assertEntryContentsForStream(filename, zis, entryName) + end + + def assertEntryContentsForStream(filename, zis, entryName) + File.open(filename, "rb") { + |file| + expected = file.read + actual = zis.read + if (expected != actual) + if (expected.length > 400 || actual.length > 400) + zipEntryFilename=entryName+".zipEntry" + File.open(zipEntryFilename, "wb") { |file| file << actual } + fail("File '#{filename}' is different from '#{zipEntryFilename}'") + else + assert_equals(expected, actual) + end + end + } + end + + def AssertEntry.assertContents(filename, aString) + fileContents = "" + File.open(filename, "rb") { |f| fileContents = f.read } + if (fileContents != aString) + if (expected.length > 400 || actual.length > 400) + stringFile = filename + ".other" + File.open(stringFile, "wb") { |f| f << aString } + fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") + else + assert_equals(expected, actual) + end + end + end + + def assertStreamContents(zis, testZipFile) + assert(zis != nil) + testZipFile.entryNames.each { + |entryName| + assertNextEntry(entryName, zis) + } + assert_equals(nil, zis.getNextEntry) + end + + def assertTestZipContents(testZipFile) + ZipInputStream.open(testZipFile.zipName) { + |zis| + assertStreamContents(zis, testZipFile) + } + end + + def assertEntryContents(zipFile, entryName, filename = entryName.to_s) + zis = zipFile.getInputStream(entryName) + assertEntryContentsForStream(filename, zis, entryName) + ensure + zis.close if zis + end +end + + + +class ZipInputStreamTest < RUNIT::TestCase + include AssertEntry + + def test_new + zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zipName) + assertStreamContents(zis, TestZipFile::TEST_ZIP2) + zis.close + end + + def test_openWithBlock + ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { + |zis| + assertStreamContents(zis, TestZipFile::TEST_ZIP2) + } + end + + def test_openWithoutBlock + zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) + assertStreamContents(zis, TestZipFile::TEST_ZIP2) + end + + def test_incompleteReads + ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { + |zis| + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[0], entry.name) + assert zis.gets.length > 0 + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[1], entry.name) + assert_equals(0, entry.size) + assert_equals(nil, zis.gets) + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[2], entry.name) + assert zis.gets.length > 0 + entry = zis.getNextEntry + assert_equals(TestZipFile::TEST_ZIP2.entryNames[3], entry.name) + assert zis.gets.length > 0 + } + end + +end + +class TestFiles + RANDOM_ASCII_FILE1 = "randomAscii1.txt" + RANDOM_ASCII_FILE2 = "randomAscii2.txt" + RANDOM_ASCII_FILE3 = "randomAscii3.txt" + RANDOM_BINARY_FILE1 = "randomBinary1.bin" + RANDOM_BINARY_FILE2 = "randomBinary2.bin" + + EMPTY_TEST_DIR = "emptytestdir" + + ASCII_TEST_FILES = [ RANDOM_ASCII_FILE1, RANDOM_ASCII_FILE2, RANDOM_ASCII_FILE3 ] + BINARY_TEST_FILES = [ RANDOM_BINARY_FILE1, RANDOM_BINARY_FILE2 ] + TEST_DIRECTORIES = [ EMPTY_TEST_DIR ] + TEST_FILES = [ ASCII_TEST_FILES, BINARY_TEST_FILES, EMPTY_TEST_DIR ].flatten! + + def TestFiles.createTestFiles(recreate) + if (recreate || + ! (TEST_FILES.inject(true) { |accum, element| accum && File.exists?(element) })) + + ASCII_TEST_FILES.each_with_index { + |filename, index| + createRandomAscii(filename, 1E4 * (index+1)) + } + + BINARY_TEST_FILES.each_with_index { + |filename, index| + createRandomBinary(filename, 1E4 * (index+1)) + } + + ensureDir(EMPTY_TEST_DIR) + end + end + + private + def TestFiles.createRandomAscii(filename, size) + File.open(filename, "wb") { + |file| + while (file.tell < size) + file << rand + end + } + end + + def TestFiles.createRandomBinary(filename, size) + File.open(filename, "wb") { + |file| + while (file.tell < size) + file << rand.to_a.pack("V") + end + } + end + + def TestFiles.ensureDir(name) + if File.exists?(name) + return if File.stat(name).directory? + File.delete(name) + end + Dir.mkdir(name) + end + +end + +# For representation and creation of +# test data +class TestZipFile + attr_accessor :zipName, :entryNames, :comment + + def initialize(zipName, entryNames, comment = "") + @zipName=zipName + @entryNames=entryNames + @comment = comment + end + + def TestZipFile.createTestZips(recreate) + files = Dir.entries(".") + if (recreate || + ! (files.index(TEST_ZIP1.zipName) && + files.index(TEST_ZIP2.zipName) && + files.index(TEST_ZIP3.zipName) && + files.index(TEST_ZIP4.zipName) && + files.index("empty.txt") && + files.index("short.txt") && + files.index("longAscii.txt") && + files.index("longBinary.bin") )) + raise "failed to create test zip '#{TEST_ZIP1.zipName}'" unless + system("zip #{TEST_ZIP1.zipName} ziptest.rb") + raise "failed to remove entry from '#{TEST_ZIP1.zipName}'" unless + system("zip #{TEST_ZIP1.zipName} -d ziptest.rb") + + File.open("empty.txt", "w") {} + + File.open("short.txt", "w") { |file| file << "ABCDEF" } + ziptestTxt="" + File.open("ziptest.rb") { |file| ziptestTxt=file.read } + File.open("longAscii.txt", "w") { + |file| + while (file.tell < 1E5) + file << ziptestTxt + end + } + + testBinaryPattern="" + File.open("empty.zip") { |file| testBinaryPattern=file.read } + testBinaryPattern *= 4 + + File.open("longBinary.bin", "wb") { + |file| + while (file.tell < 3E5) + file << testBinaryPattern << rand + end + } + raise "failed to create test zip '#{TEST_ZIP2.zipName}'" unless + system("zip #{TEST_ZIP2.zipName} #{TEST_ZIP2.entryNames.join(' ')}") + + # without bash system interprets everything after echo as parameters to + # echo including | zip -z ... + raise "failed to add comment to test zip '#{TEST_ZIP2.zipName}'" unless + system("bash -c \"echo #{TEST_ZIP2.comment} | zip -z #{TEST_ZIP2.zipName}\"") + + raise "failed to create test zip '#{TEST_ZIP3.zipName}'" unless + system("zip #{TEST_ZIP3.zipName} #{TEST_ZIP3.entryNames.join(' ')}") + + raise "failed to create test zip '#{TEST_ZIP4.zipName}'" unless + system("zip #{TEST_ZIP4.zipName} #{TEST_ZIP4.entryNames.join(' ')}") + end + rescue + raise $!.to_s + + "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" + + "to create test data. If you don't have it you can download\n" + + "the necessary test files at http://sf.net/projects/rubyzip." + end + + TEST_ZIP1 = TestZipFile.new("empty.zip", []) + TEST_ZIP2 = TestZipFile.new("4entry.zip", %w{ longAscii.txt empty.txt short.txt longBinary.bin}, + "my zip comment") + TEST_ZIP3 = TestZipFile.new("test1.zip", %w{ file1.txt }) + TEST_ZIP4 = TestZipFile.new("zipWithDir.zip", [ "file1.txt", + TestFiles::EMPTY_TEST_DIR]) +end + + +class AbstractOutputStreamTest < RUNIT::TestCase + class TestOutputStream + include AbstractOutputStream + + attr_accessor :buffer + + def initialize + @buffer = "" + end + + def << (data) + @buffer << data + self + end + end + + def setup + @outputStream = TestOutputStream.new + + @origCommaSep = $, + @origOutputSep = $\ + end + + def teardown + $, = @origCommaSep + $\ = @origOutputSep + end + + def test_write + count = @outputStream.write("a little string") + assert_equals("a little string", @outputStream.buffer) + assert_equals("a little string".length, count) + + count = @outputStream.write(". a little more") + assert_equals("a little string. a little more", @outputStream.buffer) + assert_equals(". a little more".length, count) + end + + def test_print + $\ = nil # record separator set to nil + @outputStream.print("hello") + assert_equals("hello", @outputStream.buffer) + + @outputStream.print(" world.") + assert_equals("hello world.", @outputStream.buffer) + + @outputStream.print(" You ok ", "out ", "there?") + assert_equals("hello world. You ok out there?", @outputStream.buffer) + + $\ = "\n" + @outputStream.print + assert_equals("hello world. You ok out there?\n", @outputStream.buffer) + + @outputStream.print("I sure hope so!") + assert_equals("hello world. You ok out there?\nI sure hope so!\n", @outputStream.buffer) + + $, = "X" + @outputStream.buffer = "" + @outputStream.print("monkey", "duck", "zebra") + assert_equals("monkeyXduckXzebra\n", @outputStream.buffer) + + $\ = nil + @outputStream.buffer = "" + @outputStream.print(20) + assert_equals("20", @outputStream.buffer) + end + + def test_printf + @outputStream.printf("%d %04x", 123, 123) + assert_equals("123 007b", @outputStream.buffer) + end + + def test_putc + @outputStream.putc("A") + assert_equals("A", @outputStream.buffer) + @outputStream.putc(65) + assert_equals("AA", @outputStream.buffer) + end + + def test_puts + @outputStream.puts + assert_equals("\n", @outputStream.buffer) + + @outputStream.puts("hello", "world") + assert_equals("\nhello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts("hello\n", "world\n") + assert_equals("hello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(["hello\n", "world\n"]) + assert_equals("hello\nworld\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(["hello\n", "world\n"], "bingo") + assert_equals("hello\nworld\nbingo\n", @outputStream.buffer) + + @outputStream.buffer = "" + @outputStream.puts(16, 20, 50, "hello") + assert_equals("16\n20\n50\nhello\n", @outputStream.buffer) + end +end + + +module CrcTest + def runCrcTest(compressorClass) + str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." + fakeOut = AbstractOutputStreamTest::TestOutputStream.new + + deflater = compressorClass.new(fakeOut) + deflater << str + assert_equals(0x919920fc, deflater.crc) + end +end + + + +class PassThruCompressorTest < RUNIT::TestCase + include CrcTest + + def test_size + File.open("dummy.txt", "wb") { + |file| + compressor = PassThruCompressor.new(file) + + assert_equals(0, compressor.size) + + t1 = "hello world" + t2 = "" + t3 = "bingo" + + compressor << t1 + assert_equals(compressor.size, t1.size) + + compressor << t2 + assert_equals(compressor.size, t1.size + t2.size) + + compressor << t3 + assert_equals(compressor.size, t1.size + t2.size + t3.size) + } + end + + def test_crc + runCrcTest(PassThruCompressor) + end +end + +class DeflaterTest < RUNIT::TestCase + include CrcTest + + def test_outputOperator + txt = loadFile("ziptest.rb") + deflate(txt, "deflatertest.bin") + inflatedTxt = inflate("deflatertest.bin") + assert_equals(txt, inflatedTxt) + end + + private + def loadFile(fileName) + txt = nil + File.open(fileName, "rb") { |f| txt = f.read } + end + + def deflate(data, fileName) + File.open(fileName, "wb") { + |file| + deflater = Deflater.new(file) + deflater << data + deflater.finish + assert_equals(deflater.size, data.size) + file << "trailing data for zlib with -MAX_WBITS" + } + end + + def inflate(fileName) + txt = nil + File.open(fileName, "rb") { + |file| + inflater = Inflater.new(file) + txt = inflater.read + } + end + + def test_crc + runCrcTest(Deflater) + end +end + +class ZipOutputStreamTest < RUNIT::TestCase + include AssertEntry + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zipName = "output.zip" + + def test_new + zos = ZipOutputStream.new(TEST_ZIP.zipName) + zos.comment = TEST_ZIP.comment + writeTestZip(zos) + zos.close + assertTestZipContents(TEST_ZIP) + end + + def test_open + ZipOutputStream.open(TEST_ZIP.zipName) { + |zos| + zos.comment = TEST_ZIP.comment + writeTestZip(zos) + } + assertTestZipContents(TEST_ZIP) + end + + def test_writingToClosedStream + assertIOErrorInClosedStream { |zos| zos << "hello world" } + assertIOErrorInClosedStream { |zos| zos.puts "hello world" } + assertIOErrorInClosedStream { |zos| zos.write "hello world" } + end + + def test_cannotOpenFile + name = TestFiles::EMPTY_TEST_DIR + begin + zos = ZipOutputStream.open(name) + rescue Exception + assert($!.kind_of?(Errno::EISDIR) || # Linux + $!.kind_of?(Errno::EEXIST) || # Windows/cygwin + $!.kind_of?(Errno::EACCES), # Windows + "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.type}") + end + end + + def assertIOErrorInClosedStream + assert_exception(IOError) { + zos = ZipOutputStream.new("test_putOnClosedStream.zip") + zos.close + yield zos + } + end + + def writeTestZip(zos) + TEST_ZIP.entryNames.each { + |entryName| + zos.putNextEntry(entryName) + File.open(entryName, "rb") { |f| zos.write(f.read) } + } + end +end + + + +module Enumerable + def compareEnumerables(otherEnumerable) + otherAsArray = otherEnumerable.to_a + index=0 + each_with_index { + |element, index| + return false unless yield(element, otherAsArray[index]) + } + return index+1 == otherAsArray.size + end +end + + +class ZipCentralDirectoryEntryTest < RUNIT::TestCase + + def test_readFromStream + File.open("testDirectory.bin", "rb") { + |file| + entry = ZipEntry.readCDirEntry(file) + + assert_equals("longAscii.txt", entry.name) + assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) + assert_equals(106490, entry.size) + assert_equals(3784, entry.compressedSize) + assert_equals(0xfcd1799c, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals("empty.txt", entry.name) + assert_equals(ZipEntry::STORED, entry.compressionMethod) + assert_equals(0, entry.size) + assert_equals(0, entry.compressedSize) + assert_equals(0x0, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals("short.txt", entry.name) + assert_equals(ZipEntry::STORED, entry.compressionMethod) + assert_equals(6, entry.size) + assert_equals(6, entry.compressedSize) + assert_equals(0xbb76fe69, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals("longBinary.bin", entry.name) + assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) + assert_equals(1000024, entry.size) + assert_equals(70847, entry.compressedSize) + assert_equals(0x10da7d59, entry.crc) + assert_equals("", entry.comment) + + entry = ZipEntry.readCDirEntry(file) + assert_equals(nil, entry) +# Fields that are not check by this test: +# version made by 2 bytes +# version needed to extract 2 bytes +# general purpose bit flag 2 bytes +# last mod file time 2 bytes +# last mod file date 2 bytes +# compressed size 4 bytes +# uncompressed size 4 bytes +# disk number start 2 bytes +# internal file attributes 2 bytes +# external file attributes 4 bytes +# relative offset of local header 4 bytes + +# file name (variable size) +# extra field (variable size) +# file comment (variable size) + + } + end + + def test_ReadEntryFromTruncatedZipFile + fragment="" + File.open("testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes + fragment.extend(IOizeString) + entry = ZipEntry.new + entry.readCDirEntry(fragment) + fail "ZipError expected" + rescue ZipError + end + +end + +class ZipCentralDirectoryTest < RUNIT::TestCase + + def test_readFromStream + File.open(TestZipFile::TEST_ZIP2.zipName, "rb") { + |zipFile| + cdir = ZipCentralDirectory.readFromStream(zipFile) + + assert_equals(TestZipFile::TEST_ZIP2.entryNames.size, cdir.size) + assert(cdir.compareEnumerables(TestZipFile::TEST_ZIP2.entryNames) { + |cdirEntry, testEntryName| + cdirEntry.name == testEntryName + }) + assert_equals(TestZipFile::TEST_ZIP2.comment, cdir.comment) + } + end + + def test_readFromInvalidStream + File.open("ziptest.rb", "rb") { + |zipFile| + cdir = ZipCentralDirectory.new + cdir.readFromStream(zipFile) + } + fail "ZipError expected!" + rescue ZipError + end + + def test_ReadFromTruncatedZipFile + fragment="" + File.open("testDirectory.bin") { |f| fragment = f.read } + fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete + fragment.extend(IOizeString) + entry = ZipCentralDirectory.new + entry.readFromStream(fragment) + fail "ZipError expected" + rescue ZipError + end + + def test_writeToStream + entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] + cdir = ZipCentralDirectory.new(entries, "my zip comment") + File.open("cdirtest.bin", "wb") { |f| cdir.writeToStream(f) } + cdirReadback = ZipCentralDirectory.new + File.open("cdirtest.bin", "rb") { |f| cdirReadback.readFromStream(f) } + + assert_equals(cdir.entries, cdirReadback.entries) + end + + def test_equality + cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + assert_equals(cdir1, cdir1) + assert_equals(cdir1, cdir2) + + assert(cdir1 != cdir3) + assert(cdir2 != cdir3) + assert(cdir2 != cdir3) + assert(cdir3 != cdir4) + + assert(cdir3 != "hello") + end +end + + +class BasicZipFileTest < RUNIT::TestCase + include AssertEntry + + def setup + @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zipName) + @testEntryNameIndex=0 + end + + def nextTestEntryName + retVal=TestZipFile::TEST_ZIP2.entryNames[@testEntryNameIndex] + @testEntryNameIndex+=1 + return retVal + end + + def test_entries + assert_equals(TestZipFile::TEST_ZIP2.entryNames, @zipFile.entries.map {|e| e.name} ) + end + + def test_each + @zipFile.each { + |entry| + assert_equals(nextTestEntryName, entry.name) + } + assert_equals(4, @testEntryNameIndex) + end + + def test_foreach + ZipFile.foreach(TestZipFile::TEST_ZIP2.zipName) { + |entry| + assert_equals(nextTestEntryName, entry.name) + } + assert_equals(4, @testEntryNameIndex) + end + + def test_getInputStream + @zipFile.each { + |entry| + assertEntry(nextTestEntryName, @zipFile.getInputStream(entry), + entry.name) + } + assert_equals(4, @testEntryNameIndex) + end + + def test_getInputStreamBlock + fileAndEntryName = @zipFile.entries.first.name + @zipFile.getInputStream(fileAndEntryName) { + |zis| + assertEntryContentsForStream(fileAndEntryName, + zis, + fileAndEntryName) + } + end +end + +class CommonZipFileFixture < RUNIT::TestCase + include AssertEntry + + EMPTY_FILENAME = "emptyZipFile.zip" + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zipName = "4entry_copy.zip" + + def setup + File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) + File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) + end +end + +class ZipFileTest < CommonZipFileFixture + + def test_createFromScratch + comment = "a short comment" + + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.comment = comment + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equals(comment, zfRead.comment) + assert_equals(0, zfRead.entries.length) + end + + def test_add + srcFile = "ziptest.rb" + entryName = "newEntryName.rb" + assert(File.exists? srcFile) + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.add(entryName, srcFile) + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equals("", zfRead.comment) + assert_equals(1, zfRead.entries.length) + assert_equals(entryName, zfRead.entries.first.name) + AssertEntry.assertContents(srcFile, + zfRead.getInputStream(entryName) { |zis| zis.read }) + end + + def test_addExistingEntryName + assert_exception(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.add(zf.entries.first.name, "ziptest.rb") + } + } + end + + def test_addExistingEntryNameReplace + gotCalled = false + replacedEntry = nil + ZipFile.open(TEST_ZIP.zipName) { + |zf| + replacedEntry = zf.entries.first.name + zf.add(replacedEntry, "ziptest.rb") { gotCalled = true; true } + } + assert(gotCalled) + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assertContains(zf, replacedEntry, "ziptest.rb") + } + end + + def test_addDirectory + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) + } + ZipFile.open(TEST_ZIP.zipName) { + |zf| + dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } + assert(dirEntry.isDirectory) + } + end + + def test_remove + entryToRemove, *remainingEntries = TEST_ZIP.entryNames + + File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) + + zf = ZipFile.new(TEST_ZIP.zipName) + assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) + zf.remove(entryToRemove) + assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equals(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zipName) + assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equals(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) + zfRead.close + end + + + def test_rename + entryToRename, *remainingEntries = TEST_ZIP.entryNames + + zf = ZipFile.new(TEST_ZIP.zipName) + assert(zf.entries.map { |e| e.name }.include? entryToRename) + + newName = "changed name" + assert(! zf.entries.map { |e| e.name }.include?(newName)) + + zf.rename(entryToRename, newName) + assert(zf.entries.map { |e| e.name }.include? newName) + + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zipName) + assert(zfRead.entries.map { |e| e.name }.include? newName) + zfRead.close + end + + def test_renameToExistingEntry + oldEntries = nil + ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } + + assert_exception(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.rename(zf.entries[0], zf.entries[1].name) + } + } + + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assert_equals(oldEntries.map{ |e| e.name }, zf.entries.map{ |e| e.name }) + } + end + + def test_renameToExistingEntryOverwrite + oldEntries = nil + ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } + + gotCalled = false + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } + } + + assert(gotCalled) + oldEntries.delete_at(0) + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assert_equals(oldEntries.map{ |e| e.name }, + zf.entries.map{ |e| e.name }) + } + end + + def test_renameNonEntry + nonEntry = "bogusEntry" + targetEntry = "targetEntryName" + zf = ZipFile.new(TEST_ZIP.zipName) + assert(! zf.entries.include?(nonEntry)) + assert_exception(ZipNoSuchEntryError) { + zf.rename(nonEntry, targetEntry) + } + zf.commit + assert(! zf.entries.include?(targetEntry)) + ensure + zf.close + end + + def test_renameEntryToExistingEntry + entry1, entry2, *remaining = TEST_ZIP.entryNames + zf = ZipFile.new(TEST_ZIP.zipName) + assert_exception(ZipEntryExistsError) { + zf.rename(entry1, entry2) + } + ensure + zf.close + end + + def test_replace + unchangedEntries = TEST_ZIP.entryNames.dup + entryToReplace = unchangedEntries.delete_at(2) + newEntrySrcFilename = "ziptest.rb" + + zf = ZipFile.new(TEST_ZIP.zipName) + zf.replace(entryToReplace, newEntrySrcFilename) + + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zipName) + AssertEntry::assertContents(newEntrySrcFilename, + zfRead.getInputStream(entryToReplace) { |is| is.read }) + zfRead.close + end + + def test_replaceNonEntry + entryToReplace = "nonExistingEntryname" + ZipFile.open(TEST_ZIP.zipName) { + |zf| + assert_exception(ZipNoSuchEntryError) { + zf.replace(entryToReplace, "ziptest.rb") + } + } + end + + def test_commit + newName = "renamedFirst" + zf = ZipFile.new(TEST_ZIP.zipName) + oldName = zf.entries.first + zf.rename(oldName, newName) + zf.commit + + zfRead = ZipFile.new(TEST_ZIP.zipName) + assert(zfRead.entries.detect { |e| e.name == newName } != nil) + assert(zfRead.entries.detect { |e| e.name == oldName } == nil) + zfRead.close + + zf.close + end + + # This test tests that after commit, you + # can delete the file you used to add the entry to the zip file + # with + def test_commitUseZipEntry + File.copy(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") + zf = ZipFile.open(TEST_ZIP.zipName) + zf.add("okToDelete.txt", "okToDelete.txt") + assertContains(zf, "okToDelete.txt") + zf.commit + File.move("okToDelete.txt", "okToDeleteMoved.txt") + assertContains(zf, "okToDelete.txt", "okToDeleteMoved.txt") + end + +# def test_close +# zf = ZipFile.new(TEST_ZIP.zipName) +# zf.close +# assert_exception(IOError) { +# zf.extract(TEST_ZIP.entryNames.first, "hullubullu") +# } +# end + + def test_compound1 + renamedName = "renamedName" + originalEntries = [] + begin + zf = ZipFile.new(TEST_ZIP.zipName) + originalEntries = zf.entries.dup + + assertNotContains(zf, TestFiles::RANDOM_ASCII_FILE1) + zf.add(TestFiles::RANDOM_ASCII_FILE1, + TestFiles::RANDOM_ASCII_FILE1) + assertContains(zf, TestFiles::RANDOM_ASCII_FILE1) + + zf.rename(zf.entries[0], renamedName) + assertContains(zf, renamedName) + + TestFiles::BINARY_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assertContains(zf, filename) + } + + assertContains(zf, originalEntries.last.to_s) + zf.remove(originalEntries.last.to_s) + assertNotContains(zf, originalEntries.last.to_s) + + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zipName) + assertContains(zfRead, TestFiles::RANDOM_ASCII_FILE1) + assertContains(zfRead, renamedName) + TestFiles::BINARY_TEST_FILES.each { + |filename| + assertContains(zfRead, filename) + } + assertNotContains(zfRead, originalEntries.last.to_s) + ensure + zfRead.close + end + end + + def test_compound2 + begin + zf = ZipFile.new(TEST_ZIP.zipName) + originalEntries = zf.entries.dup + + originalEntries.each { + |entry| + zf.remove(entry) + assertNotContains(zf, entry) + } + assert(zf.entries.empty?) + + TestFiles::ASCII_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assertContains(zf, filename) + } + assert_equals(zf.entries.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) + + zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") + assertNotContains(zf, TestFiles::ASCII_TEST_FILES[0]) + assertContains(zf, "newName") + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zipName) + asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup + asciiTestFiles.shift + asciiTestFiles.each { + |filename| + assertContains(zf, filename) + } + + assertContains(zf, "newName") + ensure + zfRead.close + end + end + + private + def assertContains(zf, entryName, filename = entryName) + assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") + assertEntryContents(zf, entryName, filename) if File.exists?(filename) + end + + def assertNotContains(zf, entryName) + assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") + end +end + +class ZipFileExtractTest < CommonZipFileFixture + EXTRACTED_FILENAME = "extEntry" + ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entryNames.reverse + + def setup + super + File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) + end + + def test_extract + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) + + assert(File.exists? EXTRACTED_FILENAME) + AssertEntry::assertContents(EXTRACTED_FILENAME, + zf.getInputStream(ENTRY_TO_EXTRACT) { |is| is.read }) + } + end + + def test_extractExists + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + assert_exception(ZipDestinationFileExistsError) { + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) + } + } + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert_equals(writtenText, f.read) + } + end + + def test_extractExistsOverwrite + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + gotCalled = false + ZipFile.open(TEST_ZIP.zipName) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) { gotCalled = true; true } + } + + assert(gotCalled) + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert(writtenText != f.read) + } + end + + def test_extractNonEntry + zf = ZipFile.new(TEST_ZIP.zipName) + assert_exception(ZipNoSuchEntryError) { zf.extract("nonExistingEntry", "nonExistingEntry") } + ensure + zf.close if zf + end + + def test_extractNonEntry2 + outFile = "outfile" + assert_exception(ZipNoSuchEntryError) { + zf = ZipFile.new(TEST_ZIP.zipName) + nonEntry = "hotdog-diddelidoo" + assert(! zf.entries.include?(nonEntry)) + zf.extract(nonEntry, outFile) + zf.close + } + assert(! File.exists?(outFile)) + end + +end + +class ZipFileExtractDirectoryTest < CommonZipFileFixture + TEST_OUT_NAME = "emptyOutDir" + + def openZip(&aProc) + assert(aProc != nil) + ZipFile.open(TestZipFile::TEST_ZIP4.zipName, &aProc) + end + + def extractTestDir(&aProc) + openZip { + |zf| + zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) + } + end + + def setup + super + + Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME + File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME + end + + def test_extractDirectory + extractTestDir + assert(File.directory? TEST_OUT_NAME) + end + + def test_extractDirectoryExistsAsDir + Dir.mkdir TEST_OUT_NAME + extractTestDir + assert(File.directory? TEST_OUT_NAME) + end + + def test_extractDirectoryExistsAsFile + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + assert_exception(ZipDestinationFileExistsError) { extractTestDir } + end + + def test_extractDirectoryExistsAsFileOverwrite + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + gotCalled = false + extractTestDir { + |entry, destPath| + gotCalled = true + assert_equals(TEST_OUT_NAME, destPath) + assert(entry.isDirectory) + true + } + assert(gotCalled) + assert(File.directory? TEST_OUT_NAME) + end +end + + +TestFiles::createTestFiles(ARGV.index("recreate") != nil || + ARGV.index("recreateonly") != nil) +TestZipFile::createTestZips(ARGV.index("recreate") != nil || + ARGV.index("recreateonly") != nil) +exit if ARGV.index("recreateonly") != nil + +#require 'runit/cui/testrunner' +#RUNIT::CUI::TestRunner.run(ZipFileTest.suite) + +# Copyright (C) 2002 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/notzippedruby.rb b/vendor/plugins/rubyzip-0.9.1/test/data/notzippedruby.rb new file mode 100755 index 00000000..036d25e9 --- /dev/null +++ b/vendor/plugins/rubyzip-0.9.1/test/data/notzippedruby.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +class NotZippedRuby + def returnTrue + true + end +end diff --git a/vendor/plugins/rubyzip-0.9.1/test/data/rubycode.zip b/vendor/plugins/rubyzip-0.9.1/test/data/rubycode.zip new file mode 100644 index 0000000000000000000000000000000000000000..8a68560e638088df79cd3bf68973013e5421a138 GIT binary patch literal 617 zcmWIWW@Zs#U|`^2*ef_ihj&#AyDyLz4#a#6q6}4;1qG=oMWsoVhI&Owp&^_M%*Qqz zusOT^fK6xx3qz{5pk{E`6@6X3Oa7XQ6aq^YrF$udom<7Oc6lM6}=RDLT(4ef%v0bc!4h^8y1W zF(|b-zqBYhRj;I?1ROS-fZ>81HlI)HpFDBKJKSTf$2lEFL!-|k4N6D3GG|(@>;ig~ zkx85xSJ3kU?ONSo@jDa*vNRhlwoTaWDdGpXl_l7YYQ5=Tcy zrKJm6mDneFo%MeBlu4K&z?+dtoEeupd4aBFU| zA4oGE-6BAHIKZ|xRwx*G19_o9EQG2%Ei)(8(9jU>1duaeP7pnEkh8&nhxJ1B@*nf6 zHmC3$*O)cQq4xsI(%Y*)R4Bgs(PD5T$>)hP`&4@^Uu)sOHmP%+3H(JZ*Tj$A;8upFq4-#D)-_!rOo zz!GmJQzzjm|Ki#?1CQIcEI2yj#PYsPrf+proq4ajTwpGhS;Rkm%D0A)zZ8EJcxCc?6yCYU#-$Z+V)|>29qyQgrU)ZKDMG0hH6=mA&`2McB8;FZB0rGW z^aM>2Fx?OquacTrK4QqJHwn1X`eeKbzSH2tf ze~@$y7Fr=VYr&WAVTm&&(+zp%a4xxL^+-`ycmB2e%C1^V4FwljSkE~#@%Fi*%OZ!K zJ;>Ryr7T6HD!}#Rvky$Dk$dY_Uj ZipEntry.new("zf.zip", "a"))) + assert_equal(1, (ZipEntry.new("zf.zip", "b") <=> ZipEntry.new("zf.zip", "a"))) + assert_equal(-1, (ZipEntry.new("zf.zip", "a") <=> ZipEntry.new("zf.zip", "b"))) + + entries = [ + ZipEntry.new("zf.zip", "5"), + ZipEntry.new("zf.zip", "1"), + ZipEntry.new("zf.zip", "3"), + ZipEntry.new("zf.zip", "4"), + ZipEntry.new("zf.zip", "0"), + ZipEntry.new("zf.zip", "2") + ] + + entries.sort! + assert_equal("0", entries[0].to_s) + assert_equal("1", entries[1].to_s) + assert_equal("2", entries[2].to_s) + assert_equal("3", entries[3].to_s) + assert_equal("4", entries[4].to_s) + assert_equal("5", entries[5].to_s) + end + + def test_parentAsString + entry1 = ZipEntry.new("zf.zip", "aa") + entry2 = ZipEntry.new("zf.zip", "aa/") + entry3 = ZipEntry.new("zf.zip", "aa/bb") + entry4 = ZipEntry.new("zf.zip", "aa/bb/") + entry5 = ZipEntry.new("zf.zip", "aa/bb/cc") + entry6 = ZipEntry.new("zf.zip", "aa/bb/cc/") + + assert_equal(nil, entry1.parent_as_string) + assert_equal(nil, entry2.parent_as_string) + assert_equal("aa/", entry3.parent_as_string) + assert_equal("aa/", entry4.parent_as_string) + assert_equal("aa/bb/", entry5.parent_as_string) + assert_equal("aa/bb/", entry6.parent_as_string) + end + + def test_entry_name_cannot_start_with_slash + assert_raise(ZipEntryNameError) { ZipEntry.new("zf.zip", "/hej/der") } + end +end + +module IOizeString + attr_reader :tell + + def read(count = nil) + @tell ||= 0 + count = size unless count + retVal = slice(@tell, count) + @tell += count + return retVal + end + + def seek(index, offset) + @tell ||= 0 + case offset + when IO::SEEK_END + newPos = size + index + when IO::SEEK_SET + newPos = index + when IO::SEEK_CUR + newPos = @tell + index + else + raise "Error in test method IOizeString::seek" + end + if (newPos < 0 || newPos >= size) + raise Errno::EINVAL + else + @tell=newPos + end + end + + def reset + @tell = 0 + end +end + +class ZipLocalEntryTest < Test::Unit::TestCase + def test_read_local_entryHeaderOfFirstTestZipEntry + File.open(TestZipFile::TEST_ZIP3.zip_name, "rb") { + |file| + entry = ZipEntry.read_local_entry(file) + + assert_equal("", entry.comment) + # Differs from windows and unix because of CR LF + # assert_equal(480, entry.compressed_size) + # assert_equal(0x2a27930f, entry.crc) + # extra field is 21 bytes long + # probably contains some unix attrutes or something + # disabled: assert_equal(nil, entry.extra) + assert_equal(ZipEntry::DEFLATED, entry.compression_method) + assert_equal(TestZipFile::TEST_ZIP3.entry_names[0], entry.name) + assert_equal(File.size(TestZipFile::TEST_ZIP3.entry_names[0]), entry.size) + assert(! entry.is_directory) + } + end + + def test_readDateTime + File.open("data/rubycode.zip", "rb") { + |file| + entry = ZipEntry.read_local_entry(file) + assert_equal("zippedruby1.rb", entry.name) + assert_equal(Time.at(1019261638), entry.time) + } + end + + def test_read_local_entryFromNonZipFile + File.open("data/file2.txt") { + |file| + assert_equal(nil, ZipEntry.read_local_entry(file)) + } + end + + def test_read_local_entryFromTruncatedZipFile + zipFragment="" + File.open(TestZipFile::TEST_ZIP2.zip_name) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes + zipFragment.extend(IOizeString).reset + entry = ZipEntry.new + entry.read_local_entry(zipFragment) + fail "ZipError expected" + rescue ZipError + end + + def test_writeEntry + entry = ZipEntry.new("file.zip", "entryName", "my little comment", + "thisIsSomeExtraInformation", 100, 987654, + ZipEntry::DEFLATED, 400) + write_to_file("localEntryHeader.bin", "centralEntryHeader.bin", entry) + entryReadLocal, entryReadCentral = read_from_file("localEntryHeader.bin", "centralEntryHeader.bin") + compare_local_entry_headers(entry, entryReadLocal) + compare_c_dir_entry_headers(entry, entryReadCentral) + end + + private + def compare_local_entry_headers(entry1, entry2) + assert_equal(entry1.compressed_size , entry2.compressed_size) + assert_equal(entry1.crc , entry2.crc) + assert_equal(entry1.extra , entry2.extra) + assert_equal(entry1.compression_method, entry2.compression_method) + assert_equal(entry1.name , entry2.name) + assert_equal(entry1.size , entry2.size) + assert_equal(entry1.localHeaderOffset, entry2.localHeaderOffset) + end + + def compare_c_dir_entry_headers(entry1, entry2) + compare_local_entry_headers(entry1, entry2) + assert_equal(entry1.comment, entry2.comment) + end + + def write_to_file(localFileName, centralFileName, entry) + File.open(localFileName, "wb") { |f| entry.write_local_entry(f) } + File.open(centralFileName, "wb") { |f| entry.write_c_dir_entry(f) } + end + + def read_from_file(localFileName, centralFileName) + localEntry = nil + cdirEntry = nil + File.open(localFileName, "rb") { |f| localEntry = ZipEntry.read_local_entry(f) } + File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.read_c_dir_entry(f) } + return [localEntry, cdirEntry] + end +end + + +module DecompressorTests + # expects @refText, @refLines and @decompressor + + TEST_FILE="data/file1.txt" + + def setup + @refText="" + File.open(TEST_FILE) { |f| @refText = f.read } + @refLines = @refText.split($/) + end + + def test_readEverything + assert_equal(@refText, @decompressor.sysread) + end + + def test_readInChunks + chunkSize = 5 + while (decompressedChunk = @decompressor.sysread(chunkSize)) + assert_equal(@refText.slice!(0, chunkSize), decompressedChunk) + end + assert_equal(0, @refText.size) + end + + def test_mixingReadsAndProduceInput + # Just some preconditions to make sure we have enough data for this test + assert(@refText.length > 1000) + assert(@refLines.length > 40) + + + assert_equal(@refText[0...100], @decompressor.sysread(100)) + + assert(! @decompressor.input_finished?) + buf = @decompressor.produce_input + assert_equal(@refText[100...(100+buf.length)], buf) + end +end + +class InflaterTest < Test::Unit::TestCase + include DecompressorTests + + def setup + super + @file = File.new("data/file1.txt.deflatedData", "rb") + @decompressor = Inflater.new(@file) + end + + def teardown + @file.close + end +end + + +class PassThruDecompressorTest < Test::Unit::TestCase + include DecompressorTests + def setup + super + @file = File.new(TEST_FILE) + @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) + end + + def teardown + @file.close + end +end + + +module AssertEntry + def assert_next_entry(filename, zis) + assert_entry(filename, zis, zis.get_next_entry.name) + end + + def assert_entry(filename, zis, entryName) + assert_equal(filename, entryName) + assert_entryContentsForStream(filename, zis, entryName) + end + + def assert_entryContentsForStream(filename, zis, entryName) + File.open(filename, "rb") { + |file| + expected = file.read + actual = zis.read + if (expected != actual) + if ((expected && actual) && (expected.length > 400 || actual.length > 400)) + zipEntryFilename=entryName+".zipEntry" + File.open(zipEntryFilename, "wb") { |file| file << actual } + fail("File '#{filename}' is different from '#{zipEntryFilename}'") + else + assert_equal(expected, actual) + end + end + } + end + + def AssertEntry.assert_contents(filename, aString) + fileContents = "" + File.open(filename, "rb") { |f| fileContents = f.read } + if (fileContents != aString) + if (fileContents.length > 400 || aString.length > 400) + stringFile = filename + ".other" + File.open(stringFile, "wb") { |f| f << aString } + fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") + else + assert_equal(fileContents, aString) + end + end + end + + def assert_stream_contents(zis, testZipFile) + assert(zis != nil) + testZipFile.entry_names.each { + |entryName| + assert_next_entry(entryName, zis) + } + assert_equal(nil, zis.get_next_entry) + end + + def assert_test_zip_contents(testZipFile) + ZipInputStream.open(testZipFile.zip_name) { + |zis| + assert_stream_contents(zis, testZipFile) + } + end + + def assert_entryContents(zipFile, entryName, filename = entryName.to_s) + zis = zipFile.get_input_stream(entryName) + assert_entryContentsForStream(filename, zis, entryName) + ensure + zis.close if zis + end +end + + + +class ZipInputStreamTest < Test::Unit::TestCase + include AssertEntry + + def test_new + zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zip_name) + assert_stream_contents(zis, TestZipFile::TEST_ZIP2) + assert_equal(true, zis.eof?) + zis.close + end + + def test_openWithBlock + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + assert_stream_contents(zis, TestZipFile::TEST_ZIP2) + assert_equal(true, zis.eof?) + } + end + + def test_openWithoutBlock + zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) + assert_stream_contents(zis, TestZipFile::TEST_ZIP2) + end + + def test_incompleteReads + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + entry = zis.get_next_entry # longAscii.txt + assert_equal(false, zis.eof?) + assert_equal(TestZipFile::TEST_ZIP2.entry_names[0], entry.name) + assert zis.gets.length > 0 + assert_equal(false, zis.eof?) + entry = zis.get_next_entry # empty.txt + assert_equal(TestZipFile::TEST_ZIP2.entry_names[1], entry.name) + assert_equal(0, entry.size) + assert_equal(nil, zis.gets) + assert_equal(true, zis.eof?) + entry = zis.get_next_entry # empty_chmod640.txt + assert_equal(TestZipFile::TEST_ZIP2.entry_names[2], entry.name) + assert_equal(0, entry.size) + assert_equal(nil, zis.gets) + assert_equal(true, zis.eof?) + entry = zis.get_next_entry # short.txt + assert_equal(TestZipFile::TEST_ZIP2.entry_names[3], entry.name) + assert zis.gets.length > 0 + entry = zis.get_next_entry # longBinary.bin + assert_equal(TestZipFile::TEST_ZIP2.entry_names[4], entry.name) + assert zis.gets.length > 0 + } + end + + def test_rewind + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + e = zis.get_next_entry + assert_equal(TestZipFile::TEST_ZIP2.entry_names[0], e.name) + + # Do a little reading + buf = "" + buf << zis.read(100) + buf << (zis.gets || "") + buf << (zis.gets || "") + assert_equal(false, zis.eof?) + + zis.rewind + + buf2 = "" + buf2 << zis.read(100) + buf2 << (zis.gets || "") + buf2 << (zis.gets || "") + + assert_equal(buf, buf2) + + zis.rewind + assert_equal(false, zis.eof?) + + assert_entry(e.name, zis, e.name) + } + end + + def test_mix_read_and_gets + ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { + |zis| + e = zis.get_next_entry + assert_equal("#!/usr/bin/env ruby", zis.gets.chomp) + assert_equal(false, zis.eof?) + assert_equal("", zis.gets.chomp) + assert_equal(false, zis.eof?) + assert_equal("$VERBOSE =", zis.read(10)) + assert_equal(false, zis.eof?) + } + end + +end + + +module CrcTest + + class TestOutputStream + include IOExtras::AbstractOutputStream + + attr_accessor :buffer + + def initialize + @buffer = "" + end + + def << (data) + @buffer << data + self + end + end + + def run_crc_test(compressorClass) + str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." + fakeOut = TestOutputStream.new + + deflater = compressorClass.new(fakeOut) + deflater << str + assert_equal(0x919920fc, deflater.crc) + end +end + + + +class PassThruCompressorTest < Test::Unit::TestCase + include CrcTest + + def test_size + File.open("dummy.txt", "wb") { + |file| + compressor = PassThruCompressor.new(file) + + assert_equal(0, compressor.size) + + t1 = "hello world" + t2 = "" + t3 = "bingo" + + compressor << t1 + assert_equal(compressor.size, t1.size) + + compressor << t2 + assert_equal(compressor.size, t1.size + t2.size) + + compressor << t3 + assert_equal(compressor.size, t1.size + t2.size + t3.size) + } + end + + def test_crc + run_crc_test(PassThruCompressor) + end +end + +class DeflaterTest < Test::Unit::TestCase + include CrcTest + + def test_outputOperator + txt = load_file("data/file2.txt") + deflate(txt, "deflatertest.bin") + inflatedTxt = inflate("deflatertest.bin") + assert_equal(txt, inflatedTxt) + end + + private + def load_file(fileName) + txt = nil + File.open(fileName, "rb") { |f| txt = f.read } + end + + def deflate(data, fileName) + File.open(fileName, "wb") { + |file| + deflater = Deflater.new(file) + deflater << data + deflater.finish + assert_equal(deflater.size, data.size) + file << "trailing data for zlib with -MAX_WBITS" + } + end + + def inflate(fileName) + txt = nil + File.open(fileName, "rb") { + |file| + inflater = Inflater.new(file) + txt = inflater.sysread + } + end + + def test_crc + run_crc_test(Deflater) + end +end + +class ZipOutputStreamTest < Test::Unit::TestCase + include AssertEntry + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zip_name = "output.zip" + + def test_new + zos = ZipOutputStream.new(TEST_ZIP.zip_name) + zos.comment = TEST_ZIP.comment + write_test_zip(zos) + zos.close + assert_test_zip_contents(TEST_ZIP) + end + + def test_open + ZipOutputStream.open(TEST_ZIP.zip_name) { + |zos| + zos.comment = TEST_ZIP.comment + write_test_zip(zos) + } + assert_test_zip_contents(TEST_ZIP) + end + + def test_writingToClosedStream + assert_i_o_error_in_closed_stream { |zos| zos << "hello world" } + assert_i_o_error_in_closed_stream { |zos| zos.puts "hello world" } + assert_i_o_error_in_closed_stream { |zos| zos.write "hello world" } + end + + def test_cannotOpenFile + name = TestFiles::EMPTY_TEST_DIR + begin + zos = ZipOutputStream.open(name) + rescue Exception + assert($!.kind_of?(Errno::EISDIR) || # Linux + $!.kind_of?(Errno::EEXIST) || # Windows/cygwin + $!.kind_of?(Errno::EACCES), # Windows + "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.class}") + end + end + + def assert_i_o_error_in_closed_stream + assert_raise(IOError) { + zos = ZipOutputStream.new("test_putOnClosedStream.zip") + zos.close + yield zos + } + end + + def write_test_zip(zos) + TEST_ZIP.entry_names.each { + |entryName| + zos.put_next_entry(entryName) + File.open(entryName, "rb") { |f| zos.write(f.read) } + } + end +end + + + +module Enumerable + def compare_enumerables(otherEnumerable) + otherAsArray = otherEnumerable.to_a + index=0 + each_with_index { + |element, index| + return false unless yield(element, otherAsArray[index]) + } + return index+1 == otherAsArray.size + end +end + + +class ZipCentralDirectoryEntryTest < Test::Unit::TestCase + + def test_read_from_stream + File.open("data/testDirectory.bin", "rb") { + |file| + entry = ZipEntry.read_c_dir_entry(file) + + assert_equal("longAscii.txt", entry.name) + assert_equal(ZipEntry::DEFLATED, entry.compression_method) + assert_equal(106490, entry.size) + assert_equal(3784, entry.compressed_size) + assert_equal(0xfcd1799c, entry.crc) + assert_equal("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equal("empty.txt", entry.name) + assert_equal(ZipEntry::STORED, entry.compression_method) + assert_equal(0, entry.size) + assert_equal(0, entry.compressed_size) + assert_equal(0x0, entry.crc) + assert_equal("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equal("short.txt", entry.name) + assert_equal(ZipEntry::STORED, entry.compression_method) + assert_equal(6, entry.size) + assert_equal(6, entry.compressed_size) + assert_equal(0xbb76fe69, entry.crc) + assert_equal("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equal("longBinary.bin", entry.name) + assert_equal(ZipEntry::DEFLATED, entry.compression_method) + assert_equal(1000024, entry.size) + assert_equal(70847, entry.compressed_size) + assert_equal(0x10da7d59, entry.crc) + assert_equal("", entry.comment) + + entry = ZipEntry.read_c_dir_entry(file) + assert_equal(nil, entry) +# Fields that are not check by this test: +# version made by 2 bytes +# version needed to extract 2 bytes +# general purpose bit flag 2 bytes +# last mod file time 2 bytes +# last mod file date 2 bytes +# compressed size 4 bytes +# uncompressed size 4 bytes +# disk number start 2 bytes +# internal file attributes 2 bytes +# external file attributes 4 bytes +# relative offset of local header 4 bytes + +# file name (variable size) +# extra field (variable size) +# file comment (variable size) + + } + end + + def test_ReadEntryFromTruncatedZipFile + fragment="" + File.open("data/testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes + fragment.extend(IOizeString) + entry = ZipEntry.new + entry.read_c_dir_entry(fragment) + fail "ZipError expected" + rescue ZipError + end + +end + + +class ZipEntrySetTest < Test::Unit::TestCase + ZIP_ENTRIES = [ + ZipEntry.new("zipfile.zip", "name1", "comment1"), + ZipEntry.new("zipfile.zip", "name2", "comment1"), + ZipEntry.new("zipfile.zip", "name3", "comment1"), + ZipEntry.new("zipfile.zip", "name4", "comment1"), + ZipEntry.new("zipfile.zip", "name5", "comment1"), + ZipEntry.new("zipfile.zip", "name6", "comment1") + ] + + def setup + @zipEntrySet = ZipEntrySet.new(ZIP_ENTRIES) + end + + def test_include + assert(@zipEntrySet.include?(ZIP_ENTRIES.first)) + assert(! @zipEntrySet.include?(ZipEntry.new("different.zip", "different", "aComment"))) + end + + def test_size + assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) + assert_equal(ZIP_ENTRIES.size, @zipEntrySet.length) + @zipEntrySet << ZipEntry.new("a", "b", "c") + assert_equal(ZIP_ENTRIES.size + 1, @zipEntrySet.length) + end + + def test_add + zes = ZipEntrySet.new + entry1 = ZipEntry.new("zf.zip", "name1") + entry2 = ZipEntry.new("zf.zip", "name2") + zes << entry1 + assert(zes.include?(entry1)) + zes.push(entry2) + assert(zes.include?(entry2)) + end + + def test_delete + assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) + entry = @zipEntrySet.delete(ZIP_ENTRIES.first) + assert_equal(ZIP_ENTRIES.size - 1, @zipEntrySet.size) + assert_equal(ZIP_ENTRIES.first, entry) + + entry = @zipEntrySet.delete(ZIP_ENTRIES.first) + assert_equal(ZIP_ENTRIES.size - 1, @zipEntrySet.size) + assert_nil(entry) + end + + def test_each + # Tested indirectly via each_with_index + count = 0 + @zipEntrySet.each_with_index { + |entry, index| + assert(ZIP_ENTRIES.include?(entry)) + count = count.succ + } + assert_equal(ZIP_ENTRIES.size, count) + end + + def test_entries + assert_equal(ZIP_ENTRIES.sort, @zipEntrySet.entries.sort) + end + + def test_compound + newEntry = ZipEntry.new("zf.zip", "new entry", "new entry's comment") + assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) + @zipEntrySet << newEntry + assert_equal(ZIP_ENTRIES.size + 1, @zipEntrySet.size) + assert(@zipEntrySet.include?(newEntry)) + + @zipEntrySet.delete(newEntry) + assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) + end + + def test_dup + copy = @zipEntrySet.dup + assert_equal(@zipEntrySet, copy) + + # demonstrate that this is a deep copy + copy.entries[0].name = "a totally different name" + assert(@zipEntrySet != copy) + end + + def test_parent + entries = [ + ZipEntry.new("zf.zip", "a"), + ZipEntry.new("zf.zip", "a/"), + ZipEntry.new("zf.zip", "a/b"), + ZipEntry.new("zf.zip", "a/b/"), + ZipEntry.new("zf.zip", "a/b/c"), + ZipEntry.new("zf.zip", "a/b/c/") + ] + entrySet = ZipEntrySet.new(entries) + + assert_equal(nil, entrySet.parent(entries[0])) + assert_equal(nil, entrySet.parent(entries[1])) + assert_equal(entries[1], entrySet.parent(entries[2])) + assert_equal(entries[1], entrySet.parent(entries[3])) + assert_equal(entries[3], entrySet.parent(entries[4])) + assert_equal(entries[3], entrySet.parent(entries[5])) + end + + def test_glob + res = @zipEntrySet.glob('name[2-4]') + assert_equal(3, res.size) + assert_equal(ZIP_ENTRIES[1,3], res) + end + + def test_glob2 + entries = [ + ZipEntry.new("zf.zip", "a/"), + ZipEntry.new("zf.zip", "a/b/b1"), + ZipEntry.new("zf.zip", "a/b/c/"), + ZipEntry.new("zf.zip", "a/b/c/c1") + ] + entrySet = ZipEntrySet.new(entries) + + assert_equal(entries[0,1], entrySet.glob("*")) +# assert_equal(entries[FIXME], entrySet.glob("**")) +# res = entrySet.glob('a*') +# assert_equal(entries.size, res.size) +# assert_equal(entrySet.map { |e| e.name }, res.map { |e| e.name }) + end +end + + +class ZipCentralDirectoryTest < Test::Unit::TestCase + + def test_read_from_stream + File.open(TestZipFile::TEST_ZIP2.zip_name, "rb") { + |zipFile| + cdir = ZipCentralDirectory.read_from_stream(zipFile) + + assert_equal(TestZipFile::TEST_ZIP2.entry_names.size, cdir.size) + assert(cdir.entries.sort.compare_enumerables(TestZipFile::TEST_ZIP2.entry_names.sort) { + |cdirEntry, testEntryName| + cdirEntry.name == testEntryName + }) + assert_equal(TestZipFile::TEST_ZIP2.comment, cdir.comment) + } + end + + def test_readFromInvalidStream + File.open("data/file2.txt", "rb") { + |zipFile| + cdir = ZipCentralDirectory.new + cdir.read_from_stream(zipFile) + } + fail "ZipError expected!" + rescue ZipError + end + + def test_ReadFromTruncatedZipFile + fragment="" + File.open("data/testDirectory.bin") { |f| fragment = f.read } + fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete + fragment.extend(IOizeString) + entry = ZipCentralDirectory.new + entry.read_from_stream(fragment) + fail "ZipError expected" + rescue ZipError + end + + def test_write_to_stream + entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] + cdir = ZipCentralDirectory.new(entries, "my zip comment") + File.open("cdirtest.bin", "wb") { |f| cdir.write_to_stream(f) } + cdirReadback = ZipCentralDirectory.new + File.open("cdirtest.bin", "rb") { |f| cdirReadback.read_from_stream(f) } + + assert_equal(cdir.entries.sort, cdirReadback.entries.sort) + end + + def test_equality + cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "my zip comment") + cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "secondEntryName"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, + "somethingExtra"), + ZipEntry.new("file.zip", "lastEntry.txt") ], + "comment?") + assert_equal(cdir1, cdir1) + assert_equal(cdir1, cdir2) + + assert(cdir1 != cdir3) + assert(cdir2 != cdir3) + assert(cdir2 != cdir3) + assert(cdir3 != cdir4) + + assert(cdir3 != "hello") + end +end + + +class BasicZipFileTest < Test::Unit::TestCase + include AssertEntry + + def setup + @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zip_name) + @testEntryNameIndex=0 + end + + def test_entries + assert_equal(TestZipFile::TEST_ZIP2.entry_names.sort, + @zipFile.entries.entries.sort.map {|e| e.name} ) + end + + def test_each + count = 0 + visited = {} + @zipFile.each { + |entry| + assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name)) + assert(! visited.include?(entry.name)) + visited[entry.name] = nil + count = count.succ + } + assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count) + end + + def test_foreach + count = 0 + visited = {} + ZipFile.foreach(TestZipFile::TEST_ZIP2.zip_name) { + |entry| + assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name)) + assert(! visited.include?(entry.name)) + visited[entry.name] = nil + count = count.succ + } + assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count) + end + + def test_get_input_stream + count = 0 + visited = {} + @zipFile.each { + |entry| + assert_entry(entry.name, @zipFile.get_input_stream(entry), entry.name) + assert(! visited.include?(entry.name)) + visited[entry.name] = nil + count = count.succ + } + assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count) + end + + def test_get_input_streamBlock + fileAndEntryName = @zipFile.entries.first.name + @zipFile.get_input_stream(fileAndEntryName) { + |zis| + assert_entryContentsForStream(fileAndEntryName, + zis, + fileAndEntryName) + } + end +end + +module CommonZipFileFixture + include AssertEntry + + EMPTY_FILENAME = "emptyZipFile.zip" + + TEST_ZIP = TestZipFile::TEST_ZIP2.clone + TEST_ZIP.zip_name = "5entry_copy.zip" + + def setup + File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) + File.copy(TestZipFile::TEST_ZIP2.zip_name, TEST_ZIP.zip_name) + end +end + +class ZipFileTest < Test::Unit::TestCase + include CommonZipFileFixture + + def test_createFromScratch + comment = "a short comment" + + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.get_output_stream("myFile") { |os| os.write "myFile contains just this" } + zf.mkdir("dir1") + zf.comment = comment + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equal(comment, zfRead.comment) + assert_equal(2, zfRead.entries.length) + end + + def test_get_output_stream + entryCount = nil + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + entryCount = zf.size + zf.get_output_stream('newEntry.txt') { + |os| + os.write "Putting stuff in newEntry.txt" + } + assert_equal(entryCount+1, zf.size) + assert_equal("Putting stuff in newEntry.txt", zf.read("newEntry.txt")) + + zf.get_output_stream(zf.get_entry('data/generated/empty.txt')) { + |os| + os.write "Putting stuff in data/generated/empty.txt" + } + assert_equal(entryCount+1, zf.size) + assert_equal("Putting stuff in data/generated/empty.txt", zf.read("data/generated/empty.txt")) + + zf.get_output_stream('entry.bin') { + |os| + os.write(File.open('data/generated/5entry.zip', 'rb').read) + } + } + + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_equal(entryCount+2, zf.size) + assert_equal("Putting stuff in newEntry.txt", zf.read("newEntry.txt")) + assert_equal("Putting stuff in data/generated/empty.txt", zf.read("data/generated/empty.txt")) + assert_equal(File.open('data/generated/5entry.zip', 'rb').read, zf.read("entry.bin")) + } + end + + def test_add + srcFile = "data/file2.txt" + entryName = "newEntryName.rb" + assert(File.exists?(srcFile)) + zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) + zf.add(entryName, srcFile) + zf.close + + zfRead = ZipFile.new(EMPTY_FILENAME) + assert_equal("", zfRead.comment) + assert_equal(1, zfRead.entries.length) + assert_equal(entryName, zfRead.entries.first.name) + AssertEntry.assert_contents(srcFile, + zfRead.get_input_stream(entryName) { |zis| zis.read }) + end + + def test_addExistingEntryName + assert_raise(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.add(zf.entries.first.name, "data/file2.txt") + } + } + end + + def test_addExistingEntryNameReplace + gotCalled = false + replacedEntry = nil + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + replacedEntry = zf.entries.first.name + zf.add(replacedEntry, "data/file2.txt") { gotCalled = true; true } + } + assert(gotCalled) + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_contains(zf, replacedEntry, "data/file2.txt") + } + end + + def test_addDirectory + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) + } + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } + assert(dirEntry.is_directory) + } + end + + def test_remove + entryToRemove, *remainingEntries = TEST_ZIP.entry_names + + File.copy(TestZipFile::TEST_ZIP2.zip_name, TEST_ZIP.zip_name) + + zf = ZipFile.new(TEST_ZIP.zip_name) + assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) + zf.remove(entryToRemove) + assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equal(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) + assert_equal(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) + zfRead.close + end + + + def test_rename + entryToRename, *remainingEntries = TEST_ZIP.entry_names + + zf = ZipFile.new(TEST_ZIP.zip_name) + assert(zf.entries.map { |e| e.name }.include?(entryToRename)) + + newName = "changed name" + assert(! zf.entries.map { |e| e.name }.include?(newName)) + + zf.rename(entryToRename, newName) + assert(zf.entries.map { |e| e.name }.include?(newName)) + + zf.close + + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert(zfRead.entries.map { |e| e.name }.include?(newName)) + zfRead.close + end + + def test_renameToExistingEntry + oldEntries = nil + ZipFile.open(TEST_ZIP.zip_name) { |zf| oldEntries = zf.entries } + + assert_raise(ZipEntryExistsError) { + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.rename(zf.entries[0], zf.entries[1].name) + } + } + + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_equal(oldEntries.sort.map{ |e| e.name }, zf.entries.sort.map{ |e| e.name }) + } + end + + def test_renameToExistingEntryOverwrite + oldEntries = nil + ZipFile.open(TEST_ZIP.zip_name) { |zf| oldEntries = zf.entries } + + gotCalled = false + renamedEntryName = nil + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + renamedEntryName = zf.entries[0].name + zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } + } + + assert(gotCalled) + oldEntries.delete_if { |e| e.name == renamedEntryName } + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_equal(oldEntries.sort.map{ |e| e.name }, + zf.entries.sort.map{ |e| e.name }) + } + end + + def test_renameNonEntry + nonEntry = "bogusEntry" + target_entry = "target_entryName" + zf = ZipFile.new(TEST_ZIP.zip_name) + assert(! zf.entries.include?(nonEntry)) + assert_raise(Errno::ENOENT) { + zf.rename(nonEntry, target_entry) + } + zf.commit + assert(! zf.entries.include?(target_entry)) + ensure + zf.close + end + + def test_renameEntryToExistingEntry + entry1, entry2, *remaining = TEST_ZIP.entry_names + zf = ZipFile.new(TEST_ZIP.zip_name) + assert_raise(ZipEntryExistsError) { + zf.rename(entry1, entry2) + } + ensure + zf.close + end + + def test_replace + entryToReplace = TEST_ZIP.entry_names[2] + newEntrySrcFilename = "data/file2.txt" + zf = ZipFile.new(TEST_ZIP.zip_name) + zf.replace(entryToReplace, newEntrySrcFilename) + + zf.close + zfRead = ZipFile.new(TEST_ZIP.zip_name) + AssertEntry::assert_contents(newEntrySrcFilename, + zfRead.get_input_stream(entryToReplace) { |is| is.read }) + AssertEntry::assert_contents(TEST_ZIP.entry_names[0], + zfRead.get_input_stream(TEST_ZIP.entry_names[0]) { |is| is.read }) + AssertEntry::assert_contents(TEST_ZIP.entry_names[1], + zfRead.get_input_stream(TEST_ZIP.entry_names[1]) { |is| is.read }) + AssertEntry::assert_contents(TEST_ZIP.entry_names[3], + zfRead.get_input_stream(TEST_ZIP.entry_names[3]) { |is| is.read }) + zfRead.close + end + + def test_replaceNonEntry + entryToReplace = "nonExistingEntryname" + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + assert_raise(Errno::ENOENT) { + zf.replace(entryToReplace, "data/file2.txt") + } + } + end + + def test_commit + newName = "renamedFirst" + zf = ZipFile.new(TEST_ZIP.zip_name) + oldName = zf.entries.first + zf.rename(oldName, newName) + zf.commit + + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert(zfRead.entries.detect { |e| e.name == newName } != nil) + assert(zfRead.entries.detect { |e| e.name == oldName } == nil) + zfRead.close + + zf.close + end + + # This test tests that after commit, you + # can delete the file you used to add the entry to the zip file + # with + def test_commitUseZipEntry + File.copy(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") + zf = ZipFile.open(TEST_ZIP.zip_name) + zf.add("okToDelete.txt", "okToDelete.txt") + assert_contains(zf, "okToDelete.txt") + zf.commit + File.move("okToDelete.txt", "okToDeleteMoved.txt") + assert_contains(zf, "okToDelete.txt", "okToDeleteMoved.txt") + end + +# def test_close +# zf = ZipFile.new(TEST_ZIP.zip_name) +# zf.close +# assert_raise(IOError) { +# zf.extract(TEST_ZIP.entry_names.first, "hullubullu") +# } +# end + + def test_compound1 + renamedName = "renamedName" + originalEntries = [] + begin + zf = ZipFile.new(TEST_ZIP.zip_name) + originalEntries = zf.entries.dup + + assert_not_contains(zf, TestFiles::RANDOM_ASCII_FILE1) + zf.add(TestFiles::RANDOM_ASCII_FILE1, + TestFiles::RANDOM_ASCII_FILE1) + assert_contains(zf, TestFiles::RANDOM_ASCII_FILE1) + + zf.rename(zf.entries[0], renamedName) + assert_contains(zf, renamedName) + + TestFiles::BINARY_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assert_contains(zf, filename) + } + + assert_contains(zf, originalEntries.last.to_s) + zf.remove(originalEntries.last.to_s) + assert_not_contains(zf, originalEntries.last.to_s) + + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zip_name) + assert_contains(zfRead, TestFiles::RANDOM_ASCII_FILE1) + assert_contains(zfRead, renamedName) + TestFiles::BINARY_TEST_FILES.each { + |filename| + assert_contains(zfRead, filename) + } + assert_not_contains(zfRead, originalEntries.last.to_s) + ensure + zfRead.close + end + end + + def test_compound2 + begin + zf = ZipFile.new(TEST_ZIP.zip_name) + originalEntries = zf.entries.dup + + originalEntries.each { + |entry| + zf.remove(entry) + assert_not_contains(zf, entry) + } + assert(zf.entries.empty?) + + TestFiles::ASCII_TEST_FILES.each { + |filename| + zf.add(filename, filename) + assert_contains(zf, filename) + } + assert_equal(zf.entries.sort.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) + + zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") + assert_not_contains(zf, TestFiles::ASCII_TEST_FILES[0]) + assert_contains(zf, "newName") + ensure + zf.close + end + begin + zfRead = ZipFile.new(TEST_ZIP.zip_name) + asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup + asciiTestFiles.shift + asciiTestFiles.each { + |filename| + assert_contains(zf, filename) + } + + assert_contains(zf, "newName") + ensure + zfRead.close + end + end + + private + def assert_contains(zf, entryName, filename = entryName) + assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") + assert_entryContents(zf, entryName, filename) if File.exists?(filename) + end + + def assert_not_contains(zf, entryName) + assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") + end +end + +class ZipFileExtractTest < Test::Unit::TestCase + include CommonZipFileFixture + EXTRACTED_FILENAME = "extEntry" + ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entry_names.reverse + + def setup + super + File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) + end + + def test_extract + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) + + assert(File.exists?(EXTRACTED_FILENAME)) + AssertEntry::assert_contents(EXTRACTED_FILENAME, + zf.get_input_stream(ENTRY_TO_EXTRACT) { |is| is.read }) + + + File::unlink(EXTRACTED_FILENAME) + + entry = zf.get_entry(ENTRY_TO_EXTRACT) + entry.extract(EXTRACTED_FILENAME) + + assert(File.exists?(EXTRACTED_FILENAME)) + AssertEntry::assert_contents(EXTRACTED_FILENAME, + entry.get_input_stream() { |is| is.read }) + + } + end + + def test_extractExists + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + assert_raise(ZipDestinationFileExistsError) { + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) + } + } + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert_equal(writtenText, f.read) + } + end + + def test_extractExistsOverwrite + writtenText = "written text" + File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } + + gotCalledCorrectly = false + ZipFile.open(TEST_ZIP.zip_name) { + |zf| + zf.extract(zf.entries.first, EXTRACTED_FILENAME) { + |entry, extractLoc| + gotCalledCorrectly = zf.entries.first == entry && + extractLoc == EXTRACTED_FILENAME + true + } + } + + assert(gotCalledCorrectly) + File.open(EXTRACTED_FILENAME, "r") { + |f| + assert(writtenText != f.read) + } + end + + def test_extractNonEntry + zf = ZipFile.new(TEST_ZIP.zip_name) + assert_raise(Errno::ENOENT) { zf.extract("nonExistingEntry", "nonExistingEntry") } + ensure + zf.close if zf + end + + def test_extractNonEntry2 + outFile = "outfile" + assert_raise(Errno::ENOENT) { + zf = ZipFile.new(TEST_ZIP.zip_name) + nonEntry = "hotdog-diddelidoo" + assert(! zf.entries.include?(nonEntry)) + zf.extract(nonEntry, outFile) + zf.close + } + assert(! File.exists?(outFile)) + end + +end + +class ZipFileExtractDirectoryTest < Test::Unit::TestCase + include CommonZipFileFixture + TEST_OUT_NAME = "emptyOutDir" + + def open_zip(&aProc) + assert(aProc != nil) + ZipFile.open(TestZipFile::TEST_ZIP4.zip_name, &aProc) + end + + def extract_test_dir(&aProc) + open_zip { + |zf| + zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) + } + end + + def setup + super + + Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME + File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME + end + + def test_extractDirectory + extract_test_dir + assert(File.directory?(TEST_OUT_NAME)) + end + + def test_extractDirectoryExistsAsDir + Dir.mkdir TEST_OUT_NAME + extract_test_dir + assert(File.directory?(TEST_OUT_NAME)) + end + + def test_extractDirectoryExistsAsFile + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + assert_raise(ZipDestinationFileExistsError) { extract_test_dir } + end + + def test_extractDirectoryExistsAsFileOverwrite + File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } + gotCalled = false + extract_test_dir { + |entry, destPath| + gotCalled = true + assert_equal(TEST_OUT_NAME, destPath) + assert(entry.is_directory) + true + } + assert(gotCalled) + assert(File.directory?(TEST_OUT_NAME)) + end +end + +class ZipExtraFieldTest < Test::Unit::TestCase + def test_new + extra_pure = ZipExtraField.new("") + extra_withstr = ZipExtraField.new("foo") + assert_instance_of(ZipExtraField, extra_pure) + assert_instance_of(ZipExtraField, extra_withstr) + end + + def test_unknownfield + extra = ZipExtraField.new("foo") + assert_equal(extra["Unknown"], "foo") + extra.merge("a") + assert_equal(extra["Unknown"], "fooa") + extra.merge("barbaz") + assert_equal(extra.to_s, "fooabarbaz") + end + + + def test_merge + str = "UT\x5\0\x3\250$\r@Ux\0\0" + extra1 = ZipExtraField.new("") + extra2 = ZipExtraField.new(str) + assert(! extra1.member?("UniversalTime")) + assert(extra2.member?("UniversalTime")) + extra1.merge(str) + assert_equal(extra1["UniversalTime"].mtime, extra2["UniversalTime"].mtime) + end + + def test_length + str = "UT\x5\0\x3\250$\r@Ux\0\0Te\0\0testit" + extra = ZipExtraField.new(str) + assert_equal(extra.local_length, extra.to_local_bin.length) + assert_equal(extra.c_dir_length, extra.to_c_dir_bin.length) + extra.merge("foo") + assert_equal(extra.local_length, extra.to_local_bin.length) + assert_equal(extra.c_dir_length, extra.to_c_dir_bin.length) + end + + + def test_to_s + str = "UT\x5\0\x3\250$\r@Ux\0\0Te\0\0testit" + extra = ZipExtraField.new(str) + assert_instance_of(String, extra.to_s) + + s = extra.to_s + extra.merge("foo") + assert_equal(s.length + 3, extra.to_s.length) + end + + def test_equality + str = "UT\x5\0\x3\250$\r@" + extra1 = ZipExtraField.new(str) + extra2 = ZipExtraField.new(str) + extra3 = ZipExtraField.new(str) + assert_equal(extra1, extra2) + + extra2["UniversalTime"].mtime = Time.now + assert(extra1 != extra2) + + extra3.create("IUnix") + assert(extra1 != extra3) + + extra1.create("IUnix") + assert_equal(extra1, extra3) + end + +end + +# Copyright (C) 2002-2005 Thomas Sondergaard +# rubyzip is free software; you can redistribute it and/or +# modify it under the terms of the ruby license. diff --git a/vendor/plugins/sqlite3-ruby/sqlite3.rb b/vendor/plugins/sqlite3-ruby/sqlite3.rb new file mode 100644 index 00000000..ff8af026 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3.rb @@ -0,0 +1,33 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/database' diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/constants.rb b/vendor/plugins/sqlite3-ruby/sqlite3/constants.rb new file mode 100644 index 00000000..5a20e461 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/constants.rb @@ -0,0 +1,81 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +module SQLite3 ; module Constants + + module TextRep + UTF8 = 1 + UTF16LE = 2 + UTF16BE = 3 + UTF16 = 4 + ANY = 5 + end + + module ColumnType + INTEGER = 1 + FLOAT = 2 + TEXT = 3 + BLOB = 4 + NULL = 5 + end + + module ErrorCode + OK = 0 # Successful result + ERROR = 1 # SQL error or missing database + INTERNAL = 2 # An internal logic error in SQLite + PERM = 3 # Access permission denied + ABORT = 4 # Callback routine requested an abort + BUSY = 5 # The database file is locked + LOCKED = 6 # A table in the database is locked + NOMEM = 7 # A malloc() failed + READONLY = 8 # Attempt to write a readonly database + INTERRUPT = 9 # Operation terminated by sqlite_interrupt() + IOERR = 10 # Some kind of disk I/O error occurred + CORRUPT = 11 # The database disk image is malformed + NOTFOUND = 12 # (Internal Only) Table or record not found + FULL = 13 # Insertion failed because database is full + CANTOPEN = 14 # Unable to open the database file + PROTOCOL = 15 # Database lock protocol error + EMPTY = 16 # (Internal Only) Database table is empty + SCHEMA = 17 # The database schema changed + TOOBIG = 18 # Too much data for one row of a table + CONSTRAINT = 19 # Abort due to contraint violation + MISMATCH = 20 # Data type mismatch + MISUSE = 21 # Library used incorrectly + NOLFS = 22 # Uses OS features not supported on host + AUTH = 23 # Authorization denied + + ROW = 100 # sqlite_step() has another row ready + DONE = 101 # sqlite_step() has finished executing + end + +end ; end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/database.rb b/vendor/plugins/sqlite3-ruby/sqlite3/database.rb new file mode 100644 index 00000000..6a130f90 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/database.rb @@ -0,0 +1,745 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'base64' +require 'sqlite3/constants' +require 'sqlite3/errors' +require 'sqlite3/pragmas' +require 'sqlite3/statement' +require 'sqlite3/translator' +require 'sqlite3/value' + +module SQLite3 + + # The Database class encapsulates a single connection to a SQLite3 database. + # Its usage is very straightforward: + # + # require 'sqlite3' + # + # db = SQLite3::Database.new( "data.db" ) + # + # db.execute( "select * from table" ) do |row| + # p row + # end + # + # db.close + # + # It wraps the lower-level methods provides by the selected driver, and + # includes the Pragmas module for access to various pragma convenience + # methods. + # + # The Database class provides type translation services as well, by which + # the SQLite3 data types (which are all represented as strings) may be + # converted into their corresponding types (as defined in the schemas + # for their tables). This translation only occurs when querying data from + # the database--insertions and updates are all still typeless. + # + # Furthermore, the Database class has been designed to work well with the + # ArrayFields module from Ara Howard. If you require the ArrayFields + # module before performing a query, and if you have not enabled results as + # hashes, then the results will all be indexible by field name. + class Database + include Pragmas + + class < e + @driver.result_error( func, + "#{e.message} (#{e.class})", -1 ) + end + end + + result = @driver.create_function( @handle, name, arity, text_rep, nil, + callback, nil, nil ) + Error.check( result, self ) + + self + end + + # Creates a new aggregate function for use in SQL statements. Aggregate + # functions are functions that apply over every row in the result set, + # instead of over just a single row. (A very common aggregate function + # is the "count" function, for determining the number of rows that match + # a query.) + # + # The new function will be added as +name+, with the given +arity+. (For + # variable arity functions, use -1 for the arity.) + # + # The +step+ parameter must be a proc object that accepts as its first + # parameter a FunctionProxy instance (representing the function + # invocation), with any subsequent parameters (up to the function's arity). + # The +step+ callback will be invoked once for each row of the result set. + # + # The +finalize+ parameter must be a +proc+ object that accepts only a + # single parameter, the FunctionProxy instance representing the current + # function invocation. It should invoke FunctionProxy#set_result to + # store the result of the function. + # + # Example: + # + # db.create_aggregate( "lengths", 1 ) do + # step do |func, value| + # func[ :total ] ||= 0 + # func[ :total ] += ( value ? value.length : 0 ) + # end + # + # finalize do |func| + # func.set_result( func[ :total ] || 0 ) + # end + # end + # + # puts db.get_first_value( "select lengths(name) from table" ) + # + # See also #create_aggregate_handler for a more object-oriented approach to + # aggregate functions. + def create_aggregate( name, arity, step=nil, finalize=nil, + text_rep=Constants::TextRep::ANY, &block ) + # begin + if block + proxy = AggregateDefinitionProxy.new + proxy.instance_eval(&block) + step ||= proxy.step_callback + finalize ||= proxy.finalize_callback + end + + step_callback = proc do |func,*args| + ctx = @driver.aggregate_context( func ) + unless ctx[:__error] + begin + step.call( FunctionProxy.new( @driver, func, ctx ), + *args.map{|v| Value.new(self,v)} ) + rescue Exception => e + ctx[:__error] = e + end + end + end + + finalize_callback = proc do |func| + ctx = @driver.aggregate_context( func ) + unless ctx[:__error] + begin + finalize.call( FunctionProxy.new( @driver, func, ctx ) ) + rescue Exception => e + @driver.result_error( func, + "#{e.message} (#{e.class})", -1 ) + end + else + e = ctx[:__error] + @driver.result_error( func, + "#{e.message} (#{e.class})", -1 ) + end + end + + result = @driver.create_function( @handle, name, arity, text_rep, nil, + nil, step_callback, finalize_callback ) + Error.check( result, self ) + + self + end + + # This is another approach to creating an aggregate function (see + # #create_aggregate). Instead of explicitly specifying the name, + # callbacks, arity, and type, you specify a factory object + # (the "handler") that knows how to obtain all of that information. The + # handler should respond to the following messages: + # + # +arity+:: corresponds to the +arity+ parameter of #create_aggregate. This + # message is optional, and if the handler does not respond to it, + # the function will have an arity of -1. + # +name+:: this is the name of the function. The handler _must_ implement + # this message. + # +new+:: this must be implemented by the handler. It should return a new + # instance of the object that will handle a specific invocation of + # the function. + # + # The handler instance (the object returned by the +new+ message, described + # above), must respond to the following messages: + # + # +step+:: this is the method that will be called for each step of the + # aggregate function's evaluation. It should implement the same + # signature as the +step+ callback for #create_aggregate. + # +finalize+:: this is the method that will be called to finalize the + # aggregate function's evaluation. It should implement the + # same signature as the +finalize+ callback for + # #create_aggregate. + # + # Example: + # + # class LengthsAggregateHandler + # def self.arity; 1; end + # + # def initialize + # @total = 0 + # end + # + # def step( ctx, name ) + # @total += ( name ? name.length : 0 ) + # end + # + # def finalize( ctx ) + # ctx.set_result( @total ) + # end + # end + # + # db.create_aggregate_handler( LengthsAggregateHandler ) + # puts db.get_first_value( "select lengths(name) from A" ) + def create_aggregate_handler( handler ) + arity = -1 + text_rep = Constants::TextRep::ANY + + arity = handler.arity if handler.respond_to?(:arity) + text_rep = handler.text_rep if handler.respond_to?(:text_rep) + name = handler.name + + step = proc do |func,*args| + ctx = @driver.aggregate_context( func ) + unless ctx[ :__error ] + ctx[ :handler ] ||= handler.new + begin + ctx[ :handler ].step( FunctionProxy.new( @driver, func, ctx ), + *args.map{|v| Value.new(self,v)} ) + rescue Exception, StandardError => e + ctx[ :__error ] = e + end + end + end + + finalize = proc do |func| + ctx = @driver.aggregate_context( func ) + unless ctx[ :__error ] + ctx[ :handler ] ||= handler.new + begin + ctx[ :handler ].finalize( FunctionProxy.new( @driver, func, ctx ) ) + rescue Exception => e + ctx[ :__error ] = e + end + end + + if ctx[ :__error ] + e = ctx[ :__error ] + @driver.sqlite3_result_error( func, "#{e.message} (#{e.class})", -1 ) + end + end + + result = @driver.create_function( @handle, name, arity, text_rep, nil, + nil, step, finalize ) + Error.check( result, self ) + + self + end + + # Begins a new transaction. Note that nested transactions are not allowed + # by SQLite, so attempting to nest a transaction will result in a runtime + # exception. + # + # The +mode+ parameter may be either :deferred (the default), + # :immediate, or :exclusive. + # + # If a block is given, the database instance is yielded to it, and the + # transaction is committed when the block terminates. If the block + # raises an exception, a rollback will be performed instead. Note that if + # a block is given, #commit and #rollback should never be called + # explicitly or you'll get an error when the block terminates. + # + # If a block is not given, it is the caller's responsibility to end the + # transaction explicitly, either by calling #commit, or by calling + # #rollback. + def transaction( mode = :deferred ) + execute "begin #{mode.to_s} transaction" + @transaction_active = true + + if block_given? + abort = false + begin + yield self + rescue ::Object + abort = true + raise + ensure + abort and rollback or commit + end + end + + true + end + + # Commits the current transaction. If there is no current transaction, + # this will cause an error to be raised. This returns +true+, in order + # to allow it to be used in idioms like + # abort? and rollback or commit. + def commit + execute "commit transaction" + @transaction_active = false + true + end + + # Rolls the current transaction back. If there is no current transaction, + # this will cause an error to be raised. This returns +true+, in order + # to allow it to be used in idioms like + # abort? and rollback or commit. + def rollback + execute "rollback transaction" + @transaction_active = false + true + end + + # Returns +true+ if there is a transaction active, and +false+ otherwise. + def transaction_active? + @transaction_active + end + + # Loads the corresponding driver, or if it is nil, attempts to locate a + # suitable driver. + def load_driver( driver ) + case driver + when Class + # do nothing--use what was given + when Symbol, String + require "sqlite3/driver/#{driver.to_s.downcase}/driver" + driver = SQLite3::Driver.const_get( driver )::Driver + else + [ "Native", "DL" ].each do |d| + begin + require "sqlite3/driver/#{d.downcase}/driver" + driver = SQLite3::Driver.const_get( d )::Driver + break + rescue SyntaxError + raise + rescue ScriptError, Exception, NameError + end + end + raise "no driver for sqlite3 found" unless driver + end + + @driver = driver.new + end + private :load_driver + + # A helper class for dealing with custom functions (see #create_function, + # #create_aggregate, and #create_aggregate_handler). It encapsulates the + # opaque function object that represents the current invocation. It also + # provides more convenient access to the API functions that operate on + # the function object. + # + # This class will almost _always_ be instantiated indirectly, by working + # with the create methods mentioned above. + class FunctionProxy + + # Create a new FunctionProxy that encapsulates the given +func+ object. + # If context is non-nil, the functions context will be set to that. If + # it is non-nil, it must quack like a Hash. If it is nil, then none of + # the context functions will be available. + def initialize( driver, func, context=nil ) + @driver = driver + @func = func + @context = context + end + + # Calls #set_result to set the result of this function. + def result=( result ) + set_result( result ) + end + + # Set the result of the function to the given value. The function will + # then return this value. + def set_result( result, utf16=false ) + @driver.result_text( @func, result, utf16 ) + end + + # Set the result of the function to the given error message. + # The function will then return that error. + def set_error( error ) + @driver.result_error( @func, error.to_s, -1 ) + end + + # (Only available to aggregate functions.) Returns the number of rows + # that the aggregate has processed so far. This will include the current + # row, and so will always return at least 1. + def count + ensure_aggregate! + @driver.aggregate_count( @func ) + end + + # Returns the value with the given key from the context. This is only + # available to aggregate functions. + def []( key ) + ensure_aggregate! + @context[ key ] + end + + # Sets the value with the given key in the context. This is only + # available to aggregate functions. + def []=( key, value ) + ensure_aggregate! + @context[ key ] = value + end + + # A function for performing a sanity check, to ensure that the function + # being invoked is an aggregate function. This is implied by the + # existence of the context variable. + def ensure_aggregate! + unless @context + raise MisuseException, "function is not an aggregate" + end + end + private :ensure_aggregate! + + end + + # A proxy used for defining the callbacks to an aggregate function. + class AggregateDefinitionProxy # :nodoc: + attr_reader :step_callback, :finalize_callback + + def step( &block ) + @step_callback = block + end + + def finalize( &block ) + @finalize_callback = block + end + end + + end + +end + diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/api.rb b/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/api.rb new file mode 100644 index 00000000..9cea866f --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/api.rb @@ -0,0 +1,184 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'dl/import' + +module SQLite3 ; module Driver; module DL; + + module API + extend ::DL::Importable + + library_name = case RUBY_PLATFORM.downcase + when /darwin/ + "libsqlite3.dylib" + when /linux/, /freebsd|netbsd|openbsd|dragonfly/, /solaris/ + "libsqlite3.so" + when /win32/ + "sqlite3.dll" + else + abort <<-EOF +== * UNSUPPORTED PLATFORM ====================================================== +The platform '#{RUBY_PLATFORM}' is unsupported. Please help the author by +editing the following file to allow your sqlite3 library to be found, and +submitting a patch to jamis_buck@byu.edu. Thanks! + +#{__FILE__} +=========================================================================== * == + EOF + end + + if defined? SQLITE3_LIB_PATH + library_name = File.join( SQLITE3_LIB_PATH, library_name ) + end + + dlload library_name + + typealias "db", "void*" + typealias "stmt", "void*" + typealias "value", "void*" + typealias "context", "void*" + + # until Ruby/DL supports 64-bit ints, we'll just treat them as 32-bit ints + typealias "int64", "unsigned long" + + extern "const char *sqlite3_libversion()" + + extern "int sqlite3_open(const char*,db*)" + extern "int sqlite3_open16(const void*,db*)" + extern "int sqlite3_close(db)" + extern "const char* sqlite3_errmsg(db)" + extern "void* sqlite3_errmsg16(db)" + extern "int sqlite3_errcode(db)" + + extern "int sqlite3_prepare(db,const char*,int,stmt*,const char**)" + extern "int sqlite3_prepare16(db,const void*,int,stmt*,const void**)" + extern "int sqlite3_finalize(stmt)" + extern "int sqlite3_reset(stmt)" + extern "int sqlite3_step(stmt)" + + extern "int64 sqlite3_last_insert_rowid(db)" + extern "int sqlite3_changes(db)" + extern "int sqlite3_total_changes(db)" + extern "void sqlite3_interrupt(db)" + extern "ibool sqlite3_complete(const char*)" + extern "ibool sqlite3_complete16(const void*)" + + extern "int sqlite3_busy_handler(db,void*,void*)" + extern "int sqlite3_busy_timeout(db,int)" + + extern "int sqlite3_set_authorizer(db,void*,void*)" + extern "void* sqlite3_trace(db,void*,void*)" + + extern "int sqlite3_bind_blob(stmt,int,const void*,int,void*)" + extern "int sqlite3_bind_double(stmt,int,double)" + extern "int sqlite3_bind_int(stmt,int,int)" + extern "int sqlite3_bind_int64(stmt,int,int64)" + extern "int sqlite3_bind_null(stmt,int)" + extern "int sqlite3_bind_text(stmt,int,const char*,int,void*)" + extern "int sqlite3_bind_text16(stmt,int,const void*,int,void*)" + #extern "int sqlite3_bind_value(stmt,int,value)" + + extern "int sqlite3_bind_parameter_count(stmt)" + extern "const char* sqlite3_bind_parameter_name(stmt,int)" + extern "int sqlite3_bind_parameter_index(stmt,const char*)" + + extern "int sqlite3_column_count(stmt)" + extern "int sqlite3_data_count(stmt)" + + extern "const void *sqlite3_column_blob(stmt,int)" + extern "int sqlite3_column_bytes(stmt,int)" + extern "int sqlite3_column_bytes16(stmt,int)" + extern "const char *sqlite3_column_decltype(stmt,int)" + extern "void *sqlite3_column_decltype16(stmt,int)" + extern "double sqlite3_column_double(stmt,int)" + extern "int sqlite3_column_int(stmt,int)" + extern "int64 sqlite3_column_int64(stmt,int)" + extern "const char *sqlite3_column_name(stmt,int)" + extern "const void *sqlite3_column_name16(stmt,int)" + extern "const char *sqlite3_column_text(stmt,int)" + extern "const void *sqlite3_column_text16(stmt,int)" + extern "int sqlite3_column_type(stmt,int)" + + extern "int sqlite3_create_function(db,const char*,int,int,void*,void*,void*,void*)" + extern "int sqlite3_create_function16(db,const void*,int,int,void*,void*,void*,void*)" + extern "int sqlite3_aggregate_count(context)" + + extern "const void *sqlite3_value_blob(value)" + extern "int sqlite3_value_bytes(value)" + extern "int sqlite3_value_bytes16(value)" + extern "double sqlite3_value_double(value)" + extern "int sqlite3_value_int(value)" + extern "int64 sqlite3_value_int64(value)" + extern "const char* sqlite3_value_text(value)" + extern "const void* sqlite3_value_text16(value)" + extern "const void* sqlite3_value_text16le(value)" + extern "const void* sqlite3_value_text16be(value)" + extern "int sqlite3_value_type(value)" + + extern "void *sqlite3_aggregate_context(context,int)" + extern "void *sqlite3_user_data(context)" + extern "void *sqlite3_get_auxdata(context,int)" + extern "void sqlite3_set_auxdata(context,int,void*,void*)" + + extern "void sqlite3_result_blob(context,const void*,int,void*)" + extern "void sqlite3_result_double(context,double)" + extern "void sqlite3_result_error(context,const char*,int)" + extern "void sqlite3_result_error16(context,const void*,int)" + extern "void sqlite3_result_int(context,int)" + extern "void sqlite3_result_int64(context,int64)" + extern "void sqlite3_result_null(context)" + extern "void sqlite3_result_text(context,const char*,int,void*)" + extern "void sqlite3_result_text16(context,const void*,int,void*)" + extern "void sqlite3_result_text16le(context,const void*,int,void*)" + extern "void sqlite3_result_text16be(context,const void*,int,void*)" + extern "void sqlite3_result_value(context,value)" + + extern "int sqlite3_create_collation(db,const char*,int,void*,void*)" + extern "int sqlite3_create_collation16(db,const char*,int,void*,void*)" + extern "int sqlite3_collation_needed(db,void*,void*)" + extern "int sqlite3_collation_needed16(db,void*,void*)" + + # ==== CRYPTO (NOT IN PUBLIC RELEASE) ==== + if defined?( CRYPTO_API ) && CRYPTO_API + extern "int sqlite3_key(db,void*,int)" + extern "int sqlite3_rekey(db,void*,int)" + end + + # ==== EXPERIMENTAL ==== + if defined?( EXPERIMENTAL_API ) && EXPERIMENTAL_API + extern "int sqlite3_progress_handler(db,int,void*,void*)" + extern "int sqlite3_commit_hook(db,void*,void*)" + end + + end + +end ; end ; end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/driver.rb b/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/driver.rb new file mode 100644 index 00000000..ac72e0db --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/driver/dl/driver.rb @@ -0,0 +1,338 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/driver/dl/api' + +warn "The DL driver for sqlite3-ruby is deprecated and will be removed" +warn "in a future release. Please update your installation to use the" +warn "Native driver." + +module Kernel + # Allows arbitrary objects to be passed as a pointer to functions. + # (Probably not very GC safe, but by encapsulating it like this we + # can change the implementation later.) + def to_ptr + ptr = DL.malloc(DL.sizeof("L")) + ptr.set_object self + ptr + end +end + +class DL::PtrData + # The inverse of the Kernel#to_ptr operation. + def to_object + n = to_s(4).unpack("L").first + return nil if n < 1 + ObjectSpace._id2ref(n) rescue self.to_s + end + + def set_object(obj) + self[0] = [obj.object_id].pack("L") + end +end + +module SQLite3 ; module Driver ; module DL + + class Driver + STATIC = ::DL::PtrData.new(0) + TRANSIENT = ::DL::PtrData.new(-1) + + def open( filename, utf16=false ) + handle = ::DL::PtrData.new(0) + result = API.send( ( utf16 ? :sqlite3_open16 : :sqlite3_open ), + filename+"\0", handle.ref ) + [ result, handle ] + end + + def errmsg( db, utf16=false ) + if utf16 + msg = API.sqlite3_errmsg16( db ) + msg.free = nil + msg.to_s(utf16_length(msg)) + else + API.sqlite3_errmsg( db ) + end + end + + def prepare( db, sql, utf16=false ) + handle = ::DL::PtrData.new(0) + remainder = ::DL::PtrData.new(0) + + result = API.send( ( utf16 ? :sqlite3_prepare16 : :sqlite3_prepare ), + db, sql+"\0", sql.length, handle.ref, remainder.ref ) + + args = utf16 ? [ utf16_length(remainder) ] : [] + remainder = remainder.to_s( *args ) + + [ result, handle, remainder ] + end + + def complete?( sql, utf16=false ) + API.send( utf16 ? :sqlite3_complete16 : :sqlite3_complete, sql+"\0" ) + end + + def value_blob( value ) + blob = API.sqlite3_value_blob( value ) + blob.free = nil + blob.to_s( API.sqlite3_value_bytes( value ) ) + end + + def value_text( value, utf16=false ) + method = case utf16 + when nil, false then :sqlite3_value_text + when :le then :sqlite3_value_text16le + when :be then :sqlite3_value_text16be + else :sqlite3_value_text16 + end + + result = API.send( method, value ) + if utf16 + result.free = nil + size = API.sqlite3_value_bytes( value ) + result = result.to_s( size ) + end + + result + end + + def column_blob( stmt, column ) + blob = API.sqlite3_column_blob( stmt, column ) + blob.free = nil + blob.to_s( API.sqlite3_column_bytes( stmt, column ) ) + end + + def result_text( func, text, utf16=false ) + method = case utf16 + when false, nil then :sqlite3_result_text + when :le then :sqlite3_result_text16le + when :be then :sqlite3_result_text16be + else :sqlite3_result_text16 + end + + s = text.to_s + API.send( method, func, s, s.length, TRANSIENT ) + end + + def busy_handler( db, data=nil, &block ) + @busy_handler = block + + unless @busy_handler_callback + @busy_handler_callback = ::DL.callback( "IPI" ) do |cookie, timeout| + @busy_handler.call( cookie, timeout ) || 0 + end + end + + API.sqlite3_busy_handler( db, block&&@busy_handler_callback, data ) + end + + def set_authorizer( db, data=nil, &block ) + @authorizer_handler = block + + unless @authorizer_handler_callback + @authorizer_handler_callback = ::DL.callback( "IPIPPPP" + ) do |cookie,mode,a,b,c,d| + @authorizer_handler.call( cookie, mode, + a&&a.to_s, b&&b.to_s, c&&c.to_s, d&&d.to_s ) || 0 + end + end + + API.sqlite3_set_authorizer( db, block&&@authorizer_handler_callback, + data ) + end + + def trace( db, data=nil, &block ) + @trace_handler = block + + unless @trace_handler_callback + @trace_handler_callback = ::DL.callback( "IPS" ) do |cookie,sql| + @trace_handler.call( cookie ? cookie.to_object : nil, sql ) || 0 + end + end + + API.sqlite3_trace( db, block&&@trace_handler_callback, data ) + end + + def create_function( db, name, args, text, cookie, + func, step, final ) + # begin + if @func_handler_callback.nil? && func + @func_handler_callback = ::DL.callback( "0PIP" ) do |context,nargs,args| + args = args.to_s(nargs*4).unpack("L*").map {|i| ::DL::PtrData.new(i)} + data = API.sqlite3_user_data( context ).to_object + data[:func].call( context, *args ) + end + end + + if @step_handler_callback.nil? && step + @step_handler_callback = ::DL.callback( "0PIP" ) do |context,nargs,args| + args = args.to_s(nargs*4).unpack("L*").map {|i| ::DL::PtrData.new(i)} + data = API.sqlite3_user_data( context ).to_object + data[:step].call( context, *args ) + end + end + + if @final_handler_callback.nil? && final + @final_handler_callback = ::DL.callback( "0P" ) do |context| + data = API.sqlite3_user_data( context ).to_object + data[:final].call( context ) + end + end + + data = { :cookie => cookie, + :name => name, + :func => func, + :step => step, + :final => final } + + API.sqlite3_create_function( db, name, args, text, data, + ( func ? @func_handler_callback : nil ), + ( step ? @step_handler_callback : nil ), + ( final ? @final_handler_callback : nil ) ) + end + + def aggregate_context( context ) + ptr = API.sqlite3_aggregate_context( context, 4 ) + ptr.free = nil + obj = ( ptr ? ptr.to_object : nil ) + if obj.nil? + obj = Hash.new + ptr.set_object obj + end + obj + end + + def bind_blob( stmt, index, value ) + s = value.to_s + API.sqlite3_bind_blob( stmt, index, s, s.length, TRANSIENT ) + end + + def bind_text( stmt, index, value, utf16=false ) + s = value.to_s + method = ( utf16 ? :sqlite3_bind_text16 : :sqlite3_bind_text ) + API.send( method, stmt, index, s, s.length, TRANSIENT ) + end + + def column_text( stmt, column ) + result = API.sqlite3_column_text( stmt, column ) + result ? result.to_s : nil + end + + def column_name( stmt, column ) + result = API.sqlite3_column_name( stmt, column ) + result ? result.to_s : nil + end + + def column_decltype( stmt, column ) + result = API.sqlite3_column_decltype( stmt, column ) + result ? result.to_s : nil + end + + def self.api_delegate( name ) + define_method( name ) { |*args| API.send( "sqlite3_#{name}", *args ) } + end + + api_delegate :aggregate_count + api_delegate :bind_double + api_delegate :bind_int + api_delegate :bind_null + api_delegate :bind_parameter_index + api_delegate :bind_parameter_name + api_delegate :busy_timeout + api_delegate :changes + api_delegate :close + api_delegate :column_bytes + api_delegate :column_bytes16 + api_delegate :column_count + api_delegate :column_double + api_delegate :column_int + api_delegate :column_int64 + api_delegate :column_type + api_delegate :data_count + api_delegate :errcode + api_delegate :finalize + api_delegate :interrupt + api_delegate :last_insert_rowid + api_delegate :libversion + api_delegate :reset + api_delegate :result_error + api_delegate :step + api_delegate :total_changes + api_delegate :value_bytes + api_delegate :value_bytes16 + api_delegate :value_double + api_delegate :value_int + api_delegate :value_int64 + api_delegate :value_type + + # ==== EXPERIMENTAL ==== + if defined?( EXPERIMENTAL_API ) && EXPERIMENTAL_API + def progress_handler( db, n, data=nil, &block ) + @progress_handler = block + + unless @progress_handler_callback + @progress_handler_callback = ::DL.callback( "IP" ) do |cookie| + @progress_handler.call( cookie ) + end + end + + API.sqlite3_progress_handler( db, n, block&&@progress_handler_callback, + data ) + end + + def commit_hook( db, data=nil, &block ) + @commit_hook_handler = block + + unless @commit_hook_handler_callback + @commit_hook_handler_callback = ::DL.callback( "IP" ) do |cookie| + @commit_hook_handler.call( cookie ) + end + end + + API.sqlite3_commit_hook( db, block&&@commit_hook_handler_callback, + data ) + end + end + + private + + def utf16_length(ptr) + len = 0 + loop do + break if ptr[len,1] == "\0" + len += 2 + end + len + end + + end + +end ; end ; end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/driver/native/driver.rb b/vendor/plugins/sqlite3-ruby/sqlite3/driver/native/driver.rb new file mode 100644 index 00000000..cef0cdce --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/driver/native/driver.rb @@ -0,0 +1,243 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3_api' + +module SQLite3 ; module Driver ; module Native + + class Driver + + def initialize + @callback_data = Hash.new + @authorizer = Hash.new + @busy_handler = Hash.new + @trace = Hash.new + end + + def complete?( sql, utf16=false ) + API.send( utf16 ? :sqlite3_complete16 : :sqlite3_complete, sql ) != 0 + end + + def busy_handler( db, data=nil, &block ) + if block + cb = API::CallbackData.new + cb.proc = block + cb.data = data + result = API.sqlite3_busy_handler( db, API::Sqlite3_ruby_busy_handler, cb ) + # Reference the Callback object so that + # it is not deleted by the GC + @busy_handler[db] = cb + else + # Unreference the callback *after* having removed it + # from sqlite + result = API.sqlite3_busy_handler( db, nil, nil ) + @busy_handler.delete(db) + end + + result + end + + def set_authorizer( db, data=nil, &block ) + if block + cb = API::CallbackData.new + cb.proc = block + cb.data = data + result = API.sqlite3_set_authorizer( db, API::Sqlite3_ruby_authorizer, cb ) + @authorizer[db] = cb # see comments in busy_handler + else + result = API.sqlite3_set_authorizer( db, nil, nil ) + @authorizer.delete(db) # see comments in busy_handler + end + + result + end + + def trace( db, data=nil, &block ) + if block + cb = API::CallbackData.new + cb.proc = block + cb.data = data + result = API.sqlite3_trace( db, API::Sqlite3_ruby_trace, cb ) + @trace[db] = cb # see comments in busy_handler + else + result = API.sqlite3_trace( db, nil, nil ) + @trace.delete(db) # see comments in busy_handler + end + + result + end + + def open( filename, utf16=false ) + API.send( utf16 ? :sqlite3_open16 : :sqlite3_open, filename ) + end + + def errmsg( db, utf16=false ) + API.send( utf16 ? :sqlite3_errmsg16 : :sqlite3_errmsg, db ) + end + + def prepare( db, sql, utf16=false ) + API.send( ( utf16 ? :sqlite3_prepare16 : :sqlite3_prepare ), + db, sql ) + end + + def bind_text( stmt, index, value, utf16=false ) + API.send( ( utf16 ? :sqlite3_bind_text16 : :sqlite3_bind_text ), + stmt, index, value.to_s ) + end + + def column_name( stmt, index, utf16=false ) + API.send( ( utf16 ? :sqlite3_column_name16 : :sqlite3_column_name ), + stmt, index ) + end + + def column_decltype( stmt, index, utf16=false ) + API.send( + ( utf16 ? :sqlite3_column_decltype16 : :sqlite3_column_decltype ), + stmt, index ) + end + + def column_text( stmt, index, utf16=false ) + API.send( ( utf16 ? :sqlite3_column_text16 : :sqlite3_column_text ), + stmt, index ) + end + + def create_function( db, name, args, text, cookie, func, step, final ) + if func || ( step && final ) + cb = API::CallbackData.new + cb.proc = cb.proc2 = nil + cb.data = cookie + end + + if func + cb.proc = func + + func = API::Sqlite3_ruby_function_step + step = final = nil + elsif step && final + cb.proc = step + cb.proc2 = final + + func = nil + step = API::Sqlite3_ruby_function_step + final = API::Sqlite3_ruby_function_final + end + + result = API.sqlite3_create_function( db, name, args, text, cb, func, step, final ) + + # see comments in busy_handler + if cb + @callback_data[ name ] = cb + else + @callback_data.delete( name ) + end + + return result + end + + def value_text( value, utf16=false ) + method = case utf16 + when nil, false then :sqlite3_value_text + when :le then :sqlite3_value_text16le + when :be then :sqlite3_value_text16be + else :sqlite3_value_text16 + end + + API.send( method, value ) + end + + def result_text( context, result, utf16=false ) + method = case utf16 + when nil, false then :sqlite3_result_text + when :le then :sqlite3_result_text16le + when :be then :sqlite3_result_text16be + else :sqlite3_result_text16 + end + + API.send( method, context, result.to_s ) + end + + def result_error( context, value, utf16=false ) + API.send( ( utf16 ? :sqlite3_result_error16 : :sqlite3_result_error ), + context, value ) + end + + def self.api_delegate( name ) + define_method( name ) { |*args| API.send( "sqlite3_#{name}", *args ) } + end + + api_delegate :libversion + api_delegate :close + api_delegate :last_insert_rowid + api_delegate :changes + api_delegate :total_changes + api_delegate :interrupt + api_delegate :busy_timeout + api_delegate :errcode + api_delegate :bind_blob + api_delegate :bind_double + api_delegate :bind_int + api_delegate :bind_int64 + api_delegate :bind_null + api_delegate :bind_parameter_count + api_delegate :bind_parameter_name + api_delegate :bind_parameter_index + api_delegate :column_count + api_delegate :step + api_delegate :data_count + api_delegate :column_blob + api_delegate :column_bytes + api_delegate :column_bytes16 + api_delegate :column_double + api_delegate :column_int + api_delegate :column_int64 + api_delegate :column_type + api_delegate :finalize + api_delegate :reset + api_delegate :aggregate_count + api_delegate :value_blob + api_delegate :value_bytes + api_delegate :value_bytes16 + api_delegate :value_double + api_delegate :value_int + api_delegate :value_int64 + api_delegate :value_type + api_delegate :result_blob + api_delegate :result_double + api_delegate :result_int + api_delegate :result_int64 + api_delegate :result_null + api_delegate :result_value + api_delegate :aggregate_context + + end + +end ; end ; end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/errors.rb b/vendor/plugins/sqlite3-ruby/sqlite3/errors.rb new file mode 100644 index 00000000..f83adc57 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/errors.rb @@ -0,0 +1,100 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/constants' + +module SQLite3 + + class Exception < ::Exception + @code = 0 + + # The numeric error code that this exception represents. + def self.code + @code + end + + # A convenience for accessing the error code for this exception. + def code + self.class.code + end + end + + class SQLException < Exception; end + class InternalException < Exception; end + class PermissionException < Exception; end + class AbortException < Exception; end + class BusyException < Exception; end + class LockedException < Exception; end + class MemoryException < Exception; end + class ReadOnlyException < Exception; end + class InterruptException < Exception; end + class IOException < Exception; end + class CorruptException < Exception; end + class NotFoundException < Exception; end + class FullException < Exception; end + class CantOpenException < Exception; end + class ProtocolException < Exception; end + class EmptyException < Exception; end + class SchemaChangedException < Exception; end + class TooBigException < Exception; end + class ConstraintException < Exception; end + class MismatchException < Exception; end + class MisuseException < Exception; end + class UnsupportedException < Exception; end + class AuthorizationException < Exception; end + class FormatException < Exception; end + class RangeException < Exception; end + class NotADatabaseException < Exception; end + + EXCEPTIONS = [ + nil, + SQLException, InternalException, PermissionException, + AbortException, BusyException, LockedException, MemoryException, + ReadOnlyException, InterruptException, IOException, CorruptException, + NotFoundException, FullException, CantOpenException, ProtocolException, + EmptyException, SchemaChangedException, TooBigException, + ConstraintException, MismatchException, MisuseException, + UnsupportedException, AuthorizationException, FormatException, + RangeException, NotADatabaseException + ].each_with_index { |e,i| e.instance_variable_set( :@code, i ) if e } + + module Error + def check( result, db=nil, msg=nil ) + unless result == Constants::ErrorCode::OK + msg = ( msg ? msg + ": " : "" ) + db.errmsg if db + raise(( EXCEPTIONS[result] || SQLite3::Exception ), msg) + end + end + module_function :check + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/pragmas.rb b/vendor/plugins/sqlite3-ruby/sqlite3/pragmas.rb new file mode 100644 index 00000000..72473871 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/pragmas.rb @@ -0,0 +1,254 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/errors' + +module SQLite3 + + # This module is intended for inclusion solely by the Database class. It + # defines convenience methods for the various pragmas supported by SQLite3. + # + # For a detailed description of these pragmas, see the SQLite3 documentation + # at http://sqlite.org/pragma.html. + module Pragmas + + # Returns +true+ or +false+ depending on the value of the named pragma. + def get_boolean_pragma( name ) + get_first_value( "PRAGMA #{name}" ) != "0" + end + private :get_boolean_pragma + + # Sets the given pragma to the given boolean value. The value itself + # may be +true+ or +false+, or any other commonly used string or + # integer that represents truth. + def set_boolean_pragma( name, mode ) + case mode + when String + case mode.downcase + when "on", "yes", "true", "y", "t": mode = "'ON'" + when "off", "no", "false", "n", "f": mode = "'OFF'" + else + raise Exception, + "unrecognized pragma parameter #{mode.inspect}" + end + when true, 1 + mode = "ON" + when false, 0, nil + mode = "OFF" + else + raise Exception, + "unrecognized pragma parameter #{mode.inspect}" + end + + execute( "PRAGMA #{name}=#{mode}" ) + end + private :set_boolean_pragma + + # Requests the given pragma (and parameters), and if the block is given, + # each row of the result set will be yielded to it. Otherwise, the results + # are returned as an array. + def get_query_pragma( name, *parms, &block ) # :yields: row + if parms.empty? + execute( "PRAGMA #{name}", &block ) + else + args = "'" + parms.join("','") + "'" + execute( "PRAGMA #{name}( #{args} )", &block ) + end + end + private :get_query_pragma + + # Return the value of the given pragma. + def get_enum_pragma( name ) + get_first_value( "PRAGMA #{name}" ) + end + private :get_enum_pragma + + # Set the value of the given pragma to +mode+. The +mode+ parameter must + # conform to one of the values in the given +enum+ array. Each entry in + # the array is another array comprised of elements in the enumeration that + # have duplicate values. See #synchronous, #default_synchronous, + # #temp_store, and #default_temp_store for usage examples. + def set_enum_pragma( name, mode, enums ) + match = enums.find { |p| p.find { |i| i.to_s.downcase == mode.to_s.downcase } } + raise Exception, + "unrecognized #{name} #{mode.inspect}" unless match + execute( "PRAGMA #{name}='#{match.first.upcase}'" ) + end + private :set_enum_pragma + + # Returns the value of the given pragma as an integer. + def get_int_pragma( name ) + get_first_value( "PRAGMA #{name}" ).to_i + end + private :get_int_pragma + + # Set the value of the given pragma to the integer value of the +value+ + # parameter. + def set_int_pragma( name, value ) + execute( "PRAGMA #{name}=#{value.to_i}" ) + end + private :set_int_pragma + + # The enumeration of valid synchronous modes. + SYNCHRONOUS_MODES = [ [ 'full', 2 ], [ 'normal', 1 ], [ 'off', 0 ] ] + + # The enumeration of valid temp store modes. + TEMP_STORE_MODES = [ [ 'default', 0 ], [ 'file', 1 ], [ 'memory', 2 ] ] + + # Does an integrity check on the database. If the check fails, a + # SQLite3::Exception will be raised. Otherwise it + # returns silently. + def integrity_check + execute( "PRAGMA integrity_check" ) do |row| + raise Exception, row[0] if row[0] != "ok" + end + end + + def auto_vacuum + get_boolean_pragma "auto_vacuum" + end + + def auto_vacuum=( mode ) + set_boolean_pragma "auto_vacuum", mode + end + + def schema_cookie + get_int_pragma "schema_cookie" + end + + def schema_cookie=( cookie ) + set_int_pragma "schema_cookie", cookie + end + + def user_cookie + get_int_pragma "user_cookie" + end + + def user_cookie=( cookie ) + set_int_pragma "user_cookie", cookie + end + + def cache_size + get_int_pragma "cache_size" + end + + def cache_size=( size ) + set_int_pragma "cache_size", size + end + + def default_cache_size + get_int_pragma "default_cache_size" + end + + def default_cache_size=( size ) + set_int_pragma "default_cache_size", size + end + + def default_synchronous + get_enum_pragma "default_synchronous" + end + + def default_synchronous=( mode ) + set_enum_pragma "default_synchronous", mode, SYNCHRONOUS_MODES + end + + def synchronous + get_enum_pragma "synchronous" + end + + def synchronous=( mode ) + set_enum_pragma "synchronous", mode, SYNCHRONOUS_MODES + end + + def default_temp_store + get_enum_pragma "default_temp_store" + end + + def default_temp_store=( mode ) + set_enum_pragma "default_temp_store", mode, TEMP_STORE_MODES + end + + def temp_store + get_enum_pragma "temp_store" + end + + def temp_store=( mode ) + set_enum_pragma "temp_store", mode, TEMP_STORE_MODES + end + + def full_column_names + get_boolean_pragma "full_column_names" + end + + def full_column_names=( mode ) + set_boolean_pragma "full_column_names", mode + end + + def parser_trace + get_boolean_pragma "parser_trace" + end + + def parser_trace=( mode ) + set_boolean_pragma "parser_trace", mode + end + + def vdbe_trace + get_boolean_pragma "vdbe_trace" + end + + def vdbe_trace=( mode ) + set_boolean_pragma "vdbe_trace", mode + end + + def database_list( &block ) # :yields: row + get_query_pragma "database_list", &block + end + + def foreign_key_list( table, &block ) # :yields: row + get_query_pragma "foreign_key_list", table, &block + end + + def index_info( index, &block ) # :yields: row + get_query_pragma "index_info", index, &block + end + + def index_list( table, &block ) # :yields: row + get_query_pragma "index_list", table, &block + end + + def table_info( table, &block ) # :yields: row + get_query_pragma "table_info", table, &block + end + + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/resultset.rb b/vendor/plugins/sqlite3-ruby/sqlite3/resultset.rb new file mode 100644 index 00000000..ed2cb582 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/resultset.rb @@ -0,0 +1,190 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/constants' +require 'sqlite3/errors' + +module SQLite3 + + # The ResultSet object encapsulates the enumerability of a query's output. + # It is a simple cursor over the data that the query returns. It will + # very rarely (if ever) be instantiated directly. Instead, client's should + # obtain a ResultSet instance via Statement#execute. + class ResultSet + include Enumerable + + # A trivial module for adding a +types+ accessor to an object. + module TypesContainer + attr_accessor :types + end + + # A trivial module for adding a +fields+ accessor to an object. + module FieldsContainer + attr_accessor :fields + end + + # Create a new ResultSet attached to the given database, using the + # given sql text. + def initialize( db, stmt ) + @db = db + @driver = @db.driver + @stmt = stmt + commence + end + + # A convenience method for compiling the virtual machine and stepping + # to the first row of the result set. + def commence + result = @driver.step( @stmt.handle ) + check result + @first_row = true + end + private :commence + + def check( result ) + @eof = ( result == Constants::ErrorCode::DONE ) + found = ( result == Constants::ErrorCode::ROW ) + Error.check( result, @db ) unless @eof || found + end + private :check + + # Reset the cursor, so that a result set which has reached end-of-file + # can be rewound and reiterated. + def reset( *bind_params ) + @stmt.must_be_open! + @stmt.reset!(false) + @driver.reset( @stmt.handle ) + @stmt.bind_params( *bind_params ) + @eof = false + commence + end + + # Query whether the cursor has reached the end of the result set or not. + def eof? + @eof + end + + # Obtain the next row from the cursor. If there are no more rows to be + # had, this will return +nil+. If type translation is active on the + # corresponding database, the values in the row will be translated + # according to their types. + # + # The returned value will be an array, unless Database#results_as_hash has + # been set to +true+, in which case the returned value will be a hash. + # + # For arrays, the column names are accessible via the +fields+ property, + # and the column types are accessible via the +types+ property. + # + # For hashes, the column names are the keys of the hash, and the column + # types are accessible via the +types+ property. + def next + return nil if @eof + + @stmt.must_be_open! + + unless @first_row + result = @driver.step( @stmt.handle ) + check result + end + + @first_row = false + + unless @eof + row = [] + @driver.data_count( @stmt.handle ).times do |column| + case @driver.column_type( @stmt.handle, column ) + when Constants::ColumnType::NULL then + row << nil + when Constants::ColumnType::BLOB then + row << @driver.column_blob( @stmt.handle, column ) + else + row << @driver.column_text( @stmt.handle, column ) + end + end + + if @db.type_translation + row = @stmt.types.zip( row ).map do |type, value| + @db.translator.translate( type, value ) + end + end + + if @db.results_as_hash + new_row = Hash[ *( @stmt.columns.zip( row ).flatten ) ] + row.each_with_index { |value,idx| new_row[idx] = value } + row = new_row + else + row.extend FieldsContainer unless row.respond_to?(:fields) + row.fields = @stmt.columns + end + + row.extend TypesContainer + row.types = @stmt.types + + return row + end + + nil + end + + # Required by the Enumerable mixin. Provides an internal iterator over the + # rows of the result set. + def each + while row=self.next + yield row + end + end + + # Closes the statement that spawned this result set. + # Use with caution! Closing a result set will automatically + # close any other result sets that were spawned from the same statement. + def close + @stmt.close + end + + # Queries whether the underlying statement has been closed or not. + def closed? + @stmt.closed? + end + + # Returns the types of the columns returned by this result set. + def types + @stmt.types + end + + # Returns the names of the columns returned by this result set. + def columns + @stmt.columns + end + + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/statement.rb b/vendor/plugins/sqlite3-ruby/sqlite3/statement.rb new file mode 100644 index 00000000..09f24d91 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/statement.rb @@ -0,0 +1,258 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/errors' +require 'sqlite3/resultset' + +class String + def to_blob + SQLite3::Blob.new( self ) + end +end + +module SQLite3 + + # A class for differentiating between strings and blobs, when binding them + # into statements. + class Blob < String; end + + # A statement represents a prepared-but-unexecuted SQL query. It will rarely + # (if ever) be instantiated directly by a client, and is most often obtained + # via the Database#prepare method. + class Statement + + # This is any text that followed the first valid SQL statement in the text + # with which the statement was initialized. If there was no trailing text, + # this will be the empty string. + attr_reader :remainder + + # The underlying opaque handle used to access the SQLite @driver. + attr_reader :handle + + # Create a new statement attached to the given Database instance, and which + # encapsulates the given SQL text. If the text contains more than one + # statement (i.e., separated by semicolons), then the #remainder property + # will be set to the trailing text. + def initialize( db, sql, utf16=false ) + @db = db + @driver = @db.driver + @closed = false + @results = @columns = nil + result, @handle, @remainder = @driver.prepare( @db.handle, sql ) + Error.check( result, @db ) + end + + # Closes the statement by finalizing the underlying statement + # handle. The statement must not be used after being closed. + def close + must_be_open! + @closed = true + @driver.finalize( @handle ) + end + + # Returns true if the underlying statement has been closed. + def closed? + @closed + end + + # Binds the given variables to the corresponding placeholders in the SQL + # text. + # + # See Database#execute for a description of the valid placeholder + # syntaxes. + # + # Example: + # + # stmt = db.prepare( "select * from table where a=? and b=?" ) + # stmt.bind_params( 15, "hello" ) + # + # See also #execute, #bind_param, Statement#bind_param, and + # Statement#bind_params. + def bind_params( *bind_vars ) + index = 1 + bind_vars.flatten.each do |var| + if Hash === var + var.each { |key, val| bind_param key, val } + else + bind_param index, var + index += 1 + end + end + end + + # Binds value to the named (or positional) placeholder. If +param+ is a + # Fixnum, it is treated as an index for a positional placeholder. + # Otherwise it is used as the name of the placeholder to bind to. + # + # See also #bind_params. + def bind_param( param, value ) + must_be_open! + reset! if active? + if Fixnum === param + case value + when Bignum then + @driver.bind_int64( @handle, param, value ) + when Integer then + @driver.bind_int( @handle, param, value ) + when Numeric then + @driver.bind_double( @handle, param, value.to_f ) + when Blob then + @driver.bind_blob( @handle, param, value ) + when nil then + @driver.bind_null( @handle, param ) + else + @driver.bind_text( @handle, param, value ) + end + else + param = param.to_s + param = ":#{param}" unless param[0] == ?: + index = @driver.bind_parameter_index( @handle, param ) + raise Exception, "no such bind parameter '#{param}'" if index == 0 + bind_param index, value + end + end + + # Execute the statement. This creates a new ResultSet object for the + # statement's virtual machine. If a block was given, the new ResultSet will + # be yielded to it; otherwise, the ResultSet will be returned. + # + # Any parameters will be bound to the statement using #bind_params. + # + # Example: + # + # stmt = db.prepare( "select * from table" ) + # stmt.execute do |result| + # ... + # end + # + # See also #bind_params, #execute!. + def execute( *bind_vars ) + must_be_open! + reset! if active? + + bind_params(*bind_vars) unless bind_vars.empty? + @results = ResultSet.new( @db, self ) + + if block_given? + yield @results + else + return @results + end + end + + # Execute the statement. If no block was given, this returns an array of + # rows returned by executing the statement. Otherwise, each row will be + # yielded to the block. + # + # Any parameters will be bound to the statement using #bind_params. + # + # Example: + # + # stmt = db.prepare( "select * from table" ) + # stmt.execute! do |row| + # ... + # end + # + # See also #bind_params, #execute. + def execute!( *bind_vars ) + result = execute( *bind_vars ) + rows = [] unless block_given? + while row = result.next + if block_given? + yield row + else + rows << row + end + end + rows + end + + # Resets the statement. This is typically done internally, though it might + # occassionally be necessary to manually reset the statement. + def reset!(clear_result=true) + @driver.reset(@handle) + @results = nil if clear_result + end + + # Returns true if the statement is currently active, meaning it has an + # open result set. + def active? + not @results.nil? + end + + # Return an array of the column names for this statement. Note that this + # may execute the statement in order to obtain the metadata; this makes it + # a (potentially) expensive operation. + def columns + get_metadata unless @columns + return @columns + end + + # Return an array of the data types for each column in this statement. Note + # that this may execute the statement in order to obtain the metadata; this + # makes it a (potentially) expensive operation. + def types + get_metadata unless @types + return @types + end + + # A convenience method for obtaining the metadata about the query. Note + # that this will actually execute the SQL, which means it can be a + # (potentially) expensive operation. + def get_metadata + must_be_open! + + @columns = [] + @types = [] + + column_count = @driver.column_count( @handle ) + column_count.times do |column| + @columns << @driver.column_name( @handle, column ) + @types << @driver.column_decltype( @handle, column ) + end + + @columns.freeze + @types.freeze + end + private :get_metadata + + # Performs a sanity check to ensure that the statement is not + # closed. If it is, an exception is raised. + def must_be_open! # :nodoc: + if @closed + raise SQLite3::Exception, "cannot use a closed statement" + end + end + + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/translator.rb b/vendor/plugins/sqlite3-ruby/sqlite3/translator.rb new file mode 100644 index 00000000..14d28b6c --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/translator.rb @@ -0,0 +1,136 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'time' + +module SQLite3 + + # The Translator class encapsulates the logic and callbacks necessary for + # converting string data to a value of some specified type. Every Database + # instance may have a Translator instance, in order to assist in type + # translation (Database#type_translation). + # + # Further, applications may define their own custom type translation logic + # by registering translator blocks with the corresponding database's + # translator instance (Database#translator). + class Translator + + # Create a new Translator instance. It will be preinitialized with default + # translators for most SQL data types. + def initialize + @translators = Hash.new( proc { |type,value| value } ) + register_default_translators + end + + # Add a new translator block, which will be invoked to process type + # translations to the given type. The type should be an SQL datatype, and + # may include parentheses (i.e., "VARCHAR(30)"). However, any parenthetical + # information is stripped off and discarded, so type translation decisions + # are made solely on the "base" type name. + # + # The translator block itself should accept two parameters, "type" and + # "value". In this case, the "type" is the full type name (including + # parentheses), so the block itself may include logic for changing how a + # type is translated based on the additional data. The "value" parameter + # is the (string) data to convert. + # + # The block should return the translated value. + def add_translator( type, &block ) # :yields: type, value + @translators[ type_name( type ) ] = block + end + + # Translate the given string value to a value of the given type. In the + # absense of an installed translator block for the given type, the value + # itself is always returned. Further, +nil+ values are never translated, + # and are always passed straight through regardless of the type parameter. + def translate( type, value ) + unless value.nil? + @translators[ type_name( type ) ].call( type, value ) + end + end + + # A convenience method for working with type names. This returns the "base" + # type name, without any parenthetical data. + def type_name( type ) + return "" if type.nil? + type = $1 if type =~ /^(.*?)\(/ + type.upcase + end + private :type_name + + # Register the default translators for the current Translator instance. + # This includes translators for most major SQL data types. + def register_default_translators + [ "date", + "datetime", + "time" ].each { |type| add_translator( type ) { |t,v| Time.parse( v ) } } + + [ "decimal", + "float", + "numeric", + "double", + "real", + "dec", + "fixed" ].each { |type| add_translator( type ) { |t,v| v.to_f } } + + [ "integer", + "smallint", + "mediumint", + "int", + "bigint" ].each { |type| add_translator( type ) { |t,v| v.to_i } } + + [ "bit", + "bool", + "boolean" ].each do |type| + add_translator( type ) do |t,v| + !( v.strip.gsub(/00+/,"0") == "0" || + v.downcase == "false" || + v.downcase == "f" || + v.downcase == "no" || + v.downcase == "n" ) + end + end + + add_translator( "timestamp" ) { |type, value| Time.at( value.to_i ) } + add_translator( "tinyint" ) do |type, value| + if type =~ /\(\s*1\s*\)/ + value.to_i == 1 + else + value.to_i + end + end + end + private :register_default_translators + + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/value.rb b/vendor/plugins/sqlite3-ruby/sqlite3/value.rb new file mode 100644 index 00000000..fb763763 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/value.rb @@ -0,0 +1,89 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/constants' + +module SQLite3 + + class Value + attr_reader :handle + + def initialize( db, handle ) + @driver = db.driver + @handle = handle + end + + def null? + type == :null + end + + def to_blob + @driver.value_blob( @handle ) + end + + def length( utf16=false ) + if utf16 + @driver.value_bytes16( @handle ) + else + @driver.value_bytes( @handle ) + end + end + + def to_f + @driver.value_double( @handle ) + end + + def to_i + @driver.value_int( @handle ) + end + + def to_int64 + @driver.value_int64( @handle ) + end + + def to_s( utf16=false ) + @driver.value_text( @handle, utf16 ) + end + + def type + case @driver.value_type( @handle ) + when Constants::ColumnType::INTEGER then :int + when Constants::ColumnType::FLOAT then :float + when Constants::ColumnType::TEXT then :text + when Constants::ColumnType::BLOB then :blob + when Constants::ColumnType::NULL then :null + end + end + + end + +end diff --git a/vendor/plugins/sqlite3-ruby/sqlite3/version.rb b/vendor/plugins/sqlite3-ruby/sqlite3/version.rb new file mode 100644 index 00000000..12e678b0 --- /dev/null +++ b/vendor/plugins/sqlite3-ruby/sqlite3/version.rb @@ -0,0 +1,45 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +module SQLite3 + + module Version + + MAJOR = 1 + MINOR = 2 + TINY = 0 + + STRING = [ MAJOR, MINOR, TINY ].join( "." ) + + end + +end diff --git a/vendor/rails/actionmailer/CHANGELOG b/vendor/rails/actionmailer/CHANGELOG new file mode 100644 index 00000000..34d1ac4b --- /dev/null +++ b/vendor/rails/actionmailer/CHANGELOG @@ -0,0 +1,257 @@ +*1.2.5* (August 10th, 2006) + +* Depend on Action Pack 1.12.5 + + +*1.2.4* (August 8th, 2006) + +* Backport of documentation enhancements. [Kevin Clark, Marcel Molina Jr] + +* Correct spurious documentation example code which results in a SyntaxError. [Marcel Molina Jr.] + +* Mailer template root applies to a class and its subclasses rather than acting globally. #5555 [somekool@gmail.com] + + +*1.2.3* (June 29th, 2006) + +* Depend on Action Pack 1.12.3 + + +*1.2.2* (June 27th, 2006) + +* Depend on Action Pack 1.12.2 + + +*1.2.1* (April 6th, 2005) + +* Be part of Rails 1.1.1 + + +*1.2.0* (March 27th, 2005) + +* Nil charset caused subject line to be improperly quoted in implicitly multipart messages #2662 [ehalvorsen+rails@runbox.com] + +* Parse content-type apart before using it so that sub-parts of the header can be set correctly #2918 [Jamis Buck] + +* Make custom headers work in subparts #4034 [elan@bluemandrill.com] + +* Template paths with dot chars in them no longer mess up implicit template selection for multipart messages #3332 [Chad Fowler] + +* Make sure anything with content-disposition of "attachment" is passed to the attachment presenter when parsing an email body [Jamis Buck] + +* Make sure TMail#attachments includes anything with content-disposition of "attachment", regardless of content-type [Jamis Buck] + + +*1.1.5* (December 13th, 2005) + +* Become part of Rails 1.0 + + +*1.1.4* (December 7th, 2005) + +* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.] + +* Stricter matching for implicitly multipart filenames excludes files ending in unsupported extensions (such as foo.rhtml.bak) and without a two-part content type (such as foo.text.rhtml or foo.text.really.plain.rhtml). #2398 [Dave Burt , Jeremy Kemper] + + +*1.1.3* (November 7th, 2005) + +* Allow Mailers to have custom initialize methods that set default instance variables for all mail actions #2563 [mrj@bigpond.net.au] + + +*1.1.2* (October 26th, 2005) + +* Upgraded to Action Pack 1.10.2 + + +*1.1.1* (October 19th, 2005) + +* Upgraded to Action Pack 1.10.1 + + +*1.1.0* (October 16th, 2005) + +* Update and extend documentation (rdoc) + +* Minero Aoki made TMail available to Rails/ActionMailer under the MIT license (instead of LGPL) [RubyConf '05] + +* Austin Ziegler made Text::Simple available to Rails/ActionMailer under a MIT-like licens [See rails ML, subject "Text::Format Licence Exception" on Oct 15, 2005] + +* Fix vendor require paths to prevent files being required twice + +* Don't add charset to content-type header for a part that contains subparts (for AOL compatibility) #2013 [John Long] + +* Preserve underscores when unquoting message bodies #1930 + +* Encode multibyte characters correctly #1894 + +* Multipart messages specify a MIME-Version header automatically #2003 [John Long] + +* Add a unified render method to ActionMailer (delegates to ActionView::Base#render) + +* Move mailer initialization to a separate (overridable) method, so that subclasses may alter the various defaults #1727 + +* Look at content-location header (if available) to determine filename of attachments #1670 + +* ActionMailer::Base.deliver(email) had been accidentally removed, but was documented in the Rails book #1849 + +* Fix problem with sendmail delivery where headers should be delimited by \n characters instead of \r\n, which confuses some mail readers #1742 [Kent Sibilev] + + +*1.0.1* (11 July, 2005) + +* Bind to Action Pack 1.9.1 + + +*1.0.0* (6 July, 2005) + +* Avoid adding nil header values #1392 + +* Better multipart support with implicit multipart/alternative and sorting of subparts [John Long] + +* Allow for nested parts in multipart mails #1570 [Flurin Egger] + +* Normalize line endings in outgoing mail bodies to "\n" #1536 [John Long] + +* Allow template to be explicitly specified #1448 [tuxie@dekadance.se] + +* Allow specific "multipart/xxx" content-type to be set on multipart messages #1412 [Flurin Egger] + +* Unquoted @ characters in headers are now accepted in spite of RFC 822 #1206 + +* Helper support (borrowed from ActionPack) + +* Silently ignore Errno::EINVAL errors when converting text. + +* Don't cause an error when parsing an encoded attachment name #1340 [lon@speedymac.com] + +* Nested multipart message parts are correctly processed in TMail::Mail#body + +* BCC headers are removed when sending via SMTP #1402 + +* Added 'content_type' accessor, to allow content type to be set on a per-message basis. content_type defaults to "text/plain". + +* Silently ignore Iconv::IllegalSequence errors when converting text #1341 [lon@speedymac.com] + +* Support attachments and multipart messages. + +* Added new accessors for the various mail properties. + +* Fix to only perform the charset conversion if a 'from' and a 'to' charset are given (make no assumptions about what the charset was) #1276 [Jamis Buck] + +* Fix attachments and content-type problems #1276 [Jamis Buck] + +* Fixed the TMail#body method to look at the content-transfer-encoding header and unquote the body according to the rules it specifies #1265 [Jamis Buck] + +* Added unquoting even if the iconv lib can't be loaded--in that case, only the charset conversion is skipped #1265 [Jamis Buck] + +* Added automatic decoding of base64 bodies #1214 [Jamis Buck] + +* Added that delivery errors are caught in a way so the mail is still returned whether the delivery was successful or not + +* Fixed that email address like "Jamis Buck, M.D." would cause the quoter to generate emails resulting in "bad address" errors from the mail server #1220 [Jamis Buck] + + +*0.9.1* (20th April, 2005) + +* Depend on Action Pack 1.8.1 + + +*0.9.0* (19th April, 2005) + +* Added that deliver_* will now return the email that was sent + +* Added that quoting to UTF-8 only happens if the characters used are in that range #955 [Jamis Buck] + +* Fixed quoting for all address headers, not just to #955 [Jamis Buck] + +* Fixed unquoting of emails that doesn't have an explicit charset #1036 [wolfgang@stufenlos.net] + + +*0.8.1* (27th March, 2005) + +* Fixed that if charset was found that the end of a mime part declaration TMail would throw an error #919 [lon@speedymac.com] + +* Fixed that TMail::Unquoter would fail to recognize quoting method if it was in lowercase #919 [lon@speedymac.com] + +* Fixed that TMail::Encoder would fail when it attempts to parse e-mail addresses which are encoded using something other than the messages encoding method #919 [lon@speedymac.com] + +* Added rescue for missing iconv library and throws warnings if subject/body is called on a TMail object without it instead + + +*0.8.0* (22th March, 2005) + +* Added framework support for processing incoming emails with an Action Mailer class. See example in README. + + +*0.7.1* (7th March, 2005) + +* Bind to newest Action Pack (1.5.1) + + +*0.7.0* (24th February, 2005) + +* Added support for charsets for both subject and body. The default charset is now UTF-8 #673 [Jamis Buck]. Examples: + + def iso_charset(recipient) + @recipients = recipient + @subject = "testing iso charsets" + @from = "system@loudthinking.com" + @body = "Nothing to see here." + @charset = "iso-8859-1" + end + + def unencoded_subject(recipient) + @recipients = recipient + @subject = "testing unencoded subject" + @from = "system@loudthinking.com" + @body = "Nothing to see here." + @encode_subject = false + @charset = "iso-8859-1" + end + + +*0.6.1* (January 18th, 2005) + +* Fixed sending of emails to use Tmail#from not the deprecated Tmail#from_address + + +*0.6* (January 17th, 2005) + +* Fixed that bcc and cc should be settable through @bcc and @cc -- not just @headers["Bcc"] and @headers["Cc"] #453 [Eric Hodel] + +* Fixed Action Mailer to be "warnings safe" so you can run with ruby -w and not get framework warnings #453 [Eric Hodel] + + +*0.5* + +* Added access to custom headers, like cc, bcc, and reply-to #268 [Andreas Schwarz]. Example: + + def post_notification(recipients, post) + @recipients = recipients + @from = post.author.email_address_with_name + @headers["bcc"] = SYSTEM_ADMINISTRATOR_EMAIL + @headers["reply-to"] = "notifications@example.com" + @subject = "[#{post.account.name} #{post.title}]" + @body["post"] = post + end + +*0.4* (5) + +* Consolidated the server configuration options into Base#server_settings= and expanded that with controls for authentication and more [Marten] + NOTE: This is an API change that could potentially break your application if you used the old application form. Please do change! + +* Added Base#deliveries as an accessor for an array of emails sent out through that ActionMailer class when using the :test delivery option. [Jeremy Kemper] + +* Added Base#perform_deliveries= which can be set to false to turn off the actual delivery of the email through smtp or sendmail. + This is especially useful for functional testing that shouldn't send off real emails, but still trigger delivery_* methods. + +* Added option to specify delivery method with Base#delivery_method=. Default is :smtp and :sendmail is currently the only other option. + Sendmail is assumed to be present at "/usr/sbin/sendmail" if that option is used. [Kent Sibilev] + +* Dropped "include TMail" as it added to much baggage into the default namespace (like Version) [Chad Fowler] + + +*0.3* + +* First release diff --git a/vendor/rails/actionmailer/MIT-LICENSE b/vendor/rails/actionmailer/MIT-LICENSE new file mode 100644 index 00000000..26f55e77 --- /dev/null +++ b/vendor/rails/actionmailer/MIT-LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2004 David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/rails/actionmailer/README b/vendor/rails/actionmailer/README new file mode 100755 index 00000000..8c85e1ae --- /dev/null +++ b/vendor/rails/actionmailer/README @@ -0,0 +1,148 @@ += Action Mailer -- Easy email delivery and testing + +Action Mailer is a framework for designing email-service layers. These layers +are used to consolidate code for sending out forgotten passwords, welcoming +wishes on signup, invoices for billing, and any other use case that requires +a written notification to either a person or another system. + +Additionally, an Action Mailer class can be used to process incoming email, +such as allowing a weblog to accept new posts from an email (which could even +have been sent from a phone). + +== Sending emails + +The framework works by setting up all the email details, except the body, +in methods on the service layer. Subject, recipients, sender, and timestamp +are all set up this way. An example of such a method: + + def signed_up(recipient) + recipients recipient + subject "[Signed up] Welcome #{recipient}" + from "system@loudthinking.com" + + body(:recipient => recipient) + end + +The body of the email is created by using an Action View template (regular +ERb) that has the content of the body hash parameter available as instance variables. +So the corresponding body template for the method above could look like this: + + Hello there, + + Mr. <%= @recipient %> + +And if the recipient was given as "david@loudthinking.com", the email +generated would look like this: + + Date: Sun, 12 Dec 2004 00:00:00 +0100 + From: system@loudthinking.com + To: david@loudthinking.com + Subject: [Signed up] Welcome david@loudthinking.com + + Hello there, + + Mr. david@loudthinking.com + +You never actually call the instance methods like signed_up directly. Instead, +you call class methods like deliver_* and create_* that are automatically +created for each instance method. So if the signed_up method sat on +ApplicationMailer, it would look like this: + + ApplicationMailer.create_signed_up("david@loudthinking.com") # => tmail object for testing + ApplicationMailer.deliver_signed_up("david@loudthinking.com") # sends the email + ApplicationMailer.new.signed_up("david@loudthinking.com") # won't work! + +== Receiving emails + +To receive emails, you need to implement a public instance method called receive that takes a +tmail object as its single parameter. The Action Mailer framework has a corresponding class method, +which is also called receive, that accepts a raw, unprocessed email as a string, which it then turns +into the tmail object and calls the receive instance method. + +Example: + + class Mailman < ActionMailer::Base + def receive(email) + page = Page.find_by_address(email.to.first) + page.emails.create( + :subject => email.subject, :body => email.body + ) + + if email.has_attachments? + for attachment in email.attachments + page.attachments.create({ + :file => attachment, :description => email.subject + }) + end + end + end + end + +This Mailman can be the target for Postfix. In Rails, you would use the runner like this: + + ./script/runner 'Mailman.receive(STDIN.read)' + +== Configuration + +The Base class has the full list of configuration options. Here's an example: + +ActionMailer::Base.server_settings = { + :address=>'smtp.yourserver.com', # default: localhost + :port=>'25', # default: 25 + :user_name=>'user', + :password=>'pass', + :authentication=>:plain # :plain, :login or :cram_md5 +} + +== Dependencies + +Action Mailer requires that the Action Pack is either available to be required immediately +or is accessible as a GEM. + + +== Bundled software + +* tmail 0.10.8 by Minero Aoki released under LGPL + Read more on http://i.loveruby.net/en/prog/tmail.html + +* Text::Format 0.63 by Austin Ziegler released under OpenSource + Read more on http://www.halostatue.ca/ruby/Text__Format.html + + +== Download + +The latest version of Action Mailer can be found at + +* http://rubyforge.org/project/showfiles.php?group_id=361 + +Documentation can be found at + +* http://actionmailer.rubyonrails.org + + +== Installation + +You can install Action Mailer with the following command. + + % [sudo] ruby install.rb + +from its distribution directory. + + +== License + +Action Mailer is released under the MIT license. + + +== Support + +The Action Mailer homepage is http://actionmailer.rubyonrails.org. You can find +the Action Mailer RubyForge page at http://rubyforge.org/projects/actionmailer. +And as Jim from Rake says: + + Feel free to submit commits or feature requests. If you send a patch, + remember to update the corresponding unit tests. If fact, I prefer + new feature to be submitted in the form of new unit tests. + +For other information, feel free to ask on the ruby-talk mailing list (which +is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com. diff --git a/vendor/rails/actionmailer/Rakefile b/vendor/rails/actionmailer/Rakefile new file mode 100755 index 00000000..6095c6dc --- /dev/null +++ b/vendor/rails/actionmailer/Rakefile @@ -0,0 +1,95 @@ +require 'rubygems' +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/contrib/rubyforgepublisher' +require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version') + +PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' +PKG_NAME = 'actionmailer' +PKG_VERSION = ActionMailer::VERSION::STRING + PKG_BUILD +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" + +RELEASE_NAME = "REL #{PKG_VERSION}" + +RUBY_FORGE_PROJECT = "actionmailer" +RUBY_FORGE_USER = "webster132" + +desc "Default Task" +task :default => [ :test ] + +# Run the unit tests +Rake::TestTask.new { |t| + t.libs << "test" + t.pattern = 'test/*_test.rb' + t.verbose = true + t.warning = false +} + + +# Genereate the RDoc documentation +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "Action Mailer -- Easy email delivery and testing" + rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.rdoc_files.include('README', 'CHANGELOG') + rdoc.rdoc_files.include('lib/action_mailer.rb') + rdoc.rdoc_files.include('lib/action_mailer/*.rb') +} + + +# Create compressed packages +spec = Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = PKG_NAME + s.summary = "Service layer for easy email delivery and testing." + s.description = %q{Makes it trivial to test and deliver emails sent from a single service layer.} + s.version = PKG_VERSION + + s.author = "David Heinemeier Hansson" + s.email = "david@loudthinking.com" + s.rubyforge_project = "actionmailer" + s.homepage = "http://www.rubyonrails.org" + + s.add_dependency('actionpack', '= 1.12.5' + PKG_BUILD) + + s.has_rdoc = true + s.requirements << 'none' + s.require_path = 'lib' + s.autorequire = 'action_mailer' + + s.files = [ "Rakefile", "install.rb", "README", "CHANGELOG", "MIT-LICENSE" ] + s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) } + s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "\.svn" ) } +end + +Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = true + p.need_zip = true +end + + +desc "Publish the API documentation" +task :pgem => [:package] do + Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload +end + +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/am", "doc").upload +end + +desc "Publish the release files to RubyForge." +task :release => [ :package ] do + `rubyforge login` + + for ext in %w( gem tgz zip ) + release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" + puts release_command + system(release_command) + end +end \ No newline at end of file diff --git a/vendor/rails/actionmailer/install.rb b/vendor/rails/actionmailer/install.rb new file mode 100644 index 00000000..c559edff --- /dev/null +++ b/vendor/rails/actionmailer/install.rb @@ -0,0 +1,30 @@ +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +# this was adapted from rdoc's install.rb by way of Log4r + +$sitedir = CONFIG["sitelibdir"] +unless $sitedir + version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] + $libdir = File.join(CONFIG["libdir"], "ruby", version) + $sitedir = $:.find {|x| x =~ /site_ruby/ } + if !$sitedir + $sitedir = File.join($libdir, "site_ruby") + elsif $sitedir !~ Regexp.quote(version) + $sitedir = File.join($sitedir, version) + end +end + +# the acual gruntwork +Dir.chdir("lib") + +Find.find("action_mailer", "action_mailer.rb") { |f| + if f[-3..-1] == ".rb" + File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) + else + File::makedirs(File.join($sitedir, *f.split(/\//))) + end +} diff --git a/vendor/rails/actionmailer/lib/action_mailer.rb b/vendor/rails/actionmailer/lib/action_mailer.rb new file mode 100755 index 00000000..a489e32b --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer.rb @@ -0,0 +1,51 @@ +#-- +# Copyright (c) 2004 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +begin + require 'action_controller' +rescue LoadError + begin + require File.dirname(__FILE__) + '/../../actionpack/lib/action_controller' + rescue LoadError + require 'rubygems' + require_gem 'actionpack', '>= 1.9.1' + end +end + +$:.unshift(File.dirname(__FILE__) + "/action_mailer/vendor/") + +require 'action_mailer/base' +require 'action_mailer/helpers' +require 'action_mailer/mail_helper' +require 'action_mailer/quoting' +require 'tmail' +require 'net/smtp' + +ActionMailer::Base.class_eval do + include ActionMailer::Quoting + include ActionMailer::Helpers + + helper MailHelper +end + +silence_warnings { TMail::Encoder.const_set("MAX_LINE_LEN", 200) } \ No newline at end of file diff --git a/vendor/rails/actionmailer/lib/action_mailer/adv_attr_accessor.rb b/vendor/rails/actionmailer/lib/action_mailer/adv_attr_accessor.rb new file mode 100644 index 00000000..50afe4d7 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/adv_attr_accessor.rb @@ -0,0 +1,31 @@ +module ActionMailer + module AdvAttrAccessor #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + module ClassMethods #:nodoc: + def adv_attr_accessor(*names) + names.each do |name| + ivar = "@#{name}" + + define_method("#{name}=") do |value| + instance_variable_set(ivar, value) + end + + define_method(name) do |*parameters| + raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1 + if parameters.empty? + if instance_variables.include?(ivar) + instance_variable_get(ivar) + end + else + instance_variable_set(ivar, parameters.first) + end + end + end + end + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/base.rb b/vendor/rails/actionmailer/lib/action_mailer/base.rb new file mode 100644 index 00000000..a67072a9 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/base.rb @@ -0,0 +1,528 @@ +require 'action_mailer/adv_attr_accessor' +require 'action_mailer/part' +require 'action_mailer/part_container' +require 'action_mailer/utils' +require 'tmail/net' + +module ActionMailer #:nodoc: + # ActionMailer allows you to send email from your application using a mailer model and views. + # + # = Mailer Models + # To use ActionMailer, you need to create a mailer model. + # + # $ script/generate mailer Notifier + # + # The generated model inherits from ActionMailer::Base. Emails are defined by creating methods within the model which are then + # used to set variables to be used in the mail template, to change options on the mail, or + # to add attachments. + # + # Examples: + # + # class Notifier < ActionMailer::Base + # def signup_notification(recipient) + # recipients recipient.email_address_with_name + # from "system@example.com" + # subject "New account information" + # body "account" => recipient + # end + # end + # + # Mailer methods have the following configuration methods available. + # + # * recipients - Takes one or more email addresses. These addresses are where your email will be delivered to. Sets the To: header. + # * subject - The subject of your email. Sets the Subject: header. + # * from - Who the email you are sending is from. Sets the From: header. + # * cc - Takes one or more email addresses. These addresses will receive a carbon copy of your email. Sets the Cc: header. + # * bcc - Takes one or more email address. These addresses will receive a blind carbon copy of your email. Sets the Bcc header. + # * sent_on - The date on which the message was sent. If not set, the header wil be set by the delivery agent. + # * content_type - Specify the content type of the message. Defaults to text/plain. + # * headers - Specify additional headers to be set for the message, e.g. headers 'X-Mail-Count' => 107370. + # + # The body method has special behavior. It takes a hash which generates an instance variable + # named after each key in the hash containing the value that that key points to. + # + # So, for example, body "account" => recipient would result + # in an instance variable @account with the value of recipient being accessible in the + # view. + # + # = Mailer Views + # Like ActionController, each mailer class has a corresponding view directory + # in which each method of the class looks for a template with its name. + # To define a template to be used with a mailing, create an .rhtml file with the same name as the method + # in your mailer model. For example, in the mailer defined above, the template at + # app/views/notifier/signup_notification.rhtml would be used to generate the email. + # + # Variables defined in the model are accessible as instance variables in the view. + # + # Emails by default are sent in plain text, so a sample view for our model example might look like this: + # + # Hi <%= @account.name %>, + # Thanks for joining our service! Please check back often. + # + # = Sending Mail + # Once a mailer action and template are defined, you can deliver your message or create it and save it + # for delivery later: + # + # Notifier.deliver_signup_notification(david) # sends the email + # mail = Notifier.create_signup_notification(david) # => a tmail object + # Notifier.deliver(mail) + # + # You never instantiate your mailer class. Rather, your delivery instance + # methods are automatically wrapped in class methods that start with the word + # deliver_ followed by the name of the mailer method that you would + # like to deliver. The signup_notification method defined above is + # delivered by invoking Notifier.deliver_signup_notification. + # + # = HTML Email + # To send mail as HTML, make sure your view (the .rhtml file) generates HTML and + # set the content type to html. + # + # class MyMailer < ActionMailer::Base + # def signup_notification(recipient) + # recipients recipient.email_address_with_name + # subject "New account information" + # body "account" => recipient + # from "system@example.com" + # content_type "text/html" # Here's where the magic happens + # end + # end + # + # = Multipart Email + # You can explicitly specify multipart messages: + # + # class ApplicationMailer < ActionMailer::Base + # def signup_notification(recipient) + # recipients recipient.email_address_with_name + # subject "New account information" + # from "system@example.com" + # + # part :content_type => "text/html", + # :body => render_message("signup-as-html", :account => recipient) + # + # part "text/plain" do |p| + # p.body = render_message("signup-as-plain", :account => recipient) + # p.transfer_encoding = "base64" + # end + # end + # end + # + # Multipart messages can also be used implicitly because ActionMailer will automatically + # detect and use multipart templates, where each template is named after the name of the action, followed + # by the content type. Each such detected template will be added as separate part to the message. + # + # For example, if the following templates existed: + # * signup_notification.text.plain.rhtml + # * signup_notification.text.html.rhtml + # * signup_notification.text.xml.rxml + # * signup_notification.text.x-yaml.rhtml + # + # Each would be rendered and added as a separate part to the message, + # with the corresponding content type. The same body hash is passed to + # each template. + # + # = Attachments + # Attachments can be added by using the +attachment+ method. + # + # Example: + # + # class ApplicationMailer < ActionMailer::Base + # # attachments + # def signup_notification(recipient) + # recipients recipient.email_address_with_name + # subject "New account information" + # from "system@example.com" + # + # attachment :content_type => "image/jpeg", + # :body => File.read("an-image.jpg") + # + # attachment "application/pdf" do |a| + # a.body = generate_your_pdf_here() + # end + # end + # end + # + # = Configuration options + # + # These options are specified on the class level, like ActionMailer::Base.template_root = "/my/templates" + # + # * template_root - template root determines the base from which template references will be made. + # + # * logger - the logger is used for generating information on the mailing run if available. + # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers. + # + # * server_settings - Allows detailed configuration of the server: + # * :address Allows you to use a remote mail server. Just change it from its default "localhost" setting. + # * :port On the off chance that your mail server doesn't run on port 25, you can change it. + # * :domain If you need to specify a HELO domain, you can do it here. + # * :user_name If your mail server requires authentication, set the username in this setting. + # * :password If your mail server requires authentication, set the password in this setting. + # * :authentication If your mail server requires authentication, you need to specify the authentication type here. + # This is a symbol and one of :plain, :login, :cram_md5 + # + # * raise_delivery_errors - whether or not errors should be raised if the email fails to be delivered. + # + # * delivery_method - Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test. + # Sendmail is assumed to be present at "/usr/sbin/sendmail". + # + # * perform_deliveries - Determines whether deliver_* methods are actually carried out. By default they are, + # but this can be turned off to help functional testing. + # + # * deliveries - Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful + # for unit and functional testing. + # + # * default_charset - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also + # pick a different charset from inside a method with @charset. + # * default_content_type - The default content type used for the main part of the message. Defaults to "text/plain". You + # can also pick a different content type from inside a method with @content_type. + # * default_mime_version - The default mime version used for the message. Defaults to nil. You + # can also pick a different value from inside a method with @mime_version. When multipart messages are in + # use, @mime_version will be set to "1.0" if it is not set inside a method. + # * default_implicit_parts_order - When a message is built implicitly (i.e. multiple parts are assembled from templates + # which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to + # ["text/html", "text/enriched", "text/plain"]. Items that appear first in the array have higher priority in the mail client + # and appear last in the mime encoded message. You can also pick a different order from inside a method with + # @implicit_parts_order. + class Base + include AdvAttrAccessor, PartContainer + + # Action Mailer subclasses should be reloaded by the dispatcher in Rails + # when Dependencies.mechanism = :load. + include Reloadable::Subclasses + + private_class_method :new #:nodoc: + + class_inheritable_accessor :template_root + cattr_accessor :logger + + @@server_settings = { + :address => "localhost", + :port => 25, + :domain => 'localhost.localdomain', + :user_name => nil, + :password => nil, + :authentication => nil + } + cattr_accessor :server_settings + + @@raise_delivery_errors = true + cattr_accessor :raise_delivery_errors + + @@delivery_method = :smtp + cattr_accessor :delivery_method + + @@perform_deliveries = true + cattr_accessor :perform_deliveries + + @@deliveries = [] + cattr_accessor :deliveries + + @@default_charset = "utf-8" + cattr_accessor :default_charset + + @@default_content_type = "text/plain" + cattr_accessor :default_content_type + + @@default_mime_version = nil + cattr_accessor :default_mime_version + + @@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ] + cattr_accessor :default_implicit_parts_order + + # Specify the BCC addresses for the message + adv_attr_accessor :bcc + + # Define the body of the message. This is either a Hash (in which case it + # specifies the variables to pass to the template when it is rendered), + # or a string, in which case it specifies the actual text of the message. + adv_attr_accessor :body + + # Specify the CC addresses for the message. + adv_attr_accessor :cc + + # Specify the charset to use for the message. This defaults to the + # +default_charset+ specified for ActionMailer::Base. + adv_attr_accessor :charset + + # Specify the content type for the message. This defaults to text/plain + # in most cases, but can be automatically set in some situations. + adv_attr_accessor :content_type + + # Specify the from address for the message. + adv_attr_accessor :from + + # Specify additional headers to be added to the message. + adv_attr_accessor :headers + + # Specify the order in which parts should be sorted, based on content-type. + # This defaults to the value for the +default_implicit_parts_order+. + adv_attr_accessor :implicit_parts_order + + # Override the mailer name, which defaults to an inflected version of the + # mailer's class name. If you want to use a template in a non-standard + # location, you can use this to specify that location. + adv_attr_accessor :mailer_name + + # Defaults to "1.0", but may be explicitly given if needed. + adv_attr_accessor :mime_version + + # The recipient addresses for the message, either as a string (for a single + # address) or an array (for multiple addresses). + adv_attr_accessor :recipients + + # The date on which the message was sent. If not set (the default), the + # header will be set by the delivery agent. + adv_attr_accessor :sent_on + + # Specify the subject of the message. + adv_attr_accessor :subject + + # Specify the template name to use for current message. This is the "base" + # template name, without the extension or directory, and may be used to + # have multiple mailer methods share the same template. + adv_attr_accessor :template + + # The mail object instance referenced by this mailer. + attr_reader :mail + + class << self + def method_missing(method_symbol, *parameters)#:nodoc: + case method_symbol.id2name + when /^create_([_a-z]\w*)/ then new($1, *parameters).mail + when /^deliver_([_a-z]\w*)/ then new($1, *parameters).deliver! + when "new" then nil + else super + end + end + + # Receives a raw email, parses it into an email object, decodes it, + # instantiates a new mailer, and passes the email object to the mailer + # object's #receive method. If you want your mailer to be able to + # process incoming messages, you'll need to implement a #receive + # method that accepts the email object as a parameter: + # + # class MyMailer < ActionMailer::Base + # def receive(mail) + # ... + # end + # end + def receive(raw_email) + logger.info "Received mail:\n #{raw_email}" unless logger.nil? + mail = TMail::Mail.parse(raw_email) + mail.base64_decode + new.receive(mail) + end + + # Deliver the given mail object directly. This can be used to deliver + # a preconstructed mail object, like: + # + # email = MyMailer.create_some_mail(parameters) + # email.set_some_obscure_header "frobnicate" + # MyMailer.deliver(email) + def deliver(mail) + new.deliver!(mail) + end + end + + # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer + # will be initialized according to the named method. If not, the mailer will + # remain uninitialized (useful when you only need to invoke the "receive" + # method, for instance). + def initialize(method_name=nil, *parameters) #:nodoc: + create!(method_name, *parameters) if method_name + end + + # Initialize the mailer via the given +method_name+. The body will be + # rendered and a new TMail::Mail object created. + def create!(method_name, *parameters) #:nodoc: + initialize_defaults(method_name) + send(method_name, *parameters) + + # If an explicit, textual body has not been set, we check assumptions. + unless String === @body + # First, we look to see if there are any likely templates that match, + # which include the content-type in their file name (i.e., + # "the_template_file.text.html.rhtml", etc.). Only do this if parts + # have not already been specified manually. + if @parts.empty? + templates = Dir.glob("#{template_path}/#{@template}.*") + templates.each do |path| + # TODO: don't hardcode rhtml|rxml + basename = File.basename(path) + next unless md = /^([^\.]+)\.([^\.]+\.[^\+]+)\.(rhtml|rxml)$/.match(basename) + template_name = basename + content_type = md.captures[1].gsub('.', '/') + @parts << Part.new(:content_type => content_type, + :disposition => "inline", :charset => charset, + :body => render_message(template_name, @body)) + end + unless @parts.empty? + @content_type = "multipart/alternative" + @parts = sort_parts(@parts, @implicit_parts_order) + end + end + + # Then, if there were such templates, we check to see if we ought to + # also render a "normal" template (without the content type). If a + # normal template exists (or if there were no implicit parts) we render + # it. + template_exists = @parts.empty? + template_exists ||= Dir.glob("#{template_path}/#{@template}.*").any? { |i| File.basename(i).split(".").length == 2 } + @body = render_message(@template, @body) if template_exists + + # Finally, if there are other message parts and a textual body exists, + # we shift it onto the front of the parts and set the body to nil (so + # that create_mail doesn't try to render it in addition to the parts). + if !@parts.empty? && String === @body + @parts.unshift Part.new(:charset => charset, :body => @body) + @body = nil + end + end + + # If this is a multipart e-mail add the mime_version if it is not + # already set. + @mime_version ||= "1.0" if !@parts.empty? + + # build the mail object itself + @mail = create_mail + end + + # Delivers a TMail::Mail object. By default, it delivers the cached mail + # object (from the #create! method). If no cached mail object exists, and + # no alternate has been given as the parameter, this will fail. + def deliver!(mail = @mail) + raise "no mail object available for delivery!" unless mail + logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil? + + begin + send("perform_delivery_#{delivery_method}", mail) if perform_deliveries + rescue Object => e + raise e if raise_delivery_errors + end + + return mail + end + + private + # Set up the default values for the various instance variables of this + # mailer. Subclasses may override this method to provide different + # defaults. + def initialize_defaults(method_name) + @charset ||= @@default_charset.dup + @content_type ||= @@default_content_type.dup + @implicit_parts_order ||= @@default_implicit_parts_order.dup + @template ||= method_name + @mailer_name ||= Inflector.underscore(self.class.name) + @parts ||= [] + @headers ||= {} + @body ||= {} + @mime_version = @@default_mime_version.dup if @@default_mime_version + end + + def render_message(method_name, body) + render :file => method_name, :body => body + end + + def render(opts) + body = opts.delete(:body) + initialize_template_class(body).render(opts) + end + + def template_path + "#{template_root}/#{mailer_name}" + end + + def initialize_template_class(assigns) + ActionView::Base.new(template_path, assigns, self) + end + + def sort_parts(parts, order = []) + order = order.collect { |s| s.downcase } + + parts = parts.sort do |a, b| + a_ct = a.content_type.downcase + b_ct = b.content_type.downcase + + a_in = order.include? a_ct + b_in = order.include? b_ct + + s = case + when a_in && b_in + order.index(a_ct) <=> order.index(b_ct) + when a_in + -1 + when b_in + 1 + else + a_ct <=> b_ct + end + + # reverse the ordering because parts that come last are displayed + # first in mail clients + (s * -1) + end + + parts + end + + def create_mail + m = TMail::Mail.new + + m.subject, = quote_any_if_necessary(charset, subject) + m.to, m.from = quote_any_address_if_necessary(charset, recipients, from) + m.bcc = quote_address_if_necessary(bcc, charset) unless bcc.nil? + m.cc = quote_address_if_necessary(cc, charset) unless cc.nil? + + m.mime_version = mime_version unless mime_version.nil? + m.date = sent_on.to_time rescue sent_on if sent_on + headers.each { |k, v| m[k] = v } + + real_content_type, ctype_attrs = parse_content_type + + if @parts.empty? + m.set_content_type(real_content_type, nil, ctype_attrs) + m.body = Utils.normalize_new_lines(body) + else + if String === body + part = TMail::Mail.new + part.body = Utils.normalize_new_lines(body) + part.set_content_type(real_content_type, nil, ctype_attrs) + part.set_content_disposition "inline" + m.parts << part + end + + @parts.each do |p| + part = (TMail::Mail === p ? p : p.to_mail(self)) + m.parts << part + end + + if real_content_type =~ /multipart/ + ctype_attrs.delete "charset" + m.set_content_type(real_content_type, nil, ctype_attrs) + end + end + + @mail = m + end + + def perform_delivery_smtp(mail) + destinations = mail.destinations + mail.ready_to_send + + Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain], + server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp| + smtp.sendmail(mail.encoded, mail.from, destinations) + end + end + + def perform_delivery_sendmail(mail) + IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm| + sm.print(mail.encoded.gsub(/\r/, '')) + sm.flush + end + end + + def perform_delivery_test(mail) + deliveries << mail + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/helpers.rb b/vendor/rails/actionmailer/lib/action_mailer/helpers.rb new file mode 100644 index 00000000..b53326ca --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/helpers.rb @@ -0,0 +1,115 @@ +module ActionMailer + module Helpers #:nodoc: + def self.append_features(base) #:nodoc: + super + + # Initialize the base module to aggregate its helpers. + base.class_inheritable_accessor :master_helper_module + base.master_helper_module = Module.new + + # Extend base with class methods to declare helpers. + base.extend(ClassMethods) + + base.class_eval do + # Wrap inherited to create a new master helper module for subclasses. + class << self + alias_method :inherited_without_helper, :inherited + alias_method :inherited, :inherited_with_helper + end + + # Wrap initialize_template_class to extend new template class + # instances with the master helper module. + alias_method :initialize_template_class_without_helper, :initialize_template_class + alias_method :initialize_template_class, :initialize_template_class_with_helper + end + end + + module ClassMethods + # Makes all the (instance) methods in the helper module available to templates rendered through this controller. + # See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules + # available to the templates. + def add_template_helper(helper_module) #:nodoc: + master_helper_module.module_eval "include #{helper_module}" + end + + # Declare a helper: + # helper :foo + # requires 'foo_helper' and includes FooHelper in the template class. + # helper FooHelper + # includes FooHelper in the template class. + # helper { def foo() "#{bar} is the very best" end } + # evaluates the block in the template class, adding method #foo. + # helper(:three, BlindHelper) { def mice() 'mice' end } + # does all three. + def helper(*args, &block) + args.flatten.each do |arg| + case arg + when Module + add_template_helper(arg) + when String, Symbol + file_name = arg.to_s.underscore + '_helper' + class_name = file_name.camelize + + begin + require_dependency(file_name) + rescue LoadError => load_error + requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1] + msg = (requiree == file_name) ? "Missing helper file helpers/#{file_name}.rb" : "Can't load file: #{requiree}" + raise LoadError.new(msg).copy_blame!(load_error) + end + + add_template_helper(class_name.constantize) + else + raise ArgumentError, 'helper expects String, Symbol, or Module argument' + end + end + + # Evaluate block in template class if given. + master_helper_module.module_eval(&block) if block_given? + end + + # Declare a controller method as a helper. For example, + # helper_method :link_to + # def link_to(name, options) ... end + # makes the link_to controller method available in the view. + def helper_method(*methods) + methods.flatten.each do |method| + master_helper_module.module_eval <<-end_eval + def #{method}(*args, &block) + controller.send(%(#{method}), *args, &block) + end + end_eval + end + end + + # Declare a controller attribute as a helper. For example, + # helper_attr :name + # attr_accessor :name + # makes the name and name= controller methods available in the view. + # The is a convenience wrapper for helper_method. + def helper_attr(*attrs) + attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") } + end + + private + def inherited_with_helper(child) + inherited_without_helper(child) + begin + child.master_helper_module = Module.new + child.master_helper_module.send :include, master_helper_module + child.helper child.name.underscore + rescue MissingSourceFile => e + raise unless e.is_missing?("helpers/#{child.name.underscore}_helper") + end + end + end + + private + # Extend the template class instance with our controller's helper module. + def initialize_template_class_with_helper(assigns) + returning(template = initialize_template_class_without_helper(assigns)) do + template.extend self.class.master_helper_module + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/actionmailer/lib/action_mailer/mail_helper.rb b/vendor/rails/actionmailer/lib/action_mailer/mail_helper.rb new file mode 100644 index 00000000..11fd7d77 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/mail_helper.rb @@ -0,0 +1,19 @@ +require 'text/format' + +module MailHelper + # Uses Text::Format to take the text and format it, indented two spaces for + # each line, and wrapped at 72 columns. + def block_format(text) + formatted = text.split(/\n\r\n/).collect { |paragraph| + Text::Format.new( + :columns => 72, :first_indent => 2, :body_indent => 2, :text => paragraph + ).format + }.join("\n") + + # Make list points stand on their own line + formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" } + formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" } + + formatted + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/part.rb b/vendor/rails/actionmailer/lib/action_mailer/part.rb new file mode 100644 index 00000000..31f5b441 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/part.rb @@ -0,0 +1,113 @@ +require 'action_mailer/adv_attr_accessor' +require 'action_mailer/part_container' +require 'action_mailer/utils' + +module ActionMailer + # Represents a subpart of an email message. It shares many similar + # attributes of ActionMailer::Base. Although you can create parts manually + # and add them to the #parts list of the mailer, it is easier + # to use the helper methods in ActionMailer::PartContainer. + class Part + include ActionMailer::AdvAttrAccessor + include ActionMailer::PartContainer + + # Represents the body of the part, as a string. This should not be a + # Hash (like ActionMailer::Base), but if you want a template to be rendered + # into the body of a subpart you can do it with the mailer's #render method + # and assign the result here. + adv_attr_accessor :body + + # Specify the charset for this subpart. By default, it will be the charset + # of the containing part or mailer. + adv_attr_accessor :charset + + # The content disposition of this part, typically either "inline" or + # "attachment". + adv_attr_accessor :content_disposition + + # The content type of the part. + adv_attr_accessor :content_type + + # The filename to use for this subpart (usually for attachments). + adv_attr_accessor :filename + + # Accessor for specifying additional headers to include with this part. + adv_attr_accessor :headers + + # The transfer encoding to use for this subpart, like "base64" or + # "quoted-printable". + adv_attr_accessor :transfer_encoding + + # Create a new part from the given +params+ hash. The valid params keys + # correspond to the accessors. + def initialize(params) + @content_type = params[:content_type] + @content_disposition = params[:disposition] || "inline" + @charset = params[:charset] + @body = params[:body] + @filename = params[:filename] + @transfer_encoding = params[:transfer_encoding] || "quoted-printable" + @headers = params[:headers] || {} + @parts = [] + end + + # Convert the part to a mail object which can be included in the parts + # list of another mail object. + def to_mail(defaults) + part = TMail::Mail.new + + real_content_type, ctype_attrs = parse_content_type(defaults) + + if @parts.empty? + part.content_transfer_encoding = transfer_encoding || "quoted-printable" + case (transfer_encoding || "").downcase + when "base64" then + part.body = TMail::Base64.folding_encode(body) + when "quoted-printable" + part.body = [Utils.normalize_new_lines(body)].pack("M*") + else + part.body = body + end + + # Always set the content_type after setting the body and or parts! + # Also don't set filename and name when there is none (like in + # non-attachment parts) + if content_disposition == "attachment" + ctype_attrs.delete "charset" + part.set_content_type(real_content_type, nil, + squish("name" => filename).merge(ctype_attrs)) + part.set_content_disposition(content_disposition, + squish("filename" => filename).merge(ctype_attrs)) + else + part.set_content_type(real_content_type, nil, ctype_attrs) + part.set_content_disposition(content_disposition) + end + else + if String === body + part = TMail::Mail.new + part.body = body + part.set_content_type(real_content_type, nil, ctype_attrs) + part.set_content_disposition "inline" + m.parts << part + end + + @parts.each do |p| + prt = (TMail::Mail === p ? p : p.to_mail(defaults)) + part.parts << prt + end + + part.set_content_type(real_content_type, nil, ctype_attrs) if real_content_type =~ /multipart/ + end + + headers.each { |k,v| part[k] = v } + + part + end + + private + + def squish(values={}) + values.delete_if { |k,v| v.nil? } + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/part_container.rb b/vendor/rails/actionmailer/lib/action_mailer/part_container.rb new file mode 100644 index 00000000..3e3d6b9d --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/part_container.rb @@ -0,0 +1,51 @@ +module ActionMailer + # Accessors and helpers that ActionMailer::Base and ActionMailer::Part have + # in common. Using these helpers you can easily add subparts or attachments + # to your message: + # + # def my_mail_message(...) + # ... + # part "text/plain" do |p| + # p.body "hello, world" + # p.transfer_encoding "base64" + # end + # + # attachment "image/jpg" do |a| + # a.body = File.read("hello.jpg") + # a.filename = "hello.jpg" + # end + # end + module PartContainer + # The list of subparts of this container + attr_reader :parts + + # Add a part to a multipart message, with the given content-type. The + # part itself is yielded to the block so that other properties (charset, + # body, headers, etc.) can be set on it. + def part(params) + params = {:content_type => params} if String === params + part = Part.new(params) + yield part if block_given? + @parts << part + end + + # Add an attachment to a multipart message. This is simply a part with the + # content-disposition set to "attachment". + def attachment(params, &block) + params = { :content_type => params } if String === params + params = { :disposition => "attachment", + :transfer_encoding => "base64" }.merge(params) + part(params, &block) + end + + private + + def parse_content_type(defaults=nil) + return [defaults && defaults.content_type, {}] if content_type.blank? + ctype, *attrs = content_type.split(/;\s*/) + attrs = attrs.inject({}) { |h,s| k,v = s.split(/=/, 2); h[k] = v; h } + [ctype, {"charset" => charset || defaults && defaults.charset}.merge(attrs)] + end + + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/quoting.rb b/vendor/rails/actionmailer/lib/action_mailer/quoting.rb new file mode 100644 index 00000000..d6e04e4d --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/quoting.rb @@ -0,0 +1,59 @@ +module ActionMailer + module Quoting #:nodoc: + # Convert the given text into quoted printable format, with an instruction + # that the text be eventually interpreted in the given charset. + def quoted_printable(text, charset) + text = text.gsub( /[^a-z ]/i ) { quoted_printable_encode($&) }. + gsub( / /, "_" ) + "=?#{charset}?Q?#{text}?=" + end + + # Convert the given character to quoted printable format, taking into + # account multi-byte characters (if executing with $KCODE="u", for instance) + def quoted_printable_encode(character) + result = "" + character.each_byte { |b| result << "=%02x" % b } + result + end + + # A quick-and-dirty regexp for determining whether a string contains any + # characters that need escaping. + if !defined?(CHARS_NEEDING_QUOTING) + CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/ + end + + # Quote the given text if it contains any "illegal" characters + def quote_if_necessary(text, charset) + (text =~ CHARS_NEEDING_QUOTING) ? + quoted_printable(text, charset) : + text + end + + # Quote any of the given strings if they contain any "illegal" characters + def quote_any_if_necessary(charset, *args) + args.map { |v| quote_if_necessary(v, charset) } + end + + # Quote the given address if it needs to be. The address may be a + # regular email address, or it can be a phrase followed by an address in + # brackets. The phrase is the only part that will be quoted, and only if + # it needs to be. This allows extended characters to be used in the + # "to", "from", "cc", and "bcc" headers. + def quote_address_if_necessary(address, charset) + if Array === address + address.map { |a| quote_address_if_necessary(a, charset) } + elsif address =~ /^(\S.*)\s+(<.*>)$/ + address = $2 + phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset) + "\"#{phrase}\" #{address}" + else + address + end + end + + # Quote any of the given addresses, if they need to be. + def quote_any_address_if_necessary(charset, *args) + args.map { |v| quote_address_if_necessary(v, charset) } + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/utils.rb b/vendor/rails/actionmailer/lib/action_mailer/utils.rb new file mode 100644 index 00000000..552f695a --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/utils.rb @@ -0,0 +1,8 @@ +module ActionMailer + module Utils #:nodoc: + def normalize_new_lines(text) + text.to_s.gsub(/\r\n?/, "\n") + end + module_function :normalize_new_lines + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/text/format.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/text/format.rb new file mode 100755 index 00000000..de054db8 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/text/format.rb @@ -0,0 +1,1466 @@ +#-- +# Text::Format for Ruby +# Version 0.63 +# +# Copyright (c) 2002 - 2003 Austin Ziegler +# +# $Id: format.rb,v 1.1.1.1 2004/10/14 11:59:57 webster132 Exp $ +# +# ========================================================================== +# Revision History :: +# YYYY.MM.DD Change ID Developer +# Description +# -------------------------------------------------------------------------- +# 2002.10.18 Austin Ziegler +# Fixed a minor problem with tabs not being counted. Changed +# abbreviations from Hash to Array to better suit Ruby's +# capabilities. Fixed problems with the way that Array arguments +# are handled in calls to the major object types, excepting in +# Text::Format#expand and Text::Format#unexpand (these will +# probably need to be fixed). +# 2002.10.30 Austin Ziegler +# Fixed the ordering of the <=> for binary tests. Fixed +# Text::Format#expand and Text::Format#unexpand to handle array +# arguments better. +# 2003.01.24 Austin Ziegler +# Fixed a problem with Text::Format::RIGHT_FILL handling where a +# single word is larger than #columns. Removed Comparable +# capabilities (<=> doesn't make sense; == does). Added Symbol +# equivalents for the Hash initialization. Hash initialization has +# been modified so that values are set as follows (Symbols are +# highest priority; strings are middle; defaults are lowest): +# @columns = arg[:columns] || arg['columns'] || @columns +# Added #hard_margins, #split_rules, #hyphenator, and #split_words. +# 2003.02.07 Austin Ziegler +# Fixed the installer for proper case-sensitive handling. +# 2003.03.28 Austin Ziegler +# Added the ability for a hyphenator to receive the formatter +# object. Fixed a bug for strings matching /\A\s*\Z/ failing +# entirely. Fixed a test case failing under 1.6.8. +# 2003.04.04 Austin Ziegler +# Handle the case of hyphenators returning nil for first/rest. +# 2003.09.17 Austin Ziegler +# Fixed a problem where #paragraphs(" ") was raising +# NoMethodError. +# +# ========================================================================== +#++ + +module Text #:nodoc: + # Text::Format for Ruby is copyright 2002 - 2005 by Austin Ziegler. It + # is available under Ruby's licence, the Perl Artistic licence, or the + # GNU GPL version 2 (or at your option, any later version). As a + # special exception, for use with official Rails (provided by the + # rubyonrails.org development team) and any project created with + # official Rails, the following alternative MIT-style licence may be + # used: + # + # == Text::Format Licence for Rails and Rails Applications + # Permission is hereby granted, free of charge, to any person + # obtaining a copy of this software and associated documentation files + # (the "Software"), to deal in the Software without restriction, + # including without limitation the rights to use, copy, modify, merge, + # publish, distribute, sublicense, and/or sell copies of the Software, + # and to permit persons to whom the Software is furnished to do so, + # subject to the following conditions: + # + # * The names of its contributors may not be used to endorse or + # promote products derived from this software without specific prior + # written permission. + # + # The above copyright notice and this permission notice shall be + # included in all copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + # SOFTWARE. + class Format + VERSION = '0.63' + + # Local abbreviations. More can be added with Text::Format.abbreviations + ABBREV = [ 'Mr', 'Mrs', 'Ms', 'Jr', 'Sr' ] + + # Formatting values + LEFT_ALIGN = 0 + RIGHT_ALIGN = 1 + RIGHT_FILL = 2 + JUSTIFY = 3 + + # Word split modes (only applies when #hard_margins is true). + SPLIT_FIXED = 1 + SPLIT_CONTINUATION = 2 + SPLIT_HYPHENATION = 4 + SPLIT_CONTINUATION_FIXED = SPLIT_CONTINUATION | SPLIT_FIXED + SPLIT_HYPHENATION_FIXED = SPLIT_HYPHENATION | SPLIT_FIXED + SPLIT_HYPHENATION_CONTINUATION = SPLIT_HYPHENATION | SPLIT_CONTINUATION + SPLIT_ALL = SPLIT_HYPHENATION | SPLIT_CONTINUATION | SPLIT_FIXED + + # Words forcibly split by Text::Format will be stored as split words. + # This class represents a word forcibly split. + class SplitWord + # The word that was split. + attr_reader :word + # The first part of the word that was split. + attr_reader :first + # The remainder of the word that was split. + attr_reader :rest + + def initialize(word, first, rest) #:nodoc: + @word = word + @first = first + @rest = rest + end + end + + private + LEQ_RE = /[.?!]['"]?$/ + + def brk_re(i) #:nodoc: + %r/((?:\S+\s+){#{i}})(.+)/ + end + + def posint(p) #:nodoc: + p.to_i.abs + end + + public + # Compares two Text::Format objects. All settings of the objects are + # compared *except* #hyphenator. Generated results (e.g., #split_words) + # are not compared, either. + def ==(o) + (@text == o.text) && + (@columns == o.columns) && + (@left_margin == o.left_margin) && + (@right_margin == o.right_margin) && + (@hard_margins == o.hard_margins) && + (@split_rules == o.split_rules) && + (@first_indent == o.first_indent) && + (@body_indent == o.body_indent) && + (@tag_text == o.tag_text) && + (@tabstop == o.tabstop) && + (@format_style == o.format_style) && + (@extra_space == o.extra_space) && + (@tag_paragraph == o.tag_paragraph) && + (@nobreak == o.nobreak) && + (@abbreviations == o.abbreviations) && + (@nobreak_regex == o.nobreak_regex) + end + + # The text to be manipulated. Note that value is optional, but if the + # formatting functions are called without values, this text is what will + # be formatted. + # + # *Default*:: [] + # Used in:: All methods + attr_accessor :text + + # The total width of the format area. The margins, indentation, and text + # are formatted into this space. + # + # COLUMNS + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin indent text is formatted into here right margin + # + # *Default*:: 72 + # Used in:: #format, #paragraphs, + # #center + attr_reader :columns + + # The total width of the format area. The margins, indentation, and text + # are formatted into this space. The value provided is silently + # converted to a positive integer. + # + # COLUMNS + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin indent text is formatted into here right margin + # + # *Default*:: 72 + # Used in:: #format, #paragraphs, + # #center + def columns=(c) + @columns = posint(c) + end + + # The number of spaces used for the left margin. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # LEFT MARGIN indent text is formatted into here right margin + # + # *Default*:: 0 + # Used in:: #format, #paragraphs, + # #center + attr_reader :left_margin + + # The number of spaces used for the left margin. The value provided is + # silently converted to a positive integer value. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # LEFT MARGIN indent text is formatted into here right margin + # + # *Default*:: 0 + # Used in:: #format, #paragraphs, + # #center + def left_margin=(left) + @left_margin = posint(left) + end + + # The number of spaces used for the right margin. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin indent text is formatted into here RIGHT MARGIN + # + # *Default*:: 0 + # Used in:: #format, #paragraphs, + # #center + attr_reader :right_margin + + # The number of spaces used for the right margin. The value provided is + # silently converted to a positive integer value. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin indent text is formatted into here RIGHT MARGIN + # + # *Default*:: 0 + # Used in:: #format, #paragraphs, + # #center + def right_margin=(r) + @right_margin = posint(r) + end + + # The number of spaces to indent the first line of a paragraph. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin INDENT text is formatted into here right margin + # + # *Default*:: 4 + # Used in:: #format, #paragraphs + attr_reader :first_indent + + # The number of spaces to indent the first line of a paragraph. The + # value provided is silently converted to a positive integer value. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin INDENT text is formatted into here right margin + # + # *Default*:: 4 + # Used in:: #format, #paragraphs + def first_indent=(f) + @first_indent = posint(f) + end + + # The number of spaces to indent all lines after the first line of a + # paragraph. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin INDENT text is formatted into here right margin + # + # *Default*:: 0 + # Used in:: #format, #paragraphs + attr_reader :body_indent + + # The number of spaces to indent all lines after the first line of + # a paragraph. The value provided is silently converted to a + # positive integer value. + # + # columns + # <--------------------------------------------------------------> + # <-----------><------><---------------------------><------------> + # left margin INDENT text is formatted into here right margin + # + # *Default*:: 0 + # Used in:: #format, #paragraphs + def body_indent=(b) + @body_indent = posint(b) + end + + # Normally, words larger than the format area will be placed on a line + # by themselves. Setting this to +true+ will force words larger than the + # format area to be split into one or more "words" each at most the size + # of the format area. The first line and the original word will be + # placed into #split_words. Note that this will cause the + # output to look *similar* to a #format_style of JUSTIFY. (Lines will be + # filled as much as possible.) + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + attr_accessor :hard_margins + + # An array of words split during formatting if #hard_margins is set to + # +true+. + # #split_words << Text::Format::SplitWord.new(word, first, rest) + attr_reader :split_words + + # The object responsible for hyphenating. It must respond to + # #hyphenate_to(word, size) or #hyphenate_to(word, size, formatter) and + # return an array of the word split into two parts; if there is a + # hyphenation mark to be applied, responsibility belongs to the + # hyphenator object. The size is the MAXIMUM size permitted, including + # any hyphenation marks. If the #hyphenate_to method has an arity of 3, + # the formatter will be provided to the method. This allows the + # hyphenator to make decisions about the hyphenation based on the + # formatting rules. + # + # *Default*:: +nil+ + # Used in:: #format, #paragraphs + attr_reader :hyphenator + + # The object responsible for hyphenating. It must respond to + # #hyphenate_to(word, size) and return an array of the word hyphenated + # into two parts. The size is the MAXIMUM size permitted, including any + # hyphenation marks. + # + # *Default*:: +nil+ + # Used in:: #format, #paragraphs + def hyphenator=(h) + raise ArgumentError, "#{h.inspect} is not a valid hyphenator." unless h.respond_to?(:hyphenate_to) + arity = h.method(:hyphenate_to).arity + raise ArgumentError, "#{h.inspect} must have exactly two or three arguments." unless [2, 3].include?(arity) + @hyphenator = h + @hyphenator_arity = arity + end + + # Specifies the split mode; used only when #hard_margins is set to + # +true+. Allowable values are: + # [+SPLIT_FIXED+] The word will be split at the number of + # characters needed, with no marking at all. + # repre + # senta + # ion + # [+SPLIT_CONTINUATION+] The word will be split at the number of + # characters needed, with a C-style continuation + # character. If a word is the only item on a + # line and it cannot be split into an + # appropriate size, SPLIT_FIXED will be used. + # repr\ + # esen\ + # tati\ + # on + # [+SPLIT_HYPHENATION+] The word will be split according to the + # hyphenator specified in #hyphenator. If there + # is no #hyphenator specified, works like + # SPLIT_CONTINUATION. The example is using + # TeX::Hyphen. If a word is the only item on a + # line and it cannot be split into an + # appropriate size, SPLIT_CONTINUATION mode will + # be used. + # rep- + # re- + # sen- + # ta- + # tion + # + # *Default*:: Text::Format::SPLIT_FIXED + # Used in:: #format, #paragraphs + attr_reader :split_rules + + # Specifies the split mode; used only when #hard_margins is set to + # +true+. Allowable values are: + # [+SPLIT_FIXED+] The word will be split at the number of + # characters needed, with no marking at all. + # repre + # senta + # ion + # [+SPLIT_CONTINUATION+] The word will be split at the number of + # characters needed, with a C-style continuation + # character. + # repr\ + # esen\ + # tati\ + # on + # [+SPLIT_HYPHENATION+] The word will be split according to the + # hyphenator specified in #hyphenator. If there + # is no #hyphenator specified, works like + # SPLIT_CONTINUATION. The example is using + # TeX::Hyphen as the #hyphenator. + # rep- + # re- + # sen- + # ta- + # tion + # + # These values can be bitwise ORed together (e.g., SPLIT_FIXED | + # SPLIT_CONTINUATION) to provide fallback split methods. In the + # example given, an attempt will be made to split the word using the + # rules of SPLIT_CONTINUATION; if there is not enough room, the word + # will be split with the rules of SPLIT_FIXED. These combinations are + # also available as the following values: + # * +SPLIT_CONTINUATION_FIXED+ + # * +SPLIT_HYPHENATION_FIXED+ + # * +SPLIT_HYPHENATION_CONTINUATION+ + # * +SPLIT_ALL+ + # + # *Default*:: Text::Format::SPLIT_FIXED + # Used in:: #format, #paragraphs + def split_rules=(s) + raise ArgumentError, "Invalid value provided for split_rules." if ((s < SPLIT_FIXED) || (s > SPLIT_ALL)) + @split_rules = s + end + + # Indicates whether sentence terminators should be followed by a single + # space (+false+), or two spaces (+true+). + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + attr_accessor :extra_space + + # Defines the current abbreviations as an array. This is only used if + # extra_space is turned on. + # + # If one is abbreviating "President" as "Pres." (abbreviations = + # ["Pres"]), then the results of formatting will be as illustrated in + # the table below: + # + # extra_space | include? | !include? + # true | Pres. Lincoln | Pres. Lincoln + # false | Pres. Lincoln | Pres. Lincoln + # + # *Default*:: {} + # Used in:: #format, #paragraphs + attr_accessor :abbreviations + + # Indicates whether the formatting of paragraphs should be done with + # tagged paragraphs. Useful only with #tag_text. + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + attr_accessor :tag_paragraph + + # The array of text to be placed before each paragraph when + # #tag_paragraph is +true+. When #format() is called, + # only the first element of the array is used. When #paragraphs + # is called, then each entry in the array will be used once, with + # corresponding paragraphs. If the tag elements are exhausted before the + # text is exhausted, then the remaining paragraphs will not be tagged. + # Regardless of indentation settings, a blank line will be inserted + # between all paragraphs when #tag_paragraph is +true+. + # + # *Default*:: [] + # Used in:: #format, #paragraphs + attr_accessor :tag_text + + # Indicates whether or not the non-breaking space feature should be + # used. + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + attr_accessor :nobreak + + # A hash which holds the regular expressions on which spaces should not + # be broken. The hash is set up such that the key is the first word and + # the value is the second word. + # + # For example, if +nobreak_regex+ contains the following hash: + # + # { '^Mrs?\.$' => '\S+$', '^\S+$' => '^(?:S|J)r\.$'} + # + # Then "Mr. Jones", "Mrs. Jones", and "Jones Jr." would not be broken. + # If this simple matching algorithm indicates that there should not be a + # break at the current end of line, then a backtrack is done until there + # are two words on which line breaking is permitted. If two such words + # are not found, then the end of the line will be broken *regardless*. + # If there is a single word on the current line, then no backtrack is + # done and the word is stuck on the end. + # + # *Default*:: {} + # Used in:: #format, #paragraphs + attr_accessor :nobreak_regex + + # Indicates the number of spaces that a single tab represents. + # + # *Default*:: 8 + # Used in:: #expand, #unexpand, + # #paragraphs + attr_reader :tabstop + + # Indicates the number of spaces that a single tab represents. + # + # *Default*:: 8 + # Used in:: #expand, #unexpand, + # #paragraphs + def tabstop=(t) + @tabstop = posint(t) + end + + # Specifies the format style. Allowable values are: + # [+LEFT_ALIGN+] Left justified, ragged right. + # |A paragraph that is| + # |left aligned.| + # [+RIGHT_ALIGN+] Right justified, ragged left. + # |A paragraph that is| + # | right aligned.| + # [+RIGHT_FILL+] Left justified, right ragged, filled to width by + # spaces. (Essentially the same as +LEFT_ALIGN+ except + # that lines are padded on the right.) + # |A paragraph that is| + # |left aligned. | + # [+JUSTIFY+] Fully justified, words filled to width by spaces, + # except the last line. + # |A paragraph that| + # |is justified.| + # + # *Default*:: Text::Format::LEFT_ALIGN + # Used in:: #format, #paragraphs + attr_reader :format_style + + # Specifies the format style. Allowable values are: + # [+LEFT_ALIGN+] Left justified, ragged right. + # |A paragraph that is| + # |left aligned.| + # [+RIGHT_ALIGN+] Right justified, ragged left. + # |A paragraph that is| + # | right aligned.| + # [+RIGHT_FILL+] Left justified, right ragged, filled to width by + # spaces. (Essentially the same as +LEFT_ALIGN+ except + # that lines are padded on the right.) + # |A paragraph that is| + # |left aligned. | + # [+JUSTIFY+] Fully justified, words filled to width by spaces. + # |A paragraph that| + # |is justified.| + # + # *Default*:: Text::Format::LEFT_ALIGN + # Used in:: #format, #paragraphs + def format_style=(fs) + raise ArgumentError, "Invalid value provided for format_style." if ((fs < LEFT_ALIGN) || (fs > JUSTIFY)) + @format_style = fs + end + + # Indicates that the format style is left alignment. + # + # *Default*:: +true+ + # Used in:: #format, #paragraphs + def left_align? + return @format_style == LEFT_ALIGN + end + + # Indicates that the format style is right alignment. + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + def right_align? + return @format_style == RIGHT_ALIGN + end + + # Indicates that the format style is right fill. + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + def right_fill? + return @format_style == RIGHT_FILL + end + + # Indicates that the format style is full justification. + # + # *Default*:: +false+ + # Used in:: #format, #paragraphs + def justify? + return @format_style == JUSTIFY + end + + # The default implementation of #hyphenate_to implements + # SPLIT_CONTINUATION. + def hyphenate_to(word, size) + [word[0 .. (size - 2)] + "\\", word[(size - 1) .. -1]] + end + + private + def __do_split_word(word, size) #:nodoc: + [word[0 .. (size - 1)], word[size .. -1]] + end + + def __format(to_wrap) #:nodoc: + words = to_wrap.split(/\s+/).compact + words.shift if words[0].nil? or words[0].empty? + to_wrap = [] + + abbrev = false + width = @columns - @first_indent - @left_margin - @right_margin + indent_str = ' ' * @first_indent + first_line = true + line = words.shift + abbrev = __is_abbrev(line) unless line.nil? || line.empty? + + while w = words.shift + if (w.size + line.size < (width - 1)) || + ((line !~ LEQ_RE || abbrev) && (w.size + line.size < width)) + line << " " if (line =~ LEQ_RE) && (not abbrev) + line << " #{w}" + else + line, w = __do_break(line, w) if @nobreak + line, w = __do_hyphenate(line, w, width) if @hard_margins + if w.index(/\s+/) + w, *w2 = w.split(/\s+/) + words.unshift(w2) + words.flatten! + end + to_wrap << __make_line(line, indent_str, width, w.nil?) unless line.nil? + if first_line + first_line = false + width = @columns - @body_indent - @left_margin - @right_margin + indent_str = ' ' * @body_indent + end + line = w + end + + abbrev = __is_abbrev(w) unless w.nil? + end + + loop do + break if line.nil? or line.empty? + line, w = __do_hyphenate(line, w, width) if @hard_margins + to_wrap << __make_line(line, indent_str, width, w.nil?) + line = w + end + + if (@tag_paragraph && (to_wrap.size > 0)) then + clr = %r{`(\w+)'}.match([caller(1)].flatten[0])[1] + clr = "" if clr.nil? + + if ((not @tag_text[0].nil?) && (@tag_cur.size < 1) && + (clr != "__paragraphs")) then + @tag_cur = @tag_text[0] + end + + fchar = /(\S)/.match(to_wrap[0])[1] + white = to_wrap[0].index(fchar) + if ((white - @left_margin - 1) > @tag_cur.size) then + white = @tag_cur.size + @left_margin + to_wrap[0].gsub!(/^ {#{white}}/, "#{' ' * @left_margin}#{@tag_cur}") + else + to_wrap.unshift("#{' ' * @left_margin}#{@tag_cur}\n") + end + end + to_wrap.join('') + end + + # format lines in text into paragraphs with each element of @wrap a + # paragraph; uses Text::Format.format for the formatting + def __paragraphs(to_wrap) #:nodoc: + if ((@first_indent == @body_indent) || @tag_paragraph) then + p_end = "\n" + else + p_end = '' + end + + cnt = 0 + ret = [] + to_wrap.each do |tw| + @tag_cur = @tag_text[cnt] if @tag_paragraph + @tag_cur = '' if @tag_cur.nil? + line = __format(tw) + ret << "#{line}#{p_end}" if (not line.nil?) && (line.size > 0) + cnt += 1 + end + + ret[-1].chomp! unless ret.empty? + ret.join('') + end + + # center text using spaces on left side to pad it out empty lines + # are preserved + def __center(to_center) #:nodoc: + tabs = 0 + width = @columns - @left_margin - @right_margin + centered = [] + to_center.each do |tc| + s = tc.strip + tabs = s.count("\t") + tabs = 0 if tabs.nil? + ct = ((width - s.size - (tabs * @tabstop) + tabs) / 2) + ct = (width - @left_margin - @right_margin) - ct + centered << "#{s.rjust(ct)}\n" + end + centered.join('') + end + + # expand tabs to spaces should be similar to Text::Tabs::expand + def __expand(to_expand) #:nodoc: + expanded = [] + to_expand.split("\n").each { |te| expanded << te.gsub(/\t/, ' ' * @tabstop) } + expanded.join('') + end + + def __unexpand(to_unexpand) #:nodoc: + unexpanded = [] + to_unexpand.split("\n").each { |tu| unexpanded << tu.gsub(/ {#{@tabstop}}/, "\t") } + unexpanded.join('') + end + + def __is_abbrev(word) #:nodoc: + # remove period if there is one. + w = word.gsub(/\.$/, '') unless word.nil? + return true if (!@extra_space || ABBREV.include?(w) || @abbreviations.include?(w)) + false + end + + def __make_line(line, indent, width, last = false) #:nodoc: + lmargin = " " * @left_margin + fill = " " * (width - line.size) if right_fill? && (line.size <= width) + + if (justify? && ((not line.nil?) && (not line.empty?)) && line =~ /\S+\s+\S+/ && !last) + spaces = width - line.size + words = line.split(/(\s+)/) + ws = spaces / (words.size / 2) + spaces = spaces % (words.size / 2) if ws > 0 + words.reverse.each do |rw| + next if (rw =~ /^\S/) + rw.sub!(/^/, " " * ws) + next unless (spaces > 0) + rw.sub!(/^/, " ") + spaces -= 1 + end + line = words.join('') + end + line = "#{lmargin}#{indent}#{line}#{fill}\n" unless line.nil? + if right_align? && (not line.nil?) + line.sub(/^/, " " * (@columns - @right_margin - (line.size - 1))) + else + line + end + end + + def __do_hyphenate(line, next_line, width) #:nodoc: + rline = line.dup rescue line + rnext = next_line.dup rescue next_line + loop do + if rline.size == width + break + elsif rline.size > width + words = rline.strip.split(/\s+/) + word = words[-1].dup + size = width - rline.size + word.size + if (size <= 0) + words[-1] = nil + rline = words.join(' ').strip + rnext = "#{word} #{rnext}".strip + next + end + + first = rest = nil + + if ((@split_rules & SPLIT_HYPHENATION) != 0) + if @hyphenator_arity == 2 + first, rest = @hyphenator.hyphenate_to(word, size) + else + first, rest = @hyphenator.hyphenate_to(word, size, self) + end + end + + if ((@split_rules & SPLIT_CONTINUATION) != 0) and first.nil? + first, rest = self.hyphenate_to(word, size) + end + + if ((@split_rules & SPLIT_FIXED) != 0) and first.nil? + first.nil? or @split_rules == SPLIT_FIXED + first, rest = __do_split_word(word, size) + end + + if first.nil? + words[-1] = nil + rest = word + else + words[-1] = first + @split_words << SplitWord.new(word, first, rest) + end + rline = words.join(' ').strip + rnext = "#{rest} #{rnext}".strip + break + else + break if rnext.nil? or rnext.empty? or rline.nil? or rline.empty? + words = rnext.split(/\s+/) + word = words.shift + size = width - rline.size - 1 + + if (size <= 0) + rnext = "#{word} #{words.join(' ')}".strip + break + end + + first = rest = nil + + if ((@split_rules & SPLIT_HYPHENATION) != 0) + if @hyphenator_arity == 2 + first, rest = @hyphenator.hyphenate_to(word, size) + else + first, rest = @hyphenator.hyphenate_to(word, size, self) + end + end + + first, rest = self.hyphenate_to(word, size) if ((@split_rules & SPLIT_CONTINUATION) != 0) and first.nil? + + first, rest = __do_split_word(word, size) if ((@split_rules & SPLIT_FIXED) != 0) and first.nil? + + if (rline.size + (first ? first.size : 0)) < width + @split_words << SplitWord.new(word, first, rest) + rline = "#{rline} #{first}".strip + rnext = "#{rest} #{words.join(' ')}".strip + end + break + end + end + [rline, rnext] + end + + def __do_break(line, next_line) #:nodoc: + no_brk = false + words = [] + words = line.split(/\s+/) unless line.nil? + last_word = words[-1] + + @nobreak_regex.each { |k, v| no_brk = ((last_word =~ /#{k}/) and (next_line =~ /#{v}/)) } + + if no_brk && words.size > 1 + i = words.size + while i > 0 + no_brk = false + @nobreak_regex.each { |k, v| no_brk = ((words[i + 1] =~ /#{k}/) && (words[i] =~ /#{v}/)) } + i -= 1 + break if not no_brk + end + if i > 0 + l = brk_re(i).match(line) + line.sub!(brk_re(i), l[1]) + next_line = "#{l[2]} #{next_line}" + line.sub!(/\s+$/, '') + end + end + [line, next_line] + end + + def __create(arg = nil, &block) #:nodoc: + # Format::Text.new(text-to-wrap) + @text = arg unless arg.nil? + # Defaults + @columns = 72 + @tabstop = 8 + @first_indent = 4 + @body_indent = 0 + @format_style = LEFT_ALIGN + @left_margin = 0 + @right_margin = 0 + @extra_space = false + @text = Array.new if @text.nil? + @tag_paragraph = false + @tag_text = Array.new + @tag_cur = "" + @abbreviations = Array.new + @nobreak = false + @nobreak_regex = Hash.new + @split_words = Array.new + @hard_margins = false + @split_rules = SPLIT_FIXED + @hyphenator = self + @hyphenator_arity = self.method(:hyphenate_to).arity + + instance_eval(&block) unless block.nil? + end + + public + # Formats text into a nice paragraph format. The text is separated + # into words and then reassembled a word at a time using the settings + # of this Format object. If a word is larger than the number of + # columns available for formatting, then that word will appear on the + # line by itself. + # + # If +to_wrap+ is +nil+, then the value of #text will be + # worked on. + def format(to_wrap = nil) + to_wrap = @text if to_wrap.nil? + if to_wrap.class == Array + __format(to_wrap[0]) + else + __format(to_wrap) + end + end + + # Considers each element of text (provided or internal) as a paragraph. + # If #first_indent is the same as #body_indent, then + # paragraphs will be separated by a single empty line in the result; + # otherwise, the paragraphs will follow immediately after each other. + # Uses #format to do the heavy lifting. + def paragraphs(to_wrap = nil) + to_wrap = @text if to_wrap.nil? + __paragraphs([to_wrap].flatten) + end + + # Centers the text, preserving empty lines and tabs. + def center(to_center = nil) + to_center = @text if to_center.nil? + __center([to_center].flatten) + end + + # Replaces all tab characters in the text with #tabstop spaces. + def expand(to_expand = nil) + to_expand = @text if to_expand.nil? + if to_expand.class == Array + to_expand.collect { |te| __expand(te) } + else + __expand(to_expand) + end + end + + # Replaces all occurrences of #tabstop consecutive spaces + # with a tab character. + def unexpand(to_unexpand = nil) + to_unexpand = @text if to_unexpand.nil? + if to_unexpand.class == Array + to_unexpand.collect { |te| v << __unexpand(te) } + else + __unexpand(to_unexpand) + end + end + + # This constructor takes advantage of a technique for Ruby object + # construction introduced by Andy Hunt and Dave Thomas (see reference), + # where optional values are set using commands in a block. + # + # Text::Format.new { + # columns = 72 + # left_margin = 0 + # right_margin = 0 + # first_indent = 4 + # body_indent = 0 + # format_style = Text::Format::LEFT_ALIGN + # extra_space = false + # abbreviations = {} + # tag_paragraph = false + # tag_text = [] + # nobreak = false + # nobreak_regex = {} + # tabstop = 8 + # text = nil + # } + # + # As shown above, +arg+ is optional. If +arg+ is specified and is a + # +String+, then arg is used as the default value of #text. + # Alternately, an existing Text::Format object can be used or a Hash can + # be used. With all forms, a block can be specified. + # + # *Reference*:: "Object Construction and Blocks" + # + # + def initialize(arg = nil, &block) + case arg + when Text::Format + __create(arg.text) do + @columns = arg.columns + @tabstop = arg.tabstop + @first_indent = arg.first_indent + @body_indent = arg.body_indent + @format_style = arg.format_style + @left_margin = arg.left_margin + @right_margin = arg.right_margin + @extra_space = arg.extra_space + @tag_paragraph = arg.tag_paragraph + @tag_text = arg.tag_text + @abbreviations = arg.abbreviations + @nobreak = arg.nobreak + @nobreak_regex = arg.nobreak_regex + @text = arg.text + @hard_margins = arg.hard_margins + @split_words = arg.split_words + @split_rules = arg.split_rules + @hyphenator = arg.hyphenator + end + instance_eval(&block) unless block.nil? + when Hash + __create do + @columns = arg[:columns] || arg['columns'] || @columns + @tabstop = arg[:tabstop] || arg['tabstop'] || @tabstop + @first_indent = arg[:first_indent] || arg['first_indent'] || @first_indent + @body_indent = arg[:body_indent] || arg['body_indent'] || @body_indent + @format_style = arg[:format_style] || arg['format_style'] || @format_style + @left_margin = arg[:left_margin] || arg['left_margin'] || @left_margin + @right_margin = arg[:right_margin] || arg['right_margin'] || @right_margin + @extra_space = arg[:extra_space] || arg['extra_space'] || @extra_space + @text = arg[:text] || arg['text'] || @text + @tag_paragraph = arg[:tag_paragraph] || arg['tag_paragraph'] || @tag_paragraph + @tag_text = arg[:tag_text] || arg['tag_text'] || @tag_text + @abbreviations = arg[:abbreviations] || arg['abbreviations'] || @abbreviations + @nobreak = arg[:nobreak] || arg['nobreak'] || @nobreak + @nobreak_regex = arg[:nobreak_regex] || arg['nobreak_regex'] || @nobreak_regex + @hard_margins = arg[:hard_margins] || arg['hard_margins'] || @hard_margins + @split_rules = arg[:split_rules] || arg['split_rules'] || @split_rules + @hyphenator = arg[:hyphenator] || arg['hyphenator'] || @hyphenator + end + instance_eval(&block) unless block.nil? + when String + __create(arg, &block) + when NilClass + __create(&block) + else + raise TypeError + end + end + end +end + +if __FILE__ == $0 + require 'test/unit' + + class TestText__Format < Test::Unit::TestCase #:nodoc: + attr_accessor :format_o + + GETTYSBURG = <<-'EOS' + Four score and seven years ago our fathers brought forth on this + continent a new nation, conceived in liberty and dedicated to the + proposition that all men are created equal. Now we are engaged in + a great civil war, testing whether that nation or any nation so + conceived and so dedicated can long endure. We are met on a great + battlefield of that war. We have come to dedicate a portion of + that field as a final resting-place for those who here gave their + lives that that nation might live. It is altogether fitting and + proper that we should do this. But in a larger sense, we cannot + dedicate, we cannot consecrate, we cannot hallow this ground. + The brave men, living and dead who struggled here have consecrated + it far above our poor power to add or detract. The world will + little note nor long remember what we say here, but it can never + forget what they did here. It is for us the living rather to be + dedicated here to the unfinished work which they who fought here + have thus far so nobly advanced. It is rather for us to be here + dedicated to the great task remaining before us--that from these + honored dead we take increased devotion to that cause for which + they gave the last full measure of devotion--that we here highly + resolve that these dead shall not have died in vain, that this + nation under God shall have a new birth of freedom, and that + government of the people, by the people, for the people shall + not perish from the earth. + + -- Pres. Abraham Lincoln, 19 November 1863 + EOS + + FIVE_COL = "Four \nscore\nand s\neven \nyears\nago o\nur fa\nthers\nbroug\nht fo\nrth o\nn thi\ns con\ntinen\nt a n\new na\ntion,\nconce\nived \nin li\nberty\nand d\nedica\nted t\no the\npropo\nsitio\nn tha\nt all\nmen a\nre cr\neated\nequal\n. Now\nwe ar\ne eng\naged \nin a \ngreat\ncivil\nwar, \ntesti\nng wh\nether\nthat \nnatio\nn or \nany n\nation\nso co\nnceiv\ned an\nd so \ndedic\nated \ncan l\nong e\nndure\n. We \nare m\net on\na gre\nat ba\nttlef\nield \nof th\nat wa\nr. We\nhave \ncome \nto de\ndicat\ne a p\nortio\nn of \nthat \nfield\nas a \nfinal\nresti\nng-pl\nace f\nor th\nose w\nho he\nre ga\nve th\neir l\nives \nthat \nthat \nnatio\nn mig\nht li\nve. I\nt is \naltog\nether\nfitti\nng an\nd pro\nper t\nhat w\ne sho\nuld d\no thi\ns. Bu\nt in \na lar\nger s\nense,\nwe ca\nnnot \ndedic\nate, \nwe ca\nnnot \nconse\ncrate\n, we \ncanno\nt hal\nlow t\nhis g\nround\n. The\nbrave\nmen, \nlivin\ng and\ndead \nwho s\ntrugg\nled h\nere h\nave c\nonsec\nrated\nit fa\nr abo\nve ou\nr poo\nr pow\ner to\nadd o\nr det\nract.\nThe w\norld \nwill \nlittl\ne not\ne nor\nlong \nremem\nber w\nhat w\ne say\nhere,\nbut i\nt can\nnever\nforge\nt wha\nt the\ny did\nhere.\nIt is\nfor u\ns the\nlivin\ng rat\nher t\no be \ndedic\nated \nhere \nto th\ne unf\ninish\ned wo\nrk wh\nich t\nhey w\nho fo\nught \nhere \nhave \nthus \nfar s\no nob\nly ad\nvance\nd. It\nis ra\nther \nfor u\ns to \nbe he\nre de\ndicat\ned to\nthe g\nreat \ntask \nremai\nning \nbefor\ne us-\n-that\nfrom \nthese\nhonor\ned de\nad we\ntake \nincre\nased \ndevot\nion t\no tha\nt cau\nse fo\nr whi\nch th\ney ga\nve th\ne las\nt ful\nl mea\nsure \nof de\nvotio\nn--th\nat we\nhere \nhighl\ny res\nolve \nthat \nthese\ndead \nshall\nnot h\nave d\nied i\nn vai\nn, th\nat th\nis na\ntion \nunder\nGod s\nhall \nhave \na new\nbirth\nof fr\needom\n, and\nthat \ngover\nnment\nof th\ne peo\nple, \nby th\ne peo\nple, \nfor t\nhe pe\nople \nshall\nnot p\nerish\nfrom \nthe e\narth.\n-- Pr\nes. A\nbraha\nm Lin\ncoln,\n19 No\nvembe\nr 186\n3 \n" + + FIVE_CNT = "Four \nscore\nand \nseven\nyears\nago \nour \nfath\\\ners \nbrou\\\nght \nforth\non t\\\nhis \ncont\\\ninent\na new\nnati\\\non, \nconc\\\neived\nin l\\\niber\\\nty a\\\nnd d\\\nedic\\\nated \nto t\\\nhe p\\\nropo\\\nsiti\\\non t\\\nhat \nall \nmen \nare \ncrea\\\nted \nequa\\\nl. N\\\now we\nare \nenga\\\nged \nin a \ngreat\ncivil\nwar, \ntest\\\ning \nwhet\\\nher \nthat \nnati\\\non or\nany \nnati\\\non so\nconc\\\neived\nand \nso d\\\nedic\\\nated \ncan \nlong \nendu\\\nre. \nWe a\\\nre m\\\net on\na gr\\\neat \nbatt\\\nlefi\\\neld \nof t\\\nhat \nwar. \nWe h\\\nave \ncome \nto d\\\nedic\\\nate a\nport\\\nion \nof t\\\nhat \nfield\nas a \nfinal\nrest\\\ning-\\\nplace\nfor \nthose\nwho \nhere \ngave \ntheir\nlives\nthat \nthat \nnati\\\non m\\\night \nlive.\nIt is\nalto\\\ngeth\\\ner f\\\nitti\\\nng a\\\nnd p\\\nroper\nthat \nwe s\\\nhould\ndo t\\\nhis. \nBut \nin a \nlarg\\\ner s\\\nense,\nwe c\\\nannot\ndedi\\\ncate,\nwe c\\\nannot\ncons\\\necra\\\nte, \nwe c\\\nannot\nhall\\\now t\\\nhis \ngrou\\\nnd. \nThe \nbrave\nmen, \nlivi\\\nng a\\\nnd d\\\nead \nwho \nstru\\\nggled\nhere \nhave \ncons\\\necra\\\nted \nit f\\\nar a\\\nbove \nour \npoor \npower\nto a\\\ndd or\ndetr\\\nact. \nThe \nworld\nwill \nlitt\\\nle n\\\note \nnor \nlong \nreme\\\nmber \nwhat \nwe s\\\nay h\\\nere, \nbut \nit c\\\nan n\\\never \nforg\\\net w\\\nhat \nthey \ndid \nhere.\nIt is\nfor \nus t\\\nhe l\\\niving\nrath\\\ner to\nbe d\\\nedic\\\nated \nhere \nto t\\\nhe u\\\nnfin\\\nished\nwork \nwhich\nthey \nwho \nfoug\\\nht h\\\nere \nhave \nthus \nfar \nso n\\\nobly \nadva\\\nnced.\nIt is\nrath\\\ner f\\\nor us\nto be\nhere \ndedi\\\ncated\nto t\\\nhe g\\\nreat \ntask \nrema\\\nining\nbefo\\\nre u\\\ns--t\\\nhat \nfrom \nthese\nhono\\\nred \ndead \nwe t\\\nake \nincr\\\neased\ndevo\\\ntion \nto t\\\nhat \ncause\nfor \nwhich\nthey \ngave \nthe \nlast \nfull \nmeas\\\nure \nof d\\\nevot\\\nion-\\\n-that\nwe h\\\nere \nhigh\\\nly r\\\nesol\\\nve t\\\nhat \nthese\ndead \nshall\nnot \nhave \ndied \nin v\\\nain, \nthat \nthis \nnati\\\non u\\\nnder \nGod \nshall\nhave \na new\nbirth\nof f\\\nreed\\\nom, \nand \nthat \ngove\\\nrnme\\\nnt of\nthe \npeop\\\nle, \nby t\\\nhe p\\\neopl\\\ne, f\\\nor t\\\nhe p\\\neople\nshall\nnot \nperi\\\nsh f\\\nrom \nthe \neart\\\nh. --\nPres.\nAbra\\\nham \nLinc\\\noln, \n19 N\\\novem\\\nber \n1863 \n" + + # Tests both abbreviations and abbreviations= + def test_abbreviations + abbr = [" Pres. Abraham Lincoln\n", " Pres. Abraham Lincoln\n"] + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal([], @format_o.abbreviations) + assert_nothing_raised { @format_o.abbreviations = [ 'foo', 'bar' ] } + assert_equal([ 'foo', 'bar' ], @format_o.abbreviations) + assert_equal(abbr[0], @format_o.format(abbr[0])) + assert_nothing_raised { @format_o.extra_space = true } + assert_equal(abbr[1], @format_o.format(abbr[0])) + assert_nothing_raised { @format_o.abbreviations = [ "Pres" ] } + assert_equal([ "Pres" ], @format_o.abbreviations) + assert_equal(abbr[0], @format_o.format(abbr[0])) + assert_nothing_raised { @format_o.extra_space = false } + assert_equal(abbr[0], @format_o.format(abbr[0])) + end + + # Tests both body_indent and body_indent= + def test_body_indent + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(0, @format_o.body_indent) + assert_nothing_raised { @format_o.body_indent = 7 } + assert_equal(7, @format_o.body_indent) + assert_nothing_raised { @format_o.body_indent = -3 } + assert_equal(3, @format_o.body_indent) + assert_nothing_raised { @format_o.body_indent = "9" } + assert_equal(9, @format_o.body_indent) + assert_nothing_raised { @format_o.body_indent = "-2" } + assert_equal(2, @format_o.body_indent) + assert_match(/^ [^ ]/, @format_o.format(GETTYSBURG).split("\n")[1]) + end + + # Tests both columns and columns= + def test_columns + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(72, @format_o.columns) + assert_nothing_raised { @format_o.columns = 7 } + assert_equal(7, @format_o.columns) + assert_nothing_raised { @format_o.columns = -3 } + assert_equal(3, @format_o.columns) + assert_nothing_raised { @format_o.columns = "9" } + assert_equal(9, @format_o.columns) + assert_nothing_raised { @format_o.columns = "-2" } + assert_equal(2, @format_o.columns) + assert_nothing_raised { @format_o.columns = 40 } + assert_equal(40, @format_o.columns) + assert_match(/this continent$/, + @format_o.format(GETTYSBURG).split("\n")[1]) + end + + # Tests both extra_space and extra_space= + def test_extra_space + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.extra_space) + assert_nothing_raised { @format_o.extra_space = true } + assert(@format_o.extra_space) + # The behaviour of extra_space is tested in test_abbreviations. There + # is no need to reproduce it here. + end + + # Tests both first_indent and first_indent= + def test_first_indent + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(4, @format_o.first_indent) + assert_nothing_raised { @format_o.first_indent = 7 } + assert_equal(7, @format_o.first_indent) + assert_nothing_raised { @format_o.first_indent = -3 } + assert_equal(3, @format_o.first_indent) + assert_nothing_raised { @format_o.first_indent = "9" } + assert_equal(9, @format_o.first_indent) + assert_nothing_raised { @format_o.first_indent = "-2" } + assert_equal(2, @format_o.first_indent) + assert_match(/^ [^ ]/, @format_o.format(GETTYSBURG).split("\n")[0]) + end + + def test_format_style + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(Text::Format::LEFT_ALIGN, @format_o.format_style) + assert_match(/^November 1863$/, + @format_o.format(GETTYSBURG).split("\n")[-1]) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_ALIGN + } + assert_equal(Text::Format::RIGHT_ALIGN, @format_o.format_style) + assert_match(/^ +November 1863$/, + @format_o.format(GETTYSBURG).split("\n")[-1]) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert_equal(Text::Format::RIGHT_FILL, @format_o.format_style) + assert_match(/^November 1863 +$/, + @format_o.format(GETTYSBURG).split("\n")[-1]) + assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY } + assert_equal(Text::Format::JUSTIFY, @format_o.format_style) + assert_match(/^of freedom, and that government of the people, by the people, for the$/, + @format_o.format(GETTYSBURG).split("\n")[-3]) + assert_raises(ArgumentError) { @format_o.format_style = 33 } + end + + def test_tag_paragraph + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.tag_paragraph) + assert_nothing_raised { @format_o.tag_paragraph = true } + assert(@format_o.tag_paragraph) + assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG]), + Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG])) + end + + def test_tag_text + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal([], @format_o.tag_text) + assert_equal(@format_o.format(GETTYSBURG), + Text::Format.new.format(GETTYSBURG)) + assert_nothing_raised { + @format_o.tag_paragraph = true + @format_o.tag_text = ["Gettysburg Address", "---"] + } + assert_not_equal(@format_o.format(GETTYSBURG), + Text::Format.new.format(GETTYSBURG)) + assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG]), + Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG])) + assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG, + GETTYSBURG]), + Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG, + GETTYSBURG])) + end + + def test_justify? + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.justify?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_ALIGN + } + assert(!@format_o.justify?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert(!@format_o.justify?) + assert_nothing_raised { + @format_o.format_style = Text::Format::JUSTIFY + } + assert(@format_o.justify?) + # The format testing is done in test_format_style + end + + def test_left_align? + assert_nothing_raised { @format_o = Text::Format.new } + assert(@format_o.left_align?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_ALIGN + } + assert(!@format_o.left_align?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert(!@format_o.left_align?) + assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY } + assert(!@format_o.left_align?) + # The format testing is done in test_format_style + end + + def test_left_margin + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(0, @format_o.left_margin) + assert_nothing_raised { @format_o.left_margin = -3 } + assert_equal(3, @format_o.left_margin) + assert_nothing_raised { @format_o.left_margin = "9" } + assert_equal(9, @format_o.left_margin) + assert_nothing_raised { @format_o.left_margin = "-2" } + assert_equal(2, @format_o.left_margin) + assert_nothing_raised { @format_o.left_margin = 7 } + assert_equal(7, @format_o.left_margin) + assert_nothing_raised { + ft = @format_o.format(GETTYSBURG).split("\n") + assert_match(/^ {11}Four score/, ft[0]) + assert_match(/^ {7}November/, ft[-1]) + } + end + + def test_hard_margins + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.hard_margins) + assert_nothing_raised { + @format_o.hard_margins = true + @format_o.columns = 5 + @format_o.first_indent = 0 + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert(@format_o.hard_margins) + assert_equal(FIVE_COL, @format_o.format(GETTYSBURG)) + assert_nothing_raised { + @format_o.split_rules |= Text::Format::SPLIT_CONTINUATION + assert_equal(Text::Format::SPLIT_CONTINUATION_FIXED, + @format_o.split_rules) + } + assert_equal(FIVE_CNT, @format_o.format(GETTYSBURG)) + end + + # Tests both nobreak and nobreak_regex, since one is only useful + # with the other. + def test_nobreak + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.nobreak) + assert(@format_o.nobreak_regex.empty?) + assert_nothing_raised { + @format_o.nobreak = true + @format_o.nobreak_regex = { '^this$' => '^continent$' } + @format_o.columns = 77 + } + assert(@format_o.nobreak) + assert_equal({ '^this$' => '^continent$' }, @format_o.nobreak_regex) + assert_match(/^this continent/, + @format_o.format(GETTYSBURG).split("\n")[1]) + end + + def test_right_align? + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.right_align?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_ALIGN + } + assert(@format_o.right_align?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert(!@format_o.right_align?) + assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY } + assert(!@format_o.right_align?) + # The format testing is done in test_format_style + end + + def test_right_fill? + assert_nothing_raised { @format_o = Text::Format.new } + assert(!@format_o.right_fill?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_ALIGN + } + assert(!@format_o.right_fill?) + assert_nothing_raised { + @format_o.format_style = Text::Format::RIGHT_FILL + } + assert(@format_o.right_fill?) + assert_nothing_raised { + @format_o.format_style = Text::Format::JUSTIFY + } + assert(!@format_o.right_fill?) + # The format testing is done in test_format_style + end + + def test_right_margin + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(0, @format_o.right_margin) + assert_nothing_raised { @format_o.right_margin = -3 } + assert_equal(3, @format_o.right_margin) + assert_nothing_raised { @format_o.right_margin = "9" } + assert_equal(9, @format_o.right_margin) + assert_nothing_raised { @format_o.right_margin = "-2" } + assert_equal(2, @format_o.right_margin) + assert_nothing_raised { @format_o.right_margin = 7 } + assert_equal(7, @format_o.right_margin) + assert_nothing_raised { + ft = @format_o.format(GETTYSBURG).split("\n") + assert_match(/^ {4}Four score.*forth on$/, ft[0]) + assert_match(/^November/, ft[-1]) + } + end + + def test_tabstop + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(8, @format_o.tabstop) + assert_nothing_raised { @format_o.tabstop = 7 } + assert_equal(7, @format_o.tabstop) + assert_nothing_raised { @format_o.tabstop = -3 } + assert_equal(3, @format_o.tabstop) + assert_nothing_raised { @format_o.tabstop = "9" } + assert_equal(9, @format_o.tabstop) + assert_nothing_raised { @format_o.tabstop = "-2" } + assert_equal(2, @format_o.tabstop) + end + + def test_text + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal([], @format_o.text) + assert_nothing_raised { @format_o.text = "Test Text" } + assert_equal("Test Text", @format_o.text) + assert_nothing_raised { @format_o.text = ["Line 1", "Line 2"] } + assert_equal(["Line 1", "Line 2"], @format_o.text) + end + + def test_s_new + # new(NilClass) { block } + assert_nothing_raised do + @format_o = Text::Format.new { + self.text = "Test 1, 2, 3" + } + end + assert_equal("Test 1, 2, 3", @format_o.text) + + # new(Hash Symbols) + assert_nothing_raised { @format_o = Text::Format.new(:columns => 72) } + assert_equal(72, @format_o.columns) + + # new(Hash String) + assert_nothing_raised { @format_o = Text::Format.new('columns' => 72) } + assert_equal(72, @format_o.columns) + + # new(Hash) { block } + assert_nothing_raised do + @format_o = Text::Format.new('columns' => 80) { + self.text = "Test 4, 5, 6" + } + end + assert_equal("Test 4, 5, 6", @format_o.text) + assert_equal(80, @format_o.columns) + + # new(Text::Format) + assert_nothing_raised do + fo = Text::Format.new(@format_o) + assert(fo == @format_o) + end + + # new(Text::Format) { block } + assert_nothing_raised do + fo = Text::Format.new(@format_o) { self.columns = 79 } + assert(fo != @format_o) + end + + # new(String) + assert_nothing_raised { @format_o = Text::Format.new("Test A, B, C") } + assert_equal("Test A, B, C", @format_o.text) + + # new(String) { block } + assert_nothing_raised do + @format_o = Text::Format.new("Test X, Y, Z") { self.columns = -5 } + end + assert_equal("Test X, Y, Z", @format_o.text) + assert_equal(5, @format_o.columns) + end + + def test_center + assert_nothing_raised { @format_o = Text::Format.new } + assert_nothing_raised do + ct = @format_o.center(GETTYSBURG.split("\n")).split("\n") + assert_match(/^ Four score and seven years ago our fathers brought forth on this/, ct[0]) + assert_match(/^ not perish from the earth./, ct[-3]) + end + end + + def test_expand + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal(" ", @format_o.expand("\t ")) + assert_nothing_raised { @format_o.tabstop = 4 } + assert_equal(" ", @format_o.expand("\t ")) + end + + def test_unexpand + assert_nothing_raised { @format_o = Text::Format.new } + assert_equal("\t ", @format_o.unexpand(" ")) + assert_nothing_raised { @format_o.tabstop = 4 } + assert_equal("\t ", @format_o.unexpand(" ")) + end + + def test_space_only + assert_equal("", Text::Format.new.format(" ")) + assert_equal("", Text::Format.new.format("\n")) + assert_equal("", Text::Format.new.format(" ")) + assert_equal("", Text::Format.new.format(" \n")) + assert_equal("", Text::Format.new.paragraphs("\n")) + assert_equal("", Text::Format.new.paragraphs(" ")) + assert_equal("", Text::Format.new.paragraphs(" ")) + assert_equal("", Text::Format.new.paragraphs(" \n")) + assert_equal("", Text::Format.new.paragraphs(["\n"])) + assert_equal("", Text::Format.new.paragraphs([" "])) + assert_equal("", Text::Format.new.paragraphs([" "])) + assert_equal("", Text::Format.new.paragraphs([" \n"])) + end + + def test_splendiferous + h = nil + test = "This is a splendiferous test" + assert_nothing_raised { @format_o = Text::Format.new(:columns => 6, :left_margin => 0, :indent => 0, :first_indent => 0) } + assert_match(/^splendiferous$/, @format_o.format(test)) + assert_nothing_raised { @format_o.hard_margins = true } + assert_match(/^lendif$/, @format_o.format(test)) + assert_nothing_raised { h = Object.new } + assert_nothing_raised do + @format_o.split_rules = Text::Format::SPLIT_HYPHENATION + class << h #:nodoc: + def hyphenate_to(word, size) + return ["", word] if size < 2 + [word[0 ... size], word[size .. -1]] + end + end + @format_o.hyphenator = h + end + assert_match(/^iferou$/, @format_o.format(test)) + assert_nothing_raised { h = Object.new } + assert_nothing_raised do + class << h #:nodoc: + def hyphenate_to(word, size, formatter) + return ["", word] if word.size < formatter.columns + [word[0 ... size], word[size .. -1]] + end + end + @format_o.hyphenator = h + end + assert_match(/^ferous$/, @format_o.format(test)) + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail.rb new file mode 100755 index 00000000..8cea88d3 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail.rb @@ -0,0 +1,3 @@ +require 'tmail/info' +require 'tmail/mail' +require 'tmail/mailbox' diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/address.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/address.rb new file mode 100755 index 00000000..235ec761 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/address.rb @@ -0,0 +1,242 @@ +# +# address.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/encode' +require 'tmail/parser' + + +module TMail + + class Address + + include TextUtils + + def Address.parse( str ) + Parser.parse :ADDRESS, str + end + + def address_group? + false + end + + def initialize( local, domain ) + if domain + domain.each do |s| + raise SyntaxError, 'empty word in domain' if s.empty? + end + end + @local = local + @domain = domain + @name = nil + @routes = [] + end + + attr_reader :name + + def name=( str ) + @name = str + @name = nil if str and str.empty? + end + + alias phrase name + alias phrase= name= + + attr_reader :routes + + def inspect + "#<#{self.class} #{address()}>" + end + + def local + return nil unless @local + return '""' if @local.size == 1 and @local[0].empty? + @local.map {|i| quote_atom(i) }.join('.') + end + + def domain + return nil unless @domain + join_domain(@domain) + end + + def spec + s = self.local + d = self.domain + if s and d + s + '@' + d + else + s + end + end + + alias address spec + + + def ==( other ) + other.respond_to? :spec and self.spec == other.spec + end + + alias eql? == + + def hash + @local.hash ^ @domain.hash + end + + def dup + obj = self.class.new(@local.dup, @domain.dup) + obj.name = @name.dup if @name + obj.routes.replace @routes + obj + end + + include StrategyInterface + + def accept( strategy, dummy1 = nil, dummy2 = nil ) + unless @local + strategy.meta '<>' # empty return-path + return + end + + spec_p = (not @name and @routes.empty?) + if @name + strategy.phrase @name + strategy.space + end + tmp = spec_p ? '' : '<' + unless @routes.empty? + tmp << @routes.map {|i| '@' + i }.join(',') << ':' + end + tmp << self.spec + tmp << '>' unless spec_p + strategy.meta tmp + strategy.lwsp '' + end + + end + + + class AddressGroup + + include Enumerable + + def address_group? + true + end + + def initialize( name, addrs ) + @name = name + @addresses = addrs + end + + attr_reader :name + + def ==( other ) + other.respond_to? :to_a and @addresses == other.to_a + end + + alias eql? == + + def hash + map {|i| i.hash }.hash + end + + def []( idx ) + @addresses[idx] + end + + def size + @addresses.size + end + + def empty? + @addresses.empty? + end + + def each( &block ) + @addresses.each(&block) + end + + def to_a + @addresses.dup + end + + alias to_ary to_a + + def include?( a ) + @addresses.include? a + end + + def flatten + set = [] + @addresses.each do |a| + if a.respond_to? :flatten + set.concat a.flatten + else + set.push a + end + end + set + end + + def each_address( &block ) + flatten.each(&block) + end + + def add( a ) + @addresses.push a + end + + alias push add + + def delete( a ) + @addresses.delete a + end + + include StrategyInterface + + def accept( strategy, dummy1 = nil, dummy2 = nil ) + strategy.phrase @name + strategy.meta ':' + strategy.space + first = true + each do |mbox| + if first + first = false + else + strategy.meta ',' + end + strategy.space + mbox.accept strategy + end + strategy.meta ';' + strategy.lwsp '' + end + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/attachments.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/attachments.rb new file mode 100644 index 00000000..4d8d106a --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/attachments.rb @@ -0,0 +1,39 @@ +require 'stringio' + +module TMail + class Attachment < StringIO + attr_accessor :original_filename, :content_type + end + + class Mail + def has_attachments? + multipart? && parts.any? { |part| attachment?(part) } + end + + def attachment?(part) + (part['content-disposition'] && part['content-disposition'].disposition == "attachment") || + part.header['content-type'].main_type != "text" + end + + def attachments + if multipart? + parts.collect { |part| + if attachment?(part) + content = part.body # unquoted automatically by TMail#body + file_name = (part['content-location'] && + part['content-location'].body) || + part.sub_header("content-type", "name") || + part.sub_header("content-disposition", "filename") + + next if file_name.blank? || content.blank? + + attachment = Attachment.new(content) + attachment.original_filename = file_name.strip + attachment.content_type = part.content_type + attachment + end + }.compact + end + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/base64.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/base64.rb new file mode 100755 index 00000000..8f89a489 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/base64.rb @@ -0,0 +1,71 @@ +# +# base64.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +module TMail + + module Base64 + + module_function + + def rb_folding_encode( str, eol = "\n", limit = 60 ) + [str].pack('m') + end + + def rb_encode( str ) + [str].pack('m').tr( "\r\n", '' ) + end + + def rb_decode( str, strict = false ) + str.unpack('m') + end + + begin + require 'tmail/base64.so' + alias folding_encode c_folding_encode + alias encode c_encode + alias decode c_decode + class << self + alias folding_encode c_folding_encode + alias encode c_encode + alias decode c_decode + end + rescue LoadError + alias folding_encode rb_folding_encode + alias encode rb_encode + alias decode rb_decode + class << self + alias folding_encode rb_folding_encode + alias encode rb_encode + alias decode rb_decode + end + end + + end + +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/config.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/config.rb new file mode 100755 index 00000000..b075299b --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/config.rb @@ -0,0 +1,69 @@ +# +# config.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +module TMail + + class Config + + def initialize( strict ) + @strict_parse = strict + @strict_base64decode = strict + end + + def strict_parse? + @strict_parse + end + + attr_writer :strict_parse + + def strict_base64decode? + @strict_base64decode + end + + attr_writer :strict_base64decode + + def new_body_port( mail ) + StringPort.new + end + + alias new_preamble_port new_body_port + alias new_part_port new_body_port + + end + + DEFAULT_CONFIG = Config.new(false) + DEFAULT_STRICT_CONFIG = Config.new(true) + + def Config.to_config( arg ) + return DEFAULT_STRICT_CONFIG if arg == true + return DEFAULT_CONFIG if arg == false + arg or DEFAULT_CONFIG + end + +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/encode.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/encode.rb new file mode 100755 index 00000000..91bd289c --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/encode.rb @@ -0,0 +1,467 @@ +# +# encode.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'nkf' +require 'tmail/base64.rb' +require 'tmail/stringio' +require 'tmail/utils' + + +module TMail + + module StrategyInterface + + def create_dest( obj ) + case obj + when nil + StringOutput.new + when String + StringOutput.new(obj) + when IO, StringOutput + obj + else + raise TypeError, 'cannot handle this type of object for dest' + end + end + module_function :create_dest + + def encoded( eol = "\r\n", charset = 'j', dest = nil ) + accept_strategy Encoder, eol, charset, dest + end + + def decoded( eol = "\n", charset = 'e', dest = nil ) + accept_strategy Decoder, eol, charset, dest + end + + alias to_s decoded + + def accept_strategy( klass, eol, charset, dest = nil ) + dest ||= '' + accept klass.new(create_dest(dest), charset, eol) + dest + end + + end + + + ### + ### MIME B encoding decoder + ### + + class Decoder + + include TextUtils + + encoded = '=\?(?:iso-2022-jp|euc-jp|shift_jis)\?[QB]\?[a-z0-9+/=]+\?=' + ENCODED_WORDS = /#{encoded}(?:\s+#{encoded})*/i + + OUTPUT_ENCODING = { + 'EUC' => 'e', + 'SJIS' => 's', + } + + def self.decode( str, encoding = nil ) + encoding ||= (OUTPUT_ENCODING[$KCODE] || 'j') + opt = '-m' + encoding + str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) } + end + + def initialize( dest, encoding = nil, eol = "\n" ) + @f = StrategyInterface.create_dest(dest) + @encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil + @eol = eol + end + + def decode( str ) + self.class.decode(str, @encoding) + end + private :decode + + def terminate + end + + def header_line( str ) + @f << decode(str) + end + + def header_name( nm ) + @f << nm << ': ' + end + + def header_body( str ) + @f << decode(str) + end + + def space + @f << ' ' + end + + alias spc space + + def lwsp( str ) + @f << str + end + + def meta( str ) + @f << str + end + + def text( str ) + @f << decode(str) + end + + def phrase( str ) + @f << quote_phrase(decode(str)) + end + + def kv_pair( k, v ) + @f << k << '=' << v + end + + def puts( str = nil ) + @f << str if str + @f << @eol + end + + def write( str ) + @f << str + end + + end + + + ### + ### MIME B-encoding encoder + ### + + # + # FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp). + # + class Encoder + + include TextUtils + + BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG) + + def Encoder.encode( str ) + e = new() + e.header_body str + e.terminate + e.dest.string + end + + SPACER = "\t" + MAX_LINE_LEN = 70 + + OPTIONS = { + 'EUC' => '-Ej -m0', + 'SJIS' => '-Sj -m0', + 'UTF8' => nil, # FIXME + 'NONE' => nil + } + + def initialize( dest = nil, encoding = nil, eol = "\r\n", limit = nil ) + @f = StrategyInterface.create_dest(dest) + @opt = OPTIONS[$KCODE] + @eol = eol + reset + end + + def normalize_encoding( str ) + if @opt + then NKF.nkf(@opt, str) + else str + end + end + + def reset + @text = '' + @lwsp = '' + @curlen = 0 + end + + def terminate + add_lwsp '' + reset + end + + def dest + @f + end + + def puts( str = nil ) + @f << str if str + @f << @eol + end + + def write( str ) + @f << str + end + + # + # add + # + + def header_line( line ) + scanadd line + end + + def header_name( name ) + add_text name.split(/-/).map {|i| i.capitalize }.join('-') + add_text ':' + add_lwsp ' ' + end + + def header_body( str ) + scanadd normalize_encoding(str) + end + + def space + add_lwsp ' ' + end + + alias spc space + + def lwsp( str ) + add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '') + end + + def meta( str ) + add_text str + end + + def text( str ) + scanadd normalize_encoding(str) + end + + def phrase( str ) + str = normalize_encoding(str) + if CONTROL_CHAR === str + scanadd str + else + add_text quote_phrase(str) + end + end + + # FIXME: implement line folding + # + def kv_pair( k, v ) + return if v.nil? + v = normalize_encoding(v) + if token_safe?(v) + add_text k + '=' + v + elsif not CONTROL_CHAR === v + add_text k + '=' + quote_token(v) + else + # apply RFC2231 encoding + kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v) + add_text kv + end + end + + def encode_value( str ) + str.gsub(TOKEN_UNSAFE) {|s| '%%%02x' % s[0] } + end + + private + + def scanadd( str, force = false ) + types = '' + strs = [] + + until str.empty? + if m = /\A[^\e\t\r\n ]+/.match(str) + types << (force ? 'j' : 'a') + strs.push m[0] + + elsif m = /\A[\t\r\n ]+/.match(str) + types << 's' + strs.push m[0] + + elsif m = /\A\e../.match(str) + esc = m[0] + str = m.post_match + if esc != "\e(B" and m = /\A[^\e]+/.match(str) + types << 'j' + strs.push m[0] + end + + else + raise 'TMail FATAL: encoder scan fail' + end + (str = m.post_match) unless m.nil? + end + + do_encode types, strs + end + + def do_encode( types, strs ) + # + # result : (A|E)(S(A|E))* + # E : W(SW)* + # W : (J|A)+ but must contain J # (J|A)*J(J|A)* + # A : <> + # J : <> + # S : <> + # + # An encoding unit is `E'. + # Input (parameter `types') is (J|A)(J|A|S)*(J|A) + # + if BENCODE_DEBUG + puts + puts '-- do_encode ------------' + puts types.split(//).join(' ') + p strs + end + + e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/ + + while m = e.match(types) + pre = m.pre_match + concat_A_S pre, strs[0, pre.size] unless pre.empty? + concat_E m[0], strs[m.begin(0) ... m.end(0)] + types = m.post_match + strs.slice! 0, m.end(0) + end + concat_A_S types, strs + end + + def concat_A_S( types, strs ) + i = 0 + types.each_byte do |t| + case t + when ?a then add_text strs[i] + when ?s then add_lwsp strs[i] + else + raise "TMail FATAL: unknown flag: #{t.chr}" + end + i += 1 + end + end + + METHOD_ID = { + ?j => :extract_J, + ?e => :extract_E, + ?a => :extract_A, + ?s => :extract_S + } + + def concat_E( types, strs ) + if BENCODE_DEBUG + puts '---- concat_E' + puts "types=#{types.split(//).join(' ')}" + puts "strs =#{strs.inspect}" + end + + flush() unless @text.empty? + + chunk = '' + strs.each_with_index do |s,i| + mid = METHOD_ID[types[i]] + until s.empty? + unless c = __send__(mid, chunk.size, s) + add_with_encode chunk unless chunk.empty? + flush + chunk = '' + fold + c = __send__(mid, 0, s) + raise 'TMail FATAL: extract fail' unless c + end + chunk << c + end + end + add_with_encode chunk unless chunk.empty? + end + + def extract_J( chunksize, str ) + size = max_bytes(chunksize, str.size) - 6 + size = (size % 2 == 0) ? (size) : (size - 1) + return nil if size <= 0 + "\e$B#{str.slice!(0, size)}\e(B" + end + + def extract_A( chunksize, str ) + size = max_bytes(chunksize, str.size) + return nil if size <= 0 + str.slice!(0, size) + end + + alias extract_S extract_A + + def max_bytes( chunksize, ssize ) + (restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize + end + + # + # free length buffer + # + + def add_text( str ) + @text << str + # puts '---- text -------------------------------------' + # puts "+ #{str.inspect}" + # puts "txt >>>#{@text.inspect}<<<" + end + + def add_with_encode( str ) + @text << "=?iso-2022-jp?B?#{Base64.encode(str)}?=" + end + + def add_lwsp( lwsp ) + # puts '---- lwsp -------------------------------------' + # puts "+ #{lwsp.inspect}" + fold if restsize() <= 0 + flush + @lwsp = lwsp + end + + def flush + # puts '---- flush ----' + # puts "spc >>>#{@lwsp.inspect}<<<" + # puts "txt >>>#{@text.inspect}<<<" + @f << @lwsp << @text + @curlen += (@lwsp.size + @text.size) + @text = '' + @lwsp = '' + end + + def fold + # puts '---- fold ----' + @f << @eol + @curlen = 0 + @lwsp = SPACER + end + + def restsize + MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size) + end + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/facade.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/facade.rb new file mode 100755 index 00000000..1ecd64bf --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/facade.rb @@ -0,0 +1,552 @@ +# +# facade.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/utils' + + +module TMail + + class Mail + + def header_string( name, default = nil ) + h = @header[name.downcase] or return default + h.to_s + end + + ### + ### attributes + ### + + include TextUtils + + def set_string_array_attr( key, strs ) + strs.flatten! + if strs.empty? + @header.delete key.downcase + else + store key, strs.join(', ') + end + strs + end + private :set_string_array_attr + + def set_string_attr( key, str ) + if str + store key, str + else + @header.delete key.downcase + end + str + end + private :set_string_attr + + def set_addrfield( name, arg ) + if arg + h = HeaderField.internal_new(name, @config) + h.addrs.replace [arg].flatten + @header[name] = h + else + @header.delete name + end + arg + end + private :set_addrfield + + def addrs2specs( addrs ) + return nil unless addrs + list = addrs.map {|addr| + if addr.address_group? + then addr.map {|a| a.spec } + else addr.spec + end + }.flatten + return nil if list.empty? + list + end + private :addrs2specs + + + # + # date time + # + + def date( default = nil ) + if h = @header['date'] + h.date + else + default + end + end + + def date=( time ) + if time + store 'Date', time2str(time) + else + @header.delete 'date' + end + time + end + + def strftime( fmt, default = nil ) + if t = date + t.strftime(fmt) + else + default + end + end + + + # + # destination + # + + def to_addrs( default = nil ) + if h = @header['to'] + h.addrs + else + default + end + end + + def cc_addrs( default = nil ) + if h = @header['cc'] + h.addrs + else + default + end + end + + def bcc_addrs( default = nil ) + if h = @header['bcc'] + h.addrs + else + default + end + end + + def to_addrs=( arg ) + set_addrfield 'to', arg + end + + def cc_addrs=( arg ) + set_addrfield 'cc', arg + end + + def bcc_addrs=( arg ) + set_addrfield 'bcc', arg + end + + def to( default = nil ) + addrs2specs(to_addrs(nil)) || default + end + + def cc( default = nil ) + addrs2specs(cc_addrs(nil)) || default + end + + def bcc( default = nil ) + addrs2specs(bcc_addrs(nil)) || default + end + + def to=( *strs ) + set_string_array_attr 'To', strs + end + + def cc=( *strs ) + set_string_array_attr 'Cc', strs + end + + def bcc=( *strs ) + set_string_array_attr 'Bcc', strs + end + + + # + # originator + # + + def from_addrs( default = nil ) + if h = @header['from'] + h.addrs + else + default + end + end + + def from_addrs=( arg ) + set_addrfield 'from', arg + end + + def from( default = nil ) + addrs2specs(from_addrs(nil)) || default + end + + def from=( *strs ) + set_string_array_attr 'From', strs + end + + def friendly_from( default = nil ) + h = @header['from'] + a, = h.addrs + return default unless a + return a.phrase if a.phrase + return h.comments.join(' ') unless h.comments.empty? + a.spec + end + + + def reply_to_addrs( default = nil ) + if h = @header['reply-to'] + h.addrs + else + default + end + end + + def reply_to_addrs=( arg ) + set_addrfield 'reply-to', arg + end + + def reply_to( default = nil ) + addrs2specs(reply_to_addrs(nil)) || default + end + + def reply_to=( *strs ) + set_string_array_attr 'Reply-To', strs + end + + + def sender_addr( default = nil ) + f = @header['sender'] or return default + f.addr or return default + end + + def sender_addr=( addr ) + if addr + h = HeaderField.internal_new('sender', @config) + h.addr = addr + @header['sender'] = h + else + @header.delete 'sender' + end + addr + end + + def sender( default ) + f = @header['sender'] or return default + a = f.addr or return default + a.spec + end + + def sender=( str ) + set_string_attr 'Sender', str + end + + + # + # subject + # + + def subject( default = nil ) + if h = @header['subject'] + h.body + else + default + end + end + alias quoted_subject subject + + def subject=( str ) + set_string_attr 'Subject', str + end + + + # + # identity & threading + # + + def message_id( default = nil ) + if h = @header['message-id'] + h.id || default + else + default + end + end + + def message_id=( str ) + set_string_attr 'Message-Id', str + end + + def in_reply_to( default = nil ) + if h = @header['in-reply-to'] + h.ids + else + default + end + end + + def in_reply_to=( *idstrs ) + set_string_array_attr 'In-Reply-To', idstrs + end + + def references( default = nil ) + if h = @header['references'] + h.refs + else + default + end + end + + def references=( *strs ) + set_string_array_attr 'References', strs + end + + + # + # MIME headers + # + + def mime_version( default = nil ) + if h = @header['mime-version'] + h.version || default + else + default + end + end + + def mime_version=( m, opt = nil ) + if opt + if h = @header['mime-version'] + h.major = m + h.minor = opt + else + store 'Mime-Version', "#{m}.#{opt}" + end + else + store 'Mime-Version', m + end + m + end + + + def content_type( default = nil ) + if h = @header['content-type'] + h.content_type || default + else + default + end + end + + def main_type( default = nil ) + if h = @header['content-type'] + h.main_type || default + else + default + end + end + + def sub_type( default = nil ) + if h = @header['content-type'] + h.sub_type || default + else + default + end + end + + def set_content_type( str, sub = nil, param = nil ) + if sub + main, sub = str, sub + else + main, sub = str.split(%r, 2) + raise ArgumentError, "sub type missing: #{str.inspect}" unless sub + end + if h = @header['content-type'] + h.main_type = main + h.sub_type = sub + h.params.clear + else + store 'Content-Type', "#{main}/#{sub}" + end + @header['content-type'].params.replace param if param + + str + end + + alias content_type= set_content_type + + def type_param( name, default = nil ) + if h = @header['content-type'] + h[name] || default + else + default + end + end + + def charset( default = nil ) + if h = @header['content-type'] + h['charset'] or default + else + default + end + end + + def charset=( str ) + if str + if h = @header[ 'content-type' ] + h['charset'] = str + else + store 'Content-Type', "text/plain; charset=#{str}" + end + end + str + end + + + def transfer_encoding( default = nil ) + if h = @header['content-transfer-encoding'] + h.encoding || default + else + default + end + end + + def transfer_encoding=( str ) + set_string_attr 'Content-Transfer-Encoding', str + end + + alias encoding transfer_encoding + alias encoding= transfer_encoding= + alias content_transfer_encoding transfer_encoding + alias content_transfer_encoding= transfer_encoding= + + + def disposition( default = nil ) + if h = @header['content-disposition'] + h.disposition || default + else + default + end + end + + alias content_disposition disposition + + def set_disposition( str, params = nil ) + if h = @header['content-disposition'] + h.disposition = str + h.params.clear + else + store('Content-Disposition', str) + h = @header['content-disposition'] + end + h.params.replace params if params + end + + alias disposition= set_disposition + alias set_content_disposition set_disposition + alias content_disposition= set_disposition + + def disposition_param( name, default = nil ) + if h = @header['content-disposition'] + h[name] || default + else + default + end + end + + ### + ### utils + ### + + def create_reply + mail = TMail::Mail.parse('') + mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '') + mail.to_addrs = reply_addresses([]) + mail.in_reply_to = [message_id(nil)].compact + mail.references = references([]) + [message_id(nil)].compact + mail.mime_version = '1.0' + mail + end + + + def base64_encode + store 'Content-Transfer-Encoding', 'Base64' + self.body = Base64.folding_encode(self.body) + end + + def base64_decode + if /base64/i === self.transfer_encoding('') + store 'Content-Transfer-Encoding', '8bit' + self.body = Base64.decode(self.body, @config.strict_base64decode?) + end + end + + + def destinations( default = nil ) + ret = [] + %w( to cc bcc ).each do |nm| + if h = @header[nm] + h.addrs.each {|i| ret.push i.address } + end + end + ret.empty? ? default : ret + end + + def each_destination( &block ) + destinations([]).each do |i| + if Address === i + yield i + else + i.each(&block) + end + end + end + + alias each_dest each_destination + + + def reply_addresses( default = nil ) + reply_to_addrs(nil) or from_addrs(nil) or default + end + + def error_reply_addresses( default = nil ) + if s = sender(nil) + [s] + else + from_addrs(default) + end + end + + + def multipart? + main_type('').downcase == 'multipart' + end + + end # class Mail + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/header.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/header.rb new file mode 100755 index 00000000..be97803d --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/header.rb @@ -0,0 +1,914 @@ +# +# header.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/encode' +require 'tmail/address' +require 'tmail/parser' +require 'tmail/config' +require 'tmail/utils' + + +module TMail + + class HeaderField + + include TextUtils + + class << self + + alias newobj new + + def new( name, body, conf = DEFAULT_CONFIG ) + klass = FNAME_TO_CLASS[name.downcase] || UnstructuredHeader + klass.newobj body, conf + end + + def new_from_port( port, name, conf = DEFAULT_CONFIG ) + re = Regep.new('\A(' + Regexp.quote(name) + '):', 'i') + str = nil + port.ropen {|f| + f.each do |line| + if m = re.match(line) then str = m.post_match.strip + elsif str and /\A[\t ]/ === line then str << ' ' << line.strip + elsif /\A-*\s*\z/ === line then break + elsif str then break + end + end + } + new(name, str, Config.to_config(conf)) + end + + def internal_new( name, conf ) + FNAME_TO_CLASS[name].newobj('', conf, true) + end + + end # class << self + + def initialize( body, conf, intern = false ) + @body = body + @config = conf + + @illegal = false + @parsed = false + if intern + @parsed = true + parse_init + end + end + + def inspect + "#<#{self.class} #{@body.inspect}>" + end + + def illegal? + @illegal + end + + def empty? + ensure_parsed + return true if @illegal + isempty? + end + + private + + def ensure_parsed + return if @parsed + @parsed = true + parse + end + + # defabstract parse + # end + + def clear_parse_status + @parsed = false + @illegal = false + end + + public + + def body + ensure_parsed + v = Decoder.new(s = '') + do_accept v + v.terminate + s + end + + def body=( str ) + @body = str + clear_parse_status + end + + include StrategyInterface + + def accept( strategy, dummy1 = nil, dummy2 = nil ) + ensure_parsed + do_accept strategy + strategy.terminate + end + + # abstract do_accept + + end + + + class UnstructuredHeader < HeaderField + + def body + ensure_parsed + @body + end + + def body=( arg ) + ensure_parsed + @body = arg + end + + private + + def parse_init + end + + def parse + @body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, '')) + end + + def isempty? + not @body + end + + def do_accept( strategy ) + strategy.text @body + end + + end + + + class StructuredHeader < HeaderField + + def comments + ensure_parsed + @comments + end + + private + + def parse + save = nil + + begin + parse_init + do_parse + rescue SyntaxError + if not save and mime_encoded? @body + save = @body + @body = Decoder.decode(save) + retry + elsif save + @body = save + end + + @illegal = true + raise if @config.strict_parse? + end + end + + def parse_init + @comments = [] + init + end + + def do_parse + obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments) + set obj if obj + end + + end + + + class DateTimeHeader < StructuredHeader + + PARSE_TYPE = :DATETIME + + def date + ensure_parsed + @date + end + + def date=( arg ) + ensure_parsed + @date = arg + end + + private + + def init + @date = nil + end + + def set( t ) + @date = t + end + + def isempty? + not @date + end + + def do_accept( strategy ) + strategy.meta time2str(@date) + end + + end + + + class AddressHeader < StructuredHeader + + PARSE_TYPE = :MADDRESS + + def addrs + ensure_parsed + @addrs + end + + private + + def init + @addrs = [] + end + + def set( a ) + @addrs = a + end + + def isempty? + @addrs.empty? + end + + def do_accept( strategy ) + first = true + @addrs.each do |a| + if first + first = false + else + strategy.meta ',' + strategy.space + end + a.accept strategy + end + + @comments.each do |c| + strategy.space + strategy.meta '(' + strategy.text c + strategy.meta ')' + end + end + + end + + + class ReturnPathHeader < AddressHeader + + PARSE_TYPE = :RETPATH + + def addr + addrs()[0] + end + + def spec + a = addr() or return nil + a.spec + end + + def routes + a = addr() or return nil + a.routes + end + + private + + def do_accept( strategy ) + a = addr() + + strategy.meta '<' + unless a.routes.empty? + strategy.meta a.routes.map {|i| '@' + i }.join(',') + strategy.meta ':' + end + spec = a.spec + strategy.meta spec if spec + strategy.meta '>' + end + + end + + + class SingleAddressHeader < AddressHeader + + def addr + addrs()[0] + end + + private + + def do_accept( strategy ) + a = addr() + a.accept strategy + @comments.each do |c| + strategy.space + strategy.meta '(' + strategy.text c + strategy.meta ')' + end + end + + end + + + class MessageIdHeader < StructuredHeader + + def id + ensure_parsed + @id + end + + def id=( arg ) + ensure_parsed + @id = arg + end + + private + + def init + @id = nil + end + + def isempty? + not @id + end + + def do_parse + @id = @body.slice(MESSAGE_ID) or + raise SyntaxError, "wrong Message-ID format: #{@body}" + end + + def do_accept( strategy ) + strategy.meta @id + end + + end + + + class ReferencesHeader < StructuredHeader + + def refs + ensure_parsed + @refs + end + + def each_id + self.refs.each do |i| + yield i if MESSAGE_ID === i + end + end + + def ids + ensure_parsed + @ids + end + + def each_phrase + self.refs.each do |i| + yield i unless MESSAGE_ID === i + end + end + + def phrases + ret = [] + each_phrase {|i| ret.push i } + ret + end + + private + + def init + @refs = [] + @ids = [] + end + + def isempty? + @ids.empty? + end + + def do_parse + str = @body + while m = MESSAGE_ID.match(str) + pre = m.pre_match.strip + @refs.push pre unless pre.empty? + @refs.push s = m[0] + @ids.push s + str = m.post_match + end + str = str.strip + @refs.push str unless str.empty? + end + + def do_accept( strategy ) + first = true + @ids.each do |i| + if first + first = false + else + strategy.space + end + strategy.meta i + end + end + + end + + + class ReceivedHeader < StructuredHeader + + PARSE_TYPE = :RECEIVED + + def from + ensure_parsed + @from + end + + def from=( arg ) + ensure_parsed + @from = arg + end + + def by + ensure_parsed + @by + end + + def by=( arg ) + ensure_parsed + @by = arg + end + + def via + ensure_parsed + @via + end + + def via=( arg ) + ensure_parsed + @via = arg + end + + def with + ensure_parsed + @with + end + + def id + ensure_parsed + @id + end + + def id=( arg ) + ensure_parsed + @id = arg + end + + def _for + ensure_parsed + @_for + end + + def _for=( arg ) + ensure_parsed + @_for = arg + end + + def date + ensure_parsed + @date + end + + def date=( arg ) + ensure_parsed + @date = arg + end + + private + + def init + @from = @by = @via = @with = @id = @_for = nil + @with = [] + @date = nil + end + + def set( args ) + @from, @by, @via, @with, @id, @_for, @date = *args + end + + def isempty? + @with.empty? and not (@from or @by or @via or @id or @_for or @date) + end + + def do_accept( strategy ) + list = [] + list.push 'from ' + @from if @from + list.push 'by ' + @by if @by + list.push 'via ' + @via if @via + @with.each do |i| + list.push 'with ' + i + end + list.push 'id ' + @id if @id + list.push 'for <' + @_for + '>' if @_for + + first = true + list.each do |i| + strategy.space unless first + strategy.meta i + first = false + end + if @date + strategy.meta ';' + strategy.space + strategy.meta time2str(@date) + end + end + + end + + + class KeywordsHeader < StructuredHeader + + PARSE_TYPE = :KEYWORDS + + def keys + ensure_parsed + @keys + end + + private + + def init + @keys = [] + end + + def set( a ) + @keys = a + end + + def isempty? + @keys.empty? + end + + def do_accept( strategy ) + first = true + @keys.each do |i| + if first + first = false + else + strategy.meta ',' + end + strategy.meta i + end + end + + end + + + class EncryptedHeader < StructuredHeader + + PARSE_TYPE = :ENCRYPTED + + def encrypter + ensure_parsed + @encrypter + end + + def encrypter=( arg ) + ensure_parsed + @encrypter = arg + end + + def keyword + ensure_parsed + @keyword + end + + def keyword=( arg ) + ensure_parsed + @keyword = arg + end + + private + + def init + @encrypter = nil + @keyword = nil + end + + def set( args ) + @encrypter, @keyword = args + end + + def isempty? + not (@encrypter or @keyword) + end + + def do_accept( strategy ) + if @key + strategy.meta @encrypter + ',' + strategy.space + strategy.meta @keyword + else + strategy.meta @encrypter + end + end + + end + + + class MimeVersionHeader < StructuredHeader + + PARSE_TYPE = :MIMEVERSION + + def major + ensure_parsed + @major + end + + def major=( arg ) + ensure_parsed + @major = arg + end + + def minor + ensure_parsed + @minor + end + + def minor=( arg ) + ensure_parsed + @minor = arg + end + + def version + sprintf('%d.%d', major, minor) + end + + private + + def init + @major = nil + @minor = nil + end + + def set( args ) + @major, @minor = *args + end + + def isempty? + not (@major or @minor) + end + + def do_accept( strategy ) + strategy.meta sprintf('%d.%d', @major, @minor) + end + + end + + + class ContentTypeHeader < StructuredHeader + + PARSE_TYPE = :CTYPE + + def main_type + ensure_parsed + @main + end + + def main_type=( arg ) + ensure_parsed + @main = arg.downcase + end + + def sub_type + ensure_parsed + @sub + end + + def sub_type=( arg ) + ensure_parsed + @sub = arg.downcase + end + + def content_type + ensure_parsed + @sub ? sprintf('%s/%s', @main, @sub) : @main + end + + def params + ensure_parsed + @params + end + + def []( key ) + ensure_parsed + @params and @params[key] + end + + def []=( key, val ) + ensure_parsed + (@params ||= {})[key] = val + end + + private + + def init + @main = @sub = @params = nil + end + + def set( args ) + @main, @sub, @params = *args + end + + def isempty? + not (@main or @sub) + end + + def do_accept( strategy ) + if @sub + strategy.meta sprintf('%s/%s', @main, @sub) + else + strategy.meta @main + end + @params.each do |k,v| + if v + strategy.meta ';' + strategy.space + strategy.kv_pair k, v + end + end + end + + end + + + class ContentTransferEncodingHeader < StructuredHeader + + PARSE_TYPE = :CENCODING + + def encoding + ensure_parsed + @encoding + end + + def encoding=( arg ) + ensure_parsed + @encoding = arg + end + + private + + def init + @encoding = nil + end + + def set( s ) + @encoding = s + end + + def isempty? + not @encoding + end + + def do_accept( strategy ) + strategy.meta @encoding.capitalize + end + + end + + + class ContentDispositionHeader < StructuredHeader + + PARSE_TYPE = :CDISPOSITION + + def disposition + ensure_parsed + @disposition + end + + def disposition=( str ) + ensure_parsed + @disposition = str.downcase + end + + def params + ensure_parsed + @params + end + + def []( key ) + ensure_parsed + @params and @params[key] + end + + def []=( key, val ) + ensure_parsed + (@params ||= {})[key] = val + end + + private + + def init + @disposition = @params = nil + end + + def set( args ) + @disposition, @params = *args + end + + def isempty? + not @disposition and (not @params or @params.empty?) + end + + def do_accept( strategy ) + strategy.meta @disposition + @params.each do |k,v| + strategy.meta ';' + strategy.space + strategy.kv_pair k, v + end + end + + end + + + class HeaderField # redefine + + FNAME_TO_CLASS = { + 'date' => DateTimeHeader, + 'resent-date' => DateTimeHeader, + 'to' => AddressHeader, + 'cc' => AddressHeader, + 'bcc' => AddressHeader, + 'from' => AddressHeader, + 'reply-to' => AddressHeader, + 'resent-to' => AddressHeader, + 'resent-cc' => AddressHeader, + 'resent-bcc' => AddressHeader, + 'resent-from' => AddressHeader, + 'resent-reply-to' => AddressHeader, + 'sender' => SingleAddressHeader, + 'resent-sender' => SingleAddressHeader, + 'return-path' => ReturnPathHeader, + 'message-id' => MessageIdHeader, + 'resent-message-id' => MessageIdHeader, + 'in-reply-to' => ReferencesHeader, + 'received' => ReceivedHeader, + 'references' => ReferencesHeader, + 'keywords' => KeywordsHeader, + 'encrypted' => EncryptedHeader, + 'mime-version' => MimeVersionHeader, + 'content-type' => ContentTypeHeader, + 'content-transfer-encoding' => ContentTransferEncodingHeader, + 'content-disposition' => ContentDispositionHeader, + 'content-id' => MessageIdHeader, + 'subject' => UnstructuredHeader, + 'comments' => UnstructuredHeader, + 'content-description' => UnstructuredHeader + } + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/info.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/info.rb new file mode 100755 index 00000000..5c115d58 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/info.rb @@ -0,0 +1,35 @@ +# +# info.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +module TMail + + Version = '0.10.7' + Copyright = 'Copyright (c) 1998-2002 Minero Aoki' + +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/loader.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/loader.rb new file mode 100755 index 00000000..79073154 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/loader.rb @@ -0,0 +1 @@ +require 'tmail/mailbox' diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mail.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mail.rb new file mode 100755 index 00000000..e11fa0f0 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mail.rb @@ -0,0 +1,447 @@ +# +# mail.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/facade' +require 'tmail/encode' +require 'tmail/header' +require 'tmail/port' +require 'tmail/config' +require 'tmail/utils' +require 'tmail/attachments' +require 'tmail/quoting' +require 'socket' + + +module TMail + + class Mail + + class << self + def load( fname ) + new(FilePort.new(fname)) + end + + alias load_from load + alias loadfrom load + + def parse( str ) + new(StringPort.new(str)) + end + end + + def initialize( port = nil, conf = DEFAULT_CONFIG ) + @port = port || StringPort.new + @config = Config.to_config(conf) + + @header = {} + @body_port = nil + @body_parsed = false + @epilogue = '' + @parts = [] + + @port.ropen {|f| + parse_header f + parse_body f unless @port.reproducible? + } + end + + attr_reader :port + + def inspect + "\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>" + end + + # + # to_s interfaces + # + + public + + include StrategyInterface + + def write_back( eol = "\n", charset = 'e' ) + parse_body + @port.wopen {|stream| encoded eol, charset, stream } + end + + def accept( strategy ) + with_multipart_encoding(strategy) { + ordered_each do |name, field| + next if field.empty? + strategy.header_name canonical(name) + field.accept strategy + strategy.puts + end + strategy.puts + body_port().ropen {|r| + strategy.write r.read + } + } + end + + private + + def canonical( name ) + name.split(/-/).map {|s| s.capitalize }.join('-') + end + + def with_multipart_encoding( strategy ) + if parts().empty? # DO NOT USE @parts + yield + + else + bound = ::TMail.new_boundary + if @header.key? 'content-type' + @header['content-type'].params['boundary'] = bound + else + store 'Content-Type', % + end + + yield + + parts().each do |tm| + strategy.puts + strategy.puts '--' + bound + tm.accept strategy + end + strategy.puts + strategy.puts '--' + bound + '--' + strategy.write epilogue() + end + end + + ### + ### header + ### + + public + + ALLOW_MULTIPLE = { + 'received' => true, + 'resent-date' => true, + 'resent-from' => true, + 'resent-sender' => true, + 'resent-to' => true, + 'resent-cc' => true, + 'resent-bcc' => true, + 'resent-message-id' => true, + 'comments' => true, + 'keywords' => true + } + USE_ARRAY = ALLOW_MULTIPLE + + def header + @header.dup + end + + def []( key ) + @header[key.downcase] + end + + def sub_header(key, param) + (hdr = self[key]) ? hdr[param] : nil + end + + alias fetch [] + + def []=( key, val ) + dkey = key.downcase + + if val.nil? + @header.delete dkey + return nil + end + + case val + when String + header = new_hf(key, val) + when HeaderField + ; + when Array + ALLOW_MULTIPLE.include? dkey or + raise ArgumentError, "#{key}: Header must not be multiple" + @header[dkey] = val + return val + else + header = new_hf(key, val.to_s) + end + if ALLOW_MULTIPLE.include? dkey + (@header[dkey] ||= []).push header + else + @header[dkey] = header + end + + val + end + + alias store []= + + def each_header + @header.each do |key, val| + [val].flatten.each {|v| yield key, v } + end + end + + alias each_pair each_header + + def each_header_name( &block ) + @header.each_key(&block) + end + + alias each_key each_header_name + + def each_field( &block ) + @header.values.flatten.each(&block) + end + + alias each_value each_field + + FIELD_ORDER = %w( + return-path received + resent-date resent-from resent-sender resent-to + resent-cc resent-bcc resent-message-id + date from sender reply-to to cc bcc + message-id in-reply-to references + subject comments keywords + mime-version content-type content-transfer-encoding + content-disposition content-description + ) + + def ordered_each + list = @header.keys + FIELD_ORDER.each do |name| + if list.delete(name) + [@header[name]].flatten.each {|v| yield name, v } + end + end + list.each do |name| + [@header[name]].flatten.each {|v| yield name, v } + end + end + + def clear + @header.clear + end + + def delete( key ) + @header.delete key.downcase + end + + def delete_if + @header.delete_if do |key,val| + if Array === val + val.delete_if {|v| yield key, v } + val.empty? + else + yield key, val + end + end + end + + def keys + @header.keys + end + + def key?( key ) + @header.key? key.downcase + end + + def values_at( *args ) + args.map {|k| @header[k.downcase] }.flatten + end + + alias indexes values_at + alias indices values_at + + private + + def parse_header( f ) + name = field = nil + unixfrom = nil + + while line = f.gets + case line + when /\A[ \t]/ # continue from prev line + raise SyntaxError, 'mail is began by space' unless field + field << ' ' << line.strip + + when /\A([^\: \t]+):\s*/ # new header line + add_hf name, field if field + name = $1 + field = $' #.strip + + when /\A\-*\s*\z/ # end of header + add_hf name, field if field + name = field = nil + break + + when /\AFrom (\S+)/ + unixfrom = $1 + + when /^charset=.*/ + + else + raise SyntaxError, "wrong mail header: '#{line.inspect}'" + end + end + add_hf name, field if name + + if unixfrom + add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path'] + end + end + + def add_hf( name, field ) + key = name.downcase + field = new_hf(name, field) + + if ALLOW_MULTIPLE.include? key + (@header[key] ||= []).push field + else + @header[key] = field + end + end + + def new_hf( name, field ) + HeaderField.new(name, field, @config) + end + + ### + ### body + ### + + public + + def body_port + parse_body + @body_port + end + + def each( &block ) + body_port().ropen {|f| f.each(&block) } + end + + def quoted_body + parse_body + @body_port.ropen {|f| + return f.read + } + end + + def body=( str ) + parse_body + @body_port.wopen {|f| f.write str } + str + end + + alias preamble body + alias preamble= body= + + def epilogue + parse_body + @epilogue.dup + end + + def epilogue=( str ) + parse_body + @epilogue = str + str + end + + def parts + parse_body + @parts + end + + def each_part( &block ) + parts().each(&block) + end + + private + + def parse_body( f = nil ) + return if @body_parsed + if f + parse_body_0 f + else + @port.ropen {|f| + skip_header f + parse_body_0 f + } + end + @body_parsed = true + end + + def skip_header( f ) + while line = f.gets + return if /\A[\r\n]*\z/ === line + end + end + + def parse_body_0( f ) + if multipart? + read_multipart f + else + @body_port = @config.new_body_port(self) + @body_port.wopen {|w| + w.write f.read + } + end + end + + def read_multipart( src ) + bound = @header['content-type'].params['boundary'] + is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/ + lastbound = "--#{bound}--" + + ports = [ @config.new_preamble_port(self) ] + begin + f = ports.last.wopen + while line = src.gets + if is_sep === line + f.close + break if line.strip == lastbound + ports.push @config.new_part_port(self) + f = ports.last.wopen + else + f << line + end + end + @epilogue = (src.read || '') + ensure + f.close if f and not f.closed? + end + + @body_port = ports.shift + @parts = ports.map {|p| self.class.new(p, @config) } + end + + end # class Mail + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mailbox.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mailbox.rb new file mode 100755 index 00000000..d85915ed --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mailbox.rb @@ -0,0 +1,433 @@ +# +# mailbox.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/port' +require 'socket' +require 'mutex_m' + + +unless [].respond_to?(:sort_by) +module Enumerable#:nodoc: + def sort_by + map {|i| [yield(i), i] }.sort {|a,b| a.first <=> b.first }.map {|i| i[1] } + end +end +end + + +module TMail + + class MhMailbox + + PORT_CLASS = MhPort + + def initialize( dir ) + edir = File.expand_path(dir) + raise ArgumentError, "not directory: #{dir}"\ + unless FileTest.directory? edir + @dirname = edir + @last_file = nil + @last_atime = nil + end + + def directory + @dirname + end + + alias dirname directory + + attr_accessor :last_atime + + def inspect + "#<#{self.class} #{@dirname}>" + end + + def close + end + + def new_port + PORT_CLASS.new(next_file_name()) + end + + def each_port + mail_files().each do |path| + yield PORT_CLASS.new(path) + end + @last_atime = Time.now + end + + alias each each_port + + def reverse_each_port + mail_files().reverse_each do |path| + yield PORT_CLASS.new(path) + end + @last_atime = Time.now + end + + alias reverse_each reverse_each_port + + # old #each_mail returns Port + #def each_mail + # each_port do |port| + # yield Mail.new(port) + # end + #end + + def each_new_port( mtime = nil, &block ) + mtime ||= @last_atime + return each_port(&block) unless mtime + return unless File.mtime(@dirname) >= mtime + + mail_files().each do |path| + yield PORT_CLASS.new(path) if File.mtime(path) > mtime + end + @last_atime = Time.now + end + + private + + def mail_files + Dir.entries(@dirname)\ + .select {|s| /\A\d+\z/ === s }\ + .map {|s| s.to_i }\ + .sort\ + .map {|i| "#{@dirname}/#{i}" }\ + .select {|path| FileTest.file? path } + end + + def next_file_name + unless n = @last_file + n = 0 + Dir.entries(@dirname)\ + .select {|s| /\A\d+\z/ === s }\ + .map {|s| s.to_i }.sort\ + .each do |i| + next unless FileTest.file? "#{@dirname}/#{i}" + n = i + end + end + begin + n += 1 + end while FileTest.exist? "#{@dirname}/#{n}" + @last_file = n + + "#{@dirname}/#{n}" + end + + end # MhMailbox + + MhLoader = MhMailbox + + + class UNIXMbox + + def UNIXMbox.lock( fname ) + begin + f = File.open(fname) + f.flock File::LOCK_EX + yield f + ensure + f.flock File::LOCK_UN + f.close if f and not f.closed? + end + end + + class << self + alias newobj new + end + + def UNIXMbox.new( fname, tmpdir = nil, readonly = false ) + tmpdir = ENV['TEMP'] || ENV['TMP'] || '/tmp' + newobj(fname, "#{tmpdir}/ruby_tmail_#{$$}_#{rand()}", readonly, false) + end + + def UNIXMbox.static_new( fname, dir, readonly = false ) + newobj(fname, dir, readonly, true) + end + + def initialize( fname, mhdir, readonly, static ) + @filename = fname + @readonly = readonly + @closed = false + + Dir.mkdir mhdir + @real = MhMailbox.new(mhdir) + @finalizer = UNIXMbox.mkfinal(@real, @filename, !@readonly, !static) + ObjectSpace.define_finalizer self, @finalizer + end + + def UNIXMbox.mkfinal( mh, mboxfile, writeback_p, cleanup_p ) + lambda { + if writeback_p + lock(mboxfile) {|f| + mh.each_port do |port| + f.puts create_from_line(port) + port.ropen {|r| + f.puts r.read + } + end + } + end + if cleanup_p + Dir.foreach(mh.dirname) do |fname| + next if /\A\.\.?\z/ === fname + File.unlink "#{mh.dirname}/#{fname}" + end + Dir.rmdir mh.dirname + end + } + end + + # make _From line + def UNIXMbox.create_from_line( port ) + sprintf 'From %s %s', + fromaddr(), TextUtils.time2str(File.mtime(port.filename)) + end + + def UNIXMbox.fromaddr + h = HeaderField.new_from_port(port, 'Return-Path') || + HeaderField.new_from_port(port, 'From') or return 'nobody' + a = h.addrs[0] or return 'nobody' + a.spec + end + private_class_method :fromaddr + + def close + return if @closed + + ObjectSpace.undefine_finalizer self + @finalizer.call + @finalizer = nil + @real = nil + @closed = true + @updated = nil + end + + def each_port( &block ) + close_check + update + @real.each_port(&block) + end + + alias each each_port + + def reverse_each_port( &block ) + close_check + update + @real.reverse_each_port(&block) + end + + alias reverse_each reverse_each_port + + # old #each_mail returns Port + #def each_mail( &block ) + # each_port do |port| + # yield Mail.new(port) + # end + #end + + def each_new_port( mtime = nil ) + close_check + update + @real.each_new_port(mtime) {|p| yield p } + end + + def new_port + close_check + @real.new_port + end + + private + + def close_check + @closed and raise ArgumentError, 'accessing already closed mbox' + end + + def update + return if FileTest.zero?(@filename) + return if @updated and File.mtime(@filename) < @updated + w = nil + port = nil + time = nil + UNIXMbox.lock(@filename) {|f| + begin + f.each do |line| + if /\AFrom / === line + w.close if w + File.utime time, time, port.filename if time + + port = @real.new_port + w = port.wopen + time = fromline2time(line) + else + w.print line if w + end + end + ensure + if w and not w.closed? + w.close + File.utime time, time, port.filename if time + end + end + f.truncate(0) unless @readonly + @updated = Time.now + } + end + + def fromline2time( line ) + m = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/.match(line) \ + or return nil + Time.local(m[6].to_i, m[1], m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i) + end + + end # UNIXMbox + + MboxLoader = UNIXMbox + + + class Maildir + + extend Mutex_m + + PORT_CLASS = MaildirPort + + @seq = 0 + def Maildir.unique_number + synchronize { + @seq += 1 + return @seq + } + end + + def initialize( dir = nil ) + @dirname = dir || ENV['MAILDIR'] + raise ArgumentError, "not directory: #{@dirname}"\ + unless FileTest.directory? @dirname + @new = "#{@dirname}/new" + @tmp = "#{@dirname}/tmp" + @cur = "#{@dirname}/cur" + end + + def directory + @dirname + end + + def inspect + "#<#{self.class} #{@dirname}>" + end + + def close + end + + def each_port + mail_files(@cur).each do |path| + yield PORT_CLASS.new(path) + end + end + + alias each each_port + + def reverse_each_port + mail_files(@cur).reverse_each do |path| + yield PORT_CLASS.new(path) + end + end + + alias reverse_each reverse_each_port + + def new_port + fname = nil + tmpfname = nil + newfname = nil + + begin + fname = "#{Time.now.to_i}.#{$$}_#{Maildir.unique_number}.#{Socket.gethostname}" + + tmpfname = "#{@tmp}/#{fname}" + newfname = "#{@new}/#{fname}" + end while FileTest.exist? tmpfname + + if block_given? + File.open(tmpfname, 'w') {|f| yield f } + File.rename tmpfname, newfname + PORT_CLASS.new(newfname) + else + File.open(tmpfname, 'w') {|f| f.write "\n\n" } + PORT_CLASS.new(tmpfname) + end + end + + def each_new_port + mail_files(@new).each do |path| + dest = @cur + '/' + File.basename(path) + File.rename path, dest + yield PORT_CLASS.new(dest) + end + + check_tmp + end + + TOO_OLD = 60 * 60 * 36 # 36 hour + + def check_tmp + old = Time.now.to_i - TOO_OLD + + each_filename(@tmp) do |full, fname| + if FileTest.file? full and + File.stat(full).mtime.to_i < old + File.unlink full + end + end + end + + private + + def mail_files( dir ) + Dir.entries(dir)\ + .select {|s| s[0] != ?. }\ + .sort_by {|s| s.slice(/\A\d+/).to_i }\ + .map {|s| "#{dir}/#{s}" }\ + .select {|path| FileTest.file? path } + end + + def each_filename( dir ) + Dir.foreach(dir) do |fname| + path = "#{dir}/#{fname}" + if fname[0] != ?. and FileTest.file? path + yield path, fname + end + end + end + + end # Maildir + + MaildirLoader = Maildir + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mbox.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mbox.rb new file mode 100755 index 00000000..79073154 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/mbox.rb @@ -0,0 +1 @@ +require 'tmail/mailbox' diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/net.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/net.rb new file mode 100755 index 00000000..f96cf64c --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/net.rb @@ -0,0 +1,280 @@ +# +# net.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'nkf' + + +module TMail + + class Mail + + def send_to( smtp ) + do_send_to(smtp) do + ready_to_send + end + end + + def send_text_to( smtp ) + do_send_to(smtp) do + ready_to_send + mime_encode + end + end + + def do_send_to( smtp ) + from = from_address or raise ArgumentError, 'no from address' + (dests = destinations).empty? and raise ArgumentError, 'no receipient' + yield + send_to_0 smtp, from, dests + end + private :do_send_to + + def send_to_0( smtp, from, to ) + smtp.ready(from, to) do |f| + encoded "\r\n", 'j', f, '' + end + end + + def ready_to_send + delete_no_send_fields + add_message_id + add_date + end + + NOSEND_FIELDS = %w( + received + bcc + ) + + def delete_no_send_fields + NOSEND_FIELDS.each do |nm| + delete nm + end + delete_if {|n,v| v.empty? } + end + + def add_message_id( fqdn = nil ) + self.message_id = ::TMail::new_message_id(fqdn) + end + + def add_date + self.date = Time.now + end + + def mime_encode + if parts.empty? + mime_encode_singlepart + else + mime_encode_multipart true + end + end + + def mime_encode_singlepart + self.mime_version = '1.0' + b = body + if NKF.guess(b) != NKF::BINARY + mime_encode_text b + else + mime_encode_binary b + end + end + + def mime_encode_text( body ) + self.body = NKF.nkf('-j -m0', body) + self.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'} + self.encoding = '7bit' + end + + def mime_encode_binary( body ) + self.body = [body].pack('m') + self.set_content_type 'application', 'octet-stream' + self.encoding = 'Base64' + end + + def mime_encode_multipart( top = true ) + self.mime_version = '1.0' if top + self.set_content_type 'multipart', 'mixed' + e = encoding(nil) + if e and not /\A(?:7bit|8bit|binary)\z/i === e + raise ArgumentError, + 'using C.T.Encoding with multipart mail is not permitted' + end + end + + def create_empty_mail + self.class.new(StringPort.new(''), @config) + end + + def create_reply + setup_reply create_empty_mail() + end + + def setup_reply( m ) + if tmp = reply_addresses(nil) + m.to_addrs = tmp + end + + mid = message_id(nil) + tmp = references(nil) || [] + tmp.push mid if mid + m.in_reply_to = [mid] if mid + m.references = tmp unless tmp.empty? + m.subject = 'Re: ' + subject('').sub(/\A(?:\s*re:)+/i, '') + + m + end + + def create_forward + setup_forward create_empty_mail() + end + + def setup_forward( mail ) + m = Mail.new(StringPort.new('')) + m.body = decoded + m.set_content_type 'message', 'rfc822' + m.encoding = encoding('7bit') + mail.parts.push m + end + + end + + + class DeleteFields + + NOSEND_FIELDS = %w( + received + bcc + ) + + def initialize( nosend = nil, delempty = true ) + @no_send_fields = nosend || NOSEND_FIELDS.dup + @delete_empty_fields = delempty + end + + attr :no_send_fields + attr :delete_empty_fields, true + + def exec( mail ) + @no_send_fields.each do |nm| + delete nm + end + delete_if {|n,v| v.empty? } if @delete_empty_fields + end + + end + + + class AddMessageId + + def initialize( fqdn = nil ) + @fqdn = fqdn + end + + attr :fqdn, true + + def exec( mail ) + mail.message_id = ::TMail::new_msgid(@fqdn) + end + + end + + + class AddDate + + def exec( mail ) + mail.date = Time.now + end + + end + + + class MimeEncodeAuto + + def initialize( s = nil, m = nil ) + @singlepart_composer = s || MimeEncodeSingle.new + @multipart_composer = m || MimeEncodeMulti.new + end + + attr :singlepart_composer + attr :multipart_composer + + def exec( mail ) + if mail._builtin_multipart? + then @multipart_composer + else @singlepart_composer end.exec mail + end + + end + + + class MimeEncodeSingle + + def exec( mail ) + mail.mime_version = '1.0' + b = mail.body + if NKF.guess(b) != NKF::BINARY + on_text b + else + on_binary b + end + end + + def on_text( body ) + mail.body = NKF.nkf('-j -m0', body) + mail.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'} + mail.encoding = '7bit' + end + + def on_binary( body ) + mail.body = [body].pack('m') + mail.set_content_type 'application', 'octet-stream' + mail.encoding = 'Base64' + end + + end + + + class MimeEncodeMulti + + def exec( mail, top = true ) + mail.mime_version = '1.0' if top + mail.set_content_type 'multipart', 'mixed' + e = encoding(nil) + if e and not /\A(?:7bit|8bit|binary)\z/i === e + raise ArgumentError, + 'using C.T.Encoding with multipart mail is not permitted' + end + mail.parts.each do |m| + exec m, false if m._builtin_multipart? + end + end + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/obsolete.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/obsolete.rb new file mode 100755 index 00000000..f98be747 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/obsolete.rb @@ -0,0 +1,135 @@ +# +# obsolete.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +module TMail + + # mail.rb + class Mail + alias include? key? + alias has_key? key? + + def values + ret = [] + each_field {|v| ret.push v } + ret + end + + def value?( val ) + HeaderField === val or return false + + [ @header[val.name.downcase] ].flatten.include? val + end + + alias has_value? value? + end + + + # facade.rb + class Mail + def from_addr( default = nil ) + addr, = from_addrs(nil) + addr || default + end + + def from_address( default = nil ) + if a = from_addr(nil) + a.spec + else + default + end + end + + alias from_address= from_addrs= + + def from_phrase( default = nil ) + if a = from_addr(nil) + a.phrase + else + default + end + end + + alias msgid message_id + alias msgid= message_id= + + alias each_dest each_destination + end + + + # address.rb + class Address + alias route routes + alias addr spec + + def spec=( str ) + @local, @domain = str.split(/@/,2).map {|s| s.split(/\./) } + end + + alias addr= spec= + alias address= spec= + end + + + # mbox.rb + class MhMailbox + alias new_mail new_port + alias each_mail each_port + alias each_newmail each_new_port + end + class UNIXMbox + alias new_mail new_port + alias each_mail each_port + alias each_newmail each_new_port + end + class Maildir + alias new_mail new_port + alias each_mail each_port + alias each_newmail each_new_port + end + + + # utils.rb + extend TextUtils + + class << self + alias msgid? message_id? + alias boundary new_boundary + alias msgid new_message_id + alias new_msgid new_message_id + end + + def Mail.boundary + ::TMail.new_boundary + end + + def Mail.msgid + ::TMail.new_message_id + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/parser.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/parser.rb new file mode 100755 index 00000000..825eca92 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/parser.rb @@ -0,0 +1,1522 @@ +# +# DO NOT MODIFY!!!! +# This file is automatically generated by racc 1.4.3 +# from racc grammer file "parser.y". +# +# +# parser.rb: generated by racc (runtime embedded) +# + +###### racc/parser.rb + +unless $".index 'racc/parser.rb' +$".push 'racc/parser.rb' + +self.class.module_eval <<'..end /home/aamine/lib/ruby/racc/parser.rb modeval..idb76f2e220d', '/home/aamine/lib/ruby/racc/parser.rb', 1 +# +# parser.rb +# +# Copyright (c) 1999-2003 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the same terms of ruby. +# +# As a special exception, when this code is copied by Racc +# into a Racc output file, you may use that output file +# without restriction. +# +# $Id: parser.rb,v 1.1.1.1 2004/10/14 11:59:58 webster132 Exp $ +# + +unless defined? NotImplementedError + NotImplementedError = NotImplementError +end + + +module Racc + class ParseError < StandardError; end +end +unless defined?(::ParseError) + ParseError = Racc::ParseError +end + + +module Racc + + unless defined? Racc_No_Extentions + Racc_No_Extentions = false + end + + class Parser + + Racc_Runtime_Version = '1.4.3' + Racc_Runtime_Revision = '$Revision: 1.1.1.1 $'.split(/\s+/)[1] + + Racc_Runtime_Core_Version_R = '1.4.3' + Racc_Runtime_Core_Revision_R = '$Revision: 1.1.1.1 $'.split(/\s+/)[1] + begin + require 'racc/cparse' + # Racc_Runtime_Core_Version_C = (defined in extention) + Racc_Runtime_Core_Revision_C = Racc_Runtime_Core_Id_C.split(/\s+/)[2] + unless new.respond_to?(:_racc_do_parse_c, true) + raise LoadError, 'old cparse.so' + end + if Racc_No_Extentions + raise LoadError, 'selecting ruby version of racc runtime core' + end + + Racc_Main_Parsing_Routine = :_racc_do_parse_c + Racc_YY_Parse_Method = :_racc_yyparse_c + Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_C + Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_C + Racc_Runtime_Type = 'c' + rescue LoadError + Racc_Main_Parsing_Routine = :_racc_do_parse_rb + Racc_YY_Parse_Method = :_racc_yyparse_rb + Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_R + Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_R + Racc_Runtime_Type = 'ruby' + end + + def self.racc_runtime_type + Racc_Runtime_Type + end + + private + + def _racc_setup + @yydebug = false unless self.class::Racc_debug_parser + @yydebug = false unless defined? @yydebug + if @yydebug + @racc_debug_out = $stderr unless defined? @racc_debug_out + @racc_debug_out ||= $stderr + end + arg = self.class::Racc_arg + arg[13] = true if arg.size < 14 + arg + end + + def _racc_init_sysvars + @racc_state = [0] + @racc_tstack = [] + @racc_vstack = [] + + @racc_t = nil + @racc_val = nil + + @racc_read_next = true + + @racc_user_yyerror = false + @racc_error_status = 0 + end + + + ### + ### do_parse + ### + + def do_parse + __send__ Racc_Main_Parsing_Routine, _racc_setup(), false + end + + def next_token + raise NotImplementedError, "#{self.class}\#next_token is not defined" + end + + def _racc_do_parse_rb( arg, in_debug ) + action_table, action_check, action_default, action_pointer, + goto_table, goto_check, goto_default, goto_pointer, + nt_base, reduce_table, token_table, shift_n, + reduce_n, use_result, * = arg + + _racc_init_sysvars + tok = act = i = nil + nerr = 0 + + catch(:racc_end_parse) { + while true + if i = action_pointer[@racc_state[-1]] + if @racc_read_next + if @racc_t != 0 # not EOF + tok, @racc_val = next_token() + unless tok # EOF + @racc_t = 0 + else + @racc_t = (token_table[tok] or 1) # error token + end + racc_read_token(@racc_t, tok, @racc_val) if @yydebug + @racc_read_next = false + end + end + i += @racc_t + if i >= 0 and + act = action_table[i] and + action_check[i] == @racc_state[-1] + ; + else + act = action_default[@racc_state[-1]] + end + else + act = action_default[@racc_state[-1]] + end + while act = _racc_evalact(act, arg) + end + end + } + end + + + ### + ### yyparse + ### + + def yyparse( recv, mid ) + __send__ Racc_YY_Parse_Method, recv, mid, _racc_setup(), true + end + + def _racc_yyparse_rb( recv, mid, arg, c_debug ) + action_table, action_check, action_default, action_pointer, + goto_table, goto_check, goto_default, goto_pointer, + nt_base, reduce_table, token_table, shift_n, + reduce_n, use_result, * = arg + + _racc_init_sysvars + tok = nil + act = nil + i = nil + nerr = 0 + + + catch(:racc_end_parse) { + until i = action_pointer[@racc_state[-1]] + while act = _racc_evalact(action_default[@racc_state[-1]], arg) + end + end + + recv.__send__(mid) do |tok, val| +# $stderr.puts "rd: tok=#{tok}, val=#{val}" + unless tok + @racc_t = 0 + else + @racc_t = (token_table[tok] or 1) # error token + end + @racc_val = val + @racc_read_next = false + + i += @racc_t + if i >= 0 and + act = action_table[i] and + action_check[i] == @racc_state[-1] + ; +# $stderr.puts "01: act=#{act}" + else + act = action_default[@racc_state[-1]] +# $stderr.puts "02: act=#{act}" +# $stderr.puts "curstate=#{@racc_state[-1]}" + end + + while act = _racc_evalact(act, arg) + end + + while not (i = action_pointer[@racc_state[-1]]) or + not @racc_read_next or + @racc_t == 0 # $ + if i and i += @racc_t and + i >= 0 and + act = action_table[i] and + action_check[i] == @racc_state[-1] + ; +# $stderr.puts "03: act=#{act}" + else +# $stderr.puts "04: act=#{act}" + act = action_default[@racc_state[-1]] + end + + while act = _racc_evalact(act, arg) + end + end + end + } + end + + + ### + ### common + ### + + def _racc_evalact( act, arg ) +# $stderr.puts "ea: act=#{act}" + action_table, action_check, action_default, action_pointer, + goto_table, goto_check, goto_default, goto_pointer, + nt_base, reduce_table, token_table, shift_n, + reduce_n, use_result, * = arg +nerr = 0 # tmp + + if act > 0 and act < shift_n + # + # shift + # + + if @racc_error_status > 0 + @racc_error_status -= 1 unless @racc_t == 1 # error token + end + + @racc_vstack.push @racc_val + @racc_state.push act + @racc_read_next = true + + if @yydebug + @racc_tstack.push @racc_t + racc_shift @racc_t, @racc_tstack, @racc_vstack + end + + elsif act < 0 and act > -reduce_n + # + # reduce + # + + code = catch(:racc_jump) { + @racc_state.push _racc_do_reduce(arg, act) + false + } + if code + case code + when 1 # yyerror + @racc_user_yyerror = true # user_yyerror + return -reduce_n + when 2 # yyaccept + return shift_n + else + raise RuntimeError, '[Racc Bug] unknown jump code' + end + end + + elsif act == shift_n + # + # accept + # + + racc_accept if @yydebug + throw :racc_end_parse, @racc_vstack[0] + + elsif act == -reduce_n + # + # error + # + + case @racc_error_status + when 0 + unless arg[21] # user_yyerror + nerr += 1 + on_error @racc_t, @racc_val, @racc_vstack + end + when 3 + if @racc_t == 0 # is $ + throw :racc_end_parse, nil + end + @racc_read_next = true + end + @racc_user_yyerror = false + @racc_error_status = 3 + + while true + if i = action_pointer[@racc_state[-1]] + i += 1 # error token + if i >= 0 and + (act = action_table[i]) and + action_check[i] == @racc_state[-1] + break + end + end + + throw :racc_end_parse, nil if @racc_state.size < 2 + @racc_state.pop + @racc_vstack.pop + if @yydebug + @racc_tstack.pop + racc_e_pop @racc_state, @racc_tstack, @racc_vstack + end + end + + return act + + else + raise RuntimeError, "[Racc Bug] unknown action #{act.inspect}" + end + + racc_next_state(@racc_state[-1], @racc_state) if @yydebug + + nil + end + + def _racc_do_reduce( arg, act ) + action_table, action_check, action_default, action_pointer, + goto_table, goto_check, goto_default, goto_pointer, + nt_base, reduce_table, token_table, shift_n, + reduce_n, use_result, * = arg + state = @racc_state + vstack = @racc_vstack + tstack = @racc_tstack + + i = act * -3 + len = reduce_table[i] + reduce_to = reduce_table[i+1] + method_id = reduce_table[i+2] + void_array = [] + + tmp_t = tstack[-len, len] if @yydebug + tmp_v = vstack[-len, len] + tstack[-len, len] = void_array if @yydebug + vstack[-len, len] = void_array + state[-len, len] = void_array + + # tstack must be updated AFTER method call + if use_result + vstack.push __send__(method_id, tmp_v, vstack, tmp_v[0]) + else + vstack.push __send__(method_id, tmp_v, vstack) + end + tstack.push reduce_to + + racc_reduce(tmp_t, reduce_to, tstack, vstack) if @yydebug + + k1 = reduce_to - nt_base + if i = goto_pointer[k1] + i += state[-1] + if i >= 0 and (curstate = goto_table[i]) and goto_check[i] == k1 + return curstate + end + end + goto_default[k1] + end + + def on_error( t, val, vstack ) + raise ParseError, sprintf("\nparse error on value %s (%s)", + val.inspect, token_to_str(t) || '?') + end + + def yyerror + throw :racc_jump, 1 + end + + def yyaccept + throw :racc_jump, 2 + end + + def yyerrok + @racc_error_status = 0 + end + + + # for debugging output + + def racc_read_token( t, tok, val ) + @racc_debug_out.print 'read ' + @racc_debug_out.print tok.inspect, '(', racc_token2str(t), ') ' + @racc_debug_out.puts val.inspect + @racc_debug_out.puts + end + + def racc_shift( tok, tstack, vstack ) + @racc_debug_out.puts "shift #{racc_token2str tok}" + racc_print_stacks tstack, vstack + @racc_debug_out.puts + end + + def racc_reduce( toks, sim, tstack, vstack ) + out = @racc_debug_out + out.print 'reduce ' + if toks.empty? + out.print ' ' + else + toks.each {|t| out.print ' ', racc_token2str(t) } + end + out.puts " --> #{racc_token2str(sim)}" + + racc_print_stacks tstack, vstack + @racc_debug_out.puts + end + + def racc_accept + @racc_debug_out.puts 'accept' + @racc_debug_out.puts + end + + def racc_e_pop( state, tstack, vstack ) + @racc_debug_out.puts 'error recovering mode: pop token' + racc_print_states state + racc_print_stacks tstack, vstack + @racc_debug_out.puts + end + + def racc_next_state( curstate, state ) + @racc_debug_out.puts "goto #{curstate}" + racc_print_states state + @racc_debug_out.puts + end + + def racc_print_stacks( t, v ) + out = @racc_debug_out + out.print ' [' + t.each_index do |i| + out.print ' (', racc_token2str(t[i]), ' ', v[i].inspect, ')' + end + out.puts ' ]' + end + + def racc_print_states( s ) + out = @racc_debug_out + out.print ' [' + s.each {|st| out.print ' ', st } + out.puts ' ]' + end + + def racc_token2str( tok ) + self.class::Racc_token_to_s_table[tok] or + raise RuntimeError, "[Racc Bug] can't convert token #{tok} to string" + end + + def token_to_str( t ) + self.class::Racc_token_to_s_table[t] + end + + end + +end +..end /home/aamine/lib/ruby/racc/parser.rb modeval..idb76f2e220d +end # end of racc/parser.rb + + +# +# parser.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/scanner' +require 'tmail/utils' + + +module TMail + + class Parser < Racc::Parser + +module_eval <<'..end parser.y modeval..id43721faf1c', 'parser.y', 331 + + include TextUtils + + def self.parse( ident, str, cmt = nil ) + new.parse(ident, str, cmt) + end + + MAILP_DEBUG = false + + def initialize + self.debug = MAILP_DEBUG + end + + def debug=( flag ) + @yydebug = flag && Racc_debug_parser + @scanner_debug = flag + end + + def debug + @yydebug + end + + def parse( ident, str, comments = nil ) + @scanner = Scanner.new(str, ident, comments) + @scanner.debug = @scanner_debug + @first = [ident, ident] + result = yyparse(self, :parse_in) + comments.map! {|c| to_kcode(c) } if comments + result + end + + private + + def parse_in( &block ) + yield @first + @scanner.scan(&block) + end + + def on_error( t, val, vstack ) + raise SyntaxError, "parse error on token #{racc_token2str t}" + end + +..end parser.y modeval..id43721faf1c + +##### racc 1.4.3 generates ### + +racc_reduce_table = [ + 0, 0, :racc_error, + 2, 35, :_reduce_1, + 2, 35, :_reduce_2, + 2, 35, :_reduce_3, + 2, 35, :_reduce_4, + 2, 35, :_reduce_5, + 2, 35, :_reduce_6, + 2, 35, :_reduce_7, + 2, 35, :_reduce_8, + 2, 35, :_reduce_9, + 2, 35, :_reduce_10, + 2, 35, :_reduce_11, + 2, 35, :_reduce_12, + 6, 36, :_reduce_13, + 0, 48, :_reduce_none, + 2, 48, :_reduce_none, + 3, 49, :_reduce_16, + 5, 49, :_reduce_17, + 1, 50, :_reduce_18, + 7, 37, :_reduce_19, + 0, 51, :_reduce_none, + 2, 51, :_reduce_21, + 0, 52, :_reduce_none, + 2, 52, :_reduce_23, + 1, 58, :_reduce_24, + 3, 58, :_reduce_25, + 2, 58, :_reduce_26, + 0, 53, :_reduce_none, + 2, 53, :_reduce_28, + 0, 54, :_reduce_29, + 3, 54, :_reduce_30, + 0, 55, :_reduce_none, + 2, 55, :_reduce_32, + 2, 55, :_reduce_33, + 0, 56, :_reduce_none, + 2, 56, :_reduce_35, + 1, 61, :_reduce_36, + 1, 61, :_reduce_37, + 0, 57, :_reduce_none, + 2, 57, :_reduce_39, + 1, 38, :_reduce_none, + 1, 38, :_reduce_none, + 3, 38, :_reduce_none, + 1, 46, :_reduce_none, + 1, 46, :_reduce_none, + 1, 46, :_reduce_none, + 1, 39, :_reduce_none, + 2, 39, :_reduce_47, + 1, 64, :_reduce_48, + 3, 64, :_reduce_49, + 1, 68, :_reduce_none, + 1, 68, :_reduce_none, + 1, 69, :_reduce_52, + 3, 69, :_reduce_53, + 1, 47, :_reduce_none, + 1, 47, :_reduce_none, + 2, 47, :_reduce_56, + 2, 67, :_reduce_none, + 3, 65, :_reduce_58, + 2, 65, :_reduce_59, + 1, 70, :_reduce_60, + 2, 70, :_reduce_61, + 4, 62, :_reduce_62, + 3, 62, :_reduce_63, + 2, 72, :_reduce_none, + 2, 73, :_reduce_65, + 4, 73, :_reduce_66, + 3, 63, :_reduce_67, + 1, 63, :_reduce_68, + 1, 74, :_reduce_none, + 2, 74, :_reduce_70, + 1, 71, :_reduce_71, + 3, 71, :_reduce_72, + 1, 59, :_reduce_73, + 3, 59, :_reduce_74, + 1, 76, :_reduce_75, + 2, 76, :_reduce_76, + 1, 75, :_reduce_none, + 1, 75, :_reduce_none, + 1, 75, :_reduce_none, + 1, 77, :_reduce_none, + 1, 77, :_reduce_none, + 1, 77, :_reduce_none, + 1, 66, :_reduce_none, + 2, 66, :_reduce_none, + 3, 60, :_reduce_85, + 1, 40, :_reduce_86, + 3, 40, :_reduce_87, + 1, 79, :_reduce_none, + 2, 79, :_reduce_89, + 1, 41, :_reduce_90, + 2, 41, :_reduce_91, + 3, 42, :_reduce_92, + 5, 43, :_reduce_93, + 3, 43, :_reduce_94, + 0, 80, :_reduce_95, + 5, 80, :_reduce_96, + 1, 82, :_reduce_none, + 1, 82, :_reduce_none, + 1, 44, :_reduce_99, + 3, 45, :_reduce_100, + 0, 81, :_reduce_none, + 1, 81, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none ] + +racc_reduce_n = 110 + +racc_shift_n = 168 + +racc_action_table = [ + -70, -69, 23, 25, 146, 147, 29, 31, 105, 106, + 16, 17, 20, 22, 136, 27, -70, -69, 32, 101, + -70, -69, 154, 100, 113, 115, -70, -69, -70, 109, + 75, 23, 25, 101, 155, 29, 31, 142, 143, 16, + 17, 20, 22, 107, 27, 23, 25, 32, 98, 29, + 31, 96, 94, 16, 17, 20, 22, 78, 27, 23, + 25, 32, 112, 29, 31, 74, 91, 16, 17, 20, + 22, 88, 117, 92, 81, 32, 23, 25, 80, 123, + 29, 31, 100, 125, 16, 17, 20, 22, 126, 23, + 25, 109, 32, 29, 31, 91, 128, 16, 17, 20, + 22, 129, 27, 23, 25, 32, 101, 29, 31, 101, + 130, 16, 17, 20, 22, 79, 52, 23, 25, 32, + 78, 29, 31, 133, 78, 16, 17, 20, 22, 77, + 23, 25, 75, 32, 29, 31, 65, 62, 16, 17, + 20, 22, 139, 23, 25, 101, 32, 29, 31, 60, + 100, 16, 17, 20, 22, 44, 27, 101, 148, 32, + 23, 25, 120, 149, 29, 31, 152, 153, 16, 17, + 20, 22, 42, 27, 157, 159, 32, 23, 25, 120, + 40, 29, 31, 15, 164, 16, 17, 20, 22, 40, + 27, 23, 25, 32, 68, 29, 31, 166, 167, 16, + 17, 20, 22, nil, 27, 23, 25, 32, nil, 29, + 31, 74, nil, 16, 17, 20, 22, nil, 23, 25, + nil, 32, 29, 31, nil, nil, 16, 17, 20, 22, + nil, 23, 25, nil, 32, 29, 31, nil, nil, 16, + 17, 20, 22, nil, 23, 25, nil, 32, 29, 31, + nil, nil, 16, 17, 20, 22, nil, 23, 25, nil, + 32, 29, 31, nil, nil, 16, 17, 20, 22, nil, + 27, 23, 25, 32, nil, 29, 31, nil, nil, 16, + 17, 20, 22, nil, 23, 25, nil, 32, 29, 31, + nil, nil, 16, 17, 20, 22, nil, 23, 25, nil, + 32, 29, 31, nil, nil, 16, 17, 20, 22, nil, + 84, 25, nil, 32, 29, 31, nil, 87, 16, 17, + 20, 22, 4, 6, 7, 8, 9, 10, 11, 12, + 13, 1, 2, 3, 84, 25, nil, nil, 29, 31, + nil, 87, 16, 17, 20, 22, 84, 25, nil, nil, + 29, 31, nil, 87, 16, 17, 20, 22, 84, 25, + nil, nil, 29, 31, nil, 87, 16, 17, 20, 22, + 84, 25, nil, nil, 29, 31, nil, 87, 16, 17, + 20, 22, 84, 25, nil, nil, 29, 31, nil, 87, + 16, 17, 20, 22, 84, 25, nil, nil, 29, 31, + nil, 87, 16, 17, 20, 22 ] + +racc_action_check = [ + 75, 28, 68, 68, 136, 136, 68, 68, 72, 72, + 68, 68, 68, 68, 126, 68, 75, 28, 68, 67, + 75, 28, 143, 66, 86, 86, 75, 28, 75, 75, + 28, 3, 3, 86, 143, 3, 3, 134, 134, 3, + 3, 3, 3, 73, 3, 152, 152, 3, 62, 152, + 152, 60, 56, 152, 152, 152, 152, 51, 152, 52, + 52, 152, 80, 52, 52, 52, 50, 52, 52, 52, + 52, 45, 89, 52, 42, 52, 71, 71, 41, 96, + 71, 71, 97, 98, 71, 71, 71, 71, 100, 7, + 7, 101, 71, 7, 7, 102, 104, 7, 7, 7, + 7, 105, 7, 8, 8, 7, 108, 8, 8, 111, + 112, 8, 8, 8, 8, 40, 8, 9, 9, 8, + 36, 9, 9, 117, 121, 9, 9, 9, 9, 33, + 10, 10, 70, 9, 10, 10, 13, 12, 10, 10, + 10, 10, 130, 2, 2, 131, 10, 2, 2, 11, + 135, 2, 2, 2, 2, 6, 2, 138, 139, 2, + 90, 90, 90, 140, 90, 90, 141, 142, 90, 90, + 90, 90, 5, 90, 148, 151, 90, 127, 127, 127, + 4, 127, 127, 1, 157, 127, 127, 127, 127, 159, + 127, 26, 26, 127, 26, 26, 26, 163, 164, 26, + 26, 26, 26, nil, 26, 27, 27, 26, nil, 27, + 27, 27, nil, 27, 27, 27, 27, nil, 155, 155, + nil, 27, 155, 155, nil, nil, 155, 155, 155, 155, + nil, 122, 122, nil, 155, 122, 122, nil, nil, 122, + 122, 122, 122, nil, 76, 76, nil, 122, 76, 76, + nil, nil, 76, 76, 76, 76, nil, 38, 38, nil, + 76, 38, 38, nil, nil, 38, 38, 38, 38, nil, + 38, 55, 55, 38, nil, 55, 55, nil, nil, 55, + 55, 55, 55, nil, 94, 94, nil, 55, 94, 94, + nil, nil, 94, 94, 94, 94, nil, 59, 59, nil, + 94, 59, 59, nil, nil, 59, 59, 59, 59, nil, + 114, 114, nil, 59, 114, 114, nil, 114, 114, 114, + 114, 114, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 77, 77, nil, nil, 77, 77, + nil, 77, 77, 77, 77, 77, 44, 44, nil, nil, + 44, 44, nil, 44, 44, 44, 44, 44, 113, 113, + nil, nil, 113, 113, nil, 113, 113, 113, 113, 113, + 88, 88, nil, nil, 88, 88, nil, 88, 88, 88, + 88, 88, 74, 74, nil, nil, 74, 74, nil, 74, + 74, 74, 74, 74, 129, 129, nil, nil, 129, 129, + nil, 129, 129, 129, 129, 129 ] + +racc_action_pointer = [ + 320, 152, 129, 17, 165, 172, 137, 75, 89, 103, + 116, 135, 106, 105, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, 177, 191, 1, nil, + nil, nil, nil, 109, nil, nil, 94, nil, 243, nil, + 99, 64, 74, nil, 332, 52, nil, nil, nil, nil, + 50, 31, 45, nil, nil, 257, 36, nil, nil, 283, + 22, nil, 16, nil, nil, nil, -3, -10, -12, nil, + 103, 62, -8, 15, 368, 0, 230, 320, nil, nil, + 47, nil, nil, nil, nil, nil, 4, nil, 356, 50, + 146, nil, nil, nil, 270, nil, 65, 56, 52, nil, + 57, 62, 79, nil, 68, 81, nil, nil, 77, nil, + nil, 80, 96, 344, 296, nil, nil, 108, nil, nil, + nil, 98, 217, nil, nil, nil, -19, 163, nil, 380, + 128, 116, nil, nil, 14, 124, -26, nil, 128, 141, + 148, 141, 152, 7, nil, nil, nil, nil, 160, nil, + nil, 149, 31, nil, nil, 204, nil, 167, nil, 174, + nil, nil, nil, 169, 184, nil, nil, nil ] + +racc_action_default = [ + -110, -110, -110, -110, -14, -110, -20, -110, -110, -110, + -110, -110, -110, -110, -10, -95, -106, -107, -77, -44, + -108, -11, -109, -79, -43, -103, -110, -110, -60, -104, + -55, -105, -78, -68, -54, -71, -45, -12, -110, -1, + -110, -110, -110, -2, -110, -22, -51, -48, -50, -3, + -40, -41, -110, -46, -4, -86, -5, -88, -6, -90, + -110, -7, -95, -8, -9, -99, -101, -61, -59, -56, + -69, -110, -110, -110, -110, -75, -110, -110, -57, -15, + -110, 168, -73, -80, -82, -21, -24, -81, -110, -27, + -110, -83, -47, -89, -110, -91, -110, -101, -110, -100, + -102, -75, -58, -52, -110, -110, -64, -63, -65, -76, + -72, -67, -110, -110, -110, -26, -23, -110, -29, -49, + -84, -42, -87, -92, -94, -95, -110, -110, -62, -110, + -110, -25, -74, -28, -31, -101, -110, -53, -66, -110, + -110, -34, -110, -110, -93, -96, -98, -97, -110, -18, + -13, -38, -110, -30, -33, -110, -32, -16, -19, -14, + -35, -36, -37, -110, -110, -39, -85, -17 ] + +racc_goto_table = [ + 39, 67, 70, 73, 24, 37, 69, 66, 36, 38, + 57, 59, 55, 67, 108, 83, 90, 111, 69, 99, + 85, 49, 53, 76, 158, 134, 141, 70, 73, 151, + 118, 89, 45, 156, 160, 150, 140, 21, 14, 19, + 119, 102, 64, 63, 61, 83, 70, 104, 83, 58, + 124, 132, 56, 131, 97, 54, 93, 43, 5, 83, + 95, 145, 76, nil, 116, 76, nil, nil, 127, 138, + 103, nil, nil, nil, 38, nil, nil, 110, nil, nil, + nil, nil, nil, nil, 83, 83, nil, nil, 144, nil, + nil, nil, nil, nil, nil, 57, 121, 122, nil, nil, + 83, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 135, nil, nil, + nil, nil, nil, 93, nil, nil, nil, 70, 162, 137, + 70, 163, 161, 38, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, 165 ] + +racc_goto_check = [ + 2, 37, 37, 29, 13, 13, 28, 46, 31, 36, + 41, 41, 45, 37, 25, 44, 32, 25, 28, 47, + 24, 4, 4, 42, 23, 20, 21, 37, 29, 22, + 19, 18, 17, 26, 27, 16, 15, 12, 11, 33, + 34, 35, 10, 9, 8, 44, 37, 29, 44, 7, + 47, 43, 6, 25, 46, 5, 41, 3, 1, 44, + 41, 48, 42, nil, 24, 42, nil, nil, 32, 25, + 13, nil, nil, nil, 36, nil, nil, 41, nil, nil, + nil, nil, nil, nil, 44, 44, nil, nil, 47, nil, + nil, nil, nil, nil, nil, 41, 31, 45, nil, nil, + 44, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 46, nil, nil, + nil, nil, nil, 41, nil, nil, nil, 37, 29, 13, + 37, 29, 28, 36, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, 2 ] + +racc_goto_pointer = [ + nil, 58, -4, 51, 14, 47, 43, 39, 33, 31, + 29, 37, 35, 2, nil, -94, -105, 26, -14, -59, + -93, -108, -112, -127, -24, -60, -110, -118, -20, -24, + nil, 6, -34, 37, -50, -27, 6, -25, nil, nil, + nil, 1, -5, -63, -29, 3, -8, -47, -75 ] + +racc_goto_default = [ + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, 48, 41, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, 86, nil, nil, 30, 34, + 50, 51, nil, 46, 47, nil, 26, 28, 71, 72, + 33, 35, 114, 82, 18, nil, nil, nil, nil ] + +racc_token_table = { + false => 0, + Object.new => 1, + :DATETIME => 2, + :RECEIVED => 3, + :MADDRESS => 4, + :RETPATH => 5, + :KEYWORDS => 6, + :ENCRYPTED => 7, + :MIMEVERSION => 8, + :CTYPE => 9, + :CENCODING => 10, + :CDISPOSITION => 11, + :ADDRESS => 12, + :MAILBOX => 13, + :DIGIT => 14, + :ATOM => 15, + "," => 16, + ":" => 17, + :FROM => 18, + :BY => 19, + "@" => 20, + :DOMLIT => 21, + :VIA => 22, + :WITH => 23, + :ID => 24, + :FOR => 25, + ";" => 26, + "<" => 27, + ">" => 28, + "." => 29, + :QUOTED => 30, + :TOKEN => 31, + "/" => 32, + "=" => 33 } + +racc_use_result_var = false + +racc_nt_base = 34 + +Racc_arg = [ + racc_action_table, + racc_action_check, + racc_action_default, + racc_action_pointer, + racc_goto_table, + racc_goto_check, + racc_goto_default, + racc_goto_pointer, + racc_nt_base, + racc_reduce_table, + racc_token_table, + racc_shift_n, + racc_reduce_n, + racc_use_result_var ] + +Racc_token_to_s_table = [ +'$end', +'error', +'DATETIME', +'RECEIVED', +'MADDRESS', +'RETPATH', +'KEYWORDS', +'ENCRYPTED', +'MIMEVERSION', +'CTYPE', +'CENCODING', +'CDISPOSITION', +'ADDRESS', +'MAILBOX', +'DIGIT', +'ATOM', +'","', +'":"', +'FROM', +'BY', +'"@"', +'DOMLIT', +'VIA', +'WITH', +'ID', +'FOR', +'";"', +'"<"', +'">"', +'"."', +'QUOTED', +'TOKEN', +'"/"', +'"="', +'$start', +'content', +'datetime', +'received', +'addrs_TOP', +'retpath', +'keys', +'enc', +'version', +'ctype', +'cencode', +'cdisp', +'addr_TOP', +'mbox', +'day', +'hour', +'zone', +'from', +'by', +'via', +'with', +'id', +'for', +'received_datetime', +'received_domain', +'domain', +'msgid', +'received_addrspec', +'routeaddr', +'spec', +'addrs', +'group_bare', +'commas', +'group', +'addr', +'mboxes', +'addr_phrase', +'local_head', +'routes', +'at_domains', +'local', +'word', +'dots', +'domword', +'atom', +'phrase', +'params', +'opt_semicolon', +'value'] + +Racc_debug_parser = false + +##### racc system variables end ##### + + # reduce 0 omitted + +module_eval <<'.,.,', 'parser.y', 16 + def _reduce_1( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 17 + def _reduce_2( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 18 + def _reduce_3( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 19 + def _reduce_4( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 20 + def _reduce_5( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 21 + def _reduce_6( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 22 + def _reduce_7( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 23 + def _reduce_8( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 24 + def _reduce_9( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 25 + def _reduce_10( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 26 + def _reduce_11( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 27 + def _reduce_12( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 33 + def _reduce_13( val, _values) + t = Time.gm(val[3].to_i, val[2], val[1].to_i, 0, 0, 0) + (t + val[4] - val[5]).localtime + end +.,., + + # reduce 14 omitted + + # reduce 15 omitted + +module_eval <<'.,.,', 'parser.y', 42 + def _reduce_16( val, _values) + (val[0].to_i * 60 * 60) + + (val[2].to_i * 60) + end +.,., + +module_eval <<'.,.,', 'parser.y', 47 + def _reduce_17( val, _values) + (val[0].to_i * 60 * 60) + + (val[2].to_i * 60) + + (val[4].to_i) + end +.,., + +module_eval <<'.,.,', 'parser.y', 54 + def _reduce_18( val, _values) + timezone_string_to_unixtime(val[0]) + end +.,., + +module_eval <<'.,.,', 'parser.y', 59 + def _reduce_19( val, _values) + val + end +.,., + + # reduce 20 omitted + +module_eval <<'.,.,', 'parser.y', 65 + def _reduce_21( val, _values) + val[1] + end +.,., + + # reduce 22 omitted + +module_eval <<'.,.,', 'parser.y', 71 + def _reduce_23( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 77 + def _reduce_24( val, _values) + join_domain(val[0]) + end +.,., + +module_eval <<'.,.,', 'parser.y', 81 + def _reduce_25( val, _values) + join_domain(val[2]) + end +.,., + +module_eval <<'.,.,', 'parser.y', 85 + def _reduce_26( val, _values) + join_domain(val[0]) + end +.,., + + # reduce 27 omitted + +module_eval <<'.,.,', 'parser.y', 91 + def _reduce_28( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 96 + def _reduce_29( val, _values) + [] + end +.,., + +module_eval <<'.,.,', 'parser.y', 100 + def _reduce_30( val, _values) + val[0].push val[2] + val[0] + end +.,., + + # reduce 31 omitted + +module_eval <<'.,.,', 'parser.y', 107 + def _reduce_32( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 111 + def _reduce_33( val, _values) + val[1] + end +.,., + + # reduce 34 omitted + +module_eval <<'.,.,', 'parser.y', 117 + def _reduce_35( val, _values) + val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 123 + def _reduce_36( val, _values) + val[0].spec + end +.,., + +module_eval <<'.,.,', 'parser.y', 127 + def _reduce_37( val, _values) + val[0].spec + end +.,., + + # reduce 38 omitted + +module_eval <<'.,.,', 'parser.y', 134 + def _reduce_39( val, _values) + val[1] + end +.,., + + # reduce 40 omitted + + # reduce 41 omitted + + # reduce 42 omitted + + # reduce 43 omitted + + # reduce 44 omitted + + # reduce 45 omitted + + # reduce 46 omitted + +module_eval <<'.,.,', 'parser.y', 146 + def _reduce_47( val, _values) + [ Address.new(nil, nil) ] + end +.,., + +module_eval <<'.,.,', 'parser.y', 148 + def _reduce_48( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 149 + def _reduce_49( val, _values) + val[0].push val[2]; val[0] + end +.,., + + # reduce 50 omitted + + # reduce 51 omitted + +module_eval <<'.,.,', 'parser.y', 156 + def _reduce_52( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 160 + def _reduce_53( val, _values) + val[0].push val[2] + val[0] + end +.,., + + # reduce 54 omitted + + # reduce 55 omitted + +module_eval <<'.,.,', 'parser.y', 168 + def _reduce_56( val, _values) + val[1].phrase = Decoder.decode(val[0]) + val[1] + end +.,., + + # reduce 57 omitted + +module_eval <<'.,.,', 'parser.y', 176 + def _reduce_58( val, _values) + AddressGroup.new(val[0], val[2]) + end +.,., + +module_eval <<'.,.,', 'parser.y', 178 + def _reduce_59( val, _values) + AddressGroup.new(val[0], []) + end +.,., + +module_eval <<'.,.,', 'parser.y', 181 + def _reduce_60( val, _values) + val[0].join('.') + end +.,., + +module_eval <<'.,.,', 'parser.y', 182 + def _reduce_61( val, _values) + val[0] << ' ' << val[1].join('.') + end +.,., + +module_eval <<'.,.,', 'parser.y', 186 + def _reduce_62( val, _values) + val[2].routes.replace val[1] + val[2] + end +.,., + +module_eval <<'.,.,', 'parser.y', 191 + def _reduce_63( val, _values) + val[1] + end +.,., + + # reduce 64 omitted + +module_eval <<'.,.,', 'parser.y', 196 + def _reduce_65( val, _values) + [ val[1].join('.') ] + end +.,., + +module_eval <<'.,.,', 'parser.y', 197 + def _reduce_66( val, _values) + val[0].push val[3].join('.'); val[0] + end +.,., + +module_eval <<'.,.,', 'parser.y', 199 + def _reduce_67( val, _values) + Address.new( val[0], val[2] ) + end +.,., + +module_eval <<'.,.,', 'parser.y', 200 + def _reduce_68( val, _values) + Address.new( val[0], nil ) + end +.,., + + # reduce 69 omitted + +module_eval <<'.,.,', 'parser.y', 203 + def _reduce_70( val, _values) + val[0].push ''; val[0] + end +.,., + +module_eval <<'.,.,', 'parser.y', 206 + def _reduce_71( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 209 + def _reduce_72( val, _values) + val[1].times do + val[0].push '' + end + val[0].push val[2] + val[0] + end +.,., + +module_eval <<'.,.,', 'parser.y', 217 + def _reduce_73( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 220 + def _reduce_74( val, _values) + val[1].times do + val[0].push '' + end + val[0].push val[2] + val[0] + end +.,., + +module_eval <<'.,.,', 'parser.y', 227 + def _reduce_75( val, _values) + 0 + end +.,., + +module_eval <<'.,.,', 'parser.y', 228 + def _reduce_76( val, _values) + 1 + end +.,., + + # reduce 77 omitted + + # reduce 78 omitted + + # reduce 79 omitted + + # reduce 80 omitted + + # reduce 81 omitted + + # reduce 82 omitted + + # reduce 83 omitted + + # reduce 84 omitted + +module_eval <<'.,.,', 'parser.y', 243 + def _reduce_85( val, _values) + val[1] = val[1].spec + val.join('') + end +.,., + +module_eval <<'.,.,', 'parser.y', 247 + def _reduce_86( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 248 + def _reduce_87( val, _values) + val[0].push val[2]; val[0] + end +.,., + + # reduce 88 omitted + +module_eval <<'.,.,', 'parser.y', 251 + def _reduce_89( val, _values) + val[0] << ' ' << val[1] + end +.,., + +module_eval <<'.,.,', 'parser.y', 255 + def _reduce_90( val, _values) + val.push nil + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 260 + def _reduce_91( val, _values) + val + end +.,., + +module_eval <<'.,.,', 'parser.y', 265 + def _reduce_92( val, _values) + [ val[0].to_i, val[2].to_i ] + end +.,., + +module_eval <<'.,.,', 'parser.y', 270 + def _reduce_93( val, _values) + [ val[0].downcase, val[2].downcase, decode_params(val[3]) ] + end +.,., + +module_eval <<'.,.,', 'parser.y', 274 + def _reduce_94( val, _values) + [ val[0].downcase, nil, decode_params(val[1]) ] + end +.,., + +module_eval <<'.,.,', 'parser.y', 279 + def _reduce_95( val, _values) + {} + end +.,., + +module_eval <<'.,.,', 'parser.y', 283 + def _reduce_96( val, _values) + val[0][ val[2].downcase ] = val[4] + val[0] + end +.,., + + # reduce 97 omitted + + # reduce 98 omitted + +module_eval <<'.,.,', 'parser.y', 292 + def _reduce_99( val, _values) + val[0].downcase + end +.,., + +module_eval <<'.,.,', 'parser.y', 297 + def _reduce_100( val, _values) + [ val[0].downcase, decode_params(val[1]) ] + end +.,., + + # reduce 101 omitted + + # reduce 102 omitted + + # reduce 103 omitted + + # reduce 104 omitted + + # reduce 105 omitted + + # reduce 106 omitted + + # reduce 107 omitted + + # reduce 108 omitted + + # reduce 109 omitted + + def _reduce_none( val, _values) + val[0] + end + + end # class Parser + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/port.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/port.rb new file mode 100755 index 00000000..f973c05b --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/port.rb @@ -0,0 +1,377 @@ +# +# port.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/stringio' + + +module TMail + + class Port + def reproducible? + false + end + end + + + ### + ### FilePort + ### + + class FilePort < Port + + def initialize( fname ) + @filename = File.expand_path(fname) + super() + end + + attr_reader :filename + + alias ident filename + + def ==( other ) + other.respond_to?(:filename) and @filename == other.filename + end + + alias eql? == + + def hash + @filename.hash + end + + def inspect + "#<#{self.class}:#{@filename}>" + end + + def reproducible? + true + end + + def size + File.size @filename + end + + + def ropen( &block ) + File.open(@filename, &block) + end + + def wopen( &block ) + File.open(@filename, 'w', &block) + end + + def aopen( &block ) + File.open(@filename, 'a', &block) + end + + + def read_all + ropen {|f| + return f.read + } + end + + + def remove + File.unlink @filename + end + + def move_to( port ) + begin + File.link @filename, port.filename + rescue Errno::EXDEV + copy_to port + end + File.unlink @filename + end + + alias mv move_to + + def copy_to( port ) + if FilePort === port + copy_file @filename, port.filename + else + File.open(@filename) {|r| + port.wopen {|w| + while s = r.sysread(4096) + w.write << s + end + } } + end + end + + alias cp copy_to + + private + + # from fileutils.rb + def copy_file( src, dest ) + st = r = w = nil + + File.open(src, 'rb') {|r| + File.open(dest, 'wb') {|w| + st = r.stat + begin + while true + w.write r.sysread(st.blksize) + end + rescue EOFError + end + } } + end + + end + + + module MailFlags + + def seen=( b ) + set_status 'S', b + end + + def seen? + get_status 'S' + end + + def replied=( b ) + set_status 'R', b + end + + def replied? + get_status 'R' + end + + def flagged=( b ) + set_status 'F', b + end + + def flagged? + get_status 'F' + end + + private + + def procinfostr( str, tag, true_p ) + a = str.upcase.split(//) + a.push true_p ? tag : nil + a.delete tag unless true_p + a.compact.sort.join('').squeeze + end + + end + + + class MhPort < FilePort + + include MailFlags + + private + + def set_status( tag, flag ) + begin + tmpfile = @filename + '.tmailtmp.' + $$.to_s + File.open(tmpfile, 'w') {|f| + write_status f, tag, flag + } + File.unlink @filename + File.link tmpfile, @filename + ensure + File.unlink tmpfile + end + end + + def write_status( f, tag, flag ) + stat = '' + File.open(@filename) {|r| + while line = r.gets + if line.strip.empty? + break + elsif m = /\AX-TMail-Status:/i.match(line) + stat = m.post_match.strip + else + f.print line + end + end + + s = procinfostr(stat, tag, flag) + f.puts 'X-TMail-Status: ' + s unless s.empty? + f.puts + + while s = r.read(2048) + f.write s + end + } + end + + def get_status( tag ) + File.foreach(@filename) {|line| + return false if line.strip.empty? + if m = /\AX-TMail-Status:/i.match(line) + return m.post_match.strip.include?(tag[0]) + end + } + false + end + + end + + + class MaildirPort < FilePort + + def move_to_new + new = replace_dir(@filename, 'new') + File.rename @filename, new + @filename = new + end + + def move_to_cur + new = replace_dir(@filename, 'cur') + File.rename @filename, new + @filename = new + end + + def replace_dir( path, dir ) + "#{File.dirname File.dirname(path)}/#{dir}/#{File.basename path}" + end + private :replace_dir + + + include MailFlags + + private + + MAIL_FILE = /\A(\d+\.[\d_]+\.[^:]+)(?:\:(\d),(\w+)?)?\z/ + + def set_status( tag, flag ) + if m = MAIL_FILE.match(File.basename(@filename)) + s, uniq, type, info, = m.to_a + return if type and type != '2' # do not change anything + newname = File.dirname(@filename) + '/' + + uniq + ':2,' + procinfostr(info.to_s, tag, flag) + else + newname = @filename + ':2,' + tag + end + + File.link @filename, newname + File.unlink @filename + @filename = newname + end + + def get_status( tag ) + m = MAIL_FILE.match(File.basename(@filename)) or return false + m[2] == '2' and m[3].to_s.include?(tag[0]) + end + + end + + + ### + ### StringPort + ### + + class StringPort < Port + + def initialize( str = '' ) + @buffer = str + super() + end + + def string + @buffer + end + + def to_s + @buffer.dup + end + + alias read_all to_s + + def size + @buffer.size + end + + def ==( other ) + StringPort === other and @buffer.equal? other.string + end + + alias eql? == + + def hash + @buffer.object_id.hash + end + + def inspect + "#<#{self.class}:id=#{sprintf '0x%x', @buffer.object_id}>" + end + + def reproducible? + true + end + + def ropen( &block ) + @buffer or raise Errno::ENOENT, "#{inspect} is already removed" + StringInput.open(@buffer, &block) + end + + def wopen( &block ) + @buffer = '' + StringOutput.new(@buffer, &block) + end + + def aopen( &block ) + @buffer ||= '' + StringOutput.new(@buffer, &block) + end + + def remove + @buffer = nil + end + + alias rm remove + + def copy_to( port ) + port.wopen {|f| + f.write @buffer + } + end + + alias cp copy_to + + def move_to( port ) + if StringPort === port + str = @buffer + port.instance_eval { @buffer = str } + else + copy_to port + end + remove + end + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/quoting.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/quoting.rb new file mode 100644 index 00000000..a56e2267 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/quoting.rb @@ -0,0 +1,125 @@ +module TMail + class Mail + def subject(to_charset = 'utf-8') + Unquoter.unquote_and_convert_to(quoted_subject, to_charset) + end + + def unquoted_body(to_charset = 'utf-8') + from_charset = sub_header("content-type", "charset") + case (content_transfer_encoding || "7bit").downcase + when "quoted-printable" + Unquoter.unquote_quoted_printable_and_convert_to(quoted_body, + to_charset, from_charset, true) + when "base64" + Unquoter.unquote_base64_and_convert_to(quoted_body, to_charset, + from_charset) + when "7bit", "8bit" + Unquoter.convert_to(quoted_body, to_charset, from_charset) + when "binary" + quoted_body + else + quoted_body + end + end + + def body(to_charset = 'utf-8', &block) + attachment_presenter = block || Proc.new { |file_name| "Attachment: #{file_name}\n" } + + if multipart? + parts.collect { |part| + header = part["content-type"] + + if part.multipart? + part.body(to_charset, &attachment_presenter) + elsif header.nil? + "" + elsif !attachment?(part) + part.unquoted_body(to_charset) + else + attachment_presenter.call(header["name"] || "(unnamed)") + end + }.join + else + unquoted_body(to_charset) + end + end + end + + class Unquoter + class << self + def unquote_and_convert_to(text, to_charset, from_charset = "iso-8859-1", preserve_underscores=false) + return "" if text.nil? + if text =~ /^=\?(.*?)\?(.)\?(.*)\?=$/ + from_charset = $1 + quoting_method = $2 + text = $3 + case quoting_method.upcase + when "Q" then + unquote_quoted_printable_and_convert_to(text, to_charset, from_charset, preserve_underscores) + when "B" then + unquote_base64_and_convert_to(text, to_charset, from_charset) + else + raise "unknown quoting method #{quoting_method.inspect}" + end + else + convert_to(text, to_charset, from_charset) + end + end + + def unquote_quoted_printable_and_convert_to(text, to, from, preserve_underscores=false) + text = text.gsub(/_/, " ") unless preserve_underscores + convert_to(text.unpack("M*").first, to, from) + end + + def unquote_base64_and_convert_to(text, to, from) + convert_to(Base64.decode(text).first, to, from) + end + + begin + require 'iconv' + def convert_to(text, to, from) + return text unless to && from + text ? Iconv.iconv(to, from, text).first : "" + rescue Iconv::IllegalSequence, Errno::EINVAL + # the 'from' parameter specifies a charset other than what the text + # actually is...not much we can do in this case but just return the + # unconverted text. + # + # Ditto if either parameter represents an unknown charset, like + # X-UNKNOWN. + text + end + rescue LoadError + # Not providing quoting support + def convert_to(text, to, from) + warn "Action Mailer: iconv not loaded; ignoring conversion from #{from} to #{to} (#{__FILE__}:#{__LINE__})" + text + end + end + end + end +end + +if __FILE__ == $0 + require 'test/unit' + + class TC_Unquoter < Test::Unit::TestCase + def test_unquote_quoted_printable + a ="=?ISO-8859-1?Q?[166417]_Bekr=E6ftelse_fra_Rejsefeber?=" + b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8') + assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b + end + + def test_unquote_base64 + a ="=?ISO-8859-1?B?WzE2NjQxN10gQmVrcuZmdGVsc2UgZnJhIFJlanNlZmViZXI=?=" + b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8') + assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b + end + + def test_unquote_without_charset + a ="[166417]_Bekr=E6ftelse_fra_Rejsefeber" + b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8') + assert_equal "[166417]_Bekr=E6ftelse_fra_Rejsefeber", b + end + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner.rb new file mode 100755 index 00000000..839dd793 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner.rb @@ -0,0 +1,41 @@ +# +# scanner.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/utils' + +module TMail + require 'tmail/scanner_r.rb' + begin + raise LoadError, 'Turn off Ruby extention by user choice' if ENV['NORUBYEXT'] + require 'tmail/scanner_c.so' + Scanner = Scanner_C + rescue LoadError + Scanner = Scanner_R + end +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb new file mode 100755 index 00000000..ccf576c2 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/scanner_r.rb @@ -0,0 +1,263 @@ +# +# scanner_r.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +require 'tmail/config' + + +module TMail + + class Scanner_R + + Version = '0.10.7' + Version.freeze + + MIME_HEADERS = { + :CTYPE => true, + :CENCODING => true, + :CDISPOSITION => true + } + + alnum = 'a-zA-Z0-9' + atomsyms = %q[ _#!$%&`'*+-{|}~^@/=? ].strip + tokensyms = %q[ _#!$%&`'*+-{|}~^@. ].strip + + atomchars = alnum + Regexp.quote(atomsyms) + tokenchars = alnum + Regexp.quote(tokensyms) + iso2022str = '\e(?!\(B)..(?:[^\e]+|\e(?!\(B)..)*\e\(B' + + eucstr = '(?:[\xa1-\xfe][\xa1-\xfe])+' + sjisstr = '(?:[\x81-\x9f\xe0-\xef][\x40-\x7e\x80-\xfc])+' + utf8str = '(?:[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf])+' + + quoted_with_iso2022 = /\A(?:[^\\\e"]+|#{iso2022str})+/n + domlit_with_iso2022 = /\A(?:[^\\\e\]]+|#{iso2022str})+/n + comment_with_iso2022 = /\A(?:[^\\\e()]+|#{iso2022str})+/n + + quoted_without_iso2022 = /\A[^\\"]+/n + domlit_without_iso2022 = /\A[^\\\]]+/n + comment_without_iso2022 = /\A[^\\()]+/n + + PATTERN_TABLE = {} + PATTERN_TABLE['EUC'] = + [ + /\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+/n, + /\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+/n, + quoted_with_iso2022, + domlit_with_iso2022, + comment_with_iso2022 + ] + PATTERN_TABLE['SJIS'] = + [ + /\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+/n, + /\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+/n, + quoted_with_iso2022, + domlit_with_iso2022, + comment_with_iso2022 + ] + PATTERN_TABLE['UTF8'] = + [ + /\A(?:[#{atomchars}]+|#{utf8str})+/n, + /\A(?:[#{tokenchars}]+|#{utf8str})+/n, + quoted_without_iso2022, + domlit_without_iso2022, + comment_without_iso2022 + ] + PATTERN_TABLE['NONE'] = + [ + /\A[#{atomchars}]+/n, + /\A[#{tokenchars}]+/n, + quoted_without_iso2022, + domlit_without_iso2022, + comment_without_iso2022 + ] + + + def initialize( str, scantype, comments ) + init_scanner str + @comments = comments || [] + @debug = false + + # fix scanner mode + @received = (scantype == :RECEIVED) + @is_mime_header = MIME_HEADERS[scantype] + + atom, token, @quoted_re, @domlit_re, @comment_re = PATTERN_TABLE[$KCODE] + @word_re = (MIME_HEADERS[scantype] ? token : atom) + end + + attr_accessor :debug + + def scan( &block ) + if @debug + scan_main do |arr| + s, v = arr + printf "%7d %-10s %s\n", + rest_size(), + s.respond_to?(:id2name) ? s.id2name : s.inspect, + v.inspect + yield arr + end + else + scan_main(&block) + end + end + + private + + RECV_TOKEN = { + 'from' => :FROM, + 'by' => :BY, + 'via' => :VIA, + 'with' => :WITH, + 'id' => :ID, + 'for' => :FOR + } + + def scan_main + until eof? + if skip(/\A[\n\r\t ]+/n) # LWSP + break if eof? + end + + if s = readstr(@word_re) + if @is_mime_header + yield :TOKEN, s + else + # atom + if /\A\d+\z/ === s + yield :DIGIT, s + elsif @received + yield RECV_TOKEN[s.downcase] || :ATOM, s + else + yield :ATOM, s + end + end + + elsif skip(/\A"/) + yield :QUOTED, scan_quoted_word() + + elsif skip(/\A\[/) + yield :DOMLIT, scan_domain_literal() + + elsif skip(/\A\(/) + @comments.push scan_comment() + + else + c = readchar() + yield c, c + end + end + + yield false, '$' + end + + def scan_quoted_word + scan_qstr(@quoted_re, /\A"/, 'quoted-word') + end + + def scan_domain_literal + '[' + scan_qstr(@domlit_re, /\A\]/, 'domain-literal') + ']' + end + + def scan_qstr( pattern, terminal, type ) + result = '' + until eof? + if s = readstr(pattern) then result << s + elsif skip(terminal) then return result + elsif skip(/\A\\/) then result << readchar() + else + raise "TMail FATAL: not match in #{type}" + end + end + scan_error! "found unterminated #{type}" + end + + def scan_comment + result = '' + nest = 1 + content = @comment_re + + until eof? + if s = readstr(content) then result << s + elsif skip(/\A\)/) then nest -= 1 + return result if nest == 0 + result << ')' + elsif skip(/\A\(/) then nest += 1 + result << '(' + elsif skip(/\A\\/) then result << readchar() + else + raise 'TMail FATAL: not match in comment' + end + end + scan_error! 'found unterminated comment' + end + + # string scanner + + def init_scanner( str ) + @src = str + end + + def eof? + @src.empty? + end + + def rest_size + @src.size + end + + def readstr( re ) + if m = re.match(@src) + @src = m.post_match + m[0] + else + nil + end + end + + def readchar + readstr(/\A./) + end + + def skip( re ) + if m = re.match(@src) + @src = m.post_match + true + else + false + end + end + + def scan_error!( msg ) + raise SyntaxError, msg + end + + end + +end # module TMail diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/stringio.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/stringio.rb new file mode 100755 index 00000000..532be3db --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/stringio.rb @@ -0,0 +1,277 @@ +# +# stringio.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +class StringInput#:nodoc: + + include Enumerable + + class << self + + def new( str ) + if block_given? + begin + f = super + yield f + ensure + f.close if f + end + else + super + end + end + + alias open new + + end + + def initialize( str ) + @src = str + @pos = 0 + @closed = false + @lineno = 0 + end + + attr_reader :lineno + + def string + @src + end + + def inspect + "#<#{self.class}:#{@closed ? 'closed' : 'open'},src=#{@src[0,30].inspect}>" + end + + def close + stream_check! + @pos = nil + @closed = true + end + + def closed? + @closed + end + + def pos + stream_check! + [@pos, @src.size].min + end + + alias tell pos + + def seek( offset, whence = IO::SEEK_SET ) + stream_check! + case whence + when IO::SEEK_SET + @pos = offset + when IO::SEEK_CUR + @pos += offset + when IO::SEEK_END + @pos = @src.size - offset + else + raise ArgumentError, "unknown seek flag: #{whence}" + end + @pos = 0 if @pos < 0 + @pos = [@pos, @src.size + 1].min + offset + end + + def rewind + stream_check! + @pos = 0 + end + + def eof? + stream_check! + @pos > @src.size + end + + def each( &block ) + stream_check! + begin + @src.each(&block) + ensure + @pos = 0 + end + end + + def gets + stream_check! + if idx = @src.index(?\n, @pos) + idx += 1 # "\n".size + line = @src[ @pos ... idx ] + @pos = idx + @pos += 1 if @pos == @src.size + else + line = @src[ @pos .. -1 ] + @pos = @src.size + 1 + end + @lineno += 1 + + line + end + + def getc + stream_check! + ch = @src[@pos] + @pos += 1 + @pos += 1 if @pos == @src.size + ch + end + + def read( len = nil ) + stream_check! + return read_all unless len + str = @src[@pos, len] + @pos += len + @pos += 1 if @pos == @src.size + str + end + + alias sysread read + + def read_all + stream_check! + return nil if eof? + rest = @src[@pos ... @src.size] + @pos = @src.size + 1 + rest + end + + def stream_check! + @closed and raise IOError, 'closed stream' + end + +end + + +class StringOutput#:nodoc: + + class << self + + def new( str = '' ) + if block_given? + begin + f = super + yield f + ensure + f.close if f + end + else + super + end + end + + alias open new + + end + + def initialize( str = '' ) + @dest = str + @closed = false + end + + def close + @closed = true + end + + def closed? + @closed + end + + def string + @dest + end + + alias value string + alias to_str string + + def size + @dest.size + end + + alias pos size + + def inspect + "#<#{self.class}:#{@dest ? 'open' : 'closed'},#{id}>" + end + + def print( *args ) + stream_check! + raise ArgumentError, 'wrong # of argument (0 for >1)' if args.empty? + args.each do |s| + raise ArgumentError, 'nil not allowed' if s.nil? + @dest << s.to_s + end + nil + end + + def puts( *args ) + stream_check! + args.each do |str| + @dest << (s = str.to_s) + @dest << "\n" unless s[-1] == ?\n + end + @dest << "\n" if args.empty? + nil + end + + def putc( ch ) + stream_check! + @dest << ch.chr + nil + end + + def printf( *args ) + stream_check! + @dest << sprintf(*args) + nil + end + + def write( str ) + stream_check! + s = str.to_s + @dest << s + s.size + end + + alias syswrite write + + def <<( str ) + stream_check! + @dest << str.to_s + self + end + + private + + def stream_check! + @closed and raise IOError, 'closed stream' + end + +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/tmail.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/tmail.rb new file mode 100755 index 00000000..57ed3cc5 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/tmail.rb @@ -0,0 +1 @@ +require 'tmail' diff --git a/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/utils.rb b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/utils.rb new file mode 100755 index 00000000..852acd75 --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/vendor/tmail/utils.rb @@ -0,0 +1,238 @@ +# +# utils.rb +# +#-- +# Copyright (c) 1998-2003 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. +#++ + +module TMail + + class SyntaxError < StandardError; end + + + def TMail.new_boundary + 'mimepart_' + random_tag + end + + def TMail.new_message_id( fqdn = nil ) + fqdn ||= ::Socket.gethostname + "<#{random_tag()}@#{fqdn}.tmail>" + end + + def TMail.random_tag + @uniq += 1 + t = Time.now + sprintf('%x%x_%x%x%d%x', + t.to_i, t.tv_usec, + $$, Thread.current.object_id, @uniq, rand(255)) + end + private_class_method :random_tag + + @uniq = 0 + + + module TextUtils + + aspecial = '()<>[]:;.\\,"' + tspecial = '()<>[];:\\,"/?=' + lwsp = " \t\r\n" + control = '\x00-\x1f\x7f-\xff' + + ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{lwsp}]/n + PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n + TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{lwsp}]/n + CONTROL_CHAR = /[#{control}]/n + + def atom_safe?( str ) + not ATOM_UNSAFE === str + end + + def quote_atom( str ) + (ATOM_UNSAFE === str) ? dquote(str) : str + end + + def quote_phrase( str ) + (PHRASE_UNSAFE === str) ? dquote(str) : str + end + + def token_safe?( str ) + not TOKEN_UNSAFE === str + end + + def quote_token( str ) + (TOKEN_UNSAFE === str) ? dquote(str) : str + end + + def dquote( str ) + '"' + str.gsub(/["\\]/n) {|s| '\\' + s } + '"' + end + private :dquote + + + def join_domain( arr ) + arr.map {|i| + if /\A\[.*\]\z/ === i + i + else + quote_atom(i) + end + }.join('.') + end + + + ZONESTR_TABLE = { + 'jst' => 9 * 60, + 'eet' => 2 * 60, + 'bst' => 1 * 60, + 'met' => 1 * 60, + 'gmt' => 0, + 'utc' => 0, + 'ut' => 0, + 'nst' => -(3 * 60 + 30), + 'ast' => -4 * 60, + 'edt' => -4 * 60, + 'est' => -5 * 60, + 'cdt' => -5 * 60, + 'cst' => -6 * 60, + 'mdt' => -6 * 60, + 'mst' => -7 * 60, + 'pdt' => -7 * 60, + 'pst' => -8 * 60, + 'a' => -1 * 60, + 'b' => -2 * 60, + 'c' => -3 * 60, + 'd' => -4 * 60, + 'e' => -5 * 60, + 'f' => -6 * 60, + 'g' => -7 * 60, + 'h' => -8 * 60, + 'i' => -9 * 60, + # j not use + 'k' => -10 * 60, + 'l' => -11 * 60, + 'm' => -12 * 60, + 'n' => 1 * 60, + 'o' => 2 * 60, + 'p' => 3 * 60, + 'q' => 4 * 60, + 'r' => 5 * 60, + 's' => 6 * 60, + 't' => 7 * 60, + 'u' => 8 * 60, + 'v' => 9 * 60, + 'w' => 10 * 60, + 'x' => 11 * 60, + 'y' => 12 * 60, + 'z' => 0 * 60 + } + + def timezone_string_to_unixtime( str ) + if m = /([\+\-])(\d\d?)(\d\d)/.match(str) + sec = (m[2].to_i * 60 + m[3].to_i) * 60 + m[1] == '-' ? -sec : sec + else + min = ZONESTR_TABLE[str.downcase] or + raise SyntaxError, "wrong timezone format '#{str}'" + min * 60 + end + end + + + WDAY = %w( Sun Mon Tue Wed Thu Fri Sat TMailBUG ) + MONTH = %w( TMailBUG Jan Feb Mar Apr May Jun + Jul Aug Sep Oct Nov Dec TMailBUG ) + + def time2str( tm ) + # [ruby-list:7928] + gmt = Time.at(tm.to_i) + gmt.gmtime + offset = tm.to_i - Time.local(*gmt.to_a[0,6].reverse).to_i + + # DO NOT USE strftime: setlocale() breaks it + sprintf '%s, %s %s %d %02d:%02d:%02d %+.2d%.2d', + WDAY[tm.wday], tm.mday, MONTH[tm.month], + tm.year, tm.hour, tm.min, tm.sec, + *(offset / 60).divmod(60) + end + + + MESSAGE_ID = /<[^\@>]+\@[^>\@]+>/ + + def message_id?( str ) + MESSAGE_ID === str + end + + + MIME_ENCODED = /=\?[^\s?=]+\?[QB]\?[^\s?=]+\?=/i + + def mime_encoded?( str ) + MIME_ENCODED === str + end + + + def decode_params( hash ) + new = Hash.new + encoded = nil + hash.each do |key, value| + if m = /\*(?:(\d+)\*)?\z/.match(key) + ((encoded ||= {})[m.pre_match] ||= [])[(m[1] || 0).to_i] = value + else + new[key] = to_kcode(value) + end + end + if encoded + encoded.each do |key, strings| + new[key] = decode_RFC2231(strings.join('')) + end + end + + new + end + + NKF_FLAGS = { + 'EUC' => '-e -m', + 'SJIS' => '-s -m' + } + + def to_kcode( str ) + flag = NKF_FLAGS[$KCODE] or return str + NKF.nkf(flag, str) + end + + RFC2231_ENCODED = /\A(?:iso-2022-jp|euc-jp|shift_jis|us-ascii)?'[a-z]*'/in + + def decode_RFC2231( str ) + m = RFC2231_ENCODED.match(str) or return str + begin + NKF.nkf(NKF_FLAGS[$KCODE], + m.post_match.gsub(/%[\da-f]{2}/in) {|s| s[1,2].hex.chr }) + rescue + m.post_match.gsub(/%[\da-f]{2}/in, "") + end + end + + end + +end diff --git a/vendor/rails/actionmailer/lib/action_mailer/version.rb b/vendor/rails/actionmailer/lib/action_mailer/version.rb new file mode 100644 index 00000000..21bc3f6a --- /dev/null +++ b/vendor/rails/actionmailer/lib/action_mailer/version.rb @@ -0,0 +1,9 @@ +module ActionMailer + module VERSION #:nodoc: + MAJOR = 1 + MINOR = 2 + TINY = 5 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper.rhtml b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper.rhtml new file mode 100644 index 00000000..378777f8 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper.rhtml @@ -0,0 +1 @@ +Hello, <%= person_name %>. Thanks for registering! diff --git a/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper_method.rhtml b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper_method.rhtml new file mode 100644 index 00000000..d5b8b285 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_helper_method.rhtml @@ -0,0 +1 @@ +This message brought to you by <%= name_of_the_mailer_class %>. diff --git a/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_mail_helper.rhtml b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_mail_helper.rhtml new file mode 100644 index 00000000..96ec49d1 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_mail_helper.rhtml @@ -0,0 +1,5 @@ +From "Romeo and Juliet": + +<%= block_format @text %> + +Good ol' Shakespeare. diff --git a/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_test_helper.rhtml b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_test_helper.rhtml new file mode 100644 index 00000000..52ea9aa4 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/helper_mailer/use_test_helper.rhtml @@ -0,0 +1 @@ +So, <%= test_format(@text) %> diff --git a/vendor/rails/actionmailer/test/fixtures/helpers/test_helper.rb b/vendor/rails/actionmailer/test/fixtures/helpers/test_helper.rb new file mode 100644 index 00000000..f479820c --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/helpers/test_helper.rb @@ -0,0 +1,5 @@ +module TestHelper + def test_format(text) + "#{text}" + end +end diff --git a/vendor/rails/actionmailer/test/fixtures/path.with.dots/multipart_with_template_path_with_dots.rhtml b/vendor/rails/actionmailer/test/fixtures/path.with.dots/multipart_with_template_path_with_dots.rhtml new file mode 100644 index 00000000..897a5065 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/path.with.dots/multipart_with_template_path_with_dots.rhtml @@ -0,0 +1 @@ +Have a lovely picture, from me. Enjoy! \ No newline at end of file diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email b/vendor/rails/actionmailer/test/fixtures/raw_email new file mode 100644 index 00000000..43f7a59c --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email @@ -0,0 +1,14 @@ +From jamis_buck@byu.edu Mon May 2 16:07:05 2005 +Mime-Version: 1.0 (Apple Message framework v622) +Content-Transfer-Encoding: base64 +Message-Id: +Content-Type: text/plain; + charset=EUC-KR; + format=flowed +To: willard15georgina@jamis.backpackit.com +From: Jamis Buck +Subject: =?EUC-KR?Q?NOTE:_=C7=D1=B1=B9=B8=BB=B7=CE_=C7=CF=B4=C2_=B0=CD?= +Date: Mon, 2 May 2005 16:07:05 -0600 + +tOu6zrrQwMcguLbC+bChwfa3ziwgv+y4rrTCIMfPs6q01MC7ILnPvcC0z7TZLg0KDQrBpiDAzLin +wLogSmFtaXPA1LTPtNku diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email10 b/vendor/rails/actionmailer/test/fixtures/raw_email10 new file mode 100644 index 00000000..b1fc2b26 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email10 @@ -0,0 +1,20 @@ +Return-Path: +Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for ; Tue, 10 May 2005 15:27:05 -0500 +Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for ; Tue, 10 May 2005 15:27:04 -0500 +Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for ; Tue, 10 May 2005 15:27:03 -0500 +Date: Tue, 10 May 2005 15:27:03 -0500 +From: xxx@xxxx.xxx +Sender: xxx@xxxx.xxx +To: xxxxxxxxxxx@xxxx.xxxx.xxx +Message-Id: +X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx +Delivered-To: xxx@xxxx.xxx +Importance: normal +Content-Type: text/plain; charset=X-UNKNOWN + +Test test. Hi. Waving. m + +---------------------------------------------------------------- +Sent via Bell Mobility's Text Messaging service. +Envoyé par le service de messagerie texte de Bell Mobilité. +---------------------------------------------------------------- diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email11 b/vendor/rails/actionmailer/test/fixtures/raw_email11 new file mode 100644 index 00000000..8af74b87 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email11 @@ -0,0 +1,34 @@ +From xxx@xxxx.com Wed Apr 27 14:15:31 2005 +Mime-Version: 1.0 (Apple Message framework v619.2) +To: xxxxx@xxxxx +Message-Id: <416eaebec6d333ec6939eaf8a7d80724@xxxxx> +Content-Type: multipart/alternative; + boundary=Apple-Mail-5-1037861608 +From: xxxxx@xxxxx +Subject: worse when you use them. +Date: Wed, 27 Apr 2005 14:15:31 -0700 + + + + +--Apple-Mail-5-1037861608 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=US-ASCII; + format=flowed + + +XXXXX Xxxxx + +--Apple-Mail-5-1037861608 +Content-Transfer-Encoding: 7bit +Content-Type: text/enriched; + charset=US-ASCII + + + +XXXXX Xxxxx + + +--Apple-Mail-5-1037861608-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email12 b/vendor/rails/actionmailer/test/fixtures/raw_email12 new file mode 100644 index 00000000..2cd31720 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email12 @@ -0,0 +1,32 @@ +Mime-Version: 1.0 (Apple Message framework v730) +Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151 +Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com> +From: foo@example.com +Subject: testing +Date: Mon, 6 Jun 2005 22:21:22 +0200 +To: blah@example.com + + +--Apple-Mail-13-196941151 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=ISO-8859-1; + delsp=yes; + format=flowed + +This is the first part. + +--Apple-Mail-13-196941151 +Content-Type: image/jpeg +Content-Transfer-Encoding: base64 +Content-Location: Photo25.jpg +Content-ID: +Content-Disposition: inline + +jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw +ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE +QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB +gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5= + +--Apple-Mail-13-196941151-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email13 b/vendor/rails/actionmailer/test/fixtures/raw_email13 new file mode 100644 index 00000000..7d9314e3 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email13 @@ -0,0 +1,29 @@ +Mime-Version: 1.0 (Apple Message framework v730) +Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151 +Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com> +From: foo@example.com +Subject: testing +Date: Mon, 6 Jun 2005 22:21:22 +0200 +To: blah@example.com + + +--Apple-Mail-13-196941151 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=ISO-8859-1; + delsp=yes; + format=flowed + +This is the first part. + +--Apple-Mail-13-196941151 +Content-Type: text/x-ruby-script; name="hello.rb" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="api.rb" + +puts "Hello, world!" +gets + +--Apple-Mail-13-196941151-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email2 b/vendor/rails/actionmailer/test/fixtures/raw_email2 new file mode 100644 index 00000000..3999fcc8 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email2 @@ -0,0 +1,114 @@ +From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005 +Return-Path: +X-Original-To: xxxxx@xxxxx.xxxxxxxxx.com +Delivered-To: xxxxx@xxxxx.xxxxxxxxx.com +Received: from localhost (localhost [127.0.0.1]) + by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 06C9DA98D + for ; Sun, 8 May 2005 19:09:13 +0000 (GMT) +Received: from xxxxx.xxxxxxxxx.com ([127.0.0.1]) + by localhost (xxxxx.xxxxxxxxx.com [127.0.0.1]) (amavisd-new, port 10024) + with LMTP id 88783-08 for ; + Sun, 8 May 2005 19:09:12 +0000 (GMT) +Received: from xxxxxxx.xxxxxxxxx.com (xxxxxxx.xxxxxxxxx.com [69.36.39.150]) + by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 10D8BA960 + for ; Sun, 8 May 2005 19:09:12 +0000 (GMT) +Received: from zproxy.gmail.com (zproxy.gmail.com [64.233.162.199]) + by xxxxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 9EBC4148EAB + for ; Sun, 8 May 2005 14:09:11 -0500 (CDT) +Received: by zproxy.gmail.com with SMTP id 13so1233405nzp + for ; Sun, 08 May 2005 12:09:11 -0700 (PDT) +DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; + s=beta; d=gmail.com; + h=received:message-id:date:from:reply-to:to:subject:in-reply-to:mime-version:content-type:references; + b=cid1mzGEFa3gtRa06oSrrEYfKca2CTKu9sLMkWxjbvCsWMtp9RGEILjUz0L5RySdH5iO661LyNUoHRFQIa57bylAbXM3g2DTEIIKmuASDG3x3rIQ4sHAKpNxP7Pul+mgTaOKBv+spcH7af++QEJ36gHFXD2O/kx9RePs3JNf/K8= +Received: by 10.36.10.16 with SMTP id 16mr1012493nzj; + Sun, 08 May 2005 12:09:11 -0700 (PDT) +Received: by 10.36.5.10 with HTTP; Sun, 8 May 2005 12:09:11 -0700 (PDT) +Message-ID: +Date: Sun, 8 May 2005 14:09:11 -0500 +From: xxxxxxxxx xxxxxxx +Reply-To: xxxxxxxxx xxxxxxx +To: xxxxx xxxx +Subject: Fwd: Signed email causes file attachments +In-Reply-To: +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_5028_7368284.1115579351471" +References: + +------=_Part_5028_7368284.1115579351471 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +We should not include these files or vcards as attachments. + +---------- Forwarded message ---------- +From: xxxxx xxxxxx +Date: May 8, 2005 1:17 PM +Subject: Signed email causes file attachments +To: xxxxxxx@xxxxxxxxxx.com + + +Hi, + +Just started to use my xxxxxxxx account (to set-up a GTD system, +natch) and noticed that when I send content via email the signature/ +certificate from my email account gets added as a file (e.g. +"smime.p7s"). + +Obviously I can uncheck the signature option in the Mail compose +window but how often will I remember to do that? + +Is there any way these kind of files could be ignored, e.g. via some +sort of exclusions list? + +------=_Part_5028_7368284.1115579351471 +Content-Type: application/pkcs7-signature; name=smime.p7s +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" + +MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w +ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh +d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt +YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD +ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL +7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw +o8xS3A0a1LXealcmlEbJibmKkEaoXci3MhryLgpaa+Kk/sH02SNatDO1vS28bPsibZpcc6deFrla +hSYnL+PW54mDTGHIcCN2fbx/Y6qspzqmtKaXrv75NBtuy9cB6KzU4j2xXbTkAwz3pRSghJJaAwdp ++yIivAD3vr0kJE3p+Ez34HMh33EXEpFoWcN+MCEQZD9WnmFViMrvfvMXLGVFQfAAcC060eGFSRJ1 +ZQ9UVQIDAQABoy0wKzAbBgNVHREEFDASgRBzbWhhdW5jaEBtYWMuY29tMAwGA1UdEwEB/wQCMAAw +DQYJKoZIhvcNAQEEBQADgYEAQMrg1n2pXVWteP7BBj+Pk3UfYtbuHb42uHcLJjfjnRlH7AxnSwrd +L3HED205w3Cq8T7tzVxIjRRLO/ljq0GedSCFBky7eYo1PrXhztGHCTSBhsiWdiyLWxKlOxGAwJc/ +lMMnwqLOdrQcoF/YgbjeaUFOQbUh94w9VDNpWZYCZwcwggM/MIICqKADAgECAgENMA0GCSqGSIb3 +DQEBBQUAMIHRMQswCQYDVQQGEwJaQTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlD +YXBlIFRvd24xGjAYBgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0 +aW9uIFNlcnZpY2VzIERpdmlzaW9uMSQwIgYDVQQDExtUaGF3dGUgUGVyc29uYWwgRnJlZW1haWwg +Q0ExKzApBgkqhkiG9w0BCQEWHHBlcnNvbmFsLWZyZWVtYWlsQHRoYXd0ZS5jb20wHhcNMDMwNzE3 +MDAwMDAwWhcNMTMwNzE2MjM1OTU5WjBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENv +bnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElz +c3VpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMSmPFVzVftOucqZWh5owHUEcJ3f +6f+jHuy9zfVb8hp2vX8MOmHyv1HOAdTlUAow1wJjWiyJFXCO3cnwK4Vaqj9xVsuvPAsH5/EfkTYk +KhPPK9Xzgnc9A74r/rsYPge/QIACZNenprufZdHFKlSFD0gEf6e20TxhBEAeZBlyYLf7AgMBAAGj +gZQwgZEwEgYDVR0TAQH/BAgwBgEB/wIBADBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLnRo +YXd0ZS5jb20vVGhhd3RlUGVyc29uYWxGcmVlbWFpbENBLmNybDALBgNVHQ8EBAMCAQYwKQYDVR0R +BCIwIKQeMBwxGjAYBgNVBAMTEVByaXZhdGVMYWJlbDItMTM4MA0GCSqGSIb3DQEBBQUAA4GBAEiM +0VCD6gsuzA2jZqxnD3+vrL7CF6FDlpSdf0whuPg2H6otnzYvwPQcUCCTcDz9reFhYsPZOhl+hLGZ +GwDFGguCdJ4lUJRix9sncVcljd2pnDmOjCBPZV+V2vf3h9bGCE6u9uo05RAaWzVNd+NWIXiC3CEZ +Nd4ksdMdRv9dX2VPMYIC5zCCAuMCAQEwaTBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3Rl +IENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWls +IElzc3VpbmcgQ0ECAw5c+TAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB +MBwGCSqGSIb3DQEJBTEPFw0wNTA1MDgxODE3NDZaMCMGCSqGSIb3DQEJBDEWBBQSkG9j6+hB0pKp +fV9tCi/iP59sNTB4BgkrBgEEAYI3EAQxazBpMGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3 +dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h +aWwgSXNzdWluZyBDQQIDDlz5MHoGCyqGSIb3DQEJEAILMWugaTBiMQswCQYDVQQGEwJaQTElMCMG +A1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNv +bmFsIEZyZWVtYWlsIElzc3VpbmcgQ0ECAw5c+TANBgkqhkiG9w0BAQEFAASCAQAm1GeF7dWfMvrW +8yMPjkhE+R8D1DsiCoWSCp+5gAQm7lcK7V3KrZh5howfpI3TmCZUbbaMxOH+7aKRKpFemxoBY5Q8 +rnCkbpg/++/+MI01T69hF/rgMmrGcrv2fIYy8EaARLG0xUVFSZHSP+NQSYz0TTmh4cAESHMzY3JA +nHOoUkuPyl8RXrimY1zn0lceMXlweZRouiPGuPNl1hQKw8P+GhOC5oLlM71UtStnrlk3P9gqX5v7 +Tj7Hx057oVfY8FMevjxGwU3EK5TczHezHbWWgTyum9l2ZQbUQsDJxSniD3BM46C1VcbDLPaotAZ0 +fTYLZizQfm5hcWEbfYVzkSzLAAAAAAAA +------=_Part_5028_7368284.1115579351471-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email3 b/vendor/rails/actionmailer/test/fixtures/raw_email3 new file mode 100644 index 00000000..771a9635 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email3 @@ -0,0 +1,70 @@ +From xxxx@xxxx.com Tue May 10 11:28:07 2005 +Return-Path: +X-Original-To: xxxx@xxxx.com +Delivered-To: xxxx@xxxx.com +Received: from localhost (localhost [127.0.0.1]) + by xxx.xxxxx.com (Postfix) with ESMTP id 50FD3A96F + for ; Tue, 10 May 2005 17:26:50 +0000 (GMT) +Received: from xxx.xxxxx.com ([127.0.0.1]) + by localhost (xxx.xxxxx.com [127.0.0.1]) (amavisd-new, port 10024) + with LMTP id 70060-03 for ; + Tue, 10 May 2005 17:26:49 +0000 (GMT) +Received: from xxx.xxxxx.com (xxx.xxxxx.com [69.36.39.150]) + by xxx.xxxxx.com (Postfix) with ESMTP id 8B957A94B + for ; Tue, 10 May 2005 17:26:48 +0000 (GMT) +Received: from xxx.xxxxx.com (xxx.xxxxx.com [64.233.184.203]) + by xxx.xxxxx.com (Postfix) with ESMTP id 9972514824C + for ; Tue, 10 May 2005 12:26:40 -0500 (CDT) +Received: by xxx.xxxxx.com with SMTP id 68so1694448wri + for ; Tue, 10 May 2005 10:26:40 -0700 (PDT) +DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; + s=beta; d=xxxxx.com; + h=received:message-id:date:from:reply-to:to:subject:mime-version:content-type; + b=g8ZO5ttS6GPEMAz9WxrRk9+9IXBUfQIYsZLL6T88+ECbsXqGIgfGtzJJFn6o9CE3/HMrrIGkN5AisxVFTGXWxWci5YA/7PTVWwPOhJff5BRYQDVNgRKqMl/SMttNrrRElsGJjnD1UyQ/5kQmcBxq2PuZI5Zc47u6CILcuoBcM+A= +Received: by 10.54.96.19 with SMTP id t19mr621017wrb; + Tue, 10 May 2005 10:26:39 -0700 (PDT) +Received: by 10.54.110.5 with HTTP; Tue, 10 May 2005 10:26:39 -0700 (PDT) +Message-ID: +Date: Tue, 10 May 2005 11:26:39 -0600 +From: Test Tester +Reply-To: Test Tester +To: xxxx@xxxx.com, xxxx@xxxx.com +Subject: Another PDF +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_2192_32400445.1115745999735" +X-Virus-Scanned: amavisd-new at textdrive.com + +------=_Part_2192_32400445.1115745999735 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +Just attaching another PDF, here, to see what the message looks like, +and to see if I can figure out what is going wrong here. + +------=_Part_2192_32400445.1115745999735 +Content-Type: application/pdf; name="broken.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="broken.pdf" + +JVBERi0xLjQNCiXk9tzfDQoxIDAgb2JqDQo8PCAvTGVuZ3RoIDIgMCBSDQogICAvRmlsdGVyIC9G +bGF0ZURlY29kZQ0KPj4NCnN0cmVhbQ0KeJy9Wt2KJbkNvm/od6jrhZxYln9hWEh2p+8HBvICySaE +ycLuTV4/1ifJ9qnq09NpSBimu76yLUuy/qzqcPz7+em3Ixx/CDc6CsXxs3b5+fvfjr/8cPz6/BRu +rbfAx/n3739/fuJylJ5u5fjX81OuDr4deK4Bz3z/aDP+8fz0yw8g0Ofq7ktr1Mn+u28rvhy/jVeD +QSa+9YNKHP/pxjvDNfVAx/m3MFz54FhvTbaseaxiDoN2LeMVMw+yA7RbHSCDzxZuaYB2E1Yay7QU +x89vz0+tyFDKMlAHK5yqLmnjF+c4RjEiQIUeKwblXMe+AsZjN1J5yGQL5DHpDHksurM81rF6PKab +gK6zAarIDzIiUY23rJsN9iorAE816aIu6lsgAdQFsuhhkHOUFgVjp2GjMqSewITXNQ27jrMeamkg +1rPI3iLWG2CIaSBB+V1245YVRICGbbpYKHc2USFDl6M09acQVQYhlwIrkBNLISvXhGlF1wi5FHCw +wxZkoGNJlVeJCEsqKA+3YAV5AMb6KkeaqEJQmFKKQU8T1pRi2ihE1Y4CDrqoYFFXYjJJOatsyzuI +8SIlykuxKTMibWK8H1PgEvqYgs4GmQSrEjJAalgGirIhik+p4ZQN9E3ETFPAHE1b8pp1l/0Rc1gl +fQs0ABWvyoZZzU8VnPXwVVcO9BEsyjEJaO6eBoZRyKGlrKoYoOygA8BGIzgwN3RQ15ouigG5idZQ +fx2U4Db2CqiLO0WHAZoylGiCAqhniNQjFjQPSkmjwfNTgQ6M1Ih+eWo36wFmjIxDJZiGUBiWsAyR +xX3EekGOizkGI96Ol9zVZTAivikURhRsHh2E3JhWMpSTZCnnonrLhMCodgrNcgo4uyJUJc6qnVss +nrGd1Ptr0YwisCOYyIbUwVjV4xBUNLbguSO2YHujonAMJkMdSI7bIw91Akq2AUlMUWGFTMAOamjU +OvZQCxIkY2pCpMFo/IwLdVLHs6nddwTRrgoVbvLU9eB0G4EMndV0TNoxHbt3JBWwK6hhv3iHfDtF +yokB302IpEBTnWICde4uYc/1khDbSIkQopO6lcqamGBu1OSE3N5IPSsZX00CkSHRiiyx6HQIShsS +HSVNswdVsaOUSAWq9aYhDtGDaoG5a3lBGkYt/lFlBFt1UqrYnzVtUpUQnLiZeouKgf1KhRBViRRk +ExepJCzTwEmFDalIRbLEGtw0gfpESOpIAF/NnpPzcVCG86s0g2DuSyd41uhNGbEgaSrWEXORErbw +------=_Part_2192_32400445.1115745999735-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email4 b/vendor/rails/actionmailer/test/fixtures/raw_email4 new file mode 100644 index 00000000..639ad40e --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email4 @@ -0,0 +1,59 @@ +Return-Path: +Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id 6AAEE3B4D23 for ; Sun, 8 May 2005 12:30:23 -0500 +Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id j48HUC213279 for ; Sun, 8 May 2005 12:30:13 -0500 +Received: from conversion-xxx.xxxx.xxx.net by xxx.xxxx.xxx id <0IG600901LQ64I@xxx.xxxx.xxx> for ; Sun, 8 May 2005 12:30:12 -0500 +Received: from agw1 by xxx.xxxx.xxx with ESMTP id <0IG600JFYLYCAxxx@xxxx.xxx> for ; Sun, 8 May 2005 12:30:12 -0500 +Date: Sun, 8 May 2005 12:30:08 -0500 +From: xxx@xxxx.xxx +To: xxx@xxxx.xxx +Message-Id: <7864245.1115573412626.JavaMxxx@xxxx.xxx> +Subject: Filth +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary=mimepart_427e4cb4ca329_133ae40413c81ef +X-Mms-Priority: 1 +X-Mms-Transaction-Id: 3198421808-0 +X-Mms-Message-Type: 0 +X-Mms-Sender-Visibility: 1 +X-Mms-Read-Reply: 1 +X-Original-To: xxx@xxxx.xxx +X-Mms-Message-Class: 0 +X-Mms-Delivery-Report: 0 +X-Mms-Mms-Version: 16 +Delivered-To: xxx@xxxx.xxx +X-Nokia-Ag-Version: 2.0 + +This is a multi-part message in MIME format. + +--mimepart_427e4cb4ca329_133ae40413c81ef +Content-Type: multipart/mixed; boundary=mimepart_427e4cb4cbd97_133ae40413c8217 + + + +--mimepart_427e4cb4cbd97_133ae40413c8217 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline +Content-Location: text.txt + +Some text + +--mimepart_427e4cb4cbd97_133ae40413c8217-- + +--mimepart_427e4cb4ca329_133ae40413c81ef +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +-- +This Orange Multi Media Message was sent wirefree from an Orange +MMS phone. If you would like to reply, please text or phone the +sender directly by using the phone number listed in the sender's +address. To learn more about Orange's Multi Media Messaging +Service, find us on the Web at xxx.xxxx.xxx.uk/mms + + +--mimepart_427e4cb4ca329_133ae40413c81ef + + +--mimepart_427e4cb4ca329_133ae40413c81ef- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email5 b/vendor/rails/actionmailer/test/fixtures/raw_email5 new file mode 100644 index 00000000..151c6314 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email5 @@ -0,0 +1,19 @@ +Return-Path: +Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for ; Tue, 10 May 2005 15:27:05 -0500 +Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for ; Tue, 10 May 2005 15:27:04 -0500 +Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for ; Tue, 10 May 2005 15:27:03 -0500 +Date: Tue, 10 May 2005 15:27:03 -0500 +From: xxx@xxxx.xxx +Sender: xxx@xxxx.xxx +To: xxxxxxxxxxx@xxxx.xxxx.xxx +Message-Id: +X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx +Delivered-To: xxx@xxxx.xxx +Importance: normal + +Test test. Hi. Waving. m + +---------------------------------------------------------------- +Sent via Bell Mobility's Text Messaging service. +Envoyé par le service de messagerie texte de Bell Mobilité. +---------------------------------------------------------------- diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email6 b/vendor/rails/actionmailer/test/fixtures/raw_email6 new file mode 100644 index 00000000..93289c4f --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email6 @@ -0,0 +1,20 @@ +Return-Path: +Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for ; Tue, 10 May 2005 15:27:05 -0500 +Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for ; Tue, 10 May 2005 15:27:04 -0500 +Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for ; Tue, 10 May 2005 15:27:03 -0500 +Date: Tue, 10 May 2005 15:27:03 -0500 +From: xxx@xxxx.xxx +Sender: xxx@xxxx.xxx +To: xxxxxxxxxxx@xxxx.xxxx.xxx +Message-Id: +X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx +Delivered-To: xxx@xxxx.xxx +Importance: normal +Content-Type: text/plain; charset=us-ascii + +Test test. Hi. Waving. m + +---------------------------------------------------------------- +Sent via Bell Mobility's Text Messaging service. +Envoyé par le service de messagerie texte de Bell Mobilité. +---------------------------------------------------------------- diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email7 b/vendor/rails/actionmailer/test/fixtures/raw_email7 new file mode 100644 index 00000000..da64ada8 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email7 @@ -0,0 +1,66 @@ +Mime-Version: 1.0 (Apple Message framework v730) +Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151 +Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com> +From: foo@example.com +Subject: testing +Date: Mon, 6 Jun 2005 22:21:22 +0200 +To: blah@example.com + + +--Apple-Mail-13-196941151 +Content-Type: multipart/mixed; + boundary=Apple-Mail-12-196940926 + + +--Apple-Mail-12-196940926 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=ISO-8859-1; + delsp=yes; + format=flowed + +This is the first part. + +--Apple-Mail-12-196940926 +Content-Transfer-Encoding: 7bit +Content-Type: text/x-ruby-script; + x-unix-mode=0666; + name="test.rb" +Content-Disposition: attachment; + filename=test.rb + +puts "testing, testing" + +--Apple-Mail-12-196940926 +Content-Transfer-Encoding: base64 +Content-Type: application/pdf; + x-unix-mode=0666; + name="test.pdf" +Content-Disposition: inline; + filename=test.pdf + +YmxhaCBibGFoIGJsYWg= + +--Apple-Mail-12-196940926 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=US-ASCII; + format=flowed + + + +--Apple-Mail-12-196940926-- + +--Apple-Mail-13-196941151 +Content-Transfer-Encoding: base64 +Content-Type: application/pkcs7-signature; + name=smime.p7s +Content-Disposition: attachment; + filename=smime.p7s + +jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw +ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE +QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB +gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5= + +--Apple-Mail-13-196941151-- diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email8 b/vendor/rails/actionmailer/test/fixtures/raw_email8 new file mode 100644 index 00000000..2382dfdf --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email8 @@ -0,0 +1,47 @@ +From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005 +Return-Path: +Message-ID: +Date: Sun, 8 May 2005 14:09:11 -0500 +From: xxxxxxxxx xxxxxxx +Reply-To: xxxxxxxxx xxxxxxx +To: xxxxx xxxx +Subject: Fwd: Signed email causes file attachments +In-Reply-To: +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_5028_7368284.1115579351471" +References: + +------=_Part_5028_7368284.1115579351471 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +We should not include these files or vcards as attachments. + +---------- Forwarded message ---------- +From: xxxxx xxxxxx +Date: May 8, 2005 1:17 PM +Subject: Signed email causes file attachments +To: xxxxxxx@xxxxxxxxxx.com + + +Hi, + +Test attachments oddly encoded with japanese charset. + + +------=_Part_5028_7368284.1115579351471 +Content-Type: application/octet-stream; name*=iso-2022-jp'ja'01%20Quien%20Te%20Dij%8aat.%20Pitbull.mp3 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment + +MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w +ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh +d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt +YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD +ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL +7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw +------=_Part_5028_7368284.1115579351471-- + diff --git a/vendor/rails/actionmailer/test/fixtures/raw_email9 b/vendor/rails/actionmailer/test/fixtures/raw_email9 new file mode 100644 index 00000000..8b9b1eaa --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/raw_email9 @@ -0,0 +1,28 @@ +Received: from xxx.xxx.xxx ([xxx.xxx.xxx.xxx] verified) + by xxx.com (CommuniGate Pro SMTP 4.2.8) + with SMTP id 2532598 for xxx@xxx.com; Wed, 23 Feb 2005 17:51:49 -0500 +Received-SPF: softfail + receiver=xxx.com; client-ip=xxx.xxx.xxx.xxx; envelope-from=xxx@xxx.xxx +quite Delivered-To: xxx@xxx.xxx +Received: by xxx.xxx.xxx (Wostfix, from userid xxx) + id 0F87F333; Wed, 23 Feb 2005 16:16:17 -0600 +Date: Wed, 23 Feb 2005 18:20:17 -0400 +From: "xxx xxx" +Message-ID: <4D6AA7EB.6490534@xxx.xxx> +To: xxx@xxx.com +Subject: Stop adware/spyware once and for all. +X-Scanned-By: MIMEDefang 2.11 (www dot roaringpenguin dot com slash mimedefang) + +You are infected with: +Ad Ware and Spy Ware + +Get your free scan and removal download now, +before it gets any worse. + +http://xxx.xxx.info?aid=3D13&?stat=3D4327kdzt + + + + +no more? (you will still be infected) +http://xxx.xxx.info/discon/?xxx@xxx.com diff --git a/vendor/rails/actionmailer/test/fixtures/templates/signed_up.rhtml b/vendor/rails/actionmailer/test/fixtures/templates/signed_up.rhtml new file mode 100644 index 00000000..a85d5fa4 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/templates/signed_up.rhtml @@ -0,0 +1,3 @@ +Hello there, + +Mr. <%= @recipient %> \ No newline at end of file diff --git a/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.rhtml b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.rhtml new file mode 100644 index 00000000..6940419d --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.rhtml @@ -0,0 +1 @@ +Ignored when searching for implicitly multipart parts. diff --git a/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml new file mode 100644 index 00000000..946d99ed --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.html.rhtml @@ -0,0 +1,10 @@ + + + HTML formatted message to <%= @recipient %>. + + + + + HTML formatted message to <%= @recipient %>. + + diff --git a/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml new file mode 100644 index 00000000..a6c8d54c --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.plain.rhtml @@ -0,0 +1,2 @@ +Plain text to <%= @recipient %>. +Plain text to <%= @recipient %>. diff --git a/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.yaml.rhtml b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.yaml.rhtml new file mode 100644 index 00000000..c14348c7 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.yaml.rhtml @@ -0,0 +1 @@ +yaml to: <%= @recipient %> \ No newline at end of file diff --git a/vendor/rails/actionmailer/test/fixtures/test_mailer/signed_up.rhtml b/vendor/rails/actionmailer/test/fixtures/test_mailer/signed_up.rhtml new file mode 100644 index 00000000..a85d5fa4 --- /dev/null +++ b/vendor/rails/actionmailer/test/fixtures/test_mailer/signed_up.rhtml @@ -0,0 +1,3 @@ +Hello there, + +Mr. <%= @recipient %> \ No newline at end of file diff --git a/vendor/rails/actionmailer/test/mail_helper_test.rb b/vendor/rails/actionmailer/test/mail_helper_test.rb new file mode 100644 index 00000000..bf5bf7f3 --- /dev/null +++ b/vendor/rails/actionmailer/test/mail_helper_test.rb @@ -0,0 +1,97 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") +$:.unshift File.dirname(__FILE__) + "/fixtures/helpers" + +require 'test/unit' +require 'action_mailer' + +module MailerHelper + def person_name + "Mr. Joe Person" + end +end + +class HelperMailer < ActionMailer::Base + helper MailerHelper + helper :test + + def use_helper(recipient) + recipients recipient + subject "using helpers" + from "tester@example.com" + end + + def use_test_helper(recipient) + recipients recipient + subject "using helpers" + from "tester@example.com" + self.body = { :text => "emphasize me!" } + end + + def use_mail_helper(recipient) + recipients recipient + subject "using mailing helpers" + from "tester@example.com" + self.body = { :text => + "But soft! What light through yonder window breaks? It is the east, " + + "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " + + "which is sick and pale with grief that thou, her maid, art far more " + + "fair than she. Be not her maid, for she is envious! Her vestal " + + "livery is but sick and green, and none but fools do wear it. Cast " + + "it off!" + } + end + + def use_helper_method(recipient) + recipients recipient + subject "using helpers" + from "tester@example.com" + self.body = { :text => "emphasize me!" } + end + + private + + def name_of_the_mailer_class + self.class.name + end + helper_method :name_of_the_mailer_class +end + +HelperMailer.template_root = File.dirname(__FILE__) + "/fixtures" + +class MailerHelperTest < Test::Unit::TestCase + def new_mail( charset="utf-8" ) + mail = TMail::Mail.new + mail.set_content_type "text", "plain", { "charset" => charset } if charset + mail + end + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @recipient = 'test@localhost' + end + + def test_use_helper + mail = HelperMailer.create_use_helper(@recipient) + assert_match %r{Mr. Joe Person}, mail.encoded + end + + def test_use_test_helper + mail = HelperMailer.create_use_test_helper(@recipient) + assert_match %r{emphasize me!}, mail.encoded + end + + def test_use_helper_method + mail = HelperMailer.create_use_helper_method(@recipient) + assert_match %r{HelperMailer}, mail.encoded + end + + def test_use_mail_helper + mail = HelperMailer.create_use_mail_helper(@recipient) + assert_match %r{ But soft!}, mail.encoded + assert_match %r{east, and\n Juliet}, mail.encoded + end +end + diff --git a/vendor/rails/actionmailer/test/mail_render_test.rb b/vendor/rails/actionmailer/test/mail_render_test.rb new file mode 100644 index 00000000..d5819652 --- /dev/null +++ b/vendor/rails/actionmailer/test/mail_render_test.rb @@ -0,0 +1,48 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") + +require 'test/unit' +require 'action_mailer' + +class RenderMailer < ActionMailer::Base + def inline_template(recipient) + recipients recipient + subject "using helpers" + from "tester@example.com" + body render(:inline => "Hello, <%= @world %>", :body => { :world => "Earth" }) + end + + def file_template(recipient) + recipients recipient + subject "using helpers" + from "tester@example.com" + body render(:file => "signed_up", :body => { :recipient => recipient }) + end + + def initialize_defaults(method_name) + super + mailer_name "test_mailer" + end +end + +RenderMailer.template_root = File.dirname(__FILE__) + "/fixtures" + +class RenderHelperTest < Test::Unit::TestCase + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @recipient = 'test@localhost' + end + + def test_inline_template + mail = RenderMailer.create_inline_template(@recipient) + assert_equal "Hello, Earth", mail.body.strip + end + + def test_file_template + mail = RenderMailer.create_file_template(@recipient) + assert_equal "Hello there, \n\nMr. test@localhost", mail.body.strip + end +end + diff --git a/vendor/rails/actionmailer/test/mail_service_test.rb b/vendor/rails/actionmailer/test/mail_service_test.rb new file mode 100755 index 00000000..5fdf4074 --- /dev/null +++ b/vendor/rails/actionmailer/test/mail_service_test.rb @@ -0,0 +1,832 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") + +require 'test/unit' +require 'action_mailer' + +class MockSMTP + def self.deliveries + @@deliveries + end + + def initialize + @@deliveries = [] + end + + def sendmail(mail, from, to) + @@deliveries << [mail, from, to] + end +end + +class Net::SMTP + def self.start(*args) + yield MockSMTP.new + end +end + +class FunkyPathMailer < ActionMailer::Base + self.template_root = "#{File.dirname(__FILE__)}/fixtures/path.with.dots" + + def multipart_with_template_path_with_dots(recipient) + recipients recipient + subject "Have a lovely picture" + from "Chad Fowler " + attachment :content_type => "image/jpeg", + :body => "not really a jpeg, we're only testing, after all" + end + + def template_path + "#{File.dirname(__FILE__)}/fixtures/path.with.dots" + end +end + +class TestMailer < ActionMailer::Base + + def signed_up(recipient) + @recipients = recipient + @subject = "[Signed up] Welcome #{recipient}" + @from = "system@loudthinking.com" + @sent_on = Time.local(2004, 12, 12) + @body["recipient"] = recipient + end + + def cancelled_account(recipient) + self.recipients = recipient + self.subject = "[Cancelled] Goodbye #{recipient}" + self.from = "system@loudthinking.com" + self.sent_on = Time.local(2004, 12, 12) + self.body = "Goodbye, Mr. #{recipient}" + end + + def cc_bcc(recipient) + recipients recipient + subject "testing bcc/cc" + from "system@loudthinking.com" + sent_on Time.local(2004, 12, 12) + cc "nobody@loudthinking.com" + bcc "root@loudthinking.com" + body "Nothing to see here." + end + + def iso_charset(recipient) + @recipients = recipient + @subject = "testing isø charsets" + @from = "system@loudthinking.com" + @sent_on = Time.local 2004, 12, 12 + @cc = "nobody@loudthinking.com" + @bcc = "root@loudthinking.com" + @body = "Nothing to see here." + @charset = "iso-8859-1" + end + + def unencoded_subject(recipient) + @recipients = recipient + @subject = "testing unencoded subject" + @from = "system@loudthinking.com" + @sent_on = Time.local 2004, 12, 12 + @cc = "nobody@loudthinking.com" + @bcc = "root@loudthinking.com" + @body = "Nothing to see here." + end + + def extended_headers(recipient) + @recipients = recipient + @subject = "testing extended headers" + @from = "Grytøyr " + @sent_on = Time.local 2004, 12, 12 + @cc = "Grytøyr " + @bcc = "Grytøyr " + @body = "Nothing to see here." + @charset = "iso-8859-1" + end + + def utf8_body(recipient) + @recipients = recipient + @subject = "testing utf-8 body" + @from = "Foo áëô îü " + @sent_on = Time.local 2004, 12, 12 + @cc = "Foo áëô îü " + @bcc = "Foo áëô îü " + @body = "åœö blah" + @charset = "utf-8" + end + + def multipart_with_mime_version(recipient) + recipients recipient + subject "multipart with mime_version" + from "test@example.com" + sent_on Time.local(2004, 12, 12) + mime_version "1.1" + content_type "multipart/alternative" + + part "text/plain" do |p| + p.body = "blah" + end + + part "text/html" do |p| + p.body = "blah" + end + end + + def multipart_with_utf8_subject(recipient) + recipients recipient + subject "Foo áëô îü" + from "test@example.com" + charset "utf-8" + + part "text/plain" do |p| + p.body = "blah" + end + + part "text/html" do |p| + p.body = "blah" + end + end + + def explicitly_multipart_example(recipient, ct=nil) + recipients recipient + subject "multipart example" + from "test@example.com" + sent_on Time.local(2004, 12, 12) + body "plain text default" + content_type ct if ct + + part "text/html" do |p| + p.charset = "iso-8859-1" + p.body = "blah" + end + + attachment :content_type => "image/jpeg", :filename => "foo.jpg", + :body => "123456789" + end + + def implicitly_multipart_example(recipient, cs = nil, order = nil) + @recipients = recipient + @subject = "multipart example" + @from = "test@example.com" + @sent_on = Time.local 2004, 12, 12 + @body = { "recipient" => recipient } + @charset = cs if cs + @implicit_parts_order = order if order + end + + def implicitly_multipart_with_utf8 + recipients "no.one@nowhere.test" + subject "Foo áëô îü" + from "some.one@somewhere.test" + template "implicitly_multipart_example" + body ({ "recipient" => "no.one@nowhere.test" }) + end + + def html_mail(recipient) + recipients recipient + subject "html mail" + from "test@example.com" + body "Emphasize this" + content_type "text/html" + end + + def html_mail_with_underscores(recipient) + subject "html mail with underscores" + body %{_Google} + end + + def custom_template(recipient) + recipients recipient + subject "[Signed up] Welcome #{recipient}" + from "system@loudthinking.com" + sent_on Time.local(2004, 12, 12) + template "signed_up" + + body["recipient"] = recipient + end + + def various_newlines(recipient) + recipients recipient + subject "various newlines" + from "test@example.com" + body "line #1\nline #2\rline #3\r\nline #4\r\r" + + "line #5\n\nline#6\r\n\r\nline #7" + end + + def various_newlines_multipart(recipient) + recipients recipient + subject "various newlines multipart" + from "test@example.com" + content_type "multipart/alternative" + part :content_type => "text/plain", :body => "line #1\nline #2\rline #3\r\nline #4\r\r" + part :content_type => "text/html", :body => "

    line #1

    \n

    line #2

    \r

    line #3

    \r\n

    line #4

    \r\r" + end + + def nested_multipart(recipient) + recipients recipient + subject "nested multipart" + from "test@example.com" + content_type "multipart/mixed" + part :content_type => "multipart/alternative", :content_disposition => "inline" do |p| + p.part :content_type => "text/plain", :body => "test text\nline #2" + p.part :content_type => "text/html", :body => "test HTML
    \nline #2" + end + attachment :content_type => "application/octet-stream",:filename => "test.txt", :body => "test abcdefghijklmnopqstuvwxyz" + end + + def attachment_with_custom_header(recipient) + recipients recipient + subject "custom header in attachment" + from "test@example.com" + content_type "multipart/related" + part :content_type => "text/html", :body => 'yo' + attachment :content_type => "image/jpeg",:filename => "test.jpeg", :body => "i am not a real picture", :headers => { 'Content-ID' => '' } + end + + def unnamed_attachment(recipient) + recipients recipient + subject "nested multipart" + from "test@example.com" + content_type "multipart/mixed" + part :content_type => "text/plain", :body => "hullo" + attachment :content_type => "application/octet-stream", :body => "test abcdefghijklmnopqstuvwxyz" + end + + def headers_with_nonalpha_chars(recipient) + recipients recipient + subject "nonalpha chars" + from "One: Two " + cc "Three: Four " + bcc "Five: Six " + body "testing" + end + + def custom_content_type_attributes + recipients "no.one@nowhere.test" + subject "custom content types" + from "some.one@somewhere.test" + content_type "text/plain; format=flowed" + body "testing" + end + + class < charset } + end + mail + end + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @recipient = 'test@localhost' + end + + def test_nested_parts + created = nil + assert_nothing_raised { created = TestMailer.create_nested_multipart(@recipient)} + assert_equal 2,created.parts.size + assert_equal 2,created.parts.first.parts.size + + assert_equal "multipart/mixed", created.content_type + assert_equal "multipart/alternative", created.parts.first.content_type + assert_equal "text/plain", created.parts.first.parts.first.content_type + assert_equal "text/html", created.parts.first.parts[1].content_type + assert_equal "application/octet-stream", created.parts[1].content_type + end + + def test_attachment_with_custom_header + created = nil + assert_nothing_raised { created = TestMailer.create_attachment_with_custom_header(@recipient)} + assert_equal "", created.parts[1].header['content-id'].to_s + end + + def test_signed_up + expected = new_mail + expected.to = @recipient + expected.subject = "[Signed up] Welcome #{@recipient}" + expected.body = "Hello there, \n\nMr. #{@recipient}" + expected.from = "system@loudthinking.com" + expected.date = Time.local(2004, 12, 12) + expected.mime_version = nil + + created = nil + assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) } + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised { TestMailer.deliver_signed_up(@recipient) } + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_custom_template + expected = new_mail + expected.to = @recipient + expected.subject = "[Signed up] Welcome #{@recipient}" + expected.body = "Hello there, \n\nMr. #{@recipient}" + expected.from = "system@loudthinking.com" + expected.date = Time.local(2004, 12, 12) + + created = nil + assert_nothing_raised { created = TestMailer.create_custom_template(@recipient) } + assert_not_nil created + assert_equal expected.encoded, created.encoded + end + + def test_cancelled_account + expected = new_mail + expected.to = @recipient + expected.subject = "[Cancelled] Goodbye #{@recipient}" + expected.body = "Goodbye, Mr. #{@recipient}" + expected.from = "system@loudthinking.com" + expected.date = Time.local(2004, 12, 12) + + created = nil + assert_nothing_raised { created = TestMailer.create_cancelled_account(@recipient) } + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised { TestMailer.deliver_cancelled_account(@recipient) } + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_cc_bcc + expected = new_mail + expected.to = @recipient + expected.subject = "testing bcc/cc" + expected.body = "Nothing to see here." + expected.from = "system@loudthinking.com" + expected.cc = "nobody@loudthinking.com" + expected.bcc = "root@loudthinking.com" + expected.date = Time.local 2004, 12, 12 + + created = nil + assert_nothing_raised do + created = TestMailer.create_cc_bcc @recipient + end + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised do + TestMailer.deliver_cc_bcc @recipient + end + + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_iso_charset + expected = new_mail( "iso-8859-1" ) + expected.to = @recipient + expected.subject = encode "testing isø charsets", "iso-8859-1" + expected.body = "Nothing to see here." + expected.from = "system@loudthinking.com" + expected.cc = "nobody@loudthinking.com" + expected.bcc = "root@loudthinking.com" + expected.date = Time.local 2004, 12, 12 + + created = nil + assert_nothing_raised do + created = TestMailer.create_iso_charset @recipient + end + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised do + TestMailer.deliver_iso_charset @recipient + end + + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_unencoded_subject + expected = new_mail + expected.to = @recipient + expected.subject = "testing unencoded subject" + expected.body = "Nothing to see here." + expected.from = "system@loudthinking.com" + expected.cc = "nobody@loudthinking.com" + expected.bcc = "root@loudthinking.com" + expected.date = Time.local 2004, 12, 12 + + created = nil + assert_nothing_raised do + created = TestMailer.create_unencoded_subject @recipient + end + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised do + TestMailer.deliver_unencoded_subject @recipient + end + + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_instances_are_nil + assert_nil ActionMailer::Base.new + assert_nil TestMailer.new + end + + def test_deliveries_array + assert_not_nil ActionMailer::Base.deliveries + assert_equal 0, ActionMailer::Base.deliveries.size + TestMailer.deliver_signed_up(@recipient) + assert_equal 1, ActionMailer::Base.deliveries.size + assert_not_nil ActionMailer::Base.deliveries.first + end + + def test_perform_deliveries_flag + ActionMailer::Base.perform_deliveries = false + TestMailer.deliver_signed_up(@recipient) + assert_equal 0, ActionMailer::Base.deliveries.size + ActionMailer::Base.perform_deliveries = true + TestMailer.deliver_signed_up(@recipient) + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_unquote_quoted_printable_subject + msg = <" + + expected = new_mail "iso-8859-1" + expected.to = quote_address_if_necessary @recipient, "iso-8859-1" + expected.subject = "testing extended headers" + expected.body = "Nothing to see here." + expected.from = quote_address_if_necessary "Grytøyr ", "iso-8859-1" + expected.cc = quote_address_if_necessary "Grytøyr ", "iso-8859-1" + expected.bcc = quote_address_if_necessary "Grytøyr ", "iso-8859-1" + expected.date = Time.local 2004, 12, 12 + + created = nil + assert_nothing_raised do + created = TestMailer.create_extended_headers @recipient + end + + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised do + TestMailer.deliver_extended_headers @recipient + end + + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + end + + def test_utf8_body_is_not_quoted + @recipient = "Foo áëô îü " + expected = new_mail "utf-8" + expected.to = quote_address_if_necessary @recipient, "utf-8" + expected.subject = "testing utf-8 body" + expected.body = "åœö blah" + expected.from = quote_address_if_necessary @recipient, "utf-8" + expected.cc = quote_address_if_necessary @recipient, "utf-8" + expected.bcc = quote_address_if_necessary @recipient, "utf-8" + expected.date = Time.local 2004, 12, 12 + + created = TestMailer.create_utf8_body @recipient + assert_match(/åœö blah/, created.encoded) + end + + def test_multiple_utf8_recipients + @recipient = ["\"Foo áëô îü\" ", "\"Example Recipient\" "] + expected = new_mail "utf-8" + expected.to = quote_address_if_necessary @recipient, "utf-8" + expected.subject = "testing utf-8 body" + expected.body = "åœö blah" + expected.from = quote_address_if_necessary @recipient.first, "utf-8" + expected.cc = quote_address_if_necessary @recipient, "utf-8" + expected.bcc = quote_address_if_necessary @recipient, "utf-8" + expected.date = Time.local 2004, 12, 12 + + created = TestMailer.create_utf8_body @recipient + assert_match(/\nFrom: =\?utf-8\?Q\?Foo_.*?\?= \r/, created.encoded) + assert_match(/\nTo: =\?utf-8\?Q\?Foo_.*?\?= , Example Recipient _Google}, mail.body + end + + def test_various_newlines + mail = TestMailer.create_various_newlines(@recipient) + assert_equal("line #1\nline #2\nline #3\nline #4\n\n" + + "line #5\n\nline#6\n\nline #7", mail.body) + end + + def test_various_newlines_multipart + mail = TestMailer.create_various_newlines_multipart(@recipient) + assert_equal "line #1\nline #2\nline #3\nline #4\n\n", mail.parts[0].body + assert_equal "

    line #1

    \n

    line #2

    \n

    line #3

    \n

    line #4

    \n\n", mail.parts[1].body + end + + def test_headers_removed_on_smtp_delivery + ActionMailer::Base.delivery_method = :smtp + TestMailer.deliver_cc_bcc(@recipient) + assert MockSMTP.deliveries[0][2].include?("root@loudthinking.com") + assert MockSMTP.deliveries[0][2].include?("nobody@loudthinking.com") + assert MockSMTP.deliveries[0][2].include?(@recipient) + assert_match %r{^Cc: nobody@loudthinking.com}, MockSMTP.deliveries[0][0] + assert_match %r{^To: #{@recipient}}, MockSMTP.deliveries[0][0] + assert_no_match %r{^Bcc: root@loudthinking.com}, MockSMTP.deliveries[0][0] + end + + def test_recursive_multipart_processing + fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email7") + mail = TMail::Mail.parse(fixture) + assert_equal "This is the first part.\n\nAttachment: test.rb\nAttachment: test.pdf\n\n\nAttachment: smime.p7s\n", mail.body + end + + def test_decode_encoded_attachment_filename + fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email8") + mail = TMail::Mail.parse(fixture) + attachment = mail.attachments.last + assert_equal "01QuienTeDijat.Pitbull.mp3", attachment.original_filename + end + + def test_wrong_mail_header + fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email9") + assert_raise(TMail::SyntaxError) { TMail::Mail.parse(fixture) } + end + + def test_decode_message_with_unknown_charset + fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email10") + mail = TMail::Mail.parse(fixture) + assert_nothing_raised { mail.body } + end + + def test_decode_message_with_unquoted_atchar_in_header + fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email11") + mail = TMail::Mail.parse(fixture) + assert_not_nil mail.from + end + + def test_empty_header_values_omitted + result = TestMailer.create_unnamed_attachment(@recipient).encoded + assert_match %r{Content-Type: application/octet-stream[^;]}, result + assert_match %r{Content-Disposition: attachment[^;]}, result + end + + def test_headers_with_nonalpha_chars + mail = TestMailer.create_headers_with_nonalpha_chars(@recipient) + assert !mail.from_addrs.empty? + assert !mail.cc_addrs.empty? + assert !mail.bcc_addrs.empty? + assert_match(/:/, mail.from_addrs.to_s) + assert_match(/:/, mail.cc_addrs.to_s) + assert_match(/:/, mail.bcc_addrs.to_s) + end + + def test_deliver_with_mail_object + mail = TestMailer.create_headers_with_nonalpha_chars(@recipient) + assert_nothing_raised { TestMailer.deliver(mail) } + assert_equal 1, TestMailer.deliveries.length + end + + def test_multipart_with_template_path_with_dots + mail = FunkyPathMailer.create_multipart_with_template_path_with_dots(@recipient) + assert_equal 2, mail.parts.length + end + + def test_custom_content_type_attributes + mail = TestMailer.create_custom_content_type_attributes + assert_match %r{format=flowed}, mail['content-type'].to_s + assert_match %r{charset=utf-8}, mail['content-type'].to_s + end +end + +class InheritableTemplateRootTest < Test::Unit::TestCase + def test_attr + expected = "#{File.dirname(__FILE__)}/fixtures/path.with.dots" + assert_equal expected, FunkyPathMailer.template_root + + sub = Class.new(FunkyPathMailer) + sub.template_root = 'test/path' + + assert_equal 'test/path', sub.template_root + assert_equal expected, FunkyPathMailer.template_root + end +end diff --git a/vendor/rails/actionmailer/test/quoting_test.rb b/vendor/rails/actionmailer/test/quoting_test.rb new file mode 100644 index 00000000..6291cd3d --- /dev/null +++ b/vendor/rails/actionmailer/test/quoting_test.rb @@ -0,0 +1,48 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") +$:.unshift(File.dirname(__FILE__) + "/../lib/action_mailer/vendor") + +require 'test/unit' +require 'tmail' +require 'tempfile' + +class QuotingTest < Test::Unit::TestCase + def test_quote_multibyte_chars + original = "\303\246 \303\270 and \303\245" + + result = execute_in_sandbox(<<-CODE) + $:.unshift(File.dirname(__FILE__) + "/../lib/") + $KCODE = 'u' + require 'jcode' + require 'action_mailer/quoting' + include ActionMailer::Quoting + quoted_printable(#{original.inspect}, "UTF-8") + CODE + + unquoted = TMail::Unquoter.unquote_and_convert_to(result, nil) + assert_equal unquoted, original + end + + private + + # This whole thing *could* be much simpler, but I don't think Tempfile, + # popen and others exist on all platforms (like Windows). + def execute_in_sandbox(code) + test_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.rb" + res_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.out" + + File.open(test_name, "w+") do |file| + file.write(<<-CODE) + block = Proc.new do + #{code} + end + puts block.call + CODE + end + + system("ruby #{test_name} > #{res_name}") or raise "could not run test in sandbox" + File.read(res_name) + ensure + File.delete(test_name) rescue nil + File.delete(res_name) rescue nil + end +end diff --git a/vendor/rails/actionmailer/test/tmail_test.rb b/vendor/rails/actionmailer/test/tmail_test.rb new file mode 100644 index 00000000..3930c7d3 --- /dev/null +++ b/vendor/rails/actionmailer/test/tmail_test.rb @@ -0,0 +1,17 @@ +$:.unshift(File.dirname(__FILE__) + "/../lib/") +$:.unshift File.dirname(__FILE__) + "/fixtures/helpers" + +require 'test/unit' +require 'action_mailer' + +class TMailMailTest < Test::Unit::TestCase + def test_body + m = TMail::Mail.new + expected = 'something_with_underscores' + m.encoding = 'quoted-printable' + quoted_body = [expected].pack('*M') + m.body = quoted_body + assert_equal "something_with_underscores=\n", m.quoted_body + assert_equal expected, m.body + end +end diff --git a/vendor/rails/actionpack/CHANGELOG b/vendor/rails/actionpack/CHANGELOG new file mode 100644 index 00000000..be0d4064 --- /dev/null +++ b/vendor/rails/actionpack/CHANGELOG @@ -0,0 +1,2595 @@ +*1.12.5* (August 10th, 2006) + +* Updated security fix + + +*1.12.4* (August 8th, 2006) + +* Documentation fix: integration test scripts don't require integration_test. #4914 [Frederick Ros ] + +* ActionController::Base Summary documentation rewrite. #4900 [kevin.clark@gmail.com] + +* Fix text_helper.rb documentation rendering. #4725 [Frederick Ros] + +* Fixes bad rendering of JavaScriptMacrosHelper rdoc. #4910 [Frederick Ros] + +* Enhance documentation for setting headers in integration tests. Skip auto HTTP prepending when its already there. #4079 [Rick Olson] + +* Documentation for AbstractRequest. #4895 [kevin.clark@gmail.com] + +* Remove all remaining references to @params in the documentation. [Marcel Molina Jr.] + +* Add documentation for redirect_to :back's RedirectBackError exception. [Marcel Molina Jr.] + +* Update layout and content_for documentation to use yield rather than magic @content_for instance variables. [Marcel Molina Jr.] + +* Cache CgiRequest#request_parameters so that multiple calls don't re-parse multipart data. [Rick] + +* Fixed that remote_form_for can leave out the object parameter and default to the instance variable of the object_name, just like form_for [DHH] + +* Added ActionController.filter_parameter_logging that makes it easy to remove passwords, credit card numbers, and other sensitive information from being logged when a request is handled. #1897 [jeremye@bsa.ca.gov] + +* Fixed that real files and symlinks should be treated the same when compiling templates. #5438 [zachary@panandscan.com] + +* Add :status option to send_data and send_file. Defaults to '200 OK'. #5243 [Manfred Stienstra ] + +* Update documentation for erb trim syntax. #5651 [matt@mattmargolis.net] + +* Short documentation to mention use of Mime::Type.register. #5710 [choonkeat@gmail.com] + + +*1.12.3* (June 28th, 2006) + +* Fix broken traverse_to_controller. We now: + Look for a _controller.rb file under RAILS_ROOT to load. + If we find it, we require_dependency it and return the controller it defined. (If none was defined we stop looking.) + If we don't find it, we look for a .rb file under RAILS_ROOT to load. If we find it, and it loads a constant we keep looking. + Otherwise we check to see if a directory of the same name exists, and if it does we create a module for it. + + +*1.12.2* (June 27th, 2006) + +* Refinement to avoid exceptions in traverse_to_controller. + +* (Hackish) Fix loading of arbitrary files in Ruby's load path by traverse_to_controller. [Nicholas Seckar] + + +*1.12.1* (April 6th, 2006) + +* Fixed that template extensions would be cached development mode #4624 [Stefan Kaes] + +* Update to Prototype 1.5.0_rc0 [Sam Stephenson] + +* Honor skipping filters conditionally for only certain actions even when the parent class sets that filter to conditionally be executed only for the same actions. #4522 [Marcel Molina Jr.] + +* Delegate xml_http_request in integration tests to the session instance. [Jamis Buck] + +* Update the diagnostics template skip the useless '' text. [Nicholas Seckar] + +* CHANGED DEFAULT: Don't parse YAML input by default, but keep it available as an easy option [DHH] + +* Add additional autocompleter options [aballai, Thomas Fuchs] + +* Fixed fragment caching of binary data on Windows #4493 [bellis@deepthought.org] + +* Applied Prototype $() performance patches (#4465, #4477) and updated script.aculo.us [Sam Stephenson, Thomas Fuchs] + +* Added automated timestamping to AssetTagHelper methods for stylesheets, javascripts, and images when Action Controller is run under Rails [DHH]. Example: + + image_tag("rails.png") # => 'Rails' + + ...to avoid frequent stats (not a problem for most people), you can set RAILS_ASSET_ID in the ENV to avoid stats: + + ENV["RAILS_ASSET_ID"] = "2345" + image_tag("rails.png") # => 'Rails' + + This can be used by deployment managers to set the asset id by application revision + + +*1.12.0* (March 27th, 2006) + +* Add documentation for respond_to. [Jamis Buck] + +* Fixed require of bluecloth and redcloth when gems haven't been loaded #4446 [murphy@cYcnus.de] + +* Update to Prototype 1.5.0_pre1 [Sam Stephenson] + +* Change #form_for and #fields_for so that the second argument is not required [Dave Thomas] + + <% form_for :post, @post, :url => { :action => 'create' } do |f| -%> + + becomes... + + <% form_for :post, :url => { :action => 'create' } do |f| -%> + +* Update to script.aculo.us 1.6 [Thomas Fuchs] + +* Enable application/x-yaml processing by default [Jamis Buck] + +* Fix double url escaping of remote_function. Add :escape => false option to ActionView's url_for. [Nicholas Seckar] + +* Add :script option to in_place_editor to support evalScripts (closes #4194) [codyfauser@gmail.com] + +* Fix mixed case enumerable methods in the JavaScript Collection Proxy (closes #4314) [codyfauser@gmail.com] + +* Undo accidental escaping for mail_to; add regression test. [Nicholas Seckar] + +* Added nicer message for assert_redirected_to (closes #4294) [court3nay] + + assert_redirected_to :action => 'other_host', :only_path => false + + when it was expecting... + + redirected_to :action => 'other_host', :only_path => true, :host => 'other.test.host' + + gives the error message... + + response is not a redirection to all of the options supplied (redirection is <{:only_path=>false, :host=>"other.test.host", :action=>"other_host"}>), difference: <{:only_path=>"true", :host=>"other.test.host"}> + +* Change url_for to escape the resulting URLs when called from a view. [Nicholas Seckar, coffee2code] + +* Added easy support for testing file uploads with fixture_file_upload #4105 [turnip@turnipspatch.com]. Example: + + # Looks in Test::Unit::TestCase.fixture_path + '/files/spongebob.png' + post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png') + +* Fixed UrlHelper#current_page? to behave even when url-escaped entities are present #3929 [jeremy@planetargon.com] + +* Add ability for relative_url_root to be specified via an environment variable RAILS_RELATIVE_URL_ROOT. [isaac@reuben.com, Nicholas Seckar] + +* Fixed link_to "somewhere", :post => true to produce valid XHTML by using the parentnode instead of document.body for the instant form #3007 [Bob Silva] + +* Added :function option to PrototypeHelper#observe_field/observe_form that allows you to call a function instead of submitting an ajax call as the trigger #4268 [jonathan@daikini.com] + +* Make Mime::Type.parse consider q values (if any) [Jamis Buck] + +* XML-formatted requests are typecast according to "type" attributes for :xml_simple [Jamis Buck] + +* Added protection against proxy setups treating requests as local even when they're not #3898 [stephen_purcell@yahoo.com] + +* Added TestRequest#raw_post that simulate raw_post from CgiRequest #3042 [francois.beausoleil@gmail.com] + +* Underscore dasherized keys in formatted requests [Jamis Buck] + +* Add MimeResponds::Responder#any for managing multiple types with identical responses [Jamis Buck] + +* Make the xml_http_request testing method set the HTTP_ACCEPT header [Jamis Buck] + +* Add Verification to scaffolds. Prevent destructive actions using GET [Michael Koziarski] + +* Avoid hitting the filesystem when using layouts by using a File.directory? cache. [Stefan Kaes, Nicholas Seckar] + +* Simplify ActionController::Base#controller_path [Nicholas Seckar] + +* Added simple alert() notifications for RJS exceptions when config.action_view.debug_rjs = true. [Sam Stephenson] + +* Added :content_type option to render, so you can change the content type on the fly [DHH]. Example: render :action => "atom.rxml", :content_type => "application/atom+xml" + +* CHANGED DEFAULT: The default content type for .rxml is now application/xml instead of type/xml, see http://www.xml.com/pub/a/2004/07/21/dive.html for reason [DHH] + +* Added option to render action/template/file of a specific extension (and here by template type). This means you can have multiple templates with the same name but a different extension [DHH]. Example: + + class WeblogController < ActionController::Base + def index + @posts = Post.find :all + + respond_to do |type| + type.html # using defaults, which will render weblog/index.rhtml + type.xml { render :action => "index.rxml" } + type.js { render :action => "index.rjs" } + end + end + end + +* Added better support for using the same actions to output for different sources depending on the Accept header [DHH]. Example: + + class WeblogController < ActionController::Base + def create + @post = Post.create(params[:post]) + + respond_to do |type| + type.js { render } # renders create.rjs + type.html { redirect_to :action => "index" } + type.xml do + headers["Location"] = url_for(:action => "show", :id => @post) + render(:nothing, :status => "201 Created") + end + end + end + end + +* Added Base#render(:xml => xml) that works just like Base#render(:text => text), but sets the content-type to text/xml and the charset to UTF-8 [DHH] + +* Integration test's url_for now runs in the context of the last request (if any) so after post /products/show/1 url_for :action => 'new' will yield /product/new [Tobias Luetke] + +* Re-added mixed-in helper methods for the JavascriptGenerator. Moved JavascriptGenerators methods to a module that is mixed in after the helpers are added. Also fixed that variables set in the enumeration methods like #collect are set correctly. Documentation added for the enumeration methods [Rick Olson]. Examples: + + page.select('#items li').collect('items') do |element| + element.hide + end + # => var items = $$('#items li').collect(function(value, index) { return value.hide(); }); + +* Added plugin support for parameter parsers, which allows for better support for REST web services. By default, posts submitted with the application/xml content type is handled by creating a XmlSimple hash with the same name as the root element of the submitted xml. More handlers can easily be registered like this: + + # Assign a new param parser to a new content type + ActionController::Base.param_parsers['application/atom+xml'] = Proc.new do |data| + node = REXML::Document.new(post) + { node.root.name => node.root } + end + + # Assign the default XmlSimple to a new content type + ActionController::Base.param_parsers['application/backpack+xml'] = :xml_simple + +Default YAML web services were retired, ActionController::Base.param_parsers carries an example which shows how to get this functionality back. As part of this new plugin support, request.[formatted_post?, xml_post?, yaml_post? and post_format] were all deprecated in favor of request.content_type [Tobias Luetke] + +* Fixed Effect.Appear in effects.js to work with floats in Safari #3524, #3813, #3044 [Thomas Fuchs] + +* Fixed that default image extension was not appended when using a full URL with AssetTagHelper#image_tag #4032, #3728 [rubyonrails@beautifulpixel.com] + +* Added that page caching will only happen if the response code is less than 400 #4033 [g.bucher@teti.ch] + +* Add ActionController::IntegrationTest to allow high-level testing of the way the controllers and routes all work together [Jamis Buck] + +* Added support to AssetTagHelper#javascript_include_tag for having :defaults appear anywhere in the list, so you can now make one call ala javascript_include_tag(:defaults, "my_scripts") or javascript_include_tag("my_scripts", :defaults) depending on how you want the load order #3506 [Bob Silva] + +* Added support for visual effects scoped queues to the visual_effect helper #3530 [Abdur-Rahman Advany] + +* Added .rxml (and any non-rhtml template, really) supportfor CaptureHelper#content_for and CaptureHelper#capture #3287 [Brian Takita] + +* Added script.aculo.us drag and drop helpers to RJS [Thomas Fuchs]. Examples: + + page.draggable 'product-1' + page.drop_receiving 'wastebasket', :url => { :action => 'delete' } + page.sortable 'todolist', :url => { action => 'change_order' } + +* Fixed that form elements would strip the trailing [] from the first parameter #3545 [ruby@bobsilva.com] + +* During controller resolution, update the NameError suppression to check for the expected constant. [Nicholas Seckar] + +* Update script.aculo.us to V1.5.3 [Thomas Fuchs] + +* Added various InPlaceEditor options, #3746, #3891, #3896, #3906 [Bill Burcham, ruairi, sl33p3r] + +* Added :count option to pagination that'll make it possible for the ActiveRecord::Base.count call to using something else than * for the count. Especially important for count queries using DISTINCT #3839 [skaes] + +* Update script.aculo.us to V1.5.2 [Thomas Fuchs] + +* Added element and collection proxies to RJS [DHH]. Examples: + + page['blank_slate'] # => $('blank_slate'); + page['blank_slate'].show # => $('blank_slate').show(); + page['blank_slate'].show('first').up # => $('blank_slate').show('first').up(); + + page.select('p') # => $$('p'); + page.select('p.welcome b').first # => $$('p.welcome b').first(); + page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide(); + +* Add JavaScriptGenerator#replace for replacing an element's "outer HTML". #3246 [tom@craz8.com, Sam Stephenson] + +* Remove over-engineered form_for code for a leaner implementation. [Nicholas Seckar] + +* Document form_for's :html option. [Nicholas Seckar] + +* Major components cleanup and speedup. #3527 [Stefan Kaes] + +* Fix problems with pagination and :include. [Kevin Clark] + +* Add ActiveRecordTestCase for testing AR integration. [Kevin Clark] + +* Add Unit Tests for pagination [Kevin Clark] + +* Add :html option for specifying form tag options in form_for. [Sam Stephenson] + +* Replace dubious controller parent class in filter docs. #3655, #3722 [info@rhalff.com, eigentone@gmail.com] + +* Don't interpret the :value option on text_area as an html attribute. Set the text_area's value. #3752 [gabriel@gironda.org] + +* Fix remote_form_for creates a non-ajax form. [Rick Olson] + +* Don't let arbitrary classes match as controllers -- a potentially dangerous bug. [Nicholas Seckar] + +* Fix Routing tests. Fix routing where failing to match a controller would prevent the rest of routes from being attempted. [Nicholas Seckar] + +* Add :builder => option to form_for and friends. [Nicholas Seckar, Rick Olson] + +* Fix controller resolution to avoid accidentally inheriting a controller from a parent module. [Nicholas Seckar] + +* Set sweeper's @controller to nil after a request so that the controller may be collected between requests. [Nicholas Seckar] + +* Subclasses of ActionController::Caching::Sweeper should be Reloadable. [Rick Olson] + +* Document the :xhr option for verifications. #3666 [leeo] + +* Added :only and :except controls to skip_before/after_filter just like for when you add filters [DHH] + +* Ensure that the instance variables are copied to the template when performing render :update. [Nicholas Seckar] + +* Add the ability to call JavaScriptGenerator methods from helpers called in update blocks. [Sam Stephenson] Example: + module ApplicationHelper + def update_time + page.replace_html 'time', Time.now.to_s(:db) + page.visual_effect :highlight, 'time' + end + end + + class UserController < ApplicationController + def poll + render :update { |page| page.update_time } + end + end + +* Add render(:update) to ActionView::Base. [Sam Stephenson] + +* Fix render(:update) to not render layouts. [Sam Stephenson] + +* Fixed that SSL would not correctly be detected when running lighttpd/fcgi behind lighttpd w/mod_proxy #3548 [stephen_purcell@yahoo.com] + +* Added the possibility to specify atomatic expiration for the memcachd session container #3571 [Stefan Kaes] + +* Change layout discovery to take into account the change in semantics with File.join and nil arguments. [Marcel Molina Jr.] + +* Raise a RedirectBackError if redirect_to :back is called when there's no HTTP_REFERER defined #3049 [kevin.clark@gmail.com] + +* Treat timestamps like datetimes for scaffolding purposes #3388 [Maik Schmidt] + +* Fix IE bug with link_to "something", :post => true #3443 [Justin Palmer] + +* Extract Test::Unit::TestCase test process behavior into an ActionController::TestProcess module. [Sam Stephenson] + +* Pass along blocks from render_to_string to render. [Sam Stephenson] + +* Add render :update for inline RJS. [Sam Stephenson] Example: + class UserController < ApplicationController + def refresh + render :update do |page| + page.replace_html 'user_list', :partial => 'user', :collection => @users + page.visual_effect :highlight, 'user_list' + end + end + end + +* allow nil objects for error_messages_for [Michael Koziarski] + +* Refactor human_size to exclude decimal place if it is zero. [Marcel Molina Jr.] + +* Update to Prototype 1.5.0_pre0 [Sam Stephenson] + +* Automatically discover layouts when a controller is namespaced. #2199, #3424 [me@jonnii.com rails@jeffcole.net Marcel Molina Jr.] + +* Add support for multiple proxy servers to CgiRequest#host [gaetanot@comcast.net] + +* Documentation typo fix. #2367 [Blair Zajac] + +* Remove Upload Progress. #2871 [Sean Treadway] + +* Fix typo in function name mapping in auto_complete_field. #2929 #3446 [doppler@gmail.com phil.ross@gmail.com] + +* Allow auto-discovery of third party template library layouts. [Marcel Molina Jr.] + +* Have the form builder output radio button, not check box, when calling the radio button helper. #3331 [LouisStAmour@gmail.com] + +* Added assignment of the Autocompleter object created by JavaScriptMacroHelper#auto_complete_field to a local javascript variables [DHH] + +* Added :on option for PrototypeHelper#observe_field that allows you to specify a different callback hook to have the observer trigger on [DHH] + +* Added JavaScriptHelper#button_to_function that works just like JavaScriptHelper#link_to_function but uses a button instead of a href [DHH] + +* Added that JavaScriptHelper#link_to_function will honor existing :onclick definitions when adding the function call [DHH] + +* Added :disable_with option to FormTagHelper#submit_tag to allow for easily disabled submit buttons with different text [DHH] + +* Make auto_link handle nil by returning quickly if blank? [Scott Barron] + +* Make auto_link match urls with a port number specified. [Marcel Molina Jr.] + +* Added support for toggling visual effects to ScriptaculousHelper::visual_effect, #3323. [Thomas Fuchs] + +* Update to script.aculo.us to 1.5.0 rev. 3343 [Thomas Fuchs] + +* Added :select option for JavaScriptMacroHelper#auto_complete_field that makes it easier to only use part of the auto-complete suggestion as the value for insertion [Thomas Fuchs] + +* Added delayed execution of Javascript from within RJS #3264 [devslashnull@gmail.com]. Example: + + page.delay(20) do + page.visual_effect :fade, 'notice' + end + +* Add session ID to default logging, but remove the verbose description of every step [DHH] + +* Add the following RJS methods: [Sam Stephenson] + + * alert - Displays an alert() dialog + * redirect_to - Changes window.location.href to simulate a browser redirect + * call - Calls a JavaScript function + * assign - Assigns to a JavaScript variable + * << - Inserts an arbitrary JavaScript string + +* Fix incorrect documentation for form_for [Nicholas Seckar] + +* Don't include a layout when rendering an rjs template using render's :template option. [Marcel Molina Jr.] + +*1.1.2* (December 13th, 2005) + +* Become part of Rails 1.0 + +* Update to script.aculo.us 1.5.0 final (equals 1.5.0_rc6) [Thomas Fuchs] + +* Update to Prototype 1.4.0 final [Sam Stephenson] + +* Added form_remote_for (form_for meets form_remote_tag) [DHH] + +* Update to script.aculo.us 1.5.0_rc6 + +* More robust relative url root discovery for SCGI compatibility. This solves the 'SCGI routes problem' -- you no longer need to prefix all your routes with the name of the SCGI mountpoint. #3070 [Dave Ringoen] + +* Fix docs for text_area_tag. #3083. [Christopher Cotton] + +* Change form_for and fields_for method signatures to take object name and object as separate arguments rather than as a Hash. [DHH] + +* Introduce :selected option to the select helper. Allows you to specify a selection other than the current value of object.method. Specify :selected => nil to leave all options unselected. #2991 [Jonathan Viney ] + +* Initialize @optional in routing code to avoid warnings about uninitialized access to an instance variable. [Nicholas Seckar] + +* Make ActionController's render honor the :locals option when rendering a :file. #1665. [Emanuel Borsboom, Marcel Molina Jr.] + +* Allow assert_tag(:conditions) to match the empty string when a tag has no children. Closes #2959. [Jamis Buck] + +* Update html-scanner to handle CDATA sections better. Closes #2970. [Jamis Buck] + +* Don't put flash in session if sessions are disabled. [Jeremy Kemper] + +* Strip out trailing &_= for raw post bodies. Closes #2868. [Sam Stephenson] + +* Pass multiple arguments to Element.show and Element.hide in JavaScriptGenerator instead of using iterators. [Sam Stephenson] + +* Improve expire_fragment documentation. #2966 [court3nay@gmail.com] + +* Correct docs for automatic layout assignment. #2610. [Charles M. Gerungan] + +* Always create new AR sessions rather than trying too hard to avoid database traffic. #2731 [Jeremy Kemper] + +* Update to Prototype 1.4.0_rc4. Closes #2943 (old Array.prototype.reverse behavior can be obtained by passing false as an argument). [Sam Stephenson] + +* Use Element.update('id', 'html') instead of $('id').innerHTML = 'html' in JavaScriptGenerator#replace_html so that script tags are evaluated. [Sam Stephenson] + +* Make rjs templates always implicitly skip out on layouts. [Marcel Molina Jr.] + +* Correct length for the truncate text helper. #2913 [Stefan Kaes] + +* Update to Prototype 1.4.0_rc3. Closes #1893, #2505, #2550, #2748, #2783. [Sam Stephenson] + +* Add support for new rjs templates which wrap an update_page block. [Marcel Molina Jr.] + +* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.] + +* Correct time_zone_options_for_select docs. #2892 [pudeyo@rpi.com] + +* Remove the unused, slow response_dump and session_dump variables from error pages. #1222 [lmarlow@yahoo.com] + +* Performance tweaks: use Set instead of Array to speed up prototype helper include? calls. Avoid logging code if logger is nil. Inline commonly-called template presence checks. #2880, #2881, #2882, #2883 [Stefan Kaes] + +* MemCache store may be given multiple addresses. #2869 [Ryan Carver ] + +* Handle cookie parsing irregularity for certain Nokia phones. #2530 [zaitzow@gmail.com] + +* Added PrototypeHelper::JavaScriptGenerator and PrototypeHelper#update_page for easily modifying multiple elements in an Ajax response. [Sam Stephenson] Example: + + update_page do |page| + page.insert_html :bottom, 'list', '
  • Last item
  • ' + page.visual_effect :highlight, 'list' + page.hide 'status-indicator', 'cancel-link' + end + + generates the following JavaScript: + + new Insertion.Bottom("list", "
  • Last item
  • "); + new Effect.Highlight("list"); + ["status-indicator", "cancel-link"].each(Element.hide); + +* Refactored JavaScriptHelper into PrototypeHelper and ScriptaculousHelper [Sam Stephenson] + +* Update to latest script.aculo.us version (as of [3031]) + +* Updated docs for in_place_editor, fixes a couple bugs and offers extended support for external controls [Justin Palmer] + +* Update documentation for render :file. #2858 [Tom Werner] + +* Only include builtin filters whose filenames match /^[a-z][a-z_]*_helper.rb$/ to avoid including operating system metadata such as ._foo_helper.rb. #2855 [court3nay@gmail.com] + +* Added FormHelper#form_for and FormHelper#fields_for that makes it easier to work with forms for single objects also if they don't reside in instance variables [DHH]. Examples: + + <% form_for :person, @person, :url => { :action => "update" } do |f| %> + First name: <%= f.text_field :first_name %> + Last name : <%= f.text_field :last_name %> + Biography : <%= f.text_area :biography %> + Admin? : <%= f.check_box :admin %> + <% end %> + + <% form_for :person, person, :url => { :action => "update" } do |person_form| %> + First name: <%= person_form.text_field :first_name %> + Last name : <%= person_form.text_field :last_name %> + + <% fields_for :permission => person.permission do |permission_fields| %> + Admin? : <%= permission_fields.check_box :admin %> + <% end %> + <% end %> + +* options_for_select allows any objects which respond_to? :first and :last rather than restricting to Array and Range. #2824 [Jacob Robbins , Jeremy Kemper] + +* The auto_link text helper accepts an optional block to format the link text for each url and email address. Example: auto_link(post.body) { |text| truncate(text, 10) } [Jeremy Kemper] + +* assert_tag uses exact matches for string conditions, instead of partial matches. Use regex to do partial matches. #2799 [Jamis Buck] + +* CGI::Session::ActiveRecordStore.data_column_name = 'foobar' to use a different session data column than the 'data' default. [nbpwie102@sneakemail.com] + +* Do not raise an exception when default helper is missing; log a debug message instead. It's nice to delete empty helpers. [Jeremy Kemper] + +* Controllers with acronyms in their names (e.g. PDFController) require the correct default helper (PDFHelper in file pdf_helper.rb). #2262 [jeff@opendbms.com] + + +*1.11.0* (November 7th, 2005) + +* Added request as instance method to views, so you can do <%= request.env["HTTP_REFERER"] %>, just like you can already access response, session, and the likes [DHH] + +* Fix conflict with assert_tag and Glue gem #2255 [david.felstead@gmail.com] + +* Add documentation to assert_tag indicating that it only works with well-formed XHTML #1937, #2570 [Jamis Buck] + +* Added action_pack.rb stub so that ActionPack::Version loads properly [Sam Stephenson] + +* Added short-hand to assert_tag so assert_tag :tag => "span" can be written as assert_tag "span" [DHH] + +* Added skip_before_filter/skip_after_filter for easier control of the filter chain in inheritance hierachies [DHH]. Example: + + class ApplicationController < ActionController::Base + before_filter :authenticate + end + + class WeblogController < ApplicationController + # will run the :authenticate filter + end + + class SignupController < ActionController::Base + # will not run the :authenticate filter + skip_before_filter :authenticate + end + +* Added redirect_to :back as a short-hand for redirect_to(request.env["HTTP_REFERER"]) [DHH] + +* Change javascript_include_tag :defaults to not use script.aculo.us loader, which facilitates the use of plugins for future script.aculo.us and third party javascript extensions, and provide register_javascript_include_default for plugins to specify additional JavaScript files to load. Removed slider.js and builder.js from actionpack. [Thomas Fuchs] + +* Fix problem where redirecting components can cause an infinite loop [Rick Olson] + +* Added support for the queue option on visual_effect [Thomas Fuchs] + +* Update script.aculo.us to V1.5_rc4 [Thomas Fuchs] + +* Fix that render :text didn't interpolate instance variables #2629, #2626 [skaes] + +* Fix line number detection and escape RAILS_ROOT in backtrace Regexp [Nicholas Seckar] + +* Fixed document.getElementsByClassName from Prototype to be speedy again [Sam Stephenson] + +* Recognize ./#{RAILS_ROOT} as RAILS_ROOT in error traces [Nicholas Seckar] + +* Remove ARStore session fingerprinting [Nicholas Seckar] + +* Fix obscure bug in ARStore [Nicholas Seckar] + +* Added TextHelper#strip_tags for removing HTML tags from a string (using HTMLTokenizer) #2229 [marcin@junkheap.net] + +* Added a reader for flash.now, so it's possible to do stuff like flash.now[:alert] ||= 'New if not set' #2422 [Caio Chassot] + + +*1.10.2* (October 26th, 2005) + +* Reset template variables after using render_to_string [skaes@web.de] + +* Expose the session model backing CGI::Session + +* Abbreviate RAILS_ROOT in traces + + +*1.10.1* (October 19th, 2005) + +* Update error trace templates [Nicholas Seckar] + +* Stop showing generated routing code in application traces [Nicholas Seckar] + + +*1.10.0* (October 16th, 2005) + +* Make string-keys locals assigns optional. Add documentation describing depreciated state [skaes@web.de] + +* Improve line number detection for template errors [Nicholas Seckar] + +* Update/clean up documentation (rdoc) + +* Upgrade to Prototype 1.4.0_rc0 [Sam Stephenson] + +* Added assert_vaild. Reports the proper AR error messages as fail message when the passed record is invalid [Tobias Luetke] + +* Add temporary support for passing locals to render using string keys [Nicholas Seckar] + +* Clean up error pages by providing better backtraces [Nicholas Seckar] + +* Raise an exception if an attempt is made to insert more session data into the ActiveRecordStore data column than the column can hold. #2234. [justin@textdrive.com] + +* Removed references to assertions.rb from actionpack assert's backtraces. Makes error reports in functional unit tests much less noisy. [Tobias Luetke] + +* Updated and clarified documentation for JavaScriptHelper to be more concise about the various options for including the JavaScript libs. [Thomas Fuchs] + +* Hide "Retry with Breakpoint" button on error pages until feature is functional. [DHH] + +* Fix Request#host_with_port to use the standard port when Rails is behind a proxy. [Nicholas Seckar] + +* Escape query strings in the href attribute of URLs created by url_helper. #2333 [Michael Schuerig ] + +* Improved line number reporting for template errors [Nicholas Seckar] + +* Added :locals support for render :inline #2463 [mdabney@cavoksolutions.com] + +* Unset the X-Requested-With header when using the xhr wrapper in functional tests so that future requests aren't accidentally xhr'ed #2352 [me@julik.nl, Sam Stephenson] + +* Unescape paths before writing cache to file system. #1877. [Damien Pollet] + +* Wrap javascript_tag contents in a CDATA section and add a cdata_section method to TagHelper #1691 [Michael Schuerig, Sam Stephenson] + +* Misc doc fixes (typos/grammar/etc). #2445. [coffee2code] + +* Speed improvement for session_options. #2287. [skaes@web.de] + +* Make cacheing binary files friendly with Windows. #1975. [Rich Olson] + +* Convert boolean form options form the tag_helper. #809. [Michael Schuerig ] + +* Fixed that an instance variable with the same name as a partial should be implicitly passed as the partial :object #2269 [court3nay] + +* Update Prototype to V1.4.0_pre11, script.aculo.us to [2502] [Thomas Fuchs] + +* Make assert_tag :children count appropriately. Closes #2181. [jamie@bravenet.com] + +* Forced newer versions of RedCloth to use hard breaks [DHH] + +* Added new scriptaculous options for auto_complete_field #2343 [m.stienstra@fngtps.com] + +* Don't prepend the asset host if the string is already a fully-qualified URL + +* Updated to script.aculo.us V1.5.0_rc2 and Prototype to V1.4.0_pre7 [Thomas Fuchs] + +* Undo condition change made in [2345] to prevent normal parameters arriving as StringIO. + +* Tolerate consecutive delimiters in query parameters. #2295 [darashi@gmail.com] + +* Streamline render process, code cleaning. Closes #2294. [skae] + +* Keep flash after components are rendered. #2291 [Rick Olson, Scott] + +* Shorten IE file upload path to filename only to match other browsers. #1507 [court3nay@gmail.com] + +* Fix open/save dialog in IE not opening files send with send_file/send_data, #2279 [Thomas Fuchs] + +* Fixed that auto_discovery_link_tag couldn't take a string as the URL [DHH] + +* Fixed problem with send_file and WEBrick using stdout #1812 [DHH] + +* Optimized tag_options to not sort keys, which is no longer necessary when assert_dom_equal and friend is available #1995 [skae] + +* Added assert_dom_equal and assert_dom_not_equal to compare tags generated by the helpers in an order-indifferent manner #1995 [skae] + +* Fixed that Request#domain caused an exception if the domain header wasn't set in the original http request #1795 [Michael Koziarski] + +* Make the truncate() helper multi-byte safe (assuming $KCODE has been set to something other than "NONE") #2103 + +* Add routing tests from #1945 [ben@groovie.org] + +* Add a routing test case covering #2101 [Nicholas Seckar] + +* Cache relative_url_root for all webservers, not just Apache #2193 [skae] + +* Speed up cookie use by decreasing string copying #2194 [skae] + +* Fixed access to "Host" header with requests made by crappy old HTTP/1.0 clients #2124 [Marcel Molina] + +* Added easy assignment of fragment cache store through use of symbols for included stores (old way still works too) + + Before: + ActionController::Base.fragment_cache_store = + ActionController::Base::Caching::Fragments::FileStore.new("/path/to/cache/directory") + + After: + ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory" + +* Added ActionController::Base.session_store=, session_store, and session_options to make it easier to tweak the session options (instead of going straight to ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS) + +* Added TextHelper#cycle to cycle over an array of values on each hit (useful for alternating row colors etc) #2154 [dave-ml@dribin.org] + +* Ensure that request.path never returns nil. Closes #1675 [Nicholas Seckar] + +* Add ability to specify Route Regexps for controllers. Closes #1917. [Sebastian Kanthak] + +* Provide Named Route's hash methods as helper methods. Closes #1744. [Nicholas Seckar, Steve Purcell] + +* Added :multipart option to ActiveRecordHelper#form to make it possible to add file input fields #2034 [jstirk@oobleyboo.com] + +* Moved auto-completion and in-place editing into the Macros module and their helper counterparts into JavaScriptMacrosHelper + +* Added in-place editing support in the spirit of auto complete with ActionController::Base.in_place_edit_for, JavascriptHelper#in_place_editor_field, and Javascript support from script.aculo.us #2038 [Jon Tirsen] + +* Added :disabled option to all data selects that'll make the elements inaccessible for change #2167, #253 [eigentone] + +* Fixed that TextHelper#auto_link_urls would include punctuation in the links #2166, #1671 [eigentone] + +* Fixed that number_to_currency(1000, {:precision => 0})) should return "$1,000", instead of "$1,000." #2122 [sd@notso.net] + +* Allow link_to_remote to use any DOM-element as the parent of the form elements to be submitted #2137 [erik@ruby-lang.nl]. Example: + + + + + <%= link_to_remote 'Save', :update => "row023", + :submit => "row023", :url => {:action => 'save_row'} %> + + +* Fixed that render :partial would fail when :object was a Hash (due to backwards compatibility issues) #2148 [Sam Stephenson] + +* Fixed JavascriptHelper#auto_complete_for to only include unique items #2153 [Thomas Fuchs] + +* Fixed all AssetHelper methods to work with relative paths, such that javascript_include_tag('stdlib/standard') will look in /javascripts/stdlib/standard instead of '/stdlib/standard/' #1963 + +* Avoid extending view instance with helper modules each request. Closes #1979 + +* Performance improvements to CGI methods. Closes #1980 [Skaes] + +* Added :post option to UrlHelper#link_to that makes it possible to do POST requests through normal ahref links using Javascript + +* Fixed overwrite_params + +* Added ActionController::Base.benchmark and ActionController::Base.silence to allow for easy benchmarking and turning off the log + +* Updated vendor copy of html-scanner to support better xml parsing + +* Added :popup option to UrlHelper#link_to #1996 [gabriel.gironda@gmail.com]. Examples: + + link_to "Help", { :action => "help" }, :popup => true + link_to "Busy loop", { :action => "busy" }, :popup => ['new_window', 'height=300,width=600'] + +* Drop trailing \000 if present on RAW_POST_DATA (works around bug in Safari Ajax implementation) #918 + +* Fix observe_field to fall back to event-based observation if frequency <= 0 #1916 [michael@schubert.cx] + +* Allow use of the :with option for submit_to_remote #1936 [jon@instance-design.co.uk] + +* AbstractRequest#domain returns nil when host is an ip address #2012 [kevin.clark@gmail.com] + +* ActionController documentation update #2051 [fbeausoleil@ftml.net] + +* Yield @content_for_ variables to templates #2058 [Sam Stephenson] + +* Make rendering an empty partial collection behave like :nothing => true #2080 [Sam Stephenson] + +* Add option to specify the singular name used by pagination. + +* Use string key to obtain action value. Allows indifferent hashes to be disabled. + +* Added ActionView::Base.cache_template_loading back. + +* Rewrote compiled templates to decrease code complexity. Removed template load caching in favour of compiled caching. Fixed template error messages. [Nicholas Seckar] + +* Fix Routing to handle :some_param => nil better. [Nicholas Seckar, Luminas] + +* Add support for :include with pagination (subject to existing constraints for :include with :limit and :offset) #1478 [michael@schubert.cx] + +* Prevent the benchmark module from blowing up if a non-HTTP/1.1 request is processed + +* Added :use_short_month option to select_month helper to show month names as abbreviations + +* Make link_to escape the javascript in the confirm option #1964 [nicolas.pouillard@gmail.com] + +* Make assert_redirected_to properly check URL's passed as strings #1910 [Scott Barron] + +* Make sure :layout => false is always used when rendering inside a layout + +* Use raise instead of assert_not_nil in Test::Unit::TestCase#process to ensure that the test variables (controller, request, response) have been set + +* Make sure assigns are built for every request when testing #1866 + +* Allow remote_addr to be queried on TestRequest #1668 + +* Fixed bug when a partial render was passing a local with the same name as the partial + +* Improved performance of test app req/sec with ~10% refactoring the render method #1823 [Stefan Kaes] + +* Improved performance of test app req/sec with 5-30% through a series of Action Pack optimizations #1811 [Stefan Kaes] + +* Changed caching/expiration/hit to report using the DEBUG log level and errors to use the ERROR log level instead of both using INFO + +* Added support for per-action session management #1763 + +* Improved rendering speed on complicated templates by up to 100% (the more complex the templates, the higher the speedup) #1234 [Stephan Kaes]. This did necessasitate a change to the internals of ActionView#render_template that now has four parameters. Developers of custom view handlers (like Amrita) need to update for that. + +* Added options hash as third argument to FormHelper#input, so you can do input('person', 'zip', :size=>10) #1719 [jeremye@bsa.ca.gov] + +* Added Base#expires_in(seconds)/Base#expires_now to control HTTP content cache headers #1755 [Thomas Fuchs] + +* Fixed line number reporting for Builder template errors #1753 [piotr] + +* Fixed assert_routing so that testing controllers in modules works as expected [Nicholas Seckar, Rick Olson] + +* Fixed bug with :success/:failure callbacks for the JavaScriptHelper methods #1730 [court3nay/Thomas Fuchs] + +* Added named_route method to RouteSet instances so that RouteSet instance methods do not prevent certain names from being used. [Nicholas Seckar] + +* Fixed routes so that routes which do not specify :action in the path or in the requirements have a default of :action => 'index', In addition, fixed url generation so that :action => 'index' does not need to be provided for such urls. [Nicholas Seckar, Markjuh] + +* Worked around a Safari bug where it wouldn't pass headers through if the response was zero length by having render :nothing return ' ' instead of '' + +* Fixed Request#subdomains to handle "foo.foo.com" correctly + + +*1.9.1* (11 July, 2005) + +* Fixed that auto_complete_for didn't force the input string to lower case even as the db comparison was + +* Fixed that Action View should always use the included Builder, never attempt to require the gem, to ensure compatibility + +* Added that nil options are not included in tags, so tag("p", :ignore => nil) now returns

    not

    but that tag("p", :ignore => "") still includes it #1465 [michael@schuerig.de] + +* Fixed that UrlHelper#link_to_unless/link_to_if used html_escape on the name if no link was to be applied. This is unnecessary and breaks its use with images #1649 [joergd@pobox.com] + +* Improved error message for DoubleRenderError + +* Fixed routing to allow for testing of *path components #1650 [Nicholas Seckar] + +* Added :handle as an option to sortable_element to restrict the drag handle to a given class #1642 [thejohnny] + +* Added a bunch of script.aculo.us features #1644, #1677, #1695 [Thomas Fuchs] + * Effect.ScrollTo, to smoothly scroll the page to an element + * Better Firefox flickering handling on SlideUp/SlideDown + * Removed a possible memory leak in IE with draggables + * Added support for cancelling dragging my hitting ESC + * Added capability to remove draggables/droppables and redeclare sortables in dragdrop.js (this makes it possible to call sortable_element on the same element more than once, e.g. in AJAX returns that modify the sortable element. all current sortable 'stuff' on the element will be discarded and the sortable will be rebuilt) + * Always reset background color on Effect.Highlight; this make change backwards-compatibility, to be sure include style="background-color:(target-color)" on your elements or else elements will fall back to their CSS rules (which is a good thing in most circumstances) + * Removed circular references from element to prevent memory leaks (still not completely gone in IE) + * Changes to class extension in effects.js + * Make Effect.Highlight restore any previously set background color when finishing (makes effect work with CSS classes that set a background color) + * Fixed myriads of memory leaks in IE and Gecko-based browsers [David Zülke] + * Added incremental and local autocompleting and loads of documentation to controls.js [Ivan Krstic] + * Extended the auto_complete_field helper to accept tokens option + * Changed object extension mechanism to favor Object.extend to make script.aculo.us easily adaptable to support 3rd party libs like IE7.js [David Zülke] + +* Fixed that named routes didn't use the default values for action and possible other parameters #1534 [Nicholas Seckar] + +* Fixed JavascriptHelper#visual_effect to use camelize such that :blind_up will work #1639 [pelletierm@eastmedia.net] + +* Fixed that a SessionRestoreError was thrown if a model object was placed in the session that wasn't available to all controllers. This means that it's no longer necessary to use the 'model :post' work-around in ApplicationController to have a Post model in your session. + + +*1.9.0* (6 July, 2005) + +* Added logging of the request URI in the benchmark statement (makes it easy to grep for slow actions) + +* Added javascript_include_tag :defaults shortcut that'll include all the default javascripts included with Action Pack (prototype, effects, controls, dragdrop) + +* Cache several controller variables that are expensive to calculate #1229 [skaes@web.de] + +* The session class backing CGI::Session::ActiveRecordStore may be replaced with any class that duck-types with a subset of Active Record. See docs for details #1238 [skaes@web.de] + +* Fixed that hashes was not working properly when passed by GET to lighttpd #849 [Nicholas Seckar] + +* Fixed assert_template nil will be true when no template was rendered #1565 [maceywj@telus.net] + +* Added :prompt option to FormOptions#select (and the users of it, like FormOptions#select_country etc) to create "Please select" style descriptors #1181 [Michael Schuerig] + +* Added JavascriptHelper#update_element_function, which returns a Javascript function (or expression) that'll update a DOM element according to the options passed #933 [mortonda@dgrmm.net]. Examples: + + <%= update_element_function("products", :action => :insert, :position => :bottom, :content => "

    New product!

    ") %> + + <% update_element_function("products", :action => :replace, :binding => binding) do %> +

    Product 1

    +

    Product 2

    + <% end %> + +* Added :field_name option to DateHelper#select_(year|month|day) to deviate from the year/month/day defaults #1266 [Marcel Molina] + +* Added JavascriptHelper#draggable_element and JavascriptHelper#drop_receiving_element to facilitate easy dragging and dropping through the script.aculo.us libraries #1578 [Thomas Fuchs] + +* Added that UrlHelper#mail_to will now also encode the default link title #749 [f.svehla@gmail.com] + +* Removed the default option of wrap=virtual on FormHelper#text_area to ensure XHTML compatibility #1300 [thomas@columbus.rr.com] + +* Adds the ability to include XML CDATA tags using Builder #1563 [Josh Knowles]. Example: + + xml.cdata! "some text" # => + +* Added evaluation of + # + # javascript_include_tag "common.javascript", "/elsewhere/cools" # => + # + # + # + # javascript_include_tag :defaults # => + # + # + # ... + # *see below + # + # If there's an application.js file in your public/javascripts directory, + # javascript_include_tag :defaults will automatically include it. This file + # facilitates the inclusion of small snippets of JavaScript code, along the lines of + # controllers/application.rb and helpers/application_helper.rb. + def javascript_include_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } + + if sources.include?(:defaults) + sources = sources[0..(sources.index(:defaults))] + + @@javascript_default_sources.dup + + sources[(sources.index(:defaults) + 1)..sources.length] + + sources.delete(:defaults) + sources << "application" if defined?(RAILS_ROOT) && File.exists?("#{RAILS_ROOT}/public/javascripts/application.js") + end + + sources.collect { |source| + source = javascript_path(source) + content_tag("script", "", { "type" => "text/javascript", "src" => source }.merge(options)) + }.join("\n") + end + + # Register one or more additional JavaScript files to be included when + # + # javascript_include_tag :defaults + # + # is called. This method is intended to be called only from plugin initialization + # to register extra .js files the plugin installed in public/javascripts. + def self.register_javascript_include_default(*sources) + @@javascript_default_sources.concat(sources) + end + + def self.reset_javascript_include_default #:nodoc: + @@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup + end + + # Returns path to a stylesheet asset. Example: + # + # stylesheet_path "style" # => /stylesheets/style.css + def stylesheet_path(source) + compute_public_path(source, 'stylesheets', 'css') + end + + # Returns a css link tag per source given as argument. Examples: + # + # stylesheet_link_tag "style" # => + # + # + # stylesheet_link_tag "style", :media => "all" # => + # + # + # stylesheet_link_tag "random.styles", "/css/stylish" # => + # + # + def stylesheet_link_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } + sources.collect { |source| + source = stylesheet_path(source) + tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options)) + }.join("\n") + end + + # Returns path to an image asset. Example: + # + # The +src+ can be supplied as a... + # * full path, like "/my_images/image.gif" + # * file name, like "rss.gif", that gets expanded to "/images/rss.gif" + # * file name without extension, like "logo", that gets expanded to "/images/logo.png" + def image_path(source) + compute_public_path(source, 'images', 'png') + end + + # Returns an image tag converting the +options+ into html options on the tag, but with these special cases: + # + # * :alt - If no alt text is given, the file name part of the +src+ is used (capitalized and without the extension) + # * :size - Supplied as "XxY", so "30x45" becomes width="30" and height="45" + # + # The +src+ can be supplied as a... + # * full path, like "/my_images/image.gif" + # * file name, like "rss.gif", that gets expanded to "/images/rss.gif" + # * file name without extension, like "logo", that gets expanded to "/images/logo.png" + def image_tag(source, options = {}) + options.symbolize_keys! + + options[:src] = image_path(source) + options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize + + if options[:size] + options[:width], options[:height] = options[:size].split("x") + options.delete :size + end + + tag("img", options) + end + + private + def compute_public_path(source, dir, ext) + source = "/#{dir}/#{source}" unless source.first == "/" || source.include?(":") + source << ".#{ext}" unless source.split("/").last.include?(".") + source << '?' + rails_asset_id(source) if defined?(RAILS_ROOT) && %r{^[-a-z]+://} !~ source + source = "#{@controller.request.relative_url_root}#{source}" unless %r{^[-a-z]+://} =~ source + source = ActionController::Base.asset_host + source unless source.include?(":") + source + end + + def rails_asset_id(source) + ENV["RAILS_ASSET_ID"] || + File.mtime("#{RAILS_ROOT}/public/#{source}").to_i.to_s rescue "" + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/benchmark_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/benchmark_helper.rb new file mode 100644 index 00000000..1d53be51 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/benchmark_helper.rb @@ -0,0 +1,24 @@ +require 'benchmark' + +module ActionView + module Helpers + module BenchmarkHelper + # Measures the execution time of a block in a template and reports the result to the log. Example: + # + # <% benchmark "Notes section" do %> + # <%= expensive_notes_operation %> + # <% end %> + # + # Will add something like "Notes section (0.34523)" to the log. + # + # You may give an optional logger level as the second argument + # (:debug, :info, :warn, :error). The default is :info. + def benchmark(message = "Benchmarking", level = :info) + if @logger + real = Benchmark.realtime { yield } + @logger.send level, "#{message} (#{'%.5f' % real})" + end + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/cache_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/cache_helper.rb new file mode 100644 index 00000000..de2707ac --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/cache_helper.rb @@ -0,0 +1,10 @@ +module ActionView + module Helpers + # See ActionController::Caching::Fragments for usage instructions. + module CacheHelper + def cache(name = {}, &block) + @controller.cache_erb_fragment(block, name) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/capture_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/capture_helper.rb new file mode 100644 index 00000000..28b3e299 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/capture_helper.rb @@ -0,0 +1,128 @@ +module ActionView + module Helpers + # Capture lets you extract parts of code which + # can be used in other points of the template or even layout file. + # + # == Capturing a block into an instance variable + # + # <% @script = capture do %> + # [some html...] + # <% end %> + # + # == Add javascript to header using content_for + # + # content_for("name") is a wrapper for capture which will + # make the fragment available by name to a yielding layout or template. + # + # layout.rhtml: + # + # + # + # layout with js + # + # + # + # <%= yield %> + # + # + # + # view.rhtml + # + # This page shows an alert box! + # + # <% content_for("script") do %> + # alert('hello world') + # <% end %> + # + # Normal view text + module CaptureHelper + # Capture allows you to extract a part of the template into an + # instance variable. You can use this instance variable anywhere + # in your templates and even in your layout. + # + # Example of capture being used in a .rhtml page: + # + # <% @greeting = capture do %> + # Welcome To my shiny new web page! + # <% end %> + # + # Example of capture being used in a .rxml page: + # + # @greeting = capture do + # 'Welcome To my shiny new web page!' + # end + def capture(*args, &block) + # execute the block + begin + buffer = eval("_erbout", block.binding) + rescue + buffer = nil + end + + if buffer.nil? + capture_block(*args, &block) + else + capture_erb_with_buffer(buffer, *args, &block) + end + end + + # Calling content_for stores the block of markup for later use. + # Subsequently, you can make calls to it by name with yield + # in another template or in the layout. + # + # Example: + # + # <% content_for("header") do %> + # alert('hello world') + # <% end %> + # + # You can use yield :header anywhere in your templates. + # + # <%= yield :header %> + # + # NOTE: Beware that content_for is ignored in caches. So you shouldn't use it + # for elements that are going to be fragment cached. + # + # The deprecated way of accessing a content_for block was to use a instance variable + # named @@content_for_#{name_of_the_content_block}@. So <%= content_for('footer') %> + # would be avaiable as <%= @content_for_footer %>. The preferred notation now is + # <%= yield :footer %>. + def content_for(name, &block) + eval "@content_for_#{name} = (@content_for_#{name} || '') + capture(&block)" + end + + private + def capture_block(*args, &block) + block.call(*args) + end + + def capture_erb(*args, &block) + buffer = eval("_erbout", block.binding) + capture_erb_with_buffer(buffer, *args, &block) + end + + def capture_erb_with_buffer(buffer, *args, &block) + pos = buffer.length + block.call(*args) + + # extract the block + data = buffer[pos..-1] + + # replace it in the original with empty string + buffer[pos..-1] = '' + + data + end + + def erb_content_for(name, &block) + eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_erb(&block)" + end + + def block_content_for(name, &block) + eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_block(&block)" + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/date_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/date_helper.rb new file mode 100755 index 00000000..00400956 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/date_helper.rb @@ -0,0 +1,307 @@ +require "date" + +module ActionView + module Helpers + # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods + # share a number of common options that are as follows: + # + # * :prefix - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give + # birthday[month] instead of date[month] if passed to the select_month method. + # * :include_blank - set to true if it should be possible to set an empty date. + # * :discard_type - set to true if you want to discard the type part of the select name. If set to true, the select_month + # method would use simply "date" (which can be overwritten using :prefix) instead of "date[month]". + module DateHelper + DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX') + + # Reports the approximate distance in time between two Time objects or integers. + # For example, if the distance is 47 minutes, it'll return + # "about 1 hour". See the source for the complete wording list. + # + # Integers are interpreted as seconds. So, + # distance_of_time_in_words(50) returns "less than a minute". + # + # Set include_seconds to true if you want more detailed approximations if distance < 1 minute + def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false) + from_time = from_time.to_time if from_time.respond_to?(:to_time) + to_time = to_time.to_time if to_time.respond_to?(:to_time) + distance_in_minutes = (((to_time - from_time).abs)/60).round + distance_in_seconds = ((to_time - from_time).abs).round + + case distance_in_minutes + when 0..1 + return (distance_in_minutes==0) ? 'less than a minute' : '1 minute' unless include_seconds + case distance_in_seconds + when 0..5 then 'less than 5 seconds' + when 6..10 then 'less than 10 seconds' + when 11..20 then 'less than 20 seconds' + when 21..40 then 'half a minute' + when 41..59 then 'less than a minute' + else '1 minute' + end + + when 2..45 then "#{distance_in_minutes} minutes" + when 46..90 then 'about 1 hour' + when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours" + when 1441..2880 then '1 day' + else "#{(distance_in_minutes / 1440).round} days" + end + end + + # Like distance_of_time_in_words, but where to_time is fixed to Time.now. + def time_ago_in_words(from_time, include_seconds = false) + distance_of_time_in_words(from_time, Time.now, include_seconds) + end + + alias_method :distance_of_time_in_words_to_now, :time_ago_in_words + + # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by + # +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash, + # which accepts all the keys that each of the individual select builders do (like :use_month_numbers for select_month) as well as a range of + # discard options. The discard options are :discard_year, :discard_month and :discard_day. Set to true, they'll + # drop the respective select. Discarding the month select will also automatically discard the day select. It's also possible to explicitly + # set the order of the tags using the :order option with an array of symbols :year, :month and :day in + # the desired order. Symbols may be omitted and the respective select is not included. + # + # Passing :disabled => true as part of the +options+ will make elements inaccessible for change. + # + # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. + # + # Examples: + # + # date_select("post", "written_on") + # date_select("post", "written_on", :start_year => 1995) + # date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true, + # :discard_day => true, :include_blank => true) + # date_select("post", "written_on", :order => [:day, :month, :year]) + # date_select("user", "birthday", :order => [:month, :day]) + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + def date_select(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options) + end + + # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based + # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples: + # + # datetime_select("post", "written_on") + # datetime_select("post", "written_on", :start_year => 1995) + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + def datetime_select(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_datetime_select_tag(options) + end + + # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. + def select_date(date = Date.today, options = {}) + select_year(date, options) + select_month(date, options) + select_day(date, options) + end + + # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+. + def select_datetime(datetime = Time.now, options = {}) + select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) + + select_hour(datetime, options) + select_minute(datetime, options) + end + + # Returns a set of html select-tags (one for hour and minute) + def select_time(datetime = Time.now, options = {}) + h = select_hour(datetime, options) + select_minute(datetime, options) + (options[:include_seconds] ? select_second(datetime, options) : '') + end + + # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. + # The second can also be substituted for a second number. + # Override the field name using the :field_name option, 'second' by default. + def select_second(datetime, options = {}) + second_options = [] + + 0.upto(59) do |second| + second_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) == second) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'second', second_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. + # Also can return a select tag with options by minute_step from 0 through 59 with the 00 minute selected + # The minute can also be substituted for a minute number. + # Override the field name using the :field_name option, 'minute' by default. + def select_minute(datetime, options = {}) + minute_options = [] + + 0.step(59, options[:minute_step] || 1) do |minute| + minute_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'minute', minute_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. + # The hour can also be substituted for a hour number. + # Override the field name using the :field_name option, 'hour' by default. + def select_hour(datetime, options = {}) + hour_options = [] + + 0.upto(23) do |hour| + hour_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'hour', hour_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # Returns a select tag with options for each of the days 1 through 31 with the current day selected. + # The date can also be substituted for a hour number. + # Override the field name using the :field_name option, 'day' by default. + def select_day(date, options = {}) + day_options = [] + + 1.upto(31) do |day| + day_options << ((date && (date.kind_of?(Fixnum) ? date : date.day) == day) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'day', day_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # Returns a select tag with options for each of the months January through December with the current month selected. + # The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values + # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names -- + # set the :use_month_numbers key in +options+ to true for this to happen. If you want both numbers and names, + # set the :add_month_numbers key in +options+ to true. Examples: + # + # select_month(Date.today) # Will use keys like "January", "March" + # select_month(Date.today, :use_month_numbers => true) # Will use keys like "1", "3" + # select_month(Date.today, :add_month_numbers => true) # Will use keys like "1 - January", "3 - March" + # + # Override the field name using the :field_name option, 'month' by default. + # + # If you would prefer to show month names as abbreviations, set the + # :use_short_month key in +options+ to true. + def select_month(date, options = {}) + month_options = [] + month_names = options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES + + 1.upto(12) do |month_number| + month_name = if options[:use_month_numbers] + month_number + elsif options[:add_month_numbers] + month_number.to_s + ' - ' + month_names[month_number] + else + month_names[month_number] + end + + month_options << ((date && (date.kind_of?(Fixnum) ? date : date.month) == month_number) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'month', month_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius + # can be changed using the :start_year and :end_year keys in the +options+. Both ascending and descending year + # lists are supported by making :start_year less than or greater than :end_year. The date can also be + # substituted for a year given as a number. Example: + # + # select_year(Date.today, :start_year => 1992, :end_year => 2007) # ascending year values + # select_year(Date.today, :start_year => 2005, :end_year => 1900) # descending year values + # + # Override the field name using the :field_name option, 'year' by default. + def select_year(date, options = {}) + year_options = [] + y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year + + start_year, end_year = (options[:start_year] || y-5), (options[:end_year] || y+5) + step_val = start_year < end_year ? 1 : -1 + + start_year.step(end_year, step_val) do |year| + year_options << ((date && (date.kind_of?(Fixnum) ? date : date.year) == year) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'year', year_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + private + def select_html(type, options, prefix = nil, include_blank = false, discard_type = false, disabled = false) + select_html = %(\n" + end + + def leading_zero_on_single_digits(number) + number > 9 ? number : "0#{number}" + end + end + + class InstanceTag #:nodoc: + include DateHelper + + def to_date_select_tag(options = {}) + defaults = { :discard_type => true } + options = defaults.merge(options) + options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } + date = options[:include_blank] ? (value || 0) : (value || Date.today) + + date_select = '' + options[:order] = [:month, :year, :day] if options[:month_before_year] # For backwards compatibility + options[:order] ||= [:year, :month, :day] + + position = {:year => 1, :month => 2, :day => 3} + + discard = {} + discard[:year] = true if options[:discard_year] + discard[:month] = true if options[:discard_month] + discard[:day] = true if options[:discard_day] or options[:discard_month] + + options[:order].each do |param| + date_select << self.send("select_#{param}", date, options_with_prefix.call(position[param])) unless discard[param] + end + + date_select + end + + def to_datetime_select_tag(options = {}) + defaults = { :discard_type => true } + options = defaults.merge(options) + options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } + datetime = options[:include_blank] ? (value || nil) : (value || Time.now) + + datetime_select = select_year(datetime, options_with_prefix.call(1)) + datetime_select << select_month(datetime, options_with_prefix.call(2)) unless options[:discard_month] + datetime_select << select_day(datetime, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month] + datetime_select << ' — ' + select_hour(datetime, options_with_prefix.call(4)) unless options[:discard_hour] + datetime_select << ' : ' + select_minute(datetime, options_with_prefix.call(5)) unless options[:discard_minute] || options[:discard_hour] + + datetime_select + end + end + + class FormBuilder + def date_select(method, options = {}) + @template.date_select(@object_name, method, options.merge(:object => @object)) + end + + def datetime_select(method, options = {}) + @template.datetime_select(@object_name, method, options.merge(:object => @object)) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/debug_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/debug_helper.rb new file mode 100644 index 00000000..8baea6f4 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/debug_helper.rb @@ -0,0 +1,17 @@ +module ActionView + module Helpers + # Provides a set of methods for making it easier to locate problems. + module DebugHelper + # Returns a
    -tag set with the +object+ dumped by YAML. Very readable way to inspect an object.
    +      def debug(object)
    +        begin
    +          Marshal::dump(object)
    +          "
    #{h(object.to_yaml).gsub("  ", "  ")}
    " + rescue Object => e + # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback + "#{h(object.inspect)}" + end + end + end + end +end \ No newline at end of file diff --git a/vendor/rails/actionpack/lib/action_view/helpers/form_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/form_helper.rb new file mode 100644 index 00000000..7c8748d6 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/form_helper.rb @@ -0,0 +1,406 @@ +require 'cgi' +require File.dirname(__FILE__) + '/date_helper' +require File.dirname(__FILE__) + '/tag_helper' + +module ActionView + module Helpers + # Provides a set of methods for working with forms and especially forms related to objects assigned to the template. + # The following is an example of a complete form for a person object that works for both creates and updates built + # with all the form helpers. The @person object was assigned by an action on the controller: + #
    + # Name: + # <%= text_field "person", "name", "size" => 20 %> + # + # Password: + # <%= password_field "person", "password", "maxsize" => 20 %> + # + # Single?: + # <%= check_box "person", "single" %> + # + # Description: + # <%= text_area "person", "description", "cols" => 20 %> + # + # + #
    + # + # ...is compiled to: + # + #
    + # Name: + # + # + # Password: + # + # + # Single?: + # + # + # Description: + # + # + # + #
    + # + # If the object name contains square brackets the id for the object will be inserted. Example: + # + # <%= text_field "person[]", "name" %> + # + # ...becomes: + # + # + # + # If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial + # used by render_collection_of_partials, the "index" option may come in handy. Example: + # + # <%= text_field "person", "name", "index" => 1 %> + # + # becomes + # + # + # + # There's also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html, + # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html + module FormHelper + # Creates a form and a scope around a specific model object, which is then used as a base for questioning about + # values for the fields. Examples: + # + # <% form_for :person, @person, :url => { :action => "update" } do |f| %> + # First name: <%= f.text_field :first_name %> + # Last name : <%= f.text_field :last_name %> + # Biography : <%= f.text_area :biography %> + # Admin? : <%= f.check_box :admin %> + # <% end %> + # + # Worth noting is that the form_for tag is called in a ERb evaluation block, not a ERb output block. So that's <% %>, + # not <%= %>. Also worth noting is that the form_for yields a form_builder object, in this example as f, which emulates + # the API for the stand-alone FormHelper methods, but without the object name. So instead of text_field :person, :name, + # you get away with f.text_field :name. + # + # That in itself is a modest increase in comfort. The big news is that form_for allows us to more easily escape the instance + # variable convention, so while the stand-alone approach would require text_field :person, :name, :object => person + # to work with local variables instead of instance ones, the form_for calls remain the same. You simply declare once with + # :person, person and all subsequent field calls save :person and :object => person. + # + # Also note that form_for doesn't create an exclusive scope. It's still possible to use both the stand-alone FormHelper methods + # and methods from FormTagHelper. Example: + # + # <% form_for :person, @person, :url => { :action => "update" } do |f| %> + # First name: <%= f.text_field :first_name %> + # Last name : <%= f.text_field :last_name %> + # Biography : <%= text_area :person, :biography %> + # Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %> + # <% end %> + # + # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base. + # Like collection_select and datetime_select. + # + # Html attributes for the form tag can be given as :html => {...}. Example: + # + # <% form_for :person, @person, :html => {:id => 'person_form'} do |f| %> + # ... + # <% end %> + # + # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers, + # then use your custom builder like so: + # + # <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %> + # <%= f.text_field :first_name %> + # <%= f.text_field :last_name %> + # <%= text_area :person, :biography %> + # <%= check_box_tag "person[admin]", @person.company.admin? %> + # <% end %> + # + # In many cases you will want to wrap the above in another helper, such as: + # + # def labelled_form_for(name, object, options, &proc) + # form_for(name, object, options.merge(:builder => LabellingFormBuiler), &proc) + # end + # + def form_for(object_name, *args, &proc) + raise ArgumentError, "Missing block" unless block_given? + options = args.last.is_a?(Hash) ? args.pop : {} + concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding) + fields_for(object_name, *(args << options), &proc) + concat('', proc.binding) + end + + # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes + # fields_for suitable for specifying additional model objects in the same form. Example: + # + # <% form_for :person, @person, :url => { :action => "update" } do |person_form| %> + # First name: <%= person_form.text_field :first_name %> + # Last name : <%= person_form.text_field :last_name %> + # + # <% fields_for :permission, @person.permission do |permission_fields| %> + # Admin? : <%= permission_fields.check_box :admin %> + # <% end %> + # <% end %> + # + # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base. + # Like collection_select and datetime_select. + def fields_for(object_name, *args, &proc) + raise ArgumentError, "Missing block" unless block_given? + options = args.last.is_a?(Hash) ? args.pop : {} + object = args.first + yield((options[:builder] || FormBuilder).new(object_name, object, self, options, proc)) + end + + # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. + # + # Examples (call, result): + # text_field("post", "title", "size" => 20) + # + def text_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options) + end + + # Works just like text_field, but returns an input tag of the "password" type instead. + def password_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("password", options) + end + + # Works just like text_field, but returns an input tag of the "hidden" type instead. + def hidden_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("hidden", options) + end + + # Works just like text_field, but returns an input tag of the "file" type instead, which won't have a default value. + def file_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("file", options) + end + + # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) + # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. + # + # Example (call, result): + # text_area("post", "body", "cols" => 20, "rows" => 40) + # + def text_area(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_text_area_tag(options) + end + + # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that + # integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a + # hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+ + # is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything. + # We work around this problem by adding a hidden value with the same name as the checkbox. + # + # Example (call, result). Imagine that @post.validated? returns 1: + # check_box("post", "validated") + # + # + # + # Example (call, result). Imagine that @puppy.gooddog returns no: + # check_box("puppy", "gooddog", {}, "yes", "no") + # + # + def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0") + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value) + end + + # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the + # radio button will be checked. Additional options on the input tag can be passed as a + # hash with +options+. + # Example (call, result). Imagine that @post.category returns "rails": + # radio_button("post", "category", "rails") + # radio_button("post", "category", "java") + # + # + # + def radio_button(object_name, method, tag_value, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_radio_button_tag(tag_value, options) + end + end + + class InstanceTag #:nodoc: + include Helpers::TagHelper + + attr_reader :method_name, :object_name + + DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze unless const_defined?(:DEFAULT_FIELD_OPTIONS) + DEFAULT_RADIO_OPTIONS = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS) + DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS) + DEFAULT_DATE_OPTIONS = { :discard_type => true }.freeze unless const_defined?(:DEFAULT_DATE_OPTIONS) + + def initialize(object_name, method_name, template_object, local_binding = nil, object = nil) + @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup + @template_object, @local_binding = template_object, local_binding + @object = object + if @object_name.sub!(/\[\]$/,"") + @auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id_before_type_cast + end + end + + def to_input_field_tag(field_type, options = {}) + options = options.stringify_keys + options["size"] ||= options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] + options = DEFAULT_FIELD_OPTIONS.merge(options) + if field_type == "hidden" + options.delete("size") + end + options["type"] = field_type + options["value"] ||= value_before_type_cast unless field_type == "file" + add_default_name_and_id(options) + tag("input", options) + end + + def to_radio_button_tag(tag_value, options = {}) + options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys) + options["type"] = "radio" + options["value"] = tag_value + options["checked"] = "checked" if value.to_s == tag_value.to_s + pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase + options["id"] = @auto_index ? + "#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" : + "#{@object_name}_#{@method_name}_#{pretty_tag_value}" + add_default_name_and_id(options) + tag("input", options) + end + + def to_text_area_tag(options = {}) + options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys) + add_default_name_and_id(options) + content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast), options) + end + + def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") + options = options.stringify_keys + options["type"] = "checkbox" + options["value"] = checked_value + checked = case value + when TrueClass, FalseClass + value + when NilClass + false + when Integer + value != 0 + when String + value == checked_value + else + value.to_i != 0 + end + if checked || options["checked"] == "checked" + options["checked"] = "checked" + else + options.delete("checked") + end + add_default_name_and_id(options) + tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value) + end + + def to_date_tag() + defaults = DEFAULT_DATE_OPTIONS.dup + date = value || Date.today + options = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } + html_day_select(date, options.call(3)) + + html_month_select(date, options.call(2)) + + html_year_select(date, options.call(1)) + end + + def to_boolean_select_tag(options = {}) + options = options.stringify_keys + add_default_name_and_id(options) + tag_text = "" + end + + def to_content_tag(tag_name, options = {}) + content_tag(tag_name, value, options) + end + + def object + @object || @template_object.instance_variable_get("@#{@object_name}") + end + + def value + unless object.nil? + object.send(@method_name) + end + end + + def value_before_type_cast + unless object.nil? + object.respond_to?(@method_name + "_before_type_cast") ? + object.send(@method_name + "_before_type_cast") : + object.send(@method_name) + end + end + + private + def add_default_name_and_id(options) + if options.has_key?("index") + options["name"] ||= tag_name_with_index(options["index"]) + options["id"] ||= tag_id_with_index(options["index"]) + options.delete("index") + elsif @auto_index + options["name"] ||= tag_name_with_index(@auto_index) + options["id"] ||= tag_id_with_index(@auto_index) + else + options["name"] ||= tag_name + options["id"] ||= tag_id + end + end + + def tag_name + "#{@object_name}[#{@method_name}]" + end + + def tag_name_with_index(index) + "#{@object_name}[#{index}][#{@method_name}]" + end + + def tag_id + "#{@object_name}_#{@method_name}" + end + + def tag_id_with_index(index) + "#{@object_name}_#{index}_#{@method_name}" + end + end + + class FormBuilder #:nodoc: + # The methods which wrap a form helper call. + class_inheritable_accessor :field_helpers + self.field_helpers = (FormHelper.instance_methods - ['form_for']) + + attr_accessor :object_name, :object + + def initialize(object_name, object, template, options, proc) + @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc + end + + (field_helpers - %w(check_box radio_button)).each do |selector| + src = <<-end_src + def #{selector}(method, options = {}) + @template.send(#{selector.inspect}, @object_name, method, options.merge(:object => @object)) + end + end_src + class_eval src, __FILE__, __LINE__ + end + + def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") + @template.check_box(@object_name, method, options.merge(:object => @object), checked_value, unchecked_value) + end + + def radio_button(method, tag_value, options = {}) + @template.radio_button(@object_name, method, tag_value, options.merge(:object => @object)) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/form_options_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/form_options_helper.rb new file mode 100644 index 00000000..53b39305 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -0,0 +1,361 @@ +require 'cgi' +require 'erb' +require File.dirname(__FILE__) + '/form_helper' + +module ActionView + module Helpers + # Provides a number of methods for turning different kinds of containers into a set of option tags. + # == Options + # The collection_select, country_select, select, + # and time_zone_select methods take an options parameter, + # a hash. + # + # * :include_blank - set to true if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. For example, + # + # select("post", "category", Post::CATEGORIES, {:include_blank => true}) + # + # could become: + # + # + # + # * :prompt - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. + # + # Another common case is a select tag for an belongs_to-associated object. For example, + # + # select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] }) + # + # could become: + # + # + module FormOptionsHelper + include ERB::Util + + # Create a select tag and a series of contained option tags for the provided object and method. + # The option currently held by the object will be selected, provided that the object is available. + # See options_for_select for the required format of the choices parameter. + # + # Example with @post.person_id => 1: + # select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] }, { :include_blank => true }) + # + # could become: + # + # + # + # This can be used to provide a default set of options in the standard way: before rendering the create form, a + # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved + # to the database. Instead, a second model object is created when the create request is received. + # This allows the user to submit a form page more than once with the expected results of creating multiple records. + # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms. + # + # By default, post.person_id is the selected option. Specify :selected => value to use a different selection + # or :selected => nil to leave all options unselected. + def select(object, method, choices, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_select_tag(choices, options, html_options) + end + + # Return select and option tags for the given object and method using options_from_collection_for_select to generate the list of option tags. + def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options) + end + + # Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags. + def country_select(object, method, priority_countries = nil, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options) + end + + # Return select and option tags for the given object and method, using + # #time_zone_options_for_select to generate the list of option tags. + # + # In addition to the :include_blank option documented above, + # this method also supports a :model option, which defaults + # to TimeZone. This may be used by users to specify a different time + # zone model object. (See #time_zone_options_for_select for more + # information.) + def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options) + end + + # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container + # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and + # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values + # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +Selected+ + # may also be an array of values to be selected when using a multiple select. + # + # Examples (call, result): + # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]]) + # \n + # + # options_for_select([ "VISA", "MasterCard" ], "MasterCard") + # \n + # + # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40") + # \n + # + # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"]) + # \n\n + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def options_for_select(container, selected = nil) + container = container.to_a if Hash === container + + options_for_select = container.inject([]) do |options, element| + if !element.is_a?(String) and element.respond_to?(:first) and element.respond_to?(:last) + is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element.last) : element.last == selected) ) + is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element.last) : element.last == selected) ) + if is_selected + options << "" + else + options << "" + end + else + is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element) : element == selected) ) + is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element) : element == selected) ) + options << ((is_selected) ? "" : "") + end + end + + options_for_select.join("\n") + end + + # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the + # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text. + # If +selected_value+ is specified, the element returning a match on +value_method+ will get the selected option tag. + # + # Example (call, result). Imagine a loop iterating over each +person+ in @project.people to generate an input tag: + # options_from_collection_for_select(@project.people, "id", "name") + # + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def options_from_collection_for_select(collection, value_method, text_method, selected_value = nil) + options_for_select( + collection.inject([]) { |options, object| options << [ object.send(text_method), object.send(value_method) ] }, + selected_value + ) + end + + # Returns a string of option tags, like options_from_collection_for_select, but surrounds them with tags. + # + # An array of group objects are passed. Each group should return an array of options when calling group_method + # Each group should return its name when calling group_label_method. + # + # html_option_groups_from_collection(@continents, "countries", "continent_name", "country_id", "country_name", @selected_country.id) + # + # Could become: + # + # + # + # ... + # + # + # + # + # + # ... + # + # + # with objects of the following classes: + # class Continent + # def initialize(p_name, p_countries) @continent_name = p_name; @countries = p_countries; end + # def continent_name() @continent_name; end + # def countries() @countries; end + # end + # class Country + # def initialize(id, name) @id = id; @name = name end + # def country_id() @id; end + # def country_name() @name; end + # end + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def option_groups_from_collection_for_select(collection, group_method, group_label_method, + option_key_method, option_value_method, selected_key = nil) + collection.inject("") do |options_for_select, group| + group_label_string = eval("group.#{group_label_method}") + options_for_select += "" + options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) + options_for_select += '' + end + end + + # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to + # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so + # that they will be listed above the rest of the (long) list. + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def country_options_for_select(selected = nil, priority_countries = nil) + country_options = "" + + if priority_countries + country_options += options_for_select(priority_countries, selected) + country_options += "\n" + end + + if priority_countries && priority_countries.include?(selected) + country_options += options_for_select(COUNTRIES - priority_countries, selected) + else + country_options += options_for_select(COUNTRIES, selected) + end + + return country_options + end + + # Returns a string of option tags for pretty much any time zone in the + # world. Supply a TimeZone name as +selected+ to have it marked as the + # selected option tag. You can also supply an array of TimeZone objects + # as +priority_zones+, so that they will be listed above the rest of the + # (long) list. (You can use TimeZone.us_zones as a convenience for + # obtaining a list of the US time zones.) + # + # The +selected+ parameter must be either +nil+, or a string that names + # a TimeZone. + # + # By default, +model+ is the TimeZone constant (which can be obtained + # in ActiveRecord as a value object). The only requirement is that the + # +model+ parameter be an object that responds to #all, and returns + # an array of objects that represent time zones. + # + # NOTE: Only the option tags are returned, you have to wrap this call in + # a regular HTML select tag. + def time_zone_options_for_select(selected = nil, priority_zones = nil, model = TimeZone) + zone_options = "" + + zones = model.all + convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } } + + if priority_zones + zone_options += options_for_select(convert_zones[priority_zones], selected) + zone_options += "\n" + + zones = zones.reject { |z| priority_zones.include?( z ) } + end + + zone_options += options_for_select(convert_zones[zones], selected) + zone_options + end + + private + # All the countries included in the country_options output. + COUNTRIES = [ "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", + "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", + "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", + "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", + "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", + "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burma", "Burundi", "Cambodia", + "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", + "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", + "Comoros", "Congo", "Congo, the Democratic Republic of the", "Cook Islands", + "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark", + "Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt", + "El Salvador", "England", "Equatorial Guinea", "Eritrea", "Espana", "Estonia", + "Ethiopia", "Falkland Islands", "Faroe Islands", "Fiji", "Finland", "France", + "French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia", + "Georgia", "Germany", "Ghana", "Gibraltar", "Great Britain", "Greece", "Greenland", + "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", + "Haiti", "Heard and Mc Donald Islands", "Honduras", "Hong Kong", "Hungary", "Iceland", + "India", "Indonesia", "Ireland", "Israel", "Italy", "Iran", "Iraq", "Jamaica", "Japan", "Jordan", + "Kazakhstan", "Kenya", "Kiribati", "Korea, Republic of", "Korea (South)", "Kuwait", + "Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", + "Liberia", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia", + "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", + "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", + "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", + "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", + "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", + "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Ireland", + "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Panama", + "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", + "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russia", "Rwanda", + "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", + "Samoa (Independent)", "San Marino", "Sao Tome and Principe", "Saudi Arabia", + "Scotland", "Senegal", "Serbia and Montenegro", "Seychelles", "Sierra Leone", "Singapore", + "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", + "South Georgia and the South Sandwich Islands", "South Korea", "Spain", "Sri Lanka", + "St. Helena", "St. Pierre and Miquelon", "Suriname", "Svalbard and Jan Mayen Islands", + "Swaziland", "Sweden", "Switzerland", "Taiwan", "Tajikistan", "Tanzania", "Thailand", + "Togo", "Tokelau", "Tonga", "Trinidad", "Trinidad and Tobago", "Tunisia", "Turkey", + "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", + "United Arab Emirates", "United Kingdom", "United States", + "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", + "Vatican City State (Holy See)", "Venezuela", "Viet Nam", "Virgin Islands (British)", + "Virgin Islands (U.S.)", "Wales", "Wallis and Futuna Islands", "Western Sahara", + "Yemen", "Zambia", "Zimbabwe" ] unless const_defined?("COUNTRIES") + end + + class InstanceTag #:nodoc: + include FormOptionsHelper + + def to_select_tag(choices, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + selected_value = options.has_key?(:selected) ? options[:selected] : value + content_tag("select", add_options(options_for_select(choices, selected_value), options, value), html_options) + end + + def to_collection_select_tag(collection, value_method, text_method, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + content_tag( + "select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options + ) + end + + def to_country_select_tag(priority_countries, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + content_tag("select", add_options(country_options_for_select(value, priority_countries), options, value), html_options) + end + + def to_time_zone_select_tag(priority_zones, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + content_tag("select", + add_options( + time_zone_options_for_select(value, priority_zones, options[:model] || TimeZone), + options, value + ), html_options + ) + end + + private + def add_options(option_tags, options, value = nil) + option_tags = "\n" + option_tags if options[:include_blank] + + if value.blank? && options[:prompt] + ("\n") + option_tags + else + option_tags + end + end + end + + class FormBuilder + def select(method, choices, options = {}, html_options = {}) + @template.select(@object_name, method, choices, options.merge(:object => @object), html_options) + end + + def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) + @template.collection_select(@object_name, method, collection, value_method, text_method, options.merge(:object => @object), html_options) + end + + def country_select(method, priority_countries = nil, options = {}, html_options = {}) + @template.country_select(@object_name, method, priority_countries, options.merge(:object => @object), html_options) + end + + def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) + @template.time_zone_select(@object_name, method, priority_zones, options.merge(:object => @object), html_options) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/form_tag_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/form_tag_helper.rb new file mode 100644 index 00000000..c7a5d1bb --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -0,0 +1,138 @@ +require 'cgi' +require File.dirname(__FILE__) + '/tag_helper' + +module ActionView + module Helpers + # Provides a number of methods for creating form tags that doesn't rely on conventions with an object assigned to the template like + # FormHelper does. With the FormTagHelper, you provide the names and values yourself. + # + # NOTE: The html options disabled, readonly, and multiple can all be treated as booleans. So specifying :disabled => true + # will give disabled="disabled". + module FormTagHelper + # Starts a form tag that points the action to an url configured with url_for_options just like + # ActionController::Base#url_for. The method for the form defaults to POST. + # + # Options: + # * :multipart - If set to true, the enctype is set to "multipart/form-data". + # * :method - The method to use when submitting the form, usually either "get" or "post". + def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &proc) + html_options = { "method" => "post" }.merge(options.stringify_keys) + html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") + html_options["action"] = url_for(url_for_options, *parameters_for_url) + tag :form, html_options, true + end + + alias_method :start_form_tag, :form_tag + + # Outputs "" + def end_form_tag + "" + end + + # Creates a dropdown selection box, or if the :multiple option is set to true, a multiple + # choice selection box. + # + # Helpers::FormOptions can be used to create common select boxes such as countries, time zones, or + # associated records. + # + # option_tags is a string containing the option tags for the select box: + # # Outputs + # select_tag "people", "" + # + # Options: + # * :multiple - If set to true the selection will allow multiple choices. + def select_tag(name, option_tags = nil, options = {}) + content_tag :select, option_tags, { "name" => name, "id" => name }.update(options.stringify_keys) + end + + # Creates a standard text field. + # + # Options: + # * :disabled - If set to true, the user will not be able to use this input. + # * :size - The number of visible characters that will fit in the input. + # * :maxlength - The maximum number of characters that the browser will allow the user to enter. + # + # A hash of standard HTML options for the tag. + def text_field_tag(name, value = nil, options = {}) + tag :input, { "type" => "text", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) + end + + # Creates a hidden field. + # + # Takes the same options as text_field_tag + def hidden_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "hidden")) + end + + # Creates a file upload field. + # + # If you are using file uploads then you will also need to set the multipart option for the form: + # <%= form_tag { :action => "post" }, { :multipart => true } %> + # <%= file_field_tag "file" %> + # <%= submit_tag %> + # <%= end_form_tag %> + # + # The specified URL will then be passed a File object containing the selected file, or if the field + # was left blank, a StringIO object. + def file_field_tag(name, options = {}) + text_field_tag(name, nil, options.update("type" => "file")) + end + + # Creates a password field. + # + # Takes the same options as text_field_tag + def password_field_tag(name = "password", value = nil, options = {}) + text_field_tag(name, value, options.update("type" => "password")) + end + + # Creates a text input area. + # + # Options: + # * :size - A string specifying the dimensions of the textarea. + # # Outputs + # <%= text_area_tag "body", nil, :size => "25x10" %> + def text_area_tag(name, content = nil, options = {}) + options.stringify_keys! + + if size = options.delete("size") + options["cols"], options["rows"] = size.split("x") + end + + content_tag :textarea, content, { "name" => name, "id" => name }.update(options.stringify_keys) + end + + # Creates a check box. + def check_box_tag(name, value = "1", checked = false, options = {}) + html_options = { "type" => "checkbox", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) + html_options["checked"] = "checked" if checked + tag :input, html_options + end + + # Creates a radio button. + def radio_button_tag(name, value, checked = false, options = {}) + html_options = { "type" => "radio", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) + html_options["checked"] = "checked" if checked + tag :input, html_options + end + + # Creates a submit button with the text value as the caption. If options contains a pair with the key of "disable_with", + # then the value will be used to rename a disabled version of the submit button. + def submit_tag(value = "Save changes", options = {}) + options.stringify_keys! + + if disable_with = options.delete("disable_with") + options["onclick"] = "this.disabled=true;this.value='#{disable_with}';this.form.submit();#{options["onclick"]}" + end + + tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys) + end + + # Displays an image which when clicked will submit the form. + # + # source is passed to AssetTagHelper#image_path + def image_submit_tag(source, options = {}) + tag :input, { "type" => "image", "src" => image_path(source) }.update(options.stringify_keys) + end + end + end +end diff --git a/vendor/rails/actionpack/lib/action_view/helpers/java_script_macros_helper.rb b/vendor/rails/actionpack/lib/action_view/helpers/java_script_macros_helper.rb new file mode 100644 index 00000000..2cb64c95 --- /dev/null +++ b/vendor/rails/actionpack/lib/action_view/helpers/java_script_macros_helper.rb @@ -0,0 +1,220 @@ +require File.dirname(__FILE__) + '/tag_helper' + +module ActionView + module Helpers + # Provides a set of helpers for creating JavaScript macros that rely on and often bundle methods from JavaScriptHelper into + # larger units. These macros also rely on counterparts in the controller that provide them with their backing. The in-place + # editing relies on ActionController::Base.in_place_edit_for and the autocompletion relies on + # ActionController::Base.auto_complete_for. + module JavaScriptMacrosHelper + # Makes an HTML element specified by the DOM ID +field_id+ become an in-place + # editor of a property. + # + # A form is automatically created and displayed when the user clicks the element, + # something like this: + #
    + # + # + # cancel + #
    + # + # The form is serialized and sent to the server using an AJAX call, the action on + # the server should process the value and return the updated value in the body of + # the reponse. The element will automatically be updated with the changed value + # (as returned from the server). + # + # Required +options+ are: + # :url:: Specifies the url where the updated value should + # be sent after the user presses "ok". + # + # + # Addtional +options+ are: + # :rows:: Number of rows (more than 1 will use a TEXTAREA) + # :cols:: Number of characters the text input should span (works for both INPUT and TEXTAREA) + # :size:: Synonym for :cols when using a single line text input. + # :cancel_text:: The text on the cancel link. (default: "cancel") + # :save_text:: The text on the save link. (default: "ok") + # :loading_text:: The text to display when submitting to the server (default: "Saving...") + # :external_control:: The id of an external control used to enter edit mode. + # :load_text_url:: URL where initial value of editor (content) is retrieved. + # :options:: Pass through options to the AJAX call (see prototype's Ajax.Updater) + # :with:: JavaScript snippet that should return what is to be sent + # in the AJAX call, +form+ is an implicit parameter + # :script:: Instructs the in-place editor to evaluate the remote JavaScript response (default: false) + def in_place_editor(field_id, options = {}) + function = "new Ajax.InPlaceEditor(" + function << "'#{field_id}', " + function << "'#{url_for(options[:url])}'" + + js_options = {} + js_options['cancelText'] = %('#{options[:cancel_text]}') if options[:cancel_text] + js_options['okText'] = %('#{options[:save_text]}') if options[:save_text] + js_options['loadingText'] = %('#{options[:loading_text]}') if options[:loading_text] + js_options['rows'] = options[:rows] if options[:rows] + js_options['cols'] = options[:cols] if options[:cols] + js_options['size'] = options[:size] if options[:size] + js_options['externalControl'] = "'#{options[:external_control]}'" if options[:external_control] + js_options['loadTextURL'] = "'#{url_for(options[:load_text_url])}'" if options[:load_text_url] + js_options['ajaxOptions'] = options[:options] if options[:options] + js_options['evalScripts'] = options[:script] if options[:script] + js_options['callback'] = "function(form) { return #{options[:with]} }" if options[:with] + function << (', ' + options_for_javascript(js_options)) unless js_options.empty? + + function << ')' + + javascript_tag(function) + end + + # Renders the value of the specified object and method with in-place editing capabilities. + # + # See the RDoc on ActionController::InPlaceEditing to learn more about this. + def in_place_editor_field(object, method, tag_options = {}, in_place_editor_options = {}) + tag = ::ActionView::Helpers::InstanceTag.new(object, method, self) + tag_options = {:tag => "span", :id => "#{object}_#{method}_#{tag.object.id}_in_place_editor", :class => "in_place_editor_field"}.merge!(tag_options) + in_place_editor_options[:url] = in_place_editor_options[:url] || url_for({ :action => "set_#{object}_#{method}", :id => tag.object.id }) + tag.to_content_tag(tag_options.delete(:tag), tag_options) + + in_place_editor(tag_options[:id], in_place_editor_options) + end + + # Adds AJAX autocomplete functionality to the text input field with the + # DOM ID specified by +field_id+. + # + # This function expects that the called action returns a HTML