]> sipb.mit.edu Git - ikiwiki.git/commitdiff
Merge branch 'master'
authorJoey Hess <joey@gnu.kitenet.net>
Sat, 4 Apr 2009 21:27:48 +0000 (17:27 -0400)
committerJoey Hess <joey@gnu.kitenet.net>
Sat, 4 Apr 2009 21:27:48 +0000 (17:27 -0400)
Conflicts:
doc/ikiwiki-makerepo.mdwn

544 files changed:
Bundle/IkiWiki.pm
Bundle/IkiWiki/Extras.pm
IkiWiki.pm
IkiWiki/CGI.pm
IkiWiki/Plugin/404.pm [new file with mode: 0644]
IkiWiki/Plugin/aggregate.pm
IkiWiki/Plugin/amazon_s3.pm
IkiWiki/Plugin/anonok.pm
IkiWiki/Plugin/attachment.pm
IkiWiki/Plugin/autoindex.pm
IkiWiki/Plugin/blogspam.pm [new file with mode: 0644]
IkiWiki/Plugin/brokenlinks.pm
IkiWiki/Plugin/bzr.pm
IkiWiki/Plugin/calendar.pm
IkiWiki/Plugin/camelcase.pm
IkiWiki/Plugin/color.pm
IkiWiki/Plugin/comments.pm [new file with mode: 0644]
IkiWiki/Plugin/conditional.pm
IkiWiki/Plugin/creole.pm
IkiWiki/Plugin/cutpaste.pm
IkiWiki/Plugin/ddate.pm
IkiWiki/Plugin/editdiff.pm
IkiWiki/Plugin/editpage.pm
IkiWiki/Plugin/edittemplate.pm
IkiWiki/Plugin/embed.pm
IkiWiki/Plugin/external.pm
IkiWiki/Plugin/favicon.pm
IkiWiki/Plugin/filecheck.pm
IkiWiki/Plugin/format.pm [new file with mode: 0644]
IkiWiki/Plugin/fortune.pm
IkiWiki/Plugin/git.pm
IkiWiki/Plugin/goodstuff.pm
IkiWiki/Plugin/google.pm
IkiWiki/Plugin/googlecalendar.pm [deleted file]
IkiWiki/Plugin/goto.pm [new file with mode: 0644]
IkiWiki/Plugin/graphviz.pm
IkiWiki/Plugin/haiku.pm
IkiWiki/Plugin/hnb.pm
IkiWiki/Plugin/html.pm
IkiWiki/Plugin/htmlbalance.pm [new file with mode: 0644]
IkiWiki/Plugin/htmlscrubber.pm
IkiWiki/Plugin/htmltidy.pm
IkiWiki/Plugin/httpauth.pm
IkiWiki/Plugin/img.pm
IkiWiki/Plugin/inline.pm
IkiWiki/Plugin/link.pm
IkiWiki/Plugin/linkmap.pm
IkiWiki/Plugin/listdirectives.pm
IkiWiki/Plugin/lockedit.pm
IkiWiki/Plugin/map.pm
IkiWiki/Plugin/mdwn.pm
IkiWiki/Plugin/mercurial.pm
IkiWiki/Plugin/meta.pm
IkiWiki/Plugin/mirrorlist.pm
IkiWiki/Plugin/monotone.pm
IkiWiki/Plugin/more.pm
IkiWiki/Plugin/norcs.pm
IkiWiki/Plugin/opendiscussion.pm
IkiWiki/Plugin/openid.pm
IkiWiki/Plugin/orphans.pm
IkiWiki/Plugin/otl.pm
IkiWiki/Plugin/pagecount.pm
IkiWiki/Plugin/pagestats.pm
IkiWiki/Plugin/pagetemplate.pm
IkiWiki/Plugin/parentlinks.pm
IkiWiki/Plugin/passwordauth.pm
IkiWiki/Plugin/pingee.pm
IkiWiki/Plugin/pinger.pm
IkiWiki/Plugin/poll.pm
IkiWiki/Plugin/polygen.pm
IkiWiki/Plugin/postsparkline.pm
IkiWiki/Plugin/prettydate.pm
IkiWiki/Plugin/progress.pm
IkiWiki/Plugin/rawhtml.pm
IkiWiki/Plugin/recentchanges.pm
IkiWiki/Plugin/recentchangesdiff.pm
IkiWiki/Plugin/relativedate.pm [new file with mode: 0644]
IkiWiki/Plugin/remove.pm
IkiWiki/Plugin/rename.pm
IkiWiki/Plugin/repolist.pm [new file with mode: 0644]
IkiWiki/Plugin/search.pm
IkiWiki/Plugin/shortcut.pm
IkiWiki/Plugin/sidebar.pm
IkiWiki/Plugin/signinedit.pm
IkiWiki/Plugin/skeleton.pm.example
IkiWiki/Plugin/smiley.pm
IkiWiki/Plugin/sparkline.pm
IkiWiki/Plugin/svn.pm
IkiWiki/Plugin/table.pm
IkiWiki/Plugin/tag.pm
IkiWiki/Plugin/template.pm
IkiWiki/Plugin/testpagespec.pm
IkiWiki/Plugin/teximg.pm
IkiWiki/Plugin/textile.pm
IkiWiki/Plugin/tla.pm
IkiWiki/Plugin/toc.pm
IkiWiki/Plugin/toggle.pm
IkiWiki/Plugin/txt.pm
IkiWiki/Plugin/typography.pm
IkiWiki/Plugin/underlay.pm [new file with mode: 0644]
IkiWiki/Plugin/version.pm
IkiWiki/Plugin/websetup.pm
IkiWiki/Plugin/wikitext.pm
IkiWiki/Plugin/wmd.pm [new file with mode: 0644]
IkiWiki/Receive.pm [new file with mode: 0644]
IkiWiki/Render.pm
IkiWiki/Setup.pm
IkiWiki/Setup/Automator.pm
IkiWiki/Setup/Standard.pm
IkiWiki/UserInfo.pm
IkiWiki/Wrapper.pm
Makefile.PL
auto-blog.setup [new file with mode: 0644]
auto.setup
debian/NEWS
debian/changelog
debian/compat
debian/control
debian/copyright
debian/docs [new file with mode: 0644]
debian/link [new file with mode: 0644]
debian/preinst
debian/rules
doc/bugs/Aggregated_Atom_feeds_are_double-encoded.mdwn [new file with mode: 0644]
doc/bugs/Allow_overriding_of_symlink_restriction.mdwn
doc/bugs/CGI__44___formbuilder__44___non-existent_field_address.mdwn [new file with mode: 0644]
doc/bugs/Can__39__t_create_root_page.mdwn
doc/bugs/Comments_link_is_to_index.html_if_usedirs_is_on.mdwn [new file with mode: 0644]
doc/bugs/Error:_Your_login_session_has_expired._.mdwn [new file with mode: 0644]
doc/bugs/Git:_web_commit_message_not_utf-8.mdwn [new file with mode: 0644]
doc/bugs/INC_location_not_set_correctly_in_make_test.mdwn [new file with mode: 0644]
doc/bugs/IkiWiki::Wrapper_should_use_destdir.mdwn [new file with mode: 0644]
doc/bugs/IkiWiki::Wrapper_should_use_destdir/discussion.mdwn [new file with mode: 0644]
doc/bugs/Insecure_dependency_in_eval_while_running_with_-T_switch.mdwn
doc/bugs/Meta_plugin_does_not_respect_htmlscrubber__95__skip_setting.___40__patch__41__.mdwn [new file with mode: 0644]
doc/bugs/Monotone_rcs_support.mdwn
doc/bugs/No_link_for_blog_items_when_filename_contains_a_colon.mdwn
doc/bugs/PNG_triggers_UTF-8_error_in_MimeInfo.pm.mdwn [new file with mode: 0644]
doc/bugs/Problem_with_toc.pm_plug-in.mdwn
doc/bugs/Problems_with_graphviz.pm_plug-in.mdwn
doc/bugs/RecentChanges_broken_with_empty_svnpath.mdwn
doc/bugs/SVG_files_not_recognized_as_images.mdwn [new file with mode: 0644]
doc/bugs/Spaces_in_link_text_for_ikiwiki_links.mdwn
doc/bugs/Titles_are_lower-cased_when_creating_a_page.mdwn
doc/bugs/URLs_with_parentheses_displayed_badly.mdwn [new file with mode: 0644]
doc/bugs/Use_install__40__1__41___instead_of_cp__40__1__41___for_installing_files.mdwn
doc/bugs/Warns_about_use_of_uninitialized_value_if_prefix__95__directives_is_on_and_a_directive_does_not_contain_a_space.mdwn
doc/bugs/basewiki_uses_meta_directives_but_meta_is_not_enabled_by_default.mdwn [new file with mode: 0644]
doc/bugs/beautify__95__urlpath_will_add_.__47___even_if_it_is_already_present.mdwn [new file with mode: 0644]
doc/bugs/bugfix_for:___34__mtn:_operation_canceled:_Broken_pipe__34_____40__patch__41__.mdwn [new file with mode: 0644]
doc/bugs/bzr_RecentChanges_dates_start_from_1969.mdwn [new file with mode: 0644]
doc/bugs/bzr_plugin_does_not_define_rcs__95__diff.mdwn [new file with mode: 0644]
doc/bugs/cannot_preview_shortcuts.mdwn [new file with mode: 0644]
doc/bugs/cannot_reliably_use_meta_in_template.mdwn [new file with mode: 0644]
doc/bugs/comments_produce_broken_links_in_RecentChanges.mdwn [new file with mode: 0644]
doc/bugs/disabling_backlinks.mdwn
doc/bugs/dumpsetup_does_not_save_destdir.mdwn [new file with mode: 0644]
doc/bugs/entirely_negated_pagespec_matches_internal_pages.mdwn
doc/bugs/feedfile_does_the_wrong_thing_from_index.mdwn2.mdwn [new file with mode: 0644]
doc/bugs/git_stderr_output_causes_problems.mdwn
doc/bugs/gitweb_deficiency_w.r.t._log_messages.mdwn
doc/bugs/gitweb_deficiency_w.r.t._newly_created_pages.mdwn
doc/bugs/html5_support.mdwn [new file with mode: 0644]
doc/bugs/http_proxy_for_openid.mdwn
doc/bugs/images_in_inlined_pages_have_wrong_relative_URL.mdwn
doc/bugs/img_plugin_should_pass_through_class_attribute.mdwn
doc/bugs/inline_sort-by-title_issues.mdwn
doc/bugs/inline_sort_order_and_meta_date_value.mdwn [new file with mode: 0644]
doc/bugs/links_misparsed_in_CSV_files.mdwn [new file with mode: 0644]
doc/bugs/lockedit_plugin_should_alert_user_about_an_invalid_pagespec_in_preferences.mdwn
doc/bugs/login_page_should_note_cookie_requirement.mdwn [new file with mode: 0644]
doc/bugs/markdown_bug:_email_escaping_and_plus_addresses.mdwn
doc/bugs/mercurial_fail_to_add.mdwn
doc/bugs/messed_up_repository.mdwn [new file with mode: 0644]
doc/bugs/methodResponse_in_add__95__plugins.mdwn
doc/bugs/multiple_pages_with_same_name.mdwn
doc/bugs/output_of_successful_rename_should_list_the_full_path_to_affected_pages.mdwn [new file with mode: 0644]
doc/bugs/pagespec_parsing_chokes_on_function__40____41__.mdwn
doc/bugs/pagetitle_function_does_not_respect_meta_titles.mdwn
doc/bugs/prune_causing_taint_mode_failures.mdwn
doc/bugs/quieten_mercurial.mdwn
doc/bugs/recentchanges_feed_links.mdwn
doc/bugs/relative_date_weird_results.mdwn [new file with mode: 0644]
doc/bugs/remove_orphaned_sparkline-php_from_Suggests.mdwn [new file with mode: 0644]
doc/bugs/rst_tweak.mdwn
doc/bugs/search_for_locale_data_in_the_installed_location.mdwn
doc/bugs/shortcut_plugin_will_not_work_without_shortcuts.mdwn.mdwn [new file with mode: 0644]
doc/bugs/ssl_certificates_not_checked_with_openid.mdwn
doc/bugs/stray___60____47__p__62___tags.mdwn [new file with mode: 0644]
doc/bugs/support_for_openid2_logins.mdwn [new file with mode: 0644]
doc/bugs/table_external_file_links.mdwn [new file with mode: 0644]
doc/bugs/tags__44___backlinks_and_3.x.mdwn [new file with mode: 0644]
doc/bugs/tbasewiki__95__brokenlinks.t_broken.mdwn
doc/bugs/textile_plugin_dies_if_input_has_a_non-utf8_character.mdwn
doc/bugs/txt_plugin_having_problems_with_meta_directives.mdwn [new file with mode: 0644]
doc/bugs/unicode_chars_in_wikiname_break_auth.mdwn [new file with mode: 0644]
doc/bugs/user_links_on_recentchanges_pages_problem.mdwn [new file with mode: 0644]
doc/contact.mdwn
doc/convert.mdwn [new file with mode: 0644]
doc/css_market.mdwn
doc/css_market/actiontabs.css [new file with mode: 0644]
doc/css_market/discussion.mdwn
doc/css_market/zack.css
doc/download.mdwn
doc/examples/blog.mdwn
doc/examples/blog/index.mdwn
doc/examples/blog/posts/first_post.mdwn
doc/examples/softwaresite/index.mdwn
doc/forum.mdwn
doc/forum/Adding_new_markup_to_markdown.mdwn [new file with mode: 0644]
doc/forum/Darcs_as_the_RCS___63__.mdwn [new file with mode: 0644]
doc/forum/How_does_ikiwiki_remember_times__63__.mdwn [new file with mode: 0644]
doc/forum/How_to_fix___34__does_not_map_to_Unicode__34___errors__63__.mdwn [new file with mode: 0644]
doc/forum/Is_there_a_pagespec_for_creation_dates_relative_to_today__63__.mdwn
doc/forum/Migrating_old_repository_to_new_ikiwiki_system__63__.mdwn [new file with mode: 0644]
doc/forum/What_is_wrong_with_my_recentchange_page___63__.mdwn [new file with mode: 0644]
doc/forum/chinese_character_problem.mdwn [new file with mode: 0644]
doc/forum/discussion.mdwn [new file with mode: 0644]
doc/forum/ikiwiki__39__s_notion_of_time.mdwn [new file with mode: 0644]
doc/forum/managing_todo_lists.mdwn [new file with mode: 0644]
doc/forum/multi-user_setup_of_ikiwiki__44___gitosis_and_apache2_in_Debian_Sid.mdwn
doc/forum/usedirs___38___indexpages_using_problem.mdwn [new file with mode: 0644]
doc/forum/wiki_name_in_page_titles.mdwn [new file with mode: 0644]
doc/freesoftware/discussion.mdwn [new file with mode: 0644]
doc/git.mdwn
doc/ikiwiki-makerepo.mdwn
doc/ikiwiki-transition.mdwn
doc/ikiwiki/directive.mdwn
doc/ikiwiki/directive/cutpaste.mdwn
doc/ikiwiki/directive/format.mdwn [new file with mode: 0644]
doc/ikiwiki/directive/inline.mdwn
doc/ikiwiki/directive/inline/discussion.mdwn
doc/ikiwiki/directive/linkmap.mdwn
doc/ikiwiki/directive/meta.mdwn
doc/ikiwiki/directive/pagestats/discussion.mdwn [new file with mode: 0644]
doc/ikiwiki/directive/tag.mdwn
doc/ikiwiki/directive/testpagespec/discussion.mdwn [new file with mode: 0644]
doc/ikiwiki/markdown.mdwn
doc/ikiwiki/pagespec.mdwn
doc/ikiwiki/pagespec/attachment.mdwn
doc/ikiwiki/pagespec/discussion.mdwn
doc/ikiwiki/wikilink/discussion.mdwn
doc/ikiwikiusers.mdwn
doc/ikiwikiusers/discussion.mdwn
doc/index/discussion.mdwn
doc/index/openid/discussion.mdwn [new file with mode: 0644]
doc/install.mdwn
doc/install/discussion.mdwn
doc/news/code_swarm.mdwn
doc/news/git_push_to_this_wiki.mdwn [new file with mode: 0644]
doc/news/git_push_to_this_wiki/discussion.mdwn [new file with mode: 0644]
doc/news/ikiwiki_version_3.0.mdwn [new file with mode: 0644]
doc/news/openid.mdwn
doc/news/openid/discussion.mdwn
doc/news/version_2.62.1.mdwn [deleted file]
doc/news/version_2.63.mdwn [deleted file]
doc/news/version_2.64.mdwn [deleted file]
doc/news/version_2.65.mdwn [deleted file]
doc/news/version_2.66.mdwn [deleted file]
doc/news/version_2.68.mdwn [new file with mode: 0644]
doc/news/version_2.69.mdwn [new file with mode: 0644]
doc/news/version_2.70.mdwn [new file with mode: 0644]
doc/news/version_2.71.mdwn [new file with mode: 0644]
doc/news/version_2.72.mdwn [new file with mode: 0644]
doc/news/version_3.09.mdwn [new file with mode: 0644]
doc/pagehistory.mdwn
doc/plugins/404.mdwn [new file with mode: 0644]
doc/plugins/aggregate.mdwn
doc/plugins/aggregate/discussion.mdwn
doc/plugins/anonok.mdwn
doc/plugins/autoindex/discussion.mdwn [new file with mode: 0644]
doc/plugins/blogspam.mdwn [new file with mode: 0644]
doc/plugins/calendar.mdwn
doc/plugins/calendar/discussion.mdwn
doc/plugins/comments.mdwn [new file with mode: 0644]
doc/plugins/comments/discussion.mdwn [new file with mode: 0644]
doc/plugins/contrib.mdwn
doc/plugins/contrib/default_content_for___42__copyright__42___and___42__license__42__.mdwn
doc/plugins/contrib/gallery.mdwn
doc/plugins/contrib/headinganchors.mdwn
doc/plugins/contrib/highlightcode.mdwn
doc/plugins/contrib/linguas.mdwn
doc/plugins/contrib/mediawiki.mdwn [new file with mode: 0644]
doc/plugins/contrib/opml.mdwn [new file with mode: 0644]
doc/plugins/contrib/opml/discussion.mdwn [new file with mode: 0644]
doc/plugins/contrib/po.mdwn [new file with mode: 0644]
doc/plugins/contrib/siterel2pagerel.mdwn
doc/plugins/contrib/sourcehighlight.mdwn
doc/plugins/contrib/unixauth.mdwn
doc/plugins/contrib/unixauth/discussion.mdwn
doc/plugins/creole.mdwn
doc/plugins/creole/discussion.mdwn
doc/plugins/cutpaste.mdwn
doc/plugins/ddate.mdwn
doc/plugins/discussion.mdwn
doc/plugins/embed.mdwn
doc/plugins/format.mdwn [new file with mode: 0644]
doc/plugins/format/discussion.mdwn [new file with mode: 0644]
doc/plugins/goodstuff.mdwn
doc/plugins/google/discussion.mdwn [new file with mode: 0644]
doc/plugins/googlecalendar.mdwn [deleted file]
doc/plugins/goto.mdwn [new file with mode: 0644]
doc/plugins/htmlbalance.mdwn [new file with mode: 0644]
doc/plugins/htmlbalance/discussion.mdwn [new file with mode: 0644]
doc/plugins/htmlscrubber.mdwn
doc/plugins/htmltidy.mdwn
doc/plugins/img/discussion.mdwn
doc/plugins/lockedit.mdwn
doc/plugins/mdwn/discussion.mdwn [new file with mode: 0644]
doc/plugins/meta.mdwn
doc/plugins/pagecount.mdwn
doc/plugins/passwordauth/discussion.mdwn
doc/plugins/pingee.mdwn
doc/plugins/prettydate.mdwn
doc/plugins/relativedate.mdwn [new file with mode: 0644]
doc/plugins/repolist.mdwn [new file with mode: 0644]
doc/plugins/rst.mdwn
doc/plugins/shortcut/discussion.mdwn
doc/plugins/tag.mdwn
doc/plugins/tag/discussion.mdwn
doc/plugins/textile/discussion.mdwn [deleted file]
doc/plugins/type/date.mdwn [new file with mode: 0644]
doc/plugins/underlay.mdwn [new file with mode: 0644]
doc/plugins/wmd.mdwn [new file with mode: 0644]
doc/plugins/write.mdwn
doc/plugins/write/discussion.mdwn
doc/plugins/write/external.mdwn
doc/plugins/write/tutorial.mdwn
doc/rcs/details.mdwn
doc/rcs/git.mdwn
doc/rcs/mercurial.mdwn
doc/rcs/monotone.mdwn
doc/roadmap.mdwn
doc/robots.txt [new file with mode: 0644]
doc/sandbox.mdwn
doc/sandbox/Omgwtf_a_blof_post__33____33____33____33____33__1__33__1__33__11111__33____33____33__1__33__1__33____33__1five.html [new file with mode: 0644]
doc/sandbox/castle/discussion/jon_tests_too.mdwn
doc/sandbox/foobak.mdwn [new file with mode: 0644]
doc/sandbox/한글.mdwn [new file with mode: 0644]
doc/sandbox/한글페이지.mdwn [new file with mode: 0644]
doc/security.mdwn
doc/setup.mdwn
doc/setup/byhand.mdwn [new file with mode: 0644]
doc/setup/discussion.mdwn
doc/style.css
doc/tips/DreamHost.mdwn
doc/tips/Git_repository_and_web_server_on_different_hosts.mdwn [new file with mode: 0644]
doc/tips/Google_custom_search.mdwn
doc/tips/apache_cgi.mdwn [deleted file]
doc/tips/comments_feed.mdwn [new file with mode: 0644]
doc/tips/convert_MoinMoin_and_TWiki_to_ikiwiki.mdwn [new file with mode: 0644]
doc/tips/convert_mediawiki_to_ikiwiki.mdwn [new file with mode: 0644]
doc/tips/convert_mediawiki_to_ikiwiki/discussion.mdwn [new file with mode: 0644]
doc/tips/dot_cgi.mdwn [new file with mode: 0644]
doc/tips/emacs_syntax_highlighting.mdwn [new file with mode: 0644]
doc/tips/embedding_content.mdwn [new file with mode: 0644]
doc/tips/github.mdwn [new file with mode: 0644]
doc/tips/howto_avoid_flooding_aggregators.mdwn
doc/tips/inside_dot_ikiwiki.mdwn
doc/tips/inside_dot_ikiwiki/discussion.mdwn
doc/tips/laptop_wiki_with_git.mdwn
doc/tips/laptop_wiki_with_git/discussion.mdwn
doc/tips/lighttpd_cgi.mdwn [deleted file]
doc/tips/markdown_and_eclipse.mdwn [new file with mode: 0644]
doc/tips/nearlyfreespeech.mdwn
doc/tips/untrusted_git_push.mdwn [new file with mode: 0644]
doc/tips/untrusted_git_push/discussion.mdwn [new file with mode: 0644]
doc/tips/upgrade_to_3.0.mdwn [new file with mode: 0644]
doc/tips/using_the_web_interface_with_a_real_text_editor.mdwn
doc/tips/vim_syntax_highlighting.mdwn
doc/tips/vim_syntax_highlighting/discussion.mdwn
doc/tips/vim_syntax_highlighting/ikiwiki.vim
doc/todo/Add_DATE_parameter_for_use_in_templates.mdwn
doc/todo/Add_camelcase_exclusions.mdwn [new file with mode: 0644]
doc/todo/Add_showdown_GUI_input__47__edit.mdwn
doc/todo/Add_support_for_latest_Text::Markdown_as_found_on_CPAN.mdwn
doc/todo/Allow_change_of_wiki_file_types.mdwn
doc/todo/Allow_disabling_edit_and_preferences_links.mdwn [new file with mode: 0644]
doc/todo/Allow_edittemplate_to_set_file_type.mdwn
doc/todo/Allow_filenames_that_are_all_type.mdwn [new file with mode: 0644]
doc/todo/Bestdir_along_with_bestlink_in_IkiWiki.pm.mdwn
doc/todo/Default_text_for_new_pages.mdwn
doc/todo/Feature_parity_with_Trac.mdwn
doc/todo/Gallery.mdwn
doc/todo/Give_access_to_more_TMPL__95__VAR_variables_in_templates_inserted_by_the_template_plugin.mdwn
doc/todo/Improve_display_of_OpenIDs.mdwn [new file with mode: 0644]
doc/todo/Inline_plugin_option_to_show_full_page_path.mdwn
doc/todo/Move_teximg_latex_preamble_to_config_file.mdwn
doc/todo/New_preprocessor_directive_syntax/discussion.mdwn
doc/todo/Option_to_make_title_an_h1__63__.mdwn
doc/todo/RecentChanges_page_links_without_cgi_wrapper.mdwn [new file with mode: 0644]
doc/todo/Set_arbitrary_date_to_be_used_by_calendar_plugin.mdwn
doc/todo/Silence_monotone_warning.mdwn
doc/todo/Support_wildcard_inside_of_link__40____41___within_a_pagespec.mdwn
doc/todo/Untrusted_push_in_Monotone.mdwn [new file with mode: 0644]
doc/todo/Wrapper_config_with_multiline_regexp.mdwn
doc/todo/add_forward_age_sorting_option_to_inline.mdwn
doc/todo/allow_disabling_backlinks.mdwn [new file with mode: 0644]
doc/todo/anti-spam_protection.mdwn
doc/todo/apache_404_ErrorDocument_handler.mdwn [new file with mode: 0644]
doc/todo/applydiff_plugin.mdwn
doc/todo/auto-create_tag_pages_according_to_a_template.mdwn
doc/todo/automatic_use_of_syntax_plugin_on_source_code_files/discussion.mdwn
doc/todo/avatar.mdwn [new file with mode: 0644]
doc/todo/avoid_thrashing.mdwn [new file with mode: 0644]
doc/todo/blogpost_plugin.mdwn
doc/todo/bzr.mdwn
doc/todo/cas_authentication.mdwn
doc/todo/clear_page_to_delete.mdwn [new file with mode: 0644]
doc/todo/color_plugin.mdwn
doc/todo/comments.mdwn [new file with mode: 0644]
doc/todo/configurable_timezones.mdwn
doc/todo/darcs.mdwn
doc/todo/datearchives-plugin.mdwn
doc/todo/different_search_engine.mdwn
doc/todo/directive_docs.mdwn
doc/todo/discussion_page_as_blog.mdwn
doc/todo/dynamic_rootpage.mdwn
doc/todo/enable-htaccess-files.mdwn
doc/todo/firm_up_plugin_interface.mdwn
doc/todo/format_escape.mdwn
doc/todo/fortune:_select_options_via_environment.mdwn
doc/todo/generic___39__do__61__goto__39___for_CGI.mdwn [new file with mode: 0644]
doc/todo/httpauth_feature_parity_with_passwordauth.mdwn [new file with mode: 0644]
doc/todo/index.html_allowed.mdwn
doc/todo/inline:_numerical_ordering_by_title.mdwn
doc/todo/inline_plugin:_ability_to_override_feed_name.mdwn [new file with mode: 0644]
doc/todo/inline_plugin:_hide_feed_buttons_if_empty.mdwn [new file with mode: 0644]
doc/todo/inline_postform_autotitles.mdwn [new file with mode: 0644]
doc/todo/language_definition_for_the_meta_plugin.mdwn
doc/todo/latex.mdwn
doc/todo/location_of_external_plugins.mdwn [new file with mode: 0644]
doc/todo/location_of_ikiwiki-w3m.cgi.mdwn [new file with mode: 0644]
doc/todo/mbox.mdwn
doc/todo/mdwn_preview.mdwn [new file with mode: 0644]
doc/todo/mercurial.mdwn
doc/todo/meta_rcsid.mdwn
doc/todo/missingparents.pm.mdwn
doc/todo/modify_page_filename_in_plugin.mdwn
doc/todo/natural_sorting.mdwn [new file with mode: 0644]
doc/todo/need_global_renamepage_hook.mdwn [new file with mode: 0644]
doc/todo/overriding_displayed_modification_time.mdwn [new file with mode: 0644]
doc/todo/pagespec_relative_to_a_target.mdwn
doc/todo/pingback_support.mdwn [new file with mode: 0644]
doc/todo/plugin.mdwn
doc/todo/provide_inline_diffs_in_recentchanges.mdwn
doc/todo/provide_sha1_for_git_diffurl.mdwn [new file with mode: 0644]
doc/todo/rcs_updates_needed.mdwn [moved from doc/todo/rcs_updates_needed_for_rename_and_remove.mdwn with 54% similarity]
doc/todo/redirect_automatically_after_rename.mdwn [new file with mode: 0644]
doc/todo/relative_pagespec_deficiency.mdwn [new file with mode: 0644]
doc/todo/replace_HTML::Template_with_Template_Toolkit.mdwn
doc/todo/require_CAPTCHA_to_edit.mdwn
doc/todo/rewrite_ikiwiki_in_haskell.mdwn [new file with mode: 0644]
doc/todo/rewrite_ikiwiki_in_haskell/discussion.mdwn [new file with mode: 0644]
doc/todo/source_link.mdwn
doc/todo/structured_page_data.mdwn
doc/todo/support_creole_markup.mdwn
doc/todo/supporting_comments_via_disussion_pages.mdwn
doc/todo/syntax_highlighting.mdwn [new file with mode: 0644]
doc/todo/syntax_highlighting/discussion.mdwn [new file with mode: 0644]
doc/todo/tag_pagespec_function.mdwn [new file with mode: 0644]
doc/todo/tidy_git__39__s_ctime_debug_output.mdwn
doc/todo/tla.mdwn
doc/todo/tmplvars_plugin.mdwn
doc/todo/toc_plugin:_set_a_header_ceiling___40__opposite_of_levels__61____41__.mdwn [new file with mode: 0644]
doc/todo/tracking_bugs_with_dependencies.mdwn
doc/todo/turn_edittemplate_verbosity_off_by_default.mdwn
doc/todo/using_meta_titles_for_parentlinks.html [deleted file]
doc/todo/varioki_--_add_template_variables___40__with_closures_for_values__41___in_ikiwiki.setup.mdwn
doc/todo/wiki-formatted_comments_with_syntax_plugin.mdwn
doc/todo/wikiwyg.mdwn
doc/usage.mdwn
doc/users/Edward_Betts.mdwn
doc/users/StevenBlack.mdwn [new file with mode: 0644]
doc/users/ajt.mdwn [new file with mode: 0644]
doc/users/alexander.mdwn [new file with mode: 0644]
doc/users/cfm.mdwn [new file with mode: 0644]
doc/users/hb/discussion.mdwn
doc/users/intrigeri.mdwn
doc/users/jasonblevins.mdwn
doc/users/jelmer.mdwn [new file with mode: 0644]
doc/users/jon.mdwn [new file with mode: 0644]
doc/users/jondowland.mdwn [deleted file]
doc/users/joshtriplett.mdwn
doc/users/jrblevin.mdwn [new file with mode: 0644]
doc/users/jwalzer.mdwn [new file with mode: 0644]
doc/users/neale.mdwn [new file with mode: 0644]
doc/users/nolan.mdwn [new file with mode: 0644]
doc/users/seanh.mdwn [new file with mode: 0644]
doc/users/simonraven.mdwn [new file with mode: 0644]
doc/users/smcv.mdwn
doc/users/smcv/gallery.mdwn [new file with mode: 0644]
doc/users/svend.mdwn [new file with mode: 0644]
doc/users/weakish.mdwn [new file with mode: 0644]
doc/users/weakishjiang.mdwn [new file with mode: 0644]
doc/users/xma.mdwn [new file with mode: 0644]
doc/users/xma/discussion.mdwn [new file with mode: 0644]
doc/wikiicons/openidlogin-bg.gif
doc/wikitemplates.mdwn
docwiki.setup
gitremotes [new file with mode: 0755]
ikiwiki-makerepo
ikiwiki-transition
ikiwiki.in
plugins/externaldemo
pm_filter
po/bg.po
po/cs.po
po/da.po
po/de.po
po/es.po
po/fr.po
po/gu.po
po/ikiwiki.pot
po/pl.po
po/sv.po
po/vi.po
t/404.t [new file with mode: 0755]
t/basewiki_brokenlinks/index.mdwn
t/beautify_urlpath.t [new file with mode: 0755]
t/git.t
t/htmlbalance.t [new file with mode: 0755]
t/openiduser.t [new file with mode: 0755]
t/pagename.t
t/pagespec_match.t
t/pagespec_merge.t
t/pagetype.mdwn [deleted file]
t/tinyblog/index.mdwn
t/yesno.t [new file with mode: 0755]
templates/atomitem.tmpl
templates/change.tmpl
templates/comment.tmpl [new file with mode: 0644]
templates/commentmoderation.tmpl [new file with mode: 0644]
templates/editcomment.tmpl [new file with mode: 0644]
templates/editpage.tmpl
templates/googleform.tmpl
templates/inlinepage.tmpl
templates/misc.tmpl
templates/page.tmpl
templates/rssitem.tmpl
underlays/basewiki/directive.mdwn [deleted symlink]
underlays/javascript/ikiwiki.js [new file with mode: 0644]
underlays/javascript/relativedate.js [new file with mode: 0644]
underlays/javascript/toggle.js [new file with mode: 0644]

index 74832323a1dd7616950c7a68441526cf21b82f2e..769791d305b17ce03f6159a8d3094dbdb026cb80 100644 (file)
@@ -16,17 +16,17 @@ perl -MCPAN -e 'install Bundle::IkiWiki'
 
 =head1 CONTENTS
 
-XML::Simple
 Text::Markdown
-Date::Parse
-HTML::Template
 HTML::Scrubber
-CGI
+HTML::Template
+HTML::Parser
+URI
+XML::Simple
+Date::Parse
 CGI::FormBuilder
 CGI::Session
 Mail::Sendmail
-HTML::Parser
-URI
+CGI
 Data::Dumper
 
 =head1 AUTHOR
index abf596f0060712ca599fa5326da026b447fc947f..40b36e7c81fa4563af46d3d1e5fe673b9d755e61 100644 (file)
@@ -31,6 +31,11 @@ Text::Textile
 Text::WikiFormat
 XML::Feed
 Net::Amazon::S3
+Text::WikiCreole
+Term::ReadLine::Gnu
+HTML::Tree
+Authen::Passphrase
+Sort::Naturally
 
 =head1 AUTHOR
 
index 633c5138180a9f87f4a974bef3322492c1d08ada..ee07258ecce01cac5c94ecdf3878cf2a94213778 100644 (file)
@@ -21,11 +21,12 @@ our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match
                  bestlink htmllink readfile writefile pagetype srcfile pagename
                  displaytime will_render gettext urlto targetpage
                 add_underlay pagetitle titlepage linkpage newpagefile
+                inject
                  %config %links %pagestate %wikistate %renderedfiles
                  %pagesources %destsources);
-our $VERSION = 2.00; # plugin interface version, next is ikiwiki version
+our $VERSION = 3.00; # plugin interface version, next is ikiwiki version
 our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE
-my $installdir=''; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
+our $installdir=''; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
 
 # Optimisation.
 use Memoize;
@@ -33,7 +34,7 @@ memoize("abs2rel");
 memoize("pagespec_translate");
 memoize("file_pruned");
 
-sub getsetup () { #{{{
+sub getsetup () {
        wikiname => {
                type => "string",
                default => "wiki",
@@ -99,7 +100,7 @@ sub getsetup () { #{{{
                type => "string",
                default => '',
                example => "/var/www/wiki/ikiwiki.cgi",
-               description => "cgi wrapper to generate",
+               description => "filename of cgi wrapper to generate",
                safe => 0, # file
                rebuild => 0,
        },
@@ -119,7 +120,7 @@ sub getsetup () { #{{{
        },
        default_plugins => {
                type => "internal",
-               default => [qw{mdwn link inline htmlscrubber passwordauth
+               default => [qw{mdwn link inline meta htmlscrubber passwordauth
                                openid signinedit lockedit conditional
                                recentchanges parentlinks editpage}],
                description => "plugins to enable by default",
@@ -173,7 +174,7 @@ sub getsetup () { #{{{
        verbose => {
                type => "boolean",
                example => 1,
-               description => "display verbose messages when building?",
+               description => "display verbose messages?",
                safe => 1,
                rebuild => 0,
        },
@@ -193,7 +194,7 @@ sub getsetup () { #{{{
        },
        prefix_directives => {
                type => "boolean",
-               default => 0,
+               default => 1,
                description => "use '!'-prefixed preprocessor directives?",
                safe => 0, # changing requires manual transition
                rebuild => 1,
@@ -276,13 +277,20 @@ sub getsetup () { #{{{
        },
        umask => {
                type => "integer",
-               description => "",
                example => "022",
                description => "force ikiwiki to use a particular umask",
                advanced => 1,
                safe => 0, # paranoia
                rebuild => 0,
        },
+       wrappergroup => {
+               type => "string",
+               example => "ikiwiki",
+               description => "group for wrappers to run in",
+               advanced => 1,
+               safe => 0, # paranoia
+               rebuild => 0,
+       },
        libdir => {
                type => "string",
                default => "",
@@ -381,6 +389,13 @@ sub getsetup () { #{{{
                safe => 0,
                rebuild => 0,
        },
+       test_receive => {
+               type => "internal",
+               default => 0,
+               description => "running in receive test mode",
+               safe => 0,
+               rebuild => 0,
+       },
        getctime => {
                type => "internal",
                default => 0,
@@ -395,6 +410,13 @@ sub getsetup () { #{{{
                safe => 0,
                rebuild => 0,
        },
+       wikistatedir => {
+               type => "internal",
+               default => undef,
+               description => "path to the .ikiwiki directory holding ikiwiki state",
+               safe => 0,
+               rebuild => 0,
+       },
        setupfile => {
                type => "internal",
                default => undef,
@@ -403,15 +425,15 @@ sub getsetup () { #{{{
                rebuild => 0,
        },
        allow_symlinks_before_srcdir => {
-               type => "string",
+               type => "boolean",
                default => 0,
                description => "allow symlinks in the path leading to the srcdir (potentially insecure)",
                safe => 0,
                rebuild => 0,
        },
-} #}}}
+}
 
-sub defaultconfig () { #{{{
+sub defaultconfig () {
        my %s=getsetup();
        my @ret;
        foreach my $key (keys %s) {
@@ -419,9 +441,9 @@ sub defaultconfig () { #{{{
        }
        use Data::Dumper;
        return @ret;
-} #}}}
+}
 
-sub checkconfig () { #{{{
+sub checkconfig () {
        # locale stuff; avoid LC_ALL since it overrides everything
        if (defined $ENV{LC_ALL}) {
                $ENV{LANG} = $ENV{LC_ALL};
@@ -459,7 +481,7 @@ sub checkconfig () { #{{{
        }
        
        $config{wikistatedir}="$config{srcdir}/.ikiwiki"
-               unless exists $config{wikistatedir};
+               unless exists $config{wikistatedir} && defined $config{wikistatedir};
 
        if (defined $config{umask}) {
                umask(possibly_foolish_untaint($config{umask}));
@@ -468,9 +490,9 @@ sub checkconfig () { #{{{
        run_hooks(checkconfig => sub { shift->() });
 
        return 1;
-} #}}}
+}
 
-sub listplugins () { #{{{
+sub listplugins () {
        my %ret;
 
        foreach my $dir (@INC, $config{libdir}) {
@@ -488,9 +510,9 @@ sub listplugins () { #{{{
        }
 
        return keys %ret;
-} #}}}
+}
 
-sub loadplugins () { #{{{
+sub loadplugins () {
        if (defined $config{libdir} && length $config{libdir}) {
                unshift @INC, possibly_foolish_untaint($config{libdir});
        }
@@ -511,15 +533,15 @@ sub loadplugins () { #{{{
 
        run_hooks(getopt => sub { shift->() });
        if (grep /^-/, @ARGV) {
-               print STDERR "Unknown option: $_\n"
+               print STDERR "Unknown option (or missing parameter): $_\n"
                        foreach grep /^-/, @ARGV;
                usage();
        }
 
        return 1;
-} #}}}
+}
 
-sub loadplugin ($) { #{{{
+sub loadplugin ($) {
        my $plugin=shift;
 
        return if grep { $_ eq $plugin} @{$config{disable_plugins}};
@@ -545,9 +567,9 @@ sub loadplugin ($) { #{{{
        }
        $loaded_plugins{$plugin}=1;
        return 1;
-} #}}}
+}
 
-sub error ($;$) { #{{{
+sub error ($;$) {
        my $message=shift;
        my $cleaner=shift;
        log_message('err' => $message) if $config{syslog};
@@ -555,15 +577,15 @@ sub error ($;$) { #{{{
                $cleaner->();
        }
        die $message."\n";
-} #}}}
+}
 
-sub debug ($) { #{{{
+sub debug ($) {
        return unless $config{verbose};
        return log_message(debug => @_);
-} #}}}
+}
 
 my $log_open=0;
-sub log_message ($$) { #{{{
+sub log_message ($$) {
        my $type=shift;
 
        if ($config{syslog}) {
@@ -583,56 +605,63 @@ sub log_message ($$) { #{{{
        else {
                return print STDERR "@_\n";
        }
-} #}}}
+}
 
-sub possibly_foolish_untaint ($) { #{{{
+sub possibly_foolish_untaint ($) {
        my $tainted=shift;
        my ($untainted)=$tainted=~/(.*)/s;
        return $untainted;
-} #}}}
+}
 
-sub basename ($) { #{{{
+sub basename ($) {
        my $file=shift;
 
        $file=~s!.*/+!!;
        return $file;
-} #}}}
+}
 
-sub dirname ($) { #{{{
+sub dirname ($) {
        my $file=shift;
 
        $file=~s!/*[^/]+$!!;
        return $file;
-} #}}}
+}
 
-sub pagetype ($) { #{{{
+sub isinternal ($) {
        my $page=shift;
+       return exists $pagesources{$page} &&
+               $pagesources{$page} =~ /\._([^.]+)$/;
+}
+
+sub pagetype ($) {
+       my $file=shift;
        
-       if ($page =~ /\.([^.]+)$/) {
+       if ($file =~ /\.([^.]+)$/) {
                return $1 if exists $hooks{htmlize}{$1};
        }
+       my $base=basename($file);
+       if (exists $hooks{htmlize}{$base} &&
+           $hooks{htmlize}{$base}{noextension}) {
+               return $base;
+       }
        return;
-} #}}}
+}
 
-sub isinternal ($) { #{{{
-       my $page=shift;
-       return exists $pagesources{$page} &&
-               $pagesources{$page} =~ /\._([^.]+)$/;
-} #}}}
-
-sub pagename ($) { #{{{
+sub pagename ($) {
        my $file=shift;
 
        my $type=pagetype($file);
        my $page=$file;
-       $page=~s/\Q.$type\E*$// if defined $type && !$hooks{htmlize}{$type}{keepextension};
+       $page=~s/\Q.$type\E*$//
+               if defined $type && !$hooks{htmlize}{$type}{keepextension}
+                       && !$hooks{htmlize}{$type}{noextension};
        if ($config{indexpages} && $page=~/(.*)\/index$/) {
                $page=$1;
        }
        return $page;
-} #}}}
+}
 
-sub newpagefile ($$) { #{{{
+sub newpagefile ($$) {
        my $page=shift;
        my $type=shift;
 
@@ -642,27 +671,31 @@ sub newpagefile ($$) { #{{{
        else {
                return $page."/index.".$type;
        }
-} #}}}
+}
 
-sub targetpage ($$) { #{{{
+sub targetpage ($$;$) {
        my $page=shift;
        my $ext=shift;
+       my $filename=shift;
        
-       if (! $config{usedirs} || $page eq 'index') {
+       if (defined $filename) {
+               return $page."/".$filename.".".$ext;
+       }
+       elsif (! $config{usedirs} || $page eq 'index') {
                return $page.".".$ext;
        }
        else {
                return $page."/index.".$ext;
        }
-} #}}}
+}
 
-sub htmlpage ($) { #{{{
+sub htmlpage ($) {
        my $page=shift;
        
        return targetpage($page, $config{htmlext});
-} #}}}
+}
 
-sub srcfile_stat { #{{{
+sub srcfile_stat {
        my $file=shift;
        my $nothrow=shift;
 
@@ -672,26 +705,27 @@ sub srcfile_stat { #{{{
        }
        error("internal error: $file cannot be found in $config{srcdir} or underlay") unless $nothrow;
        return;
-} #}}}
+}
 
-sub srcfile ($;$) { #{{{
+sub srcfile ($;$) {
        return (srcfile_stat(@_))[0];
-} #}}}
+}
 
-sub add_underlay ($) { #{{{
+sub add_underlay ($) {
        my $dir=shift;
 
-       if ($dir=~/^\//) {
-               unshift @{$config{underlaydirs}}, $dir;
+       if ($dir !~ /^\//) {
+               $dir="$config{underlaydir}/../$dir";
        }
-       else {
-               unshift @{$config{underlaydirs}}, "$config{underlaydir}/../$dir";
+
+       if (! grep { $_ eq $dir } @{$config{underlaydirs}}) {
+               unshift @{$config{underlaydirs}}, $dir;
        }
 
        return 1;
-} #}}}
+}
 
-sub readfile ($;$$) { #{{{
+sub readfile ($;$$) {
        my $file=shift;
        my $binary=shift;
        my $wantfd=shift;
@@ -705,11 +739,15 @@ sub readfile ($;$$) { #{{{
        binmode($in) if ($binary);
        return \*$in if $wantfd;
        my $ret=<$in>;
+       # check for invalid utf-8, and toss it back to avoid crashes
+       if (! utf8::valid($ret)) {
+               $ret=encode_utf8($ret);
+       }
        close $in || error("failed to read $file: $!");
        return $ret;
-} #}}}
+}
 
-sub prep_writefile ($$) { #{{{
+sub prep_writefile ($$) {
        my $file=shift;
        my $destdir=shift;
        
@@ -733,9 +771,9 @@ sub prep_writefile ($$) { #{{{
        }
 
        return 1;
-} #}}}
+}
 
-sub writefile ($$$;$$) { #{{{
+sub writefile ($$$;$$) {
        my $file=shift; # can include subdirs
        my $destdir=shift; # directory to put file in
        my $content=shift;
@@ -763,10 +801,10 @@ sub writefile ($$$;$$) { #{{{
                error("failed renaming $newfile to $destdir/$file: $!", $cleanup);
 
        return 1;
-} #}}}
+}
 
 my %cleared;
-sub will_render ($$;$) { #{{{
+sub will_render ($$;$) {
        my $page=shift;
        my $dest=shift;
        my $clear=shift;
@@ -790,9 +828,9 @@ sub will_render ($$;$) { #{{{
        $destsources{$dest}=$page;
 
        return 1;
-} #}}}
+}
 
-sub bestlink ($$) { #{{{
+sub bestlink ($$) {
        my $page=shift;
        my $link=shift;
        
@@ -828,15 +866,15 @@ sub bestlink ($$) { #{{{
 
        #print STDERR "warning: page $page, broken link: $link\n";
        return "";
-} #}}}
+}
 
-sub isinlinableimage ($) { #{{{
+sub isinlinableimage ($) {
        my $file=shift;
        
        return $file =~ /\.(png|gif|jpg|jpeg)$/i;
-} #}}}
+}
 
-sub pagetitle ($;$) { #{{{
+sub pagetitle ($;$) {
        my $page=shift;
        my $unescaped=shift;
 
@@ -848,31 +886,31 @@ sub pagetitle ($;$) { #{{{
        }
 
        return $page;
-} #}}}
+}
 
-sub titlepage ($) { #{{{
+sub titlepage ($) {
        my $title=shift;
        # support use w/o %config set
        my $chars = defined $config{wiki_file_chars} ? $config{wiki_file_chars} : "-[:alnum:]+/.:_";
        $title=~s/([^$chars]|_)/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
        return $title;
-} #}}}
+}
 
-sub linkpage ($) { #{{{
+sub linkpage ($) {
        my $link=shift;
        my $chars = defined $config{wiki_file_chars} ? $config{wiki_file_chars} : "-[:alnum:]+/.:_";
        $link=~s/([^$chars])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
        return $link;
-} #}}}
+}
 
-sub cgiurl (@) { #{{{
+sub cgiurl (@) {
        my %params=@_;
 
        return $config{cgiurl}."?".
                join("&amp;", map $_."=".uri_escape_utf8($params{$_}), keys %params);
-} #}}}
+}
 
-sub baseurl (;$) { #{{{
+sub baseurl (;$) {
        my $page=shift;
 
        return "$config{url}/" if ! defined $page;
@@ -881,9 +919,9 @@ sub baseurl (;$) { #{{{
        $page=~s/[^\/]+$//;
        $page=~s/[^\/]+\//..\//g;
        return $page;
-} #}}}
+}
 
-sub abs2rel ($$) { #{{{
+sub abs2rel ($$) {
        # Work around very innefficient behavior in File::Spec if abs2rel
        # is passed two relative paths. It's much faster if paths are
        # absolute! (Debian bug #376658; fixed in debian unstable now)
@@ -894,9 +932,16 @@ sub abs2rel ($$) { #{{{
        my $ret=File::Spec->abs2rel($path, $base);
        $ret=~s/^// if defined $ret;
        return $ret;
-} #}}}
+}
 
-sub displaytime ($;$) { #{{{
+sub displaytime ($;$) {
+       # Plugins can override this function to mark up the time to
+       # display.
+       return '<span class="date">'.formattime(@_).'</span>';
+}
+
+sub formattime ($;$) {
+       # Plugins can override this function to format the time.
        my $time=shift;
        my $format=shift;
        if (! defined $format) {
@@ -906,25 +951,25 @@ sub displaytime ($;$) { #{{{
        # strftime doesn't know about encodings, so make sure
        # its output is properly treated as utf8
        return decode_utf8(POSIX::strftime($format, localtime($time)));
-} #}}}
+}
 
-sub beautify_urlpath ($) { #{{{
+sub beautify_urlpath ($) {
        my $url=shift;
 
-       if ($config{usedirs}) {
-               $url =~ s!/index.$config{htmlext}$!/!;
+       # Ensure url is not an empty link, and if necessary,
+       # add ./ to avoid colon confusion.
+       if ($url !~ /^\// && $url !~ /^\.\.?\//) {
+               $url="./$url";
        }
 
-       # Ensure url is not an empty link, and
-       # if it's relative, make that explicit to avoid colon confusion.
-       if ($url !~ /^\//) {
-               $url="./$url";
+       if ($config{usedirs}) {
+               $url =~ s!/index.$config{htmlext}$!/!;
        }
 
        return $url;
-} #}}}
+}
 
-sub urlto ($$;$) { #{{{
+sub urlto ($$;$) {
        my $to=shift;
        my $from=shift;
        my $absolute=shift;
@@ -944,9 +989,9 @@ sub urlto ($$;$) { #{{{
        my $link = abs2rel($to, dirname(htmlpage($from)));
 
        return beautify_urlpath($link);
-} #}}}
+}
 
-sub htmllink ($$$;@) { #{{{
+sub htmllink ($$$;@) {
        my $lpage=shift; # the page doing the linking
        my $page=shift; # the page that will contain the link (different for inline)
        my $link=shift;
@@ -1009,9 +1054,9 @@ sub htmllink ($$$;@) { #{{{
        }
 
        return "<a href=\"$bestlink\"@attrs>$linktext</a>";
-} #}}}
+}
 
-sub userlink ($) { #{{{
+sub userlink ($) {
        my $user=shift;
 
        my $oiduser=eval { openiduser($user) };
@@ -1026,9 +1071,9 @@ sub userlink ($) { #{{{
                        length $config{userdir} ? $config{userdir}."/".$user : $user
                ), noimageinline => 1);
        }
-} #}}}
+}
 
-sub htmlize ($$$$) { #{{{
+sub htmlize ($$$$) {
        my $page=shift;
        my $destpage=shift;
        my $type=shift;
@@ -1063,9 +1108,9 @@ sub htmlize ($$$$) { #{{{
        }
 
        return $content;
-} #}}}
+}
 
-sub linkify ($$$) { #{{{
+sub linkify ($$$) {
        my $page=shift;
        my $destpage=shift;
        my $content=shift;
@@ -1079,11 +1124,11 @@ sub linkify ($$$) { #{{{
        });
        
        return $content;
-} #}}}
+}
 
 our %preprocessing;
 our $preprocess_preview=0;
-sub preprocess ($$$;$$) { #{{{
+sub preprocess ($$$;$$) {
        my $page=shift; # the page the data comes from
        my $destpage=shift; # the page the data will appear in (different for inline)
        my $content=shift;
@@ -1236,9 +1281,9 @@ sub preprocess ($$$;$$) { #{{{
 
        $content =~ s{$regex}{$handle->($1, $2, $3, $4)}eg;
        return $content;
-} #}}}
+}
 
-sub filter ($$$) { #{{{
+sub filter ($$$) {
        my $page=shift;
        my $destpage=shift;
        my $content=shift;
@@ -1249,16 +1294,79 @@ sub filter ($$$) { #{{{
        });
 
        return $content;
-} #}}}
+}
 
-sub indexlink () { #{{{
+sub indexlink () {
        return "<a href=\"$config{url}\">$config{wikiname}</a>";
-} #}}}
+}
+
+sub check_canedit ($$$;$) {
+       my $page=shift;
+       my $q=shift;
+       my $session=shift;
+       my $nonfatal=shift;
+       
+       my $canedit;
+       run_hooks(canedit => sub {
+               return if defined $canedit;
+               my $ret=shift->($page, $q, $session);
+               if (defined $ret) {
+                       if ($ret eq "") {
+                               $canedit=1;
+                       }
+                       elsif (ref $ret eq 'CODE') {
+                               $ret->() unless $nonfatal;
+                               $canedit=0;
+                       }
+                       elsif (defined $ret) {
+                               error($ret) unless $nonfatal;
+                               $canedit=0;
+                       }
+               }
+       });
+       return defined $canedit ? $canedit : 1;
+}
+
+sub check_content (@) {
+       my %params=@_;
+       
+       return 1 if ! exists $hooks{checkcontent}; # optimisation
+
+       if (exists $pagesources{$params{page}}) {
+               my @diff;
+               my %old=map { $_ => 1 }
+                       split("\n", readfile(srcfile($pagesources{$params{page}})));
+               foreach my $line (split("\n", $params{content})) {
+                       push @diff, $line if ! exists $old{$_};
+               }
+               $params{content}=join("\n", @diff);
+       }
+
+       my $ok;
+       run_hooks(checkcontent => sub {
+               return if defined $ok;
+               my $ret=shift->(%params);
+               if (defined $ret) {
+                       if ($ret eq "") {
+                               $ok=1;
+                       }
+                       elsif (ref $ret eq 'CODE') {
+                               $ret->() unless $params{nonfatal};
+                               $ok=0;
+                       }
+                       elsif (defined $ret) {
+                               error($ret) unless $params{nonfatal};
+                               $ok=0;
+                       }
+               }
+
+       });
+       return defined $ok ? $ok : 1;
+}
 
 my $wikilock;
 
-sub lockwiki (;$) { #{{{
-       my $wait=@_ ? shift : 1;
+sub lockwiki () {
        # Take an exclusive lock on the wiki to prevent multiple concurrent
        # run issues. The lock will be dropped on program exit.
        if (! -d $config{wikistatedir}) {
@@ -1266,32 +1374,21 @@ sub lockwiki (;$) { #{{{
        }
        open($wikilock, '>', "$config{wikistatedir}/lockfile") ||
                error ("cannot write to $config{wikistatedir}/lockfile: $!");
-       if (! flock($wikilock, 2 | 4)) { # LOCK_EX | LOCK_NB
-               if ($wait) {
-                       debug("wiki seems to be locked, waiting for lock");
-                       my $wait=600; # arbitrary, but don't hang forever to 
-                                     # prevent process pileup
-                       for (1..$wait) {
-                               return if flock($wikilock, 2 | 4);
-                               sleep 1;
-                       }
-                       error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
-               }
-               else {
-                       return 0;
-               }
+       if (! flock($wikilock, 2)) { # LOCK_EX
+               error("failed to get lock");
        }
        return 1;
-} #}}}
+}
 
-sub unlockwiki () { #{{{
+sub unlockwiki () {
+       POSIX::close($ENV{IKIWIKI_CGILOCK_FD}) if exists $ENV{IKIWIKI_CGILOCK_FD};
        return close($wikilock) if $wikilock;
        return;
-} #}}}
+}
 
 my $commitlock;
 
-sub commit_hook_enabled () { #{{{
+sub commit_hook_enabled () {
        open($commitlock, '+>', "$config{wikistatedir}/commitlock") ||
                error("cannot write to $config{wikistatedir}/commitlock: $!");
        if (! flock($commitlock, 1 | 4)) { # LOCK_SH | LOCK_NB to test
@@ -1300,23 +1397,23 @@ sub commit_hook_enabled () { #{{{
        }
        close($commitlock) || error("failed closing commitlock: $!");
        return 1;
-} #}}}
+}
 
-sub disable_commit_hook () { #{{{
+sub disable_commit_hook () {
        open($commitlock, '>', "$config{wikistatedir}/commitlock") ||
                error("cannot write to $config{wikistatedir}/commitlock: $!");
        if (! flock($commitlock, 2)) { # LOCK_EX
                error("failed to get commit lock");
        }
        return 1;
-} #}}}
+}
 
-sub enable_commit_hook () { #{{{
+sub enable_commit_hook () {
        return close($commitlock) if $commitlock;
        return;
-} #}}}
+}
 
-sub loadindex () { #{{{
+sub loadindex () {
        %oldrenderedfiles=%pagectime=();
        if (! $config{rebuild}) {
                %pagesources=%pagemtime=%oldlinks=%links=%depends=
@@ -1376,9 +1473,9 @@ sub loadindex () { #{{{
                $destsources{$_}=$page foreach @{$renderedfiles{$page}};
        }
        return close($in);
-} #}}}
+}
 
-sub saveindex () { #{{{
+sub saveindex () {
        run_hooks(savestate => sub { shift->() });
 
        my %hookids;
@@ -1434,18 +1531,18 @@ sub saveindex () { #{{{
                error("failed renaming $newfile to $config{wikistatedir}/indexdb", $cleanup);
        
        return 1;
-} #}}}
+}
 
-sub template_file ($) { #{{{
+sub template_file ($) {
        my $template=shift;
 
        foreach my $dir ($config{templatedir}, "$installdir/share/ikiwiki/templates") {
                return "$dir/$template" if -e "$dir/$template";
        }
        return;
-} #}}}
+}
 
-sub template_params (@) { #{{{
+sub template_params (@) {
        my $filename=template_file(shift);
 
        if (! defined $filename) {
@@ -1464,14 +1561,14 @@ sub template_params (@) { #{{{
                @_
        );
        return wantarray ? @ret : {@ret};
-} #}}}
+}
 
-sub template ($;@) { #{{{
+sub template ($;@) {
        require HTML::Template;
        return HTML::Template->new(template_params(@_));
-} #}}}
+}
 
-sub misctemplate ($$;@) { #{{{
+sub misctemplate ($$;@) {
        my $title=shift;
        my $pagebody=shift;
        
@@ -1488,9 +1585,9 @@ sub misctemplate ($$;@) { #{{{
                shift->(page => "", destpage => "", template => $template);
        });
        return $template->output;
-}#}}}
+}
 
-sub hook (@) { # {{{
+sub hook (@) {
        my %param=@_;
        
        if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
@@ -1501,109 +1598,86 @@ sub hook (@) { # {{{
        
        $hooks{$param{type}}{$param{id}}=\%param;
        return 1;
-} # }}}
+}
 
-sub run_hooks ($$) { # {{{
+sub run_hooks ($$) {
        # Calls the given sub for each hook of the given type,
        # passing it the hook function to call.
        my $type=shift;
        my $sub=shift;
 
        if (exists $hooks{$type}) {
-               my @deferred;
+               my (@first, @middle, @last);
                foreach my $id (keys %{$hooks{$type}}) {
-                       if ($hooks{$type}{$id}{last}) {
-                               push @deferred, $id;
-                               next;
+                       if ($hooks{$type}{$id}{first}) {
+                               push @first, $id;
+                       }
+                       elsif ($hooks{$type}{$id}{last}) {
+                               push @last, $id;
+                       }
+                       else {
+                               push @middle, $id;
                        }
-                       $sub->($hooks{$type}{$id}{call});
                }
-               foreach my $id (@deferred) {
+               foreach my $id (@first, @middle, @last) {
                        $sub->($hooks{$type}{$id}{call});
                }
        }
 
        return 1;
-} #}}}
+}
 
-sub rcs_update () { #{{{
+sub rcs_update () {
        $hooks{rcs}{rcs_update}{call}->(@_);
-} #}}}
+}
 
-sub rcs_prepedit ($) { #{{{
+sub rcs_prepedit ($) {
        $hooks{rcs}{rcs_prepedit}{call}->(@_);
-} #}}}
+}
 
-sub rcs_commit ($$$;$$) { #{{{
+sub rcs_commit ($$$;$$) {
        $hooks{rcs}{rcs_commit}{call}->(@_);
-} #}}}
+}
 
-sub rcs_commit_staged ($$$) { #{{{
+sub rcs_commit_staged ($$$) {
        $hooks{rcs}{rcs_commit_staged}{call}->(@_);
-} #}}}
+}
 
-sub rcs_add ($) { #{{{
+sub rcs_add ($) {
        $hooks{rcs}{rcs_add}{call}->(@_);
-} #}}}
+}
 
-sub rcs_remove ($) { #{{{
+sub rcs_remove ($) {
        $hooks{rcs}{rcs_remove}{call}->(@_);
-} #}}}
+}
 
-sub rcs_rename ($$) { #{{{
+sub rcs_rename ($$) {
        $hooks{rcs}{rcs_rename}{call}->(@_);
-} #}}}
+}
 
-sub rcs_recentchanges ($) { #{{{
+sub rcs_recentchanges ($) {
        $hooks{rcs}{rcs_recentchanges}{call}->(@_);
-} #}}}
+}
 
-sub rcs_diff ($) { #{{{
+sub rcs_diff ($) {
        $hooks{rcs}{rcs_diff}{call}->(@_);
-} #}}}
+}
 
-sub rcs_getctime ($) { #{{{
+sub rcs_getctime ($) {
        $hooks{rcs}{rcs_getctime}{call}->(@_);
-} #}}}
+}
 
-sub globlist_to_pagespec ($) { #{{{
-       my @globlist=split(' ', shift);
-
-       my (@spec, @skip);
-       foreach my $glob (@globlist) {
-               if ($glob=~/^!(.*)/) {
-                       push @skip, $glob;
-               }
-               else {
-                       push @spec, $glob;
-               }
-       }
+sub rcs_receive () {
+       $hooks{rcs}{rcs_receive}{call}->();
+}
 
-       my $spec=join(' or ', @spec);
-       if (@skip) {
-               my $skip=join(' and ', @skip);
-               if (length $spec) {
-                       $spec="$skip and ($spec)";
-               }
-               else {
-                       $spec=$skip;
-               }
-       }
-       return $spec;
-} #}}}
-
-sub is_globlist ($) { #{{{
-       my $s=shift;
-       return ( $s =~ /[^\s]+\s+([^\s]+)/ && $1 ne "and" && $1 ne "or" );
-} #}}}
-
-sub safequote ($) { #{{{
+sub safequote ($) {
        my $s=shift;
        $s=~s/[{}]//g;
        return "q{$s}";
-} #}}}
+}
 
-sub add_depends ($$) { #{{{
+sub add_depends ($$) {
        my $page=shift;
        my $pagespec=shift;
        
@@ -1617,9 +1691,9 @@ sub add_depends ($$) { #{{{
        }
 
        return 1;
-} # }}}
+}
 
-sub file_pruned ($$) { #{{{
+sub file_pruned ($$) {
        require File::Spec;
        my $file=File::Spec->canonpath(shift);
        my $base=File::Spec->canonpath(shift);
@@ -1627,9 +1701,9 @@ sub file_pruned ($$) { #{{{
 
        my $regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
        return $file =~ m/$regexp/ && $file ne $base;
-} #}}}
+}
 
-sub gettext { #{{{
+sub gettext {
        # Only use gettext in the rare cases it's needed.
        if ((exists $ENV{LANG} && length $ENV{LANG}) ||
            (exists $ENV{LC_ALL} && length $ENV{LC_ALL}) ||
@@ -1650,39 +1724,50 @@ sub gettext { #{{{
        else {
                return shift;
        }
-} #}}}
+}
 
-sub yesno ($) { #{{{
+sub yesno ($) {
        my $val=shift;
 
-       return (defined $val && lc($val) eq gettext("yes"));
-} #}}}
+       return (defined $val && (lc($val) eq gettext("yes") || lc($val) eq "yes" || $val eq "1"));
+}
+
+sub inject {
+       # Injects a new function into the symbol table to replace an
+       # exported function.
+       my %params=@_;
 
-sub pagespec_merge ($$) { #{{{
+       # This is deep ugly perl foo, beware.
+       no strict;
+       no warnings;
+       if (! defined $params{parent}) {
+               $params{parent}='::';
+               $params{old}=\&{$params{name}};
+               $params{name}=~s/.*:://;
+       }
+       my $parent=$params{parent};
+       foreach my $ns (grep /^\w+::/, keys %{$parent}) {
+               $ns = $params{parent} . $ns;
+               inject(%params, parent => $ns) unless $ns eq '::main::';
+               *{$ns . $params{name}} = $params{call}
+                       if exists ${$ns}{$params{name}} &&
+                          \&{${$ns}{$params{name}}} == $params{old};
+       }
+       use strict;
+       use warnings;
+}
+
+sub pagespec_merge ($$) {
        my $a=shift;
        my $b=shift;
 
        return $a if $a eq $b;
-
-        # Support for old-style GlobLists.
-        if (is_globlist($a)) {
-                $a=globlist_to_pagespec($a);
-        }
-        if (is_globlist($b)) {
-                $b=globlist_to_pagespec($b);
-        }
-
        return "($a) or ($b)";
-} #}}}
+}
 
-sub pagespec_translate ($) { #{{{
+sub pagespec_translate ($) {
        my $spec=shift;
 
-       # Support for old-style GlobLists.
-       if (is_globlist($spec)) {
-               $spec=globlist_to_pagespec($spec);
-       }
-
        # Convert spec to perl code.
        my $code="";
        while ($spec=~m{
@@ -1715,7 +1800,7 @@ sub pagespec_translate ($) { #{{{
                                $code.="IkiWiki::PageSpec::match_$1(\$page, ".safequote($2).", \@_)";
                        }
                        else {
-                               $code.=' 0';
+                               $code.="IkiWiki::FailReason->new(".safequote(qq{unknown function in pagespec "$word"}).")";
                        }
                }
                else {
@@ -1724,14 +1809,14 @@ sub pagespec_translate ($) { #{{{
        }
 
        if (! length $code) {
-               $code=0;
+               $code="IkiWiki::FailReason->new('empty pagespec')";
        }
 
        no warnings;
        return eval 'sub { my $page=shift; '.$code.' }';
-} #}}}
+}
 
-sub pagespec_match ($$;@) { #{{{
+sub pagespec_match ($$;@) {
        my $page=shift;
        my $spec=shift;
        my @params=@_;
@@ -1742,69 +1827,76 @@ sub pagespec_match ($$;@) { #{{{
        }
 
        my $sub=pagespec_translate($spec);
-       return IkiWiki::FailReason->new("syntax error in pagespec \"$spec\"") if $@;
+       return IkiWiki::FailReason->new("syntax error in pagespec \"$spec\"")
+               if $@ || ! defined $sub;
        return $sub->($page, @params);
-} #}}}
+}
 
-sub pagespec_valid ($) { #{{{
+sub pagespec_valid ($) {
        my $spec=shift;
 
        my $sub=pagespec_translate($spec);
        return ! $@;
-} #}}}
-       
-sub glob2re ($) { #{{{
+}
+
+sub glob2re ($) {
        my $re=quotemeta(shift);
        $re=~s/\\\*/.*/g;
        $re=~s/\\\?/./g;
        return $re;
-} #}}}
+}
 
 package IkiWiki::FailReason;
 
-use overload ( #{{{
+use overload (
        '""'    => sub { ${$_[0]} },
        '0+'    => sub { 0 },
        '!'     => sub { bless $_[0], 'IkiWiki::SuccessReason'},
        fallback => 1,
-); #}}}
+);
 
-sub new { #{{{
+sub new {
        my $class = shift;
        my $value = shift;
        return bless \$value, $class;
-} #}}}
+}
 
 package IkiWiki::SuccessReason;
 
-use overload ( #{{{
+use overload (
        '""'    => sub { ${$_[0]} },
        '0+'    => sub { 1 },
        '!'     => sub { bless $_[0], 'IkiWiki::FailReason'},
        fallback => 1,
-); #}}}
+);
 
-sub new { #{{{
+sub new {
        my $class = shift;
        my $value = shift;
        return bless \$value, $class;
-}; #}}}
+};
 
 package IkiWiki::PageSpec;
 
-sub match_glob ($$;@) { #{{{
+sub derel ($$) {
+       my $path=shift;
+       my $from=shift;
+
+       if ($path =~ m!^\./!) {
+               $from=~s#/?[^/]+$## if defined $from;
+               $path=~s#^\./##;
+               $path="$from/$path" if length $from;
+       }
+
+       return $path;
+}
+
+sub match_glob ($$;@) {
        my $page=shift;
        my $glob=shift;
        my %params=@_;
        
-       my $from=exists $params{location} ? $params{location} : '';
-       
-       # relative matching
-       if ($glob =~ m!^\./!) {
-               $from=~s#/?[^/]+$##;
-               $glob=~s#^\./##;
-               $glob="$from/$glob" if length $from;
-       }
+       $glob=derel($glob, $params{location});
 
        my $regexp=IkiWiki::glob2re($glob);
        if ($page=~/^$regexp$/i) {
@@ -1818,26 +1910,20 @@ sub match_glob ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("$glob does not match $page");
        }
-} #}}}
+}
 
-sub match_internal ($$;@) { #{{{
+sub match_internal ($$;@) {
        return match_glob($_[0], $_[1], @_, internal => 1)
-} #}}}
+}
 
-sub match_link ($$;@) { #{{{
+sub match_link ($$;@) {
        my $page=shift;
        my $link=lc(shift);
        my %params=@_;
 
+       $link=derel($link, $params{location});
        my $from=exists $params{location} ? $params{location} : '';
 
-       # relative matching
-       if ($link =~ m!^\.! && defined $from) {
-               $from=~s#/?[^/]+$##;
-               $link=~s#^\./##;
-               $link="$from/$link" if length $from;
-       }
-
        my $links = $IkiWiki::links{$page};
        return IkiWiki::FailReason->new("$page has no links") unless $links && @{$links};
        my $bestlink = IkiWiki::bestlink($from, $link);
@@ -1849,18 +1935,25 @@ sub match_link ($$;@) { #{{{
                else {
                        return IkiWiki::SuccessReason->new("$page links to page $p matching $link")
                                if match_glob($p, $link, %params);
+                       $p=~s/^\///;
+                       $link=~s/^\///;
+                       return IkiWiki::SuccessReason->new("$page links to page $p matching $link")
+                               if match_glob($p, $link, %params);
                }
        }
        return IkiWiki::FailReason->new("$page does not link to $link");
-} #}}}
+}
 
-sub match_backlink ($$;@) { #{{{
+sub match_backlink ($$;@) {
        return match_link($_[1], $_[0], @_);
-} #}}}
+}
 
-sub match_created_before ($$;@) { #{{{
+sub match_created_before ($$;@) {
        my $page=shift;
        my $testpage=shift;
+       my %params=@_;
+       
+       $testpage=derel($testpage, $params{location});
 
        if (exists $IkiWiki::pagectime{$testpage}) {
                if ($IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage}) {
@@ -1873,11 +1966,14 @@ sub match_created_before ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("$testpage has no ctime");
        }
-} #}}}
+}
 
-sub match_created_after ($$;@) { #{{{
+sub match_created_after ($$;@) {
        my $page=shift;
        my $testpage=shift;
+       my %params=@_;
+       
+       $testpage=derel($testpage, $params{location});
 
        if (exists $IkiWiki::pagectime{$testpage}) {
                if ($IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage}) {
@@ -1890,36 +1986,36 @@ sub match_created_after ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("$testpage has no ctime");
        }
-} #}}}
+}
 
-sub match_creation_day ($$;@) { #{{{
+sub match_creation_day ($$;@) {
        if ((gmtime($IkiWiki::pagectime{shift()}))[3] == shift) {
                return IkiWiki::SuccessReason->new('creation_day matched');
        }
        else {
                return IkiWiki::FailReason->new('creation_day did not match');
        }
-} #}}}
+}
 
-sub match_creation_month ($$;@) { #{{{
+sub match_creation_month ($$;@) {
        if ((gmtime($IkiWiki::pagectime{shift()}))[4] + 1 == shift) {
                return IkiWiki::SuccessReason->new('creation_month matched');
        }
        else {
                return IkiWiki::FailReason->new('creation_month did not match');
        }
-} #}}}
+}
 
-sub match_creation_year ($$;@) { #{{{
+sub match_creation_year ($$;@) {
        if ((gmtime($IkiWiki::pagectime{shift()}))[5] + 1900 == shift) {
                return IkiWiki::SuccessReason->new('creation_year matched');
        }
        else {
                return IkiWiki::FailReason->new('creation_year did not match');
        }
-} #}}}
+}
 
-sub match_user ($$;@) { #{{{
+sub match_user ($$;@) {
        shift;
        my $user=shift;
        my %params=@_;
@@ -1937,9 +2033,9 @@ sub match_user ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("user is $params{user}, not $user");
        }
-} #}}}
+}
 
-sub match_admin ($$;@) { #{{{
+sub match_admin ($$;@) {
        shift;
        shift;
        my %params=@_;
@@ -1957,9 +2053,9 @@ sub match_admin ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("user is not an admin");
        }
-} #}}}
+}
 
-sub match_ip ($$;@) { #{{{
+sub match_ip ($$;@) {
        shift;
        my $ip=shift;
        my %params=@_;
@@ -1974,6 +2070,6 @@ sub match_ip ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("IP is $params{ip}, not $ip");
        }
-} #}}}
+}
 
 1
index dac522eea470a544248c9c77406009b27d4d9997..04f24b04f986c2618b0cbf619e09ba7dfbff58ee 100644 (file)
@@ -9,7 +9,7 @@ use IkiWiki::UserInfo;
 use open qw{:utf8 :std};
 use Encode;
 
-sub printheader ($) { #{{{
+sub printheader ($) {
        my $session=shift;
        
        if ($config{sslcookie}) {
@@ -19,9 +19,9 @@ sub printheader ($) { #{{{
                print $session->header(-charset => 'utf-8',
                        -cookie => $session->cookie(-httponly => 1));
        }
-} #}}}
+}
 
-sub showform ($$$$;@) { #{{{
+sub showform ($$$$;@) {
        my $form=shift;
        my $buttons=shift;
        my $session=shift;
@@ -38,7 +38,7 @@ sub showform ($$$$;@) { #{{{
        print misctemplate($form->title, $form->render(submit => $buttons), @_);
 }
 
-sub redirect ($$) { #{{{
+sub redirect ($$) {
        my $q=shift;
        my $url=shift;
        if (! $config{w3mmode}) {
@@ -48,9 +48,9 @@ sub redirect ($$) { #{{{
                print "Content-type: text/plain\n";
                print "W3m-control: GOTO $url\n\n";
        }
-} #}}}
+}
 
-sub decode_cgi_utf8 ($) { #{{{
+sub decode_cgi_utf8 ($) {
        # decode_form_utf8 method is needed for 5.10
        if ($] < 5.01) {
                my $cgi = shift;
@@ -58,9 +58,9 @@ sub decode_cgi_utf8 ($) { #{{{
                        $cgi->param($f, map { decode_utf8 $_ } $cgi->param($f));
                }
        }
-} #}}}
+}
 
-sub decode_form_utf8 ($) { #{{{
+sub decode_form_utf8 ($) {
        if ($] >= 5.01) {
                my $form = shift;
                foreach my $f ($form->field) {
@@ -70,11 +70,11 @@ sub decode_form_utf8 ($) { #{{{
                        );
                }
        }
-} #}}}
+}
 
 # Check if the user is signed in. If not, redirect to the signin form and
 # save their place to return to later.
-sub needsignin ($$) { #{{{
+sub needsignin ($$) {
        my $q=shift;
        my $session=shift;
 
@@ -85,9 +85,9 @@ sub needsignin ($$) { #{{{
                cgi_savesession($session);
                exit;
        }
-} #}}} 
+}
 
-sub cgi_signin ($$) { #{{{
+sub cgi_signin ($$) {
        my $q=shift;
        my $session=shift;
 
@@ -127,9 +127,9 @@ sub cgi_signin ($$) { #{{{
        }
 
        showform($form, $buttons, $session, $q);
-} #}}}
+}
 
-sub cgi_postsignin ($$) { #{{{
+sub cgi_postsignin ($$) {
        my $q=shift;
        my $session=shift;
        
@@ -142,11 +142,16 @@ sub cgi_postsignin ($$) { #{{{
                exit;
        }
        else {
-               error(gettext("login failed, perhaps you need to turn on cookies?"));
+               if ($config{sslcookie} && ! $q->https()) {
+                       error(gettext("probable misconfiguration: sslcookie is set, but you are attepting to login via http, not https"));
+               }
+               else {
+                       error(gettext("login failed, perhaps you need to turn on cookies?"));
+               }
        }
-} #}}}
+}
 
-sub cgi_prefs ($$) { #{{{
+sub cgi_prefs ($$) {
        my $q=shift;
        my $session=shift;
 
@@ -203,25 +208,9 @@ sub cgi_prefs ($$) { #{{{
        
        my $user_name=$session->param("name");
 
-       # XXX deprecated, should be removed eventually
-       $form->field(name => "banned_users", size => 50, fieldset => "admin");
-       if (! is_admin($user_name)) {
-               $form->field(name => "banned_users", type => "hidden");
-       }
        if (! $form->submitted) {
                $form->field(name => "email", force => 1,
                        value => userinfo_get($user_name, "email"));
-               if (is_admin($user_name)) {
-                       my $value=join(" ", get_banned_users());
-                       if (length $value) {
-                               $form->field(name => "banned_users", force => 1,
-                                       value => join(" ", get_banned_users()),
-                                       comment => "deprecated; please move to banned_users in setup file");
-                       }
-                       else {
-                               $form->field(name => "banned_users", type => "hidden");
-                       }
-               }
        }
        
        if ($form->submitted eq 'Logout') {
@@ -239,48 +228,48 @@ sub cgi_prefs ($$) { #{{{
                                error("failed to set email");
                }
 
-               # XXX deprecated, should be removed eventually
-               if (is_admin($user_name)) {
-                       set_banned_users(grep { ! is_admin($_) }
-                                       split(' ',
-                                               $form->field("banned_users"))) ||
-                               error("failed saving changes");
-                       if (! length $form->field("banned_users")) {
-                               $form->field(name => "banned_users", type => "hidden");
-                       }
-               }
-
                $form->text(gettext("Preferences saved."));
        }
        
        showform($form, $buttons, $session, $q);
-} #}}}
+}
 
-sub check_banned ($$) { #{{{
+sub cgi_custom_failure ($$) {
+       my $header=shift;
+       my $message=shift;
+
+       print $header;
+       print $message;
+
+       # Internet Explod^Hrer won't show custom 404 responses
+       # unless they're >= 512 bytes
+       print ' ' x 512;
+
+       exit;
+}
+
+sub check_banned ($$) {
        my $q=shift;
        my $session=shift;
 
        my $name=$session->param("name");
        if (defined $name) {
-               # XXX banned in userinfo is deprecated, should be removed
-               # eventually, and only banned_users be checked.
-               if (userinfo_get($session->param("name"), "banned") ||
-                   grep { $name eq $_ } @{$config{banned_users}}) {
-                       print $q->header(-status => "403 Forbidden");
+               if (grep { $name eq $_ } @{$config{banned_users}}) {
                        $session->delete();
-                       print gettext("You are banned.");
                        cgi_savesession($session);
-                       exit;
+                       cgi_custom_failure(
+                               $q->header(-status => "403 Forbidden"),
+                               gettext("You are banned."));
                }
        }
 }
 
-sub cgi_getsession ($) { #{{{
+sub cgi_getsession ($) {
        my $q=shift;
 
-       eval q{use CGI::Session};
+       eval q{use CGI::Session; use HTML::Entities};
        error($@) if $@;
-       CGI::Session->name("ikiwiki_session_".encode_utf8($config{wikiname}));
+       CGI::Session->name("ikiwiki_session_".encode_entities($config{wikiname}));
        
        my $oldmask=umask(077);
        my $session = eval {
@@ -294,18 +283,34 @@ sub cgi_getsession ($) { #{{{
        umask($oldmask);
 
        return $session;
-} #}}}
+}
+
+# To guard against CSRF, the user's session id (sid)
+# can be stored on a form. This function will check
+# (for logged in users) that the sid on the form matches
+# the session id in the cookie.
+sub checksessionexpiry ($$) {
+       my $q=shift;
+       my $session = shift;
 
-sub cgi_savesession ($) { #{{{
+       if (defined $session->param("name")) {
+               my $sid=$q->param('sid');
+               if (! defined $sid || $sid ne $session->id) {
+                       error(gettext("Your login session has expired."));
+               }
+       }
+}
+
+sub cgi_savesession ($) {
        my $session=shift;
 
        # Force session flush with safe umask.
        my $oldmask=umask(077);
        $session->flush;
        umask($oldmask);
-} #}}}
+}
 
-sub cgi (;$$) { #{{{
+sub cgi (;$$) {
        my $q=shift;
        my $session=shift;
 
@@ -331,7 +336,7 @@ sub cgi (;$$) { #{{{
                        error("\"do\" parameter missing");
                }
        }
-       
+
        # Need to lock the wiki before getting a session.
        lockwiki();
        loadindex();
@@ -375,16 +380,16 @@ sub cgi (;$$) { #{{{
        else {
                error("unknown do parameter");
        }
-} #}}}
+}
 
 # Does not need to be called directly; all errors will go through here.
-sub cgierror ($) { #{{{
+sub cgierror ($) {
        my $message=shift;
 
        print "Content-type: text/html\n\n";
        print misctemplate(gettext("Error"),
                "<p class=\"error\">".gettext("Error").": $message</p>");
        die $@;
-} #}}}
+}
 
 1
diff --git a/IkiWiki/Plugin/404.pm b/IkiWiki/Plugin/404.pm
new file mode 100644 (file)
index 0000000..bae9e15
--- /dev/null
@@ -0,0 +1,78 @@
+#!/usr/bin/perl
+# Copyright © 2009 Simon McVittie <http://smcv.pseudorandom.co.uk/>
+# Licensed under the GNU GPL, version 2, or any later version published by the
+# Free Software Foundation
+package IkiWiki::Plugin::404;
+
+use warnings;
+use strict;
+use IkiWiki 3.00;
+
+sub import {
+       hook(type => "cgi", id => '404',  call => \&cgi);
+       IkiWiki::loadplugin("goto");
+}
+
+sub getsetup () {
+       return
+               plugin => {
+                       # not really a matter of safety, but enabling/disabling
+                       # through a web interface is useless - it needs web
+                       # server admin action too
+                       safe => 0,
+                       rebuild => 0,
+               }
+}
+
+sub cgi_page_from_404 ($$$) {
+       my $path = shift;
+       my $baseurl = shift;
+       my $usedirs = shift;
+
+       # fail if missing from environment or whatever
+       return undef unless defined $path;
+       return undef unless defined $baseurl;
+
+       # with usedirs on, path is like /~fred/foo/bar/ or /~fred/foo/bar or
+       #    /~fred/foo/bar/index.html
+       # with usedirs off, path is like /~fred/foo/bar.html
+       # baseurl is like 'http://people.example.com/~fred'
+
+       # convert baseurl to ~fred
+       unless ($baseurl =~ s{^https?://[^/]+/?}{}) {
+               return undef;
+       }
+
+       # convert path to /~fred/foo/bar
+       if ($usedirs) {
+               $path =~ s/\/*(?:index\.$config{htmlext})?$//;
+       }
+       else {
+               $path =~ s/\.$config{htmlext}$//;
+       }
+
+       # remove /~fred/
+       unless ($path =~ s{^/*\Q$baseurl\E/*}{}) {
+               return undef;
+       }
+
+       # special case for the index
+       unless ($path) {
+               return 'index';
+       }
+
+       return $path;
+}
+
+sub cgi ($) {
+       my $cgi=shift;
+
+       if (exists $ENV{REDIRECT_STATUS} && 
+           $ENV{REDIRECT_STATUS} eq '404') {
+               my $page = cgi_page_from_404($ENV{REDIRECT_URL},
+                       $config{url}, $config{usedirs});
+               IkiWiki::Plugin::goto::cgi_goto($cgi, $page);
+       }
+}
+
+1;
index 26c5cc9ae209bd63c90adb573014c333ecb34644..e1baae666a02ffe0e9230ed4d81667a2f7ece79a 100644 (file)
@@ -4,7 +4,7 @@ package IkiWiki::Plugin::aggregate;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 use HTML::Parser;
 use HTML::Tagset;
 use HTML::Entities;
@@ -14,7 +14,7 @@ use open qw{:utf8 :std};
 my %feeds;
 my %guids;
 
-sub import { #{{{
+sub import {
        hook(type => "getopt", id => "aggregate", call => \&getopt);
        hook(type => "getsetup", id => "aggregate", call => \&getsetup);
        hook(type => "checkconfig", id => "aggregate", call => \&checkconfig);
@@ -26,9 +26,9 @@ sub import { #{{{
        if (exists $config{aggregate_webtrigger} && $config{aggregate_webtrigger}) {
                hook(type => "cgi", id => "aggregate", call => \&cgi);
        }
-} # }}}
+}
 
-sub getopt () { #{{{
+sub getopt () {
         eval q{use Getopt::Long};
        error($@) if $@;
         Getopt::Long::Configure('pass_through');
@@ -36,9 +36,9 @@ sub getopt () { #{{{
                "aggregate" => \$config{aggregate},
                "aggregateinternal!" => \$config{aggregateinternal},
        );
-} #}}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
@@ -46,7 +46,7 @@ sub getsetup () { #{{{
                },
                aggregateinternal => {
                        type => "boolean",
-                       example => 0,
+                       example => 1,
                        description => "enable aggregation to internal pages?",
                        safe => 0, # enabling needs manual transition
                        rebuild => 0,
@@ -58,16 +58,20 @@ sub getsetup () { #{{{
                        safe => 1,
                        rebuild => 0,
                },
-} #}}}
+}
+
+sub checkconfig () {
+       if (! defined $config{aggregateinternal}) {
+               $config{aggregateinternal}=1;
+       }
 
-sub checkconfig () { #{{{
        if ($config{aggregate} && ! ($config{post_commit} && 
                                     IkiWiki::commit_hook_enabled())) {
                launchaggregation();
        }
-} #}}}
+}
 
-sub cgi ($) { #{{{
+sub cgi ($) {
        my $cgi=shift;
 
        if (defined $cgi->param('do') &&
@@ -90,9 +94,9 @@ sub cgi ($) { #{{{
                }
                exit 0;
        }
-} #}}}
+}
 
-sub launchaggregation () { #{{{
+sub launchaggregation () {
        # See if any feeds need aggregation.
        loadstate();
        my @feeds=needsaggregate();
@@ -135,16 +139,16 @@ sub launchaggregation () { #{{{
        unlockaggregate();
 
        return 1;
-} #}}}
+}
 
 #  Pages with extension _aggregated have plain html markup, pass through.
-sub htmlize (@) { #{{{
+sub htmlize (@) {
        my %params=@_;
        return $params{content};
-} #}}}
+}
 
 # Used by ikiwiki-transition aggregateinternal.
-sub migrate_to_internal { #{{{
+sub migrate_to_internal {
        if (! lockaggregate()) {
                error("an aggregation process is currently running");
        }
@@ -190,9 +194,9 @@ sub migrate_to_internal { #{{{
        IkiWiki::unlockwiki;
        
        unlockaggregate();
-} #}}}
+}
 
-sub needsbuild (@) { #{{{
+sub needsbuild (@) {
        my $needsbuild=shift;
        
        loadstate();
@@ -206,9 +210,9 @@ sub needsbuild (@) { #{{{
                        markunseen($feed->{sourcepage});
                }
        }
-} # }}}
+}
 
-sub preprocess (@) { #{{{
+sub preprocess (@) {
        my %params=@_;
 
        foreach my $required (qw{name url}) {
@@ -245,6 +249,7 @@ sub preprocess (@) { #{{{
        $feed->{template}=$params{template} . ".tmpl";
        delete $feed->{unseen};
        $feed->{lastupdate}=0 unless defined $feed->{lastupdate};
+       $feed->{lasttry}=$feed->{lastupdate} unless defined $feed->{lasttry};
        $feed->{numposts}=0 unless defined $feed->{numposts};
        $feed->{newposts}=0 unless defined $feed->{newposts};
        $feed->{message}=gettext("new feed") unless defined $feed->{message};
@@ -265,9 +270,9 @@ sub preprocess (@) { #{{{
               ($feed->{newposts} ? "; ".$feed->{newposts}.
                                    " ".gettext("new") : "").
               ")";
-} # }}}
+}
 
-sub delete (@) { #{{{
+sub delete (@) {
        my @files=@_;
 
        # Remove feed data for removed pages.
@@ -275,9 +280,9 @@ sub delete (@) { #{{{
                my $page=pagename($file);
                markunseen($page);
        }
-} #}}}
+}
 
-sub markunseen ($) { #{{{
+sub markunseen ($) {
        my $page=shift;
 
        foreach my $id (keys %feeds) {
@@ -285,11 +290,11 @@ sub markunseen ($) { #{{{
                        $feeds{$id}->{unseen}=1;
                }
        }
-} #}}}
+}
 
 my $state_loaded=0;
 
-sub loadstate () { #{{{
+sub loadstate () {
        return if $state_loaded;
        $state_loaded=1;
        if (-e "$config{wikistatedir}/aggregate") {
@@ -323,9 +328,9 @@ sub loadstate () { #{{{
 
                close IN;
        }
-} #}}}
+}
 
-sub savestate () { #{{{
+sub savestate () {
        return unless $state_loaded;
        garbage_collect();
        my $newfile="$config{wikistatedir}/aggregate.new";
@@ -342,7 +347,8 @@ sub savestate () { #{{{
                                push @line, "tag=$_" foreach @{$data->{tags}};
                        }
                        else {
-                               push @line, "$field=".$data->{$field};
+                               push @line, "$field=".$data->{$field}
+                                       if defined $data->{$field};
                        }
                }
                print OUT join(" ", @line)."\n" || error("write $newfile: $!", $cleanup);
@@ -350,9 +356,9 @@ sub savestate () { #{{{
        close OUT || error("save $newfile: $!", $cleanup);
        rename($newfile, "$config{wikistatedir}/aggregate") ||
                error("rename $newfile: $!", $cleanup);
-} #}}}
+}
 
-sub garbage_collect () { #{{{
+sub garbage_collect () {
        foreach my $name (keys %feeds) {
                # remove any feeds that were not seen while building the pages
                # that used to contain them
@@ -375,9 +381,9 @@ sub garbage_collect () { #{{{
                        delete $guid->{md5};
                }
        }
-} #}}}
+}
 
-sub mergestate () { #{{{
+sub mergestate () {
        # Load the current state in from disk, and merge into it
        # values from the state in memory that might have changed
        # during aggregation.
@@ -390,8 +396,8 @@ sub mergestate () { #{{{
        # fields.
        foreach my $name (keys %myfeeds) {
                if (exists $feeds{$name}) {
-                       foreach my $field (qw{message lastupdate numposts
-                                             newposts error}) {
+                       foreach my $field (qw{message lastupdate lasttry
+                                             numposts newposts error}) {
                                $feeds{$name}->{$field}=$myfeeds{$name}->{$field};
                        }
                }
@@ -407,15 +413,15 @@ sub mergestate () { #{{{
                        $guids{$guid}=$myguids{$guid};
                }
        }
-} #}}}
+}
 
-sub clearstate () { #{{{
+sub clearstate () {
        %feeds=();
        %guids=();
        $state_loaded=0;
-} #}}}
+}
 
-sub expire () { #{{{
+sub expire () {
        foreach my $feed (values %feeds) {
                next unless $feed->{expireage} || $feed->{expirecount};
                my $count=0;
@@ -444,24 +450,24 @@ sub expire () { #{{{
                        }
                }
        }
-} #}}}
+}
 
-sub needsaggregate () { #{{{
+sub needsaggregate () {
        return values %feeds if $config{rebuild};
        return grep { time - $_->{lastupdate} >= $_->{updateinterval} } values %feeds;
-} #}}}
+}
 
-sub aggregate (@) { #{{{
+sub aggregate (@) {
        eval q{use XML::Feed};
        error($@) if $@;
        eval q{use URI::Fetch};
        error($@) if $@;
 
        foreach my $feed (@_) {
-               $feed->{lastupdate}=time;
+               $feed->{lasttry}=time;
                $feed->{newposts}=0;
-               $feed->{message}=sprintf(gettext("processed ok at %s"),
-                       displaytime($feed->{lastupdate}));
+               $feed->{message}=sprintf(gettext("last checked %s"),
+                       displaytime($feed->{lasttry}));
                $feed->{error}=0;
 
                debug(sprintf(gettext("checking feed %s ..."), $feed->{name}));
@@ -483,6 +489,10 @@ sub aggregate (@) { #{{{
                        debug($feed->{message});
                        next;
                }
+
+               # lastupdate is only set if we were able to contact the server
+               $feed->{lastupdate}=$feed->{lasttry};
+
                if ($res->status == URI::Fetch::URI_GONE()) {
                        $feed->{message}=gettext("feed not found");
                        $feed->{error}=1;
@@ -496,15 +506,19 @@ sub aggregate (@) { #{{{
                        # that contains invalid UTF-8 sequences. Convert
                        # feed to ascii to try to work around.
                        $feed->{message}.=" ".sprintf(gettext("(invalid UTF-8 stripped from feed)"));
-                       $content=Encode::decode_utf8($content, 0);
-                       $f=eval{XML::Feed->parse(\$content)};
+                       $f=eval {
+                               $content=Encode::decode_utf8($content, 0);
+                               XML::Feed->parse(\$content)
+                       };
                }
                if ($@) {
                        # Another possibility is badly escaped entities.
                        $feed->{message}.=" ".sprintf(gettext("(feed entities escaped)"));
                        $content=~s/\&(?!amp)(\w+);/&amp;$1;/g;
-                       $content=Encode::decode_utf8($content, 0);
-                       $f=eval{XML::Feed->parse(\$content)};
+                       $f=eval {
+                               $content=Encode::decode_utf8($content, 0);
+                               XML::Feed->parse(\$content)
+                       };
                }
                if ($@) {
                        $feed->{message}=gettext("feed crashed XML::Feed!")." ($@)";
@@ -520,10 +534,15 @@ sub aggregate (@) { #{{{
                }
 
                foreach my $entry ($f->entries) {
-                       my $content=$content=$entry->content->body;
+                       # XML::Feed doesn't work around XML::Atom's bizarre
+                       # API, so we will. Real unicode strings? Yes please.
+                       # See [[bugs/Aggregated_Atom_feeds_are_double-encoded]]
+                       local $XML::Atom::ForceUnicode = 1;
+
+                       my $c=$entry->content;
                        # atom feeds may have no content, only a summary
-                       if (! defined $content && ref $entry->summary) {
-                               $content=$entry->summary->body;
+                       if (! defined $c && ref $entry->summary) {
+                               $c=$entry->summary;
                        }
 
                        add_page(
@@ -531,15 +550,16 @@ sub aggregate (@) { #{{{
                                copyright => $f->copyright,
                                title => defined $entry->title ? decode_entities($entry->title) : "untitled",
                                link => $entry->link,
-                               content => defined $content ? $content : "",
+                               content => (defined $c && defined $c->body) ? $c->body : "",
                                guid => defined $entry->id ? $entry->id : time."_".$feed->{name},
                                ctime => $entry->issued ? ($entry->issued->epoch || time) : time,
+                               base => (defined $c && $c->can("base")) ? $c->base : undef,
                        );
                }
        }
-} #}}}
+}
 
-sub add_page (@) { #{{{
+sub add_page (@) {
        my %params=@_;
        
        my $feed=$params{feed};
@@ -605,7 +625,8 @@ sub add_page (@) { #{{{
        my $template=template($feed->{template}, blind_cache => 1);
        $template->param(title => $params{title})
                if defined $params{title} && length($params{title});
-       $template->param(content => htmlescape(htmlabs($params{content}, $feed->{feedurl})));
+       $template->param(content => wikiescape(htmlabs($params{content},
+               defined $params{base} ? $params{base} : $feed->{feedurl})));
        $template->param(name => $feed->{name});
        $template->param(url => $feed->{url});
        $template->param(copyright => $params{copyright})
@@ -625,23 +646,25 @@ sub add_page (@) { #{{{
                # Store it in pagectime for expiry code to use also.
                $IkiWiki::pagectime{$guid->{page}}=$mtime;
        }
-} #}}}
+       else {
+               # Dummy value for expiry code.
+               $IkiWiki::pagectime{$guid->{page}}=time;
+       }
+}
 
-sub htmlescape ($) { #{{{
+sub wikiescape ($) {
        # escape accidental wikilinks and preprocessor stuff
-       my $html=shift;
-       $html=~s/(?<!\\)\[\[/\\\[\[/g;
-       return $html;
-} #}}}
+       return encode_entities(shift, '\[\]');
+}
 
-sub urlabs ($$) { #{{{
+sub urlabs ($$) {
        my $url=shift;
        my $urlbase=shift;
 
        URI->new_abs($url, $urlbase)->as_string;
-} #}}}
+}
 
-sub htmlabs ($$) { #{{{
+sub htmlabs ($$) {
        # Convert links in html from relative to absolute.
        # Note that this is a heuristic, which is not specified by the rss
        # spec and may not be right for all feeds. Also, see Debian
@@ -677,15 +700,15 @@ sub htmlabs ($$) { #{{{
        $p->eof;
 
        return $ret;
-} #}}}
+}
 
-sub htmlfn ($) { #{{{
+sub htmlfn ($) {
        return shift().".".($config{aggregateinternal} ? "_aggregated" : $config{htmlext});
-} #}}}
+}
 
 my $aggregatelock;
 
-sub lockaggregate () { #{{{
+sub lockaggregate () {
        # Take an exclusive lock to prevent multiple concurrent aggregators.
        # Returns true if the lock was aquired.
        if (! -d $config{wikistatedir}) {
@@ -698,11 +721,11 @@ sub lockaggregate () { #{{{
                return 0;
        }
        return 1;
-} #}}}
+}
 
-sub unlockaggregate () { #{{{
+sub unlockaggregate () {
        return close($aggregatelock) if $aggregatelock;
        return;
-} #}}}
+}
 
 1
index 597539c13b0f2c218955dc81da2f26dfb7aa6fb4..10bf358e44a69148954fc36a0557330016fa4024 100644 (file)
@@ -4,7 +4,7 @@ package IkiWiki::Plugin::amazon_s3;
 use warnings;
 no warnings 'redefine';
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 use IkiWiki::Render;
 use Net::Amazon::S3;
 
@@ -16,13 +16,13 @@ BEGIN {
        }
 };
 
-sub import { #{{{
+sub import {
        hook(type => "getopt", id => "amazon_s3", call => \&getopt);
        hook(type => "getsetup", id => "amazon_s3", call => \&getsetup);
        hook(type => "checkconfig", id => "amazon_s3", call => \&checkconfig);
-} # }}}
+}
 
-sub getopt () { #{{{
+sub getopt () {
         eval q{use Getopt::Long};
         error($@) if $@;
         Getopt::Long::Configure('pass_through');
@@ -38,9 +38,9 @@ sub getopt () { #{{{
                debug(gettext("done"));
                exit(0);
        });
-} #}}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 0,
@@ -88,9 +88,9 @@ sub getsetup () { #{{{
                        safe => 1,
                        rebuild => 1,
                },
-} #}}}
+}
 
-sub checkconfig { #{{{
+sub checkconfig {
        foreach my $field (qw{amazon_s3_key_id amazon_s3_key_file
                              amazon_s3_bucket}) {
                if (! exists $config{$field} || ! defined $config{$field}) {
@@ -101,11 +101,11 @@ sub checkconfig { #{{{
            ! defined $config{amazon_s3_prefix}) {
            $config{amazon_s3_prefix}="wiki/";
        }
-} #}}}
+}
 
 {
 my $bucket;
-sub getbucket { #{{{
+sub getbucket {
        return $bucket if defined $bucket;
        
        open(IN, "<", $config{amazon_s3_key_file}) || error($config{amazon_s3_key_file}.": ".$!);
@@ -138,11 +138,11 @@ sub getbucket { #{{{
        }
 
        return $bucket;
-} #}}}
+}
 }
 
 # Given a file, return any S3 keys associated with it.
-sub file2keys ($) { #{{{
+sub file2keys ($) {
        my $file=shift;
 
        my @keys;
@@ -162,14 +162,14 @@ sub file2keys ($) { #{{{
                }
        }
        return @keys;
-} #}}}
+}
 
 package IkiWiki;
 use File::MimeInfo;
 use Encode;
 
 # This is a wrapper around the real writefile.
-sub writefile ($$$;$$) { #{{{
+sub writefile ($$$;$$) {
         my $file=shift;
         my $destdir=shift;
         my $content=shift;
@@ -225,10 +225,10 @@ sub writefile ($$$;$$) { #{{{
        }
 
        return $ret;
-} #}}}
+}
 
 # This is a wrapper around the real prune.
-sub prune ($) { #{{{
+sub prune ($) {
        my $file=shift;
 
        my @keys=IkiWiki::Plugin::amazon_s3::file2keys($file);
@@ -247,6 +247,6 @@ sub prune ($) { #{{{
        }
 
        return $IkiWiki::Plugin::amazon_s3::subs{'IkiWiki::prune'}->($file);
-} #}}}
+}
 
 1
index 2be983693d7d2d67f5cbd39a93b19c0885f6cfa6..243b9892056028ea3cd289257c70274a54a410f6 100644 (file)
@@ -3,14 +3,14 @@ package IkiWiki::Plugin::anonok;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "anonok", call => \&getsetup);
        hook(type => "canedit", id => "anonok", call => \&canedit);
-} # }}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
@@ -24,9 +24,9 @@ sub getsetup () { #{{{
                        safe => 1,
                        rebuild => 0,
                },
-} #}}}
+}
 
-sub canedit ($$$) { #{{{
+sub canedit ($$$) {
        my $page=shift;
        my $cgi=shift;
        my $session=shift;
@@ -45,6 +45,6 @@ sub canedit ($$$) { #{{{
        else {
                return "";
        }
-} #}}}
+}
 
 1
index dcac3e82006bf9679732727d4ed683b5b31bf8a9..087c315a9af355569921c9150c05c0ba89ec8df1 100644 (file)
@@ -3,17 +3,18 @@ package IkiWiki::Plugin::attachment;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
-sub import { #{{{
+sub import {
+       add_underlay("javascript");
        hook(type => "getsetup", id => "attachment", call => \&getsetup);
        hook(type => "checkconfig", id => "attachment", call => \&checkconfig);
        hook(type => "formbuilder_setup", id => "attachment", call => \&formbuilder_setup);
        hook(type => "formbuilder", id => "attachment", call => \&formbuilder);
        IkiWiki::loadplugin("filecheck");
-} # }}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
@@ -34,9 +35,9 @@ sub getsetup () { #{{{
                        safe => 0, # executed
                        rebuild => 0,
                },
-} #}}}
+}
 
-sub check_canattach ($$;$) { #{{{
+sub check_canattach ($$;$) {
        my $session=shift;
        my $dest=shift; # where it's going to be put, under the srcdir
        my $file=shift; # the path to the attachment currently
@@ -60,36 +61,19 @@ sub check_canattach ($$;$) { #{{{
                );
        }
 
-       # XXX deprecated, should be removed eventually
-       if ($allowed) {
-               foreach my $admin (@{$config{adminuser}}) {
-                       my $allowed_attachments=IkiWiki::userinfo_get($admin, "allowed_attachments");
-                       if (defined $allowed_attachments &&
-                           length $allowed_attachments) {
-                               $allowed=pagespec_match($dest,
-                                       $allowed_attachments,
-                                       file => $file,
-                                       user => $session->param("name"),
-                                       ip => $ENV{REMOTE_ADDR},
-                               );
-                               last if $allowed;
-                       }
-               }
-       }
-
        if (! $allowed) {
                error(gettext("prohibited by allowed_attachments")." ($allowed)");
        }
        else {
                return 1;
        }
-} #}}}
+}
 
-sub checkconfig () { #{{{
+sub checkconfig () {
        $config{cgi_disable_uploads}=0;
-} #}}}
+}
 
-sub formbuilder_setup (@) { #{{{
+sub formbuilder_setup (@) {
        my %params=@_;
        my $form=$params{form};
        my $q=$params{cgi};
@@ -104,10 +88,10 @@ sub formbuilder_setup (@) { #{{{
                $form->tmpl_param("field-upload" => '<input name="_submit" type="submit" value="Upload Attachment" />');
                $form->tmpl_param("field-link" => '<input name="_submit" type="submit" value="Insert Links" />');
 
-               # Add the javascript from the toggle plugin;
-               # the attachments interface uses it to toggle visibility.
+               # Add the toggle javascript; the attachments interface uses
+               # it to toggle visibility.
                require IkiWiki::Plugin::toggle;
-               $form->tmpl_param("javascript" => $IkiWiki::Plugin::toggle::javascript);
+               $form->tmpl_param("javascript" => IkiWiki::Plugin::toggle::include_javascript($params{page}, 1));
                # Start with the attachments interface toggled invisible,
                # but if it was used, keep it open.
                if ($form->submitted ne "Upload Attachment" &&
@@ -119,42 +103,9 @@ sub formbuilder_setup (@) { #{{{
                        $form->tmpl_param("attachments-class" => "toggleable-open");
                }
        }
-       elsif ($form->title eq "preferences") {
-               # XXX deprecated, should remove eventually
-               my $session=$params{session};
-               my $user_name=$session->param("name");
-
-               $form->field(name => "allowed_attachments", size => 50,
-                       fieldset => "admin",
-                       comment => "deprecated; please move to allowed_attachments in setup file",
-               );
-               if (! IkiWiki::is_admin($user_name)) {
-                       $form->field(name => "allowed_attachments", type => "hidden");
-               }
-                if (! $form->submitted) {
-                       my $value=IkiWiki::userinfo_get($user_name, "allowed_attachments");
-                       if (length $value) {
-                               $form->field(name => "allowed_attachments", force => 1,
-                                       value => IkiWiki::userinfo_get($user_name, "allowed_attachments"));
-                       }
-                       else {
-                               $form->field(name => "allowed_attachments", type => "hidden");
-                       }
-                }
-               if ($form->submitted && $form->submitted eq 'Save Preferences') {
-                       if (defined $form->field("allowed_attachments")) {
-                               IkiWiki::userinfo_set($user_name, "allowed_attachments",
-                               $form->field("allowed_attachments")) ||
-                                       error("failed to set allowed_attachments");
-                               if (! length $form->field("allowed_attachments")) {
-                                       $form->field(name => "allowed_attachments", type => "hidden");
-                               }
-                       }
-               }
-       }
-} #}}}
+}
 
-sub formbuilder (@) { #{{{
+sub formbuilder (@) {
        my %params=@_;
        my $form=$params{form};
        my $q=$params{cgi};
@@ -252,9 +203,9 @@ sub formbuilder (@) { #{{{
        # Generate the attachment list only after having added any new
        # attachments.
        $form->tmpl_param("attachment_list" => [attachment_list($form->field('page'))]);
-} # }}}
+}
 
-sub attachment_location ($) { #{{{
+sub attachment_location ($) {
        my $page=shift;
        
        # Put the attachment in a subdir of the page it's attached
@@ -263,9 +214,9 @@ sub attachment_location ($) { #{{{
        $page.="/" if length $page;
        
        return $page;
-} #}}}
+}
 
-sub attachment_list ($) { #{{{
+sub attachment_list ($) {
        my $page=shift;
        my $loc=attachment_location($page);
 
@@ -279,7 +230,6 @@ sub attachment_list ($) { #{{{
                                link => htmllink($page, $page, $f, noimageinline => 1),
                                size => IkiWiki::Plugin::filecheck::humansize((stat(_))[7]),
                                mtime => displaytime($IkiWiki::pagemtime{$f}),
-                               mtime_raw => $IkiWiki::pagemtime{$f},
                        };
                }
        }
@@ -287,6 +237,6 @@ sub attachment_list ($) { #{{{
        # Sort newer attachments to the top of the list, so a newly-added
        # attachment appears just before the form used to add it.
        return sort { $b->{mtime_raw} <=> $a->{mtime_raw} || $a->{link} cmp $b->{link} } @ret;
-} #}}}
+}
 
 1
index d1b3edb1f3cb6394d6bda558aed507fee627b4c3..555856b1189fe4508ac25997cad15669fb28ceea 100644 (file)
@@ -3,23 +3,23 @@ package IkiWiki::Plugin::autoindex;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 use Encode;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "autoindex", call => \&getsetup);
        hook(type => "refresh", id => "autoindex", call => \&refresh);
-} # }}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
                        rebuild => 0,
                },
-} #}}}
+}
 
-sub genindex ($) { #{{{
+sub genindex ($) {
        my $page=shift;
        my $file=newpagefile($page, $config{default_pageext});
        my $template=template("autoindex.tmpl");
@@ -28,9 +28,9 @@ sub genindex ($) { #{{{
        if ($config{rcs}) {
                IkiWiki::rcs_add($file);
        }
-} #}}}
+}
 
-sub refresh () { #{{{
+sub refresh () {
        eval q{use File::Find};
        error($@) if $@;
 
@@ -107,6 +107,6 @@ sub refresh () { #{{{
                        IkiWiki::enable_commit_hook();
                }
        }
-} #}}}
+}
 
 1
diff --git a/IkiWiki/Plugin/blogspam.pm b/IkiWiki/Plugin/blogspam.pm
new file mode 100644 (file)
index 0000000..cbd9859
--- /dev/null
@@ -0,0 +1,116 @@
+#!/usr/bin/perl
+package IkiWiki::Plugin::blogspam;
+
+use warnings;
+use strict;
+use IkiWiki 3.00;
+
+my $defaulturl='http://test.blogspam.net:8888/';
+
+sub import {
+       hook(type => "getsetup", id => "blogspam",  call => \&getsetup);
+       hook(type => "checkcontent", id => "blogspam", call => \&checkcontent);
+}
+
+sub getsetup () {
+       return
+               plugin => {
+                       safe => 1,
+                       rebuild => 0,
+               },
+               blogspam_pagespec => {
+                       type => 'pagespec',
+                       example => 'postcomment(*)',
+                       description => 'PageSpec of pages to check for spam',
+                       link => 'ikiwiki/PageSpec',
+                       safe => 1,
+                       rebuild => 0,
+               },
+               blogspam_options => {
+                       type => "string",
+                       example => "blacklist=1.2.3.4,blacklist=8.7.6.5,max-links=10",
+                       description => "options to send to blogspam server",
+                       link => "http://blogspam.net/api/testComment.html#options",
+                       safe => 1,
+                       rebuild => 0,
+               },
+               blogspam_server => {
+                       type => "string",
+                       default => $defaulturl,
+                       description => "blogspam server XML-RPC url",
+                       safe => 1,
+                       rebuild => 0,
+               },
+}
+
+sub checkcontent (@) {
+       my %params=@_;
+
+       eval q{
+               use RPC::XML;
+               use RPC::XML::Client;
+       };
+       if ($@) {
+               warn($@);
+               return undef;
+       }
+       
+       if (exists $config{blogspam_pagespec}) {
+               return undef
+                       if ! pagespec_match($params{page}, $config{blogspam_pagespec},
+                               location => $params{page});
+       }
+
+       my $url=$defaulturl;
+       $url = $config{blogspam_server} if exists $config{blogspam_server};
+       my $client = RPC::XML::Client->new($url);
+
+       my @options = split(",", $config{blogspam_options})
+               if exists $config{blogspam_options};
+
+       # Allow short comments and whitespace-only edits, unless the user
+       # has overridden min-words themselves.
+       push @options, "min-words=0"
+               unless grep /^min-words=/i, @options;
+       # Wiki pages can have a lot of urls, unless the user specifically
+       # wants to limit them.
+       push @options, "exclude=lotsaurls"
+               unless grep /^max-links/i, @options;
+       # Unless the user specified a size check, disable such checking.
+       push @options, "exclude=size"
+               unless grep /^(?:max|min)-size/i, @options;
+       # This test has absurd false positives on words like "alpha"
+       # and "buy".
+       push @options, "exclude=stopwords";
+
+       my %req=(
+               ip => $ENV{REMOTE_ADDR},
+               comment => $params{content},
+               subject => defined $params{subject} ? $params{subject} : "",
+               name => defined $params{author} ? $params{author} : "",
+               link => exists $params{url} ? $params{url} : "",
+               options => join(",", @options),
+               site => $config{url},
+               version => "ikiwiki ".$IkiWiki::version,
+       );
+       my $res = $client->send_request('testComment', \%req);
+
+       if (! ref $res || ! defined $res->value) {
+               debug("failed to get response from blogspam server ($url)");
+               return undef;
+       }
+       elsif ($res->value =~ /^SPAM:(.*)/) {
+               eval q{use Data::Dumper};
+               debug("blogspam server reports ".$res->value.": ".Dumper(\%req));
+               return gettext("Sorry, but that looks like spam to <a href=\"http://blogspam.net/\">blogspam</a>: ").$1;
+       }
+       elsif ($res->value ne 'OK') {
+               debug("blogspam server failure: ".$res->value);
+               return undef;
+       }
+       else {
+               return undef;
+       }
+}
+
+1
index 37752dd3ef9e9b4113bfcaa1a9b49bb0a4e06bab..bf0d7560dbc8a86a28b1ad694d9de326d0153f1f 100644 (file)
@@ -4,22 +4,22 @@ package IkiWiki::Plugin::brokenlinks;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "brokenlinks", call => \&getsetup);
        hook(type => "preprocess", id => "brokenlinks", call => \&preprocess);
-} # }}}
+}
 
-sub getsetup { #{{{
+sub getsetup {
        return
                plugin => {
                        safe => 1,
                        rebuild => undef,
                },
-} #}}}
+}
 
-sub preprocess (@) { #{{{
+sub preprocess (@) {
        my %params=@_;
        $params{pages}="*" unless defined $params{pages};
        
@@ -61,6 +61,6 @@ sub preprocess (@) { #{{{
                        }
                        sort @broken)
                ."</ul>\n";
-} # }}}
+}
 
 1
index 101e91b930836126f9ef4a00b6a722af42cf5c50..8830073672663b77d2e21fb837a15a913b7b6c0d 100644 (file)
@@ -7,7 +7,7 @@ use IkiWiki;
 use Encode;
 use open qw{:utf8 :std};
 
-sub import { #{{{
+sub import {
        hook(type => "checkconfig", id => "bzr", call => \&checkconfig);
        hook(type => "getsetup", id => "bzr", call => \&getsetup);
        hook(type => "rcs", id => "rcs_update", call => \&rcs_update);
@@ -20,18 +20,18 @@ sub import { #{{{
        hook(type => "rcs", id => "rcs_recentchanges", call => \&rcs_recentchanges);
        hook(type => "rcs", id => "rcs_diff", call => \&rcs_diff);
        hook(type => "rcs", id => "rcs_getctime", call => \&rcs_getctime);
-} #}}}
+}
 
-sub checkconfig () { #{{{
+sub checkconfig () {
        if (defined $config{bzr_wrapper} && length $config{bzr_wrapper}) {
                push @{$config{wrappers}}, {
                        wrapper => $config{bzr_wrapper},
                        wrappermode => (defined $config{bzr_wrappermode} ? $config{bzr_wrappermode} : "06755"),
                };
        }
-} #}}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 0, # rcs plugin
@@ -65,9 +65,9 @@ sub getsetup () { #{{{
                        safe => 1,
                        rebuild => 1,
                },
-} #}}}
+}
 
-sub bzr_log ($) { #{{{
+sub bzr_log ($) {
        my $out = shift;
        my @infos = ();
        my $key = undef;
@@ -99,20 +99,20 @@ sub bzr_log ($) { #{{{
        close $out;
 
        return @infos;
-} #}}}
+}
 
-sub rcs_update () { #{{{
+sub rcs_update () {
        my @cmdline = ("bzr", "update", "--quiet", $config{srcdir});
        if (system(@cmdline) != 0) {
                warn "'@cmdline' failed: $!";
        }
-} #}}}
+}
 
-sub rcs_prepedit ($) { #{{{
+sub rcs_prepedit ($) {
        return "";
-} #}}}
+}
 
-sub bzr_author ($$) { #{{{
+sub bzr_author ($$) {
        my ($user, $ipaddr) = @_;
 
        if (defined $user) {
@@ -124,9 +124,9 @@ sub bzr_author ($$) { #{{{
        else {
                return "Anonymous";
        }
-} #}}}
+}
 
-sub rcs_commit ($$$;$$) { #{{{
+sub rcs_commit ($$$;$$) {
        my ($file, $message, $rcstoken, $user, $ipaddr) = @_;
 
        $user = bzr_author($user, $ipaddr);
@@ -143,7 +143,7 @@ sub rcs_commit ($$$;$$) { #{{{
        }
 
        return undef; # success
-} #}}}
+}
 
 sub rcs_commit_staged ($$$) {
        # Commits all staged changes. Changes can be staged using rcs_add,
@@ -164,27 +164,27 @@ sub rcs_commit_staged ($$$) {
        }
 
        return undef; # success
-} #}}}
+}
 
-sub rcs_add ($) { # {{{
+sub rcs_add ($) {
        my ($file) = @_;
 
        my @cmdline = ("bzr", "add", "--quiet", "$config{srcdir}/$file");
        if (system(@cmdline) != 0) {
                warn "'@cmdline' failed: $!";
        }
-} #}}}
+}
 
-sub rcs_remove ($) { # {{{
+sub rcs_remove ($) {
        my ($file) = @_;
 
        my @cmdline = ("bzr", "rm", "--force", "--quiet", "$config{srcdir}/$file");
        if (system(@cmdline) != 0) {
                warn "'@cmdline' failed: $!";
        }
-} #}}}
+}
 
-sub rcs_rename ($$) { # {{{
+sub rcs_rename ($$) {
        my ($src, $dest) = @_;
 
        my $parent = IkiWiki::dirname($dest);
@@ -196,9 +196,9 @@ sub rcs_rename ($$) { # {{{
        if (system(@cmdline) != 0) {
                warn "'@cmdline' failed: $!";
        }
-} #}}}
+}
 
-sub rcs_recentchanges ($) { #{{{
+sub rcs_recentchanges ($) {
        my ($num) = @_;
 
        my @cmdline = ("bzr", "log", "-v", "--show-ids", "--limit", $num, 
@@ -246,16 +246,36 @@ sub rcs_recentchanges ($) { #{{{
                        rev        => $info->{"revno"},
                        user       => $user,
                        committype => "bzr",
-                       when       => time - str2time($info->{"timestamp"}),
+                       when       => str2time($info->{"timestamp"}),
                        message    => [@message],
                        pages      => [@pages],
                };
        }
 
        return @ret;
-} #}}}
+}
 
-sub rcs_getctime ($) { #{{{
+sub rcs_diff ($) {
+       my $taintedrev=shift;
+       my ($rev) = $taintedrev =~ /^(\d+(\.\d+)*)$/; # untaint
+
+       my $prevspec = "before:" . $rev;
+       my $revspec = "revno:" . $rev;
+       my @cmdline = ("bzr", "diff", "--old", $config{srcdir},
+               "--new", $config{srcdir},
+               "-r", $prevspec . ".." . $revspec);
+       open (my $out, "@cmdline |");
+
+       my @lines = <$out>;
+       if (wantarray) {
+               return @lines;
+       }
+       else {
+               return join("", @lines);
+       }
+}
+
+sub rcs_getctime ($) {
        my ($file) = @_;
 
        # XXX filename passes through the shell here, should try to avoid
@@ -274,6 +294,6 @@ sub rcs_getctime ($) { #{{{
        
        my $ctime = str2time($log[0]->{"timestamp"});
        return $ctime;
-} #}}}
+}
 
 1
index 6d536a91bb63a6e0a32f399844115e52208d5bdf..d473c8348234145b5dc7526584b9b7825b027e4a 100644 (file)
@@ -20,7 +20,7 @@ package IkiWiki::Plugin::calendar;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 use Time::Local;
 use POSIX;
 
@@ -29,13 +29,13 @@ my %linkcache;
 my $time=time;
 my @now=localtime($time);
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "calendar", call => \&getsetup);
        hook(type => "needsbuild", id => "calendar", call => \&needsbuild);
        hook(type => "preprocess", id => "calendar", call => \&preprocess);
-} #}}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
@@ -48,23 +48,23 @@ sub getsetup () { #{{{
                        safe => 1,
                        rebuild => 1,
                },
-} #}}}
+}
 
-sub is_leap_year (@) { #{{{
+sub is_leap_year (@) {
        my %params=@_;
        return ($params{year} % 4 == 0 && (($params{year} % 100 != 0) || $params{year} % 400 == 0));
-} #}}}
+}
 
-sub month_days { #{{{
+sub month_days {
        my %params=@_;
        my $days_in_month = (31,28,31,30,31,30,31,31,30,31,30,31)[$params{month}-1];
        if ($params{month} == 2 && is_leap_year(%params)) {
                $days_in_month++;
        }
        return $days_in_month;
-} #}}}
+}
 
-sub format_month (@) { #{{{
+sub format_month (@) {
        my %params=@_;
 
        my $pagespec = $params{pages};
@@ -215,9 +215,9 @@ EOF
         add_depends($params{page}, join(" or ", @list));
 
        return $calendar;
-} #}}}
+}
 
-sub format_year (@) { #{{{
+sub format_year (@) {
        my %params=@_;
 
        my $pagespec = $params{pages};
@@ -318,9 +318,9 @@ EOF
 EOF
 
        return $calendar;
-} #}}}
+}
 
-sub preprocess (@) { #{{{
+sub preprocess (@) {
        my %params=@_;
        $params{pages} = "*"            unless defined $params{pages};
        $params{type}  = "month"        unless defined $params{type};
@@ -397,7 +397,7 @@ sub preprocess (@) { #{{{
        return "\n<div><div class=\"calendar\">$calendar</div></div>\n";
 } #}}
 
-sub needsbuild (@) { #{{{
+sub needsbuild (@) {
        my $needsbuild=shift;
        foreach my $page (keys %pagestate) {
                if (exists $pagestate{$page}{calendar}{nextchange}) {
@@ -415,6 +415,6 @@ sub needsbuild (@) { #{{{
                        }
                }
        }
-} # }}}
+}
 
 1
index 7881becd5a1174f3539a93385d415a906574ada7..74a8397d7ebe33096393d4174f2975a4ce7bd03d 100644 (file)
@@ -4,7 +4,7 @@ package IkiWiki::Plugin::camelcase;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
 # This regexp is based on the one in Text::WikiFormat.
 my $link_regexp=qr{
@@ -22,40 +22,52 @@ my $link_regexp=qr{
        )
 }x;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "camelcase", call => \&getsetup);
        hook(type => "linkify", id => "camelcase", call => \&linkify);
        hook(type => "scan", id => "camelcase", call => \&scan);
-} # }}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
                        rebuild => undef,
-               };
-} #}}}
+               },
+               camelcase_ignore => {
+                       type => "string",
+                       example => [],
+                       description => "list of words to not turn into links",
+                       safe => 1,
+                       rebuild => undef, # might change links
+               },
+}
 
-sub linkify (@) { #{{{
+sub linkify (@) {
        my %params=@_;
        my $page=$params{page};
        my $destpage=$params{destpage};
 
        $params{content}=~s{$link_regexp}{
-               htmllink($page, $destpage, linkpage($1))
+               ignored($1) ? $1 : htmllink($page, $destpage, linkpage($1))
        }eg;
 
        return $params{content};
-} #}}}
+}
 
-sub scan (@) { #{{{
+sub scan (@) {
         my %params=@_;
         my $page=$params{page};
         my $content=$params{content};
 
        while ($content =~ /$link_regexp/g) {
-               push @{$links{$page}}, linkpage($1);
+               push @{$links{$page}}, linkpage($1) unless ignored($1)
        }
 }
 
+sub ignored ($) {
+       my $word=lc shift;
+       grep { $word eq lc $_ } @{$config{'camelcase_ignore'}}
+}
+
 1
index ac702ff026052e34b780ec380cf3cc048b7443bc..20505893b5885847370a99c84c900d6032d72ef9 100644 (file)
@@ -5,14 +5,14 @@ package IkiWiki::Plugin::color;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
-sub import { #{{{
+sub import {
        hook(type => "preprocess", id => "color", call => \&preprocess);
        hook(type => "format",     id => "color", call => \&format);
-} #}}}
+}
 
-sub preserve_style ($$$) { #{{{
+sub preserve_style ($$$) {
        my $foreground = shift;
        my $background = shift;
        my $text       = shift;
@@ -37,18 +37,18 @@ sub preserve_style ($$$) { #{{{
        
        return $preserved;
 
-} #}}}
+}
 
-sub replace_preserved_style ($) { #{{{
+sub replace_preserved_style ($) {
        my $content = shift;
 
        $content =~ s!<span class="color">((color: ([a-z]+|\#[0-9a-f]{3,6})?)?((; )?(background-color: ([a-z]+|\#[0-9a-f]{3,6})?)?)?)</span>!<span class="color" style="$1">!g;
        $content =~ s!<span class="colorend">!!g;
 
        return $content;
-} #}}}
+}
 
-sub preprocess (@) { #{{{
+sub preprocess (@) {
        my %params = @_;
 
        # Preprocess the text to expand any preprocessor directives
@@ -57,13 +57,13 @@ sub preprocess (@) { #{{{
                                IkiWiki::filter($params{page}, $params{destpage}, $params{text}));
 
        return preserve_style($params{foreground}, $params{background}, $params{text});
-} #}}}
+}
 
-sub format (@) { #{{{
+sub format (@) {
        my %params = @_;
 
        $params{content} = replace_preserved_style($params{content});
        return $params{content};        
-} #}}}
+}
 
 1
diff --git a/IkiWiki/Plugin/comments.pm b/IkiWiki/Plugin/comments.pm
new file mode 100644 (file)
index 0000000..98f9f8b
--- /dev/null
@@ -0,0 +1,840 @@
+#!/usr/bin/perl
+# Copyright © 2006-2008 Joey Hess <joey@ikiwiki.info>
+# Copyright © 2008 Simon McVittie <http://smcv.pseudorandom.co.uk/>
+# Licensed under the GNU GPL, version 2, or any later version published by the
+# Free Software Foundation
+package IkiWiki::Plugin::comments;
+
+use warnings;
+use strict;
+use IkiWiki 3.00;
+use Encode;
+use POSIX qw(strftime);
+
+use constant PREVIEW => "Preview";
+use constant POST_COMMENT => "Post comment";
+use constant CANCEL => "Cancel";
+
+my $postcomment;
+my %commentstate;
+
+sub import {
+       hook(type => "checkconfig", id => 'comments',  call => \&checkconfig);
+       hook(type => "getsetup", id => 'comments',  call => \&getsetup);
+       hook(type => "preprocess", id => '_comment', call => \&preprocess);
+       hook(type => "sessioncgi", id => 'comment', call => \&sessioncgi);
+       hook(type => "htmlize", id => "_comment", call => \&htmlize);
+       hook(type => "pagetemplate", id => "comments", call => \&pagetemplate);
+       hook(type => "formbuilder_setup", id => "comments", call => \&formbuilder_setup);
+       # Load goto to fix up user page links for logged-in commenters
+       IkiWiki::loadplugin("goto");
+       IkiWiki::loadplugin("inline");
+}
+
+sub getsetup () {
+       return
+               plugin => {
+                       safe => 1,
+                       rebuild => 1,
+               },
+               comments_pagespec => {
+                       type => 'pagespec',
+                       example => 'blog/* and !*/Discussion',
+                       description => 'PageSpec of pages where comments are allowed',
+                       link => 'ikiwiki/PageSpec',
+                       safe => 1,
+                       rebuild => 1,
+               },
+               comments_closed_pagespec => {
+                       type => 'pagespec',
+                       example => 'blog/controversial or blog/flamewar',
+                       description => 'PageSpec of pages where posting new comments is not allowed',
+                       link => 'ikiwiki/PageSpec',
+                       safe => 1,
+                       rebuild => 1,
+               },
+               comments_pagename => {
+                       type => 'string',
+                       default => 'comment_',
+                       description => 'Base name for comments, e.g. "comment_" for pages like "sandbox/comment_12"',
+                       safe => 0, # manual page moving required
+                       rebuild => undef,
+               },
+               comments_allowdirectives => {
+                       type => 'boolean',
+                       example => 0,
+                       description => 'Interpret directives in comments?',
+                       safe => 1,
+                       rebuild => 0,
+               },
+               comments_allowauthor => {
+                       type => 'boolean',
+                       example => 0,
+                       description => 'Allow anonymous commenters to set an author name?',
+                       safe => 1,
+                       rebuild => 0,
+               },
+               comments_commit => {
+                       type => 'boolean',
+                       example => 1,
+                       description => 'commit comments to the VCS',
+                       # old uncommitted comments are likely to cause
+                       # confusion if this is changed
+                       safe => 0,
+                       rebuild => 0,
+               },
+}
+
+sub checkconfig () {
+       $config{comments_commit} = 1
+               unless defined $config{comments_commit};
+       $config{comments_pagespec} = ''
+               unless defined $config{comments_pagespec};
+       $config{comments_closed_pagespec} = ''
+               unless defined $config{comments_closed_pagespec};
+       $config{comments_pagename} = 'comment_'
+               unless defined $config{comments_pagename};
+}
+
+sub htmlize {
+       my %params = @_;
+       return $params{content};
+}
+
+# FIXME: copied verbatim from meta
+sub safeurl ($) {
+       my $url=shift;
+       if (exists $IkiWiki::Plugin::htmlscrubber::{safe_url_regexp} &&
+           defined $IkiWiki::Plugin::htmlscrubber::safe_url_regexp) {
+               return $url=~/$IkiWiki::Plugin::htmlscrubber::safe_url_regexp/;
+       }
+       else {
+               return 1;
+       }
+}
+
+sub preprocess {
+       my %params = @_;
+       my $page = $params{page};
+
+       my $format = $params{format};
+       if (defined $format && ! exists $IkiWiki::hooks{htmlize}{$format}) {
+               error(sprintf(gettext("unsupported page format %s"), $format));
+       }
+
+       my $content = $params{content};
+       if (! defined $content) {
+               error(gettext("comment must have content"));
+       }
+       $content =~ s/\\"/"/g;
+
+       $content = IkiWiki::filter($page, $params{destpage}, $content);
+
+       if ($config{comments_allowdirectives}) {
+               $content = IkiWiki::preprocess($page, $params{destpage},
+                       $content);
+       }
+
+       # no need to bother with htmlize if it's just HTML
+       $content = IkiWiki::htmlize($page, $params{destpage}, $format, $content)
+               if defined $format;
+
+       IkiWiki::run_hooks(sanitize => sub {
+               $content = shift->(
+                       page => $page,
+                       destpage => $params{destpage},
+                       content => $content,
+               );
+       });
+
+       # set metadata, possibly overriding [[!meta]] directives from the
+       # comment itself
+
+       my $commentuser;
+       my $commentip;
+       my $commentauthor;
+       my $commentauthorurl;
+       my $commentopenid;
+       if (defined $params{username}) {
+               $commentuser = $params{username};
+
+               my $oiduser = eval { IkiWiki::openiduser($commentuser) };
+
+               if (defined $oiduser) {
+                       # looks like an OpenID
+                       $commentauthorurl = $commentuser;
+                       $commentauthor = $oiduser;
+                       $commentopenid = $commentuser;
+               }
+               else {
+                       $commentauthorurl = IkiWiki::cgiurl(
+                               do => 'goto',
+                               page => (length $config{userdir}
+                                       ? "$config{userdir}/$commentuser"
+                                       : "$commentuser"));
+
+                       $commentauthor = $commentuser;
+               }
+       }
+       else {
+               if (defined $params{ip}) {
+                       $commentip = $params{ip};
+               }
+               $commentauthor = gettext("Anonymous");
+       }
+
+       $commentstate{$page}{commentuser} = $commentuser;
+       $commentstate{$page}{commentopenid} = $commentopenid;
+       $commentstate{$page}{commentip} = $commentip;
+       $commentstate{$page}{commentauthor} = $commentauthor;
+       $commentstate{$page}{commentauthorurl} = $commentauthorurl;
+       if (! defined $pagestate{$page}{meta}{author}) {
+               $pagestate{$page}{meta}{author} = $commentauthor;
+       }
+       if (! defined $pagestate{$page}{meta}{authorurl}) {
+               $pagestate{$page}{meta}{authorurl} = $commentauthorurl;
+       }
+
+       if ($config{comments_allowauthor}) {
+               if (defined $params{claimedauthor}) {
+                       $pagestate{$page}{meta}{author} = $params{claimedauthor};
+               }
+
+               if (defined $params{url}) {
+                       my $url=$params{url};
+
+                       eval q{use URI::Heuristic}; 
+                       if (! $@) {
+                               $url=URI::Heuristic::uf_uristr($url);
+                       }
+
+                       if (safeurl($url)) {
+                               $pagestate{$page}{meta}{authorurl} = $url;
+                       }
+               }
+       }
+       else {
+               $pagestate{$page}{meta}{author} = $commentauthor;
+               $pagestate{$page}{meta}{authorurl} = $commentauthorurl;
+       }
+
+       if (defined $params{subject}) {
+               $pagestate{$page}{meta}{title} = $params{subject};
+       }
+
+       if ($params{page} =~ m/\/(\Q$config{comments_pagename}\E\d+)$/) {
+               $pagestate{$page}{meta}{permalink} = urlto(IkiWiki::dirname($params{page}), undef, 1).
+                       "#".page_to_id($params{page});
+       }
+
+       eval q{use Date::Parse};
+       if (! $@) {
+               my $time = str2time($params{date});
+               $IkiWiki::pagectime{$page} = $time if defined $time;
+       }
+
+       return $content;
+}
+
+sub sessioncgi ($$) {
+       my $cgi=shift;
+       my $session=shift;
+
+       my $do = $cgi->param('do');
+       if ($do eq 'comment') {
+               editcomment($cgi, $session);
+       }
+       elsif ($do eq 'commentmoderation') {
+               commentmoderation($cgi, $session);
+       }
+}
+
+# Mostly cargo-culted from IkiWiki::plugin::editpage
+sub editcomment ($$) {
+       my $cgi=shift;
+       my $session=shift;
+
+       IkiWiki::decode_cgi_utf8($cgi);
+
+       eval q{use CGI::FormBuilder};
+       error($@) if $@;
+
+       my @buttons = (POST_COMMENT, PREVIEW, CANCEL);
+       my $form = CGI::FormBuilder->new(
+               fields => [qw{do sid page subject editcontent type author url}],
+               charset => 'utf-8',
+               method => 'POST',
+               required => [qw{editcontent}],
+               javascript => 0,
+               params => $cgi,
+               action => $config{cgiurl},
+               header => 0,
+               table => 0,
+               template => scalar IkiWiki::template_params('editcomment.tmpl'),
+       );
+
+       IkiWiki::decode_form_utf8($form);
+       IkiWiki::run_hooks(formbuilder_setup => sub {
+                       shift->(title => "comment", form => $form, cgi => $cgi,
+                               session => $session, buttons => \@buttons);
+               });
+       IkiWiki::decode_form_utf8($form);
+
+       my $type = $form->param('type');
+       if (defined $type && length $type && $IkiWiki::hooks{htmlize}{$type}) {
+               $type = IkiWiki::possibly_foolish_untaint($type);
+       }
+       else {
+               $type = $config{default_pageext};
+       }
+       my @page_types;
+       if (exists $IkiWiki::hooks{htmlize}) {
+               @page_types = grep { ! /^_/ } keys %{$IkiWiki::hooks{htmlize}};
+       }
+
+       $form->field(name => 'do', type => 'hidden');
+       $form->field(name => 'sid', type => 'hidden', value => $session->id,
+               force => 1);
+       $form->field(name => 'page', type => 'hidden');
+       $form->field(name => 'subject', type => 'text', size => 72);
+       $form->field(name => 'editcontent', type => 'textarea', rows => 10);
+       $form->field(name => "type", value => $type, force => 1,
+               type => 'select', options => \@page_types);
+
+       $form->tmpl_param(username => $session->param('name'));
+
+       if ($config{comments_allowauthor} and
+           ! defined $session->param('name')) {
+               $form->tmpl_param(allowauthor => 1);
+               $form->field(name => 'author', type => 'text', size => '40');
+               $form->field(name => 'url', type => 'text', size => '40');
+       }
+       else {
+               $form->tmpl_param(allowauthor => 0);
+               $form->field(name => 'author', type => 'hidden', value => '',
+                       force => 1);
+               $form->field(name => 'url', type => 'hidden', value => '',
+                       force => 1);
+       }
+
+       # The untaint is OK (as in editpage) because we're about to pass
+       # it to file_pruned anyway
+       my $page = $form->field('page');
+       $page = IkiWiki::possibly_foolish_untaint($page);
+       if (! defined $page || ! length $page ||
+               IkiWiki::file_pruned($page, $config{srcdir})) {
+               error(gettext("bad page name"));
+       }
+
+       my $baseurl = urlto($page, undef, 1);
+
+       $form->title(sprintf(gettext("commenting on %s"),
+                       IkiWiki::pagetitle($page)));
+
+       $form->tmpl_param('helponformattinglink',
+               htmllink($page, $page, 'ikiwiki/formatting',
+                       noimageinline => 1,
+                       linktext => 'FormattingHelp'),
+                       allowdirectives => $config{allow_directives});
+
+       if ($form->submitted eq CANCEL) {
+               # bounce back to the page they wanted to comment on, and exit.
+               # CANCEL need not be considered in future
+               IkiWiki::redirect($cgi, urlto($page, undef, 1));
+               exit;
+       }
+
+       if (not exists $pagesources{$page}) {
+               error(sprintf(gettext(
+                       "page '%s' doesn't exist, so you can't comment"),
+                       $page));
+       }
+
+       if (pagespec_match($page, $config{comments_closed_pagespec},
+               location => $page)) {
+               error(sprintf(gettext(
+                       "comments on page '%s' are closed"),
+                       $page));
+       }
+
+       # Set a flag to indicate that we're posting a comment,
+       # so that postcomment() can tell it should match.
+       $postcomment=1;
+       IkiWiki::check_canedit($page, $cgi, $session);
+       $postcomment=0;
+
+       my $location=unique_comment_location($page, $config{srcdir});
+
+       my $content = "[[!_comment format=$type\n";
+
+       # FIXME: handling of double quotes probably wrong?
+       if (defined $session->param('name')) {
+               my $username = $session->param('name');
+               $username =~ s/"/&quot;/g;
+               $content .= " username=\"$username\"\n";
+       }
+       elsif (defined $ENV{REMOTE_ADDR}) {
+               my $ip = $ENV{REMOTE_ADDR};
+               if ($ip =~ m/^([.0-9]+)$/) {
+                       $content .= " ip=\"$1\"\n";
+               }
+       }
+
+       if ($config{comments_allowauthor}) {
+               my $author = $form->field('author');
+               if (defined $author && length $author) {
+                       $author =~ s/"/&quot;/g;
+                       $content .= " claimedauthor=\"$author\"\n";
+               }
+               my $url = $form->field('url');
+               if (defined $url && length $url) {
+                       $url =~ s/"/&quot;/g;
+                       $content .= " url=\"$url\"\n";
+               }
+       }
+
+       my $subject = $form->field('subject');
+       if (defined $subject && length $subject) {
+               $subject =~ s/"/&quot;/g;
+               $content .= " subject=\"$subject\"\n";
+       }
+
+       $content .= " date=\"" . decode_utf8(strftime('%Y-%m-%dT%H:%M:%SZ', gmtime)) . "\"\n";
+
+       my $editcontent = $form->field('editcontent') || '';
+       $editcontent =~ s/\r\n/\n/g;
+       $editcontent =~ s/\r/\n/g;
+       $editcontent =~ s/"/\\"/g;
+       $content .= " content=\"\"\"\n$editcontent\n\"\"\"]]\n";
+
+       # This is essentially a simplified version of editpage:
+       # - the user does not control the page that's created, only the parent
+       # - it's always a create operation, never an edit
+       # - this means that conflicts should never happen
+       # - this means that if they do, rocks fall and everyone dies
+
+       if ($form->submitted eq PREVIEW) {
+               my $preview=previewcomment($content, $location, $page, time);
+               IkiWiki::run_hooks(format => sub {
+                       $preview = shift->(page => $page,
+                               content => $preview);
+               });
+               $form->tmpl_param(page_preview => $preview);
+       }
+       else {
+               $form->tmpl_param(page_preview => "");
+       }
+
+       if ($form->submitted eq POST_COMMENT && $form->validate) {
+               IkiWiki::checksessionexpiry($cgi, $session);
+               
+               $postcomment=1;
+               my $ok=IkiWiki::check_content(content => $form->field('editcontent'),
+                       subject => $form->field('subject'),
+                       $config{comments_allowauthor} ? (
+                               author => $form->field('author'),
+                               url => $form->field('url'),
+                       ) : (),
+                       page => $location,
+                       cgi => $cgi,
+                       session => $session,
+                       nonfatal => 1,
+               );
+               $postcomment=0;
+
+               if (! $ok) {
+                       my $penddir=$config{wikistatedir}."/comments_pending";
+                       $location=unique_comment_location($page, $penddir);
+                       writefile("$location._comment", $penddir, $content);
+                       IkiWiki::printheader($session);
+                       print IkiWiki::misctemplate(gettext(gettext("comment stored for moderation")),
+                               "<p>".
+                               gettext("Your comment will be posted after moderator review").
+                               "</p>");
+                       exit;
+               }
+
+               # FIXME: could probably do some sort of graceful retry
+               # on error? Would require significant unwinding though
+               my $file = "$location._comment";
+               writefile($file, $config{srcdir}, $content);
+
+               my $conflict;
+
+               if ($config{rcs} and $config{comments_commit}) {
+                       my $message = gettext("Added a comment");
+                       if (defined $form->field('subject') &&
+                               length $form->field('subject')) {
+                               $message = sprintf(
+                                       gettext("Added a comment: %s"),
+                                       $form->field('subject'));
+                       }
+
+                       IkiWiki::rcs_add($file);
+                       IkiWiki::disable_commit_hook();
+                       $conflict = IkiWiki::rcs_commit_staged($message,
+                               $session->param('name'), $ENV{REMOTE_ADDR});
+                       IkiWiki::enable_commit_hook();
+                       IkiWiki::rcs_update();
+               }
+
+               # Now we need a refresh
+               require IkiWiki::Render;
+               IkiWiki::refresh();
+               IkiWiki::saveindex();
+
+               # this should never happen, unless a committer deliberately
+               # breaks it or something
+               error($conflict) if defined $conflict;
+
+               # Jump to the new comment on the page.
+               # The trailing question mark tries to avoid broken
+               # caches and get the most recent version of the page.
+               IkiWiki::redirect($cgi, urlto($page, undef, 1).
+                       "?updated#".page_to_id($location));
+
+       }
+       else {
+               IkiWiki::showform ($form, \@buttons, $session, $cgi,
+                       forcebaseurl => $baseurl);
+       }
+
+       exit;
+}
+
+sub commentmoderation ($$) {
+       my $cgi=shift;
+       my $session=shift;
+
+       IkiWiki::needsignin($cgi, $session);
+       if (! IkiWiki::is_admin($session->param("name"))) {
+               error(gettext("you are not logged in as an admin"));
+       }
+
+       IkiWiki::decode_cgi_utf8($cgi);
+       
+       if (defined $cgi->param('sid')) {
+               IkiWiki::checksessionexpiry($cgi, $session);
+
+               my $rejectalldefer=$cgi->param('rejectalldefer');
+
+               my %vars=$cgi->Vars;
+               my $added=0;
+               foreach my $id (keys %vars) {
+                       if ($id =~ /(.*)\Q._comment\E$/) {
+                               my $action=$cgi->param($id);
+                               next if $action eq 'Defer' && ! $rejectalldefer;
+
+                               # Make sure that the id is of a legal
+                               # pending comment before untainting.
+                               my ($f)= $id =~ /$config{wiki_file_regexp}/;
+                               if (! defined $f || ! length $f ||
+                                   IkiWiki::file_pruned($f, $config{srcdir})) {
+                                       error("illegal file");
+                               }
+
+                               my $page=IkiWiki::possibly_foolish_untaint(IkiWiki::dirname($1));
+                               my $file="$config{wikistatedir}/comments_pending/".
+                                       IkiWiki::possibly_foolish_untaint($id);
+
+                               if ($action eq 'Accept') {
+                                       my $content=eval { readfile($file) };
+                                       next if $@; # file vanished since form was displayed
+                                       my $dest=unique_comment_location($page, $config{srcdir})."._comment";
+                                       writefile($dest, $config{srcdir}, $content);
+                                       if ($config{rcs} and $config{comments_commit}) {
+                                               IkiWiki::rcs_add($dest);
+                                       }
+                                       $added++;
+                               }
+
+                               # This removes empty subdirs, so the
+                               # .ikiwiki/comments_pending dir will
+                               # go away when all are moderated.
+                               require IkiWiki::Render;
+                               IkiWiki::prune($file);
+                       }
+               }
+
+               if ($added) {
+                       my $conflict;
+                       if ($config{rcs} and $config{comments_commit}) {
+                               my $message = gettext("Comment moderation");
+                               IkiWiki::disable_commit_hook();
+                               $conflict=IkiWiki::rcs_commit_staged($message,
+                                       $session->param('name'), $ENV{REMOTE_ADDR});
+                               IkiWiki::enable_commit_hook();
+                               IkiWiki::rcs_update();
+                       }
+               
+                       # Now we need a refresh
+                       require IkiWiki::Render;
+                       IkiWiki::refresh();
+                       IkiWiki::saveindex();
+               
+                       error($conflict) if defined $conflict;
+               }
+       }
+
+       my @comments=map {
+               my ($id, $ctime)=@{$_};
+               my $file="$config{wikistatedir}/comments_pending/$id";
+               my $content=readfile($file);
+               my $preview=previewcomment($content, $id,
+                       IkiWiki::dirname($_), $ctime);
+               {
+                       id => $id,
+                       view => $preview,
+               } 
+       } sort { $b->[1] <=> $a->[1] } comments_pending();
+
+       my $template=template("commentmoderation.tmpl");
+       $template->param(
+               sid => $session->id,
+               comments => \@comments,
+       );
+       IkiWiki::printheader($session);
+       my $out=$template->output;
+       IkiWiki::run_hooks(format => sub {
+               $out = shift->(page => "", content => $out);
+       });
+       print IkiWiki::misctemplate(gettext("comment moderation"), $out);
+       exit;
+}
+
+sub formbuilder_setup (@) {
+       my %params=@_;
+
+       my $form=$params{form};
+       if ($form->title eq "preferences" &&
+           IkiWiki::is_admin($params{session}->param("name"))) {
+               push @{$params{buttons}}, "Comment Moderation";
+               if ($form->submitted && $form->submitted eq "Comment Moderation") {
+                       commentmoderation($params{cgi}, $params{session});
+               }
+       }
+}
+
+sub comments_pending () {
+       my $dir="$config{wikistatedir}/comments_pending/";
+       return unless -d $dir;
+
+       my @ret;
+       eval q{use File::Find};
+       error($@) if $@;
+       find({
+               no_chdir => 1,
+               wanted => sub {
+                       $_=decode_utf8($_);
+                       if (IkiWiki::file_pruned($_, $dir)) {
+                               $File::Find::prune=1;
+                       }
+                       elsif (! -l $_ && ! -d _) {
+                               $File::Find::prune=0;
+                               my ($f)=/$config{wiki_file_regexp}/; # untaint
+                               if (defined $f && $f =~ /\Q._comment\E$/) {
+                                       my $ctime=(stat($f))[10];
+                                       $f=~s/^\Q$dir\E\/?//;
+                                        push @ret, [$f, $ctime];
+                               }
+                       }
+               }
+       }, $dir);
+
+       return @ret;
+}
+
+sub previewcomment ($$$) {
+       my $content=shift;
+       my $location=shift;
+       my $page=shift;
+       my $time=shift;
+
+       my $preview = IkiWiki::htmlize($location, $page, '_comment',
+                       IkiWiki::linkify($location, $page,
+                       IkiWiki::preprocess($location, $page,
+                       IkiWiki::filter($location, $page, $content), 0, 1)));
+
+       my $template = template("comment.tmpl");
+       $template->param(content => $preview);
+       $template->param(ctime => displaytime($time));
+
+       IkiWiki::run_hooks(pagetemplate => sub {
+               shift->(page => $location,
+                       destpage => $page,
+                       template => $template);
+       });
+
+       $template->param(have_actions => 0);
+
+       return $template->output;
+}
+
+sub commentsshown ($) {
+       my $page=shift;
+
+       return ! pagespec_match($page, "internal(*/$config{comments_pagename}*)",
+                               location => $page) &&
+              pagespec_match($page, $config{comments_pagespec},
+                             location => $page);
+}
+
+sub commentsopen ($) {
+       my $page = shift;
+
+       return length $config{cgiurl} > 0 &&
+              (! length $config{comments_closed_pagespec} ||
+               ! pagespec_match($page, $config{comments_closed_pagespec},
+                                location => $page));
+}
+
+sub pagetemplate (@) {
+       my %params = @_;
+
+       my $page = $params{page};
+       my $template = $params{template};
+       my $shown = ($template->query(name => 'commentslink') ||
+                    $template->query(name => 'commentsurl') ||
+                    $template->query(name => 'atomcommentsurl') ||
+                    $template->query(name => 'comments')) &&
+                   commentsshown($page);
+
+       if ($template->query(name => 'comments')) {
+               my $comments = undef;
+               if ($shown) {
+                       $comments = IkiWiki::preprocess_inline(
+                               pages => "internal($page/$config{comments_pagename}*)",
+                               template => 'comment',
+                               show => 0,
+                               reverse => 'yes',
+                               page => $page,
+                               destpage => $params{destpage},
+                               feedfile => 'comments',
+                               emptyfeeds => 'no',
+                       );
+               }
+
+               if (defined $comments && length $comments) {
+                       $template->param(comments => $comments);
+               }
+
+               if ($shown && commentsopen($page)) {
+                       my $addcommenturl = IkiWiki::cgiurl(do => 'comment',
+                               page => $page);
+                       $template->param(addcommenturl => $addcommenturl);
+               }
+       }
+
+       if ($template->query(name => 'commentsurl')) {
+               if ($shown) {
+                       $template->param(commentsurl =>
+                               urlto($page, undef, 1).'#comments');
+               }
+       }
+
+       if ($template->query(name => 'atomcommentsurl') && $config{usedirs}) {
+               if ($shown) {
+                       # This will 404 until there are some comments, but I
+                       # think that's probably OK...
+                       $template->param(atomcommentsurl =>
+                               urlto($page, undef, 1).'comments.atom');
+               }
+       }
+
+       if ($template->query(name => 'commentslink')) {
+               # XXX Would be nice to say how many comments there are in
+               # the link. But, to update the number, blog pages
+               # would have to update whenever comments of any inlines
+               # page are added, which is not currently done.
+               if ($shown) {
+                       $template->param(commentslink =>
+                               htmllink($page, $params{destpage}, $page,
+                                       linktext => gettext("Comments"),
+                                       anchor => "comments",
+                                       noimageinline => 1));
+               }
+       }
+
+       # everything below this point is only relevant to the comments
+       # themselves
+       if (!exists $commentstate{$page}) {
+               return;
+       }
+       
+       if ($template->query(name => 'commentid')) {
+               $template->param(commentid => page_to_id($page));
+       }
+
+       if ($template->query(name => 'commentuser')) {
+               $template->param(commentuser =>
+                       $commentstate{$page}{commentuser});
+       }
+
+       if ($template->query(name => 'commentopenid')) {
+               $template->param(commentopenid =>
+                       $commentstate{$page}{commentopenid});
+       }
+
+       if ($template->query(name => 'commentip')) {
+               $template->param(commentip =>
+                       $commentstate{$page}{commentip});
+       }
+
+       if ($template->query(name => 'commentauthor')) {
+               $template->param(commentauthor =>
+                       $commentstate{$page}{commentauthor});
+       }
+
+       if ($template->query(name => 'commentauthorurl')) {
+               $template->param(commentauthorurl =>
+                       $commentstate{$page}{commentauthorurl});
+       }
+
+       if ($template->query(name => 'removeurl') &&
+           IkiWiki::Plugin::remove->can("check_canremove") &&
+           length $config{cgiurl}) {
+               $template->param(removeurl => IkiWiki::cgiurl(do => 'remove',
+                       page => $page));
+               $template->param(have_actions => 1);
+       }
+}
+
+sub unique_comment_location ($) {
+       my $page=shift;
+       my $dir=shift;
+
+       my $location;
+       my $i = 0;
+       do {
+               $i++;
+               $location = "$page/$config{comments_pagename}$i";
+       } while (-e "$dir/$location._comment");
+
+       return $location;
+}
+
+sub page_to_id ($) {
+       # Converts a comment page name into a unique, legal html id
+       # addtibute value, that can be used as an anchor to link to the
+       # comment.
+       my $page=shift;
+
+       eval q{use Digest::MD5 'md5_hex'};
+       error($@) if $@;
+
+       return "comment-".md5_hex($page);
+}
+       
+package IkiWiki::PageSpec;
+
+sub match_postcomment ($$;@) {
+       my $page = shift;
+       my $glob = shift;
+
+       if (! $postcomment) {
+               return IkiWiki::FailReason->new("not posting a comment");
+       }
+       return match_glob($page, $glob);
+}
+
+1
index e787424aaee5ca0e01c58f236a87f32c2d493e02..7445dbdad71420bb4228f573543ac673732b197c 100644 (file)
@@ -3,23 +3,23 @@ package IkiWiki::Plugin::conditional;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 use UNIVERSAL;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "conditional", call => \&getsetup);
        hook(type => "preprocess", id => "if", call => \&preprocess_if);
-} # }}}
+}
 
-sub getsetup { #{{{
+sub getsetup {
        return
                plugin => {
                        safe => 1,
                        rebuild => undef,
                },
-} #}}}
+}
 
-sub preprocess_if (@) { #{{{
+sub preprocess_if (@) {
        my %params=@_;
 
        foreach my $param (qw{test then}) {
@@ -66,11 +66,11 @@ sub preprocess_if (@) { #{{{
        }
        return IkiWiki::preprocess($params{page}, $params{destpage}, 
                IkiWiki::filter($params{page}, $params{destpage}, $ret));
-} # }}}
+}
 
 package IkiWiki::PageSpec;
 
-sub match_enabled ($$;@) { #{{{
+sub match_enabled ($$;@) {
        shift;
        my $plugin=shift;
        
@@ -81,12 +81,14 @@ sub match_enabled ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("$plugin is not enabled");
        }
-} #}}}
+}
 
-sub match_sourcepage ($$;@) { #{{{
+sub match_sourcepage ($$;@) {
        shift;
        my $glob=shift;
        my %params=@_;
+       
+       $glob=derel($glob, $params{location});
 
        return IkiWiki::FailReason->new("cannot match sourcepage") unless exists $params{sourcepage};
        if (match_glob($params{sourcepage}, $glob, @_)) {
@@ -95,13 +97,15 @@ sub match_sourcepage ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("sourcepage does not match $glob");
        }
-} #}}}
+}
 
-sub match_destpage ($$;@) { #{{{
+sub match_destpage ($$;@) {
        shift;
        my $glob=shift;
        my %params=@_;
        
+       $glob=derel($glob, $params{location});
+
        return IkiWiki::FailReason->new("cannot match destpage") unless exists $params{destpage};
        if (match_glob($params{destpage}, $glob, @_)) {
                return IkiWiki::SuccessReason->new("destpage matches $glob");
@@ -109,9 +113,9 @@ sub match_destpage ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("destpage does not match $glob");
        }
-} #}}}
+}
 
-sub match_included ($$;@) { #{{{
+sub match_included ($$;@) {
        shift;
        shift;
        my %params=@_;
@@ -123,6 +127,6 @@ sub match_included ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("page $params{sourcepage} is not included");
        }
-} #}}}
+}
 
 1
index 7c729300d9a7e964a72dd137e67a6e42e3fc9991..425e71043f0133f5b855c32932bc114542c83d7d 100644 (file)
@@ -5,22 +5,22 @@ package IkiWiki::Plugin::creole;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "creole", call => \&getsetup);
        hook(type => "htmlize", id => "creole", call => \&htmlize);
-} # }}}
+}
 
-sub getsetup { #{{{
+sub getsetup {
        return
                plugin => {
                        safe => 1,
                        rebuild => 1, # format plugin
                },
-} #}}}
+}
 
-sub htmlize (@) { #{{{
+sub htmlize (@) {
        my %params=@_;
        my $content = $params{content};
 
@@ -32,6 +32,6 @@ sub htmlize (@) { #{{{
        creole_custombarelinks();
 
        return creole_parse($content);
-} # }}}
+}
 
 1
index 92667a1ef458922f6cc8e18a9df6eef4e1002937..417442f34e51d3e089186cd9f6839c001efb3bee 100644 (file)
@@ -3,26 +3,26 @@ package IkiWiki::Plugin::cutpaste;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
 my %savedtext;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "cutpaste", call => \&getsetup);
        hook(type => "preprocess", id => "cut", call => \&preprocess_cut, scan => 1);
        hook(type => "preprocess", id => "copy", call => \&preprocess_copy, scan => 1);
        hook(type => "preprocess", id => "paste", call => \&preprocess_paste);
-} # }}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
                        rebuild => undef,
                },
-} #}}}
+}
 
-sub preprocess_cut (@) { #{{{
+sub preprocess_cut (@) {
        my %params=@_;
 
        foreach my $param (qw{id text}) {
@@ -35,9 +35,9 @@ sub preprocess_cut (@) { #{{{
        $savedtext{$params{page}}->{$params{id}} = $params{text};
 
        return "" if defined wantarray;
-} # }}}
+}
 
-sub preprocess_copy (@) { #{{{
+sub preprocess_copy (@) {
        my %params=@_;
 
        foreach my $param (qw{id text}) {
@@ -51,9 +51,9 @@ sub preprocess_copy (@) { #{{{
 
        return IkiWiki::preprocess($params{page}, $params{destpage}, 
                IkiWiki::filter($params{page}, $params{destpage}, $params{text})) if defined wantarray;
-} # }}}
+}
 
-sub preprocess_paste (@) { #{{{
+sub preprocess_paste (@) {
        my %params=@_;
 
        foreach my $param (qw{id}) {
@@ -71,6 +71,6 @@ sub preprocess_paste (@) { #{{{
 
        return IkiWiki::preprocess($params{page}, $params{destpage}, 
                IkiWiki::filter($params{page}, $params{destpage}, $savedtext{$params{page}}->{$params{id}}));
-} # }}}
+}
 
 1;
index 6c36de0a6b120b6047c44cca610bcd5e80333c29..bb77ce59f267a5c9e541a1f42f88d6eca0eee4f1 100644 (file)
@@ -2,22 +2,22 @@
 # Discordian date support fnord ikiwiki.
 package IkiWiki::Plugin::ddate;
 
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 no warnings;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "ddate", call => \&getsetup);
-} # }}}
+}
 
-sub getsetup { #{{{
+sub getsetup {
        return
                plugin => {
                        safe => 1,
                        rebuild => 1,
                },
-} #}}}
+}
 
-sub IkiWiki::displaytime ($;$) { #{{{
+sub IkiWiki::formattime ($;$) {
        my $time=shift;
        my $format=shift;
        if (! defined $format) {
@@ -36,6 +36,6 @@ sub IkiWiki::displaytime ($;$) { #{{{
        my $dt = DateTime->from_epoch(epoch => $time);
        my $dd = DateTime::Calendar::Discordian->from_object(object => $dt);
        return $dd->strftime($format);
-} #}}}
+}
 
 5
index f5d7837fc4c4712de2b693497ce3dd1b7a977b59..7df6a9ffb42f53960f4881b43b34b86f49f6e798 100644 (file)
@@ -4,25 +4,25 @@ package IkiWiki::Plugin::editdiff;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 use HTML::Entities;
 use IPC::Open2;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "editdiff", call => \&getsetup);
        hook(type => "formbuilder_setup", id => "editdiff",
                call => \&formbuilder_setup);
-} #}}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
                        rebuild => 0,
                },
-} #}}}
+}
 
-sub diff ($$) { #{{{
+sub diff ($$) {
        my $orig=shift;
        my $content=shift;
 
@@ -50,9 +50,9 @@ sub diff ($$) { #{{{
        return "couldn't run diff\n" if $sigpipe;
 
        return "<pre>".encode_entities($ret)."</pre>";
-} #}}}
+}
 
-sub formbuilder_setup { #{{{
+sub formbuilder_setup {
        my %params=@_;
        my $form=$params{form};
 
@@ -72,6 +72,6 @@ sub formbuilder_setup { #{{{
                my $diff = diff(srcfile($pagesources{$page}), $content);
                $form->tmpl_param("page_preview", $diff);
        }
-} #}}}
+}
 
 1
index 30c93df20961090f0bc1e278d0bbf0e5685ecbbb..0068a6b118cd15ca7e110a51fdf81fa1ab313526 100644 (file)
@@ -6,19 +6,19 @@ use strict;
 use IkiWiki;
 use open qw{:utf8 :std};
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "editpage", call => \&getsetup);
        hook(type => "refresh", id => "editpage", call => \&refresh);
         hook(type => "sessioncgi", id => "editpage", call => \&IkiWiki::cgi_editpage);
-} # }}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
                        rebuild => 1,
                },
-} #}}}
+}
 
 sub refresh () {
        if (exists $wikistate{editpage} && exists $wikistate{editpage}{previews}) {
@@ -51,37 +51,10 @@ sub refresh () {
 
 # Back to ikiwiki namespace for the rest, this code is very much
 # internal to ikiwiki even though it's separated into a plugin,
-# and other plugins use the functions below.
+# and other plugins use the function below.
 package IkiWiki;
 
-sub check_canedit ($$$;$) { #{{{
-       my $page=shift;
-       my $q=shift;
-       my $session=shift;
-       my $nonfatal=shift;
-       
-       my $canedit;
-       run_hooks(canedit => sub {
-               return if defined $canedit;
-               my $ret=shift->($page, $q, $session);
-               if (defined $ret) {
-                       if ($ret eq "") {
-                               $canedit=1;
-                       }
-                       elsif (ref $ret eq 'CODE') {
-                               $ret->() unless $nonfatal;
-                               $canedit=0;
-                       }
-                       elsif (defined $ret) {
-                               error($ret) unless $nonfatal;
-                               $canedit=0;
-                       }
-               }
-       });
-       return $canedit;
-} #}}}
-
-sub cgi_editpage ($$) { #{{{
+sub cgi_editpage ($$) {
        my $q=shift;
        my $session=shift;
        
@@ -105,7 +78,6 @@ sub cgi_editpage ($$) { #{{{
                header => 0,
                table => 0,
                template => scalar template_params("editpage.tmpl"),
-               wikiname => $config{wikiname},
        );
        
        decode_form_utf8($form);
@@ -122,7 +94,7 @@ sub cgi_editpage ($$) { #{{{
        my $absolute=($page =~ s#^/+##);
        if (! defined $page || ! length $page ||
            file_pruned($page, $config{srcdir})) {
-               error("bad page name");
+               error(gettext("bad page name"));
        }
 
        my $baseurl = urlto($page, undef, 1);
@@ -340,16 +312,7 @@ sub cgi_editpage ($$) { #{{{
        else {
                # save page
                check_canedit($page, $q, $session);
-       
-               # The session id is stored on the form and checked to
-               # guard against CSRF. But only if the user is logged in,
-               # as anonok can allow anonymous edits.
-               if (defined $session->param("name")) {
-                       my $sid=$q->param('sid');
-                       if (! defined $sid || $sid ne $session->id) {
-                               error(gettext("Your login session has expired."));
-                       }
-               }
+               checksessionexpiry($q, $session, $q->param('sid'));
 
                my $exists=-e "$config{srcdir}/$file";
 
@@ -378,8 +341,17 @@ sub cgi_editpage ($$) { #{{{
                        showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
                        exit;
                }
+                       
+               my $message="";
+               if (defined $form->field('comments') &&
+                   length $form->field('comments')) {
+                       $message=$form->field('comments');
+               }
                
                my $content=$form->field('editcontent');
+               check_content(content => $content, page => $page,
+                       cgi => $q, session => $session,
+                       subject => $message);
                run_hooks(editcontent => sub {
                        $content=shift->(
                                content => $content,
@@ -413,12 +385,6 @@ sub cgi_editpage ($$) { #{{{
                
                my $conflict;
                if ($config{rcs}) {
-                       my $message="";
-                       if (defined $form->field('comments') &&
-                           length $form->field('comments')) {
-                               $message=$form->field('comments');
-                       }
-                       
                        if (! $exists) {
                                rcs_add($file);
                        }
@@ -462,6 +428,6 @@ sub cgi_editpage ($$) { #{{{
        }
 
        exit;
-} #}}}
+}
 
 1
index 846b4e7c832c499a0755860b2c277b282a2b2f1d..0bafc95d06d854b860e566a8c14134119d51db17 100644 (file)
@@ -3,11 +3,11 @@ package IkiWiki::Plugin::edittemplate;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 use HTML::Template;
 use Encode;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "edittemplate",
                call => \&getsetup);
        hook(type => "needsbuild", id => "edittemplate",
@@ -16,17 +16,17 @@ sub import { #{{{
                call => \&preprocess);
        hook(type => "formbuilder", id => "edittemplate",
                call => \&formbuilder);
-} #}}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
                        rebuild => undef,
                },
-} #}}}
+}
 
-sub needsbuild (@) { #{{{
+sub needsbuild (@) {
        my $needsbuild=shift;
 
        foreach my $page (keys %pagestate) {
@@ -40,9 +40,9 @@ sub needsbuild (@) { #{{{
                        }
                }
        }
-} #}}}
+}
 
-sub preprocess (@) { #{{{
+sub preprocess (@) {
         my %params=@_;
 
        return "" if $params{page} ne $params{destpage};
@@ -62,9 +62,9 @@ sub preprocess (@) { #{{{
        return sprintf(gettext("edittemplate %s registered for %s"),
                htmllink($params{page}, $params{destpage}, $link),
                $params{match});
-} # }}}
+}
 
-sub formbuilder (@) { #{{{
+sub formbuilder (@) {
        my %params=@_;
        my $form=$params{form};
 
@@ -103,9 +103,9 @@ sub formbuilder (@) { #{{{
                        }
                }
        }
-} #}}}
+}
 
-sub filltemplate ($$) { #{{{
+sub filltemplate ($$) {
        my $template_page=shift;
        my $page=shift;
 
@@ -136,6 +136,6 @@ sub filltemplate ($$) { #{{{
        $template->param(name => $page);
 
        return $template->output;
-} #}}}
+}
 
 1
index 2a16373921436d78db16be6455fb59081149ecd2..a7d38358fe3ab4177c2cf6a7817f3e037f2c8e4a 100644 (file)
@@ -3,7 +3,7 @@ package IkiWiki::Plugin::embed;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
 my $attribr=qr/[^<>"]+/;
 
@@ -43,35 +43,35 @@ my $safehtml=qr{(
 
 my @embedded;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "embed", call => \&getsetup);
        hook(type => "filter", id => "embed", call => \&filter);
-} # }}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
                        rebuild => undef,
                },
-} #}}}
+}
 
-sub embed ($) { #{{{
+sub embed ($) {
        hook(type => "format", id => "embed", call => \&format) unless @embedded;
        push @embedded, shift;
        return "<div class=\"embed$#embedded\"></div>";
-} #}}}
+}
 
-sub filter (@) { #{{{
+sub filter (@) {
        my %params=@_;
        $params{content} =~ s/$safehtml/embed($1)/eg;
        return $params{content};
-} # }}}
+}
 
-sub format (@) { #{{{
+sub format (@) {
         my %params=@_;
        $params{content} =~ s/<div class="embed(\d+)"><\/div>/$embedded[$1]/eg;
         return $params{content};
-} # }}}
+}
 
 1
index ba6c7d8b9f064577bcfb2c9e2716c7653da1fc72..066f15cf1b2f12ef4a027e5c9842d1b859358f23 100644 (file)
@@ -6,7 +6,7 @@ package IkiWiki::Plugin::external;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 use RPC::XML;
 use RPC::XML::Parser;
 use IPC::Open2;
@@ -14,7 +14,7 @@ use IO::Handle;
 
 my %plugins;
 
-sub import { #{{{
+sub import {
        my $self=shift;
        my $plugin=shift;
        return unless defined $plugin;
@@ -32,17 +32,17 @@ sub import { #{{{
        $RPC::XML::ENCODING="utf-8";
 
        rpc_call($plugins{$plugin}, "import");
-} #}}}
+}
 
-sub rpc_write ($$) { #{{{
+sub rpc_write ($$) {
        my $fh=shift;
        my $string=shift;
 
        $fh->print($string."\n");
        $fh->flush;
-} #}}}
+}
 
-sub rpc_call ($$;@) { #{{{
+sub rpc_call ($$;@) {
        my $plugin=shift;
        my $command=shift;
 
@@ -131,12 +131,12 @@ sub rpc_call ($$;@) { #{{{
        }
 
        return undef;
-} #}}}
+}
 
 package IkiWiki::RPC::XML;
 use Memoize;
 
-sub getvar ($$$) { #{{{
+sub getvar ($$$) {
        my $plugin=shift;
        my $varname="IkiWiki::".shift;
        my $key=shift;
@@ -145,9 +145,9 @@ sub getvar ($$$) { #{{{
        my $ret=$varname->{$key};
        use strict 'refs';
        return $ret;
-} #}}}
+}
 
-sub setvar ($$$;@) { #{{{
+sub setvar ($$$;@) {
        my $plugin=shift;
        my $varname="IkiWiki::".shift;
        my $key=shift;
@@ -157,18 +157,18 @@ sub setvar ($$$;@) { #{{{
        my $ret=$varname->{$key}=$value;
        use strict 'refs';
        return $ret;
-} #}}}
+}
 
-sub getstate ($$$$) { #{{{
+sub getstate ($$$$) {
        my $plugin=shift;
        my $page=shift;
        my $id=shift;
        my $key=shift;
 
        return $IkiWiki::pagestate{$page}{$id}{$key};
-} #}}}
+}
 
-sub setstate ($$$$;@) { #{{{
+sub setstate ($$$$;@) {
        my $plugin=shift;
        my $page=shift;
        my $id=shift;
@@ -176,22 +176,22 @@ sub setstate ($$$$;@) { #{{{
        my $value=shift;
 
        return $IkiWiki::pagestate{$page}{$id}{$key}=$value;
-} #}}}
+}
 
-sub getargv ($) { #{{{
+sub getargv ($) {
        my $plugin=shift;
 
        return \@ARGV;
-} #}}}
+}
 
-sub setargv ($@) { #{{{
+sub setargv ($@) {
        my $plugin=shift;
        my $array=shift;
 
        @ARGV=@$array;
-} #}}}
+}
 
-sub inject ($@) { #{{{
+sub inject ($@) {
        # Bind a given perl function name to a particular RPC request.
        my $plugin=shift;
        my %params=@_;
@@ -202,12 +202,20 @@ sub inject ($@) { #{{{
        my $sub = sub {
                IkiWiki::Plugin::external::rpc_call($plugin, $params{call}, @_)
        };
+       $sub=memoize($sub) if $params{memoize};
+
+       # This will add it to the symbol table even if not present.
+       no warnings;
        eval qq{*$params{name}=\$sub};
-       memoize($params{name}) if $params{memoize};
+       use warnings;
+
+       # This will ensure that everywhere it was exported to sees
+       # the injected version.
+       IkiWiki::inject(name => $params{name}, call => $sub);
        return 1;
-} #}}}
+}
 
-sub hook ($@) { #{{{
+sub hook ($@) {
        # the call parameter is a function name to call, since XML RPC
        # cannot pass a function reference
        my $plugin=shift;
@@ -219,13 +227,13 @@ sub hook ($@) { #{{{
        IkiWiki::hook(%params, call => sub {
                IkiWiki::Plugin::external::rpc_call($plugin, $callback, @_);
        });
-} #}}}
+}
 
-sub pagespec_match ($@) { #{{{
+sub pagespec_match ($@) {
        # convert pagespec_match's return object into a XML RPC boolean
        my $plugin=shift;
 
        return RPC::XML::boolean->new(0 + IkiWiki::pagespec_march(@_));
-} #}}}
+}
 
 1
index e9204dea9fbf1841b386c87b107e33bf0bbfd603..6060914c5a59a6736971f5557d97b3efc992d953 100644 (file)
@@ -5,22 +5,22 @@ package IkiWiki::Plugin::favicon;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "favicon", call => \&getsetup);
        hook(type => "pagetemplate", id => "favicon", call => \&pagetemplate);
-} # }}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
                        rebuild => 1,
                },
-} #}}}
+}
 
-sub pagetemplate (@) { #{{{
+sub pagetemplate (@) {
        my %params=@_;
 
        my $template=$params{template};
@@ -28,6 +28,6 @@ sub pagetemplate (@) { #{{{
        if ($template->query(name => "favicon")) {
                $template->param(favicon => "favicon.ico");
        }
-} # }}}
+}
 
 1
index 27f764e3b344a344ef2d491552824c2c40e6e5f1..8575ee108eece4135c783993b718cfbb1f278c2e 100644 (file)
@@ -3,7 +3,7 @@ package IkiWiki::Plugin::filecheck;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
 my %units=( #{{{       # size in bytes
        B               => 1,
@@ -37,9 +37,9 @@ my %units=( #{{{      # size in bytes
        # ikiwiki, if you find you need larger data quantities, either modify
        # yourself to add them, or travel back in time to 2008 and kill me.
        #   -- Joey
-); #}}}
+);
 
-sub parsesize ($) { #{{{
+sub parsesize ($) {
        my $size=shift;
 
        no warnings;
@@ -51,10 +51,10 @@ sub parsesize ($) { #{{{
                }
        }
        return $base;
-} #}}}
+}
 
 # This is provided for other plugins that want to convert back the other way.
-sub humansize ($) { #{{{
+sub humansize ($) {
        my $size=shift;
 
        foreach my $unit (reverse sort { $units{$a} <=> $units{$b} || $b cmp $a } keys %units) {
@@ -63,11 +63,11 @@ sub humansize ($) { #{{{
                }
        }
        return $size; # near zero, or negative
-} #}}}
+}
 
 package IkiWiki::PageSpec;
 
-sub match_maxsize ($$;@) { #{{{
+sub match_maxsize ($$;@) {
        my $page=shift;
        my $maxsize=eval{IkiWiki::Plugin::filecheck::parsesize(shift)};
        if ($@) {
@@ -86,9 +86,9 @@ sub match_maxsize ($$;@) { #{{{
        else {
                return IkiWiki::SuccessReason->new("file not too large");
        }
-} #}}}
+}
 
-sub match_minsize ($$;@) { #{{{
+sub match_minsize ($$;@) {
        my $page=shift;
        my $minsize=eval{IkiWiki::Plugin::filecheck::parsesize(shift)};
        if ($@) {
@@ -107,9 +107,9 @@ sub match_minsize ($$;@) { #{{{
        else {
                return IkiWiki::SuccessReason->new("file not too small");
        }
-} #}}}
+}
 
-sub match_mimetype ($$;@) { #{{{
+sub match_mimetype ($$;@) {
        my $page=shift;
        my $wanted=shift;
 
@@ -140,9 +140,9 @@ sub match_mimetype ($$;@) { #{{{
        else {
                return IkiWiki::SuccessReason->new("file MIME type is $mimetype");
        }
-} #}}}
+}
 
-sub match_virusfree ($$;@) { #{{{
+sub match_virusfree ($$;@) {
        my $page=shift;
        my $wanted=shift;
 
@@ -182,9 +182,9 @@ sub match_virusfree ($$;@) { #{{{
        else {
                return IkiWiki::SuccessReason->new("file seems virusfree ($reason)");
        }
-} #}}}
+}
 
-sub match_ispage ($$;@) { #{{{
+sub match_ispage ($$;@) {
        my $filename=shift;
 
        if (defined IkiWiki::pagetype($filename)) {
@@ -193,4 +193,4 @@ sub match_ispage ($$;@) { #{{{
        else {
                return IkiWiki::FailReason->new("file is not a wiki page");
        }
-} #}}}
+}
diff --git a/IkiWiki/Plugin/format.pm b/IkiWiki/Plugin/format.pm
new file mode 100644 (file)
index 0000000..bbe3aa9
--- /dev/null
@@ -0,0 +1,30 @@
+#!/usr/bin/perl
+package IkiWiki::Plugin::format;
+
+use warnings;
+use strict;
+use IkiWiki 3.00;
+
+sub import {
+       hook(type => "preprocess", id => "format", call => \&preprocess);
+}
+
+sub preprocess (@) {
+       my $format=$_[0];
+       shift; shift;
+       my $text=$_[0];
+       shift; shift;
+       my %params=@_;
+
+       if (! defined $format || ! defined $text) {
+               error(gettext("must specify format and text"));
+       }
+       elsif (! exists $IkiWiki::hooks{htmlize}{$format}) {
+               error(sprintf(gettext("unsupported page format %s"), $format));
+       }
+
+       return IkiWiki::htmlize($params{page}, $params{destpage}, $format,
+               IkiWiki::preprocess($params{page}, $params{destpage}, $text));
+}
+
+1
index 456b63e9f341f215dba522603c20c8a005fec7a0..17e57dea14fccb4c3f9e1c21a5926ef4880033cd 100644 (file)
@@ -4,22 +4,22 @@ package IkiWiki::Plugin::fortune;
 
 use warnings;
 use strict;
-use IkiWiki 2.00;
+use IkiWiki 3.00;
 
-sub import { #{{{
+sub import {
        hook(type => "getsetup", id => "fortune", call => \&getsetup);
        hook(type => "preprocess", id => "fortune", call => \&preprocess);
-} # }}}
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 1,
                        rebuild => undef,
                },
-} #}}}
+}
 
-sub preprocess (@) { #{{{
+sub preprocess (@) {
        $ENV{PATH}="$ENV{PATH}:/usr/games:/usr/local/games";
        my $f = `fortune 2>/dev/null`;
 
@@ -29,6 +29,6 @@ sub preprocess (@) { #{{{
        else {
                return "<pre>$f</pre>\n";
        }
-} # }}}
+}
 
 1
index 14b0ab2851dc03b561e4c86e9e0c5d660a8ca3a2..68b114a7328332a54d8e66426bd09dc1eec0b030 100644 (file)
@@ -9,8 +9,9 @@ use open qw{:utf8 :std};
 
 my $sha1_pattern     = qr/[0-9a-fA-F]{40}/; # pattern to validate Git sha1sums
 my $dummy_commit_msg = 'dummy commit';      # message to skip in recent changes
+my $no_chdir=0;
 
-sub import { #{{{
+sub import {
        hook(type => "checkconfig", id => "git", call => \&checkconfig);
        hook(type => "getsetup", id => "git", call => \&getsetup);
        hook(type => "rcs", id => "rcs_update", call => \&rcs_update);
@@ -23,24 +24,34 @@ sub import { #{{{
        hook(type => "rcs", id => "rcs_recentchanges", call => \&rcs_recentchanges);
        hook(type => "rcs", id => "rcs_diff", call => \&rcs_diff);
        hook(type => "rcs", id => "rcs_getctime", call => \&rcs_getctime);
-} #}}}
+       hook(type => "rcs", id => "rcs_receive", call => \&rcs_receive);
+}
 
-sub checkconfig () { #{{{
+sub checkconfig () {
        if (! defined $config{gitorigin_branch}) {
                $config{gitorigin_branch}="origin";
        }
        if (! defined $config{gitmaster_branch}) {
                $config{gitmaster_branch}="master";
        }
-       if (defined $config{git_wrapper} && length $config{git_wrapper}) {
+       if (defined $config{git_wrapper} &&
+           length $config{git_wrapper}) {
                push @{$config{wrappers}}, {
                        wrapper => $config{git_wrapper},
                        wrappermode => (defined $config{git_wrappermode} ? $config{git_wrappermode} : "06755"),
                };
        }
-} #}}}
+       if (defined $config{git_test_receive_wrapper} &&
+           length $config{git_test_receive_wrapper}) {
+               push @{$config{wrappers}}, {
+                       test_receive => 1,
+                       wrapper => $config{git_test_receive_wrapper},
+                       wrappermode => (defined $config{git_wrappermode} ? $config{git_wrappermode} : "06755"),
+               };
+       }
+}
 
-sub getsetup () { #{{{
+sub getsetup () {
        return
                plugin => {
                        safe => 0, # rcs plugin
@@ -60,6 +71,20 @@ sub getsetup () { #{{{
                        safe => 0,
                        rebuild => 0,
                },
+               git_test_receive_wrapper => {
+                       type => "string",
+                       example => "/git/wiki.git/hooks/pre-receive",
+                       description => "git pre-receive hook to generate",
+                       safe => 0, # file
+                       rebuild => 0,
+               },
+               untrusted_committers => {
+                       type => "string",
+                       example => [],
+                       description => "unix users whose commits should be checked by the pre-receive hook",
+                       safe => 0,
+                       rebuild => 0,
+               },
                historyurl => {
                        type => "string",
                        example => "http://git.example.com/gitweb.cgi?p=wiki.git;a=history;f=[[file]]",
@@ -69,8 +94,8 @@ sub getsetup () { #{{{
                },
                diffurl => {
                        type => "string",
-                       example => "http://git.example.com/gitweb.cgi?p=wiki.git;a=blobdiff;h=[[sha1_to]];hp=[[sha1_from]];hb=[[sha1_parent]];f=[[file]]",
-                       description => "gitweb url to show a diff ([[sha1_to]], [[sha1_from]], [[sha1_parent]], and [[file]] substituted)",
+                       example => "http://git.example.com/gitweb.cgi?p=wiki.git;a=blobdiff;f=[[file]];h=[[sha1_to]];hp=[[sha1_from]];hb=[[sha1_commit]];hpb=[[sha1_parent]]",
+                       description => "gitweb url to show a diff ([[file]], [[sha1_to]], [[sha1_from]], [[sha1_commit]], and [[sha1_parent]] substituted)",
                        safe => 1,
                        rebuild => 1,
                },
@@ -88,9 +113,9 @@ sub getsetup () { #{{{
                        safe => 0, # paranoia
                        rebuild => 0,
                },
-} #}}}
+}
 
-sub safe_git (&@) { #{{{
+sub safe_git (&@) {
        # Start a child process safely without resorting /bin/sh.
        # Return command output or success state (in scalar context).
 
@@ -103,15 +128,25 @@ sub safe_git (&@) { #{{{
        if (!$pid) {
                # In child.
                # Git commands want to be in wc.
-               chdir $config{srcdir}
-                   or error("Cannot chdir to $config{srcdir}: $!");
+               if (! $no_chdir) {
+                       chdir $config{srcdir}
+                           or error("Cannot chdir to $config{srcdir}: $!");
+               }
                exec @cmdline or error("Cannot exec '@cmdline': $!");
        }
        # In parent.
 
+       # git output is probably utf-8 encoded, but may contain
+       # other encodings or invalidly encoded stuff. So do not rely
+       # on the normal utf-8 IO layer, decode it by hand.
+       binmode($OUT);
+
        my @lines;
        while (<$OUT>) {
+               $_=decode_utf8($_, 0);
+
                chomp;
+
                push @lines, $_;
        }
 
@@ -125,9 +160,9 @@ sub safe_git (&@) { #{{{
 sub run_or_die ($@) { safe_git(\&error, @_) }
 sub run_or_cry ($@) { safe_git(sub { warn @_ },  @_) }
 sub run_or_non ($@) { safe_git(undef,            @_) }
-#}}}
 
-sub merge_past ($$$) { #{{{
+
+sub merge_past ($$$) {
        # Unlike with Subversion, Git cannot make a 'svn merge -rN:M file'.
        # Git merge commands work with the committed changes, except in the
        # implicit case of '-m' of git checkout(1).  So we should invent a
@@ -219,9 +254,9 @@ sub merge_past ($$$) { #{{{
        error("Git merge failed!\n$failure\n") if $failure;
 
        return $conflict;
-} #}}}
+}
 
-sub parse_diff_tree ($@) { #{{{
+sub parse_diff_tree ($@) {
        # Parse the raw diff tree chunk and return the info hash.
        # See git-diff-tree(1) for the syntax.
 
@@ -320,6 +355,9 @@ sub parse_diff_tree ($@) { #{{{
                                        'file'      => decode("utf8", $file),
                                        'sha1_from' => $sha1_from[0],
                                        'sha1_to'   => $sha1_to,
+                                       'mode_from' => $mode_from[0],
+                                       'mode_to'   => $mode_to,
+                                       'status'    => $status,
                                };
                        }
                        next;
@@ -328,17 +366,17 @@ sub parse_diff_tree ($@) { #{{{
        }
 
        return \%ci;
-} #}}}
+}
 
-sub git_commit_info ($;$) { #{{{
-       # Return an array of commit info hashes of num commits (default: 1)
+sub git_commit_info ($;$) {
+       # Return an array of commit info hashes of num commits
        # starting from the given sha1sum.
-
        my ($sha1, $num) = @_;
 
-       $num ||= 1;
+       my @opts;
+       push @opts, "--max-count=$num" if defined $num;
 
-       my @raw_lines = run_or_die('git', 'log', "--max-count=$num", 
+       my @raw_lines = run_or_die('git', 'log', @opts,
                '--pretty=raw', '--raw', '--abbrev=40', '--always', '-c',
                '-r', $sha1, '--', '.');
        my ($prefix) = run_or_die('git', 'rev-parse', '--show-prefix');
@@ -351,11 +389,10 @@ sub git_commit_info ($;$) { #{{{
        warn "Cannot parse commit info for '$sha1' commit" if !@ci;
 
        return wantarray ? @ci : $ci[0];
-} #}}}
+}
 
-sub git_sha1 (;$) { #{{{
+sub git_sha1 (;$) {
        # Return head sha1sum (of given file).
-
        my $file = shift || q{--};
 
        # Ignore error since a non-existing file might be given.
@@ -365,26 +402,25 @@ sub git_sha1 (;$) { #{{{
                ($sha1) = $sha1 =~ m/($sha1_pattern)/; # sha1 is untainted now
        } else { debug("Empty sha1sum for '$file'.") }
        return defined $sha1 ? $sha1 : q{};
-} #}}}
+}
 
-sub rcs_update () { #{{{
+sub rcs_update () {
        # Update working directory.
 
        if (length $config{gitorigin_branch}) {
                run_or_cry('git', 'pull', $config{gitorigin_branch});
        }
-} #}}}
+}
 
-sub rcs_prepedit ($) { #{{{
+sub rcs_prepedit ($) {
        # Return the commit sha1sum of the file when editing begins.
        # This will be later used in rcs_commit if a merge is required.
-
        my ($file) = @_;
 
        return git_sha1($file);
-} #}}}
+}
 
-sub rcs_commit ($$$;$$) { #{{{
+sub rcs_commit ($$$;$$) {
        # Try to commit the page; returns undef on _success_ and
        # a version of the page with the rcs's conflict markers on
        # failure.
@@ -403,7 +439,7 @@ sub rcs_commit ($$$;$$) { #{{{
 
        rcs_add($file); 
        return rcs_commit_staged($message, $user, $ipaddr);
-} #}}}
+}
 
 sub rcs_commit_staged ($$$) {
        # Commits all staged changes. Changes can be staged using rcs_add,
@@ -413,7 +449,7 @@ sub rcs_commit_staged ($$$) {
        # Set the commit author and email to the web committer.
        my %env=%ENV;
        if (defined $user || defined $ipaddr) {
-               my $u=defined $user ? $user : $ipaddr;
+               my $u=encode_utf8(defined $user ? $user : $ipaddr);
                $ENV{GIT_AUTHOR_NAME}=$u;
                $ENV{GIT_AUTHOR_EMAIL}="$u\@web";
        }
@@ -444,29 +480,29 @@ sub rcs_commit_staged ($$$) {
        return undef; # success
 }
 
-sub rcs_add ($) { # {{{
+sub rcs_add ($) {
        # Add file to archive.
 
        my ($file) = @_;
 
        run_or_cry('git', 'add', $file);
-} #}}}
+}
 
-sub rcs_remove ($) { # {{{
+sub rcs_remove ($) {
        # Remove file from archive.
 
        my ($file) = @_;
 
        run_or_cry('git', 'rm', '-f', $file);
-} #}}}
+}
 
-sub rcs_rename ($$) { # {{{
+sub rcs_rename ($$) {
        my ($src, $dest) = @_;
 
        run_or_cry('git', 'mv', '-f', $src, $dest);
-} #}}}
+}
 
-sub rcs_recentchanges ($) { #{{{
+sub rcs_recentchanges ($) {
        # List of recent changes.
 
        my ($num) = @_;
@@ -475,7 +511,7 @@ sub rcs_recentchanges ($) { #{{{
        error($@) if $@;
 
        my @rets;
-       foreach my $ci (git_commit_info('HEAD', $num)) {
+       foreach my $ci (git_commit_info('HEAD', $num || 1)) {
                # Skip redundant commits.
                next if ($ci->{'comment'} && @{$ci->{'comment'}}[0] eq $dummy_commit_msg);
 
@@ -493,6 +529,7 @@ sub rcs_recentchanges ($) { #{{{
                        $diffurl =~ s/\[\[sha1_parent\]\]/$ci->{'parent'}/go;
                        $diffurl =~ s/\[\[sha1_from\]\]/$detail->{'sha1_from'}/go;
                        $diffurl =~ s/\[\[sha1_to\]\]/$detail->{'sha1_to'}/go;
+                       $diffurl =~ s/\[\[sha1_commit\]\]/$sha1/go;
 
                        push @pages, {
                                page => pagename($file),
@@ -533,9 +570,9 @@ sub rcs_recentchanges ($) { #{{{
        }
 
        return @rets;
-} #}}}
+}
 
-sub rcs_diff ($) { #{{{
+sub rcs_diff ($) {
        my $rev=shift;
        my ($sha1) = $rev =~ /^($sha1_pattern)$/; # untaint
        my @lines;
@@ -550,19 +587,112 @@ sub rcs_diff ($) { #{{{
        else {
                return join("", @lines);
        }
-} #}}}
+}
 
-sub rcs_getctime ($) { #{{{
+sub rcs_getctime ($) {
        my $file=shift;
        # Remove srcdir prefix
        $file =~ s/^\Q$config{srcdir}\E\/?//;
 
-       my $sha1  = git_sha1($file);
-       my $ci    = git_commit_info($sha1);
+       my @sha1s = run_or_non('git', 'rev-list', 'HEAD', '--', $file);
+       my $ci    = git_commit_info($sha1s[$#sha1s], 1);
        my $ctime = $ci->{'author_epoch'};
        debug("ctime for '$file': ". localtime($ctime));
 
        return $ctime;
-} #}}}
+}
+
+sub rcs_receive () {
+       # The wiki may not be the only thing in the git repo.
+       # Determine if it is in a subdirectory by examining the srcdir,
+       # and its parents, looking for the .git directory.
+       my $subdir="";
+       my $dir=$config{srcdir};
+       while (! -d "$dir/.git") {
+               $subdir=IkiWiki::basename($dir)."/".$subdir;
+               $dir=IkiWiki::dirname($dir);
+               if (! length $dir) {
+                       error("cannot determine root of git repo");
+               }
+       }
+
+       my @rets;
+       while (<>) {
+               chomp;
+               my ($oldrev, $newrev, $refname) = split(' ', $_, 3);
+               
+               # only allow changes to gitmaster_branch
+               if ($refname !~ /^refs\/heads\/\Q$config{gitmaster_branch}\E$/) {
+                       error sprintf(gettext("you are not allowed to change %s"), $refname);
+               }
+               
+               # Avoid chdir when running git here, because the changes
+               # are in the master git repo, not the srcdir repo.
+               # The pre-recieve hook already puts us in the right place.
+               $no_chdir=1;
+               my @changes=git_commit_info($oldrev."..".$newrev);
+               $no_chdir=0;
+
+               foreach my $ci (@changes) {
+                       foreach my $detail (@{ $ci->{'details'} }) {
+                               my $file = $detail->{'file'};
+
+                               # check that all changed files are in the
+                               # subdir
+                               if (length $subdir &&
+                                   ! ($file =~ s/^\Q$subdir\E//)) {
+                                       error sprintf(gettext("you are not allowed to change %s"), $file);
+                               }
+
+                               my ($action, $mode, $path);
+                               if ($detail->{'status'} =~ /^[M]+\d*$/) {
+                                       $action="change";
+                                       $mode=$detail->{'mode_to'};
+                               }
+                               elsif ($detail->{'status'} =~ /^[AM]+\d*$/) {
+                                       $action="add";
+                                       $mode=$detail->{'mode_to'};
+                               }
+                               elsif ($detail->{'status'} =~ /^[DAM]+\d*/) {
+                                       $action="remove";
+                                       $mode=$detail->{'mode_from'};
+                               }
+                               else {
+                                       error "unknown status ".$detail->{'status'};
+                               }
+                               
+                               # test that the file mode is ok
+                               if ($mode !~ /^100[64][64][64]$/) {
+                                       error sprintf(gettext("you cannot act on a file with mode %s"), $mode);
+                               }
+                               if ($action eq "change") {
+                                       if ($detail->{'mode_from'} ne $detail->{'mode_to'}) {
+                                               error gettext("you are not allowed to change file modes");
+                                       }
+                               }
+                               
+                               # extract attachment to temp file
+                               if (($action eq 'add' || $action eq 'change') &&
+                                    ! pagetype($file)) {
+                                       eval q{use File::Temp};
+                                       die $@ if $@;
+                                       my $fh;
+                                       ($fh, $path)=File::Temp::tempfile("XXXXXXXXXX", UNLINK => 1);
+                                       if (system("git show ".$detail->{sha1_to}." > '$path'") != 0) {
+                                               error("failed writing temp file");
+                                       }
+                               }
+
+                               push @rets, {
+                                       file => $file,
+                                       action => $action,
+                                       path => $path,